⚡ TL;DR (quick version) |
---|
FusionCache natively supports .NET dependency injection including keyed services, along with a builder to allow for easy customization: all the features, options and components are supported, thanks to a rich fluent API. |
In .NET there's full support for Dependency Injection (DI), a design pattern to achieve a form of Inversion of Control (IoC) in our code.
This is a common way to handle creation, dependencies, scopes and disposal of resources that makes it easier and more flexible to work with any service we may need.
🙋♂️ Updating from before v0.24.0 ? please read here. |
---|
It's very easy to work with FusionCache when using DI: all we need to do is just register it like any other service and, if needed, configure it the way we want.
In our startup phase we just add this:
services.AddFusionCache();
And FusionCache will be registered, ready to be used somewhere else in our code.
For example in an MVC controller we can just add an IFusionCache
param in the constructor and it will be available to us, like this:
public class MyController : Controller
{
private readonly IFusionCache _cache;
// THE cache PARAM WILL BE AUTOMATICALLY POPULATED BY THE DI FRAMEWORK
public MyController(IFusionCache cache)
{
_cache = cache;
}
[Route("product/{id:int}")]
public IActionResult Product(int id)
{
var product = _cache.GetOrSet<Product>(
$"product:{id}",
_ => GetProductFromDb(id),
TimeSpan.FromSeconds(30)
);
return View(product);
}
}
In this way the cache
param will be automatically populated by the DI framework.
By simply calling services.AddFusionCache()
we will have FusionCache configured with the default options and using only a memory cache (L1) and nothing else: no distributed cache (L2), no backplane nor any plugin.
But usually we want to do more, right? Maybe add a distributed cache or configure some options.
For that we can use a builder approach.
By calling services.AddFusionCache()
what we get back is an instance of IFusionCacheBuilder
from which we have access to a lot of different extension methods with a fluent interface design, all readily available to do whatever we want.
To configure some cache-wide options we can use:
services.AddFusionCache()
.WithOptions(opt =>
{
opt.AutoRecoveryMaxItems = 123;
})
;
To configure some default entry options we can use:
services.AddFusionCache()
.WithDefaultEntryOptions(opt =>
{
opt.Duration = TimeSpan.FromSeconds(30);
opt.FactorySoftTimeout = TimeSpan.FromMilliseconds(100);
})
;
Of course we can combine them (remember? fluent interface!):
services.AddFusionCache()
.WithOptions(opt =>
{
opt.AutoRecoveryMaxItems = 123;
})
.WithDefaultEntryOptions(opt =>
{
opt.Duration = TimeSpan.FromSeconds(30);
opt.FactorySoftTimeout = TimeSpan.FromMilliseconds(100);
})
;
Ok, these are various types of options, but what about sub-components like the memory cache, the distributed cache, the serializer, the backplane, etc?
FusionCache has a unified approach, and everything is (hopefully) very uniform for each component.
For example for the memory cache we can tell FusionCache:
WithRegisteredMemoryCache()
: USE the one registered in the DI container. If not there, an EXCEPTION will be thrownTryWithRegisteredMemoryCache()
: TRY TO USE the one registered in the DI container. If not there, no problemWithRegisteredKeyedMemoryCache(object? serviceKey)
: USE the one registered in the DI container with the specified SERVICE KEY. If not there, an EXCEPTION will be thrownTryWithRegisteredKeyedMemoryCache(object? serviceKey)
: TRY TO USE the one registered in the DI container with the specified SERVICE KEY. If not there, no problemWithRegisteredKeyedMemoryCacheByCacheName()
: USE the one registered in the DI container with the CacheName as the SERVICE KEY. If not there, an EXCEPTION will be thrownTryWithRegisteredKeyedMemoryCacheByCacheName()
: TRY TO USE the one registered in the DI container with the CacheName as the SERVICE KEY. If not there, no problemWithMemoryCache(IMemoryCache memoryCache)
: USE an instance that we provide DIRECTLYWithMemoryCache(Func<IServiceProvider, IMemoryCache> factory)
: USE an instance built via a FACTORY that we provide
The same is available for other components, like the backplane for example:
WithRegisteredBackplane()
TryWithRegisteredBackplane()
WithRegisteredKeyedBackplane(object? serviceKey)
TryWithRegisteredKeyedBackplane(object? serviceKey)
WithRegisteredKeyedBackplaneByCacheName()
TryWithRegisteredKeyedBackplaneByCacheName()
WithBackplane(IFusionCacheBackplane backplane)
WithBackplane(Func<IServiceProvider, IFusionCacheBackplane> factory)
and so on.
This approach is currently available for these components:
- logger
- memory locker
- memory cache
- distributed cache + serializer
- backplane
- plugins (slightly different, see below)
Of course we can mix these approach for the different components for example by using the registered logger, a specific memory cache via an instance, a backplane via a factory and so on.
A slightly particular case is the distributed cache, since it requires a serializer to do its job and there's a common case that we may want to ignore.
Because of this, in these methods there are some extra params like:
bool throwIfMissingSerializer
: tells FusionCache if it should throw in case if finds a valid distributed cache but no serializer, to avoid surprises down the road like "I specified a distributed cache, but it's not using it and it didn't tell me anything, why?"bool ignoreMemoryDistributedCache
: tells FusionCache if it should accept an instance ofMemoryDistributedCache
, which is not really a distributed cache and is typically registered automatically by ASP.NET MVC without us being able to avoid it, and using it is just a waste of resources
Everything is the same, but since we can have multiple plugins some methods works in a "plural" way: for example we have WithAllRegisteredPlugins()
which will use all of the registered IFusionCachePlugin
services, not just one.
There's also the keyed version via WithAllRegisteredKeyedPlugins(object? serviceKey)
, which act as we can imagine: same as above but by a specified service key (or by CacheName
if not specified).
Also, we can call WithPlugin(...)
multiple times and add multiple plugins to the same FusionCache instance.
Sometimes we may need to further customize a FusionCache instance in some ways.
To do that we have at our disposal a WithPostSetup(Action<IServiceProvider, IFusionCache> action)
method where we can specify some custom logic to apply after the creation of that FusionCache instance.
It's probably not that frequent, but let's say we would like FusionCache to try and look into the DI container and basically use everything it can. Found a backplane? Use it. Found a valid distributed cache + a valid serializer? Use them. Any plugin found? Yep, use them all.
In this way we don't have a lot of control about what has been done so we may have surprises at runtime, but if this is what we want then we can use the TryWithAutoSetup()
method: as the name implies, it will try to automatically do a setup of what it can find.
In more details, it will:
- try to look for a registered logger (any implementation of
ILogger<FusionCache>
), and use it - try to look for a registered memory cache (any implementation of
IMemoryCache
), and use it - try to look for a registered distributed cache (any implementation of
IDistributedCache
) and, if it also finds a valid serializer (any implementation ofIFusionCacheSerializer
), add a 2nd level - try to look for a registered backplane (any implementation of
IFusionCacheBackplane
) and use it - try to look for all registered FusionCache plugins (all registered implementations of
IFusionCachePlugin
) and add + initialize them
When specifying which components to use we have 2 choices:
- tell FusionCache exactly what to use (either via an direct instance or a factory)
- register a component in the DI container, then tell FusionCache to use what is registered
This is an example of the first approach, via a direct instance:
services.AddFusionCache()
.WithBackplane(new RedisBackplane(new RedisBackplaneOptions
{
Configuration = "CONNECTION_STRING_HERE"
}))
;
Again the first approach, but with a factory:
services.AddFusionCache()
.WithBackplane(serviceProvider => new RedisBackplane(new RedisBackplaneOptions
{
Configuration = serviceProvider.GetService<MyConfigStuff>().RedisConfig
}))
;
And this is an example of the second approach:
services.AddFusionCacheStackExchangeRedisBackplane(opt =>
{
opt.Configuration = "CONNECTION_STRING_HERE";
});
services.AddFusionCache()
.WithRegisteredBackplane()
;
As previously stated, by default nothing is done automagically: the only exceptions to this rule - because of how these specific components work - are the logger, the options and the serializer. The serializer by the way will only be used if it's needed (eg: when also using a distributed cache).
All in all, we may prefer flexibility in what we register and then use what is registered, or we may prefer to directly specify what we want to use.
FusionCache supports both approaches, and let us choose what we prefer.
FusionCache natively supports multiple Named Caches thanks to the IFusionCacheProvider
interface: require it via DI and we can ask for a particular cache with the specified name via provider.GetCache(name)
.
Since .NET 8 though we now have native support for multiple services of the same type, identified by different keys, thanks to the addition of so called keyed services.
So, can the two approaches work together? Oh yeah, totally.
The idea is that, when registering a FusionCache instance, we simply add .AsKeyedServiceByCacheName()
, and that's it: that cache will be available both via named caches and via keyed services.
As an example, at startup we simply do this:
services.AddFusionCache("MyCache")
.AsKeyedServiceByCacheName();
and later, to resolve it, we either use IFusionCacheProvider
:
app.MapGet("/foo", (IFusionCacheProvider cacheProvider) => {
var cache = cacheProvider.GetCache("MyCache");
cache.Set("cache-key", 42);
});
or just mark a constructor param or web action param of type IFusionCache
with the FromKeyedServices
attribute:
app.MapGet("/foo", ([FromKeyedServices("MyCache")] IFusionCache) => {
cache.Set("cache-key", 42);
});
Nice, eh?
We can even specify a different service key by using AsKeyedService(myServiceKey)
, where the key can be either of type string
or anything else really, since the Keyed Services approach in .NET supports that.
So, the next question is which one to choose?
The [FromKeyedServices("MyCache")]
approach is declarative and quicker, but the IFusionCacheProvider
approach is programmatic and more powerful, since we can add logic to the selection of the cache name, while allowing us to use both the GetCache(name)
or GetCacheOrNull(name)
approach, adding a fallback logic or whatever we can think of.
Basically it's up to us, and as always we can mix and match the two approaches in different scenarios, freely.
But registering FusionCache as a keyed service is one half of the story: the other half is how to consume keyed services as FusionCache components.
Can we do that too? Yep!
When registering a FusionCache instance via the builder we always had the ability to say, for example, WithRegisteredBackplane()
and FusionCache will look for a service of type IFusionCacheBackplane
right? Cool now it's also possible to say WithRegisteredKeyedBackplane(object? serviceKey)
to pick the service of type IFusionCacheBackplane
registered with the specified serviceKey
or even just say WithRegisteredKeyedBackplane()
to do the same but using the CacheName
as the service key.
This is available for all the building blocks: logger, memory cache, memory locker, distributed cache, serializer, backplane and plugins.
Nice.