O paradigma CQRS, de uma forma ou de outra, pressupõe que as chamadas de consulta não alterem o estado do aplicativo. Ou seja, várias chamadas para a mesma consulta na mesma consulta terão o mesmo resultado.
Permita que todas as interfaces usadas como parte da consulta sejam do tipo IQuery ou IAsyncQuery:
public interface IQuery<TIn, TOut> { TOut Query(TIn input); } public interface IAsyncQuery<TIn, TOut>: IQuery<TIn, Task<TOut> { }
Essas interfaces descrevem completamente o recebimento de dados, por exemplo, o recebimento de preços formatados, levando em consideração descontos / bônus e tudo mais:
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; } }
Interfaces de pipeline
A vantagem dessa abordagem é a uniformidade das interfaces no aplicativo, que podem ser construídas no pipeline:
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)); }
Registre-se assim:
serviceCollection.AddScoped(typeof(Aggregate2Query<,,>));
Temos:
public ProductPriceQuery( BaseAggregateQuery<ProductDto,PriceWithSalesDto,PricePresentationDto> query) { _aggregateQuery = query; } public PricePresentationDto Query(ProductDto dto) => _aggregateQuery.Query(dto);
Idealmente, a programação deve se transformar em uma montagem do construtor, mas, na realidade, é apenas um recurso bonito para satisfazer o orgulho do programador.
Decoradores e ASP.NET CORE
A biblioteca MediatR é construída precisamente na uniformidade das interfaces e nos decoradores.
Os decoradores permitem pendurar algumas funções adicionais na interface padrão do IQuery <TIn, TOut>, por exemplo, log:
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; } }
Omitirei o fato de que os decoradores permitem que você escreva funcionalidades transversais em um só lugar, em vez de se espalharem por todo o programa, isso não faz parte do escopo deste artigo.
O contêiner IoC padrão fornecido pelo .Net Core não pode registrar decoradores. A dificuldade é que temos duas implementações da mesma interface: a consulta original e o decorador, e a mesma interface que o decorador implementa chega ao construtor do decorador. O contêiner não pode resolver esse gráfico e gera um erro de "dependência circular".
Existem várias maneiras de resolver esse problema: especialmente para o contêiner .Net Core, a biblioteca Scrutor é gravada, ela pode registrar decoradores:
services.Decorate(typeof(IQuery<,>), typeof(LoggingQuery<,>));
Se você não deseja adicionar dependências extras ao projeto, pode escrever essa funcionalidade você mesmo, o que eu fiz. Antes de demonstrar o código, vamos discutir o cache dos resultados da consulta como parte de uma consulta. Se você precisar adicionar armazenamento em cache e a chave for a classe, substitua GetHashCode e Equals, para que possamos nos livrar da comparação por referência.
Métodos de armazenamento em cache
Vou apresentar um exemplo de um cache simples:
Ao procurar um valor, o método GetHashCode é chamado primeiro para localizar a cesta desejada e, em seguida, se a cesta tiver mais de um elemento, Igual será chamado para comparação. Veja se você não entende bem como isso funciona.
O próprio ReSharper gera esses métodos, mas implementamos o cache globalmente, o programador que implementou a interface IQuery <TIn, TOut> e, em geral, a interface IQuery <TIn, TOut>, não se esqueça do SRP. Portanto, a geração de métodos pelo resharper não nos convém.
Quando lidamos com a funcionalidade de ponta a ponta, as estruturas de AOP vêm em socorro. EqualsFody, um plug-in para Fody, reescreve IL, substituindo Equals e GetHashCode nas classes marcadas com o atributo EqualsAttribute.
Para não sinalizar cada Dto com esse atributo, podemos reescrever um pouco a interface IQuery
public IQuery<TIn,TOut> where TIn : CachedDto{ } [Equals] public class CachedDto{ }
Agora, todo o Dto definitivamente redefinirá os métodos necessários, e não precisamos adicionar um atributo a cada DTO de entrada (ele será escolhido na classe base). Se a reescrita da IL não for adequada para você, implemente o CachedDto assim (use o contexto no qual os métodos da classe base são chamados):
public class CachedDto{ public override bool Equals(object x) => DeepEquals.Equals(this,x); public override int GetHashCode() => DeepHash.GetHashCode(this); }
DeepEquals.Equals e DeepHash.GetHashCode usam reflexão, será mais lento que Fody, não é fatal para aplicativos corporativos.
Mas lembre-se do SRP, o IQuery não deve saber que está armazenado em cache.
A melhor solução seria implementar o IEqualityComparer. O dicionário o pega no construtor e o utiliza ao inserir / excluir / pesquisar.
public class EqualityComparerUsingReflection<TKey> : IEqualityComparer<TKey> { public bool Equals(TKey x, TKey y) => DeepEqualsCommonType(x, y); public int GetHashCode(TKey obj) => Hash.GetHashCode(obj); }
Agora você pode colocar restrições no TIn, conseguimos o que queríamos. Vamos escrever um decorador de cache:
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)); }
Preste atenção ao IConcurrentDictionaryFactory, o objetivo desta fábrica é fornecer uma instância do dicionário, mas por que não criá-lo no construtor?
Em primeiro lugar, DI e SRP, é bem possível que você precise adicionar outra implementação de comparador (por exemplo, mais fácil para certos tipos de DTO) ou alterar completamente a implementação. Em segundo lugar, uma situação é possível quando o cache começa a ficar lento devido à reflexão e abstração vazando. Vou comprometer e se Equals forem substituídos no Dto e GetHashCode não usará o EqualityComparer "pesado".
O objetivo da fábrica é verificar se os métodos são substituídos, se houver, para retornar um dicionário padrão usando métodos redefinidos no DTO, não - um dicionário com comparador.
Registo
Vamos voltar a como registrar tudo isso.
O argumento de serviços do método ConfigureServices é uma coleção de ServiceDescriptors, cada descritor contém informações sobre a dependência registrada
public class ServiceDescriptor{
Assim, um novo ServiceDescriptor com LifeTime = Scoped é adicionado à coleção de serviços.
ServiceType = typeof (IService), ImplementType = typeof (Service):
services.AddScoped<IService,Service>().
A propriedade ImplementationFactory permite especificar como criar a dependência; nós a usaremos. Escreverei uma extensão para IServiceCollection, que encontrará todo o IQuery e IAsyncQuery em assemblies, travar decoradores e registrar.
public static void AddCachedQueries(this IServiceCollection serviceCollection) {
O método AddDecorator merece atenção especial, aqui usamos os métodos estáticos da classe ActivatorUtilities. ActivatorUtilities.CreateInstance aceita IServiceProvider, o tipo de objeto a ser criado e as instâncias de dependência que esse objeto aceita no construtor (você pode especificar apenas aqueles que não estão registrados, o restante será permitido pelo provedor)
ActivatorUtilities.GetServiceOrCreateInstance - faz a mesma coisa, mas não permite que as dependências ausentes sejam passadas para o construtor do objeto criado. Se o objeto estiver registrado no contêiner, ele simplesmente o criará (ou retornará o já criado); caso contrário, ele criará o objeto, desde que ele possa resolver todas as suas dependências
Assim, você pode criar uma função que retorna um objeto de cache e adicionar um descritor que descreve esse registro aos serviços.
Vamos escrever um teste:
public class DtoQuery : IQuery<Dto, Something> { private readonly IRepository _repository; public DtoQuery(IRepository repository) { _repository = repository; } public Something Query(Dto input) => _repository.GetSomething(); }
ReposityMock - O mock da biblioteca Moq é engraçado, mas para testar quantas vezes o método GetSomething () do repositório foi chamado, ele também usa decoradores, embora os gere automaticamente usando o Castle.Interceptor. Testamos decoradores usando decoradores.
É assim que você pode adicionar o cache de todos os resultados do IQuery <TIn, TOut>; é muito inconveniente escrever tanto código para implementar um pouco de funcionalidade.
Outras soluções
Mediatr
Interface central da biblioteca:
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken); }
A principal funcionalidade do MediatR é adicionar wrappers em um IRequestHandler, por exemplo, implementar um pipeline usando a interface IPipelineBehavior, é assim que você pode registrar o CachePipelineBehaviour, que será aplicado a todas as interfaces registradas do IRequestHandler:
sc.AddScoped(typeof(IPipelineBehavior<,>), typeof(CachePipelineBehaviour<,>));
Implementamos o cache 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()); }
O Dto da solicitação, o token de cancelamento e RequestHandlerDelegate chegam ao método Handle. Este último é apenas um invólucro durante as próximas chamadas de outros decoradores e manipuladores. O MediatR varre os assemblies e registra todas as implementações da própria interface. Para usar, você precisa injetar o IMediator e chamar o método Send, passando Dto:
public async Task<IActionResult>([FromBody] Dto dto){ return Ok(mediator.Send(dto)); }
O próprio MediatR o encontrará, encontrará a implementação apropriada do IRequestHabdler e aplicará todos os decoradores (além do PipelineBehaviour, também existem IPreRequestHandler e IPostRequestHandler)
Castelo windsor
O recurso do contêiner é a geração de wrappers dinâmicos, isso é AOP dinâmico.
O Entity Framework usa-o para Lazy Loading, no getter de propriedades, o método Load da interface ILazyLoader é chamado, que é injetado nas classes de todos os wrappers nas entidades por meio da implementação do construtor .
Para configurar o contêiner com a geração de wrappers, você precisa criar um Interceptor e registrá-lo
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); } } }
A interface IInvocation fornece informações sobre o membro do objeto decorado que foi acessado; o único membro público da interface é o método Query, portanto, não verificaremos se o acesso foi registrado a ela, não há outras opções.
Se houver um objeto com essa chave no cache, preencha o valor de retorno do método (sem chamá-lo); caso contrário, chame o método Proceed, que, por sua vez, chamará o método decorado e preencha ReturnValue.
O registro do interceptador e o código completo podem ser visualizados no Githab