使用 EF Core 编写用于排序和投影的动态 LINQ 查询

Write Dynamic LINQ queries for sorting and projecting with EF Core

我在尝试为 "inner graph" 成员获取有效表达式时遇到问题。

我根据之前的堆栈溢出答案写了几行代码。但我找不到好的 "solution approach".

例如尝试将 "a string" 表示为 lambda 字段成员...

我已经重构了 ToMemberOf 扩展方法,它确实做到了,但对于 "inner graph" 个成员会失败。

即仅适用于 "direct members".

var order1 = "FullName".ToMemberOf<Player>();   // will be converted to {e => Convert(e.FullName, Object)}

但对于 "inner graph" 成员,即

var order2 = "Catalog.Photo.FileName".ToMemberOf<Player>()

将失败并返回 System.ArgumentException,因为 ToMemberOf 扩展方法无法处理 "dots" 和 "inner graph" 成员。

然后我尝试编写一个名为 ToExtendedMemberOf

的新扩展方法

它似乎有效,但在使用 EF Core 时失败。

即"inner graph" 成员 "Catalog.Photo.FileName" 将转换为。

var order2 = "Catalog.Photo.FileName".ToExtendedMemberOf<Player>();  // will be converted to {e => new Player() {Catalog = new Catalog() {Photo = new Photo() {FileName = e.Catalog.Photo.FileName}}}}

但是当我尝试将它与 EF 核心一起使用时,我遇到了提供程序特定的错误。

即对于 InMemory 提供程序,我得到 System.InvalidOperationException:无法比较数组中的两个元素

即对于 SlqServer 提供程序,我得到 System.InvalidOperationException 无法翻译 LINQ 表达式

确实我希望在服务器而不是客户端上执行评估。

我不知道如何重构 ToExtendedMemberOf 或 ToMemberOf 来实现这些目标。我需要你的帮助

我正在粘贴整个单元文本示例,以实现更全球化的视野。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Xunit;

namespace XUnitTestProject
{
    public static class QueryableExtensions
    {
        public static IQueryable<T> Select<T>(this IQueryable queryable, IEnumerable<string> fields) where T : class
        {
            var sourceType = queryable.ElementType;
            var resultType = typeof(T);
            var parameter = Expression.Parameter(sourceType, "e");
            var body = GetNewMember(typeof(T), parameter, fields.Select(f => f.Split('.')));
            var selector = Expression.Lambda(body, parameter);
            return queryable.Provider.CreateQuery<T>(Expression.Call(typeof(Queryable), "Select", new[] { sourceType, resultType }, queryable.Expression, Expression.Quote(selector)));
        }

        public static IQueryable<T> OrderBy<T>(this IQueryable queryable, IEnumerable<string> fields) where T : class
        {
            var sourceType = queryable.ElementType;
            var resultType = typeof(T);
            var parameter = Expression.Parameter(sourceType, "e");
            var body = GetNewMember(typeof(T), parameter, fields.Select(f => f.Split('.')));
            var selector = Expression.Lambda(body, parameter);
            return queryable.Provider.CreateQuery<T>(Expression.Call(typeof(Queryable), "OrderBy", new[] { sourceType, resultType }, queryable.Expression, Expression.Quote(selector))); // {e => new Player() {Catalog = new Catalog() {Photo = new Photo() {FileName = e.Catalog.Photo.FileName}}}}
        }

        private static Expression GetNewMember(Type targetType, Expression source, IEnumerable<string[]> memberPaths, int depth = 0)
        {
            var target = Expression.Constant(null, targetType);
            var bindings = memberPaths.GroupBy(path => path[depth]).Select(memberGroup =>
            {
                var memberName = memberGroup.Key;
                var targetMember = Expression.PropertyOrField(target, memberName);
                var sourceMember = Expression.PropertyOrField(source, memberName);
                var childMembers = memberGroup.Where(path => depth + 1 < path.Length);
                var enumerable = childMembers as string[][] ?? childMembers.ToArray();
                var targetValue = !enumerable.Any() ? sourceMember : GetNewMember(targetMember.Type, sourceMember, enumerable, depth + 1);
                return Expression.Bind(targetMember.Member, targetValue);
            });
            return Expression.MemberInit(Expression.New(targetType), bindings);
        }

    }

