jasontaylordev / NorthwindTraders

Northwind Traders is a sample application built using ASP.NET Core and Entity Framework Core.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Caching

andrew1555 opened this issue · comments

Hi

Which layers would be responsible for implementing caching, I was thinking I would create a "ICacheManager" in application and then implement this in Infrastructure? Then I could cache the queries into memory to save on repeated database requests.

Also where would you place all the standard utils code like ToCsv, Hashing, Serialisation etc. I was thinking to create interfaces again for all of these in application and implement in Infra layer, then the commands/queries can use these tools in a safe way

Any advice would be greatly appreciated.

Thanks

Nice question. As always it depends on your needs.

Perfect solution imo? Setup caching at Api Gateway.

Here? There are several decent solutions:

  • Simplest? Use cache in your cqrs. Interface for caching in app layer and Implementation in infrastructure. Then you can do something like this:
public async Task<IList<LocationDto>> Handle(SearchLocationsQuery request)
    {
      return await _cache.GetOrSetData(
        $"maps:search#{request.Search}",
        TimeSpan.FromDays(14),
        async () => await _mapsService.Search(request.Search));
    }
  • Boring? Cache at controller level with asp.net core annotations.
  • Fancy? Caching entire queries is an interesting idea. I like it. I just don't know where should you put it. I'll probably consider adding this to mediatr pipeline somehow or wrap mediatr into something else. But sometimes you want to cache only part of commands. Then you'll need maybe some extra markers like ICachingQuery instead of IRequest to distinguish between cachable and noncachable queries.
  • Maybe you don't need cache yet? It's easy to mess up this. I hate dealing with cache.

For utils I'll probably keep them in Common project as extensions or (static?) classes.

Thank you for your reply. I liked the idea of caching the handlers like you suggested.

I added a new ICacheRequest interface and new behaviour into the mediator pipeline. So now before the handler for the query is executed the cache is checked first. Seems to work really well.

This is the Pipeline Behaviour code. The Implementation for ICacheManager is external and part of Infrastructure.

public class RequestCachedBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : ICachedRequest<TResponse>
    {
        private readonly ICacheManager _cacheManager;
        
        public RequestCachedBehaviour(ICacheManager cacheManager)
        {
            _cacheManager = cacheManager;
        }
        
        public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
        {
            var cacheKey = request.GetCacheKey();

            var response = await _cacheManager.GetOrCreateAsync(cacheKey, async () => await next(), 60);
            return await response;
        }
    }

Great discussion / implementation. It would be good to see this example expanded further to support caching for specific queries with specified a duration.

There's an issue, with the provided code in asp-net-core2.2.
As Jimmy Bogard provided in this issue

ASP.NET Core DI does not support constrained open generics

So you might end up switching IoC containers for those, which support it.
See the list of supported containers

@andrew1555, do you use third-party IoC and if not, how did you surpass that limitaion?

@LuridSNK Hi, I use autofac for my DI so didn't encounter this issue.

@jasontaylordev

It would be good to see this example expanded further to support caching for specific queries with specified a duration.

So, I was working recently to make a configurable caching within Clean Architecture, without using unnecessary fields in IRequest implementations. I was inspired by EntityFramework & FluentValidation implementations when I decided to make a raw prototype.
Here's what I've managed to do:

  • The base class to inherit and create specific configuration
// we implement empty 'holistic' interface ICacheConfiguration for DI purposes
/ *note: cached object must implement ICachedEntity 'holistic' interface to 
get into the RequestPipeline, since there are always objects 
that developers might decide not to cache */
public class CacheConfiguration<TTarget> : ICacheConfiguration 
   where TTarget : class, ICachedEntity
   {
       // we have an expiry configured as well as key building event, so we could get keys in runtime
       internal TimeSpan Lifetime;
       internal Func<TTarget, string> Builder;

       public void WithKey(Func<TTarget, string> keyConfiguring) => 
           Builder = keyConfiguring; 

       public void WithLifetime(TimeSpan lifetime) => Lifetime = lifetime;
   }
  • The configuration factory:
public class CacheKeyFactory
    {
        private readonly IServiceProvider _sp;

        public CacheKeyFactory(IServiceProvider sp) => _sp = sp;

        public CacheKey GetKeyFor<TTarget>(TTarget target) where TTarget : class, ICachedEntity
        {
            if (!(_sp.GetService<ICacheConfiguration>() is CacheConfiguration<TTarget> config))
            {
                throw new ArgumentNullException($"CacheConfiguration<{typeof(TTarget).Name}>");
            }

            return new CacheKey(config.Builder(target), config.Lifetime);
        }
    }
  • NetCore DI Extension method:
 public static class CacheKeyConfigurationExtensions
    {
        public static IServiceCollection AddKeyConfigurationFromAssemblyContaining<T>(this IServiceCollection services)
        {
            var types = typeof(T).Assembly.GetTypes().Where(t => t.IsClass && !t.IsAbstract && typeof(ICacheConfiguration).IsAssignableFrom(t));
            foreach (var type in types)
            {
                services.TryAdd(ServiceDescriptor.Scoped(typeof(ICacheConfiguration), type));
            }

            services.TryAddSingleton<CacheKeyFactory>();

            return services;
        }
    }
  • Then the PipelineBehaviour implementation, that filters only ICachedEntity:
public class RequestCacheBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
        where TRequest : class, ICachedEntity
    {
        private readonly ICache _cache;
        private readonly CacheKeyFactory _factory;

        public RequestCacheBehaviour(ICache cache, CacheKeyFactory factory)
        {
            _cache = cache;
            _factory = factory;
        }

        public async Task<TResponse> Handle(
            TRequest request,
            CancellationToken cancellationToken,
            RequestHandlerDelegate<TResponse> next)
        {
            var cacheKey = _factory.GetKeyFor(request);
            var response = await _cache.GetAsync<TResponse>(cacheKey.Value).ConfigureAwait(false) ?? (TResponse) await next().ConfigureAwait(false);
            await _cache.AddAsync(cacheKey.Value, response, cacheKey.Lifetime).ConfigureAwait(false);
            return response;
        }
    }

Please note, that this is a raw prototype, and can be greatly improved.
For example, to create a configuration store to stop using ServiceProdiver for object allocations and use Expression API instead of raw functions, to use dynamic method invoke.

Thank you for your interest in this project. This repository has been archived and is no longer actively maintained or supported. We appreciate your understanding. Feel free to explore the codebase and adapt it to your own needs if it serves as a useful reference. If you have any further questions or concerns, please refer to the README for more information.