Globales Zwischenspeichern von Abfrageergebnissen in ASP.NET CORE

Das CQRS-Paradigma in der einen oder anderen Form geht davon aus, dass Abfrageaufrufe den Status der Anwendung nicht ändern. Das heißt, mehrere Aufrufe derselben Abfrage innerhalb derselben Abfrage haben dasselbe Ergebnis.


Lassen Sie alle als Teil der Abfrage verwendeten Schnittstellen vom Typ IQuery oder IAsyncQuery sein:


public interface IQuery<TIn, TOut> { TOut Query(TIn input); } public interface IAsyncQuery<TIn, TOut>: IQuery<TIn, Task<TOut> { } 

Diese Schnittstellen beschreiben den Empfang von Daten vollständig, z. B. den Empfang formatierter Preise unter Berücksichtigung von Rabatten / Boni und allem anderen:


 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; } } 

Pipeline-Schnittstellen


Der Vorteil dieses Ansatzes ist die Einheitlichkeit der Schnittstellen in der Anwendung, die in der Pipeline erstellt werden können:


 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)); } 

Registrieren Sie sich wie folgt:


 serviceCollection.AddScoped(typeof(Aggregate2Query<,,>)); 

Wir bekommen:


 public ProductPriceQuery( BaseAggregateQuery<ProductDto,PriceWithSalesDto,PricePresentationDto> query) { _aggregateQuery = query; } public PricePresentationDto Query(ProductDto dto) => _aggregateQuery.Query(dto); 

Im Idealfall sollte die Programmierung zu einer Baugruppe des Konstruktors werden, aber in Wirklichkeit ist es nur ein schöner Trick, um den Stolz des Programmierers zu befriedigen.


Dekorateure und ASP.NET CORE


Die MediatR-Bibliothek basiert genau auf der Einheitlichkeit der Schnittstellen und der Dekorateure.


Mit Dekorateuren können Sie einige zusätzliche Funktionen an die Standardschnittstelle von IQuery <TIn, TOut> hängen, z. B. die Protokollierung:


 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; } } 

Ich werde die Tatsache weglassen, dass Dekorateure es Ihnen ermöglichen, Querschnittsfunktionen an einer Stelle zu schreiben, anstatt sie im gesamten Programm zu verbreiten. Dies ist nicht Teil des Geltungsbereichs dieses Artikels.


Der von .Net Core bereitgestellte Standard-IoC-Container kann Dekoratoren nicht registrieren. Die Schwierigkeit besteht darin, dass zwei Implementierungen derselben Schnittstelle vorhanden sind: die ursprüngliche Abfrage und der Dekorator sowie dieselbe Schnittstelle, die der Dekorator implementiert, kommen zum Dekoratorkonstruktor. Der Container kann ein solches Diagramm nicht auflösen und gibt einen Fehler "Zirkularabhängigkeit" aus.


Es gibt verschiedene Möglichkeiten, um dieses Problem zu lösen. Insbesondere für den .Net Core-Container ist die Scrutor-Bibliothek geschrieben und kann Dekorateure registrieren:


  services.Decorate(typeof(IQuery<,>), typeof(LoggingQuery<,>)); 

Wenn Sie dem Projekt keine zusätzlichen Abhängigkeiten hinzufügen möchten, können Sie diese Funktionalität selbst schreiben, was ich auch getan habe. Bevor wir den Code demonstrieren, wollen wir das Zwischenspeichern von Abfrageergebnissen als Teil einer Abfrage diskutieren. Wenn Sie Caching hinzufügen müssen und der Schlüssel die Klasse ist, müssen Sie GetHashCode und Equals überschreiben, damit wir den Vergleich durch Referenz entfernen.


Caching-Methoden


Ich werde ein Beispiel für einen einfachen Cache präsentieren:


 //Cache ConcurrentDictionary<Key,Value> _cache { get; } //Key public class Key { //ReSharper-generated code protected bool Equals(Key other) { return Field1 == other.Field1 && Field2 == other.Field2; } //ReSharper-generated code public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != this.GetType()) return false; return Equals((Key) obj); } //ReSharper-generated code public override int GetHashCode() { unchecked { return (Field1 * 397) ^ Field2; } } public int Field1 { get; set; } public int Field2 { get; set; } } //Value irrelevant 

