📜 ⬆️ ⬇️

Global caching of Query results in ASP.NET CORE

In one form or another, the CQRS paradigm implies that calls to Query will not change the state of the application. That is, multiple calls to the same query, within the same query, will have the same result.


Let all interfaces used within the query be of type IQuery or IAsyncQuery:


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

These interfaces fully describe the acquisition of data, for example, the receipt of formatted prices, taking into account discounts / bonuses and everything else:


 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 interfaces


The advantage of this approach is the uniformity of the interfaces in the application, which can be built in the 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)); } 

Register like this:


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

We get:


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

Ideally, programming should turn into an assembly of the designer, but in reality it’s just a pretty thing to satisfy the programmer’s pride.


Decorators and ASP.NET CORE


MediatR library is built just on the uniformity of interfaces and decorators.


Decorators allow you to hang on the standard interface IQuery <TIn, TOut> some additional functions, such as logging:


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

I will omit the fact that decorators allow you to write cross-cutting functionality in one place, and not to spread it all over the program, this is not within the scope of this article.


The standard IoC container provided by .Net Core does not know how to register decorators. The difficulty is that we have two implementations of one interface: the original query and the decorator, and the same interface that the decorator implements comes to the designer of the decorator. The container cannot resolve such a graph and throws the error "circular dependency".


There are several ways to solve this problem. The Scrutor library is written specifically for the .Net Core container, it can register decorators:


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

If you do not want to add unnecessary dependencies to the project, you can write this functionality yourself, which I did. Before demonstrating the code, let's discuss caching Query results within a query. If you need to add caching and the key is a class, you need to override GetHashCode and Equals, so we will get rid of the comparison by reference.


Caching Methods


I will provide an example of a simple cache:


 //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 

When searching for a value, the GetHashCode method is first called to search for the desired basket, and then, if there is more than one item in the basket, Equals is called for comparison. See if you don't quite understand how it works.


ReSharper itself generates these methods, but we implement caching globally, the programmer who implements the IQuery <TIn, TOut> interface and the IQuery <TIn, TOut> interface in general do not need to know, let’s not forget about SRP. Therefore, the generation of resharper methods does not suit us.


When we deal with end-to-end functionality, AOP frameworks come to the rescue. EqualsFody, a plugin for Fody, rewrites IL, overriding Equals and GetHashCode in classes marked with the EqualsAttribute attribute.


In order not to mark every Dto with this attribute, we can slightly rewrite the IQuery interface


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

Now all Dtos will accurately override the necessary methods, and we do not need to add an attribute to each input DTO (it will be picked up from the base class). If overwriting IL does not suit you, implement CachedDto like this (use the context in which the base class methods are called):


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

DeepEquals.Equals and DeepHash.GetHashCode use reflection, it will be slower than Fody, for corporate applications it is not fatal.


But we remember about SRP, IQuery should not know about the fact that it is cached.


The most correct solution would be the implementation of IEqualityComparer. Dictionary accepts it in the constructor and uses it when inserting / deleting / searching.


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

Now you can throw constraint on TIn, we achieved what we wanted. Let's write a caching decorator:


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

Pay attention to IConcurrentDictionaryFactory, the purpose of this factory is to provide a dictionary instance, but why not just create it in the constructor?


First, DI and SRP, it is possible that you will need to add another implementation of the comparator (for example, easier for certain types of DTO) or change the implementation altogether, secondly, it is possible that the cache will start to slow down due to reflection and abstraction will leak. I'll compromise if Equals are redefined in Dto and GetHashCode will not use the “heavy” EqualityComparer.


The goal of the factory is to check whether methods are redefined, if yes, to return a standard dictionary using methods redefined in DTO, no - a dictionary with a comparer.


check in


Let's go back to how to register it all.


The services argument of the ConfigureServices method is a collection of ServiceDescriptors, each descriptor contains information about a registered dependency.


 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 } 

Thus, a new ServiceDescriptor with LifeTime = Scoped is added to the services collection.
ServiceType = typeof (IService), ImplementType = typeof (Service):


 services.AddScoped<IService,Service>(). 

The ImplementationFactory property allows you to specify how to create a dependency, we will use it. I will write an extension to the IServiceCollection, which will find all IQuery and IAsyncQuery in assemblies, hang decorators and register.


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

The AddDecorator method deserves special attention, it uses the static methods of the ActivatorUtilities class. ActivatorUtilities.CreateInstance accepts IServiceProvider, the type of object being created and the dependency instances this object accepts in the constructor (you can specify only those that are not registered, the rest will be resolved by the provider)


ActivatorUtilities.GetServiceOrCreateInstance - does the same thing, but does not allow the missing dependencies to be passed to the constructor of the created object. If an object is registered in a container, then it will simply create it (or return an already created one), if not, create an object, provided that it can solve all its dependencies.


Thus, you can create a function that returns a cache object and add a handle to services describing this registration.


Let's write a 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 from the Moq library, funny, but to test how many times the GetSomething () method of the repository was called, it also uses decorators, though it generates them automatically using Castle.Interceptor. We test decorators using decorators.

This is how you can add caching of all IQuery <TIn, TOut> results, it is very inconvenient to write as much code to implement a small functionality.


Other solutions


MediatR


Central library interface:


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

The main functionality of MediatR is the addition of wrappers over an IRequestHandler, for example, the implementation of a pipeline using the IPipelineBehavior interface, this is how you can register CachePipelineBehaviour, it will be applied to all registered IRequestHandler interfaces:


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

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

Dto request, cancellation token and RequestHandlerDelegate come to the Handle method. The latter is just a wrapper over the following calls from other decorators and handler. MediatR scans assemblies and registers all interface implementations by itself. To use you need to inject IMediator, and call its Send method, passing Dto:


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

MediatR will find a suitable implementation of IRequestHabdler by itself and apply all decorators (Except PipelineBehaviour there are also IPreRequestHandler and IPostRequestHandler)


Castle windsor


The container chip is the generation of dynamic wrappers, this is dynamic AOP.


The Entity Framework uses it for Lazy Loading, in the property getter, the Load method of the ILazyLoader interface is invoked, which is injected into the classes of all the wrappers above the entities through the implementation of the constructor .


To configure a container with generation of wrappers, you need to create an Interceptor and register it


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

The IInvocation interface provides information about the member of the object being decorated to which there was an appeal, the only public member of the interface is the Query method, so we will not check whether the reference was to it, there are no other options.


If there is an object with such a key in the cache, fill in the return value of the method (without calling it), if not, call the Proceed method, which, in turn, will call the decorated method and fill in the ReturnValue.


Interceptor registration and full code can be viewed on Githab


')

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


All Articles