剔除linq查询中的表达式的因素[英] Factoring out expressions in linq queries

本文是小编为大家收集整理的关于剔除linq查询中的表达式的因素的处理/解决方法,可以参考本文帮助大家快速定位并解决问题,中文翻译不准确的可切换到English标签页查看源文。

问题描述

假设我对某些服务方法有以下 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);

如果分解出的代码是过滤器的谓词,这将特别有趣.

所以我的问题是:

  1. Linq2Entities 是否支持这样的功能?如果有,怎么做?
  2. 是否有另一种独立于 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 的示例代码.

本文地址:https://www.itbaoku.cn/post/1556820.html

问题描述

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?

推荐答案

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.