Skip to main content

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 IDistributedCache interface

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:

  1. User-isolated cache entry
  2. Organization-isolated cache entry
  3. 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.