Caching
Overview
Provides an implementation of the IDistributedCache interface for distributed caching.
Install
- ASOL.Core.Caching - contains the basic types for caching
- ASOL.Core.Caching.Distributed - contains the implementation of the
IDistributedCacheinterface
Third-party libraries (NuGet)
Changelog
- 0.0.1.47859-dev
- Support for older .NET frameworks has been removed; only .NET 8 is now supported.
- 0.0.1.33225-dev
- multiversion support added
Dependency injection
Mandatory dependency injection to register the distributed cache implementation from the ASOL.Core.Caching.Distributed.Extensions namespace.
The default registration uses application isolation settings based on the AuthorizationServiceOptions:ApplicationCode configuration.
services.AddDistributedCache(builder.Configuration); // namespace ASOL.Core.Caching.Distributed.Extensions
Option to override the application code for isolation.
services.AddDistributedCache(Configuration, applicationCode: "MyCustomApplicationCode"); // namespace ASOL.Core.Caching.Distributed.Extensions
Configuration
The default implementation of IDistributedCache uses a remote distributed cache under the hood.
For local development, you can test your code using remote distributed cache storage. You can install and run Redis Open Source via Docker: https://redis.io/docs/latest/operate/oss_and_stack/install/install-stack/docker/.
Update the local configuration in appsettings.Development.json:
{
"DistributedCache": {
"Storage": "RemoteCache"
},
"RedisConnection": {
"ConnectionString": "localhost:6379"
}
}
DistributedCache contains only cache-specific settings. Redis connection, retry, timeout, and health check settings are shared with other distributed coordination features through the RedisConnection section.
You also have the option to simulate behavior by switching to MemoryCache, which eliminates the need for a local Redis instance.
{
"DistributedCache": {
"Storage": "MemoryCache"
}
}
Breaking configuration migration
Redis connection settings are no longer read from the DistributedCache section. Move the following settings to RedisConnection:
{
"RedisConnection": {
"ConnectionString": "localhost:6379",
"ConnectRetryCount": 5,
"InitConnectRetryDelay": 5,
"ConnectTimeoutMs": 500,
"SyncTimeoutMs": 5000,
"HealthCheckMode": "Enabled"
}
}
Keep only cache-specific settings in DistributedCache:
{
"DistributedCache": {
"Storage": "RemoteCache",
"ReturnNullWhenRemoteCacheUnavailableOnRead": true
}
}
Usage
All cache entries are application-isolated. You can add an additional isolation layer for each cache entry:
- Tenant-isolated cache entry
- Organization-isolated cache entry
- User-isolated cache entry
Cache key formats:
- Application-isolated cache entry:
APP_CODE:CACHE_KEY - Tenant-isolated cache entry:
APP_CODE:TENANT_ID:CACHE_KEY - Organization-isolated cache entry:
APP_CODE:TENANT_ID:ORGANIZATION_ID:CACHE_KEY - User-isolated cache entry:
APP_CODE:TENANT_ID:USER_ID:CACHE_KEY
The IDistributedCache.SetAsync and IDistributedCache.Set methods in IDistributedCache set cache entries with tenancy isolation.
The IDistributedCache.GetAsync and IDistributedCache.Get methods automatically resolve the cache
entry by the specified cache key using the current IRuntimeContextScope, and return a value by searching isolation levels in the following order:
- User-isolated cache entry
- Organization-isolated cache entry
- Tenant-isolated cache entry
For anonymous contexts, only application isolation is applied.
Resolve IDistributedCache using DI
using Microsoft.Extensions.Caching.Distributed;
using ASOL.Core.Caching.Distributed.Extensions;
public MyClass(IDistributedCache distributedCache) { }
DistributedCacheEntryOptions
Absolute and sliding expiration configuration for cache entries:
private readonly DistributedCacheEntryOptions distributedCacheEntryOptions = new()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(60), // The cache item will expire after 60 minutes from now
SlidingExpiration = TimeSpan.FromMinutes(30) // The cache item will expire if not accessed for 10 minutes
};
Examples
Application-isolated cache entry
private const string TestDistributedCacheKeyA1 = "test-distributed-cache-key-A1";
public async Task<IEnumerable<TestCachingModel>> PublicGenerics(CancellationToken cancellationToken = default)
{
var cacheResult = await distributedCache.GetPublicAsync<IEnumerable<TestCachingModel>>(TestDistributedCacheKeyA1, cancellationToken);
if (cacheResult != null) return cacheResult;
var result = Enumerable.Range(1, 5).Select(index => new TestCachingModel { Name = $"Name {index} {Random.Shared.Next(-20, 55)}"}).ToArray();
await distributedCache.SetPublicAsync(TestDistributedCacheKeyA1, result, distributedCacheEntryOptions, cancellationToken);
return result;
}
Pass a string value:
private const string TestDistributedCacheKeyA2 = "test-distributed-cache-key-A2";
public async Task<TestCachingModel> PublicString(CancellationToken cancellationToken = default)
{
var cacheResult = await distributedCache.GetPublicAsync(TestDistributedCacheKeyA2, cancellationToken);
if (cacheResult != null) return new TestCachingModel { Name = Encoding.UTF8.GetString(cacheResult, 0, cacheResult.Length) };
var result = new TestCachingModel { Name = $"Name {Random.Shared.Next(-20, 55)}" };
await distributedCache.SetPublicAsync(TestDistributedCacheKeyA2, Encoding.UTF8.GetBytes(result.Name), distributedCacheEntryOptions, cancellationToken);
return result;
}
Tenant-isolated cache entry
private const string TestDistributedCacheKeyT1 = "test-distributed-cache-key-t1";
public async Task<IEnumerable<TestCachingModel>> TenantGenerics(CancellationToken cancellationToken = default)
{
var cacheResult = await distributedCache.GetAsync<IEnumerable<TestCachingModel>>(TestDistributedCacheKeyT1, cancellationToken);
if (cacheResult != null) return cacheResult;
var result = Enumerable.Range(1, 5).Select(index => new TestCachingModel { Name = $"Name {index} {Random.Shared.Next(-20, 55)}"}).ToArray();
await distributedCache.SetAsync(TestDistributedCacheKeyT1, result, distributedCacheEntryOptions, cancellationToken);
return result;
}
or
private const string TestDistributedCacheKeyT1 = "test-distributed-cache-key-t1";
public async Task<IEnumerable<TestCachingModel>> TenantGenerics(CancellationToken cancellationToken = default)
{
var cacheResult = await distributedCache.GetByTenantAsync<IEnumerable<TestCachingModel>>(TestDistributedCacheKeyT1, cancellationToken);
if (cacheResult != null) return cacheResult;
var result = Enumerable.Range(1, 5).Select(index => new TestCachingModel { Name = $"Name {index} {Random.Shared.Next(-20, 55)}"}).ToArray();
await distributedCache.SetWithTenantAsync(TestDistributedCacheKeyT1, result, distributedCacheEntryOptions, cancellationToken);
return result;
}
Pass a string value:
private const string TestDistributedCacheKeyT2 = "test-distributed-cache-key-t2";
public async Task<TestCachingModel> TenantString(CancellationToken cancellationToken = default)
{
var cacheResult = await distributedCache.GetStringAsync(TestDistributedCacheKeyT2, cancellationToken);
if (cacheResult != null) return new TestCachingModel(){Name = cacheResult};
var result = new TestCachingModel { Name = $"Name {Random.Shared.Next(-20, 55)}" };
await distributedCache.SetStringAsync(TestDistributedCacheKeyT2, result.Name, distributedCacheEntryOptions, cancellationToken);
return result;
}
Pass tenantId as a parameter:
private const string TestDistributedCacheKeyT4 = "test-distributed-cache-key-t4";
public async Task<IEnumerable<TestCachingModel>> TenantManual(CancellationToken cancellationToken = default)
{
var tenantId = "XTenant";
var cacheResult = await distributedCache.GetByTenantAsync<IEnumerable<TestCachingModel>>(TestDistributedCacheKeyT4, tenantId, cancellationToken);
if (cacheResult != null) return cacheResult;
var result = Enumerable.Range(1, 5).Select(index => new TestCachingModel { Name = $"Name {index} {Random.Shared.Next(-20, 55)}"}).ToArray();
await distributedCache.SetWithTenantAsync(TestDistributedCacheKeyT4, tenantId, result, distributedCacheEntryOptions, cancellationToken);
return result;
}
Organization-isolated cache entry
private const string TestDistributedCacheKeyO1 = "test-distributed-cache-key-O1";
public async Task<IEnumerable<TestCachingModel>> OrganizationGenerics(CancellationToken cancellationToken = default)
{
var cacheResult = await distributedCache.GetAsync<IEnumerable<TestCachingModel>>(TestDistributedCacheKeyO1, cancellationToken);
if (cacheResult != null) return cacheResult;
var result = Enumerable.Range(1, 5).Select(index => new TestCachingModel { Name = $"Name {index} {Random.Shared.Next(-20, 55)}"}).ToArray();
await distributedCache.SetWithTenantOrganizationAsync(TestDistributedCacheKeyO1, result, distributedCacheEntryOptions, cancellationToken);
return result;
}
Pass tenantId and organizationId as parameters:
private const string TestDistributedCacheKeyO3 = "test-distributed-cache-key-O3";
public async Task<IEnumerable<TestCachingModel>> ManualOrganizationGeneric(CancellationToken cancellationToken = default)
{
var tenantId = "ASOLEU-DEV-fd9ad6b9-2f29-4c7a-9a3a-c7469e19b1ff"; // tenantId for example XTenant
var organizationId = "fff2d400-9838-4513-9c7b-535a03bd8c94"; // organizationId
var cacheResult = await distributedCache.GetByTenantOrganizationAsync<IEnumerable<TestCachingModel>>(TestDistributedCacheKeyO3, tenantId, organizationId, cancellationToken);
if (cacheResult != null) return cacheResult;
var result = Enumerable.Range(1, 5).Select(index => new TestCachingModel { Name = $"Name {index} {Random.Shared.Next(-20, 55)}" }).ToArray();
await distributedCache.SetWithTenantOrganizationAsync(TestDistributedCacheKeyO3, tenantId, organizationId, result, distributedCacheEntryOptions, cancellationToken);
return result;
}
User-isolated cache entry
private const string TestDistributedCacheKeyU1 = "test-distributed-cache-key-U1";
public async Task<IEnumerable<TestCachingModel>> UserGenerics(CancellationToken cancellationToken = default)
{
var cacheResult = await distributedCache.GetAsync<IEnumerable<TestCachingModel>>(TestDistributedCacheKeyU1, cancellationToken);
if (cacheResult != null) return cacheResult;
var result = Enumerable.Range(1, 5).Select(index => new TestCachingModel { Name = $"Name {index} {Random.Shared.Next(-20, 55)}"}).ToArray();
await distributedCache.SetWithTenantUserAsync(TestDistributedCacheKeyU1, result, distributedCacheEntryOptions, cancellationToken);
return result;
}
Pass a string value:
private const string TestDistributedCacheKeyU2 = "test-distributed-cache-key-U2";
public async Task<TestCachingModel> UserString(CancellationToken cancellationToken = default)
{
var cacheResult = await distributedCache.GetAsync(TestDistributedCacheKeyU2, cancellationToken);
if (cacheResult != null) return new TestCachingModel { Name = Encoding.UTF8.GetString(cacheResult, 0, cacheResult.Length) };
var result = new TestCachingModel { Name = $"Name {Random.Shared.Next(-20, 55)}" };
await distributedCache.SetWithTenantUserAsync(TestDistributedCacheKeyU2, Encoding.UTF8.GetBytes(result.Name), distributedCacheEntryOptions, cancellationToken);
return result;
}
Examples - Impersonated
private const string TestDistributedCacheKeyT3 = "test-distributed-cache-key-t3";
public async Task<IEnumerable<TestCachingModel>> TenantImpersonated(CancellationToken cancellationToken = default)
{
var tenantId = "XTenant";
using (var scope = serviceProvider.GetRequiredService<IRuntimeContextScope>())
{
var tenantHolder = scope.ScopeProvider.GetRequiredService<ITenantContextHolder>();
tenantHolder.TenantContext = new TenantContext { Tenant = new TenantInfo(tenantId, null) };
var languageCode = runtimeContext.Localization?.LanguageCode;
var artificalUser = ClaimsPrincipalBuilder.CreateClientPrincipal(scope.SsoAuthOptions.ClientId, tenantId)
.AddLocale(languageCode)
.AddAuthenticateType()
.Build();
scope.LocalImpersonate(artificalUser, runtimeContext.Trace);
var cacheResult = await distributedCache.GetAsync<IEnumerable<TestCachingModel>>(TestDistributedCacheKeyT3, cancellationToken);
if (cacheResult != null) return cacheResult;
var result = Enumerable.Range(1, 5).Select(index => new TestCachingModel { Name = $"Name {index} {Random.Shared.Next(-20, 55)}"}).ToArray();
await distributedCache.SetAsync(TestDistributedCacheKeyT3, result, distributedCacheEntryOptions, cancellationToken);
return result;
}
}
Organization isolated cache entry:
private const string TestDistributedCacheKeyO2 = "test-distributed-cache-key-O2";
public async Task<IEnumerable<TestCachingModel>> OrganizationImpersonated(CancellationToken cancellationToken = default)
{
var tenantId = "ASOLEU-DEV-fd9ad6b9-2f29-4c7a-9a3a-c7469e19b1ff"; // tenantId for example XTenant
using (var scope = serviceProvider.GetRequiredService<IRuntimeContextScope>())
{
var tenantHolder = scope.ScopeProvider.GetRequiredService<ITenantContextHolder>();
tenantHolder.TenantContext = new TenantContext { Tenant = new TenantInfo(tenantId, null) };
var languageCode = runtimeContext.Localization?.LanguageCode;
var artificalUser = ClaimsPrincipalBuilder.CreateClientPrincipal(scope.SsoAuthOptions.ClientId, tenantId)
.AddLocale(languageCode)
.AddSelectedOrganization("fff2d400-9838-4513-9c7b-535a03bd8c94", "64949541|CZ")
.AddAuthenticateType()
.Build();
scope.LocalImpersonate(artificalUser, runtimeContext.Trace);
var cacheResult = await distributedCache.GetAsync<IEnumerable<TestCachingModel>>(TestDistributedCacheKeyO2, cancellationToken);
if (cacheResult != null) return cacheResult;
var result = Enumerable.Range(1, 5).Select(index => new TestCachingModel { Name = $"Name {index} {Random.Shared.Next(-20, 55)}" }).ToArray();
await distributedCache.SetWithTenantOrganizationAsync(TestDistributedCacheKeyO2, result, distributedCacheEntryOptions, cancellationToken);
return result;
}
}
All of the above scenarios have a method for deleting the cached value with the given key.