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

时间:2015-10-19 22:01:41

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

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

我的类结构看起来像LogicClass(Repository)>界面>装饰者>波科。这些类看起来像:

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所述。然而,这篇文章非常过时,我认为在过去的5年中有更好的方法。我发现re-linq听起来有潜力。但是,当我搜索re-linq的教程时,没有多少东西可以用。

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

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

这让我有了自己的选择。

  1. 完全自定义Linq提供商like this
  2. 使用re-linq - 可以使用帮助查找教程
  3. 或者最近Linq提供了更精确的实施方法
  4. 那么最新的方法是什么?

1 个答案:

答案 0 :(得分:0)

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

关于re-linq资源,有(过时的,但仍然大部分没事)CodeProject samplemy old blogmailing list

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

即,创建一个DecoratorLayerQuery<...>课程,在不实施IEnumerable<T>IQueryable<T>的情况下,定义您需要的查询运算符(WhereSelect,{ {1}}等)。然后,这些可以在您的真实数据源上构建基础LINQ查询。因为C#将使用它找到的任何SelectManyWhere等方法,所以这将与“真实”枚举一样好。

这就是我的意思:

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; TranslateToUnderlying方法是这种方法的真正难点,因为它们需要知道如何(并生成表达式)将程序员编写的内容翻译成EF理解的内容。作为扩展版本,它们还可能提取一些要在内存中执行的查询内容。然而,这是您努力的基本复杂性:)

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