Metalama.Patterns.Caching.TestHelpers 2026.1.26

Prefix Reserved
dotnet add package Metalama.Patterns.Caching.TestHelpers --version 2026.1.26
                    
NuGet\Install-Package Metalama.Patterns.Caching.TestHelpers -Version 2026.1.26
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Metalama.Patterns.Caching.TestHelpers" Version="2026.1.26" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Metalama.Patterns.Caching.TestHelpers" Version="2026.1.26" />
                    
Directory.Packages.props
<PackageReference Include="Metalama.Patterns.Caching.TestHelpers" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Metalama.Patterns.Caching.TestHelpers --version 2026.1.26
                    
#r "nuget: Metalama.Patterns.Caching.TestHelpers, 2026.1.26"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Metalama.Patterns.Caching.TestHelpers@2026.1.26
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Metalama.Patterns.Caching.TestHelpers&version=2026.1.26
                    
Install as a Cake Addin
#tool nuget:?package=Metalama.Patterns.Caching.TestHelpers&version=2026.1.26
                    
Install as a Cake Tool

Metalama.Patterns.Caching.TestHelpers

This package contains helper classes used, together with the Metalama.Patterns.TestHelpers package, when developing unit tests for a new CachingBackend implementation.

Unless you are developing your own CachingBackend, you don't need this package.

Substituting the dependencies of a backend

A caching backend has three dependencies that this package can replace: the clock, the object that dispatches work items, and the memory cache. A test that substitutes them advances the clock and waits for the pending work items, instead of sleeping for a duration or waiting for a timeout.

Type Replaces Product default
FakeCachingServices The service provider given to the backend None
Microsoft.Extensions.Time.Testing.FakeTimeProvider System.TimeProvider TimeProvider.System
TestWorkItemDispatcher Metalama.Patterns.Caching.Implementation.IWorkItemDispatcher ThreadPoolWorkItemDispatcher
FakeMemoryCache Microsoft.Extensions.Caching.Memory.IMemoryCache Microsoft.Extensions.Caching.Memory.MemoryCache

FakeCachingServices creates the three implementations and registers them in a single service provider. Pass its ServiceProvider property to the backend under test. The backend then resolves the substitutes instead of the defaults.

FakeTimeProvider

FakeTimeProvider comes from the Microsoft.Extensions.TimeProvider.Testing package. Its clock only moves when the test moves it. FakeCachingServices.TimeProvider exposes the instance, and the constructor of FakeCachingServices takes the instant at which the clock starts.

TestWorkItemDispatcher

TestWorkItemDispatcher runs the work items on the thread pool, as the product implementation does, and counts the work items that have been queued and have not completed yet. WhenPendingWorkItemsCompletedAsync returns a task that completes when that count reaches zero.

The work items still run on real threads. The caching code blocks in several places, so a single pump thread executing the work that it queues to itself would deadlock. The class therefore offers a completion point, not an ordering guarantee.

A work item that queues another work item before it returns is covered. The count does not reach zero between the two, so the wait observes the whole chain.

FakeMemoryCache

FakeMemoryCache reads a TimeProvider instead of the wall clock. An entry expires as soon as the clock passes its expiration instant. The class registers a timer with the TimeProvider for the earliest expiration instant, so advancing a FakeTimeProvider evicts the entries that fall due and invokes their post-eviction callbacks, without the test having to notify the cache.

FakeMemoryCache implements IClearableMemoryCache, so a backend that stores its entries in it reports the Clear feature, as it does with MemoryCache.

FakeCachingServices registers the memory cache as a single instance. Two backends that resolve it from the same service provider therefore share one store, which is not what the two layers of a layered backend need. Give the second layer its own store in that case.

A worked example

The sequence is always the same: advance the clock, await the dispatcher, assert. FakeCachingServices.AdvanceAsync does the first two steps in one call.

