Entity Framework Core: Dynamically build select list with navigational properties

entity-framework entity-framework-core

Question

The question is similar to this one, but answer does not provide 2 critical things to me:

  • I need the code to work over navigational properties
  • I am trying to build extension method

I want to write queries like this:

this.context.User
    .Where(t => t.Id > 10)
    .SelectCustom(t => t.Address.Country.Title)
    .OrderBy(t => t.DisplayName)
    .Skip(10).Take(5);

With answer in provided link I get this far:

public class SelectList<TSource>
{
    private List<MemberInfo> members = new List<MemberInfo>();
    public SelectList<TSource> Add<TValue>(Expression<Func<TSource, TValue>> selector)
    {
        var member = ((MemberExpression)selector.Body).Member;
        members.Add(member);
        return this;
    }

    public Expression<Func<TSource, TResult>> ToDynamicColumns()
    {
        return this.members.??????????;
    }
}

public static IQueryable<T> SelectCustom<T>(this IQueryable<T> query, Expression<Func<TSource, TKey>> FirstAdditional = null)
{
    var columns = new SelectList<T>();
    columns.Add(t => t.Id);
    columns.Add(t => t.DisplayName)
    if (FirstAdditional != null)
        columns.Add(FirstAdditional);

    return query.Select(columns.ToDynamicColumns);
}

Can this be done with EF Core 2.0?

1
0
11/27/2017 2:20:34 PM

Accepted Answer

You could do this with Expression.ListInit, here the TResult must have an Add instance method e.g. Dictionary<string, object>. This could just work but I havent even compiled it. In any way this should give enough hint on how to build this the way you want.

public Expression<Func<TSource, Dictionary<string, object>>> ToDynamicColumns()
{
   var addMethod = typeof(TResult).GetMethod("Add");

   var paramX = Expression.Parameter(typeof(TSource), "e");

   var bindings = 
       this.members.Select (
          member => 
             return Expression.ElementInit(
                addMethod,
                Expression.Constant(mem.Name),
                Expression.Convert(Expression.Property(paramX, member), typeof(object))
          );
       )

   var listInit = Expression.ListInit(
       Expression.New(typeof(TResult)),
       bindings
   );

   return Expression.Lambda<Func<TSource, Dictionary<string, object>>(
       listInit,
       paramX
   );
}
0
5/21/2018 8:00:46 AM

Popular Answer

EF will look through lambda invoke operations as if the body of that expression was inlined. So I would recommend leaving the source expressions alone and just generate expressions to invoke them.

Also I would keep the result type simple, and just return each row as an array of object. This should result in less overhead than creating lots of dictionaries. If you do need to access fields by name, you should create a single dictionary to maintain the relationship between names and column numbers.

public class SelectList<TSource>
{
    private List<LambdaExpression> members = new List<LambdaExpression>();
    public SelectList<TSource> Add<TValue>(Expression<Func<TSource, TValue>> selector)
    {
        members.Add(selector);
        return this;
    }

    public Expression<Func<TSource, TResult>> ToDynamicColumns()
    {
        var parameter = Expression.Parameter(typeof(TSource), "e");
        return Expression.Lambda<Func<TSource, object[]>>(
            Expression.NewArrayInit(
                typeof(object),
                members.Select(m =>
                    Expression.Convert(Expression.Invoke(m, parameter), typeof(object))
                )
            ),
            parameter);
    }
}

Though in your case, since you are writing an extension method to only return the same key details and a single additional field, you could probably define a single generic type to hold the results, and avoid any mucking around with Linq expressions at all;

public class UserResult<V>{
    public int Id { get; set; }
    public string DisplayName { get; set; }
    public V Value { get; set; }
}

public static IQueryable<UserResult<V>> SelectCustom<V>(this IQueryable<User> query, Expression<Func<User, V>> ValueGetter)
{
    return query.Select(u => new UserResult<V>{
        Id = u.Id,
        DisplayName = u.DisplayName,
        Value = ValueGetter(u)
    });
}

Well almost, if c# would just allow you to compile a call of one Expression<Delegate> from within another. Instead we can implement an ExpressionVisitor to unwrap any call to Compile;

public class DontCompile : ExpressionVisitor
{
    protected override Expression VisitMember(MemberExpression node)
    {
        // Inline any lambda arguments that are expressions
        if (node.Expression is ConstantExpression lambdaArgs
            && node.Member is FieldInfo field
            && typeof(Expression).IsAssignableFrom(field.FieldType))
            return (Expression)field.GetValue(lambdaArgs.Value);
        return base.VisitMember(node);
    }

    protected override Expression VisitMethodCall(MethodCallExpression node)
    {
        // Don't compile lambda expressions
        if (node.Method.Name == "Compile" 
            && typeof(LambdaExpression).IsAssignableFrom(node.Object.Type))
            return Visit(node.Object);
        return base.VisitMethodCall(node);
    }

    public static Expression<T> Tidy<T>(Expression<T> func) => (Expression<T>)new DontCompile().Visit(func);
}
...
    return query.Select(DontCompile.Tidy<...>(u => new UserResult<V>{
        Id = u.Id,
        DisplayName = u.DisplayName,
        Value = ValueGetter.Compile()(u)
    });
...

But that's starting to look a bit messy.



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow