【问题标题】:Encapsulating LINQ queries in navigation properties for re-use?将 LINQ 查询封装在导航属性中以供重用?
【发布时间】:2014-05-10 04:02:07
【问题描述】:

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

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

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

【问题讨论】:

  • 我肯定对此感到困惑。我建议调试两个版本以确保 ItemLocations 无论是直接访问还是通过 Location 属性访问都相同。不过,这可能是一个缓存问题...
  • @Bobson 它无法按现状工作。在Location 属性中完成的查询已经编译成IL 代码。查询提供者不知道如何将其解析为 SQL 代码;它看到的只是一个Location 属性,它没有映射到数据库列。

标签: c# linq entity-framework


【解决方案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)

(自己添加的空格)

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

【讨论】:

  • 谢谢。希望有一个更简单的解决方案,但这看起来可行。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多