一种形式或另一种形式的CQRS范例假定Query调用不会更改应用程序的状态。 也就是说,在同一查询中对同一查询的多次调用将具有相同的结果。
让用作查询一部分的所有接口均为IQuery或IAsyncQuery类型:
public interface IQuery<TIn, TOut> { TOut Query(TIn input); } public interface IAsyncQuery<TIn, TOut>: IQuery<TIn, Task<TOut> { }
这些接口完全描述了数据的接收,例如,考虑到折扣/奖金以及其他所有因素的格式化价格的接收:
public class ProductPriceQuery: IQuery<ProductDto,PricePresentationDto> { public ProductPriceQuery( IQuery<ProductDto, PriceWithSalesDto> priceWithSalesQuery, IQuery<PriceWithSalesDto, PricePresentationDto> pricePresentationQuery) { _priceWithSalesQuery = priceWithSalesQuery; _pricePresentationQuery = pricePresentationQuery; } public PricePresentationDto Query(ProductDto dto) { var withSales = _priceWithSalesQuery(dto); var result = _pricePresentationQuery(withSales); return result; } }
管道接口
这种方法的优点是应用程序中接口的统一性,可以在管道中构建它们:
public class Aggregate2Query<TIn, TOut1, TOut2> : BaseAggregateQuery<TIn, TOut2> { public Aggregate2Query( IQuery<TIn, TOut1> query0, IQuery<TOut1, TOut2> query1) : base(query0, query1){} } public abstract class BaseAggregateQuery<TIn, TOut> : IQuery<TIn, TOut> { private object[] queries { get; set; } protected BaseAggregateQuery(params object[] queries) { this.queries = queries; } public TOut Query(TIn input) => queries.Aggregate<object, dynamic>(input, (current, query) => ((dynamic) query).Query(current)); }
像这样注册:
serviceCollection.AddScoped(typeof(Aggregate2Query<,,>));
我们得到:
public ProductPriceQuery( BaseAggregateQuery<ProductDto,PriceWithSalesDto,PricePresentationDto> query) { _aggregateQuery = query; } public PricePresentationDto Query(ProductDto dto) => _aggregateQuery.Query(dto);
理想情况下,编程应该变成构造函数的组合,但实际上,这只是满足程序员的骄傲的一项漂亮功能。
装饰器和ASP.NET CORE
MediatR库正是基于接口和装饰器的统一性而构建的。
装饰器使您可以在标准IQuery <TIn,TOut>接口上挂起一些附加功能,例如记录:
public class LoggingQuery<TIn,TOut>: IQuery<TIn,TOut> { public LoggingQuery(IQuery<TIn,TOut> priceQuery) { _priceQuery = priceQuery } public TOut Query(TIn input) { Console.WriteLine($"Query {_priceQuery.GetType()} Start"); var result= _priceQuery.Query(input); Console.WriteLine($"Query {_priceQuery.GetType()} End"); return result; } }
我将忽略这样一个事实,即装饰器使您可以在一个地方编写跨领域功能,而不是在整个程序中进行扩展,这不属于本文的范围。
.Net Core提供的标准IoC容器无法注册装饰器,困难之处在于我们有两个接口相同的实现:原始查询和装饰器,装饰器实现的接口与装饰器构造函数相同。 容器无法解析此类图,并引发“循环依赖”错误。
解决此问题的方法有几种,特别是对于.Net Core容器,编写了Scrutor库,它可以注册装饰器:
services.Decorate(typeof(IQuery<,>), typeof(LoggingQuery<,>));
如果您不想向项目添加额外的依赖项,那么您可以自己编写此功能,就像我所做的那样。 在演示代码之前,让我们讨论将查询结果缓存为查询的一部分。 如果需要添加缓存并且键是类,则必须重写GetHashCode和Equals,因此我们将通过引用摆脱比较。
缓存方法
我将提供一个简单的缓存示例:
在搜索值时,首先调用GetHashCode方法以查找所需的篮子,然后,如果篮子中有多个元素,则调用Equals进行比较。 看看您是否不太了解它的工作原理。
ReSharper本身会生成这些方法,但是我们在全球范围内实施缓存,实现IQuery <TIn,TOut>接口的程序员,以及通常IQuery <TIn,TOut>接口的程序员,请不要忘记SRP。 因此,修复工具生成的方法不适合我们。
当我们处理端到端功能时,AOP框架便可以解决。 EqualsFody(Fody的插件)重写IL,重写带有EqualsAttribute属性标记的类中的Equals和GetHashCode。
为了不使用此属性标记每个Dto,我们可以稍微重写IQuery接口
public IQuery<TIn,TOut> where TIn : CachedDto{ } [Equals] public class CachedDto{ }
现在,所有Dto肯定会重新定义必要的方法,并且我们不需要为每个输入DTO添加一个属性(它将从基类中提取)。 如果重写IL不适合您,请按以下方式实现CachedD(使用调用基类方法的上下文):
public class CachedDto{ public override bool Equals(object x) => DeepEquals.Equals(this,x); public override int GetHashCode() => DeepHash.GetHashCode(this); }
DeepEquals.Equals和DeepHash.GetHashCode使用反射,它将比Fody慢,对于企业应用程序不是致命的。
但是请记住有关SRP的信息,IQuery不应该知道它已被缓存。
最好的解决方案是实现IEqualityComparer。 字典将其带入构造函数中,并在插入/删除/搜索时使用它。
public class EqualityComparerUsingReflection<TKey> : IEqualityComparer<TKey> { public bool Equals(TKey x, TKey y) => DeepEqualsCommonType(x, y); public int GetHashCode(TKey obj) => Hash.GetHashCode(obj); }
现在您可以对TIn施加约束,我们实现了我们想要的。 让我们写一个缓存装饰器:
public class BaseCacheQuery<TIn, TOut> : IQuery<TIn, TOut> { private readonly ConcurrentDictionary<TIn, TOut> _cache; private readonly IQuery<TIn, TOut> _query; protected BaseCacheQuery( IQuery<TIn, TOut> query, IConcurrentDictionaryFactory<TIn, TOut> factory) { _cache = factory.Create(); _query = query; } public TOut Query(TIn input) => _cache .GetOrAdd(input, x => _query.Query(input)); }
注意IConcurrentDictionaryFactory,该工厂的目标是提供字典的实例,但是为什么不只在构造函数中创建它呢?
首先,DI和SRP,很可能需要添加另一个比较器实现(例如,对于某些类型的DTO来说更容易)或完全更改实现,其次,缓存可能由于反射和抽象而开始变慢的情况泄漏。 我会妥协,如果在Dto中重写了Equals,并且GetHashCode将不使用“繁重的” EqualityComparer。
工厂的目的是检查方法是否被覆盖,如果是,则使用DTO中重新定义的方法返回标准字典,否-带有比较器的字典。
报名
让我们回到如何注册所有这些内容。
ConfigureServices方法的services参数是ServiceDescriptor的集合,每个描述符包含有关已注册依赖项的信息
public class ServiceDescriptor{
因此,将具有LifeTime = Scoped的新ServiceDescriptor添加到服务集合。
ServiceType = typeof(IService),ImplementType = typeof(Service):
services.AddScoped<IService,Service>().
ImplementationFactory属性允许您指定如何创建依赖关系;我们将使用它。 我将编写一个IServiceCollection扩展,它将在程序集,悬挂装饰器和注册中找到所有IQuery和IAsyncQuery。
public static void AddCachedQueries(this IServiceCollection serviceCollection) {
AddDecorator方法值得特别注意,这里我们使用ActivatorUtilities类的静态方法。 ActivatorUtilities.CreateInstance接受IServiceProvider,要创建的对象的类型以及此对象在构造函数中接受的依赖项实例(您只能指定未注册的对象,其余的将由提供程序允许)
ActivatorUtilities.GetServiceOrCreateInstance-执行相同的操作,但不允许将丢失的依赖项传递给创建的对象的构造函数。 如果对象已在容器中注册,则它会简单地创建它(或返回已经创建的对象),否则,它将创建对象,前提是它可以解决所有依赖关系
因此,您可以创建一个返回缓存对象的函数,并向服务添加描述此注册的描述符。
让我们编写一个测试:
public class DtoQuery : IQuery<Dto, Something> { private readonly IRepository _repository; public DtoQuery(IRepository repository) { _repository = repository; } public Something Query(Dto input) => _repository.GetSomething(); }
ReposityMock-来自Moq库的模拟很有趣,但是要测试存储库的GetSomething()方法被调用了多少次,它也使用装饰器,尽管它使用Castle.Interceptor自动生成它们。 我们使用装饰器测试装饰器。
这是您可以添加所有IQuery <TIn,TOut>结果的缓存的方法,编写如此多的代码来实现一些功能非常不方便。
其他解决方案
Mediatr
中央库界面:
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken); }
MediatR的主要功能是在IRequestHandler上添加包装器,例如,使用IPipelineBehavior接口实现管道,这是注册CachePipelineBehaviour的方式,它将应用于所有已注册的IRequestHandler接口:
sc.AddScoped(typeof(IPipelineBehavior<,>), typeof(CachePipelineBehaviour<,>));
我们实现缓存PipelineBehaviour:
public class CachePipelineBehaviour<TDto, TResult> : IPipelineBehavior<TDto, TResult> { private readonly ConcurrentDictionary<TDto, Task<TResult>> _cache; public CachePipelineBehaviour( IConcurrentDictionaryFactory<TDto, Task<TResult>> cacheFactory) { _cache = cacheFactory.Create(); } public async Task<TResult> Handle(TDto request, CancellationToken cancellationToken, RequestHandlerDelegate<TResult> next) => await _cache.GetOrAdd(request, x => next()); }
请求的Dto,取消令牌和RequestHandlerDelegate进入Handle方法。 后者只是其他装饰器和handler'a的后续调用的包装。 MediatR扫描程序集并注册接口本身的所有实现。 要使用它,您需要注入IMediator,并通过Dto对其调用Send方法:
public async Task<IActionResult>([FromBody] Dto dto){ return Ok(mediator.Send(dto)); }
MediatR本身会找到它,找到合适的IRequestHabdler'a实现并应用所有装饰器(除了PipelineBehaviour之外,还有IPreRequestHandler和IPostRequestHandler)
温莎城堡
容器的特征是动态包装器的生成,这就是动态AOP。
实体框架将其用于延迟加载,在属性getter中,将调用ILazyLoader接口的Load方法,该方法通过构造函数的实现注入到实体上所有包装的类中。
要使用包装器的生成来配置容器,您需要创建一个拦截器并注册它
public class CacheInterceptor<TIn, TOut> : IInterceptor { private readonly ConcurrentDictionary<TIn, TOut> _cache; public CacheInterceptor( IConcurrentDictionaryFactory<TIn, TOut> cacheFactory) { _cache = cacheFactory.Create(); } public void Intercept(IInvocation invocation) { var input = (TIn) invocation.Arguments.Single(); if (_cache.TryGetValue(input, out var value)) invocation.ReturnValue = value; else { invocation.Proceed(); _cache.TryAdd(input, (TOut) invocation.ReturnValue); } } }
IInvocation接口提供有关已访问的装饰对象成员的信息;该接口的唯一公共成员是Query方法,因此我们将不检查访问是否已注册到它,没有其他选项。
如果高速缓存中存在带有此类键的对象,则填写该方法的返回值(不调用它),否则,请调用Proceed方法,该方法将依次调用修饰的方法并填写ReturnValue。
可以在Githab上查看拦截器注册和完整代码