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

时间:2014-05-09 19:49:17

标签: c# linq entity-framework

我在SQL Server中使用Entity Framework Code First,其域名实体与此类似:

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

项目可以在整个生命周期中分配到多个位置,但只有一个项目可以随时生效,我们使用它来获取项目的实际位置:

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

如果我加载整个项目对象,则此属性按预期工作:

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

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

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

1 个答案:

答案 0 :(得分:1)

首先,如果我们希望能够将此子查询与另一个查询相结合,那么我们需要将其定义为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)

(我自己添加的空白)

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