Open sandboxFocusImprove this doc

Using Caching with Dependency Injection

By default, the cached methods use one cache for the whole process: DefaultService, which you configure at startup (see Caching Method Return Values). In an application whose services are created by a dependency injection container, for instance an ASP.NET Core application, you can instead register the cache in the container. The classes that have cached methods then receive the cache from their constructors, as they receive their other dependencies.

This has the following benefits:

  • The cache is configured in the composition root of the application, which is the code that registers the services in the container, and it can depend on them, for instance on the IMemoryCache of the container.
  • Each container has its own cache, which it disposes with the application. Two applications, or two tests, in the same process do not share their cache.
  • The cache is an interface, ICachingService, which a test can replace.

The caching service

ICachingService represents a cache: the backend that stores the items (Backend), the key builder that computes their keys (KeyBuilder), the caching profiles (Profiles), and the operations that the caching aspects call, including the invalidation methods. CachingService is its implementation. Pass the backend, and optionally a key builder, to its constructor.

The methods that do not receive a service from a container use DefaultService. This property returns the result of the DefaultServiceLocator delegate, which returns a CachingService created by PostSharp unless you replace it. Setting the delegate to null restores the default one. The delegate runs on each call to a cached method, so it must be fast and return the same object every time, for instance a singleton resolved from a container. Set it before you configure DefaultService, because the configuration does not move to the service that a new delegate returns.

Receiving the caching service from the container

To make the classes with cached methods receive their caching service from the container:

  1. Add the CacheConfigurationAttribute custom attribute to the assembly, to the classes or to a base class, and set its UseDependencyInjection property to true.

  2. Register an ICachingService in the container, typically a singleton CachingService created with the backend of your choice (see Caching Backends).

  3. Let the container create the objects of these classes.

The setting applies to every class that has a method with CacheAttribute or InvalidateCacheAttribute. PostSharp reads it at build time from the declaring class of the method, then from its base classes, then from the assembly.

At build time, PostSharp appends a parameter of type ICachingService to the instance constructors of these classes, and of the classes that derive from them, in the same project or in another project that PostSharp processes. A constructor that already has a parameter of type ICachingService keeps its signature, and the aspects use this parameter. For the other constructors, PostSharp creates a new constructor with the additional parameter, and the original constructor becomes a forwarder to it. A public forwarder becomes protected internal. For the details of this transformation, see Receiving Dependencies from the Constructors of the Target Class.

Important

UseDependencyInjection changes the public constructors of the classes. When such a class is public and used by other assemblies, this is a binary breaking change for them.

Example

The following ProductCatalog class has a cached method and an invalidating method. Its source code declares only the repository in its constructor:

[CacheConfiguration( UseDependencyInjection = true )]
public class ProductCatalog
{
    private readonly IProductRepository repository;

    // The build appends an ICachingService parameter to this constructor.
    public ProductCatalog( IProductRepository repository )
    {
        this.repository = repository;
    }

    [Cache( IgnoreThisParameter = true )]
    public Product GetProduct( int id ) => this.repository.Load( id );

    [InvalidateCache( nameof(GetProduct) )]
    public void RenameProduct( int id, string name ) => this.repository.Rename( id, name );
}

The catalog is a scoped service, so each request has its own catalog. IgnoreThisParameter excludes the instance from the cache key, so that all the catalogs share the cached items. The following controller uses the catalog, and knows nothing about caching:

public sealed class ProductController
{
    private readonly ProductCatalog catalog;

    public ProductController( ProductCatalog catalog )
    {
        this.catalog = catalog;
    }

    public void Show( int id ) => Console.WriteLine( "Showing {0}.", this.catalog.GetProduct( id ).Name );

    public void Rename( int id, string name ) => this.catalog.RenameProduct( id, name );
}

The composition root registers the IMemoryCache of Microsoft.Extensions.Caching.Memory, a MemoryCacheBackend over it (see Using In-Memory Cache), and the CachingService, whose default profile it configures:

ServiceCollection services = new ServiceCollection();