using Metalama.Patterns.Caching.Backends;
using Metalama.Patterns.Caching.Building;
using Metalama.Patterns.Caching.Implementation;
using Metalama.Patterns.Caching.TestHelpers;
using Xunit;

public sealed class MemoryCachingBackendExpirationTests
{
    private static readonly DateTimeOffset _origin = new( 2026, 1, 1, 0, 0, 0, TimeSpan.Zero );

    [Fact]
    public async Task AbsoluteExpiration_RaisesItemRemoved_WhenTheClockAdvances()
    {
        using var fakes = new FakeCachingServices( _origin );
        using var cancellationTokenSource = new CancellationTokenSource();

        using var backend = CachingBackend.Create(
            b => b.Memory( new MemoryCachingBackendConfiguration() ),
            fakes.ServiceProvider );

        backend.Initialize();

        CacheItemRemovedEventArgs? removedArgs = null;
        backend.ItemRemoved += ( _, args ) => removedArgs = args;

        const string key = "expiring-key";

        backend.SetItem(
            key,
            new CacheItem( "value", configuration: new CacheItemConfiguration { AbsoluteExpiration = TimeSpan.FromMinutes( 5 ) } ) );

        Assert.NotNull( backend.GetItem( key ) );

        await fakes.AdvanceAsync( TimeSpan.FromMinutes( 6 ), cancellationTokenSource.Token );

        Assert.Null( backend.GetItem( key ) );
        Assert.NotNull( removedArgs );
        Assert.Equal( CacheItemRemovedReason.Expired, removedArgs.RemovedReason );
    }
}

The call to AdvanceAsync returns when the whole chain has completed:

  1. The clock passes the expiration instant of the entry.
  2. FakeMemoryCache evicts the entry and invokes its post-eviction callback.
  3. The backend queues a work item that raises ItemRemoved.
  4. The count of pending work items of TestWorkItemDispatcher reaches zero.

There is no sleep and no timeout in the sequence.

Use WhenPendingWorkItemsCompletedAsync instead of AdvanceAsync when the operation under test is not the passage of time, for example an explicit RemoveItem whose event is raised on a work item.

Substitution is opt-in

Substitution is opt-in per test class. A test class that does not create a FakeCachingServices keeps running against the real clock, the real thread pool and MemoryCache. The defaults of the backend are unchanged, so an existing test suite that ignores this package continues to behave as before.

Some test suites stay on the real thread pool on purpose, because they exist to exercise a real and contended one. In this repository, AwaitableEventRaceTests reproduces interleavings of AwaitableEvent between two real threads, BackgroundTaskSchedulerEdgeCaseTests exercises the concurrency limit and the overload detection of BackgroundTaskScheduler, and AwaitableEventHangDiagnostic is a load test that repeats the enqueue-then-await handshake a large number of times. A deterministic dispatcher would remove the very contention that these suites measure.

BaseCacheBackendTests, the shared base class of the backend test suites, stays on the real clock for the same kind of reason. The Redis and Azure backends of Metalama.Premium derive from it and run against a network and a real clock, which a fake clock cannot drive.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Framework net472 is compatible.  net48 was computed.  net481 was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2026.1.26 36 9/5/2026
2026.1.25 90 8/21/2026
2026.1.24 100 8/13/2026
2026.1.23 95 8/10/2026
2026.1.22 94 8/5/2026
2026.1.21 104 7/21/2026
2026.1.20 110 7/15/2026
2026.1.19 116 6/27/2026
2026.1.18 130 6/10/2026
2026.1.17 116 6/5/2026
2026.1.16 128 6/3/2026
2026.1.15-rc 113 5/15/2026
2026.1.14-rc 103 5/13/2026
2026.1.13-preview 115 5/13/2026
2026.1.12-preview 104 5/12/2026
2026.1.11-preview 106 5/6/2026
2026.0.25 111 6/30/2026
2026.0.24 113 6/27/2026
2026.0.23 120 5/15/2026
2025.1.18 115 6/30/2026
Loading failed