Bei der Suche nach einem Wert wird zuerst die GetHashCode-Methode aufgerufen, um den gewünschten Warenkorb zu finden. Wenn der Warenkorb mehr als ein Element enthält, wird Equals zum Vergleich aufgerufen. Sehen Sie nach, ob Sie nicht ganz verstehen, wie es funktioniert.


ReSharper selbst generiert diese Methoden, aber wir implementieren das Caching global. Der Programmierer, der die IQuery <TIn, TOut> -Schnittstelle und im Allgemeinen die IQuery <TIn, TOut> -Schnittstelle implementiert hat, vergisst SRP nicht. Daher passt die Generierung von Methoden durch den Resharper nicht zu uns.


Wenn wir uns mit End-to-End-Funktionen befassen, helfen AOP-Frameworks. EqualsFody, ein Plugin für Fody, schreibt IL neu und überschreibt Equals und GetHashCode in Klassen, die mit dem EqualsAttribute-Attribut gekennzeichnet sind.


Um nicht jedes Dto mit diesem Attribut zu kennzeichnen, können wir die IQuery-Schnittstelle neu schreiben


 public IQuery<TIn,TOut> where TIn : CachedDto{ } [Equals] public class CachedDto{ } 

Jetzt werden alle Dto definitiv die erforderlichen Methoden neu definieren, und wir müssen nicht jedem Eingabe-DTO ein Attribut hinzufügen (es wird von der Basisklasse übernommen). Wenn das Umschreiben von IL für Sie nicht geeignet ist, implementieren Sie CachedDto folgendermaßen (verwenden Sie den Kontext, in dem die Basisklassenmethoden aufgerufen werden):


 public class CachedDto{ public override bool Equals(object x) => DeepEquals.Equals(this,x); public override int GetHashCode() => DeepHash.GetHashCode(this); } 

DeepEquals.Equals und DeepHash.GetHashCode verwenden Reflection. Es ist langsamer als Fody und für Unternehmensanwendungen nicht schwerwiegend.


Denken Sie jedoch an SRP. IQuery sollte nicht wissen, dass es zwischengespeichert ist.


Die beste Lösung wäre die Implementierung von IEqualityComparer. Dictionary nimmt es in den Konstruktor und verwendet es beim Einfügen / Löschen / Suchen.


  public class EqualityComparerUsingReflection<TKey> : IEqualityComparer<TKey> { public bool Equals(TKey x, TKey y) => DeepEqualsCommonType(x, y); public int GetHashCode(TKey obj) => Hash.GetHashCode(obj); } 

Jetzt können Sie TIn einschränken. Wir haben erreicht, was wir wollten. Schreiben wir einen Caching-Dekorateur:


  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)); } 

Achten Sie auf IConcurrentDictionaryFactory. Das Ziel dieser Factory ist es, eine Instanz des Wörterbuchs bereitzustellen. Warum aber nicht einfach im Konstruktor erstellen?


Erstens, DI und SRP, ist es durchaus möglich, dass Sie eine weitere Vergleichsimplementierung hinzufügen müssen (z. B. einfacher für bestimmte DTO-Typen) oder die Implementierung insgesamt ändern müssen, und zweitens ist eine Situation möglich, in der der Cache aufgrund von Reflexion und Abstraktion langsamer wird undicht. Ich werde Kompromisse eingehen und wenn Equals in Dto überschrieben werden und GetHashCode den "schweren" EqualityComparer nicht verwenden wird.


Der Zweck der Factory besteht darin, zu überprüfen, ob die Methoden überschrieben werden, wenn ja, um ein Standardwörterbuch mit im DTO neu definierten Methoden zurückzugeben, nein - ein Wörterbuch mit Komparator.


Registrierung


Kommen wir zurück zu der Registrierung.


