'Is there a way to skip MediatR Pipeline?
I would like to cache some responses from CommandsHandlers.
I Already did this using IPipelineBehaviour, but only 5% of my requests really must have cache, and the other 95% must skip this Pipeline. Is there a way to do that?
Below follows my code.
Thanks!
public class PipelineBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>, IProvideCacheKey
{
private readonly IMemoryCache _cache;
public PipelineBehavior(IMemoryCache cache)
{
_cache = cache;
}
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken,
RequestHandlerDelegate<TResponse> next)
{
// Check in cache if we already have what we're looking for
var cacheKey = request.CacheKey;
if (_cache.TryGetValue<TResponse>(cacheKey, out var cachedResponse))
{
return cachedResponse;
}
// If we don't, execute the rest of the pipeline, and add the result to the cache
var response = await next();
_cache.Set(cacheKey, response);
return response;
}
}
public class GetUserByEmailCommand : Command, IRequest<bool>, IProvideCacheKey
{
public string Email { get; set; }
public string CacheKey => $"{GetType().Name}:{Email}";
public override bool IsValid()
{
ValidationResult = new GetUserByEmailCommandValidation<GetUserByEmailCommand>().Validate(this);
return ValidationResult.IsValid;
}
}
public interface IProvideCacheKey
{
string CacheKey { get; }
}
Solution 1:[1]
You can wrap your caching behavior in a check that is bypassed if the request is not cacheable to let the pipeline continue. In your case, you can probably just check if the request implements your interface at the start of the Handle method:
if (request is IProvideCacheKey)
{
// perform cache behavior, return if cached and terminate the pipeline
}
// else continue the pipeline
There's a couple good examples of this in more detail at:
https://lurumad.github.io/cross-cutting-concerns-in-asp-net-core-with-meaditr
https://anderly.com/2019/12/12/cross-cutting-concerns-with-mediatr-pipeline-behaviors/
Solution 2:[2]
I faced the same problem.
I fixed it by checking in the Pipeline Behaviour Handler if the request implements the interface.
If the request implements the interface ICachable
, that means that the request will go through the cachable logic.
With this change, there is no need to use the where TRequest : IQueryable
.
If you leave this constrain it will throw an error when trying to process a Query or Command that doesn't implement the IQueryable interface.
If you are wondering how to access the interface properties the answer is by creating a variable that implements the interface.
theObject is theInterface newObjectWithInterfaceProperties
.
{
public class CacheBehaviour<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
//where TRequest : ICacheableQuery
{
private readonly IMemoryCache _memoryCache;
private readonly ILogger<CacheBehaviour<TRequest, TResponse>> _logger;
public CacheBehaviour(IMemoryCache memoryCache, ILogger<CacheBehaviour<TRequest, TResponse>> logger)
{
_memoryCache = memoryCache;
_logger = logger;
}
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken,
RequestHandlerDelegate<TResponse> next)
{
//Check if the request implements ICachableQuery
//If it does then it creates a cachableRequest variable that will contain the properties of the ICachableQuery interface.
if (request is ICachableQuery cachableRequest)
{
var requestName = request.GetType().Name;
_logger.LogInformation($"Request : {requestName} is configured to cache");
TResponse response;
if(_memoryCache.TryGetValue(cachableRequest.CacheKey, out response))
{
_logger.LogInformation($"Request: {requestName} returning response from cache");
return response;
}
response = await next();
_logger.LogInformation($"Request: {requestName} returning response from DB");
_memoryCache.Set(cachableRequest.CacheKey, response);
return response;
}
return await next();
}
}
}
Solution 3:[3]
Instead of skipping behaviors, it may be better to construct multiple pipelines with different behaviors.
Register pipeline behaviors:
// pipeline 1's behaviors
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(FooBehavior<,>));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(BarBehavior<,>));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(BazBehavior<,>));
// pipeline 2's behaviors
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(CatBehavior<,>));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(DogBehavior<,>));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(PigBehavior<,>));
Define pipelines:
// pipeline 1
public interface IPipeline1 { }
public class FooBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IPipeline1 { }
public class BarBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IPipeline1 { }
public class BazBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IPipeline1 { }
// pipeline 2
public interface IPipeline2 { }
public class CatBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IPipeline2 { }
public class DogBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IPipeline2 { }
public class PigBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : IPipeline2 { }
Define requests:
// requests to be processed by pipeline 1
public class ARequest : IRequest, IPipeline1 { }
public class BRequest : IRequest, IPipeline1 { }
public class CRequest : IRequest, IPipeline1 { }
// requests to be processed by pipeline 2
public class XRequest : IRequest, IPipeline2 { }
public class YRequest : IRequest, IPipeline2 { }
public class ZRequest : IRequest, IPipeline2 { }
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Adam |
Solution 2 | marc_s |
Solution 3 | lonix |