Le paradigme CQRS sous une forme ou une autre suppose que les appels de requĂȘte ne changeront pas l'Ă©tat de l'application. C'est-Ă -dire que plusieurs appels Ă la mĂȘme requĂȘte dans la mĂȘme requĂȘte auront le mĂȘme rĂ©sultat.
Que toutes les interfaces utilisĂ©es dans le cadre de la requĂȘte soient de type IQuery ou IAsyncQuery:
public interface IQuery<TIn, TOut> { TOut Query(TIn input); } public interface IAsyncQuery<TIn, TOut>: IQuery<TIn, Task<TOut> { }
Ces interfaces décrivent en détail la réception de données, par exemple, la réception de prix formatés en tenant compte des remises / bonus et tout le reste:
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
L'avantage de cette approche est l'uniformitĂ© des interfaces dans l'application, qui peuvent ĂȘtre construites dans le 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)); }
Inscrivez-vous comme ceci:
serviceCollection.AddScoped(typeof(Aggregate2Query<,,>));
Nous obtenons:
public ProductPriceQuery( BaseAggregateQuery<ProductDto,PriceWithSalesDto,PricePresentationDto> query) { _aggregateQuery = query; } public PricePresentationDto Query(ProductDto dto) => _aggregateQuery.Query(dto);
Idéalement, la programmation devrait se transformer en un assemblage du constructeur, mais en réalité c'est juste une belle astuce pour satisfaire la fierté du programmeur.
Décorateurs et ASP.NET CORE
La bibliothÚque MediatR est construite précisément sur l'uniformité des interfaces et sur les décorateurs.
Les décorateurs vous permettent de suspendre certaines fonctions supplémentaires sur l'interface standard IQuery <TIn, TOut>, par exemple la journalisation:
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; } }
Je vais omettre le fait que les décorateurs vous permettent d'écrire des fonctionnalités transversales en un seul endroit, plutÎt que de les diffuser dans le programme, cela ne fait pas partie de la portée de cet article.
Le conteneur IoC standard fourni par .Net Core ne peut pas enregistrer les dĂ©corateurs. La difficultĂ© est que nous avons deux implĂ©mentations de la mĂȘme interface: la requĂȘte d'origine et le dĂ©corateur, et la mĂȘme interface que le dĂ©corateur implĂ©mente vient au constructeur du dĂ©corateur. Le conteneur ne peut pas rĂ©soudre un tel graphique et gĂ©nĂšre une erreur de "dĂ©pendance circulaire".
Il existe plusieurs façons de résoudre ce problÚme. En particulier pour le conteneur .Net Core, la bibliothÚque Scrutor est écrite, elle peut enregistrer des décorateurs:
services.Decorate(typeof(IQuery<,>), typeof(LoggingQuery<,>));
Si vous ne souhaitez pas ajouter de dĂ©pendances supplĂ©mentaires au projet, vous pouvez Ă©crire cette fonctionnalitĂ© vous-mĂȘme, ce que j'ai fait. Avant de dĂ©montrer le code, discutons de la mise en cache des rĂ©sultats de la requĂȘte dans le cadre d'une requĂȘte. Si vous devez ajouter la mise en cache et que la clĂ© est la classe, vous devez remplacer GetHashCode et Equals, nous nous dĂ©barrasserons donc de la comparaison par rĂ©fĂ©rence.
Méthodes de mise en cache
Je vais vous présenter un exemple de cache simple:
Lors de la recherche d'une valeur, la méthode GetHashCode est d'abord appelée pour trouver le panier souhaité, puis, si le panier contient plusieurs éléments, Equals est appelé pour comparaison. Voyez si vous ne comprenez pas trÚs bien comment cela fonctionne.
ReSharper gĂ©nĂšre lui-mĂȘme ces mĂ©thodes, mais nous implĂ©mentons la mise en cache globalement, le programmeur qui a implĂ©mentĂ© l'interface IQuery <TIn, TOut>, et en gĂ©nĂ©ral l'interface IQuery <TIn, TOut>, n'oublions pas SRP. Par consĂ©quent, la gĂ©nĂ©ration de mĂ©thodes par le resharper ne nous convient pas.
Lorsque nous traitons des fonctionnalités de bout en bout, les frameworks AOP viennent à la rescousse. EqualsFody, un plugin pour Fody, réécrit IL, remplaçant Equals et GetHashCode dans les classes marquées avec l'attribut EqualsAttribute.
Afin de ne pas marquer chaque Dto avec cet attribut, nous pouvons réécrire un peu l'interface IQuery
public IQuery<TIn,TOut> where TIn : CachedDto{ } [Equals] public class CachedDto{ }
Désormais, tous les Dto vont définitivement redéfinir les méthodes nécessaires, et nous n'avons pas besoin d'ajouter un attribut à chaque DTO d'entrée (il sera repris de la classe de base). Si la réécriture d'IL ne vous convient pas, implémentez CachedDto comme ceci (utilisez le contexte dans lequel les méthodes de classe de base sont appelées):
public class CachedDto{ public override bool Equals(object x) => DeepEquals.Equals(this,x); public override int GetHashCode() => DeepHash.GetHashCode(this); }
DeepEquals.Equals et DeepHash.GetHashCode utilisent la réflexion, il sera plus lent que Fody, il n'est pas fatal pour les applications d'entreprise.
Mais souvenez-vous de SRP, IQuery ne doit pas savoir qu'il est mis en cache.
La meilleure solution serait d'implémenter IEqualityComparer. Le dictionnaire le prend dans le constructeur et l'utilise lors de l'insertion / suppression / recherche.
public class EqualityComparerUsingReflection<TKey> : IEqualityComparer<TKey> { public bool Equals(TKey x, TKey y) => DeepEqualsCommonType(x, y); public int GetHashCode(TKey obj) => Hash.GetHashCode(obj); }
Maintenant, vous pouvez lever la contrainte sur TIn, nous avons atteint ce que nous voulions. Ăcrivons un dĂ©corateur de mise en 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)); }
Faites attention à IConcurrentDictionaryFactory, le but de cette fabrique est de fournir une instance du dictionnaire, mais pourquoi ne pas simplement le créer dans le constructeur?
PremiÚrement, DI et SRP, il est tout à fait possible que vous ayez besoin d'ajouter une autre implémentation de comparateur (par exemple, plus facile pour certains types de DTO) ou de changer complÚtement l'implémentation, et deuxiÚmement, une situation est possible lorsque le cache commence à ralentir en raison de la réflexion et de l'abstraction fuite. Je vais faire des compromis et si Equals sont remplacés dans Dto et GetHashCode n'utilisera pas le "heavy" EqualityComparer.
Le but de l'usine est de vérifier si les méthodes sont remplacées, si c'est le cas, pour retourner un dictionnaire standard en utilisant des méthodes redéfinies dans le DTO, non - un dictionnaire avec comparateur.
Inscription
Revenons à la façon d'enregistrer tout cela.
L'argument services de la méthode ConfigureServices est une collection de ServiceDescriptors, chaque descripteur contient des informations sur la dépendance enregistrée
public class ServiceDescriptor{
Ainsi, un nouveau ServiceDescriptor avec LifeTime = Scoped est ajouté à la collection de services.
ServiceType = typeof (IService), ImplementType = typeof (Service):
services.AddScoped<IService,Service>().
La propriété ImplementationFactory vous permet de spécifier comment créer la dépendance, nous allons l'utiliser. J'écrirai une extension à IServiceCollection, qui trouvera tous les IQuery et IAsyncQuery dans les assemblages, suspendra les décorateurs et s'enregistrera.
public static void AddCachedQueries(this IServiceCollection serviceCollection) {
La méthode AddDecorator mérite une attention particuliÚre, nous utilisons ici les méthodes statiques de la classe ActivatorUtilities. ActivatorUtilities.CreateInstance accepte IServiceProvider, le type d'objet à créer et les instances de dépendance que cet objet accepte dans le constructeur (vous pouvez spécifier uniquement celles qui ne sont pas enregistrées, le reste sera autorisé par le fournisseur)
ActivatorUtilities.GetServiceOrCreateInstance - fait la mĂȘme chose, mais ne permet pas de transmettre les dĂ©pendances manquantes au constructeur de l'objet créé. Si l'objet est enregistrĂ© dans le conteneur, il le crĂ©era simplement (ou retournera celui dĂ©jĂ créé), sinon, il crĂ©era l'objet, Ă condition qu'il puisse rĂ©soudre toutes ses dĂ©pendances
Ainsi, vous pouvez créer une fonction qui renvoie un objet cache et ajouter un descripteur décrivant cet enregistrement aux services.
Ăcrivons un test:
public class DtoQuery : IQuery<Dto, Something> { private readonly IRepository _repository; public DtoQuery(IRepository repository) { _repository = repository; } public Something Query(Dto input) => _repository.GetSomething(); }
ReposityMock - Mock de la bibliothÚque Moq est drÎle, mais pour tester combien de fois la méthode GetSomething () du référentiel a été appelée, il utilise également des décorateurs, bien qu'il les génÚre automatiquement à l'aide de Castle.Interceptor. Nous testons les décorateurs à l'aide de décorateurs.
C'est ainsi que vous pouvez ajouter la mise en cache de tous les rĂ©sultats IQuery <TIn, TOut>, il est trĂšs gĂȘnant d'Ă©crire autant de code pour implĂ©menter un peu de fonctionnalitĂ©.
Autres solutions
Mediatr
Interface de la bibliothĂšque centrale:
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken); }
La principale fonctionnalité de MediatR consiste à ajouter des wrappers sur un IRequestHandler, par exemple, en implémentant un pipeline à l'aide de l'interface IPipelineBehavior, voici comment vous pouvez enregistrer CachePipelineBehaviour, il sera appliqué à toutes les interfaces IRequestHandler enregistrées:
sc.AddScoped(typeof(IPipelineBehavior<,>), typeof(CachePipelineBehaviour<,>));
Nous implémentons le 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()); }
Le Dto de la demande, le jeton d'annulation et RequestHandlerDelegate arrivent Ă la mĂ©thode Handle. Ce dernier est juste un wrapper sur les prochains appels d'autres dĂ©corateurs et handler'a. MediatR scanne les assemblages et enregistre toutes les implĂ©mentations de l'interface elle-mĂȘme. Pour l'utiliser, vous devez injecter IMediator et appeler la mĂ©thode Send en passant Dto:
public async Task<IActionResult>([FromBody] Dto dto){ return Ok(mediator.Send(dto)); }
MediatR lui-mĂȘme le trouvera, trouvera une implĂ©mentation appropriĂ©e d'IRequestHabdler'a et appliquera tous les dĂ©corateurs (en plus de PipelineBehaviour, il existe Ă©galement IPreRequestHandler et IPostRequestHandler)
ChĂąteau de Windsor
La caractéristique du conteneur est la génération de wrappers dynamiques, c'est AOP dynamique.
Entity Framework l'utilise pour le chargement différé, dans le getter de propriété, la méthode Load de l'interface ILazyLoader est appelée, qui est injectée dans les classes de tous les wrappers sur les entités via l' implémentation du constructeur .
Pour configurer le conteneur avec la génération de wrappers, vous devez créer un intercepteur et l'enregistrer
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); } } }
L'interface IInvocation fournit des informations sur le membre de l'objet décoré qui a été consulté; le seul membre public de l'interface est la méthode Query, nous ne vérifierons donc pas que l'accÚs y a été enregistré, il n'y a pas d'autres options.
S'il y a un objet avec une telle clé dans le cache, remplissez la valeur de retour de la méthode (sans l'appeler), sinon, appelez la méthode Proceed, qui, à son tour, appellera la méthode décorée et remplira ReturnValue.
L'enregistrement des intercepteurs et le code complet peuvent ĂȘtre consultĂ©s sur Githab