I have a widely used cache interface in a web application with the implementation currently registered as SingleInstance.
This current cache implementation assumes single threaded initialization, but once initialized is immutable, so is safely shared across multiple threads.
However, this means that currently, if the underlying values change, the cache doesn't get updated until the application is restarted. While updating the underlying values is rare, we would now like to provide application behavior that modifies the underlying values, and then tells the cache to refresh.
I could modify the cache implementation to use locking, or perhaps utilize one of the .NET concurrent collections to safely update the cache values.
However, I'm wondering if autofac provides a capability that would allow me to change out the registered instance for a new instance on the next request, so that the cache implementation itself would not need to be modified.
So the ideal behavior would be, that when we modify the underlying values, we trigger the creation of a new cache instance. Once the instance is finished initializing, all in-progress requests continue with the old cache instance, any new http request scopes resolve to the updated instance.
Does autofac provide a built-in way to support this scenario?
You can never safely replace a singleton registered instance in your container. Once other singleton components depend on that, they will simply hold a reference to the old instance, and replacing the instance in the container means that some components (that will be created after the replace action) will refer to the new instance, while other components keep referring to the old instance. This will hardly ever lead to the behavior you like, and will most likely cause bugs.
My advice is never try to change your container's registrations, once the application is running. This will very quickly become quite complex to oversee whether the situation is correct and is thread-safe. For instance, what if you replace the instance at the time that the object graph for another thread is being resolved? It could mean that that object graph holds both a reference to the old and the new instance.
Instead, solve this problem at the application level. First of all, you need two APIs; one for reading the cache, and a second for updating the cache. Both can be implemented using the same component though:
// Very simplified version of what you actually might need
interface ICache { CacheObject Get(); }
interface ICacheUpdater { void Set(CacheObject o); }
A simplistic implementation could look like this:
sealed class Cache : ICache, ICacheUpdater
{
private static CacheObject instance;
public void Set(CacheObject o) => instance = o;
public CacheObject Get() => instance;
}
This implementation might work, but if the cache is retrieved multiple times within the same request, it's possible to read both the old and the new values within the same request (since a different thread can call Set in between). This might be a problem. In that case, you can change the implementation to the following:
sealed class HttpCache : ICache, ICacheUpdater
{
private static readonly object key = typeof(HttpCache);
private static CacheObject instance;
private static IDictionary items => HttpContext.Current.Items;
public void Set(CacheObject o) => instance = o;
public CacheObject Get() => (CacheObject)items[key] ?? (items[key] = instance);
}
In this implementation an extra reference to the cache object is stored in the HttpContext.Items dictionary. This ensures that during the execution of a single (web) request, always the same instance is retrieved.
This example assumes you are running a web application, but you can easily imagine a solution for a different application type.
To update a component registered as a single instance, you can have a registration like this :
builder.RegisterType<ServiceProvider>().SingleInstance();
builder.Register(c => c.Resolve<ServiceProvider>().Service).As<IService>();
and ServiceProvider like this :
public class ServiceProvider
{
public ServiceProvider()
{
this.Service = new Service();
}
public IService Service { get; set; }
}
To update the instance you only have to do that :
container.Resolve<ServiceProvider>().Service = newInstance;
The second part of the question may be more difficult :
Once the instance is finished initializing, all in-progress requests continue with the old cache instance, any new http request scopes resolve to the updated instance.
What you want is to inject a single instance registration in a specific scope. To make this, you can use the ChildLifetimeScopeBeginning event to set the instance for the whole life of scope.
builder.RegisterType<ServiceProvider>().Named<ServiceProvider>("root").SingleInstance();
builder.RegisterType<ServiceProvider>().InstancePerRequest();
builder.Register(c => c.Resolve<ServiceProvider>().Service).As<IService>();
IContainer container = builder.Build();
container.ChildLifetimeScopeBeginning += (sender, e) =>
{
ServiceProvider scopeServiceProvider = e.LifetimeScope.Resolve<ServiceProvider>();
ServiceProvider rootServiceProvider = container.ResolveNamed<ServiceProvider>("root");
scopeServiceProvider.Service = rootServiceProvider.Service;
};
To change the global IService instance you will have to resolve the "root" named ServiceProvider
scope.ResolveNamed<ServiceProvider>("root").Service = newInstance;
and to change the scope only IService instance you will resolve a normal ServiceProvider
scope.Resolve<ServiceProvider>().Service = newInstance;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With