services.AddMemoryCache();
services.AddSingleton<CachingBackend>( provider => new MemoryCacheBackend( provider.GetRequiredService<IMemoryCache>() ) );
services.AddSingleton<ICachingService>( provider =>
{
    CachingService cachingService = new CachingService( provider.GetRequiredService<CachingBackend>() );
    cachingService.Profiles.Default.AbsoluteExpiration = TimeSpan.FromMinutes( 10 );

    return cachingService;
} );

services.AddSingleton<IProductRepository, ProductRepository>();
services.AddScoped<ProductCatalog>();
services.AddTransient<ProductController>();

The container passes the repository and the caching service to the constructor of the catalog. The following code handles three requests, each in its own scope, as ASP.NET Core does for each HTTP request:

using ( ServiceProvider application = services.BuildServiceProvider() )
{
    // Each request has its own scope, as in ASP.NET Core, and its own ProductCatalog.
    for ( int request = 1; request <= 2; request++ )
    {
        Console.WriteLine( "Request {0}:", request );

        using ( IServiceScope scope = application.CreateScope() )
        {
            scope.ServiceProvider.GetRequiredService<ProductController>().Show( 1 );
        }
    }

    Console.WriteLine( "Request 3:" );

    using ( IServiceScope scope = application.CreateScope() )
    {
        ProductController controller = scope.ServiceProvider.GetRequiredService<ProductController>();
        controller.Rename( 1, "Renamed product" );
        controller.Show( 1 );
    }
}

The second request reads the product from the cache. The third one renames the product, which invalidates the cached item:

Request 1:
>> Loading product 1 from the database.
Showing Product 1.
Request 2:
Showing Product 1.
Request 3:
>> Loading product 1 from the database.
Showing Renamed product.

Because the backend is a singleton of the container, the container disposes it when it is disposed.

Objects created with new

The objects of these classes are normally created by the container, by ActivatorUtilities, or by the constructor of a derived class. When the source code creates an object with new, it calls a forwarder, which passes null for the caching service. The constructor then gets the service from RunTimeServiceLocator (see Objects created without a container).

PostSharp Caching registers DefaultService in RunTimeServiceLocator as the ICachingService, unless the application registered its own service first. An object created with new therefore uses the default service, as a static method does. To give it another service, register it at startup:

RunTimeServiceLocator.RegisterService<ICachingService>( () => application.GetRequiredService<ICachingService>() );

Static methods and structs

A static method, or a method of a struct, has no object that receives the service from the container. It uses DefaultService, and PostSharp reports the message CAC011. Its severity is the value of the StaticMethodFallbackSeverity property of CacheConfigurationAttribute:

When the method of a struct changes the struct, the change reaches the caller, with or without dependency injection.

Invalidating cached methods

InvalidateCacheAttribute invalidates the cache of the object that it receives, so an invalidating method and the methods it invalidates must use the same service. InvalidateCacheAttribute reads its own UseDependencyInjection setting, and PostSharp reports the error CAC012 when it invalidates a method whose setting is different.

To invalidate imperatively, call the Invalidate methods of the service: with a MethodInfo, the instance and the arguments to remove one method call result, or with a dependency (see Working with Cache Dependencies). The overloads of CachingServices.Invalidation act on DefaultService, so they do not invalidate the items of a service received from the container. A class whose constructor declares a parameter of type ICachingService receives the same service as its cached methods, and can store it in a field.

The aspects import the caching service of the target class by its type:

Profiles, formatters and the caching context

Each caching service has its own caching profiles, in its Profiles property. Profiles configures only the profiles of DefaultService, so it does not configure the methods of the classes that receive a service from the container. Configure their profiles on the service that you register, as in the example above.

The following settings are shared by all the services of the process:

When a method cached by one service calls a method cached by another service, the inner method is not added as a dependency of the outer method (see Working with Cache Dependencies), because its invalidation would not reach the backend of the outer method.

See Also

Reference

ICachingService
CachingService
DefaultService
DefaultServiceLocator
UseDependencyInjection
StaticMethodFallbackSeverity
RunTimeServiceLocator

Other Resources

Caching Method Return Values
Caching Backends
Receiving Dependencies from the Constructors of the Target Class
Dependency injection in .NET