如何为装饰器实现自定义LINQ Provider

本文关键字:实现 自定义 LINQ Provider | 更新日期: 2023-09-27 18:18:20

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

我的类结构看起来像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)'来进行查询内转换是不可能的,因为在decorator层中有复杂的逻辑,这是在那里处理的,我们不想允许直接访问EF层,而是更愿意将查询保留在decorator的更抽象的层上。

我考虑过从零开始实现一个自定义Linq提供程序,就像这里提到的那样。然而,这篇文章非常过时,我认为在过去的5年里有更好的方法。我找到了"重灵"听起来很有潜力。然而,当我搜索关于re-linq的教程时,没有太多的东西可以找到。

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

我最近发现了支持Linq的流体方法,但我仍然缺乏关于如何分解'Where'表达式的信息,以便在PersonEfPoco上为IPerson使用过滤器。

这让我想到了我的选择。

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

那么什么是最新的方法呢?

如何为装饰器实现自定义LINQ Provider

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

关于重新链接的资源,有(过时的,但仍然很好)CodeProject示例,我的旧博客和邮件列表。

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

。,创建一个DecoratorLayerQuery<...>类,虽然不实现IEnumerable<T>IQueryable<T>,但它定义了您需要的查询操作符(WhereSelectSelectMany等)。然后可以在实际数据源上构造一个底层LINQ查询。因为c#将使用它找到的任何Where, Select等方法,这将与使用"真正的"枚举一样好。

我的意思是:

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是如何检测和处理这种情况的。如果您可以避免此功能,我建议您这样做。