Factoring out expressions in linq queries

淺唱寂寞╮ 提交于 2019-12-14 03:57:13

问题


Let's say I have the following Linq2Entities query for some service method:

public IQueryable<CustomerProjection>()
{
    return
        from i in this.DbContext.Customers
        select new CustomerProjection()
        {
            Reference = new EntityReference()
            {
                Id = i.Id,
                Name = i.Name
            }
        }
}

All my model entities, including Customer, all support the following interface:

interface INamedEntity
{
    String Name { get; set; }
    Guid Id { get; set; }
}

so that in principle I'm tempted to do some refactoring:

public IQueryable<CustomerProjection>()
{
    return
        from i in this.DbContext.Customers
        select new CustomerProjection()
        {
            // I want something something like this:
            Reference = GetReference(i)
        }
}

obviously I can't naivly define GetReference this way:

public EntityReference GetReference<E>(E i)
    where E : INamedEntity
{
    return new EntityReference()
    {
        Id = i.Id,
        Name = i.Name,
    };
}

What I need to factor out is the logic to create an expression for a query, not an EntityReference directly. So let's define it this way:

public Expression<Func<INamedEntity, EntityReference>> GetReferenceExpression()
    where E : INamedEntity
{
    return i => new EntityReference()
    {
        Id = i.Id,
        Name = i.Name,
    };
}

This correctly factors the logic away. However, I can't use it in the main query:

public IQueryable<CustomerProjection>()
{
    return
        from i in this.DbContext.Customers
        select new CustomerProjection()
        {
            // Something like "Invoke" doesn't exist!
            Reference = GetReferenceExpression().Invoke(i)
        }
}

I need something like this "Invoke" extension method supported by Linq2Entities to help me actually use my factored-out logic.

Note that this is an example scenario with reduced complexity. I understand that factoring in this simple instance isn't really necessary, but I had cases more complex where I wish I could do something like that.

Also, there is a second, related scenario in which one not only wants to use the factored-out code in more than one query, but evaluate it directly:

GetReferenceExpression().Compile()(myEntity);

This is especially interesting if the factored-out code is a predicate for a filter.

So my question is:

  1. Does Linq2Entities support something like this? If so, how?
  2. Is there another linq provider independent way of factoring out expressions?

回答1:


You can use a proxy LINQ provider that sits in front of EF's LINQ provider. You can do any rewrite that you want in that place.

I have done this. It is a lot of work but it can certainly be done. You could, for example, make your provider rewrite this:

Expression<Func<MyEntity, bool>> someFilter = e => e.SomeProperty == 1234;
...
from e in db.Entities
where MyCustomLINQProvider.CallExpression(someFilter, e)
select e

to this:

from e in db.Entities
where e.SomeProperty == 1234
select e

which is acceptable for EF's LINQ provider. MyCustomLINQProvider.CallExpression would be a helper that is never called at runtime. It is just there as a marker for your rewriting engine to inline the given expression. Without that helper the code would not compile.

To do this you'd need to implement a custom IQueryable. This interface is defined as:

public interface IQueryable : IEnumerable
{
    Type ElementType { get; }
    Expression Expression { get; }
    IQueryProvider Provider { get; }
}

public interface IQueryProvider
{
    IQueryable CreateQuery(Expression expression);
    IQueryable<TElement> CreateQuery<TElement>(Expression expression);
    object Execute(Expression expression);
    TResult Execute<TResult>(Expression expression);
}

In IQueryProvider.Execute you perform the rewriting and relay the query to EF's IQueryProvider.


You could make MyCustomLINQProvider.CallExpression an extension so it's less awkward to use:

static bool Call<TTarget, TArg0>(
  this Expression<Func<TArg0, TReturn>> target, TArg0 arg0) {
 throw new NotSupportedException("cannot call this statically");
}
...
where someFilter.Call(e) //looks almost like a func

You need a layer above EF to abstract away the LINQ provider:

ObjectContext objectContext;
IQueryable<T> Query<T>() {
 return new MyCustomQueryable<T>(objectContext.CreateObjectSet<T>());
}

So don't ask EF for a query, ask your code so that you can be handed a proxy.

In your expression rewriter, you don't have to do a lot. You look for the right pattern and just replace the predicate ParameterExpression with whatever was passed as an argument to Call. Here's a sketch:

protected virtual Expression VisitMethodCall(MethodCallExpression node)
{
    if (IsCall(node)) {
     var expressionToInline = GetExpressionToInline(node.Arguments[0]);
     var arg0 = node.Arguments[1];
     var parameter = expressionToInline.Parameters[0];
     var predicateExpression = ReplaceExpression(original: expressionToInline.Body, toReplace: parameter, replaceWith: arg0);
     return predicateExpression;
    }

    return base.VisitMethodCall(node);
}

There's sample code for ReplaceExpression available on the web.




回答2:


You can do this:

Interfaces:

public interface IReference
{
  int ID { get; }
  string Name { get; }
}   

Concrete classes that implement the interfaces

public class Reference : IReference
{
  public int ID { get; set; }
  public string Name { get; set; }
}

public class ReferenceAndEntity<T>
{
  public Reference Reference { get; set; }
  public T Entity { get; set }
}

IQueryable

public static IQueryable<ReferenceAndEntity<T>> GetReferenceAndEntityQuery<T>(IQueryable<T> set) where T: class, IReference
{
  var query = from x in set
              select new ReferenceAndEntity<T>()
              {
                Reference = new Reference()
                {
                  ID = x.ID,
                  Name = x.Name
                },
                Entity = x
              };
  return query;
}

Usage

using(DbContext context = new Context())
{
  var query = from x in GetReferenceAndEntityQuery(context.dataSet)
              select new
              {
                Reference = x.Reference,
                EntityProperty1 = x.Entity.Property1,
              };
}


来源:https://stackoverflow.com/questions/21050855/factoring-out-expressions-in-linq-queries

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!