【问题标题】:How to implement custom LINQ Provider for decorator?如何为装饰器实现自定义 LINQ 提供程序?
【发布时间】:2015-10-22 01:53:19
【问题描述】:

我有一个项目,它允许通过装饰器和接口实现一些计算属性和业务逻辑,这些接口控制对 EF 代码第一层的所有访问。我想通过 oData 公开这个业务逻辑层,并允许标准的 IQueryable 功能来过滤、排序和分页。由于各种原因,我需要将查询应用于数据库级别,而不仅仅是通过 Linq 生成 IEnumerable to Objects 查询。

我的类结构类似于 LogicClass(Repository) > Interface > Decorator > Poco。这些类看起来像:

public class PeopleLogicLayer
{
    // ... business and query  logic ...

    // basic query used internally
    private System.Linq.IQueryable<PersonEfPoco> GetQuery()
    {
        if (this.CurrentQuery == null) this.ResetQuery();

        var skipQuantity = (this.Page <= 1) ? 0 : (this.Page - 1) * this.PageSize;

        return this.CurrentQuery.Skip(skipQuantity)
                    .Take(this.PageSize)
                    .AsQueryable();
    }
}

public interface IPerson
{
    int Id { get; set; }
    String FirstName { get; set; }
    String LastName { get; set; }
    String FullName { get; }
}

public class PersonEfPoco
{
    public int Id { get; set; }
    public String FirstName { get; set; }

    public String LastName { get; set; }
}

public class PersonDecorator : IPerson
{
    private PersonEfPoco _person;

    public PersonDecorator(PersonEfPoco person)
    {
        this._person = person;
    }

    public int Id
    {
        get { return this._person.Id; }
        set { this._person.Id = value; }
    }

    public String FirstName
    {
        get { return this._person.FirstName; }
        set { this._person.FirstName = value; }
    }

    public String LastName
    {
        get { return this._person.LastName }
        set { this._person.LastName = value }
    }

    public String FullName
    {
        get { return $"{this._person.FirstName} {this._person.LastName}"; }
    }
}

我想要做的是:

List<IPerson> peopleNamedBob = 
    from o in (new PeopleHiddenBehindBusinessLogic()) where o.FirstName == "Bob" select o;

List<IPerson> peopleNamedBob = 
    (new PeopleHiddenBehindBusinessLogic()).Where(o => o.FirstName == "Bob").ToList();

这是一个过度简化。实际上不可能通过“select new PersonDecorator(o)”进行查询内转换,因为装饰层中有复杂的逻辑,并且在其中处理,我们不想允许直接访问 EF层,而不是更喜欢将查询保留在装饰器的更抽象层上。

我考虑过像here 提到的那样从头开始实现自定义 Linq 提供程序。但是那篇文章已经过时了,我认为在过去的 5 年里有更好的方法。我发现re-linq 听起来很有潜力。但是,当我搜索 re-linq 的教程时,没有太多内容可供选择。

据我所知,高级步骤是创建访问者以替换查询的主题转换过滤器表达式以匹配 poco(如果它可以,大多数属性名称都会匹配)并将其传递给 EF。然后保存与 EF Poco 不兼容的表达式,以便稍后过滤最终的装饰集合。 (暂时不考虑分页的复杂性)

更新 我最近发现了支持 Linq 的流畅方法,但我仍然缺乏关于如何分解“Where”表达式的信息,目的是在 PersonEfPoco 上使用 IPerson 的过滤器。

这让我做出了选择。

  1. 完全自定义的 Linq 提供程序 like this
  2. 使用re-linq - 可以使用帮助查找教程
  3. 或者最近的 Linq 提供了更精确的实现方法

那么最新的方法是什么?

