如何为装饰器实现自定义 LINQ 提供程序?

How to implement custom LINQ Provider for decorator?

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

我的 class 结构看起来像 LogicClass(Repository) > Interface > Decorator > Poco。 classes 看起来像:

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 层,而是更愿意将查询保留在装饰器的更抽象层上。

我考虑过从头开始实现自定义 Linq 提供程序的路径,就像提到的那样 here. However that article is very dated and I would think there is a better way in the last 5 years. I found re-linq,这听起来很有潜力。但是,当我搜索 re-linq 的教程时,却没有太多内容。

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

更新 我最近找到了支持 Linq 的流畅方法,但我仍然缺乏有关如何分解 'Where' 表达式以在 PersonEfPoco.[=18= 上使用 IPerson 过滤器的信息]

这让我做出了选择。

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

那么最新的方法是什么?

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

关于 re-linq 资源,有(过时的,但大部分还可以)CodeProject sample, my old blog and the mailing list

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

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

我的意思是:

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 如何检测和处理此类情况。如果你能避免这个功能,我建议你这样做。