在 Simple Injector v3.0.3 中使用 ResolveUnregisteredType 导致 Torn Lifestyle 警告

Use of ResolveUnregisteredType causes Torn Lifestyle warning in Simple Injector v3.0.3

我在创建新的网络应用程序时使用自制的 "external" 库来获得一些基本的基础设施。我最近对我的存储库的工作方式做了一些更改,并从 Simple Injector Diagnostic Warnings 中遇到了这个警告:

SimpleInjector.DiagnosticVerificationException : The configuration is invalid. The following diagnostic warnings were reported: -[Torn Lifestyle] The registration for IUnitOfWork maps to the same implementation and lifestyle as the registrations for IUnitOfWork, IUnitOfWork, IUnitOfWork and IUnitOfWork do. They all map to EmptyUnitOfWork (Singleton). This will cause each registration to resolve to a different instance: each registration will have its own instance. See the Error property for detailed information about the warnings. Please see https://simpleinjector.org/diagnostics how to fix problems and how to suppress individual warnings.

我的库的核心程序集有一个名为 EmptyUnitOfWorkIUnitOfWork 的空实现,它只是一个简单的空操作 class:

internal sealed class EmptyUnitOfWork : IUnitOfWork
{
    public void SaveChanges()
    {
        // Do nothing
    }
}

当没有其他工作单元可用时,class 将在容器中注册。我通过使用 container.ResolveUnregisteredType(...) 来做到这一点:

// Register an EmptyUnitOfWork to be returned when a IUnitOfWork is requested:
container.ResolveUnregisteredType += (sender, e) =>
{
    if (e.UnregisteredServiceType == typeof(IUnitOfWork))
    {
        // Register the instance as singleton.
        var registration = Lifestyle.Singleton.CreateRegistration<IUnitOfWork, EmptyUnitOfWork>(container);
        e.Register(registration);
    }
};

我第一次使用上述方法 - 但在这个测试中效果很好:

[Fact]
public void RegistersEmptyUnitOfWork_AsSingleton_WhenIUnitOfWorkIsNotRegisted()
{
    var instance = _container.GetInstance<IUnitOfWork>();
    var registration = _container.GetRegistration(typeof (IUnitOfWork));

    Assert.NotNull(instance);
    Assert.IsType<EmptyUnitOfWork>(instance);
    Assert.Equal(Lifestyle.Singleton, registration.Lifestyle);
}

现在有趣的是,我在支持库中有一个扩展方法,如果我的应用程序需要,它会注册一个 EntityFramework IUnitOfWork

public static void RegisterEntityFramework<TContext>(this Container container) where TContext : DbContext
{
    if (container == null) 
        throw new ArgumentNullException(nameof(container));

    var lifestyle = Lifestyle.CreateHybrid(() =>
        HttpContext.Current != null,
        new WebRequestLifestyle(),
        new LifetimeScopeLifestyle()
    );

    container.Register<DbContext, TContext>(lifestyle);
    container.Register<IUnitOfWork, EntityFrameworkUnitOfWork>(lifestyle);
    container.Register(typeof (IRepository<>), typeof (EntityFrameworkRepository<>), lifestyle);
}

但不知何故,这会引发简单注入器的警告 - 但我只是注入了 EntityFrameworkUnitOfWork,所以不应触发 EmptyUnitOfWork

这种设计的原因是我的核心库中有一个 CommandTransactionDecorator,它使用 IUnitOfWork 来保存更改。如果应用程序不需要 IUnitOfWork,我只想有一个空的。

作为参考,这是装饰器:

 internal sealed class CommandTransactionDecorator<TCommand> : IHandleCommand<TCommand> where TCommand : ICommand
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly Func<IHandleCommand<TCommand>> _handlerFactory;

    public CommandTransactionDecorator(IUnitOfWork unitOfWork, Func<IHandleCommand<TCommand>> handlerFactory)
    {
        _unitOfWork = unitOfWork;
        _handlerFactory = handlerFactory;
    }

    public void Handle(TCommand command)
    {
        _handlerFactory().Handle(command);
        _unitOfWork.SaveChanges();
    }
}

更新

好像是这个注册发出了警告:

var registration = Lifestyle.Singleton.CreateRegistration<IUnitOfWork, EmptyUnitOfWork>(container);
e.Register(registration);

改成e.Register(() => new EmptyUnitOfWork());警告消失,但生活方式不是单身?

您看到的是 ResolveUnregisteredType 被多次调用。这会导致对同一类型进行多个单例注册。每个注册都有自己的实例。这将导致应用程序包含该类型的多个实例,这通常不是您将类型注册为单例时希望发生的情况。由于你的EmptyUnitOfWork没有任何行为,所以很可能没有问题,但是Simple Injector显然猜不到是这种情况,所以抛出异常。

然而,您遇到的是 Simple Injector v3 中引入的重大更改/错误。在 Simple Injector v1 和 v2 中缓存了 ResolveUnregisteredType 的结果注册;这意味着对 Verify() 的调用只会触发您的自定义委托一次。然而,在 Simple Injector v3.0 中,不再缓存生成的注册。这是一个疏忽,已经溜走了。这个想法是让 ResolveUnregisteredType 上下文感知。为了上下文感知,缓存不再是一种选择。所以缓存被删除了,但我们最终决定不让 ResolveUnregisteredType 上下文感知,而我们忘记再次添加缓存。

然而,这种意外行为的有趣之处在于,它暴露了您代码中的错误。这个错误在你使用 v2 的时候就已经存在了,但是 v3 现在(不小心)用它打了你一记耳光。使用 v2,注册的正确性取决于 Verify() 方法的使用。 Verify() 在单个线程上构建所有对象图。但是,如果不使用 Verify(),对象图将被延迟编译,如果您是 运行 多线程应用程序,多个线程可以同时调用 ResolveUnregisteredType;简单注入器从未锁定 ResolveUnregisteredType 并且已记录在案。

所以这样做的结果是,如果不调用 Verify(),您仍然可能会在该特定组件的多个注册中结束,这当然会再次导致非常丑陋且难以发现的问题,通常在生产中只以某种方式出现一次。

实际上您应该这样写注册:

Lazy<Registration> registration = new Lazy<Registration>(() =>
    Lifestyle.Singleton.CreateRegistration<IUnitOfWork, EmptyUnitOfWork>(container));

container.ResolveUnregisteredType += (sender, e) => {
    if (e.UnregisteredServiceType == typeof(IUnitOfWork)) {
        e.Register(registration.Value);
    }
};

然而,使用 Simple Injector v3,您几乎不必再使用 ResolveUnregisteredType 事件。您可以改为进行以下注册:

container.RegisterConditional<IUnitOfWork, EntityFrameworkUnitOfWork>(Lifestyle.Scoped,
    c => true);

// NOTE: This registration must be made second
container.RegisterConditional<IUnitOfWork, EmptyUnitOfWork>(Lifestyle.Singleton, 
    c => !c.Handled);

这完全解决了必须考虑多线程的问题。这里我们做了两个条件注册,其中第一个总是应用(使用谓词 c => true)。您可能想使用 Register<IUnitOfWork, EFUoW>() 使第一次注册成为无条件的,但这是行不通的,因为 Simple Injector 会检测到第二次注册永远无法应用,并且会抛出异常。所以使用 c => true 谓词会抑制这种检测。我通常不会建议这样的构造,因为它会使 Simple Injector 蒙蔽。然而,在您的情况下,这似乎是合理的,因为两次注册都是在不同的时间进行的。

我现在必须考虑是否要在 v3 中更改此行为并进行缓存。缓存的优点是可以提高性能,缺点是隐藏bug