The LINQ expression could not be translated for ba

2020-02-14 05:59发布

问题:

I have custom OrderBy implementation, it only works for types without inheritance, if I want order by field from base type I got The LINQ expression could not be translated

public static IOrderedQueryable<TEntity> OrderBy<TEntity>(this IQueryable<TEntity> source, string orderByProperty, bool desc)
{
    if (source == null)
    {
        throw new ArgumentNullException(nameof(source));
    }

    if (string.IsNullOrEmpty(orderByProperty))
    {
        throw new ArgumentNullException(nameof(orderByProperty));
    }

    var command = desc ? "OrderByDescending" : "OrderBy";

    var type = typeof(TEntity);

    var param = Expression.Parameter(type, "p");
    var property = type.GetProperty(orderByProperty);
    var propertyAccess = Expression.MakeMemberAccess(param, property);
    var orderByExpression = Expression.Lambda(propertyAccess, param);
    var resultExpression = Expression.Call(
            typeof(Queryable),
            command,
            new Type[] { type, property.PropertyType },
            source.Expression,
            Expression.Quote(orderByExpression));
    return (IOrderedQueryable<TEntity>)source.Provider.CreateQuery(resultExpression);
}

I am using entityframework core 2.2 but the very interesting thigs is that if I write just source.OrderBy(x=>x.someBaseField) then it works without any problem, so there must by something with my custom implementation

In error log I got also the translated query and it looks like this, intereresting is end part

orderby new SomeType() {NewField = [entity].DbField, Id = [entity].Id}.Id desc

orderByExpression.Body {p => p.Id}

resultExpression

.Call System.Linq.Queryable.OrderByDescending(
    .Call System.Linq.Queryable.Select(
        .Call System.Linq.Queryable.Where(
            .Call System.Linq.Queryable.Where(
                .Constant<Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[MyTypeView]>(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[MyTypeView]),
                '(.Lambda #Lambda1<System.Func`2[MyTypeView,System.Boolean]>)),
            '(.Lambda #Lambda2<System.Func`2[MyTypeView,System.Boolean]>)),
        '(.Lambda #Lambda3<System.Func`2[MyTypeView, MyTypeResult]>))
    ,
    '(.Lambda #Lambda4<System.Func`2[MyTypeResult,System.Guid]>))

回答1:

I've seen something like this before. The only difference between compiler generated and manual expression is the ReflectedType property of the PropertyInfo - in compiler generated code it's the same as DeclaringType, which in this case is the base class, while in the PropertyInfo obtained via type.GetProperty it is the derived type used to obtain it.

For some unknown reason (probably a bug) this is confusing EF Core. The workaround is to change the code as follows:

var property = type.GetProperty(orderByProperty);
if (property.DeclaringType != property.ReflectedType)
    property = property.DeclaringType.GetProperty(property.Name);

or use a helper method like this

static PropertyInfo GetProperty(Type type, string name)
{
    for (; type != null; type = type.BaseType)
    {
        var property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly);
        if (property != null) return property;
    }
    return null;
}

In order to support nested properties, I would add the following helpers

static Expression Property(Expression target, string name) =>
    name.Split('.').Aggregate(target, SimpleProperty);

static Expression SimpleProperty(Expression target, string name) =>
    Expression.MakeMemberAccess(target, GetProperty(target.Type, name));

and then use

var propertyAccess = Property(param, orderByProperty);

and

new Type[] { type, orderByExpression.ReturnType },

inside the method in question.