    public static class StringExtensions
    {
        public static Expression<Func<T, object>> ToMemberOf<T>(this string name) where T : class
        {
            var parameter = Expression.Parameter(typeof(T), "e");
            var propertyOrField = Expression.PropertyOrField(parameter, name);
            var unaryExpression = Expression.MakeUnary(ExpressionType.Convert, propertyOrField, typeof(object));

            return Expression.Lambda<Func<T, object>>(unaryExpression, parameter);
        }

        public static UnaryExpression ToExtendedMemberOf<T>(this string name) where T : class
        {
            var parameter = Expression.Parameter(typeof(T), "e");
            var body = GetNewExtendedMember(typeof(T), parameter, new[] { name.Split('.').ToArray() });
            var selector = Expression.Lambda(body, parameter);

            return Expression.Quote(selector);
        }

        private static Expression GetNewExtendedMember(Type targetType, Expression source, IEnumerable<string[]> memberPaths, int depth = 0)
        {
            var target = Expression.Constant(null, targetType);
            var bindings = memberPaths.GroupBy(path => path[depth]).Select(memberGroup =>
            {
                var memberName = memberGroup.Key;
                var targetMember = Expression.PropertyOrField(target, memberName);
                var sourceMember = Expression.PropertyOrField(source, memberName);
                var childMembers = memberGroup.Where(path => depth + 1 < path.Length);
                var enumerable = childMembers as string[][] ?? childMembers.ToArray();
                var targetValue = !enumerable.Any() ? sourceMember : GetNewExtendedMember(targetMember.Type, sourceMember, enumerable, depth + 1);
                return Expression.Bind(targetMember.Member, targetValue);
            });
            return Expression.MemberInit(Expression.New(targetType), bindings);
        }
    }

    public class DataContext : DbContext
    {
        public DbSet<Player> Players { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder builder)
        {
            base.OnConfiguring(builder);

            if (!builder.IsConfigured)
            {
                builder.UseInMemoryDatabase(Guid.NewGuid().ToString());
                builder.ConfigureWarnings(w => w.Throw(RelationalEventId.QueryClientEvaluationWarning));
            }
        }
    }

    public class Player
    {
        public int Id { get; set; }
        public string FullName { get; set; }
        public int Age { get; set; }
        public Catalog Catalog { get; set; }
    }

    public class Photo
    {
        public int Id { get; set; }
        public string FileName { get; set; }
        public string Format { get; set; }
    }

    public class Catalog
    {
        public int Id { get; set; }
        public string CatalogName { get; set; }
        public string Color { get; set; }
        public Photo Photo { get; set; }
    }

