在 DI 容器中安全地重新初始化“单实例”依赖项

Nat*_*han 3 dependency-injection autofac

我在 Web 应用程序中有一个广泛使用的缓存接口,其实现当前注册为SingleInstance.

当前的缓存实现假定单线程初始化,但一旦初始化是不可变的,因此可以在多个线程之间安全地共享。

但是,这意味着当前,如果基础值发生更改,则缓存不会更新,直到应用程序重新启动。虽然更新底层值很少见,但我们现在希望提供修改底层值的应用程序行为,然后告诉缓存刷新。

我可以修改缓存实现以使用锁定,或者可能利用 .NET 并发集合之一来安全地更新缓存值。

但是,我想知道 autofac 是否提供了一种功能,允许我在下一个请求中为新实例更改已注册的实例,以便不需要修改缓存实现本身。

所以理想的行为是,当我们修改底层值时,我们会触发一个新缓存实例的创建。一旦实例完成初始化,所有正在进行的请求将继续使用旧的缓存实例,任何新的 http 请求范围解析为更新的实例。

autofac 是否提供内置方式来支持这种情况?

Ste*_*ven 6

您永远无法安全地替换容器中的单例注册实例。一旦其他单例组件依赖它,它们将简单地持有对旧实例的引用,并且替换容器中的实例意味着某些组件(将在替换操作后创建)将引用新实例,而其他组件继续参考旧实例。这几乎不会导致您喜欢的行为,并且很可能会导致错误。

我的建议是永远不要尝试在应用程序运行后更改容器的注册。这将很快变得非常复杂,以监督情况是否正确并且是线程安全的。例如,如果您在解析另一个线程的对象图时替换实例怎么办?这可能意味着该对象图同时包含对旧实例和新实例的引用。

相反,在应用程序级别解决这个问题。首先,你需要两个 API;一个用于读取缓存,第二个用于更新缓存。两者都可以使用相同的组件来实现:

// Very simplified version of what you actually might need
interface ICache { CacheObject Get(); }

interface ICacheUpdater { void Set(CacheObject o); }
Run Code Online (Sandbox Code Playgroud)

一个简单的实现可能如下所示:

sealed class Cache : ICache, ICacheUpdater
{
    private static CacheObject instance;
    public void Set(CacheObject o) => instance = o;
    public CacheObject Get() => instance;
}
Run Code Online (Sandbox Code Playgroud)

此实现可能有效,但如果在同一个请求中多次检索缓存,则可以在同一个请求中读取旧值和新值(因为不同的线程可以Set在两者之间调用)。这可能是一个问题。在这种情况下,您可以将实现更改为以下内容:

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);
}
Run Code Online (Sandbox Code Playgroud)

在这个实现中,对缓存对象的额外引用存储在HttpContext.Items字典中。这确保在执行单个(Web)请求期间,始终检索相同的实例。

此示例假设您正在运行一个 Web 应用程序,但您可以轻松想象出针对不同应用程序类型的解决方案。