HybridCache library in ASP.NET Core
Important
HybridCache
is currently still in preview but will be fully released after .NET 9.0 in a future minor release of .NET Extensions.
This article explains how to configure and use the HybridCache
library in an ASP.NET Core app. For an introduction to the library, see the HybridCache
section of the Caching overview.
Get the library
Install the Microsoft.Extensions.Caching.Hybrid
package.
dotnet add package Microsoft.Extensions.Caching.Hybrid --version "9.0.0-preview.7.24406.2"
Register the service
Add the HybridCache
service to the dependency injection (DI) container by calling AddHybridCache
:
// Add services to the container.
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddAuthorization();
builder.Services.AddHybridCache();
The preceding code registers the HybridCache
service with default options. The registration API can also configure options and serialization.
Get and store cache entries
The HybridCache
service provides a GetOrCreateAsync
method with two overloads, taking a key and:
- A factory method.
- State, and a factory method.
The method uses the key to try to retrieve the object from the primary cache. If the item isn't found in the primary cache (a cache miss), it then checks the secondary cache if one is configured. If it doesn't find the data there (another cache miss), it calls the factory method to get the object from the data source. It then stores the object in both primary and secondary caches. The factory method is never called if the object is found in the primary or secondary cache (a cache hit).
The HybridCache
service ensures that only one concurrent caller for a given key calls the factory method, and all other callers wait for the result of that call. The CancellationToken
passed to GetOrCreateAsync
represents the combined cancellation of all concurrent callers.
The main GetOrCreateAsync
overload
The stateless overload of GetOrCreateAsync
is recommended for most scenarios. The code to call it is relatively simple. Here's an example:
public class SomeService(HybridCache cache)
{
private HybridCache _cache = cache;
public async Task<string> GetSomeInfoAsync(string name, int id, CancellationToken token = default)
{
return await _cache.GetOrCreateAsync(
$"{name}-{id}", // Unique key to the cache entry
async cancel => await GetDataFromTheSourceAsync(name, id, cancel),
cancellationToken: token
);
}
public async Task<string> GetDataFromTheSourceAsync(string name, int id, CancellationToken token)
{
string someInfo = $"someinfo-{name}-{id}";
return someInfo;
}
}
The alternative GetOrCreateAsync
overload
The alternative overload might reduce some overhead from captured variables and per-instance callbacks, but at the expense of more complex code. For most scenarios the performance increase doesn't outweigh the code complexity. Here's an example that uses the alternative overload:
public class SomeService(HybridCache cache)
{
private HybridCache _cache = cache;
public async Task<string> GetSomeInfoAsync(string name, int id, CancellationToken token = default)
{
return await _cache.GetOrCreateAsync(
$"{name}-{id}", // Unique key to the cache entry
(name, id, obj: this),
static async (state, token) =>
await state.obj.GetDataFromTheSourceAsync(state.name, state.id, token),
cancellationToken: token
);
}
public async Task<string> GetDataFromTheSourceAsync(string name, int id, CancellationToken token)
{
string someInfo = $"someinfo-{name}-{id}";
return someInfo;
}
}
The SetAsync
method
In many scenarios, GetOrCreateAsync
is the only API needed. But HybridCache
also has SetAsync
to store an object in cache without trying to retrieve it first.
Remove cache entries by key
When the underlying data for a cache entry changes before it expires, remove the entry explicitly by calling RemoveAsync
with the key to the entry. An overload lets you specify a collection of key values.
When an entry is removed, it is removed from both the primary and secondary caches.
Remove cache entries by tag
Important
This feature is still under development. If you try to remove entries by tag, you will notice that it doesn't have any effect.
Tags can be used to group cache entries and invalidate them together.
Set tags when calling GetOrCreateAsync
, as shown in the following example:
public class SomeService(HybridCache cache)
{
private HybridCache _cache = cache;
public async Task<string> GetSomeInfoAsync(string name, int id, CancellationToken token = default)
{
var tags = new List<string> { "tag1", "tag2", "tag3" };
var entryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(1),
LocalCacheExpiration = TimeSpan.FromMinutes(1)
};
return await _cache.GetOrCreateAsync(
$"{name}-{id}", // Unique key to the cache entry
async cancel => await GetDataFromTheSourceAsync(name, id, cancel),
entryOptions,
tags,
cancellationToken: token
);
}
public async Task<string> GetDataFromTheSourceAsync(string name, int id, CancellationToken token)
{
string someInfo = $"someinfo-{name}-{id}";
return someInfo;
}
}
Remove all entries for a specified tag by calling RemoveByTagAsync
with the tag value. An overload lets you specify a collection of tag values.
When an entry is removed, it is removed from both the primary and secondary caches.
Options
The AddHybridCache
method can be used to configure global defaults. The following example shows how to configure some of the available options:
// Add services to the container.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services.AddHybridCache(options =>
{
options.MaximumPayloadBytes = 1024 * 1024;
options.MaximumKeyLength = 1024;
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(5),
LocalCacheExpiration = TimeSpan.FromMinutes(5)
};
});
The GetOrCreateAsync
method can also take a HybridCacheEntryOptions
object to override the global defaults for a specific cache entry. Here's an example:
public class SomeService(HybridCache cache)
{
private HybridCache _cache = cache;
public async Task<string> GetSomeInfoAsync(string name, int id, CancellationToken token = default)
{
var tags = new List<string> { "tag1", "tag2", "tag3" };
var entryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromMinutes(1),
LocalCacheExpiration = TimeSpan.FromMinutes(1)
};
return await _cache.GetOrCreateAsync(
$"{name}-{id}", // Unique key to the cache entry
async cancel => await GetDataFromTheSourceAsync(name, id, cancel),
entryOptions,
tags,
cancellationToken: token
);
}
public async Task<string> GetDataFromTheSourceAsync(string name, int id, CancellationToken token)
{
string someInfo = $"someinfo-{name}-{id}";
return someInfo;
}
}
For more information about the options, see the source code:
- HybridCacheOptions class.
- HybridCacheEntryOptions class.
Limits
The following properties of HybridCacheOptions
let you configure limits that apply to all cache entries:
- MaximumPayloadBytes - Maximum size of a cache entry. Default value is 1 MB. Attempts to store values over this size are logged, and the value isn't stored in cache.
- MaximumKeyLength - Maximum length of a cache key. Default value is 1024 characters. Attempts to store values over this size are logged, and the value isn't stored in cache.
Serialization
Use of a secondary, out-of-process cache requires serialization. Serialization is configured as part of registering the HybridCache
service. Type-specific and general-purpose serializers can be configured via the AddSerializer
and AddSerializerFactory
methods, chained from the AddHybridCache
call. By default, the library
handles string
and byte[]
internally, and uses System.Text.Json
for everything else. HybridCache
can also use other serializers, such as protobuf or XML.
The following example configures the service to use a type-specific protobuf serializer:
// Add services to the container.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services.AddHybridCache(options =>
{
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromSeconds(10),
LocalCacheExpiration = TimeSpan.FromSeconds(5)
};
}).AddSerializer<SomeProtobufMessage,
GoogleProtobufSerializer<SomeProtobufMessage>>();
The following example configures the service to use a general-purpose protobuf serializer that can handle many protobuf types:
// Add services to the container.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services.AddHybridCache(options =>
{
options.DefaultEntryOptions = new HybridCacheEntryOptions
{
Expiration = TimeSpan.FromSeconds(10),
LocalCacheExpiration = TimeSpan.FromSeconds(5)
};
}).AddSerializerFactory<GoogleProtobufSerializerFactory>();
The secondary cache requires a data store, such as Redis or SqlServer. To use Azure Cache for Redis, for example:
Install the
Microsoft.Extensions.Caching.StackExchangeRedis
package.Create an instance of Azure Cache for Redis.
Get a connection string that connects to the Redis instance. Find the connection string by selecting Show access keys on the Overview page in the Azure portal.
Store the connection string in the app's configuration. For example, use a user secrets file that looks like the following JSON, with the connection string in the
ConnectionStrings
section. Replace<the connection string>
with the actual connection string:{ "ConnectionStrings": { "RedisConnectionString": "<the connection string>" } }
Register in DI the
IDistributedCache
implementation that the Redis package provides. To do that, callAddStackExchangeRedisCache
, and pass in the connection string. For example:builder.Services.AddStackExchangeRedisCache(options => { options.Configuration = builder.Configuration.GetConnectionString("RedisConnectionString"); });
The Redis
IDistributedCache
implementation is now available from the app's DI container.HybridCache
uses it as the secondary cache and uses the serializer configured for it.
For more information, see the HybridCache serialization sample app.
Cache storage
By default HybridCache
uses MemoryCache for its primary cache storage. Cache entries are stored in-process, so each server has a separate cache that is lost whenever the server process is restarted. For secondary out-of-process storage, such as Redis or SQL Server, HybridCache
uses the configured IDistributedCache
implementation, if any. But even without an IDistributedCache
implementation, the HybridCache
service still provides in-process caching and stampede protection.
Note
When invalidating cache entries by key or by tags, they are invalidated in the current server and in the secondary out-of-process storage. However, the in-memory cache in other servers isn't affected.
Optimize performance
To optimize performance, configure HybridCache
to reuse objects and avoid byte[]
allocations.
Reuse objects
By reusing instances, HybridCache
can reduce the overhead of CPU and object allocations associated with per-call deserialization. This can lead to performance improvements in scenarios where the cached objects are large or accessed frequently.
In typical existing code that uses IDistributedCache
, every retrieval of an object from the cache results in deserialization. This behavior means that each concurrent caller gets a separate instance of the object, which can't interact with other instances. The result is thread safety, as there's no risk of concurrent modifications to the same object instance.
Because much HybridCache
usage will be adapted from existing IDistributedCache
code, HybridCache
preserves this behavior by default to avoid introducing concurrency bugs. However, objects are inherently thread-safe if:
- They are immutable types.
- The code doesn't modify them.
In such cases, inform HybridCache
that it's safe to reuse instances by:
- Marking the type as
sealed
. Thesealed
keyword in C# means that the class can't be inherited. - Applying the
[ImmutableObject(true)]
attribute to the type. The[ImmutableObject(true)]
attribute indicates that the object's state can't be changed after it's created.
Avoid byte[]
allocations
HybridCache
also provides optional APIs for IDistributedCache
implementations, to avoid byte[]
allocations. This feature is implemented by the preview versions of the Microsoft.Extensions.Caching.StackExchangeRedis
and Microsoft.Extensions.Caching.SqlServer
packages. For more information, see IBufferDistributedCache
Here are the .NET CLI commands to install the packages:
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
dotnet add package Microsoft.Extensions.Caching.SqlServer
Custom HybridCache implementations
A concrete implementation of the HybridCache
abstract class is included in the shared framework and is provided via dependency injection. But developers are welcome to provide custom implementations of the API.
Compatibility
The HybridCache
library supports older .NET runtimes, down to .NET Framework 4.7.2 and .NET Standard 2.0.
Additional resources
For more information about HybridCache
, see the following resources:
- GitHub issue dotnet/aspnetcore #54647.
HybridCache
source code
ASP.NET Core