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 2021/01/12 10:54:13 UTC

[ignite] branch master updated: IGNITE-13754 .NET: Fix LINQ provider for queries with JOIN and GROUP BY combined

This is an automated email from the ASF dual-hosted git repository.

ptupitsyn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/master by this push:
     new 634ac6d  IGNITE-13754 .NET: Fix LINQ provider for queries with JOIN and GROUP BY combined
634ac6d is described below

commit 634ac6dfe36c4946abd6049423bc65bff8907c17
Author: Pavel Tupitsyn <pt...@apache.org>
AuthorDate: Tue Jan 12 13:53:40 2021 +0300

    IGNITE-13754 .NET: Fix LINQ provider for queries with JOIN and GROUP BY combined
    
    Fix projection traversal in `AliasDictionary.GetQuerySource` and `CacheQueryExpressionVisitor.VisitMember`: use `memberHint` to understand where the given member of the projected type comes from to emit correct table and field name.
---
 .../Apache.Ignite.Core.Tests.csproj                |   1 +
 .../Cache/Query/Linq/CacheLinqTest.Functions.cs    |  98 ---------
 ...qTest.Functions.cs => CacheLinqTest.GroupBy.cs} | 222 ++++++++++++---------
 .../Log/CustomLoggerTest.cs                        |   9 +-
 .../Apache.Ignite.Linq/Impl/AliasDictionary.cs     |  43 +---
 .../Impl/CacheQueryExpressionVisitor.cs            |   5 +-
 .../Apache.Ignite.Linq/Impl/ExpressionWalker.cs    | 130 +++++++++++-
 7 files changed, 270 insertions(+), 238 deletions(-)

diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Apache.Ignite.Core.Tests.csproj b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Apache.Ignite.Core.Tests.csproj
index e7fb47c..01c7e65 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Apache.Ignite.Core.Tests.csproj
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Apache.Ignite.Core.Tests.csproj
@@ -137,6 +137,7 @@
     <Compile Include="Cache\Query\Linq\CacheLinqTest.CompiledQuery.cs" />
     <Compile Include="Cache\Query\Linq\CacheLinqTest.DateTime.cs" />
     <Compile Include="Cache\Query\Linq\CacheLinqTest.Aggregates.cs" />
+    <Compile Include="Cache\Query\Linq\CacheLinqTest.GroupBy.cs" />
     <Compile Include="Cache\Query\Linq\CacheLinqTest.Join.cs" />
     <Compile Include="Cache\Query\Linq\CacheLinqTest.Join.LocalCollection.cs" />
     <Compile Include="Cache\Query\Linq\CacheLinqTest.Strings.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
index dd6b2ce..7020504 100644
--- 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
@@ -61,104 +61,6 @@ namespace Apache.Ignite.Core.Tests.Cache.Query.Linq
         }
 
         /// <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 GroupBy with Where subquery.
