You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by pt...@apache.org on 2017/07/04 11:51:34 UTC

[2/3] ignite git commit: IGNITE-5532 .NET: Split CacheLinqTest into partial classes

http://git-wip-us.apache.org/repos/asf/ignite/blob/84c7427a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/CacheLinqTestSimpleName.cs
----------------------------------------------------------------------
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/CacheLinqTestSimpleName.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/CacheLinqTestSimpleName.cs
deleted file mode 100644
index 9743e60..0000000
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/CacheLinqTestSimpleName.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-namespace Apache.Ignite.Core.Tests.Cache.Query
-{
-    using Apache.Ignite.Core.Binary;
-    using NUnit.Framework;
-
-    /// <summary>
-    /// LINQ test with simple name mapper.
-    /// </summary>
-    [TestFixture]
-    public class CacheLinqTestSimpleName : CacheLinqTest
-    {
-        /** <inheritdoc /> */
-        protected override IBinaryNameMapper GetNameMapper()
-        {
-            return BinaryBasicNameMapper.SimpleNameInstance;
-        }
-    }
-}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ignite/blob/84c7427a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/CacheLinqTestSqlEscapeAll.cs
----------------------------------------------------------------------
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/CacheLinqTestSqlEscapeAll.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/CacheLinqTestSqlEscapeAll.cs
deleted file mode 100644
index 7473be9..0000000
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/CacheLinqTestSqlEscapeAll.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-namespace Apache.Ignite.Core.Tests.Cache.Query
-{
-    using NUnit.Framework;
-
-    /// <summary>
-    /// LINQ test with simple name mapper.
-    /// </summary>
-    [TestFixture]
-    public class CacheLinqTestSqlEscapeAll : CacheLinqTest
-    {
-        /** <inheritdoc /> */
-        protected override bool GetSqlEscapeAll()
-        {
-            return true;
-        }
-    }
-}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ignite/blob/84c7427a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Aggregates.cs
----------------------------------------------------------------------
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Aggregates.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Aggregates.cs
new file mode 100644
index 0000000..b6bbb67
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Aggregates.cs
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// ReSharper disable SuspiciousTypeConversion.Global
+// ReSharper disable MemberCanBePrivate.Global
+// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
+// ReSharper disable UnusedAutoPropertyAccessor.Global
+// ReSharper disable StringIndexOfIsCultureSpecific.1
+// ReSharper disable StringIndexOfIsCultureSpecific.2
+// ReSharper disable StringCompareToIsCultureSpecific
+// ReSharper disable StringCompareIsCultureSpecific.1
+// ReSharper disable UnusedMemberInSuper.Global
+namespace Apache.Ignite.Core.Tests.Cache.Query.Linq
+{
+    using System;
+    using System.Linq;
+    using Apache.Ignite.Linq;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests LINQ.
+    /// </summary>
+    public partial class CacheLinqTest
+    {
+        /// <summary>
+        /// Tests aggregates.
+        /// </summary>
+        [Test]
+        public void TestAggregates()
+        {
+            var cache = GetPersonCache().AsCacheQueryable();
+
+            Assert.AreEqual(PersonCount, cache.Count());
+            Assert.AreEqual(PersonCount, cache.Select(x => x.Key).Count());
+            Assert.AreEqual(2, cache.Select(x => x.Value.OrganizationId).Distinct().Count());
+
+            // ReSharper disable once ReturnValueOfPureMethodIsNotUsed
+            Assert.Throws<NotSupportedException>(() => cache.Select(x => new { x.Key, x.Value }).Count());
+
+            // Min/max/sum/avg
+            var ints = cache.Select(x => x.Key);
+            Assert.AreEqual(0, ints.Min());
+            Assert.AreEqual(PersonCount - 1, ints.Max());
+            Assert.AreEqual(ints.ToArray().Sum(), ints.Sum());
+            Assert.AreEqual((int)ints.ToArray().Average(), (int)ints.Average());
+
+            var dupInts = ints.Select(x => x / 10);  // duplicate values
+            CollectionAssert.AreEquivalent(dupInts.ToArray().Distinct().ToArray(), dupInts.Distinct().ToArray());
+            Assert.AreEqual(dupInts.ToArray().Distinct().Sum(), dupInts.Distinct().Sum());
+
+            // All/any
+            Assert.IsFalse(ints.Where(x => x > -5).Any(x => x > PersonCount && x > 0));
+            Assert.IsTrue(ints.Any(x => x < PersonCount / 2));
+
+            // Skip/take
+            var keys = cache.Select(x => x.Key).OrderBy(x => x);
+            Assert.AreEqual(new[] { 0, 1 }, keys.Take(2).ToArray());
+            Assert.AreEqual(new[] { 1, 2 }, keys.Skip(1).Take(2).ToArray());
+            Assert.AreEqual(new[] { PersonCount - 2, PersonCount - 1 }, keys.Skip(PersonCount - 2).ToArray());
+        }
+
+        /// <summary>
+        /// Tests aggregates with all clause.
+        /// </summary>
+        [Test]
+        public void TestAggregatesAll()
+        {
+            var ints = GetPersonCache().AsCacheQueryable().Select(x => x.Key);
+
+            // ReSharper disable once ReturnValueOfPureMethodIsNotUsed
+            var ex = Assert.Throws<NotSupportedException>(() => ints.Where(x => x > -10)
+                .All(x => x < PersonCount && x >= 0));
+
+            Assert.IsTrue(ex.Message.StartsWith("Operator is not supported: All"));
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ignite/blob/84c7427a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Base.cs
----------------------------------------------------------------------
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Base.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Base.cs
new file mode 100644
index 0000000..ae9fd5f
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Base.cs
@@ -0,0 +1,506 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// ReSharper disable SuspiciousTypeConversion.Global
+// ReSharper disable MemberCanBePrivate.Global
+// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
+// ReSharper disable UnusedAutoPropertyAccessor.Global
+// ReSharper disable StringIndexOfIsCultureSpecific.1
+// ReSharper disable StringIndexOfIsCultureSpecific.2
+// ReSharper disable StringCompareToIsCultureSpecific
+// ReSharper disable StringCompareIsCultureSpecific.1
+// ReSharper disable UnusedMemberInSuper.Global
+namespace Apache.Ignite.Core.Tests.Cache.Query.Linq
+{
+    using System;
+    using System.Collections;
+    using System.Linq;
+    using System.Linq.Expressions;
+    using System.Threading;
+    using Apache.Ignite.Core.Binary;
+    using Apache.Ignite.Core.Cache;
+    using Apache.Ignite.Core.Cache.Configuration;
+    using Apache.Ignite.Linq;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests LINQ.
+    /// </summary>
+    public partial class CacheLinqTest
+    {
+        /** Cache name. */
+        private const string PersonOrgCacheName = "person_org";
+
+        /** Cache name. */
+        private const string PersonSecondCacheName = "person_cache";
+
+        /** Role cache name. */
+        private const string RoleCacheName = "role_cache";
+
+        /** */
+        private const int RoleCount = 3;
+
+        /** */
+        private const int PersonCount = 900;
+
+        /** */
+        private bool _runDbConsole;
+
+        /** */
+        private static readonly DateTime StartDateTime = new DateTime(2000, 5, 17, 15, 4, 5, DateTimeKind.Utc);
+
+        /// <summary>
+        /// Fixture set up.
+        /// </summary>
+        [TestFixtureSetUp]
+        public void FixtureSetUp()
+        {
+            _runDbConsole = false;  // set to true to open H2 console
+
+            if (_runDbConsole)
+                Environment.SetEnvironmentVariable("IGNITE_H2_DEBUG_CONSOLE", "true");
+
+            Ignition.Start(GetConfig());
+            Ignition.Start(GetConfig("grid2"));
+
+            // Populate caches
+            var cache = GetPersonCache();
+            var personCache = GetSecondPersonCache();
+
+            for (var i = 0; i < PersonCount; i++)
+            {
+                cache.Put(i, new Person(i, string.Format(" Person_{0}  ", i))
+                {
+                    Address = new Address {Zip = i, Street = "Street " + i, AliasTest = i},
+                    OrganizationId = i%2 + 1000,
+                    Birthday = StartDateTime.AddYears(i),
+                    AliasTest = -i
+                });
+
+                var i2 = i + PersonCount;
+                personCache.Put(i2, new Person(i2, "Person_" + i2)
+                {
+                    Address = new Address {Zip = i2, Street = "Street " + i2},
+                    OrganizationId = i%2 + 1000,
+                    Birthday = StartDateTime.AddYears(i)
+                });
+            }
+
+            var orgCache = GetOrgCache();
+
+            orgCache[1000] = new Organization {Id = 1000, Name = "Org_0"};
+            orgCache[1001] = new Organization {Id = 1001, Name = "Org_1"};
+            orgCache[1002] = new Organization {Id = 1002, Name = null};
+
+            var roleCache = GetRoleCache();
+
+            roleCache[new RoleKey(1, 101)] = new Role {Name = "Role_1", Date = StartDateTime};
+            roleCache[new RoleKey(2, 102)] = new Role {Name = "Role_2", Date = StartDateTime.AddYears(1)};
+            roleCache[new RoleKey(3, 103)] = new Role {Name = null, Date = StartDateTime.AddHours(5432)};
+        }
+
+        /// <summary>
+        /// Gets the configuration.
+        /// </summary>
+        private IgniteConfiguration GetConfig(string gridName = null)
+        {
+            return new IgniteConfiguration(TestUtils.GetTestConfiguration())
+            {
+                BinaryConfiguration = new BinaryConfiguration(typeof(Person),
+                    typeof(Organization), typeof(Address), typeof(Role), typeof(RoleKey), typeof(Numerics))
+                {
+                    NameMapper = GetNameMapper()
+                },
+                IgniteInstanceName = gridName
+            };
+        }
+
+        /// <summary>
+        /// Gets the name mapper.
+        /// </summary>
+        protected virtual IBinaryNameMapper GetNameMapper()
+        {
+            return BinaryBasicNameMapper.FullNameInstance;
+        }
+
+        /// <summary>
+        /// Gets the SqlEscapeAll setting.
+        /// </summary>
+        protected virtual bool GetSqlEscapeAll()
+        {
+            return false;
+        }
+
+        /// <summary>
+        /// Fixture tear down.
+        /// </summary>
+        [TestFixtureTearDown]
+        public void FixtureTearDown()
+        {
+            if (_runDbConsole)
+                Thread.Sleep(Timeout.Infinite);
+            Ignition.StopAll(true);
+        }
+
+        /// <summary>
+        /// Gets the person cache.
+        /// </summary>
+        /// <returns></returns>
+        private ICache<int, Person> GetPersonCache()
+        {
+            return GetCacheOf<Person>();
+        }
+
+        /// <summary>
+        /// Gets the org cache.
+        /// </summary>
+        /// <returns></returns>
+        private ICache<int, Organization> GetOrgCache()
+        {
+            return GetCacheOf<Organization>();
+        }
+
+        /// <summary>
+        /// Gets the cache.
+        /// </summary>
+        private ICache<int, T> GetCacheOf<T>()
+        {
+            return Ignition.GetIgnite()
+                .GetOrCreateCache<int, T>(new CacheConfiguration(PersonOrgCacheName,
+                    new QueryEntity(typeof (int), typeof (Person))
+                    {
+                        Aliases = new[]
+                        {
+                            new QueryAlias("AliasTest", "Person_AliasTest"),
+                            new QueryAlias("Address.AliasTest", "Addr_AliasTest")
+                        },
+                        KeyFieldName = "MyKey",
+                        ValueFieldName = "MyValue",
+                        Fields =
+                        {
+                            new QueryField("MyKey", typeof(int)),
+                            new QueryField("MyValue", typeof(T)),
+                        }
+                    },
+                    new QueryEntity(typeof (int), typeof (Organization)))
+                {
+                    CacheMode = CacheMode.Replicated,
+                    SqlEscapeAll = GetSqlEscapeAll()
+                });
+        }
+
+        /// <summary>
+        /// Gets the role cache.
+        /// </summary>
+        private ICache<RoleKey, Role> GetRoleCache()
+        {
+            return Ignition.GetIgnite()
+                .GetOrCreateCache<RoleKey, Role>(new CacheConfiguration(RoleCacheName,
+                    new QueryEntity(typeof (RoleKey), typeof (Role)))
+                {
+                    CacheMode = CacheMode.Replicated,
+                    SqlEscapeAll = GetSqlEscapeAll()
+                });
+        }
+
+        /// <summary>
+        /// Gets the second person cache.
+        /// </summary>
+        private ICache<int, Person> GetSecondPersonCache()
+        {
+            return Ignition.GetIgnite()
+                .GetOrCreateCache<int, Person>(
+                    new CacheConfiguration(PersonSecondCacheName,
+                        new QueryEntity(typeof(int), typeof(Person))
+                        {
+                            TableName = "CustomPersons"
+                        })
+                    {
+                        CacheMode = CacheMode.Replicated,
+                        SqlEscapeAll = GetSqlEscapeAll()
+                    });
+        }
+
+        /// <summary>
+        /// Checks that function maps to SQL function properly.
+        /// </summary>
+        private static void CheckFunc<T, TR>(Expression<Func<T, TR>> exp, IQueryable<T> query, 
+            Func<TR, TR> localResultFunc = null)
+        {
+            localResultFunc = localResultFunc ?? (x => x);
+
+            // Calculate result locally, using real method invocation
+            var expected = query.ToArray().AsQueryable().Select(exp).Select(localResultFunc).OrderBy(x => x).ToArray();
+
+            // Perform SQL query
+            var actual = query.Select(exp).ToArray().OrderBy(x => x).ToArray();
+
+            // Compare results
+            CollectionAssert.AreEqual(expected, actual, new NumericComparer());
+
+            // Perform intermediate anonymous type conversion to check type projection
+            actual = query.Select(exp).Select(x => new {Foo = x}).ToArray().Select(x => x.Foo)
+                .OrderBy(x => x).ToArray();
+
+            // Compare results
+            CollectionAssert.AreEqual(expected, actual, new NumericComparer());
+        }
+
+        /// <summary>
+        /// Checks that function used in Where Clause maps to SQL function properly
+        /// </summary>
+        private static void CheckWhereFunc<TKey, TEntry>(IQueryable<ICacheEntry<TKey,TEntry>> query, 
+            Expression<Func<ICacheEntry<TKey, TEntry>,bool>> whereExpression)
+        {
+            // Calculate result locally, using real method invocation
+            var expected = query
+                .ToArray()
+                .AsQueryable()
+                .Where(whereExpression)
+                .Select(entry => entry.Key)
+                .OrderBy(x => x)
+                .ToArray();
+
+            // Perform SQL query
+            var actual = query
+                .Where(whereExpression)
+                .Select(entry => entry.Key)
+                .ToArray()
+                .OrderBy(x => x)
+                .ToArray();
+
+            // Compare results
+            CollectionAssert.AreEqual(expected, actual, new NumericComparer());
+        }
+
+        /// <summary>
+        /// Tests conditinal statement
+        /// </summary>
+        private void TestConditional<T>(T even , T odd, Func<T,T,bool> comparer = null)
+        {
+            var persons = GetPersonCache().AsCacheQueryable();
+
+            var res = persons
+                .Select(x => new {Foo = x.Key % 2 == 0 ? even : odd, x.Value})
+                .ToArray();
+
+            if (comparer != null)
+            {
+                Assert.IsTrue(comparer(even, res[0].Foo));
+                Assert.IsTrue(comparer(odd, res[1].Foo));
+            }
+            else
+            {
+                Assert.AreEqual(even, res[0].Foo);
+                Assert.AreEqual(odd, res[1].Foo);
+            }
+        }
+
+        /// <summary>
+        /// Tests conditinal statement for structs with default and null values
+        /// </summary>
+        private void TestConditionalWithNullableStructs<T>(T? defaultFalue = null) where T : struct
+        {
+            var def = defaultFalue ?? default(T);
+            TestConditional(def, (T?) null);
+        }
+
+        public interface IPerson
+        {
+            int Age { get; set; }
+            string Name { get; set; }
+        }
+
+        public class Person : IBinarizable, IPerson
+        {
+            public Person(int age, string name)
+            {
+                Age = age;
+                Name = name;
+            }
+
+            [QuerySqlField(Name = "age1")] public int Age { get; set; }
+
+            [QuerySqlField] public string Name { get; set; }
+
+            [QuerySqlField] public Address Address { get; set; }
+
+            [QuerySqlField] public int OrganizationId { get; set; }
+
+            [QuerySqlField] public DateTime? Birthday { get; set; }
+
+            [QuerySqlField] public int AliasTest { get; set; }
+
+            public void WriteBinary(IBinaryWriter writer)
+            {
+                writer.WriteInt("age1", Age);
+                writer.WriteString("name", Name);
+                writer.WriteInt("OrganizationId", OrganizationId);
+                writer.WriteObject("Address", Address);
+                writer.WriteTimestamp("Birthday", Birthday);
+                writer.WriteInt("AliasTest", AliasTest);
+            }
+
+            public void ReadBinary(IBinaryReader reader)
+            {
+                Age = reader.ReadInt("age1");
+                Name = reader.ReadString("name");
+                OrganizationId = reader.ReadInt("OrganizationId");
+                Address = reader.ReadObject<Address>("Address");
+                Birthday = reader.ReadTimestamp("Birthday");
+                AliasTest = reader.ReadInt("AliasTest");
+            }
+        }
+
+        public class Address
+        {
+            [QuerySqlField] public int Zip { get; set; }
+            [QuerySqlField] public string Street { get; set; }
+            [QuerySqlField] public int AliasTest { get; set; }
+        }
+
+        public class Organization
+        {
+            [QuerySqlField] public int Id { get; set; }
+            [QuerySqlField] public string Name { get; set; }
+        }
+
+        public interface IRole
+        {
+            string Name { get; }
+            DateTime Date { get; }
+        }
+
+        public class Role : IRole
+        {
+            [QuerySqlField] public string Name { get; set; }
+            [QuerySqlField] public DateTime Date { get; set; }
+        }
+
+        public class Numerics
+        {
+            public Numerics(double val)
+            {
+                Double = val;
+                Float = (float) val;
+                Decimal = (decimal) val;
+                Int = (int) val;
+                Uint = (uint) val;
+                Long = (long) val;
+                Ulong = (ulong) val;
+                Short = (short) val;
+                Ushort = (ushort) val;
+                Byte = (byte) val;
+                Sbyte =  (sbyte) val;
+            }
+
+            [QuerySqlField] public double Double { get; set; }
+            [QuerySqlField] public float Float { get; set; }
+            [QuerySqlField] public decimal Decimal { get; set; }
+            [QuerySqlField] public int Int { get; set; }
+            [QuerySqlField] public uint Uint { get; set; }
+            [QuerySqlField] public long Long { get; set; }
+            [QuerySqlField] public ulong Ulong { get; set; }
+            [QuerySqlField] public short Short { get; set; }
+            [QuerySqlField] public ushort Ushort { get; set; }
+            [QuerySqlField] public byte Byte { get; set; }
+            [QuerySqlField] public sbyte Sbyte { get; set; }
+        }
+
+        public struct RoleKey : IEquatable<RoleKey>
+        {
+            private readonly int _foo;
+            private readonly long _bar;
+
+            public RoleKey(int foo, long bar)
+            {
+                _foo = foo;
+                _bar = bar;
+            }
+
+            [QuerySqlField(Name = "_foo")]
+            public int Foo
+            {
+                get { return _foo; }
+            }
+
+            [QuerySqlField(Name = "_bar")]
+            public long Bar
+            {
+                get { return _bar; }
+            }
+
+            public bool Equals(RoleKey other)
+            {
+                return _foo == other._foo && _bar == other._bar;
+            }
+
+            public override bool Equals(object obj)
+            {
+                if (ReferenceEquals(null, obj)) return false;
+                return obj is RoleKey && Equals((RoleKey) obj);
+            }
+
+            public override int GetHashCode()
+            {
+                unchecked
+                {
+                    return (_foo*397) ^ _bar.GetHashCode();
+                }
+            }
+
+            public static bool operator ==(RoleKey left, RoleKey right)
+            {
+                return left.Equals(right);
+            }
+
+            public static bool operator !=(RoleKey left, RoleKey right)
+            {
+                return !left.Equals(right);
+            }
+        }
+
+        /// <summary>
+        /// Epsilon comparer.
+        /// </summary>
+        private class NumericComparer : IComparer
+        {
+            /** <inheritdoc /> */
+            public int Compare(object x, object y)
+            {
+                if (Equals(x, y))
+                    return 0;
+
+                if (x is double)
+                {
+                    var dx = (double) x;
+                    var dy = (double) y;
+
+                    // Epsilon is proportional to the min value, but not too small.
+                    const double epsilon = 2E-10d;
+                    var min = Math.Min(Math.Abs(dx), Math.Abs(dy));
+                    var relEpsilon = Math.Max(min*epsilon, epsilon);
+
+                    // Compare with epsilon because some funcs return slightly different results.
+                    return Math.Abs((double) x - (double) y) < relEpsilon ? 0 : 1;
+                }
+
+                return ((IComparable) x).CompareTo(y);
+            }
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/84c7427a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.CompiledQuery.cs
----------------------------------------------------------------------
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.CompiledQuery.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.CompiledQuery.cs
new file mode 100644
index 0000000..1cd377e
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.CompiledQuery.cs
@@ -0,0 +1,215 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// ReSharper disable SuspiciousTypeConversion.Global
+// ReSharper disable MemberCanBePrivate.Global
+// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
+// ReSharper disable UnusedAutoPropertyAccessor.Global
+// ReSharper disable StringIndexOfIsCultureSpecific.1
+// ReSharper disable StringIndexOfIsCultureSpecific.2
+// ReSharper disable StringCompareToIsCultureSpecific
+// ReSharper disable StringCompareIsCultureSpecific.1
+// ReSharper disable UnusedMemberInSuper.Global
+namespace Apache.Ignite.Core.Tests.Cache.Query.Linq
+{
+    using System.Linq;
+    using Apache.Ignite.Linq;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests LINQ.
+    /// </summary>
+    public partial class CacheLinqTest
+    {
+        /// <summary>
+        /// Tests the compiled query with various constructs.
+        /// </summary>
+        [Test]
+        public void TestCompiledQuery()
+        {
+            var persons = GetPersonCache().AsCacheQueryable();
+            var roles = GetRoleCache().AsCacheQueryable();
+
+            // Embedded args
+            var qry0 = CompiledQuery.Compile(() => persons.Where(x => x.Key < 3 && x.Value.Name.Contains("son")));
+            Assert.AreEqual(3, qry0().Count());
+
+            // Lambda args
+            var qry1 = CompiledQuery.Compile((int minKey, int take, int skip) => persons.Where(x => x.Key > minKey)
+                .Take(take).Skip(skip));
+            Assert.AreEqual(3, qry1(-1, 3, 1).GetAll().Count);
+
+            qry1 = CompiledQuery.Compile((int skip, int take, int minKey) => persons.Where(x => x.Key > minKey)
+                .Take(take).Skip(skip));
+
+            Assert.AreEqual(5, qry1(2, 5, 20).GetAll().Count);
+
+            // Mixed args
+            var qry2 = CompiledQuery.Compile((int maxKey, int minKey) =>
+                persons.Where(x => x.Key < maxKey
+                                   && x.Value.Name.Contains("er")
+                                   && x.Value.Age < maxKey
+                                   && x.Key > minKey).Skip(2));
+
+            Assert.AreEqual(6, qry2(10, 1).Count());
+
+            // Join
+            var qry3 = CompiledQuery.Compile(() =>
+                roles.Join(persons, r => r.Key.Foo, p => p.Key, (r, p) => r.Value.Name));
+
+            Assert.AreEqual(RoleCount, qry3().Count());
+
+            // Join with subquery
+            var qry4 = CompiledQuery.Compile(
+                (int a, int b, string sep) =>
+                    roles
+                        .Where(x => x.Key.Bar > a)
+                        .OrderBy(x => x.Key.Bar)
+                        .Join(persons.Where(x => x.Key < b && x.Key > 0),
+                            r => r.Key.Foo,
+                            p => p.Value.Address.Zip,
+                            (r, p) => p.Value.Name + sep + r.Value.Name + "|")
+                        .Skip(a).Take(1000)
+            );
+
+            Assert.AreEqual(new[] { " Person_2  =Role_2|", " Person_3  =|" }, qry4(1, PersonCount, "=").ToArray());
+
+            // Union
+            var qry5 = CompiledQuery.Compile(() => roles.Select(x => -x.Key.Foo).Union(persons.Select(x => x.Key)));
+
+            Assert.AreEqual(RoleCount + PersonCount, qry5().Count());
+
+            // Projection
+            var qry6 = CompiledQuery.Compile((int minAge) => persons
+                .Select(x => x.Value)
+                .Where(x => x.Age >= minAge)
+                .Select(x => new { x.Name, x.Age })
+                .OrderBy(x => x.Name));
+
+            var res = qry6(PersonCount - 3).GetAll();
+
+            Assert.AreEqual(3, res.Count);
+            Assert.AreEqual(PersonCount - 3, res[0].Age);
+        }
+
+        /// <summary>
+        /// Tests the compiled query overloads.
+        /// </summary>
+        [Test]
+        public void TestCompiledQueryOverloads()
+        {
+            var cache = GetPersonCache().AsCacheQueryable();
+
+            // const args are allowed
+            Assert.AreEqual(5, CompiledQuery.Compile(() => cache.Where(x => x.Key < 5))().GetAll().Count);
+
+            // 0 arg
+            var qry0 = CompiledQuery.Compile(() => cache.Select(x => x.Value.Name));
+            Assert.AreEqual(PersonCount, qry0().ToArray().Length);
+
+            // 1 arg
+            var qry1 = CompiledQuery.Compile((int k) => cache.Where(x => x.Key < k));
+            Assert.AreEqual(3, qry1(3).ToArray().Length);
+
+            // 1 arg twice
+            var qry1T = CompiledQuery.Compile((int k) => cache.Where(x => x.Key < k && x.Value.Age < k));
+            Assert.AreEqual(3, qry1T(3).ToArray().Length);
+
+            // 2 arg
+            var qry2 =
+                CompiledQuery.Compile((int i, string s) => cache.Where(x => x.Key < i && x.Value.Name.StartsWith(s)));
+            Assert.AreEqual(5, qry2(5, " Pe").ToArray().Length);
+
+            // Changed param order
+            var qry2R =
+                CompiledQuery.Compile((string s, int i) => cache.Where(x => x.Key < i && x.Value.Name.StartsWith(s)));
+            Assert.AreEqual(5, qry2R(" Pe", 5).ToArray().Length);
+
+            // 3 arg
+            var qry3 = CompiledQuery.Compile((int i, string s, double d) =>
+                cache.Where(x => x.Value.Address.Zip > d && x.Key < i && x.Value.Name.Contains(s)));
+            Assert.AreEqual(5, qry3(5, "son", -10).ToArray().Length);
+
+            // 4 arg
+            var qry4 = CompiledQuery.Compile((int a, int b, int c, int d) =>
+                cache.Select(x => x.Key).Where(k => k > a && k > b && k > c && k < d));
+            Assert.AreEqual(new[] { 3, 4 }, qry4(0, 1, 2, 5).ToArray());
+
+            // 5 arg
+            var qry5 = CompiledQuery.Compile((int a, int b, int c, int d, int e) =>
+                cache.Select(x => x.Key).Where(k => k > a && k > b && k > c && k < d && k < e));
+            Assert.AreEqual(new[] { 3, 4 }, qry5(0, 1, 2, 5, 6).ToArray());
+
+            // 6 arg
+            var qry6 = CompiledQuery.Compile((int a, int b, int c, int d, int e, int f) =>
+                cache.Select(x => x.Key).Where(k => k > a && k > b && k > c && k < d && k < e && k < f));
+            Assert.AreEqual(new[] { 3, 4 }, qry6(0, 1, 2, 5, 6, 7).ToArray());
+
+            // 7 arg
+            var qry7 = CompiledQuery.Compile((int a, int b, int c, int d, int e, int f, int g) =>
+                cache.Select(x => x.Key).Where(k => k > a && k > b && k > c && k < d && k < e && k < f && k < g));
+            Assert.AreEqual(new[] { 3, 4 }, qry7(0, 1, 2, 5, 6, 7, 8).ToArray());
+
+            // 8 arg
+            var qry8 = CompiledQuery.Compile((int a, int b, int c, int d, int e, int f, int g, int h) =>
+                cache.Select(x => x.Key)
+                    .Where(k => k > a && k > b && k > c && k < d && k < e && k < f && k < g && k < h));
+            Assert.AreEqual(new[] { 3, 4 }, qry8(0, 1, 2, 5, 6, 7, 8, 9).ToArray());
+        }
+
+        /// <summary>
+        /// Tests the free-form compiled query, where user provides an array of arguments.
+        /// </summary>
+        [Test]
+        public void TestCompiledQueryFreeform()
+        {
+            var cache = GetPersonCache().AsCacheQueryable();
+
+            var qry = cache.Where(x => x.Key < 5);
+
+            // Simple
+            var compiled = CompiledQuery.Compile(qry);
+
+            Assert.AreEqual(5, compiled(5).Count());
+            Assert.AreEqual(6, compiled(6).Count());
+
+            // Select
+            var compiledSelect = CompiledQuery.Compile(qry.Select(x => x.Value.Name).OrderBy(x => x));
+
+            Assert.AreEqual(3, compiledSelect(3).Count());
+            Assert.AreEqual(" Person_0  ", compiledSelect(1).Single());
+
+            // Join
+            var compiledJoin = CompiledQuery.Compile(qry.Join(
+                GetOrgCache().AsCacheQueryable().Where(x => x.Value.Name.StartsWith("Org")),
+                p => p.Value.OrganizationId, o => o.Value.Id, (p, o) => o.Key));
+
+            Assert.AreEqual(1000, compiledJoin("Org", 1).Single());
+
+            // Many parameters
+            var qry2 = cache.Where(x => x.Key < 3)
+                .Where(x => x.Key > 0)
+                .Where(x => x.Value.Name.Contains(""))
+                .Where(x => x.Value.Address.Zip > 0)
+                .Where(x => x.Value.Age == 7);
+
+            var compiled2 = CompiledQuery.Compile(qry2);
+
+            Assert.AreEqual(17, compiled2(18, 16, "ers", 13, 17).Single().Key);
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ignite/blob/84c7427a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Contains.cs
----------------------------------------------------------------------
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Contains.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Contains.cs
new file mode 100644
index 0000000..032c746
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Contains.cs
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// ReSharper disable SuspiciousTypeConversion.Global
+// ReSharper disable MemberCanBePrivate.Global
+// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
+// ReSharper disable UnusedAutoPropertyAccessor.Global
+// ReSharper disable StringIndexOfIsCultureSpecific.1
+// ReSharper disable StringIndexOfIsCultureSpecific.2
+// ReSharper disable StringCompareToIsCultureSpecific
+// ReSharper disable StringCompareIsCultureSpecific.1
+// ReSharper disable UnusedMemberInSuper.Global
+namespace Apache.Ignite.Core.Tests.Cache.Query.Linq
+{
+    using System;
+    using System.Collections.Generic;
+    using System.Linq;
+    using Apache.Ignite.Linq;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests LINQ.
+    /// </summary>
+    public partial class CacheLinqTest
+    {
+        /// <summary>
+        /// Tests IEnumerable.Contains.
+        /// </summary>
+        [Test]
+        public void TestContains()
+        {
+            var cache = GetPersonCache().AsCacheQueryable();
+            var orgCache = GetOrgCache().AsCacheQueryable();
+
+            var keys = new[] { 1, 2, 3 };
+            var emptyKeys = new int[0];
+
+            var bigNumberOfKeys = 10000;
+            var aLotOfKeys = Enumerable.Range(-bigNumberOfKeys + 10 - PersonCount, bigNumberOfKeys + PersonCount)
+                .ToArray();
+            var hashSetKeys = new HashSet<int>(keys);
+            var defferedCollection = Enumerable.Range(1, 10)
+                .Select(i => new { Id = i })
+                .Select(arg => arg.Id);
+
+            CheckWhereFunc(cache, e => new[] { 1, 2, 3 }.Contains(e.Key));
+            CheckWhereFunc(cache, e => emptyKeys.Contains(e.Key));
+            CheckWhereFunc(cache, e => new int[0].Contains(e.Key));
+            CheckWhereFunc(cache, e => new int[0].Contains(e.Key));
+            CheckWhereFunc(cache, e => new List<int> { 1, 2, 3 }.Contains(e.Key));
+            CheckWhereFunc(cache, e => new List<int>(keys).Contains(e.Key));
+            CheckWhereFunc(cache, e => aLotOfKeys.Contains(e.Key));
+            CheckWhereFunc(cache, e => hashSetKeys.Contains(e.Key));
+            CheckWhereFunc(cache, e => !keys.Contains(e.Key));
+            CheckWhereFunc(cache, e => defferedCollection.Contains(e.Key));
+            CheckWhereFunc(orgCache, e => new[] { "Org_1", "NonExistentName", null }.Contains(e.Value.Name));
+            CheckWhereFunc(orgCache, e => !new[] { "Org_1", "NonExistentName", null }.Contains(e.Value.Name));
+            CheckWhereFunc(orgCache, e => new[] { "Org_1", null, null }.Contains(e.Value.Name));
+            CheckWhereFunc(orgCache, e => !new[] { "Org_1", null, null }.Contains(e.Value.Name));
+            CheckWhereFunc(orgCache, e => new string[] { null }.Contains(e.Value.Name));
+            CheckWhereFunc(orgCache, e => !new string[] { null }.Contains(e.Value.Name));
+            CheckWhereFunc(orgCache, e => !new string[] { null, null }.Contains(e.Value.Name));
+            CheckWhereFunc(orgCache, e => new string[] { null, null }.Contains(e.Value.Name));
+
+            //check passing a null object as collection
+            int[] nullKeys = null;
+            var nullKeysEntries = cache
+                .Where(e => nullKeys.Contains(e.Key))
+                .ToArray();
+            Assert.AreEqual(0, nullKeysEntries.Length, "Evaluating 'null.Contains' should return zero results");
+
+
+            Func<int[]> getKeysFunc = () => null;
+            var funcNullKeyEntries = cache
+                .Where(e => getKeysFunc().Contains(e.Key))
+                .ToArray();
+            Assert.AreEqual(0, funcNullKeyEntries.Length, "Evaluating 'null.Contains' should return zero results");
+
+
+            // Check subselect from other cache
+            var subSelectCount = cache
+                .Count(entry => orgCache
+                    .Where(orgEntry => orgEntry.Value.Name == "Org_1")
+                    .Select(orgEntry => orgEntry.Key)
+                    .Contains(entry.Value.OrganizationId));
+            var orgNumberOne = orgCache
+                .Where(orgEntry => orgEntry.Value.Name == "Org_1")
+                .Select(orgEntry => orgEntry.Key)
+                .First();
+            var subSelectCheckCount = cache.Count(entry => entry.Value.OrganizationId == orgNumberOne);
+            Assert.AreEqual(subSelectCheckCount, subSelectCount, "subselecting another CacheQueryable failed");
+
+            var ex = Assert.Throws<NotSupportedException>(() =>
+                CompiledQuery.Compile((int[] k) => cache.Where(x => k.Contains(x.Key))));
+            Assert.AreEqual("'Contains' clause on compiled query parameter is not supported.", ex.Message);
+
+            // check subquery from another cache put in separate variable
+            var orgIds = orgCache
+                .Where(o => o.Value.Name == "Org_1")
+                .Select(o => o.Key);
+
+            var subQueryFromVar = cache
+                .Where(x => orgIds.Contains(x.Value.OrganizationId))
+                .ToArray();
+
+            var subQueryInline = cache
+                .Where(x => orgCache.Where(o => o.Value.Name == "Org_1")
+                    .Select(o => o.Key).Contains(x.Value.OrganizationId))
+                .ToArray();
+
+            Assert.AreEqual(subQueryInline.Length, subQueryFromVar.Length);
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ignite/blob/84c7427a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Custom.cs
----------------------------------------------------------------------
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Custom.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Custom.cs
new file mode 100644
index 0000000..f797b4c
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Custom.cs
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// ReSharper disable SuspiciousTypeConversion.Global
+// ReSharper disable MemberCanBePrivate.Global
+// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
+// ReSharper disable UnusedAutoPropertyAccessor.Global
+// ReSharper disable StringIndexOfIsCultureSpecific.1
+// ReSharper disable StringIndexOfIsCultureSpecific.2
+// ReSharper disable StringCompareToIsCultureSpecific
+// ReSharper disable StringCompareIsCultureSpecific.1
+// ReSharper disable UnusedMemberInSuper.Global
+namespace Apache.Ignite.Core.Tests.Cache.Query.Linq
+{
+    using System;
+    using System.Linq;
+    using Apache.Ignite.Core.Cache.Configuration;
+    using Apache.Ignite.Core.Common;
+    using Apache.Ignite.Linq;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests LINQ.
+    /// </summary>
+    public partial class CacheLinqTest
+    {
+        /// <summary>
+        /// Tests the RemoveAll extension.
+        /// </summary>
+        [Test]
+        public void TestRemoveAll()
+        {
+            // Use new cache to avoid touching static data.
+            var cache = Ignition.GetIgnite().CreateCache<int, Person>(new CacheConfiguration("deleteAllTest",
+                new QueryEntity(typeof(int), typeof(Person)))
+            {
+                SqlEscapeAll = GetSqlEscapeAll()
+            });
+
+            Enumerable.Range(1, 10).ToList().ForEach(x => cache.Put(x, new Person(x, x.ToString())));
+
+            var queryable = cache.AsCacheQueryable();
+
+            Func<int[]> getKeys = () => cache.Select(x => x.Key).OrderBy(x => x).ToArray();
+
+            // Without predicate.
+            var res = queryable.Where(x => x.Key < 3).RemoveAll();
+            Assert.AreEqual(2, res);
+            Assert.AreEqual(Enumerable.Range(3, 8), getKeys());
+
+            // With predicate.
+            res = queryable.RemoveAll(x => x.Key < 7);
+            Assert.AreEqual(4, res);
+            Assert.AreEqual(Enumerable.Range(7, 4), getKeys());
+
+            // Subquery-style join.
+            var ids = GetPersonCache().AsCacheQueryable().Where(x => x.Key == 7).Select(x => x.Key);
+
+            res = queryable.Where(x => ids.Contains(x.Key)).RemoveAll();
+            Assert.AreEqual(1, res);
+            Assert.AreEqual(Enumerable.Range(8, 3), getKeys());
+
+            // Row number limit.
+            res = queryable.Take(2).RemoveAll();
+            Assert.AreEqual(2, res);
+            Assert.AreEqual(1, getKeys().Length);
+
+            // Unconditional.
+            queryable.RemoveAll();
+            Assert.AreEqual(0, cache.GetSize());
+
+            // Skip is not supported with DELETE.
+            var nex = Assert.Throws<NotSupportedException>(() => queryable.Skip(1).RemoveAll());
+            Assert.AreEqual(
+                "RemoveAll can not be combined with result operators (other than Take): SkipResultOperator",
+                nex.Message);
+
+            // Multiple result operators are not supported with DELETE.
+            nex = Assert.Throws<NotSupportedException>(() => queryable.Skip(1).Take(1).RemoveAll());
+            Assert.AreEqual(
+                "RemoveAll can not be combined with result operators (other than Take): SkipResultOperator, " +
+                "TakeResultOperator, RemoveAllResultOperator", nex.Message);
+
+            // Joins are not supported in H2.
+            var qry = queryable
+                .Where(x => x.Key == 7)
+                .Join(GetPersonCache().AsCacheQueryable(), p => p.Key, p => p.Key, (p1, p2) => p1);
+
+            var ex = Assert.Throws<IgniteException>(() => qry.RemoveAll());
+            Assert.AreEqual("Failed to parse query", ex.Message.Substring(0, 21));
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ignite/blob/84c7427a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.DateTime.cs
----------------------------------------------------------------------
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.DateTime.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.DateTime.cs
new file mode 100644
index 0000000..b869dc4
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.DateTime.cs
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// ReSharper disable SuspiciousTypeConversion.Global
+// ReSharper disable MemberCanBePrivate.Global
+// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
+// ReSharper disable UnusedAutoPropertyAccessor.Global
+// ReSharper disable StringIndexOfIsCultureSpecific.1
+// ReSharper disable StringIndexOfIsCultureSpecific.2
+// ReSharper disable StringCompareToIsCultureSpecific
+// ReSharper disable StringCompareIsCultureSpecific.1
+// ReSharper disable UnusedMemberInSuper.Global
+namespace Apache.Ignite.Core.Tests.Cache.Query.Linq
+{
+    using System;
+    using System.Linq;
+    using Apache.Ignite.Core.Binary;
+    using Apache.Ignite.Linq;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests LINQ.
+    /// </summary>
+    public partial class CacheLinqTest
+    {
+        /// <summary>
+        /// Tests date time.
+        /// </summary>
+        [Test]
+        public void TestDateTime()
+        {
+            var roles = GetRoleCache().AsCacheQueryable();
+            var persons = GetPersonCache().AsCacheQueryable();
+
+            // Invalid dateTime
+            // ReSharper disable once ReturnValueOfPureMethodIsNotUsed
+            var ex = Assert.Throws<BinaryObjectException>(() =>
+                roles.Where(x => x.Value.Date > DateTime.Now).ToArray());
+            Assert.AreEqual("DateTime is not UTC. Only UTC DateTime can be used for interop with other platforms.",
+                ex.Message);
+
+            // Test retrieval
+            var dates = roles.OrderBy(x => x.Value.Date).Select(x => x.Value.Date);
+            var expDates = GetRoleCache().Select(x => x.Value.Date).OrderBy(x => x).ToArray();
+            Assert.AreEqual(expDates, dates.ToArray());
+
+            // Filtering
+            Assert.AreEqual(1, persons.Count(x => x.Value.Birthday == StartDateTime));
+            Assert.AreEqual(PersonCount, persons.Count(x => x.Value.Birthday >= StartDateTime));
+            Assert.Greater(persons.Count(x => x.Value.Birthday > DateTime.UtcNow), 1);
+
+            // Joins
+            var join =
+                from role in roles
+                join person in persons on role.Value.Date equals person.Value.Birthday
+                select person;
+
+            Assert.AreEqual(2, join.Count());
+
+            // Functions
+            var strings = dates.Select(x => x.ToString("dd MM YYYY HH:mm:ss")).ToArray();
+            Assert.AreEqual(new[] { "17 05 2000 15:04:05", "29 12 2000 23:04:05", "17 05 2001 15:04:05" }, strings);
+
+            // Properties
+            Assert.AreEqual(new[] { 2000, 2000, 2001 }, dates.Select(x => x.Year).ToArray());
+            Assert.AreEqual(new[] { 5, 12, 5 }, dates.Select(x => x.Month).ToArray());
+            Assert.AreEqual(new[] { 17, 29, 17 }, dates.Select(x => x.Day).ToArray());
+            Assert.AreEqual(expDates.Select(x => x.DayOfYear).ToArray(), dates.Select(x => x.DayOfYear).ToArray());
+            Assert.AreEqual(expDates.Select(x => x.DayOfWeek).ToArray(), dates.Select(x => x.DayOfWeek).ToArray());
+            Assert.AreEqual(new[] { 15, 23, 15 }, dates.Select(x => x.Hour).ToArray());
+            Assert.AreEqual(new[] { 4, 4, 4 }, dates.Select(x => x.Minute).ToArray());
+            Assert.AreEqual(new[] { 5, 5, 5 }, dates.Select(x => x.Second).ToArray());
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ignite/blob/84c7427a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Functions.cs
----------------------------------------------------------------------
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Functions.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Functions.cs
new file mode 100644
index 0000000..a672d5a
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Functions.cs
@@ -0,0 +1,211 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// ReSharper disable SuspiciousTypeConversion.Global
+// ReSharper disable MemberCanBePrivate.Global
+// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
+// ReSharper disable UnusedAutoPropertyAccessor.Global
+// ReSharper disable StringIndexOfIsCultureSpecific.1
+// ReSharper disable StringIndexOfIsCultureSpecific.2
+// ReSharper disable StringCompareToIsCultureSpecific
+// ReSharper disable StringCompareIsCultureSpecific.1
+// ReSharper disable UnusedMemberInSuper.Global
+namespace Apache.Ignite.Core.Tests.Cache.Query.Linq
+{
+    using System.Linq;
+    using Apache.Ignite.Linq;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests LINQ.
+    /// </summary>
+    public partial class CacheLinqTest
+    {
+        /// <summary>
+        /// Tests where clause.
+        /// </summary>
+        [Test]
+        public void TestWhere()
+        {
+            var cache = GetPersonCache().AsCacheQueryable();
+
+            // Test const and var parameters
+            const int age = 10;
+            var key = 15;
+
+            Assert.AreEqual(age, cache.Where(x => x.Value.Age < age).ToArray().Length);
+            Assert.AreEqual(age, cache.Where(x => x.Value.Address.Zip < age).ToArray().Length);
+            Assert.AreEqual(19, cache.Where(x => x.Value.Age > age && x.Value.Age < 30).ToArray().Length);
+            Assert.AreEqual(20, cache.Where(x => x.Value.Age > age).Count(x => x.Value.Age < 30 || x.Value.Age == 50));
+            Assert.AreEqual(key, cache.Where(x => x.Key < key).ToArray().Length);
+            Assert.AreEqual(key, cache.Where(x => -x.Key > -key).ToArray().Length);
+
+            Assert.AreEqual(1, GetRoleCache().AsCacheQueryable().Where(x => x.Key.Foo < 2).ToArray().Length);
+            Assert.AreEqual(2, GetRoleCache().AsCacheQueryable().Where(x => x.Key.Bar > 2 && x.Value.Name != "11")
+                .ToArray().Length);
+        }
+
+        /// <summary>
+        /// Tests the group by.
+        /// </summary>
+        [Test]
+        public void TestGroupBy()
+        {
+            var persons = GetPersonCache().AsCacheQueryable();
+            var orgs = GetOrgCache().AsCacheQueryable();
+
+            // Simple, unordered
+            CollectionAssert.AreEquivalent(new[] { 1000, 1001 },
+                persons.GroupBy(x => x.Value.OrganizationId).Select(x => x.Key).ToArray());
+
+            // Aggregate
+            Assert.AreEqual(1000,
+                persons.GroupBy(x => x.Value.OrganizationId).Select(x => x.Key).OrderBy(x => x).First());
+
+            // Ordering and count
+            var res1 =
+                from p in persons
+                orderby p.Value.Name
+                group p by p.Value.OrganizationId
+                into gs
+                orderby gs.Key
+                where gs.Count() > 10
+                select new { Count = gs.Count(), OrgId = gs.Key, AvgAge = gs.Average(x => x.Value.Age) };
+
+            var resArr = res1.ToArray();
+
+            Assert.AreEqual(new[]
+            {
+                new {Count = PersonCount/2, OrgId = 1000, AvgAge = (double) PersonCount/2 - 1},
+                new {Count = PersonCount/2, OrgId = 1001, AvgAge = (double) PersonCount/2}
+            }, resArr);
+
+            // Join and sum
+            var res2 = persons.Join(orgs.Where(o => o.Key > 10), p => p.Value.OrganizationId, o => o.Key,
+                    (p, o) => new { p, o })
+                .GroupBy(x => x.o.Value.Name)
+                .Select(g => new { Org = g.Key, AgeSum = g.Select(x => x.p.Value.Age).Sum() });
+
+            var resArr2 = res2.ToArray();
+
+            Assert.AreEqual(new[]
+            {
+                new {Org = "Org_0", AgeSum = persons.Where(x => x.Value.OrganizationId == 1000).Sum(x => x.Value.Age)},
+                new {Org = "Org_1", AgeSum = persons.Where(x => x.Value.OrganizationId == 1001).Sum(x => x.Value.Age)}
+            }, resArr2);
+        }
+
+        /// <summary>
+        /// Tests the union.
+        /// </summary>
+        [Test]
+        public void TestUnion()
+        {
+            // Direct union
+            var persons = GetPersonCache().AsCacheQueryable();
+            var persons2 = GetSecondPersonCache().AsCacheQueryable();
+
+            var res = persons.Union(persons2).ToArray();
+
+            Assert.AreEqual(PersonCount * 2, res.Length);
+
+            // Subquery
+            var roles = GetRoleCache().AsCacheQueryable().Select(x => -x.Key.Foo);
+            var ids = GetPersonCache().AsCacheQueryable().Select(x => x.Key).Union(roles).ToArray();
+
+            Assert.AreEqual(RoleCount + PersonCount, ids.Length);
+        }
+
+        /// <summary>
+        /// Tests intersect.
+        /// </summary>
+        [Test]
+        public void TestIntersect()
+        {
+            // Direct intersect
+            var persons = GetPersonCache().AsCacheQueryable();
+            var persons2 = GetSecondPersonCache().AsCacheQueryable();
+
+            var res = persons.Intersect(persons2).ToArray();
+
+            Assert.AreEqual(0, res.Length);
+
+            // Subquery
+            var roles = GetRoleCache().AsCacheQueryable().Select(x => x.Key.Foo);
+            var ids = GetPersonCache().AsCacheQueryable().Select(x => x.Key).Intersect(roles).ToArray();
+
+            Assert.AreEqual(RoleCount, ids.Length);
+        }
+
+        /// <summary>
+        /// Tests except.
+        /// </summary>
+        [Test]
+        public void TestExcept()
+        {
+            // Direct except
+            var persons = GetPersonCache().AsCacheQueryable();
+            var persons2 = GetSecondPersonCache().AsCacheQueryable();
+
+            var res = persons.Except(persons2).ToArray();
+
+            Assert.AreEqual(PersonCount, res.Length);
+
+            // Subquery
+            var roles = GetRoleCache().AsCacheQueryable().Select(x => x.Key.Foo);
+            var ids = GetPersonCache().AsCacheQueryable().Select(x => x.Key).Except(roles).ToArray();
+
+            Assert.AreEqual(PersonCount - RoleCount, ids.Length);
+        }
+
+        /// <summary>
+        /// Tests ordering.
+        /// </summary>
+        [Test]
+        public void TestOrdering()
+        {
+            var persons = GetPersonCache().AsCacheQueryable()
+                .OrderByDescending(x => x.Key)
+                .ThenBy(x => x.Value.Age)
+                .ToArray();
+
+            Assert.AreEqual(Enumerable.Range(0, PersonCount).Reverse().ToArray(),
+                persons.Select(x => x.Key).ToArray());
+
+            var personsByOrg = GetPersonCache().AsCacheQueryable()
+                .Join(GetOrgCache().AsCacheQueryable(), p => p.Value.OrganizationId, o => o.Value.Id,
+                    (p, o) => new
+                    {
+                        PersonId = p.Key,
+                        PersonName = p.Value.Name.ToUpper(),
+                        OrgName = o.Value.Name
+                    })
+                .OrderBy(x => x.OrgName.ToLower())
+                .ThenBy(x => x.PersonName)
+                .ToArray();
+
+            var expectedIds = Enumerable.Range(0, PersonCount)
+                .OrderBy(x => (x % 2).ToString())
+                .ThenBy(x => x.ToString())
+                .ToArray();
+
+            var actualIds = personsByOrg.Select(x => x.PersonId).ToArray();
+
+            Assert.AreEqual(expectedIds, actualIds);
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ignite/blob/84c7427a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Introspection.cs
----------------------------------------------------------------------
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Introspection.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Introspection.cs
new file mode 100644
index 0000000..9430dd8
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Introspection.cs
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// ReSharper disable SuspiciousTypeConversion.Global
+// ReSharper disable MemberCanBePrivate.Global
+// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
+// ReSharper disable UnusedAutoPropertyAccessor.Global
+// ReSharper disable StringIndexOfIsCultureSpecific.1
+// ReSharper disable StringIndexOfIsCultureSpecific.2
+// ReSharper disable StringCompareToIsCultureSpecific
+// ReSharper disable StringCompareIsCultureSpecific.1
+// ReSharper disable UnusedMemberInSuper.Global
+namespace Apache.Ignite.Core.Tests.Cache.Query.Linq
+{
+    using System;
+    using System.Linq;
+    using Apache.Ignite.Core.Cache.Query;
+    using Apache.Ignite.Linq;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests LINQ.
+    /// </summary>
+    public partial class CacheLinqTest
+    {
+        /// <summary>
+        /// Tests the introspection.
+        /// </summary>
+        [Test]
+        public void TestIntrospection()
+        {
+            var cache = GetPersonCache();
+
+            // Check regular query
+            var query = cache.AsCacheQueryable(new QueryOptions
+            {
+                Local = true,
+                PageSize = 999,
+                EnforceJoinOrder = true,
+                Timeout = TimeSpan.FromSeconds(2.5),
+                ReplicatedOnly = true,
+                Colocated = true
+            }).Where(x => x.Key > 10).ToCacheQueryable();
+
+            Assert.AreEqual(cache.Name, query.CacheName);
+            Assert.AreEqual(cache.Ignite, query.Ignite);
+
+            var fq = query.GetFieldsQuery();
+
+            Assert.AreEqual(
+                GetSqlEscapeAll()
+                    ? "select _T0._KEY, _T0._VAL from \"person_org\".\"Person\" as _T0 where (_T0.\"_KEY\" > ?)"
+                    : "select _T0._KEY, _T0._VAL from \"person_org\".Person as _T0 where (_T0._KEY > ?)",
+                fq.Sql);
+
+            Assert.AreEqual(new[] { 10 }, fq.Arguments);
+            Assert.IsTrue(fq.Local);
+            Assert.AreEqual(PersonCount - 11, cache.QueryFields(fq).GetAll().Count);
+            Assert.AreEqual(999, fq.PageSize);
+            Assert.IsFalse(fq.EnableDistributedJoins);
+            Assert.IsTrue(fq.EnforceJoinOrder);
+            Assert.IsTrue(fq.ReplicatedOnly);
+            Assert.IsTrue(fq.Colocated);
+            Assert.AreEqual(TimeSpan.FromSeconds(2.5), fq.Timeout);
+
+            var str = query.ToString();
+            Assert.AreEqual(GetSqlEscapeAll()
+                ? "CacheQueryable [CacheName=person_org, TableName=Person, Query=SqlFieldsQuery " +
+                  "[Sql=select _T0._KEY, _T0._VAL from \"person_org\".\"Person\" as _T0 where " +
+                  "(_T0.\"_KEY\" > ?), Arguments=[10], " +
+                  "Local=True, PageSize=999, EnableDistributedJoins=False, EnforceJoinOrder=True, " +
+                  "Timeout=00:00:02.5000000, ReplicatedOnly=True, Colocated=True, Schema=]]"
+                : "CacheQueryable [CacheName=person_org, TableName=Person, Query=SqlFieldsQuery " +
+                  "[Sql=select _T0._KEY, _T0._VAL from \"person_org\".Person as _T0 where " +
+                  "(_T0._KEY > ?), Arguments=[10], " +
+                  "Local=True, PageSize=999, EnableDistributedJoins=False, EnforceJoinOrder=True, " +
+                  "Timeout=00:00:02.5000000, ReplicatedOnly=True, Colocated=True, Schema=]]", str);
+
+            // Check fields query
+            var fieldsQuery = (ICacheQueryable)cache.AsCacheQueryable().Select(x => x.Value.Name);
+
+            Assert.AreEqual(cache.Name, fieldsQuery.CacheName);
+            Assert.AreEqual(cache.Ignite, fieldsQuery.Ignite);
+
+            fq = fieldsQuery.GetFieldsQuery();
+            Assert.AreEqual(GetSqlEscapeAll()
+                    ? "select _T0.\"Name\" from \"person_org\".\"Person\" as _T0"
+                    : "select _T0.NAME from \"person_org\".Person as _T0",
+                fq.Sql);
+
+            Assert.IsFalse(fq.Local);
+            Assert.AreEqual(SqlFieldsQuery.DefaultPageSize, fq.PageSize);
+            Assert.IsFalse(fq.EnableDistributedJoins);
+            Assert.IsFalse(fq.EnforceJoinOrder);
+
+            str = fieldsQuery.ToString();
+            Assert.AreEqual(GetSqlEscapeAll()
+                ? "CacheQueryable [CacheName=person_org, TableName=Person, Query=SqlFieldsQuery " +
+                  "[Sql=select _T0.\"Name\" from \"person_org\".\"Person\" as _T0, Arguments=[], Local=False, " +
+                  "PageSize=1024, EnableDistributedJoins=False, EnforceJoinOrder=False, " +
+                  "Timeout=00:00:00, ReplicatedOnly=False, Colocated=False, Schema=]]"
+                : "CacheQueryable [CacheName=person_org, TableName=Person, Query=SqlFieldsQuery " +
+                  "[Sql=select _T0.NAME from \"person_org\".Person as _T0, Arguments=[], Local=False, " +
+                  "PageSize=1024, EnableDistributedJoins=False, EnforceJoinOrder=False, " +
+                  "Timeout=00:00:00, ReplicatedOnly=False, Colocated=False, Schema=]]", str);
+
+            // Check distributed joins flag propagation
+            var distrQuery = cache.AsCacheQueryable(new QueryOptions { EnableDistributedJoins = true })
+                .Where(x => x.Key > 10 && x.Value.Age > 20 && x.Value.Name.Contains("x"));
+
+            query = distrQuery.ToCacheQueryable();
+
+            Assert.IsTrue(query.GetFieldsQuery().EnableDistributedJoins);
+
+            str = distrQuery.ToString();
+            Assert.AreEqual(GetSqlEscapeAll()
+                ? "CacheQueryable [CacheName=person_org, TableName=Person, Query=SqlFieldsQuery " +
+                  "[Sql=select _T0._KEY, _T0._VAL from \"person_org\".\"Person\" as _T0 where " +
+                  "(((_T0.\"_KEY\" > ?) and (_T0.\"age1\" > ?)) " +
+                  "and (_T0.\"Name\" like \'%\' || ? || \'%\') ), Arguments=[10, 20, x], Local=False, " +
+                  "PageSize=1024, EnableDistributedJoins=True, EnforceJoinOrder=False, " +
+                  "Timeout=00:00:00, ReplicatedOnly=False, Colocated=False, Schema=]]"
+                : "CacheQueryable [CacheName=person_org, TableName=Person, Query=SqlFieldsQuery " +
+                  "[Sql=select _T0._KEY, _T0._VAL from \"person_org\".Person as _T0 where " +
+                  "(((_T0._KEY > ?) and (_T0.AGE1 > ?)) " +
+                  "and (_T0.NAME like \'%\' || ? || \'%\') ), Arguments=[10, 20, x], Local=False, " +
+                  "PageSize=1024, EnableDistributedJoins=True, EnforceJoinOrder=False, " +
+                  "Timeout=00:00:00, ReplicatedOnly=False, Colocated=False, Schema=]]", str);
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ignite/blob/84c7427a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Join.LocalCollection.cs
----------------------------------------------------------------------
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Join.LocalCollection.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Join.LocalCollection.cs
new file mode 100644
index 0000000..e7e05f4
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Join.LocalCollection.cs
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// ReSharper disable SuspiciousTypeConversion.Global
+// ReSharper disable MemberCanBePrivate.Global
+// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
+// ReSharper disable UnusedAutoPropertyAccessor.Global
+// ReSharper disable StringIndexOfIsCultureSpecific.1
+// ReSharper disable StringIndexOfIsCultureSpecific.2
+// ReSharper disable StringCompareToIsCultureSpecific
+// ReSharper disable StringCompareIsCultureSpecific.1
+// ReSharper disable UnusedMemberInSuper.Global
+namespace Apache.Ignite.Core.Tests.Cache.Query.Linq
+{
+    using System.Collections.Generic;
+    using System.Linq;
+    using Apache.Ignite.Linq;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests LINQ.
+    /// </summary>
+    public partial class CacheLinqTest
+    {
+        /// <summary>
+        /// Tests the join with local collection.
+        /// </summary>
+        [Test]
+        public void TestLocalJoin()
+        {
+            var persons = GetPersonCache().AsCacheQueryable();
+            var orgs = GetOrgCache().AsCacheQueryable();
+
+            var localOrgs = orgs
+                .Select(e => e.Value)
+                .ToArray();
+
+            var allOrganizationIds = localOrgs
+                .Select(e => e.Id)
+                .ToArray();
+
+            // Join with local collection 
+            var qry1 = persons.Join(allOrganizationIds,
+                    pe => pe.Value.OrganizationId,
+                    i => i,
+                    (pe, o) => pe
+                )
+                .ToArray();
+
+            Assert.AreEqual(PersonCount, qry1.Length);
+
+            // Join using expression in innerKeySelector
+            var qry2 = persons.Join(allOrganizationIds,
+                    pe => pe.Value.OrganizationId,
+                    i => i + 1 - 1,
+                    (pe, o) => pe
+                )
+                .ToArray();
+
+            Assert.AreEqual(PersonCount, qry2.Length);
+
+            // Local collection subquery
+            var qry3 = persons.Join(localOrgs.Select(e => e.Id),
+                    pe => pe.Value.OrganizationId,
+                    i => i,
+                    (pe, o) => pe
+                )
+                .ToArray();
+
+            Assert.AreEqual(PersonCount, qry3.Length);
+
+            // Compiled query
+            var qry4 = CompiledQuery.Compile(() => persons.Join(allOrganizationIds,
+                pe => pe.Value.OrganizationId,
+                i => i,
+                (pe, o) => pe
+            ));
+
+            Assert.AreEqual(PersonCount, qry4().Count());
+
+            // Compiled query with outer join
+            var qry4A = CompiledQuery.Compile(() => persons.Join(new int[] { }.DefaultIfEmpty(),
+                pe => pe.Value.OrganizationId,
+                i => i,
+                (pe, o) => pe
+            ));
+
+            Assert.AreEqual(PersonCount, qry4A().Count());
+
+            // Compiled query
+            var qry5 = CompiledQuery.Compile(() => persons.Join(new[] { -1, -2 }.DefaultIfEmpty(),
+                pe => pe.Value.OrganizationId,
+                i => i,
+                (pe, o) => pe
+            ));
+
+            Assert.AreEqual(PersonCount, qry5().Count());
+
+            // Outer join
+            var qry6 = persons.Join(new[] { -1, -2 }.DefaultIfEmpty(),
+                    pe => pe.Value.OrganizationId,
+                    i => i,
+                    (pe, o) => pe
+                )
+                .ToArray();
+
+            Assert.AreEqual(PersonCount, qry6.Length);
+
+            // Join with local list
+            var qry7 = persons.Join(new List<int> { 1000, 1001, 1002, 1003 },
+                    pe => pe.Value.OrganizationId,
+                    i => i,
+                    (pe, o) => pe
+                )
+                .ToArray();
+
+            Assert.AreEqual(PersonCount, qry7.Length);
+
+            // Join with local list variable
+            var list = new List<int> { 1000, 1001, 1002, 1003 };
+            var qry8 = persons.Join(list,
+                    pe => pe.Value.OrganizationId,
+                    i => i,
+                    (pe, o) => pe
+                )
+                .ToArray();
+
+            Assert.AreEqual(PersonCount, qry8.Length);
+        }
+
+        /// <summary>
+        /// Tests the compiled query containing join with local collection passed as parameter.
+        /// </summary>
+        [Test]
+        [Ignore("IGNITE-5404")]
+        public void TestLocalJoinCompiledQueryParameter()
+        {
+            var persons = GetPersonCache().AsCacheQueryable();
+            var orgs = GetOrgCache().AsCacheQueryable();
+
+            var localOrgs = orgs
+                .Select(e => e.Value)
+                .ToArray();
+
+            var allOrganizationIds = localOrgs
+                .Select(e => e.Id)
+                .ToArray();
+
+            // Join with local collection passed as parameter
+            var qry1 = CompiledQuery.Compile((IEnumerable<int> lc) => persons.Join(lc,
+                pe => pe.Value.OrganizationId,
+                i => i,
+                (pe, o) => pe
+            ));
+
+            Assert.AreEqual(PersonCount, qry1(allOrganizationIds).Count());
+
+            //Compiled query with outer join
+            var qry2 = CompiledQuery.Compile((IEnumerable<int> lc) => persons.Join(lc.DefaultIfEmpty(),
+                pe => pe.Value.OrganizationId,
+                i => i,
+                (pe, o) => pe
+            ));
+
+            Assert.AreEqual(PersonCount, qry2(new[] { -11 }).Count());
+        }
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ignite/blob/84c7427a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Join.cs
----------------------------------------------------------------------
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Join.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Join.cs
new file mode 100644
index 0000000..f589329
--- /dev/null
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Join.cs
@@ -0,0 +1,310 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// ReSharper disable SuspiciousTypeConversion.Global
+// ReSharper disable MemberCanBePrivate.Global
+// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global
+// ReSharper disable UnusedAutoPropertyAccessor.Global
+// ReSharper disable StringIndexOfIsCultureSpecific.1
+// ReSharper disable StringIndexOfIsCultureSpecific.2
+// ReSharper disable StringCompareToIsCultureSpecific
+// ReSharper disable StringCompareIsCultureSpecific.1
+// ReSharper disable UnusedMemberInSuper.Global
+namespace Apache.Ignite.Core.Tests.Cache.Query.Linq
+{
+    using System;
+    using System.Linq;
+    using Apache.Ignite.Linq;
+    using NUnit.Framework;
+
+    /// <summary>
+    /// Tests LINQ.
+    /// </summary>
+    public partial class CacheLinqTest
+    {
+        /// <summary>
+        /// Tests the same cache join.
+        /// </summary>
+        [Test]
+        public void TestSameCacheJoin()
+        {
+            // Select persons in specific organization
+            var organizations = GetOrgCache().AsCacheQueryable();
+            var persons = GetPersonCache().AsCacheQueryable();
+
+            var res = persons.Join(organizations, person => person.Value.OrganizationId + 3, org => org.Value.Id + 3,
+                    (person, org) => new { Person = person.Value, Org = org.Value })
+                .Where(x => x.Org.Name == "Org_1")
+                .ToList();
+
+            Assert.AreEqual(PersonCount / 2, res.Count);
+
+            Assert.IsTrue(res.All(r => r.Person.OrganizationId == r.Org.Id));
+
+            // Test full projection (selects pair of ICacheEntry)
+            var res2 = persons.Join(organizations, person => person.Value.OrganizationId - 1, org => org.Value.Id - 1,
+                    (person, org) => new { Person = person, Org = org })
+                .Where(x => x.Org.Value.Name.ToLower() == "org_0")
+                .ToList();
+
+            Assert.AreEqual(PersonCount / 2, res2.Count);
+        }
+
+        /// <summary>
+        /// Tests the multi key join.
+        /// </summary>
+        [Test]
+        public void TestMultiKeyJoin()
+        {
+            var organizations = GetOrgCache().AsCacheQueryable();
+            var persons = GetPersonCache().AsCacheQueryable();
+
+            var multiKey =
+                from person in persons
+                join org in organizations on
+                new { OrgId = person.Value.OrganizationId, person.Key } equals
+                new { OrgId = org.Value.Id, Key = org.Key - 1000 }
+                where person.Key == 1
+                select new { PersonName = person.Value.Name, OrgName = org.Value.Name };
+
+            Assert.AreEqual(" Person_1  ", multiKey.Single().PersonName);
+        }
+
+        /// <summary>
+        /// Tests the cross cache join.
+        /// </summary>
+        [Test]
+        public void TestCrossCacheJoin()
+        {
+            var persons = GetPersonCache().AsCacheQueryable();
+            var roles = GetRoleCache().AsCacheQueryable();
+
+            var res = persons.Join(roles, person => person.Key, role => role.Key.Foo, (person, role) => role)
+                .OrderBy(x => x.Key.Bar)
+                .ToArray();
+
+            Assert.AreEqual(RoleCount, res.Length);
+            Assert.AreEqual(101, res[0].Key.Bar);
+        }
+
+        /// <summary>
+        /// Tests the cross cache join.
+        /// </summary>
+        [Test]
+        public void TestCrossCacheJoinInline()
+        {
+            var res = GetPersonCache().AsCacheQueryable().Join(GetRoleCache().AsCacheQueryable(),
+                    person => person.Key, role => role.Key.Foo, (person, role) => role)
+                .OrderBy(x => x.Key.Bar).ToArray();
+
+            Assert.AreEqual(RoleCount, res.Length);
+            Assert.AreEqual(101, res[0].Key.Bar);
+        }
+
+        /// <summary>
+        /// Tests the multi cache join.
+        /// </summary>
+        [Test]
+        public void TestMultiCacheJoin()
+        {
+            var organizations = GetOrgCache().AsCacheQueryable();
+            var persons = GetPersonCache().AsCacheQueryable();
+            var roles = GetRoleCache().AsCacheQueryable();
+
+            var res = roles.Join(persons, role => role.Key.Foo, person => person.Key,
+                    (role, person) => new { person, role })
+                .Join(organizations, pr => pr.person.Value.OrganizationId, org => org.Value.Id,
+                    (pr, org) => new { org, pr.person, pr.role }).ToArray();
+
+            Assert.AreEqual(RoleCount, res.Length);
+        }
+
+        /// <summary>
+        /// Tests the multi cache join subquery.
+        /// </summary>
+        [Test]
+        public void TestMultiCacheJoinSubquery()
+        {
+            var organizations = GetOrgCache().AsCacheQueryable().Where(x => x.Key == 1001);
+            var persons = GetPersonCache().AsCacheQueryable().Where(x => x.Key < 20);
+            var roles = GetRoleCache().AsCacheQueryable().Where(x => x.Key.Foo >= 0);
+
+            var res = roles.Join(persons, role => role.Key.Foo, person => person.Key,
+                    (role, person) => new { person, role })
+                .Join(organizations, pr => pr.person.Value.OrganizationId, org => org.Value.Id,
+                    (pr, org) => new { org, pr.person, pr.role }).ToArray();
+
+            Assert.AreEqual(2, res.Length);
+        }
+
+        /// <summary>
+        /// Tests the outer join.
+        /// </summary>
+        [Test]
+        public void TestOuterJoin()
+        {
+            var persons = GetPersonCache().AsCacheQueryable();
+            var roles = GetRoleCache().AsCacheQueryable();
+
+            var res = persons.Join(roles.Where(r => r.Key.Bar > 0).DefaultIfEmpty(),
+                    person => person.Key, role => role.Key.Foo,
+                    (person, role) => new
+                    {
+                        PersonName = person.Value.Name,
+                        RoleName = role.Value.Name
+                    })
+                .Where(x => x.PersonName != " ")
+                .ToArray();
+
+            Assert.AreEqual(PersonCount, res.Length);
+        }
+
+        /// <summary>
+        /// Tests the subquery join.
+        /// </summary>
+        [Test]
+        public void TestSubqueryJoin()
+        {
+            var persons = GetPersonCache().AsCacheQueryable().Where(x => x.Key >= 0);
+
+            var orgs = GetOrgCache().AsCacheQueryable().Where(x => x.Key > 10);
+
+            var qry1 = persons.Join(orgs,
+                    p => p.Value.OrganizationId,
+                    o => o.Value.Id,
+                    (p, o) => p)
+                .Where(x => x.Key >= 0)
+                .ToList();
+
+            Assert.AreEqual(PersonCount, qry1.Count);
+
+            // With selector inline
+            var qry2 = persons
+                .Join(orgs.Select(orgEntry => orgEntry.Key),
+                    e => e.Value.OrganizationId,
+                    i => i,
+                    (e, i) => e)
+                .ToList();
+
+            Assert.AreEqual(PersonCount, qry2.Count);
+
+            // With selector from variable
+            var innerSequence = orgs
+                .Select(orgEntry => orgEntry.Key);
+
+            var qry3 = persons
+                .Join(innerSequence,
+                    e => e.Value.OrganizationId,
+                    i => i,
+                    (e, i) => e)
+                .ToList();
+
+            Assert.AreEqual(PersonCount, qry3.Count);
+        }
+
+        /// <summary>
+        /// Tests the invalid join.
+        /// </summary>
+        [Test]
+        public void TestInvalidJoin()
+        {
+            var localComplexTypeCollection = GetOrgCache().AsCacheQueryable()
+                .Select(e => e.Value)
+                .ToArray();
+
+            // Join on non-IQueryable with complex(not supported) type
+            var ex = Assert.Throws<NotSupportedException>(() =>
+                // ReSharper disable once ReturnValueOfPureMethodIsNotUsed
+            {
+                GetPersonCache().AsCacheQueryable().Join(localComplexTypeCollection, p => p.Value.OrganizationId,
+                    o => o.Id, (p, o) => p).ToList();
+            });
+
+            Assert.IsTrue(ex.Message.StartsWith("Not supported item type for Join with local collection"));
+        }
+
+        /// <summary>
+        /// Tests query with multiple from clause.
+        /// </summary>
+        [Test]
+        public void TestMultipleFrom()
+        {
+            var persons = GetPersonCache().AsCacheQueryable().Where(x => x.Key < PersonCount);
+            var roles = GetRoleCache().AsCacheQueryable().Where(x => x.Value.Name != "1");
+
+            var all = persons.SelectMany(person => roles.Select(role => new { role, person }));
+            Assert.AreEqual(RoleCount * PersonCount, all.Count());
+
+            var filtered =
+                from person in persons
+                from role in roles
+                where person.Key == role.Key.Foo
+                select new { Person = person.Value.Name, Role = role.Value.Name };
+
+            var res = filtered.ToArray();
+
+            Assert.AreEqual(RoleCount, res.Length);
+        }
+
+        /// <summary>
+        /// Tests query with multiple from clause with inline query sources.
+        /// </summary>
+        [Test]
+        public void TestMultipleFromInline()
+        {
+            var filtered =
+                from person in GetPersonCache().AsCacheQueryable()
+                from role in GetRoleCache().AsCacheQueryable()
+                where person.Key == role.Key.Foo
+                select new { Person = person.Value.Name, Role = role.Value.Name };
+
+            var res = filtered.ToArray();
+
+            Assert.AreEqual(RoleCount, res.Length);
+        }
+
+        /// <summary>
+        /// Tests the join of a table to itself.
+        /// </summary>
+        [Test]
+        public void TestSelfJoin()
+        {
+            // Different queryables
+            var p1 = GetPersonCache().AsCacheQueryable();
+            var p2 = GetPersonCache().AsCacheQueryable();
+
+            var qry = p1.Join(p2, x => x.Value.Age, x => x.Key, (x, y) => x.Key);
+            Assert.AreEqual(PersonCount, qry.ToArray().Distinct().Count());
+
+            // Same queryables
+            var qry2 = p1.Join(p1, x => x.Value.Age, x => x.Key, (x, y) => x.Key);
+            Assert.AreEqual(PersonCount, qry2.ToArray().Distinct().Count());
+        }
+
+        /// <summary>
+        /// Tests the join of a table to itself with inline queryable.
+        /// </summary>
+        [Test]
+        public void TestSelfJoinInline()
+        {
+            var qry = GetPersonCache().AsCacheQueryable().Join(GetPersonCache().AsCacheQueryable(),
+                x => x.Value.Age, x => x.Key, (x, y) => x.Key);
+
+            Assert.AreEqual(PersonCount, qry.ToArray().Distinct().Count());
+        }
+    }
+}
\ No newline at end of file