.NET Core 3 InvalidOperationException on OrderBy 动态字段名称

.NET Core 3 InvalidOperationException on OrderBy with dynamic field name

我正在从 .NET Core 2 或 3 版本迁移现有网站 API。 在几个问题之后,我设法让它工作,除了按列名的动态 OrderBy。

这是我的代码,适用于 .net core 2:

public async Task<IEnumerable<Clientes_view>> GetClientes(int bActivos, int nRegistroInic, int nRegistros, string sOrdenar, 
        int nSentido, string sFiltro, int nTipo = -1, int idCliente = -1)
    {
        var clientes = this.context.Set<Clientes_view>()
           .Where(e => e.RazonFantasia.Contains(sFiltro) || e.RazonFantasia.Contains(sFiltro)
               || e.Cuit.Contains(sFiltro) || e.Mail.StartsWith(sFiltro) || string.IsNullOrEmpty(sFiltro))
           .Where(e => (e.Activo && bActivos == 1) || bActivos == -1 || (!e.Activo && bActivos == 0))
           .Where(e => e.IdTipoCliente == nTipo || nTipo == -1)
           .Where(e => e.IdCliente == idCliente || idCliente == -1);

        if (!string.IsNullOrEmpty(sOrdenar))
        {
            var propertyInfo = this.context.Set<Clientes_view>().First().GetType().GetProperty(sOrdenar, 
                BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);

            if (propertyInfo != null) if (nSentido == -1) clientes = clientes.OrderByDescending(e => propertyInfo.GetValue(e, null));
                else clientes = clientes.OrderBy(e => propertyInfo.GetValue(e, null));
        }

        clientes = clientes.Skip(nRegistroInic).Take(nRegistros);

        return await clientes.ToListAsync();
    }

我得到的错误如下:

System.InvalidOperationException: The LINQ expression 'DbSet .Where(c => True) .Where(c => c.Activo && True || False || False) .Where(c => True) .Where(c => True) .OrderBy(c => __propertyInfo_3.GetValue( obj: c, index: null))' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync().

有什么想法吗? 谢谢!

很明显,通过反射调用属性不能自动转换为 SQL 查询。

它以前唯一可行的方法是从未采用此分支,或者整个查询由您的应用程序而不是在数据库端处理。

要解决此问题,请按照错误消息的建议进行操作:将查询分解为数据库和应用程序部分,例如

if (!string.IsNullOrEmpty(sOrdenar))
{
    IEnumerable<Clientes_view> list = await clientes.AsAsyncEnumerable();
    list = list.Where(.....); //here you may use everything you like
    return list;
}

如果您正在寻找一种在服务器端动态生成 OrderBy 部分的方法,请查看 ;显然它是为经典 EF 编写的,但应该可以在 EF Core 中使用,只需稍作调整。

EF Core 尝试将尽可能多的查询转换为服务器端查询(即 SQL)。在 3.0 之前的版本中,任何无法转换的代码都会在客户端静默 运行 - 然而,这可能会导致大量且通常不直观的性能问题,因此从 3.0 开始决定,如果无法转换任何查询代码, 将立即抛出异常。

参考:https://docs.microsoft.com/en-us/ef/core/querying/client-eval#previous-versions

最终结果是,您要么需要重新构建代码,将服务器上可以和不能 运行 的部分分开,要么强制所有内容都在服务器上 运行客户。参考文档解释了如何实现后者,但请注意,这样做可能会对性能产生重大影响。

在您的情况下,if (!string.IsNullOrEmpty(sOrdenar)) 块内的内容是导致问题的原因。您应该知道,这意味着无论何时执行该块,它后面的分页(SkipTake)都不会在服务器上执行,总是在客户端上执行 - 所以如果您使用此方法曾遇到过性能问题,现在你知道为什么了!

您的问题是您在 order by 内部使用反射,而您可能应该使用排序字符串。 options

之一
Install-Package System.Linq.Dynamic
using System.Linq.Dynamic;

然后你可以排序

query.OrderBy("item.item_id DESC")

如果您没有很多排序选项,则没有任何库的其他选项是:

switch(sOrdenar){
   case "Field1"
     clientes = nSentido == -1 ? clientes.OrderBy(entity=> entity.Field1) : clientes.OrderByDescending(entity=> entity.Field1);
     break;
   case "OtherField"
          clientes = nSentido == -1 ? clientes.OrderBy(entity=> entity.OtherField) : clientes.OrderByDescending(entity=> entity.OtherField);
     break;
}

我个人更喜欢第二种选择,因为那样我可以确定用户只能对允许的字段进行排序,否则如果你有大表并且用户开始对错误的字段进行排序,你可能会遇到性能问题(永远不要相信您的用户 :)).

您需要实际生成成员访问表达式,您所做的只是使用反射来获取某个对象的值,并将其作为表达式提供。那行不通,查询提供程序将无法翻译它。

你需要做这样的事情:

if (!String.IsNullOrEmpty(sOrdenar))
{
    var type = typeof(Clientes_view);
    var prop = type.GetProperty(sOrdenar);
    if (prop != null)
    {
        var param = Expression.Parameter(type);
        var expr = Expression.Lambda<Func<Clientes_view, object>>(
            Expression.Convert(Expression.Property(param, prop), typeof(object)),
            param
        );
        if (nSentido == -1)
            clientes = clientes.OrderByDescending(expr);
        else
            clientes = clientes.OrderBy(expr);
    }
}