-        /// </summary>
-        [Test]
-        public void TestGroupBySubquery()
-        {
-            var persons = GetPersonCache().AsCacheQueryable().Where(p => p.Value.OrganizationId == 1000);
-            var orgs = GetOrgCache().AsCacheQueryable();
-
-            // Simple, unordered
-            CollectionAssert.AreEquivalent(new[] { 1000 },
-                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},
-            }, 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)},
-            }, resArr2);
-        }
-
-        /// <summary>
         /// Tests the union.
         /// </summary>
         [Test]
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.GroupBy.cs
similarity index 50%
copy from modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.Functions.cs
copy to modules/platforms/dotnet/Apache.Ignite.Core.Tests/Cache/Query/Linq/CacheLinqTest.GroupBy.cs
index dd6b2ce..94233a1 100644
--- 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.GroupBy.cs
@@ -1,4 +1,4 @@
-/*
+/*
  * 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.
@@ -15,15 +15,6 @@
  * 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;
@@ -31,37 +22,12 @@ namespace Apache.Ignite.Core.Tests.Cache.Query.Linq
     using NUnit.Framework;
 
     /// <summary>
-    /// Tests LINQ.
+    /// GroupBy tests.
     /// </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);
-            var cacheEntries = GetRoleCache().AsCacheQueryable().Where(x => x.Key.Bar > 2 && x.Value.Name != "11")
-                .ToArray();
-            Assert.AreEqual(3, cacheEntries.Length);
-        }
-
-        /// <summary>
-        /// Tests the group by.
+        /// Tests grouping.
         /// </summary>
         [Test]
         public void TestGroupBy()
@@ -111,10 +77,10 @@ namespace Apache.Ignite.Core.Tests.Cache.Query.Linq
         }
 
         /// <summary>
-        /// Tests the GroupBy with Where subquery.
+        /// Tests the GroupBy with Where sub-query.
         /// </summary>
         [Test]
-        public void TestGroupBySubquery()
+        public void TestGroupBySubQuery()
         {
             var persons = GetPersonCache().AsCacheQueryable().Where(p => p.Value.OrganizationId == 1000);
             var orgs = GetOrgCache().AsCacheQueryable();
@@ -159,102 +125,170 @@ namespace Apache.Ignite.Core.Tests.Cache.Query.Linq
         }
 
         /// <summary>
-        /// Tests the union.
+        /// Tests grouping combined with join.
         /// </summary>
         [Test]
-        public void TestUnion()
+        public void TestGroupByWithJoin()
         {
-            // Direct union
+            var organizations = GetOrgCache().AsCacheQueryable();
             var persons = GetPersonCache().AsCacheQueryable();
-            var persons2 = GetSecondPersonCache().AsCacheQueryable();
 
-            var res = persons.Union(persons2).ToArray();
+            var qry = persons.Join(
+                    organizations,
+                    p => p.Value.OrganizationId,
+                    o => o.Value.Id,
+                    (person, org) => new {person, org})
+                .GroupBy(x => x.person.Value.OrganizationId)
+                .Select(g => new {OrgId = g.Key, MaxAge = g.Max(x => x.person.Value.Age)})
+                .OrderBy(x => x.MaxAge);
 
-            Assert.AreEqual(PersonCount * 2, res.Length);
+            var res = qry.ToArray();
 
-            // Subquery
-            var roles = GetRoleCache().AsCacheQueryable().Select(x => -x.Key.Foo);
-            var ids = GetPersonCache().AsCacheQueryable().Select(x => x.Key).Union(roles).ToArray();
+            Assert.AreEqual(2, res.Length);
 
-            Assert.AreEqual(RoleCount + PersonCount, ids.Length);
+            Assert.AreEqual(1000, res[0].OrgId);
+            Assert.AreEqual(898, res[0].MaxAge);
+
+            Assert.AreEqual(1001, res[1].OrgId);
+            Assert.AreEqual(899, res[1].MaxAge);
         }
 
         /// <summary>
-        /// Tests intersect.
+        /// Tests grouping combined with join in a reverse order.
         /// </summary>
         [Test]
-        public void TestIntersect()
+        public void TestGroupByWithReverseJoin()
         {
-            // Direct intersect
+            var organizations = GetOrgCache().AsCacheQueryable();
             var persons = GetPersonCache().AsCacheQueryable();
-            var persons2 = GetSecondPersonCache().AsCacheQueryable();
 
-            var res = persons.Intersect(persons2).ToArray();
+            var qry = organizations.Join(
+                    persons,
+                    o => o.Value.Id,
+                    p => p.Value.OrganizationId,
+                    (org, person) => new {person, org})
+                .GroupBy(x => x.person.Value.OrganizationId)
+                .Select(g =>
+                    new {OrgId = g.Key, MaxAge = g.Max(x => x.person.Value.Age)})
+                .OrderBy(x => x.MaxAge);
 
-            Assert.AreEqual(0, res.Length);
+            var res = qry.ToArray();
 
-            // Subquery
-            var roles = GetRoleCache().AsCacheQueryable().Select(x => x.Key.Foo);
-            var ids = GetPersonCache().AsCacheQueryable().Select(x => x.Key).Intersect(roles).ToArray();
+            Assert.AreEqual(2, res.Length);
 
-            Assert.AreEqual(RoleCount, ids.Length);
-        }
+            Assert.AreEqual(1000, res[0].OrgId);
+            Assert.AreEqual(898, res[0].MaxAge);
 
+            Assert.AreEqual(1001, res[1].OrgId);
+            Assert.AreEqual(899, res[1].MaxAge);
+        }
+        
         /// <summary>
-        /// Tests except.
+        /// Tests grouping combined with join in a reverse order followed by a projection to an anonymous type.
         /// </summary>
         [Test]
-        public void TestExcept()
+        public void TestGroupByWithReverseJoinAndProjection()
         {
-            // Direct except
+            var organizations = GetOrgCache().AsCacheQueryable();
             var persons = GetPersonCache().AsCacheQueryable();
-            var persons2 = GetSecondPersonCache().AsCacheQueryable();
 
-            var res = persons.Except(persons2).ToArray();
+            var qry = organizations.Join(
+                    persons,
+                    o => o.Value.Id,
+                    p => p.Value.OrganizationId,
+                    (org, person) => new
+                    {
+                        OrgName = org.Value.Name,
+                        person.Value.Age
+                    })
+                .GroupBy(x => x.OrgName)
+                .Select(g => new {OrgId = g.Key, MaxAge = g.Max(x => x.Age)})
+                .OrderBy(x => x.MaxAge);
 
-            Assert.AreEqual(PersonCount, res.Length);
+            var res = qry.ToArray();
 
-            // Subquery
-            var roles = GetRoleCache().AsCacheQueryable().Select(x => x.Key.Foo);
-            var ids = GetPersonCache().AsCacheQueryable().Select(x => x.Key).Except(roles).ToArray();
+            Assert.AreEqual(2, res.Length);
 
-            Assert.AreEqual(PersonCount - RoleCount, ids.Length);
-        }
+            Assert.AreEqual("Org_0", res[0].OrgId);
+            Assert.AreEqual(898, res[0].MaxAge);
 
+            Assert.AreEqual("Org_1", res[1].OrgId);
+            Assert.AreEqual(899, res[1].MaxAge);
+        }
+        
         /// <summary>
-        /// Tests ordering.
+        /// Tests grouping combined with join in a reverse order followed by a projection to an anonymous type with
+        /// custom projected column names.
         /// </summary>
         [Test]
-        public void TestOrdering()
+        public void TestGroupByWithReverseJoinAndProjectionWithRename()
         {
-            var persons = GetPersonCache().AsCacheQueryable()
-                .OrderByDescending(x => x.Key)
-                .ThenBy(x => x.Value.Age)
-                .ToArray();
+            var organizations = GetOrgCache().AsCacheQueryable();
+            var persons = GetPersonCache().AsCacheQueryable();
 
-            Assert.AreEqual(Enumerable.Range(0, PersonCount).Reverse().ToArray(),
-                persons.Select(x => x.Key).ToArray());
+            var qry = organizations.Join(
+                    persons,
+                    o => o.Value.Id,
+                    p => p.Value.OrganizationId,
+                    (org, person) => new Projection
+                    {
+                        OrgName = org.Value.Name,
+                        PersonAge = person.Value.Age
+                    })
+                .GroupBy(x => x.OrgName)
+                .Select(g => new {OrgId = g.Key, MaxAge = g.Max(x => x.PersonAge)})
+                .OrderBy(x => x.MaxAge);
+
+            var res = qry.ToArray();
+
+            Assert.AreEqual(2, res.Length);
+
+            Assert.AreEqual("Org_0", res[0].OrgId);
+            Assert.AreEqual(898, res[0].MaxAge);
+
+            Assert.AreEqual("Org_1", res[1].OrgId);
+            Assert.AreEqual(899, res[1].MaxAge);
+        }
+        
+        /// <summary>
+        /// Tests grouping combined with join in a reverse order followed by a projection to an anonymous type with
+        /// custom projected column names.
+        /// </summary>
+        [Test]
+        public void TestGroupByWithReverseJoinAndAnonymousProjectionWithRename()
+        {
+            var organizations = GetOrgCache().AsCacheQueryable();
+            var persons = GetPersonCache().AsCacheQueryable();
 
-            var personsByOrg = GetPersonCache().AsCacheQueryable()
-                .Join(GetOrgCache().AsCacheQueryable(), p => p.Value.OrganizationId, o => o.Value.Id,
-                    (p, o) => new
+            var qry = organizations.Join(
+                    persons,
+                    o => o.Value.Id,
+                    p => p.Value.OrganizationId,
+                    (org, person) => new
                     {
-                        PersonId = p.Key,
-                        PersonName = p.Value.Name.ToUpper(),
-                        OrgName = o.Value.Name
+                        OrgName = org.Value.Name,
+                        PersonAge = person.Value.Age
                     })
-                .OrderBy(x => x.OrgName.ToLower())
-                .ThenBy(x => x.PersonName)
-                .ToArray();
+                .GroupBy(x => x.OrgName)
+                .Select(g => new {OrgId = g.Key, MaxAge = g.Max(x => x.PersonAge)})
+                .OrderBy(x => x.MaxAge);
 
-            var expectedIds = Enumerable.Range(0, PersonCount)
-                .OrderBy(x => (x % 2).ToString())
-                .ThenBy(x => x.ToString())
-                .ToArray();
+            var res = qry.ToArray();
 
-            var actualIds = personsByOrg.Select(x => x.PersonId).ToArray();
+            Assert.AreEqual(2, res.Length);
 
-            Assert.AreEqual(expectedIds, actualIds);
+            Assert.AreEqual("Org_0", res[0].OrgId);
+            Assert.AreEqual(898, res[0].MaxAge);
+
+            Assert.AreEqual("Org_1", res[1].OrgId);
+            Assert.AreEqual(899, res[1].MaxAge);
+        }
+        
+        private class Projection
+        {
+            public int PersonAge { get; set; }
+            
+            public string OrgName { get; set; }
         }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Log/CustomLoggerTest.cs b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Log/CustomLoggerTest.cs
index 06d54b6..50394fa 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Log/CustomLoggerTest.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Core.Tests/Log/CustomLoggerTest.cs
@@ -140,7 +140,7 @@ namespace Apache.Ignite.Core.Tests.Log
 
                 var errFromJava = TestLogger.Entries.Single(x => x.Exception != null);
                 Assert.IsNotNull(errFromJava.Exception.InnerException);
-                Assert.AreEqual("Error in func.", 
+                Assert.AreEqual("Error in func.",
                     ((ArithmeticException) errFromJava.Exception.InnerException).Message);
             }
         }
@@ -399,7 +399,7 @@ namespace Apache.Ignite.Core.Tests.Log
         /// <summary>
         /// Checks the last message.
         /// </summary>
-        private static void CheckLastMessage(LogLevel level, string message, object[] args = null, 
+        private static void CheckLastMessage(LogLevel level, string message, object[] args = null,
             IFormatProvider formatProvider = null, string category = null, string nativeErr = null, Exception e = null)
         {
             var msg = TestLogger.Entries.Last();
@@ -440,7 +440,7 @@ namespace Apache.Ignite.Core.Tests.Log
             public override string ToString()
             {
                 return string.Format("Level: {0}, Message: {1}, Args: {2}, FormatProvider: {3}, Category: {4}, " +
-                                     "NativeErrorInfo: {5}, Exception: {6}", Level, Message, Args, FormatProvider, 
+                                     "NativeErrorInfo: {5}, Exception: {6}", Level, Message, Args, FormatProvider,
                                      Category, NativeErrorInfo, Exception);
             }
         }
@@ -470,7 +470,7 @@ namespace Apache.Ignite.Core.Tests.Log
                 }
             }
 
-            public void Log(LogLevel level, string message, object[] args, IFormatProvider formatProvider, 
+            public void Log(LogLevel level, string message, object[] args, IFormatProvider formatProvider,
                 string category, string nativeErrorInfo, Exception ex)
             {
                 if (!IsEnabled(level))
@@ -538,6 +538,7 @@ namespace Apache.Ignite.Core.Tests.Log
         /// </summary>
         private struct CustomEnum
         {
+            // ReSharper disable once UnusedMember.Local
             public int Field { get; set; }
         }
     }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/AliasDictionary.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/AliasDictionary.cs
index 15710db..495f422 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/AliasDictionary.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/AliasDictionary.cs
@@ -17,7 +17,6 @@
 
 namespace Apache.Ignite.Linq.Impl
 {
-    using System;
     using System.Collections.Generic;
     using System.Diagnostics;
     using System.Linq;
@@ -75,7 +74,7 @@ namespace Apache.Ignite.Linq.Impl
         {
             Debug.Assert(expression != null);
 
-            return GetTableAlias(GetQuerySource(expression));
+            return GetTableAlias(ExpressionWalker.GetQuerySource(expression));
         }
 
         /// <summary>
@@ -83,7 +82,7 @@ namespace Apache.Ignite.Linq.Impl
         /// </summary>
         public string GetTableAlias(IFromClause fromClause)
         {
-            return GetTableAlias(GetQuerySource(fromClause.FromExpression) ?? fromClause);
+            return GetTableAlias(ExpressionWalker.GetQuerySource(fromClause.FromExpression) ?? fromClause);
         }
 
         /// <summary>
@@ -91,7 +90,7 @@ namespace Apache.Ignite.Linq.Impl
         /// </summary>
         public string GetTableAlias(JoinClause joinClause)
         {
-            return GetTableAlias(GetQuerySource(joinClause.InnerSequence) ?? joinClause);
+            return GetTableAlias(ExpressionWalker.GetQuerySource(joinClause.InnerSequence) ?? joinClause);
         }
 
         /// <summary>
@@ -159,41 +158,5 @@ namespace Apache.Ignite.Linq.Impl
 
             return builder;
         }
-
-        /// <summary>
-        /// Gets the query source.
-        /// </summary>
-        private static IQuerySource GetQuerySource(Expression expression)
-        {
-            var subQueryExp = expression as SubQueryExpression;
-
-            if (subQueryExp != null)
-                return GetQuerySource(subQueryExp.QueryModel.MainFromClause.FromExpression)
-                    ?? subQueryExp.QueryModel.MainFromClause;
-
-            var srcRefExp = expression as QuerySourceReferenceExpression;
-
-            if (srcRefExp != null)
-            {
-                var fromSource = srcRefExp.ReferencedQuerySource as IFromClause;
-
-                if (fromSource != null)
-                    return GetQuerySource(fromSource.FromExpression) ?? fromSource;
-
-                var joinSource = srcRefExp.ReferencedQuerySource as JoinClause;
-
-                if (joinSource != null)
-                    return GetQuerySource(joinSource.InnerSequence) ?? joinSource;
-
-                throw new NotSupportedException("Unexpected query source: " + srcRefExp.ReferencedQuerySource);
-            }
-
-            var memberExpr = expression as MemberExpression;
-
-            if (memberExpr != null)
-                return GetQuerySource(memberExpr.Expression);
-
-            return null;
-        }
     }
 }
diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryExpressionVisitor.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryExpressionVisitor.cs
index 91bde9a0..8cfb604 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryExpressionVisitor.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/CacheQueryExpressionVisitor.cs
@@ -324,6 +324,10 @@ namespace Apache.Ignite.Linq.Impl
 
             if (queryable != null)
             {
+                // Find where the projection comes from.
+                expression = ExpressionWalker.GetProjectedMember(expression.Expression, expression.Member) ??
+                             expression;
+
                 var fieldName = GetEscapedFieldName(expression, queryable);
 
                 ResultBuilder.AppendFormat("{0}.{1}", Aliases.GetTableAlias(expression), fieldName);
@@ -334,7 +338,6 @@ namespace Apache.Ignite.Linq.Impl
             return expression;
         }
 
-        
         /// <summary>
         /// Gets the name of the field from a member expression, with quotes when necessary.
         /// </summary>
diff --git a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/ExpressionWalker.cs b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/ExpressionWalker.cs
index 9a684d9..e49bd32 100644
--- a/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/ExpressionWalker.cs
+++ b/modules/platforms/dotnet/Apache.Ignite.Linq/Impl/ExpressionWalker.cs
@@ -116,10 +116,69 @@ namespace Apache.Ignite.Linq.Impl
                 throw new NotSupportedException("Unexpected query source: " + expression);
 
             return null;
+        }        
+        
+        /// <summary>
+        /// Gets the projected member.
+        /// Queries can have multiple projections, e.g. <c>qry.Select(person => new {Foo = person.Name})</c>.
+        /// This method finds the original member expression from the given projected expression, e.g finds
+        /// <c>Person.Name</c> from <c>Foo</c>.
+        /// </summary>
+        public static MemberExpression GetProjectedMember(Expression expression, MemberInfo memberHint)
+        {
+            Debug.Assert(memberHint != null);
+            
+            var subQueryExp = expression as SubQueryExpression;
+            if (subQueryExp != null)
+            {
+                var selector = subQueryExp.QueryModel.SelectClause.Selector;
+                var newExpr = selector as NewExpression;
+
+                if (newExpr != null)
+                {
+                    Debug.Assert(newExpr.Members.Count == newExpr.Arguments.Count);
+
+                    for (var i = 0; i < newExpr.Members.Count; i++)
+                    {
+                        var member = newExpr.Members[i];
+
+                        if (member == memberHint)
+                        {
+                            return newExpr.Arguments[i] as MemberExpression;
+                        }
+                    }
+                }
+
+                var initExpr = selector as MemberInitExpression;
+
+                if (initExpr != null)
+                {
+                    foreach (var binding in initExpr.Bindings)
+                    {
+                        if (binding.Member == memberHint && binding.BindingType == MemberBindingType.Assignment)
+                        {
+                            return ((MemberAssignment)binding).Expression as MemberExpression;
+                        }
+                    }
+                }
+
+                return GetProjectedMember(subQueryExp.QueryModel.MainFromClause.FromExpression, memberHint);
+            }
+
+            var srcRefExp = expression as QuerySourceReferenceExpression;
+            if (srcRefExp != null)
+            {
+                var fromSource = srcRefExp.ReferencedQuerySource as IFromClause;
+
+                if (fromSource != null)
+                    return GetProjectedMember(fromSource.FromExpression, memberHint);
+            }
+
+            return null;
         }
 
         /// <summary>
-        /// Tries to find QuerySourceReferenceExpression
+        /// Gets the original QuerySourceReferenceExpression.
         /// </summary>
         public static QuerySourceReferenceExpression GetQuerySourceReference(Expression expression,
             // ReSharper disable once ParameterOnlyUsedForPreconditionCheck.Global
@@ -152,6 +211,75 @@ namespace Apache.Ignite.Linq.Impl
         }
 
         /// <summary>
+        /// Gets the query source.
+        /// </summary>
+        public static IQuerySource GetQuerySource(Expression expression, MemberExpression memberHint = null)
+        {
+            if (memberHint != null)
+            {
+                var newExpr = expression as NewExpression;
+
+                if (newExpr != null)
+                {
+                    for (var i = 0; i < newExpr.Members.Count; i++)
+                    {
+                        var member = newExpr.Members[i];
+
+                        if (member == memberHint.Member)
+                        {
+                            return GetQuerySource(newExpr.Arguments[i]);
+                        }
+                    }
+                }
+            }
+
+            var subQueryExp = expression as SubQueryExpression;
+
+            if (subQueryExp != null)
+            {
+                var source = GetQuerySource(subQueryExp.QueryModel.SelectClause.Selector, memberHint);
+                if (source != null)
+                {
+                    return source;
+                }
+
+                return subQueryExp.QueryModel.MainFromClause;
+            }
+
+            var srcRefExp = expression as QuerySourceReferenceExpression;
+
+            if (srcRefExp != null)
+            {
+                var fromSource = srcRefExp.ReferencedQuerySource as IFromClause;
+
+                if (fromSource != null)
+                {
+                    var source = GetQuerySource(fromSource.FromExpression, memberHint);
+                    if (source != null)
+                    {
+                        return source;
+                    }
+
+                    return fromSource;
+                }
+
+                var joinSource = srcRefExp.ReferencedQuerySource as JoinClause;
+
+                if (joinSource != null)
+                    return GetQuerySource(joinSource.InnerSequence, memberHint) ?? joinSource;
+
+                throw new NotSupportedException("Unexpected query source: " + srcRefExp.ReferencedQuerySource);
+            }
+
+            var memberExpr = expression as MemberExpression;
+
+            if (memberExpr != null)
+                return GetQuerySource(memberExpr.Expression, memberExpr);
+
+            return null;
+        }
+
+        /// <summary>
         /// Evaluates the expression.
         /// </summary>
         public static T EvaluateExpression<T>(Expression expr)