c# - 使用 EF Core 编写动态 LINQ 查询以进行排序和投影

标签 c# entity-framework linq entity-framework-core expression-trees

我在尝试获取“内部图”成员的有效表达式时遇到了麻烦。

我根据之前的堆栈溢出答案编写了几行代码。但我在寻找好的“解决方案”时遇到问题。

例如尝试将“字符串”表示为 lambda 字段成员...

我重构了 ToMemberOf 扩展方法,确实可以做到这一点,但对于“内部图”成员来说会失败。

即仅适用于“直接成员(member)”。

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

但对于“内部图”成员,即

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

将失败并出现 System.ArgumentException,因为 ToMemberOf 扩展方法无法处理“点”和“内部图”成员。

然后我尝试编写一个名为 ToExtendedMemberOf 的新扩展方法

它似乎可以工作,但在使用 EF Core 时失败。

即“内部图”成员“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 core 一起使用时,我遇到了特定于提供程序的错误。

即对于 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();
            }
        }
    }
}

除此之外..我打算使用投影

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

即 result1 应该是匿名对象的集合 result2、result3 和 result3 是 Player 对象的集合,其中并非所有属性都已由 EF 填充,正如我要求使用投影进行的那样

请参阅下面的其他相关链接:

Dynamically build select list from linq to entities query

Sorting a list using Lambda/Linq to objects

enter image description here

最佳答案

关于从点分隔的字符串名称(也称为成员路径)为您所谓的“内部图”成员(我称之为嵌套成员)构建成员选择器表达式。

您的方法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)}

关于c# - 使用 EF Core 编写动态 LINQ 查询以进行排序和投影,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53500412/

相关文章:

c# - 是否可以从 xml 文本创建 ConfigurationElement?

c# - 为什么在路径中找不到异常 SignTool.exe?

c# - IgnoreExceptionTypes 不起作用(C# Webdriver)

c# - 使用可选参数创建 Type 实例的 Linq 表达式?

C# LINQ 如何从数据库中获取数据源?

c# - 简单的数学运算在 double 数据类型上比在 float 数据类型上更快?

mysql - 带索引的 order by limit 的缓慢 MySql 查询

entity-framework - EF Code First - 指向另一个表的多个属性

c# - 使用实体服务/存储库模式时,在模型中引用 System.Web.Security 是一种不好的做法吗?

c# - linq to sql 从性能索引列开始