Skip to content
m4bwav edited this page Oct 1, 2026 · 1 revision

Each recipe is a whole Program.cs. Each ran against the published 2.0.0 on .NET 10, .NET 8 and .NET Framework 4.8 (Windows), with the lowest and the newest dependency sets from Getting started, and printed the output shown on all of them.

A store per test

RegisterCachingModule() stores in MemoryCache.Default, which lives as long as the process, so one test's cached results answer the next test's calls (How results are cached). Register a MemoryCacheService over a MemoryCache of the test's own after the module; Autofac uses the last registration of ICacheService.

using System;
using System.Runtime.Caching;
using Autofac;
using CachingServiceWithAOP;
using CachingServiceWithAOP.CachingServices;
using CachingServiceWithAOP.Extensions;

static IContainer BuildForTest(MemoryCache store)
{
    var builder = new ContainerBuilder();
    builder.RegisterCachingModule();
    builder.RegisterInstance(new MemoryCacheService(store)).As<ICacheService>();
    builder.RegisterType<Greeter>().As<IGreeter>().EnableCacheInterception();
    return builder.Build();
}

foreach (var test in new[] { "first test", "second test" })
{
    using var store = new MemoryCache(test);
    using var container = BuildForTest(store);
    var greeter = container.Resolve<IGreeter>();
    Console.WriteLine(test + ": " + greeter.Hello("Ada") + "; " + greeter.Hello("Ada"));
}

public interface IGreeter
{
    string Hello(string name);
}

public class Greeter : IGreeter
{
    private static int runs;

    [Cache]
    public string Hello(string name) => "hello " + name + " (run " + ++runs + ")";
}
first test: hello Ada (run 1); hello Ada (run 1)
second test: hello Ada (run 2); hello Ada (run 2)

Each test still caches within itself, and the second test starts empty. The same registration is the way to give one container, or one tenant, a store apart from the rest of the process. 1.0.1's MemoryCacheService(ObjectCache) kept nothing, so this recipe needs 2.0.0.

Log every cached call

ICacheService is the seam: an implementation of your own, registered after the module, sees every call to a [Cache] method. This one wraps a MemoryCacheService and prints the method and the lifetime the attribute asked for:

using System;
using Autofac;
using Castle.DynamicProxy;
using CachingServiceWithAOP;
using CachingServiceWithAOP.CachingServices;
using CachingServiceWithAOP.Extensions;

var builder = new ContainerBuilder();
builder.RegisterCachingModule();
builder.RegisterType<LoggingCacheService>().As<ICacheService>().SingleInstance();
builder.RegisterType<Shipping>().As<IShipping>().EnableCacheInterception();
using var container = builder.Build();
var shipping = container.Resolve<IShipping>();

Console.WriteLine(shipping.Quote("Oslo"));
Console.WriteLine(shipping.Quote("Oslo"));
Console.WriteLine(shipping.Days("Oslo"));
shipping.Track("Oslo");

public interface IShipping
{
    decimal Quote(string city);

    int Days(string city);

    void Track(string city);
}

public class Shipping : IShipping
{
    [Cache(0, 0, 30)]
    public decimal Quote(string city)
    {
        Console.WriteLine("pricing " + city);
        return 12.5m;
    }

    [Cache]
    public int Days(string city) => 3;

    public void Track(string city) => Console.WriteLine("tracking " + city);
}

public class LoggingCacheService : ICacheService
{
    private readonly MemoryCacheService inner = new MemoryCacheService(TimeSpan.FromMinutes(1));

    public T? Get<T>(string key) => inner.Get<T>(key);

    public T? Get<T>(string key, Func<T> retrievalFunc) => inner.Get(key, retrievalFunc);

    public void Set<T>(string key, T value) => inner.Set(key, value);

    public void GetByInvocation(IInvocation invocation, TimeSpan? duration = null)
    {
        Console.WriteLine("cache: " + invocation.Method.Name + " for " + (duration?.ToString() ?? "the service's lifetime"));
        inner.GetByInvocation(invocation, duration);
    }
}
cache: Quote for 00:00:30
pricing Oslo
12.5
cache: Quote for 00:00:30
12.5
cache: Days for the service's lifetime
3
tracking Oslo

Track has no [Cache], so it never reaches the cache service. A bare [Cache] passes a null duration, and the inner service applies its own one minute. The interface does not say whether the call was answered from the store, so a hit counter needs the store itself (a MemoryCache of your own, as in the recipe above).

Entries per instance

Every instance of a class shares entries, because the key starts with the instance's ToString(). Override ToString() to return what tells your instances apart (a region, a tenant id, a connection name); How results are cached has the example and its output.

Replace a value you cached by key

For values cached with MemoryCacheService.Get(key, func), Set with the same key replaces the value, and the next Get returns it without running the function:

using System;
using CachingServiceWithAOP.CachingServices;

var cache = new MemoryCacheService(TimeSpan.FromMinutes(10));

decimal LoadRate()
{
    Console.WriteLine("loading the rate");
    return 1.08m;
}

Console.WriteLine(cache.Get("eur-usd", LoadRate));
cache.Set("eur-usd", 1.10m);
Console.WriteLine(cache.Get("eur-usd", LoadRate));
loading the rate
1.08
1.10

The replaced value gets a new lifetime of ten minutes from the Set. The package has no method that removes an entry, and the keys of intercepted calls are built inside the package, so a result cached by [Cache] is replaced only by expiry. Choose its lifetime with that in mind (How results are cached).

Async methods

A method returning Task or Task<T> is cached as the task: callers during and after the first run await the same task, and a task that faults or is cancelled is removed. A method returning ValueTask is never cached. How results are cached shows both.

Not covered here

ASP.NET Core with AutofacServiceProviderFactory, and other containers, were not tested. The interceptor needs Autofac.Extras.DynamicProxy's EnableInterfaceInterceptors(), which EnableCacheInterception() calls.

Clone this wiki locally