在编译查询中重用现有的linq表达式

本文关键字:linq 表达式 编译 查询 | 更新日期: 2023-09-27 18:03:05

考虑以下代码,它提供了两个方法:一个返回IQueryable,另一个利用编译查询非常有效地返回与特定ID匹配的位置:

    public IQueryable<Location> GetAllLocations()
    {
        return from location in Context.Location
               where location.DeletedDate == null
                     && location.Field1 = false
                     && location.Field2 = true
                     && location.Field3 > 5
               select new LocationDto 
               {
                     Id = location.Id,
                     Name = location.Name
               }
    }

    private static Func<MyDataContext, int, Location> _getByIdCompiled;
    public Location GetLocationById(int locationId)
    {
        if (_getByIdCompiled == null) // precompile the query if needed
        {
            _getByIdCompiled = CompiledQuery.Compile<MyDataContext, int, Location>((context, id) => 
                (from location in Context.Location
                where location.DeletedDate == null
                      && location.Field1 = false
                      && location.Field2 = true
                      && location.Field3 > 5
                      && location.Id == id
                select new LocationDto {
                     Id = location.Id,
                     Name = location.Name
                })).First());
        }
        // Context is a public property on the repository all of this lives in
        return _getByIdCompiled(Context, locationId);
    }

这是对实际代码的一个相当大的简化,但我认为它理解了思想,并且工作得很好。我想做的下一件事是重构代码,以便表达式的公共位可以被重用,因为它将在许多其他类型的编译查询中使用。换句话说,这个表达式:

                from location in Context.Location
                where location.DeletedDate == null
                      && location.Field1 = false
                      && location.Field2 = true
                      && location.Field3 > 5
                select new LocationDto 
                {
                     Id = location.Id,
                     Name = location.Name
                };

我如何以某种方式捕获这在一个变量或函数和重用它在多个编译查询?到目前为止,我的尝试已经导致错误抱怨的东西不能被翻译成SQL,成员访问不允许,等等。

更新:我可以问这个问题的另一种可能更好的方式是:

考虑下面两个已编译的查询:

_getByIdCompiled = CompiledQuery.Compile<MyDataContext, int, LocationDto>((context, id) => 
      (from location in Context.Location // here
      where location.DeletedDate == null // here
            && location.Field1 = false // here
            && location.Field2 = true // here
            && location.Field3 > 5 // here
            && location.Id == id
      select new LocationDto { // here
          Id = location.Id, // here
          Name = location.Name
      })).First()); // here
_getByNameCompiled = CompiledQuery.Compile<MyDataContext, int, LocationDto>((context, name) => 
       (from location in Context.Location // here
     where location.DeletedDate == null // here
         && location.Field1 = false // here
         && location.Field2 = true // here
         && location.Field3 > 5 // here
         && location.Name == name
     select new LocationDto { // here
       Id = location.Id, // here
       Name = location.Name // here
     })).First()); // here

所有标记为// here的行都是重复的非常不干燥的代码片段。(在我的代码库中,这实际上是30多行代码。)如何将其分解并使其可重用?

在编译查询中重用现有的linq表达式

所以,整个事情有点奇怪,因为Compile方法不仅需要将传递给每个查询操作符(Where, Select等)的Expression对象视为它可以理解的东西,而且还需要将整个查询,包括所有操作符的使用,视为它可以理解的Expression对象。这基本上消除了更传统的查询组合作为一个选项。

这会变得有点乱;比我真正想要的要多,但我没有看到很多好的选择。

我们要做的是创建一个方法来构造我们的查询。它将接受一个filter作为参数,并且该过滤器将是一个Expression,表示某个对象的过滤器。

接下来,我们将定义一个lambda,它看起来与传递给Compile的lambda几乎完全相同,但增加了一个额外的参数。这个额外的参数将与我们的过滤器具有相同的类型,它将表示实际的过滤器。我们将在整个lambda中使用这个参数,而不是过滤器。然后,我们将使用UseIn方法将新lambda中第三个参数的所有实例替换为我们提供给该方法的filter表达式。

下面是构造查询的方法:
private static Expression<Func<MyDataContext, int, IQueryable<LocationDto>>>
    ConstructQuery(Expression<Func<Location, bool>> filter)
{
    return filter.UseIn((MyDataContext context, int id,
        Expression<Func<Location, bool>> predicate) =>
        from location in context.Location.Where(predicate)
        where location.DeletedDate == null
                && location.Field1 == false
                && location.Field2 == true
                && location.Field3 > 5
                && location.Id == id
        select new LocationDto
        {
            Id = location.Id,
            Name = location.Name
        });
}

下面是UseIn方法:

public static Expression<Func<T3, T4, T5>>
    UseIn<T1, T2, T3, T4, T5>(
    this Expression<Func<T1, T2>> first,
    Expression<Func<T3, T4, Expression<Func<T1, T2>>, T5>> second)
{
    return Expression.Lambda<Func<T3, T4, T5>>(
        second.Body.Replace(second.Parameters[2], first),
        second.Parameters[0],
        second.Parameters[1]);
}

(这里的类型很乱,我不知道如何给泛型类型起有意义的名字。)

以下方法用于将一个表达式的所有实例替换为另一个表达式:

public static Expression Replace(this Expression expression,
    Expression searchEx, Expression replaceEx)
{
    return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
}
internal class ReplaceVisitor : ExpressionVisitor
{
    private readonly Expression from, to;
    public ReplaceVisitor(Expression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }
    public override Expression Visit(Expression node)
    {
        return node == from ? to : base.Visit(node);
    }
}

现在我们已经度过了这个血淋淋的烂摊子,接下来是简单的部分。在这一点上,ConstructQuery应该能够被修改,以表示您的真实的查询,没有太多困难。

要调用该方法,我们只需要提供我们想要应用于查询更改的任何过滤器,例如:

var constructedQuery = ConstructQuery(location => location.Id == locationId); 

Linq语句必须在selectgroup子句中结束,所以你不能删除查询的一部分并将其存储在其他地方,但是如果你总是通过相同的四个条件进行过滤,你可以使用lambda语法代替,然后在新的查询中添加任何额外的where子句。

正如@Servy所指出的,您需要调用First()FirstOrDefault()才能从查询结果中获取单个元素。

IQueryable<Location> GetAllLocations()
{
    return Context.Location.Where( x => x.DeletedDate == null
                                     && x.Field1      == false
                                     && x.Field2      == true
                                     && x.Field3       > 5
                                 ).Select( x => x );
}
Location GetLocationById( int id )
{
    return ( from x in GetAllLocations()
             where x.Id == id
             select x ).FirstOrDefault();
}
//or with lambda syntax
Location GetLocationById( int id )
{
    return GetAllLocations()
               .Where( x => x.Id == id )
               .Select( x => x )
               .FirstOrDefault();
}