Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature Request] Replace the memory cache and distributed cache implementation with .NET9's Hybrid Cache #3153

Open
bgavrilMS opened this issue Nov 19, 2024 · 2 comments

Comments

@bgavrilMS
Copy link
Member

[Is your feature request related to a problem? Please describe.
.NET team provides a new library called HybridCache, which by default uses MemoryCache, but can optionally also be configured with a DistributedCache

https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid?view=aspnetcore-9.0

This library works on netstandard2.0 and net722

Describe the solution you'd like
Update the MemoryTokenCache and DistributedTokenCache implementation to use a HybridCache.

@jennyf19
Copy link
Collaborator

jennyf19 commented Jan 4, 2025

Would recommend adding a new class MsalHybridTokenCacheProvider to avoid breaking changes and until a stable version is released in .NET 9. In a new major version (later), we will remove the InMemory and Distributed implementations and only have Hybrid.

Developer experience today:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDistributedMemoryCache(); // or any other distributed cache service

    services.Configure<MsalDistributedTokenCacheAdapterOptions>(options =>
    {
        options.DisableL1Cache = false; // Configure as per your requirements
        options.L1ExpirationTimeRatio = 0.5;
        options.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30); // Example setting
    });

    services.AddSingleton<IMsalTokenCacheProvider, MsalDistributedTokenCacheAdapter>();

    // Other service configurations
}

Proposal:

public void ConfigureServices(IServiceCollection services)
{
    services.AddHybridCache(); // Add HybridCache service

    services.Configure<HybridCacheOptions>(options =>
    {
        options.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30); // Example setting
        options.SizeLimit = 500 * 1024 * 1024; // 500MB
    });

    services.AddSingleton<IMsalTokenCacheProvider, MsalHybridTokenCacheProvider>();

    // Other service configurations
}

Example:

public class MsalHybridTokenCacheProvider : MsalAbstractTokenCacheProvider
{
    private readonly IHybridCache _hybridCache;
    private readonly HybridCacheOptions _cacheOptions;

    public MsalHybridTokenCacheProvider(
        IHybridCache hybridCache,
        IOptions<HybridCacheOptions> cacheOptions)
    {
        _ = Throws.IfNull(cacheOptions);

        _hybridCache = hybridCache;
        _cacheOptions = cacheOptions.Value;
    }

    protected override Task RemoveKeyAsync(string cacheKey)
    {
        _hybridCache.Remove(cacheKey);
        return Task.CompletedTask;
    }

    protected override Task<byte[]?> ReadCacheBytesAsync(string cacheKey)
    {
        byte[]? tokenCacheBytes = (byte[]?)_hybridCache.Get(cacheKey);
        return Task.FromResult(tokenCacheBytes);
    }

    protected override Task WriteCacheBytesAsync(string cacheKey, byte[] bytes, CacheSerializerHints cacheSerializerHints)
    {
        HybridCacheEntryOptions hybridCacheEntryOptions = new HybridCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = DetermineCacheEntryExpiry(cacheSerializerHints),
            Size = bytes?.Length
        };

        _hybridCache.Set(cacheKey, bytes, hybridCacheEntryOptions);
        return Task.CompletedTask;
    }

    internal TimeSpan DetermineCacheEntryExpiry(CacheSerializerHints cacheSerializerHints)
    {
        TimeSpan? cacheExpiry = null;
        if (cacheSerializerHints != null && cacheSerializerHints.SuggestedCacheExpiry != null)
        {
            cacheExpiry = cacheSerializerHints.SuggestedCacheExpiry.Value.UtcDateTime - DateTime.UtcNow;
            if (cacheExpiry < TimeSpan.Zero)
            {
                cacheExpiry = TimeSpan.FromMilliseconds(1);
            }
        }

        return cacheExpiry is null || _cacheOptions.AbsoluteExpirationRelativeToNow < cacheExpiry
            ? _cacheOptions.AbsoluteExpirationRelativeToNow
            : cacheExpiry.Value;
    }
}

Possible test coverage:

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Microsoft.Identity.Web.TokenCacheProviders.InMemory;
using Moq;
using Xunit;

namespace Microsoft.Identity.Web.Test.TokenCacheProviders.Hybrid
{
    public class MsalHybridTokenCacheProviderTests
    {
        private readonly Mock<IHybridCache> _hybridCacheMock;
        private readonly MsalHybridTokenCacheProvider _tokenCacheProvider;

        public MsalHybridTokenCacheProviderTests()
        {
            _hybridCacheMock = new Mock<IHybridCache>();
            var options = Options.Create(new HybridCacheOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30),
                SizeLimit = 500 * 1024 * 1024
            });

            _tokenCacheProvider = new MsalHybridTokenCacheProvider(_hybridCacheMock.Object, options);
        }

        [Fact]
        public async Task RemoveKeyAsync_RemovesKeyFromCache()
        {
            // Arrange
            var cacheKey = "test_key";

            // Act
            await _tokenCacheProvider.RemoveKeyAsync(cacheKey);

            // Assert
            _hybridCacheMock.Verify(cache => cache.Remove(cacheKey), Times.Once);
        }

        [Fact]
        public async Task ReadCacheBytesAsync_ReturnsCachedBytes()
        {
            // Arrange
            var cacheKey = "test_key";
            var expectedBytes = new byte[] { 1, 2, 3 };
            _hybridCacheMock.Setup(cache => cache.Get(cacheKey)).Returns(expectedBytes);

            // Act
            var result = await _tokenCacheProvider.ReadCacheBytesAsync(cacheKey);

            // Assert
            Assert.Equal(expectedBytes, result);
        }

        [Fact]
        public async Task WriteCacheBytesAsync_WritesBytesToCache()
        {
            // Arrange
            var cacheKey = "test_key";
            var bytes = new byte[] { 1, 2, 3 };
            var hints = new CacheSerializerHints();

            // Act
            await _tokenCacheProvider.WriteCacheBytesAsync(cacheKey, bytes, hints);

            // Assert
            _hybridCacheMock.Verify(cache => cache.Set(
                cacheKey, 
                bytes, 
                It.Is<HybridCacheEntryOptions>(options => 
                    options.AbsoluteExpirationRelativeToNow == TimeSpan.FromMinutes(30) &&
                    options.Size == bytes.Length)), 
                Times.Once);
        }
    }
}

@jennyf19
Copy link
Collaborator

jennyf19 commented Jan 4, 2025

related issue #3160

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants