These extensions add caching behaviors to MediatR that can be quickly and easily dropped into a new or existing pipeline.
Add a reference to Imprise.MediatR.Extensions.Caching
to the project that configures and initialises
MediatR.
NOTE: Ensure you have configured implementations for either
IMemoryCache
orIDistributedMemoryCache
configured. If you have an ASP.NET Core 2.0 or greater web application and you are callingservices.AddMvc()
in yourConfigureServices
method then there is a good chance both of these have been configured with default in-memory implementations already.Otherwise, simple add
services.AddMemoryCache()
andservices.AddDistrubutedMemoryCache()
to get started.
For any IRequest<TResponse>
requests you wish to cache, create a new class using this pattern:
public class PingCache : DistributedCache<Ping, Pong>
{
protected override TimeSpan? SlidingExpiration => TimeSpan.FromMinutes(30);
public PingCache(IDistributedCache distributedCache) : base(distributedCache)
{
}
protected override string GetCacheKeyIdentifier(Ping request)
{
return request.Message.ToString();
}
}
These classes inherit from either the DistributedCache
or MemoryCache
abstract
base classes which wrap the
corresponding cache type implementation to cache your requests response.
The generic parameters TRequest
and TResponse
are the request type (Ping
in this case) and the respons
type the request returns to be cached (Pong
).
You must override GetCacheKeyIdentifier
even if it returns null or an empty string. The key identifier is used by the
base implementation to separate requests in the cache. For example, if you want to cache a user profile response from a
GetUser
request, you could return the user ID to ensure each user profile is cached separately.
In the example above, every Message
on the Ping
class that is different will be cached.
You can also optionally override one of SlidingExpiration
, AbsoluteExpiration
and AbsoluteExpirationRelativeToNow
.
These will be passed to the underlying cache mechanism to control how long requests are cached for. Documentation for
these are in Microsoft's ASP.NET Core documentation
NOTE: When using the DistributedCache base class that ships with these extensions, the TResponse type returned by the cached request must be marked with the
[Serializable]
attribute as all objects need to be serialized to a byte array when stored in anIDistributedCache
implementation. It is out of scope for this getting started guide, but if needed it is possible to derive your own custom class fromICache<TRequest, TResponse>
if you have particular serialization requirements such JSON, BSON or protobuf for example.
Consult the MediatR documentation for full information on configuring your container to support the pipeline behaviors. In the case of the ASP.NET Core default container, you will likely need at least to install the Scrutor package to ensure all implementations of your cached requests are registered.
A basic version may look something like the following, and of course you may have additional pipeline behaviors and registration logic:
private static IServiceCollection AddMediator(IServiceCollection services)
{
// MediatR ServiceFactory
services.AddScoped<ServiceFactory>(p => p.GetService);
// Configure MediatR Pipeline with cache invalidation and cached request behaviors
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPreProcessorBehavior<,>));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(CacheInvalidationBehavior<,>));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(CacheBehavior<,>));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(RequestPostProcessorBehavior<,>));
// Use Scrutor to scan and register all classes as their implemented interfaces.
// This simplifies hooking up any ICache<Request, Response> implementation for the pipeline
services.Scan(scan => scan
.FromAssembliesOf(typeof(IMediator), typeof(Startup))
.AddClasses()
.AsImplementedInterfaces());
return services;
}
If you don't invalidate your cached requests when something changes, say from another request, you will have to rely on the cache expiration values before a new version of the request response will be returned.
There are at least two straight-forward ways you can invalidate cached requests:
- Automatic invalidation using instances of
ICacheInvalidator
in conjunction with theCacheInvalidationBehavior
- Manually invalidation by injecting an
ICache<Request, Response>
into, for example, anINotificationHandler
that responds to a notification that should invalidate the request.
In most case the first option is probably the quickest and easiest, whereas the second is better when you need complete control of when and how the cache is invalidated.
To use automatic invalidation, create a class that follows this pattern:
public class PingCacheInvalidator : CacheInvalidator<MessageUpdated, Ping, Pong>
{
public PingCacheInvalidator(ICache<Ping, Pong> cache) : base(cache)
{
}
protected override string GetCacheKeyIdentifier(MessageUpdated request)
{
return request.message.ToString();
}
}
In this example, let's say the UpdateMessage
command updated the message for a given Ping
request, so when we update
the message, the cached versions of Ping
for that message should be invalidated so that the new message can be return.
We inherit from CacheInvalidator<TRequest, TCachedRequest, TResponse>
where TRequest
(UpdateMessage
) is the
request that will invalidate the TCachedRequest
(Ping
) that returns a TResponse
(Pong
).
To use the invalidation, just make sure it is added to the pipeline as it is in the example above:
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(CacheInvalidationBehavior<,>));
NOTE: It can be helpful to group your invalidators in the same file or feature folder as the request they invalidate as one request could be invalidated by many other requests.
If it's not possible to invalidate the cached requests through the pipeline behavior, for example, your invalidation
needs to be done through a separate process to your normal MediatR pipeline such as messages coming from an external
queue, another is option is to create an INotificationHandler
that handles one or INotification
s and inject an
ICache<TRequest, TResponse>
directly into it.
For example, let's say we're caching a list of friend connection requests from one user to another. Every user can see a list of connection requests they've sent and received. Anytime a user sends a connection request to another user we should invalidate both their cached list of requests. Likewise, if either user revokes the request, we should invalidate the cache:
public class GetConnectionRequestsCacheInvalidator : INotificationHandler<ConnectionRequestSent>, INotificationHandler<ConnectionRequestRevoked>
{
private readonly ICache<GetConnectionRequests, List<ConnectionRequest>> _cache;
public GetConnectionRequestsCacheInvalidator(ICache<GetConnectionRequests, List<ConnectionRequests>> cache)
{
_cache = cache;
}
public async Task Handle(ConnectionRequestSent notification, CancellationToken cancellationToken)
{
await _cache.Remove(notification.FromUserId.ToString());
await _cache.Remove(notification.ToUserId.ToString());
}
public async Task Handle(ConnectionRequestRevoked notification, CancellationToken cancellationToken)
{
await _cache.Remove(notification.FromUserId.ToString());
await _cache.Remove(notification.ToUserId.ToString());
}
}