如何为装饰器实现自定义LINQ Provider? [英] How to implement custom LINQ Provider for decorator?

查看:42
本文介绍了如何为装饰器实现自定义LINQ Provider?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个项目,该项目允许通过装饰器和接口实现一些计算出的属性和业务逻辑,该接口控制对EF Code第一层的所有访问.我想通过oData公开此业务逻辑层,并允许标准的IQueryable功能进行过滤,排序和分页.我需要将查询应用于数据库级别,而不仅仅是由于各种原因而导致通过Linq到对象查询的IEnumerable.

I have a project that allows for some computed properties and business logic implemented via decorator and interfaces that controls all access to an EF Code first layer. I want to expose this business logic layer via oData and allow standard IQueryable functionality to filter, order and page. I need the query to be applied on the db level and not just resulting IEnumerable via Linq to Objects query for various reasons.

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

My class structure looks something like LogicClass(Repository) > Interface > Decorator > Poco. And the classes look something like:

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

我想做的事情是这样的:

What I want to be able to do is something like:

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();

这是一个过分的简化.实际上,不可能通过选择新的PersonDecorator(o)"进行查询内转换,因为装饰器层中存在复杂的逻辑,并且在其中进行了处理,因此我们不希望直接访问EF层,而不是将查询保留在装饰器的更抽象的层上.

This is an over simplification. It is not really possible to do a in-query conversion via 'select new PersonDecorator(o)' since there is complex logic in the decorator layer and this is handled in there, and we don't want to allow access directly to the EF layer, instead preferring to keep the queries on the more abstract layer of the decorator.

我已经考虑过此处所述从头开始实现自定义Linq提供程序的路径.但是,该文章过时,我认为过去五年中有更好的方法.我发现 re-linq 听起来很有潜力.但是,当我搜索有关re-linq的教程时,就没有太多了.

I have considered going down the path of implementing a custom Linq provider from scratch like is mentioned 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 which sounds like it has potential. However, when I search for tutorials for re-linq there isn't much to be had.

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

From what I can gather, the high level steps are to create a visitor to replace the subject of the query and convert the filter expressions to match the poco (if it can, most the property names will match) and pass it on to EF. Then save off the expressions not compatible with the EF Poco to later filter the final decorated collection. (purposely leaving out the complications of paging for now)

更新我最近发现了支持Linq的灵活方法,但是我仍然缺少有关如何分解'Where'表达式的信息,其目标是在 PersonEfPoco 上的 IPerson 上使用过滤器.

UPDATE I recently found the fluid method of supporting Linq but I am still lacking information about how to break down a 'Where' expression with the goal of using filters for IPerson on PersonEfPoco.

这使我可以选择.

  1. 完全自定义的Linq提供程序像这样
  2. 使用 re-linq -可以使用帮助来查找教程
  3. 或者最近的Linq提供了更精确的实现方法
  1. completely custom Linq provider like this
  2. use re-linq - could use help finding a tutorial
  3. or does more recent Linq offer a more precise method of implementation

那么什么是最新方法?

推荐答案

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

re-linq is great for implementing LINQ providers that translate queries into another representation, such as SQL or other query languages (disclaimer: I'm one of the original authors). You can also use it to implement a provider that "just" wants a model of the query that's easier to understand than the Expression AST generated by the C# compiler, but your mileage may vary if you actually need your result to look a lot like the original Expression AST.

关于re-linq资源,有一个(已过时,但基本上还可以) CodeProject示例我的旧博客

About re-linq resources, there's the (dated, but still mostly okay) CodeProject sample, my old blog and the mailing list.

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

For your scenario, I'd like to suggest a fourth option which might be simpler than the first two (and no, current LINQ doesn't offer easier methods of provider implementation): Provide your own versions of the LINQ query operator methods.

即,创建一个 DecoratorLayerQuery< ...> 类,该类不实现 IEnumerable< T> IQueryable< T> 定义所需的查询运算符( Where Select SelectMany 等).然后,这些可以在实际数据源上构造基础LINQ查询.因为C#将使用找到的任何 Where Select 等方法,所以它与真实"可枚举的效果一样好.

I.e., create a DecoratorLayerQuery<...> class that, while not implementing IEnumerable<T> or IQueryable<T>, defines the query operators you need (Where, Select, SelectMany, etc.). These could then construct an underlying LINQ query on your real data source. Because C# will use any Where, Select, etc. method it finds, this will work as good as with "real" enumerables.

这就是我的意思:

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;

TranslateToUnderlying ReplaceDecoratedItem 方法是此方法的真正困难,因为它们需要知道如何(并生成将程序员编写的内容转换为EF的表达式)了解.作为扩展版本,它们还可以提取一些查询内容以在内存中执行.但是,这是您的工作必不可少的复杂性:)

The TranslateToUnderlying and ReplaceDecoratedItem methods are the real difficulty in this approach because they need to know how to (and generate expressions that) translate what the programmer wrote to what EF understands. As an extended version, they might also extract some query stuff to be executed in-memory. However, this is the essential complexity of your endeavor :)

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

Some additional (IMO accidental) complexity will raise its ugly head when you need to support subqueries, e.g., a Where predicate containing another query. In these cases, I'd suggest taking a look at how re-linq detects and handles such situations. If you can avoid this feature, I'd suggest you do so.

这篇关于如何为装饰器实现自定义LINQ Provider?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