Das services-Argument der ConfigureServices-Methode ist eine Sammlung von ServiceDescriptors. Jeder Deskriptor enthält Informationen zur registrierten Abhängigkeit


 public class ServiceDescriptor{ // other methods /// <inheritdoc /> public ServiceLifetime Lifetime { get; } /// <inheritdoc /> public Type ServiceType { get; } /// <inheritdoc /> public Type ImplementationType { get; } /// <inheritdoc /> public object ImplementationInstance { get; } /// <inheritdoc /> public Func<IServiceProvider, object> ImplementationFactory { get; } // other methods } 

Daher wird der Servicesammlung ein neuer ServiceDescriptor mit LifeTime = Scoped hinzugefügt.
ServiceType = typeof (IService), ImplementType = typeof (Service):


 services.AddScoped<IService,Service>(). 

Mit der ImplementationFactory-Eigenschaft können Sie angeben, wie die Abhängigkeit erstellt werden soll. Wir werden sie verwenden. Ich werde eine Erweiterung für IServiceCollection schreiben, die alle IQuery und IAsyncQuery in Assemblys findet, Dekorateure aufhängt und sich registriert.


  public static void AddCachedQueries(this IServiceCollection serviceCollection) { // Func<Type,bool>    IAsyncQuery var asyncQueryScanPredicate = AggregatePredicates( IsClass, ContainsAsyncQueryInterface); // Func<Type,bool>     IQuery var queryScanAssemblesPredicate =AggregatePredicates( IsClass, x => !asyncQueryScanPredicate(x), ContainsQueryInterface); //    IAsyncQuery    var asyncQueries = GetAssemblesTypes( asyncQueryScanPredicate, DestAsyncQuerySourceType); //    IQuery    var queries = GetAssemblesTypes( queryScanAssemblesPredicate, DestQuerySourceType); //   ConcurrentDictionary serviceCollection.AddScoped( typeof(IConcurrentDictionaryFactory<,>), typeof(ConcDictionaryFactory<,>)); //   services ServiceDescriptor'   IAsyncQuery serviceCollection.QueryDecorate(asyncQueries, typeof(AsyncQueryCache<,>)); //   services ServiceDescriptor'   IQuery serviceCollection.QueryDecorate(queries, typeof(QueryCache<,>)); } private static void QueryDecorate(this IServiceCollection serviceCollection, IEnumerable<(Type source, Type dest)> parameters, Type cacheType, ServiceLifetime lifeTime = ServiceLifetime.Scoped) { foreach (var (source, dest) in parameters) serviceCollection.AddDecorator( cacheType.MakeGenericType(source.GenericTypeArguments), source, dest, lifeTime); } private static void AddDecorator( this IServiceCollection serviceCollection, Type cacheType, Type querySourceType, Type queryDestType, ServiceLifetime lifetime = ServiceLifetime.Scoped) { //ReSharper disable once ConvertToLocalFunction Func<IServiceProvider, object> factory = provider => ActivatorUtilities.CreateInstance(provider, cacheType, ActivatorUtilities.GetServiceOrCreateInstance(provider, queryDestType)); serviceCollection.Add( new ServiceDescriptor(querySourceType, factory, lifetime)); } } 

Die AddDecorator-Methode verdient besondere Aufmerksamkeit. Hier verwenden wir die statischen Methoden der ActivatorUtilities-Klasse. ActivatorUtilities.CreateInstance akzeptiert IServiceProvider, den Typ des zu erstellenden Objekts und die Abhängigkeitsinstanzen, die dieses Objekt im Konstruktor akzeptiert (Sie können nur diejenigen angeben, die nicht registriert sind, der Rest wird vom Anbieter zugelassen).


ActivatorUtilities.GetServiceOrCreateInstance - macht dasselbe, erlaubt jedoch nicht, dass fehlende Abhängigkeiten an den Konstruktor des erstellten Objekts übergeben werden. Wenn das Objekt im Container registriert ist, erstellt es es einfach (oder gibt das bereits erstellte zurück). Wenn nicht, erstellt es das Objekt, sofern es alle seine Abhängigkeiten lösen kann


Auf diese Weise können Sie eine Funktion erstellen, die ein Cache-Objekt zurückgibt, und einen Deskriptor hinzufügen, der diese Registrierung für Dienste beschreibt.


Schreiben wir einen Test:


 public class DtoQuery : IQuery<Dto, Something> { private readonly IRepository _repository; public DtoQuery(IRepository repository) { _repository = repository; } public Something Query(Dto input) => _repository.GetSomething(); } //    private IQuery<Dto, Something> query { get; set; } public void TwoCallQueryTest() { var dto = new Dto {One = 1}; var dto1 = new Dto {One = 1}; //query -        query.Query(dto); query.Query(dto1); // : services.AddScoped<IRepository>(x => MockRepository.Object) RepositoryMock.Verify(x => x.GetSomething(), Times.Once); } 

ReposityMock - Mock aus der Moq-Bibliothek ist lustig, aber um zu testen, wie oft die GetSomething () -Methode des Repositorys aufgerufen wurde, werden auch Dekoratoren verwendet, die jedoch automatisch mit Castle.Interceptor generiert werden. Wir testen Dekorateure mit Dekorateuren.

Auf diese Weise können Sie das Zwischenspeichern aller IQuery <TIn, TOut> -Ergebnisse hinzufügen. Es ist sehr unpraktisch, so viel Code zu schreiben, um ein wenig Funktionalität zu implementieren.


Andere Lösungen


Mediatr


Zentralbibliotheksschnittstelle:


 public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken); } 