【问题讨论】:

    标签: c# entity-framework linq linq-to-sql


    【解决方案1】:

    re-linq 非常适合实现将查询转换为另一种表示形式的 LINQ 提供程序,例如 SQL 或其他查询语言(免责声明:我是原始作者之一)。您还可以使用它来实现“只是”想要一个比 C# 编译器生成的Expression AST 更容易理解的查询模型的提供程序,但是如果您确实需要让结果看起来很多,您的里程可能会有所不同就像原来的ExpressionAST。

    关于 re-linq 资源,有(过时的,但基本上还可以)CodeProject samplemy old blogmailing list

    对于您的场景,我想建议第四个选项,它可能比前两个更简单(不,当前的 LINQ 不提供更简单的提供程序实现方法):提供您自己的 LINQ 查询运算符版本方法。

    即,创建一个DecoratorLayerQuery&lt;...&gt; 类,虽然没有实现IEnumerable&lt;T&gt;IQueryable&lt;T&gt;,但它定义了您需要的查询运算符(WhereSelectSelectMany 等)。然后,这些可以在您的真实数据源上构造一个底层 LINQ 查询。因为 C# 将使用它找到的任何 WhereSelect 等方法,这将与“真实”枚举一样好。

    这就是我的意思:

    public interface IDecoratorLayerQuery<TDecorated>
    {
      IDecoratorLayerQuery<TDecorated> Where (Expression<Func<TDecorated, bool>> predicate);
      // etc.      
    }
    
    public class DecoratorLayerQuery<TDecorated, TUnderlying> : IDecoratorLayerQuery<TDecorated>
    {
      private IQueryable<TUnderlying> _underlyingQuery;
    
      public DecoratorLayerQuery(IQueryable<TUnderlying> underlyingQuery)
      {
        _underlyingQuery = underlyingQuery;
      }
    
      public IDecoratorLayerQuery<TDecorated> Where (Expression<Func<TDecorated, bool>> predicate)
      {
        var newUnderlyingQuery = _underlyingQuery.Where(TranslateToUnderlying(predicate));
        return new DecoratorLayerQuery<TDecorated, TUnderlying> (newUnderlyingQuery);
      }
    
      private Expression<Func<TUnderlying, bool>> TranslateToUnderlying(Expression<Func<TDecorated, bool>> predicate)
      {
        var decoratedParameter = predicate.Parameters.Single();
        var underlyingParameter = Expression.Parameter(typeof(TUnderlying), decoratedParameter.Name + "_underlying");
    
        var bodyWithUnderlyingParameter = ReplaceDecoratedItem (decoratedParameter, underlyingParameter, predicate.Body);
    
        return Expression.Lambda<Func<TUnderlying, bool>> (bodyWithUnderlyingParameter, underlyingParameter);
      }
    
      private Expression ReplaceDecoratedItem(Expression decorated, Expression underlying, Expression body)
      {
        // Magic happens here: Implement an expression visitor that iterates over body and replaces all occurrences with _corresponding_ occurrences of _underlying_.
        // This will probably involve translating member expressions as well. E.g., if decorated is of type IPerson, decorated.FullName must instead become 
        // the Expression equivalent of 'underlying.FirstName + " " + underlying.FullName'.
      }
    
      public List<TDecorated> ToList() // And AsEnumerable, AsQueryable, etc.
      {
        var projection = /* construct Expression that transforms TUnderlying to TDecorated here */;
        return _underlyingQuery.Select(projection).ToList();
      }
    }
    
    public static class DecoratorLayerQueryFactory
    {
      public static IDecoratorLayerQuery<TDecorated> CreateQuery<TDecorated>()
      {
        var underlyingType = /* calculate underlying type for TDecorated here */;
        var queryType = typeof (DecoratorLayerQuery<,>).MakeGenericType (typeof (TDecorated), underlyingType);
    
        var initialSource = DbContext.Set(underlyingType);
        return (IDecoratorLayerQuery<TDecorated>) Activator.CreateInstance (queryType, initialSource);
      }
    }
    
    var exampleQuery =
        from p in DecoratorLayerQueryFactory.CreateQuery<IPerson>
        where p.FullName == "John Doe"
        select p.FirstName;
    

    TranslateToUnderlyingReplaceDecoratedItem 方法是这种方法的真正困难,因为它们需要知道如何(并生成表达式)将程序员编写的内容转换为 EF 理解的内容。作为扩展版本,它们还可能提取一些要在内存中执行的查询内容。但是,这是您努力的基本复杂性:)

    当您需要支持子查询时,一些额外的(IMO 意外)复杂性会引起它的丑陋,例如,包含另一个查询的 Where 谓词。在这些情况下,我建议看看 re-linq 如何检测和处理这种情况。如果您可以避免使用此功能,我建议您这样做。

    【讨论】:

    • 毫无疑问,这是我在这种情况下看到的最好的信息。您是否知道 linq 定义的好资源,其中,选择,跳过,采取 SelectMany...?顺便说一句,我确实计划不支持子查询。主要消费者是 oData 服务,因此如果他们尝试一些花哨的东西,很容易抛出不支持查询的异常......不确定 oData 是否可以这样做。
    • 我猜IEnumerator (MSDN) 方法是要匹配的定义。
    • @DrydenMaker 更好地使用IQueryable&lt;T&gt; 的扩展方法作为模板——它们将谓词、选择器等作为Expressions 而不是委托。这样,您可以解析和翻译它们以成为目标查询的一部分。不过,您绝对不需要支持所有这些。
    • 好的,这更有意义。我发现another answer 有一个简单的访问者来翻译查询。我将限制在哪里,采取,跳过以保持简单。那么也许我会弄清楚如何做 Include() ...谢谢!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-05
    • 2011-08-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多