问题描述
假设我对某些服务方法有以下 Linq2Entities 查询:
public IQueryable<CustomerProjection>() { return from i in this.DbContext.Customers select new CustomerProjection() { Reference = new EntityReference() { Id = i.Id, Name = i.Name } } }
我所有的模型实体,包括客户,都支持以下接口:
interface INamedEntity { String Name { get; set; } Guid Id { get; set; } }
所以原则上我很想进行一些重构:
public IQueryable<CustomerProjection>() { return from i in this.DbContext.Customers select new CustomerProjection() { // I want something something like this: Reference = GetReference(i) } }
显然我不能用这种方式天真地定义 GetReference:
public EntityReference GetReference<E>(E i) where E : INamedEntity { return new EntityReference() { Id = i.Id, Name = i.Name, }; }
我需要考虑的是为查询创建表达式的逻辑,而不是直接创建 EntityReference.所以让我们这样定义:
public Expression<Func<INamedEntity, EntityReference>> GetReferenceExpression() where E : INamedEntity { return i => new EntityReference() { Id = i.Id, Name = i.Name, }; }
这正确地排除了逻辑.但是,我不能在主查询中使用它:
public IQueryable<CustomerProjection>() { return from i in this.DbContext.Customers select new CustomerProjection() { // Something like "Invoke" doesn't exist! Reference = GetReferenceExpression().Invoke(i) } }
我需要类似 Linq2Entities 支持的"Invoke"扩展方法来帮助我实际使用分解后的逻辑.
请注意,这是一个复杂性降低的示例场景.我知道在这个简单的例子中考虑因素并不是真正必要的,但我有一些更复杂的案例,我希望我能做这样的事情.
另外,还有第二种相关的场景,在这种场景中,人们不仅想在多个查询中使用分解后的代码,而且直接对其进行评估:
GetReferenceExpression().Compile()(myEntity);
如果分解出的代码是过滤器的谓词,这将特别有趣.
所以我的问题是:
- Linq2Entities 是否支持这样的功能?如果有,怎么做?
- 是否有另一种独立于 linq 提供程序的方式来分解表达式?
推荐答案
您可以使用位于 EF 的 LINQ 提供程序前面的代理 LINQ 提供程序.你可以在那个地方做任何你想做的重写.
我已经做到了.这是很多工作,但肯定可以完成.例如,您可以让您的提供商重写:
Expression<Func<MyEntity, bool>> someFilter = e => e.SomeProperty == 1234; ... from e in db.Entities where MyCustomLINQProvider.CallExpression(someFilter, e) select e
到这里:
from e in db.Entities where e.SomeProperty == 1234 select e
这对于 EF 的 LINQ 提供程序是可以接受的.MyCustomLINQProvider.CallExpression 将是一个永远不会在运行时调用的助手.它只是作为重写引擎内联给定表达式的标记.如果没有那个助手,代码将无法编译.
为此,您需要实现自定义 IQueryable.该接口定义为:
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); }
在 IQueryProvider.Execute 中执行重写并将查询中继到 EF 的 IQueryProvider.
<小时>你可以把 MyCustomLINQProvider.CallExpression 做成一个扩展,这样使用起来就不会那么尴尬了:
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
您需要在 EF 之上有一个层来抽象出 LINQ 提供程序:
ObjectContext objectContext; IQueryable<T> Query<T>() { return new MyCustomQueryable<T>(objectContext.CreateObjectSet<T>()); }
因此,不要向 EF 询问查询,询问您的代码,以便为您提供代理.
在您的表达式重写器中,您不必做很多事情.您寻找正确的模式,只需将谓词 ParameterExpression 替换为作为参数传递给 Call 的任何内容.这是一个草图:
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); }
网上有 ReplaceExpression 的示例代码.
问题描述
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?
推荐答案
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.