Die Hauptfunktionalität von MediatR besteht darin, Wrapper über einen IRequestHandler hinzuzufügen, z. B. eine Pipeline mithilfe der IPipelineBehavior-Schnittstelle zu implementieren. Auf diese Weise können Sie CachePipelineBehaviour registrieren. Es wird auf alle registrierten IRequestHandler-Schnittstellen angewendet:


 sc.AddScoped(typeof(IPipelineBehavior<,>), typeof(CachePipelineBehaviour<,>)); 

Wir implementieren das Caching 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()); } 

Das Dto der Anforderung, das Stornierungstoken und RequestHandlerDelegate kommen zur Handle-Methode. Letzteres ist nur eine Hülle für die nächsten Anrufe anderer Dekorateure und Handler. MediatR scannt Assemblys und registriert alle Schnittstellenimplementierungen selbst. Um es zu verwenden, müssen Sie IMediator injizieren und die Send-Methode aufrufen, indem Sie Dto übergeben:


 public async Task<IActionResult>([FromBody] Dto dto){ return Ok(mediator.Send(dto)); } 

MediatR selbst wird es finden, die entsprechende Implementierung von IRequestHabdler finden und alle Dekoratoren anwenden (Zusätzlich zu PipelineBehaviour gibt es auch IPreRequestHandler und IPostRequestHandler).


Schloss Windsor


Das Merkmal des Containers ist die Erzeugung dynamischer Wrapper, dies ist dynamisches AOP.


Entity Framework verwendet es für Lazy Loading. Im Property Getter wird die Load-Methode der ILazyLoader-Schnittstelle aufgerufen, die durch die Implementierung des Konstruktors in die Klassen aller Wrapper über Entities eingefügt wird.


Um den Container mit der Generierung von Wrappern zu konfigurieren, müssen Sie einen Interceptor erstellen und registrieren


  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); } } } 

Die IInvocation-Schnittstelle bietet Informationen über das Mitglied des dekorierten Objekts, auf das zugegriffen wurde. Das einzige öffentliche Mitglied der Schnittstelle ist die Abfragemethode. Daher werden wir nicht überprüfen, ob der Zugriff darauf registriert wurde. Es gibt keine anderen Optionen.


Wenn sich ein Objekt mit einem solchen Schlüssel im Cache befindet, geben Sie den Rückgabewert der Methode ein (ohne ihn aufzurufen). Wenn nicht, rufen Sie die Proceed-Methode auf, die wiederum die dekorierte Methode aufruft und ReturnValue ausfüllt.


Die Interceptor-Registrierung und der vollständige Code können auf Githab eingesehen werden

Source: https://habr.com/ru/post/de449744/


All Articles