HybridCache library in ASP.NET Core
Important
This information relates to a pre-release product that may be substantially modified before it's commercially released. Microsoft makes no warranties, express or implied, with respect to the information provided here.
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 --prerelease
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),
token: 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),
token: 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
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,
token: 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,
token: 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)
};
}).WithSerializerFactory<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.StackExchangeRedispackage.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
ConnectionStringssection. Replace<the connection string>with the actual connection string:{ "ConnectionStrings": { "RedisConnectionString": "<the connection string>" } }Register in DI the
IDistributedCacheimplementation 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
IDistributedCacheimplementation is now available from the app's DI container.HybridCacheuses 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 IDistributedCacheimplementation, the HybridCache service still provides in-process caching and stampede protection.
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. Thesealedkeyword 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 --prerelease
dotnet add package Microsoft.Extensions.Caching.SqlServer --prerelease
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.
HybridCachesource code
ASP.NET Core
Feedback
Coming soon: Throughout 2024 we will be phasing out GitHub Issues as the feedback mechanism for content and replacing it with a new feedback system. For more information see: https://aka.ms/ContentUserFeedback.
Submit and view feedback for