在简单注入器v3.0.3中,使用ResolveUnregisteredType会导致Torn Lifestyle警告

本文关键字:ResolveUnregisteredType Torn 警告 Lifestyle 使用 注入器 简单 v3 | 更新日期: 2023-09-27 18:10:24

在创建新的web应用程序时,我使用一个自制的"外部"库来获得一些基本的基础设施。我最近对我的存储库的工作方式做了一些改变,并在简单注入器诊断警告中遇到了这个警告:

SimpleInjector。DiagnosticVerificationException:配置为无效的。报告了以下诊断警告:IUnitOfWork的注册映射到与IUnitOfWork注册相同的实现和生活方式,IUnitOfWork、IUnitOfWork和IUnitOfWork都可以。它们都映射到EmptyUnitOfWork (Singleton)。这将导致每个注册解析到不同的实例:每个注册都有自己的实例实例。的详细信息,请参阅"错误"属性警告。请参见https://simpleinjector.org/diagnostics如何修复问题及如何抑制个别警告。

我的库的核心程序集有一个IUnitOfWork的空实现称为EmptyUnitOfWork,它只是一个简单的无操作类:

internal sealed class EmptyUnitOfWork : IUnitOfWork
{
    public void SaveChanges()
    {
        // Do nothing
    }
}
当没有其他可用的工作单元时,将该类注册到容器中。我通过使用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());使警告消失,但生活方式不是单身吗?

在简单注入器v3.0.3中,使用ResolveUnregisteredType会导致Torn Lifestyle警告

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

你正在经历的是一个在Simple Injector v3中引入的突破性更改/错误。在简单注入器v1和v2中,ResolveUnregisteredType的注册结果被缓存;这意味着对Verify()的调用只会触发你的自定义委托一次。然而,在Simple Injector v3.0中,结果注册不再被缓存。这是一个疏忽,已经被忽略了。我们的想法是让ResolveUnregisteredType具有上下文意识。为了了解上下文,缓存不再是一种选择。所以缓存被删除了,但我们最终决定不让ResolveUnregisteredType上下文感知,而我们忘记了再次添加缓存。

然而,这种意外行为的有趣之处在于,它暴露了代码中的一个bug。这个bug甚至在你使用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>()使第一次注册成为无条件的,但这是行不通的,因为简单注入器会检测到第二次注册永远不能应用,并且会抛出异常。因此,c => true谓词的使用抑制了这种检测。我通常不会建议这样的构造,因为它蒙蔽了简单注入器。然而,在你的情况下,这似乎是合理的,因为两个注册是在不同的时刻进行的。

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

相关文章:
  • 没有找到相关文章