| Severity | Error |
| Reported by | PostSharp.Patterns.Caching |
| Message | The [InvalidateCache] aspect applied to <InvalidatingMethod> cannot invalidate <CachedMethod>: <InvalidatingMethod> uses <InvalidatingService>, and <CachedMethod> uses <CachedService>, so the invalidation would remove the item from another cache. Apply the same UseDependencyInjection setting of [CacheConfiguration] to both methods. |
Cause
The InvalidateCacheAttribute aspect removes items from the caching service of its own method, <InvalidatingMethod>. PostSharp determines this service at build time, from the UseDependencyInjection property of the CacheConfigurationAttribute of the declaring class, its base classes or its assembly:
- With
true, an instance method of a class uses the caching service that its object receives from dependency injection. - Otherwise, or for a static method or a method of a struct, the method uses the default caching service, DefaultService.
PostSharp reports CAC012 when the cached method, <CachedMethod>, uses the other service, because the invalidation would then never reach its items. <InvalidatingService> and <CachedService> are the default caching service or the caching service from dependency injection.
In the following example, ProductCatalog receives its caching service from dependency injection, but PriceEditor does not.
using System;
using System.Collections.Generic;
using PostSharp.Extensibility;
using PostSharp.Patterns.Caching;
using PostSharp.Patterns.Caching.Backends;
namespace Errors.CAC012_Error;
public static class PriceStore
{
public static Dictionary<string, decimal> Prices { get; } = new() { ["TEA-01"] = 4.50m };
}
[CacheConfiguration( UseDependencyInjection = true )]
public class ProductCatalog
{
[Cache( IgnoreThisParameter = true )]
public decimal GetPrice( string productId )
{
Console.WriteLine( $"Reading the price of {productId}." );
return PriceStore.Prices[productId];
}
}
public class PriceEditor
{
[InvalidateCache( typeof(ProductCatalog), nameof(ProductCatalog.GetPrice) )]
public void SetPrice( string productId, decimal price ) // PriceEditor uses the default caching service, and ProductCatalog the injected one.
{
PriceStore.Prices[productId] = price;
}
}
public static class Program
{
public static void Main()
{
// The objects created with new, rather than by a container, receive the service that RunTimeServiceLocator returns.
var cachingService = new CachingService( new MemoryCachingBackend() );
RunTimeServiceLocator.RegisterService<ICachingService>( () => cachingService );
var catalog = new ProductCatalog();
var editor = new PriceEditor();
Console.WriteLine( catalog.GetPrice( "TEA-01" ) );
Console.WriteLine( catalog.GetPrice( "TEA-01" ) );
editor.SetPrice( "TEA-01", 5.00m );
Console.WriteLine( catalog.GetPrice( "TEA-01" ) );
}
}
error CAC012: CAC012_Error.cs(29,17): The [InvalidateCache] aspect applied to Errors.CAC012_Error.PriceEditor.SetPrice(System.String, System.Decimal) cannot invalidate Errors.CAC012_Error.ProductCatalog.GetPrice(System.String): Errors.CAC012_Error.PriceEditor.SetPrice(System.String, System.Decimal) uses the default caching service, and Errors.CAC012_Error.ProductCatalog.GetPrice(System.String) uses the caching service from dependency injection, so the invalidation would remove the item from another cache. Apply the same UseDependencyInjection setting of [CacheConfiguration] to both methods.
Resolution
- Set UseDependencyInjection to the same value for both classes, for example with a CacheConfigurationAttribute on the assembly, and make the invalidating method an instance method of a class, whose objects the container creates.
- Otherwise, remove the
[InvalidateCache]aspect, and invalidate the item imperatively with theInvalidatemethods of the ICachingService that the cached method uses.
In the following example, PriceEditor also receives its caching service from dependency injection. The program creates both objects with new, so they receive the service registered in RunTimeServiceLocator, and the call after SetPrice reads the new price.
using System;
using System.Collections.Generic;
using PostSharp.Extensibility;
using PostSharp.Patterns.Caching;
using PostSharp.Patterns.Caching.Backends;
namespace Errors.CAC012_Fixed;
public static class PriceStore
{
public static Dictionary<string, decimal> Prices { get; } = new() { ["TEA-01"] = 4.50m };
}
[CacheConfiguration( UseDependencyInjection = true )]
public class ProductCatalog
{
[Cache( IgnoreThisParameter = true )]
public decimal GetPrice( string productId )
{
Console.WriteLine( $"Reading the price of {productId}." );
return PriceStore.Prices[productId];
}
}
[CacheConfiguration( UseDependencyInjection = true )] // PriceEditor now receives its caching service as ProductCatalog does.
public class PriceEditor
{
[InvalidateCache( typeof(ProductCatalog), nameof(ProductCatalog.GetPrice) )]
public void SetPrice( string productId, decimal price )
{
PriceStore.Prices[productId] = price;
}
}
public static class Program
{
public static void Main()
{
// The objects created with new, rather than by a container, receive the service that RunTimeServiceLocator returns.
var cachingService = new CachingService( new MemoryCachingBackend() );
RunTimeServiceLocator.RegisterService<ICachingService>( () => cachingService );
var catalog = new ProductCatalog();
var editor = new PriceEditor();
Console.WriteLine( catalog.GetPrice( "TEA-01" ) );
Console.WriteLine( catalog.GetPrice( "TEA-01" ) );
editor.SetPrice( "TEA-01", 5.00m );
Console.WriteLine( catalog.GetPrice( "TEA-01" ) );
}
}
Reading the price of TEA-01.
4.50
4.50
Reading the price of TEA-01.
5.00
See Also
Reference
InvalidateCacheAttribute
UseDependencyInjection
ICachingService
Other Resources
Using Caching with Dependency Injection
Removing Items From the Cache