File: LocalInvalidationTests.cs
Web Access
Project: src\test\Libraries\Microsoft.Extensions.Caching.Hybrid.Tests\Microsoft.Extensions.Caching.Hybrid.Tests.csproj (Microsoft.Extensions.Caching.Hybrid.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Hybrid.Internal;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Xunit.Abstractions;
using static Microsoft.Extensions.Caching.Hybrid.Tests.L2Tests;
 
namespace Microsoft.Extensions.Caching.Hybrid.Tests;
public class LocalInvalidationTests(ITestOutputHelper log) : IClassFixture<TestEventListener>
{
    private static ServiceProvider GetDefaultCache(out DefaultHybridCache cache, Action<ServiceCollection>? config = null)
    {
        var services = new ServiceCollection();
        config?.Invoke(services);
        services.AddHybridCache();
        ServiceProvider provider = services.BuildServiceProvider();
        cache = Assert.IsType<DefaultHybridCache>(provider.GetRequiredService<HybridCache>());
        return provider;
    }
 
    [Fact]
    public async Task GlobalInvalidateNoTags()
    {
        using var services = GetDefaultCache(out var cache);
        var value = await cache.GetOrCreateAsync<Guid>("abc", ct => new(Guid.NewGuid()));
 
        // should work immediately as-is
        Assert.Equal(value, await cache.GetOrCreateAsync<Guid>("abc", ct => new(Guid.NewGuid())));
 
        // invalidating a normal tag should have no effect
        await cache.RemoveByTagAsync("foo");
        Assert.Equal(value, await cache.GetOrCreateAsync<Guid>("abc", ct => new(Guid.NewGuid())));
 
        // invalidating everything should force a re-fetch
        await cache.RemoveByTagAsync("*");
        var newValue = await cache.GetOrCreateAsync<Guid>("abc", ct => new(Guid.NewGuid()));
        Assert.NotEqual(value, newValue);
 
        // which should now be repeatable again
        Assert.Equal(newValue, await cache.GetOrCreateAsync<Guid>("abc", ct => new(Guid.NewGuid())));
    }
 
    [Theory]
    [InlineData(false, false)]
    [InlineData(true, false)]
    [InlineData(false, true)]
    [InlineData(true, true)]
    public async Task TagBasedInvalidate(bool withL2, bool withExtraTag)
    {
        using IMemoryCache l1 = new MemoryCache(new MemoryCacheOptions());
        IDistributedCache? l2 = null;
        if (withL2)
        {
            MemoryDistributedCacheOptions options = new();
            MemoryDistributedCache mdc = new(Options.Create(options));
            l2 = new LoggingCache(log, mdc);
        }
 
        Guid lastValue = Guid.Empty;
 
        // loop because we want to test pre-existing L1/L2 impact
        for (int i = 0; i < 3; i++)
        {
            using var services = GetDefaultCache(out var cache, svc =>
            {
                svc.AddSingleton(l1);
                if (l2 is not null)
                {
                    svc.AddSingleton(l2);
                }
            });
            var clock = services.GetRequiredService<TimeProvider>();
 
            string key = "mykey";
            string tag = "abc";
            string[] tags = withExtraTag ? [tag, "other"] : [tag];
            var value = await cache.GetOrCreateAsync<Guid>(key, ct => new(Guid.NewGuid()), tags: tags);
            log.WriteLine($"First value: {value}");
            if (lastValue != Guid.Empty)
            {
                Assert.Equal(lastValue, value);
            }
 
            // should work immediately as-is
            var tmp = await cache.GetOrCreateAsync<Guid>(key, ct => new(Guid.NewGuid()), tags: tags);
            log.WriteLine($"Second value: {tmp} (should be {value})");
            Assert.Equal(value, tmp);
 
            // invalidating a normal tag should have no effect
            await cache.RemoveByTagAsync("foo");
            tmp = await cache.GetOrCreateAsync<Guid>(key, ct => new(Guid.NewGuid()), tags: tags);
            log.WriteLine($"Value after invalidating tag foo: {tmp} (should be {value})");
            Assert.Equal(value, tmp);
 
            // invalidating a tag we have should force a re-fetch
            await cache.RemoveByTagAsync(tag);
            var newValue = await cache.GetOrCreateAsync<Guid>(key, ct => new(Guid.NewGuid()), tags: tags);
            log.WriteLine($"Value after invalidating tag {tag}: {newValue} (should not be {value})");
            Assert.NotEqual(value, newValue);
 
            // which should now be repeatable again
            tmp = await cache.GetOrCreateAsync<Guid>(key, ct => new(Guid.NewGuid()), tags: tags);
            log.WriteLine($"And repeating: {tmp} (should be {newValue})");
            Assert.Equal(newValue, tmp);
            value = newValue;
 
            // invalidating everything should force a re-fetch
            await cache.RemoveByTagAsync("*");
            newValue = await cache.GetOrCreateAsync<Guid>(key, ct => new(Guid.NewGuid()), tags: tags);
            log.WriteLine($"Value after invalidating tag *: {newValue} (should not be {value})");
            Assert.NotEqual(value, newValue);
 
            // which should now be repeatable again
            tmp = await cache.GetOrCreateAsync<Guid>(key, ct => new(Guid.NewGuid()), tags: tags);
            log.WriteLine($"And repeating: {tmp} (should be {newValue})");
            Assert.Equal(newValue, tmp);
            lastValue = newValue;
 
            var now = clock.GetUtcNow().UtcTicks;
            do
            {
                await Task.Delay(10);
            }
            while (clock.GetUtcNow().UtcTicks == now);
        }
    }
}