Repository navigation
Errors and Edge Cases
The errors the package and its dependencies raise, with their messages, and the setups where a [Cache] method runs on every call without any error. The examples ran on .NET 10, .NET 8 and .NET Framework 4.8 (Windows) with the lowest and the newest dependency sets from Getting started; where the output differs, each version is shown with its runtime or Autofac version.
using System;
using System.Runtime.Caching;
using CachingServiceWithAOP.CachingServices;
var cache = new MemoryCacheService();
try
{
cache.Set<string?>("k", null);
}
catch (Exception e)
{
Console.WriteLine(e.GetType().Name + ": " + e.Message);
}
try
{
cache.Get<string>(null!);
}
catch (Exception e)
{
Console.WriteLine(e.GetType().Name + ": " + e.Message);
}
try
{
new MemoryCacheService((ObjectCache)null!).Get<string>("k");
}
catch (Exception e)
{
Console.WriteLine(e.GetType().Name + ": " + e.Message);
}On .NET 10 and .NET 8:
ArgumentNullException: Value cannot be null. (Parameter 'value')
ArgumentNullException: Value cannot be null. (Parameter 'key')
NullReferenceException: Object reference not set to an instance of an object.
On .NET Framework 4.8:
ArgumentNullException: Value cannot be null.
Parameter name: value
ArgumentNullException: Value cannot be null.
Parameter name: key
NullReferenceException: Object reference not set to an instance of an object.
1.0.1 gives the same three errors on each runtime. The constructor accepts a null ObjectCache and the first read fails. Set with a null value throws; Get(key, func) whose function returns null stores the null without an error (How results are cached).
EnableCacheInterception() names AOPCachingInterceptor, which RegisterCachingModule() registers. Without the module the error comes at the first resolve:
using System;
using Autofac;
using CachingServiceWithAOP;
using CachingServiceWithAOP.Extensions;
var builder = new ContainerBuilder();
builder.RegisterType<Stock>().As<IStock>().EnableCacheInterception();
using var container = builder.Build();
try
{
container.Resolve<IStock>();
}
catch (Exception e)
{
for (var error = e; error != null; error = error.InnerException)
{
Console.WriteLine(error.GetType().Name + ": " + error.Message);
}
}
public interface IStock
{
int Level(string sku);
}
public class Stock : IStock
{
[Cache]
public int Level(string sku) => 3;
}With Autofac 6.5.0 (every runtime):
DependencyResolutionException: An exception was thrown while activating Stock.
ComponentNotRegisteredException: The requested service 'CachingServiceWithAOP.AOPCachingInterceptor' has not been registered. To avoid this exception, either register a component to provide the service, check for service registration using IsRegistered(), or use the ResolveOptional() method to resolve an optional dependency.
With Autofac 9.3.4 the inner message gains a link:
DependencyResolutionException: An exception was thrown while activating Stock.
ComponentNotRegisteredException: The requested service 'CachingServiceWithAOP.AOPCachingInterceptor' has not been registered. To avoid this exception, either register a component to provide the service, check for service registration using IsRegistered(), or use the ResolveOptional() method to resolve an optional dependency.
See https://autofac.rtfd.io/help/service-not-registered for more info.
1.0.1 threw the ComponentNotRegisteredException itself, without the DependencyResolutionException around it (Versions and upgrading).
Interception is for interfaces. A registration that also offers the class as a service fails when the class is resolved:
using System;
using Autofac;
using CachingServiceWithAOP;
using CachingServiceWithAOP.Extensions;
var builder = new ContainerBuilder();
builder.RegisterCachingModule();
builder.RegisterType<Ledger>().AsSelf().As<ILedger>().EnableCacheInterception();
using var container = builder.Build();
try
{
var ledger = container.Resolve<Ledger>();
Console.WriteLine(ledger.Total() + ", " + ledger.Total());
}
catch (Exception e)
{
for (var error = e; error != null; error = error.InnerException)
{
Console.WriteLine(error.GetType().Name + ": " + error.Message);
}
}
public interface ILedger
{
int Total();
}
public class Ledger : ILedger
{
private int runs;
[Cache]
public int Total() => ++runs;
}On every runtime and dependency set:
DependencyResolutionException: An exception was thrown while activating Ledger.
InvalidOperationException: The component Activator = Ledger (ReflectionActivator), Services = [Ledger, ILedger], Lifetime = Autofac.Core.Lifetime.CurrentScopeLifetime, Sharing = None, Ownership = OwnedByLifetimeScope, Pipeline = Autofac.Core.Pipeline.ResolvePipeline cannot use interface interception as it provides services that are not publicly visible interfaces. Check your registration of the component to ensure you're not enabling interception and registering it as an internal/private interface type.
The message comes from Autofac.Extras.DynamicProxy. Register the component with .As<ILedger>() only, and resolve ILedger.
The interface must be public. An internal one fails the same way:
using System;
using Autofac;
using CachingServiceWithAOP;
using CachingServiceWithAOP.Extensions;
var builder = new ContainerBuilder();
builder.RegisterCachingModule();
builder.RegisterType<Hidden>().As<IHidden>().EnableCacheInterception();
using var container = builder.Build();
try
{
Console.WriteLine(container.Resolve<IHidden>().Value());
}
catch (Exception e)
{
for (var error = e; error != null; error = error.InnerException)
{
Console.WriteLine(error.GetType().Name + ": " + error.Message);
}
}
internal interface IHidden
{
int Value();
}
public class Hidden : IHidden
{
[Cache]
public int Value() => 1;
}On every runtime and dependency set:
DependencyResolutionException: An exception was thrown while activating Hidden.
InvalidOperationException: The component Activator = Hidden (ReflectionActivator), Services = [IHidden], Lifetime = Autofac.Core.Lifetime.CurrentScopeLifetime, Sharing = None, Ownership = OwnedByLifetimeScope, Pipeline = Autofac.Core.Pipeline.ResolvePipeline cannot use interface interception as it provides services that are not publicly visible interfaces. Check your registration of the component to ensure you're not enabling interception and registering it as an internal/private interface type.
The interceptor reads [Cache] from the class's method. On the interface it is ignored, without an error:
using System;
using Autofac;
using CachingServiceWithAOP;
using CachingServiceWithAOP.Extensions;
var builder = new ContainerBuilder();
builder.RegisterCachingModule();
builder.RegisterType<Basket>().As<IBasket>().SingleInstance().EnableCacheInterception();
using var container = builder.Build();
var basket = container.Resolve<IBasket>();
Console.WriteLine(basket.Count() + ", " + basket.Count());
public interface IBasket
{
[Cache]
int Count();
}
public class Basket : IBasket
{
private int runs;
public int Count() => ++runs;
}1, 2
Only calls through the proxy are intercepted. A [Cache] method calling another one on this calls it directly:
using System;
using Autofac;
using CachingServiceWithAOP;
using CachingServiceWithAOP.Extensions;
var builder = new ContainerBuilder();
builder.RegisterCachingModule();
builder.RegisterType<Tax>().As<ITax>().SingleInstance().EnableCacheInterception();
using var container = builder.Build();
var tax = container.Resolve<ITax>();
Console.WriteLine(tax.Summary("eu"));
Console.WriteLine(tax.Rate("eu"));
Console.WriteLine(tax.Rate("eu"));
public interface ITax
{
int Rate(string region);
string Summary(string region);
}
public class Tax : ITax
{
private int runs;
[Cache]
public int Rate(string region) => ++runs;
[Cache]
public string Summary(string region) => "rate " + Rate(region);
}rate 1
2
2
Summary ran Rate without the cache, so the first call to Rate through the proxy ran it again; from then on the proxy's entry answers.
An override of a [Cache] method is intercepted, because the check looks at the methods it overrides. The lifetime, though, is read from the override alone, so a lifetime set on the base method is lost:
using System;
using Autofac;
using CachingServiceWithAOP;
using CachingServiceWithAOP.Extensions;
var builder = new ContainerBuilder();
builder.RegisterCachingModule();
builder.RegisterType<BaseQuote>().Named<IQuote>("base").SingleInstance().EnableCacheInterception();
builder.RegisterType<FreshQuote>().Named<IQuote>("override").SingleInstance().EnableCacheInterception();
using var container = builder.Build();
foreach (var name in new[] { "base", "override" })
{
var quote = container.ResolveNamed<IQuote>(name);
Console.WriteLine($"{name}: {quote.Get()}, {quote.Get()}");
}
public interface IQuote
{
int Get();
}
public class BaseQuote : IQuote
{
protected int runs;
[Cache(0)]
public virtual int Get() => ++runs;
}
public class FreshQuote : BaseQuote
{
public override int Get() => ++runs;
}base: 1, 2
override: 1, 1
The base method's [Cache(0)] keeps nothing; the override, with no attribute of its own, is cached for the service's five minutes. Repeat the attribute on the override to keep its lifetime.
The key has the method's name and parameter types, not its interface. A class that implements two interfaces with the same method shares one entry between them:
using System;
using Autofac;
using CachingServiceWithAOP;
using CachingServiceWithAOP.Extensions;
var builder = new ContainerBuilder();
builder.RegisterCachingModule();
builder.RegisterType<Product>().As<ICount>().As<ISize>().As<ILabel>().SingleInstance().EnableCacheInterception();
using var container = builder.Build();
Console.WriteLine("count: " + container.Resolve<ICount>().Get("A-1"));
Console.WriteLine("size: " + container.Resolve<ISize>().Get("A-1"));
try
{
Console.WriteLine("label: " + container.Resolve<ILabel>().Get("A-1"));
}
catch (Exception e)
{
Console.WriteLine("label: " + e.GetType().Name + ": " + e.Message);
}
public interface ICount
{
int Get(string sku);
}
public interface ISize
{
int Get(string sku);
}
public interface ILabel
{
string Get(string sku);
}
public class Product : ICount, ISize, ILabel
{
[Cache]
int ICount.Get(string sku) => 3;
[Cache]
int ISize.Get(string sku) => 40;
[Cache]
string ILabel.Get(string sku) => "label " + sku;
}On every runtime and dependency set:
count: 3
size: 3
label: InvalidCastException: Unable to cast object of type 'System.Int32' to type 'System.String'.
ISize.Get never ran and answered 3, and ILabel.Get got the cached int. Give such methods different names, or different parameters.
When an argument cannot be written into the key, the call runs and nothing is stored. No error reaches the caller:
using System;
using System.Collections.Generic;
using Autofac;
using CachingServiceWithAOP;
using CachingServiceWithAOP.Extensions;
var builder = new ContainerBuilder();
builder.RegisterCachingModule();
builder.RegisterType<Lookup>().As<ILookup>().SingleInstance().EnableCacheInterception();
using var container = builder.Build();
var lookup = container.Resolve<ILookup>();
var node = new Node();
node.Next = node;
var map = new Dictionary<int, string> { [1] = "a" };
var text = new string('x', 2100000);
Console.WriteLine($"cycle: {lookup.ByNode(node)}, {lookup.ByNode(node)}");
Console.WriteLine($"int keys: {lookup.ByMap(map)}, {lookup.ByMap(map)}");
Console.WriteLine($"long enum: {lookup.BySize(Size.Large)}, {lookup.BySize(Size.Large)}");
Console.WriteLine($"long text: {lookup.ByText(text)}, {lookup.ByText(text)}");
try
{
lookup.ByItems(Broken());
}
catch (ArgumentException e)
{
Console.WriteLine("caught: " + e.Message + "; ByItems ran " + Lookup.ItemRuns + " times");
}
static IEnumerable<int> Broken()
{
yield return 1;
throw new ArgumentException("bad item");
}
public class Node
{
public Node? Next { get; set; }
}
public enum Size : long
{
Large = 1,
}
public interface ILookup
{
int ByNode(Node node);
int ByMap(Dictionary<int, string> map);
int BySize(Size size);
int ByText(string text);
int ByItems(IEnumerable<int> items);
}
public class Lookup : ILookup
{
public static int ItemRuns;
private int runs;
[Cache]
public int ByNode(Node node) => ++runs;
[Cache]
public int ByMap(Dictionary<int, string> map) => ++runs;
[Cache]
public int BySize(Size size) => ++runs;
[Cache]
public int ByText(string text) => ++runs;
[Cache]
public int ByItems(IEnumerable<int> items) => ++ItemRuns;
}On every runtime and dependency set:
cycle: 1, 2
int keys: 3, 4
long enum: 5, 6
long text: 7, 8
caught: bad item; ByItems ran 0 times
- A reference cycle, a dictionary whose keys are not strings, an enum based on
longorulong, and a key longer than 2 097 152 characters each make the call run uncached. So does nesting deeper than 100 levels (from the README and the source; not shown). - An exception the argument itself throws while the key is written (an iterator here, or a property getter) reaches the caller, and the method does not run.
- 1.0.1 threw before running the method in the first four cases (the CHANGELOG's Fixed list).
An argument whose getter blocks blocks the call: a pending Task as an argument waits on its Result. Not shown, since the example would hang.
An exception thrown by the method reaches the caller as it was thrown and nothing is stored, and a faulted or cancelled Task is dropped from the cache (How results are cached).
This wiki describes CachingServiceWithAOPSupport 2.0.0 and was last updated on 2026-09-30. The library is MIT licensed. Report problems in the issues. On NuGet: CachingServiceWithAOPSupport.