如何为装饰器实现自定义 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
过滤器的信息]
这让我做出了选择。
那么最新的方法是什么?
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>
,但它定义了您需要的查询运算符(Where
,Select
、SelectMany
等)。然后,这些可以在您的真实数据源上构建底层 LINQ 查询。因为 C# 将使用它找到的任何 Where
、Select
等方法,所以这与 "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;
TranslateToUnderlying
和 ReplaceDecoratedItem
方法是这种方法的真正难点,因为它们需要知道如何(并生成表达式)将程序员编写的内容转换为 EF 可以理解的内容。作为扩展版本,它们还可能提取一些查询内容以在内存中执行。然而,这是您努力的基本复杂性:)
当您需要支持子查询时,一些额外的(IMO 意外的)复杂性会让人头疼,例如,包含另一个查询的 Where
谓词。在这些情况下,我建议看一下 re-linq 如何检测和处理此类情况。如果你能避免这个功能,我建议你这样做。
我有一个项目允许通过装饰器和接口实现一些计算属性和业务逻辑,这些装饰器和接口控制对 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
过滤器的信息]
这让我做出了选择。
那么最新的方法是什么?
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>
,但它定义了您需要的查询运算符(Where
,Select
、SelectMany
等)。然后,这些可以在您的真实数据源上构建底层 LINQ 查询。因为 C# 将使用它找到的任何 Where
、Select
等方法,所以这与 "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;
TranslateToUnderlying
和 ReplaceDecoratedItem
方法是这种方法的真正难点,因为它们需要知道如何(并生成表达式)将程序员编写的内容转换为 EF 可以理解的内容。作为扩展版本,它们还可能提取一些查询内容以在内存中执行。然而,这是您努力的基本复杂性:)
当您需要支持子查询时,一些额外的(IMO 意外的)复杂性会让人头疼,例如,包含另一个查询的 Where
谓词。在这些情况下,我建议看一下 re-linq 如何检测和处理此类情况。如果你能避免这个功能,我建议你这样做。