问题
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:
- Does Linq2Entities support something like this? If so, how?
- 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