在导航属性中封装LINQ查询以便重用

本文关键字:查询 LINQ 导航 属性 封装 | 更新日期: 2023-09-27 18:02:33

我使用实体框架代码首先与SQL Server,与域实体是类似的:

public class Item
{
  public ICollection<ItemLocation> ItemLocations { get; set; }
}

一个物品在它的生命周期中可以被分配到许多位置,但在任何时候只有一个是活动的,我们使用这个来获得物品的实际位置:

public Location
{
  get
  {
    return ItemLocations.Where(x => x.IsActive).Select(x => x.Location).FirstOrDefault()
  }
}

如果我加载整个item对象,该属性将按预期工作:

var item = (from i in db.Items select i).FirstOrDefault();
Console.WriteLine(item.Location.Name);

然而,我不能在我的LINQ查询中使用它,我需要返回一个匿名类型,像这样:

var items = from i in db.Items
            select new
                   {
                     ItemId = i.ItemId,
                     LocationName = i.Location.Name
                   };
相反,我必须每次都使用完整的查询:
var items = from i in db.Items
            select new
                   {
                     ItemId = i.ItemId,
                     LocationName = i.ItemLocations.Where(x => x.IsActive).Select(x => x.Location).FirstOrDefault().Name
                   };

理想情况下,我希望将检索项目位置的逻辑保留在一个地方(如属性),而不是将这些逻辑分散到各个地方。

实现这一目标的最佳方法是什么?

在导航属性中封装LINQ查询以便重用

因此,首先,如果我们希望能够将此子查询与另一个查询结合起来,那么我们需要将其定义为Expression对象,而不是c#代码。如果它已经被编译成IL代码,那么查询提供程序就不能检查它来查看执行了哪些操作并将其转换为SQL代码。创建表示此操作的Expression非常简单:

public static readonly Expression<Func<Item, ItemLocation>> LocationSelector =
    item => item.ItemLocations.Where(x => x.IsActive)
            .Select(x => x.Location)
            .FirstOrDefault();

现在我们有了一个表达式来从一个项目中获取位置,我们需要将它与自定义表达式结合起来,使用这个位置从一个项目中选择出一个匿名对象。要做到这一点,我们需要一个Combine方法,它可以接受一个表达式选择一个对象到另一个对象,以及另一个表达式,它接受原始对象,第一个表达式的结果,并计算一个新的结果:

public static Expression<Func<TFirstParam, TResult>>
    Combine<TFirstParam, TIntermediate, TResult>(
    this Expression<Func<TFirstParam, TIntermediate>> first,
    Expression<Func<TFirstParam, TIntermediate, TResult>> second)
{
    var param = Expression.Parameter(typeof(TFirstParam), "param");
    var newFirst = first.Body.Replace(first.Parameters[0], param);
    var newSecond = second.Body.Replace(second.Parameters[0], param)
        .Replace(second.Parameters[1], newFirst);
    return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
}

在内部,这只是用第一个表达式的主体替换第二个表达式的形参的所有实例;其余的代码只是确保整个过程中只有一个参数,并将结果包装回一个新的lambda。这段代码依赖于用另一个表达式替换一个表达式的所有实例的能力,我们可以使用:

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);
    }
}

现在我们有了Combine方法,我们需要做的就是调用它:

db.Items.Select(Item.LocationSelector.Combine((item, location) => new
    {
        ItemId = item.ItemId,
        LocationName = location.Name
    }));

瞧。

如果需要,可以输出调用Combine生成的表达式,而不是传递给Select。这样做,它打印出:

param => new <>f__AnonymousType3`2(ItemId = param.ItemId, 
    LocationName = param.ItemLocations.Where(x => x.IsActive)
    .Select(x => x.Location).FirstOrDefault().Name)

(空格自行添加)

这正是您手动指定的查询,但是这里我们重用了现有的子查询,而不需要每次都输入它。