    public class UnitTest
    {
        [Fact]
        public void Test()
        {
            var players = new[]
            {
                new Player
                {
                    Id = 1,
                    FullName = "FullName 01",
                    Age = 1,
                    Catalog = new Catalog
                    {
                        Id = 1,
                        CatalogName = "CatalogName 01",
                        Color = "Color 01",
                        Photo = new Photo {Id = 1, FileName = "FileName 01", Format = "Format 01"}
                    }
                },
                new Player
                {
                    Id = 2,
                    FullName = "FullName 02",
                    Age = 2,
                    Catalog = new Catalog
                    {
                        Id = 1,
                        CatalogName = "CatalogName 02",
                        Color = "Color 02",
                        Photo = new Photo {Id = 1, FileName = "FileName 02", Format = "Format 02"}
                    }
                },
                new Player
                {
                    Id = 3,
                    FullName = "FullName 03",
                    Age = 3,
                    Catalog = new Catalog
                    {
                        Id = 1,
                        CatalogName = "CatalogName 03",
                        Color = "Color 03",
                        Photo = new Photo {Id = 1, FileName = "FileName 03", Format = "Format 03"}
                    }
                },
                new Player
                {
                    Id = 4,
                    FullName = "FullName 04",
                    Age = 4,
                    Catalog = new Catalog
                    {
                        Id = 1,
                        CatalogName = "CatalogName 04",
                        Color = "Color 04",
                        Photo = new Photo {Id = 1, FileName = "FileName 04", Format = "Format 04"}
                    }
                },
                new Player
                {
                    Id = 5,
                    FullName = "FullName 05",
                    Age = 5,
                    Catalog = new Catalog
                    {
                        Id = 1,
                        CatalogName = "CatalogName 05",
                        Color = "Color 05",
                        Photo = new Photo {Id = 1, FileName = "FileName 05", Format = "Format 05"}
                    }
                },
            };

            using (var context = new DataContext())
            {
                context.Players.AddRange(players);
                context.SaveChanges();

                var queryable = context.Players as IQueryable<Player>;

                var result1 = queryable
                    .Select(p => new
                    {
                        p.Id,
                        p.FullName,
                        p.Catalog.CatalogName,
                        p.Catalog.Photo.FileName
                    })
                    .Where(a => a.Id > 1)
                    .OrderBy(a => a.FileName)
                    .ToArray();

                // This is OK to filter and order
                Expression<Func<Player, bool>> filter = p => p.Id > 1;  // will be converted to {p => (p.Id > 1)}
                var order1 = "FullName".ToMemberOf<Player>();           // will be converted to {e => Convert(e.FullName, Object)}
                var result2 = queryable
                    .Select<Player>(new[]
                    {
                        "Id",
                        "FullName",
                        "Catalog.CatalogName",
                        "Catalog.Photo.FileName"
                    })
                    .Where(filter)
                    .OrderBy(order1)
                    .ToArray();

                // HOW TO ENHANCE ToMemberOf to GET SUCH MEMBER
                // This line of code will fail with System.ArgumentException : 'Catalog.Photo.FileName' is not a member of type 'Player'
                // var order2 = "Catalog.Photo.FileName".ToMemberOf<Player>();  

                // I could do as..
                // But I could not use it in on the OrderBy clause below
                var order2 = "Catalog.Photo.FileName".ToExtendedMemberOf<Player>();  // will be converted to {e => new Player() {Catalog = new Catalog() {Photo = new Photo() {FileName = e.Catalog.Photo.FileName}}}}
                var result3 = queryable
                    .Select<Player>(new[]
                    {
                        "Id",
                        "FullName",
                        "Catalog.CatalogName",
                        "Catalog.Photo.FileName"
                    })
                    .Where(filter)
                    //.OrderBy(order2)
                    .ToArray();

                // I could do as.. 
                // But this line of code will with InMemory provider fails with System.InvalidOperationException : Failed to compare two elements in the array. System.ArgumentException : At least one object must implement IComparable.
                // But this line of code will with SqlServer provider fails with System.InvalidOperationException : Error generated for warning 'Microsoft.EntityFrameworkCore.Query.QueryClientEvaluationWarning: The LINQ expression 'orderby new Player() {Catalog = new Catalog() {Photo = new Photo() {FileName = e.Catalog.Photo.FileName}}} asc' could not be translated and will be evaluated locally.'.
                var result4 = queryable
                    .Select<Player>(new[]
                    {
                        "Id",
                        "FullName",
                        "Catalog.CatalogName",
                        "Catalog.Photo.FileName"
                    })
                    .Where(filter)
                    .OrderBy<Player>(new[]
                    {
                        "Catalog.Photo.FileName"
                    })
                    .ToArray();
            }
        }
    }
}

除此之外..我打算用projectioned

我在返回的对象上粘贴了一些有差异的图像,只是为了更好地理解我想要实现的目标。

即 result1 应该是匿名对象的集合 result2、result3 和 result3 是 Player 对象的集合,其中并非所有属性都已按照我要求使用 projection

由 EF 填充

查看下面的其他相关链接:

Sorting a list using Lambda/Linq to objects

关于从点分隔的字符串名称(a.k.a. 成员路径)为你所谓的 "inner graph" 成员(我称之为嵌套成员)构建成员选择器表达式。

你的方法MemberOf:

public static Expression<Func<T, object>> ToMemberOf<T>(this string name) where T : class
{
    var parameter = Expression.Parameter(typeof(T), "e");
    var propertyOrField = Expression.PropertyOrField(parameter, name);
    var unaryExpression = Expression.MakeUnary(ExpressionType.Convert, propertyOrField, typeof(object));

    return Expression.Lambda<Func<T, object>>(unaryExpression, parameter);
}

可以轻松调整以处理直接成员和嵌套成员。您只需要更改一行代码:

var propertyOrField = Expression.PropertyOrField(parameter, name);

var propertyOrField = name.Split('.')
    .Aggregate((Expression)parameter, Expression.PropertyOrField);

所以

var order2 = "Catalog.Photo.FileName".ToMemberOf<Player>();

将转换为

{e => Convert(e.Catalog.Photo.FileName)}