Simpleinjector:这是RegisterManyForOpenGeneric正确的方式时,

2019-06-23 22:27发布

使用简单的喷油器与下面介绍的命令模式和这里所描述的查询模式 。 对于其中一个命令,我有2个处理程序实现。 第一个是“正常”的实现,同步执行:

public class SendEmailMessageHandler
    : IHandleCommands<SendEmailMessageCommand>
{
    public SendEmailMessageHandler(IProcessQueries queryProcessor
        , ISendMail mailSender
        , ICommandEntities entities
        , IUnitOfWork unitOfWork
        , ILogExceptions exceptionLogger)
    {
        // save constructor args to private readonly fields
    }

    public void Handle(SendEmailMessageCommand command)
    {
        var emailMessageEntity = GetThisFromQueryProcessor(command);
        var mailMessage = ConvertEntityToMailMessage(emailMessageEntity);
        _mailSender.Send(mailMessage);
        emailMessageEntity.SentOnUtc = DateTime.UtcNow;
        _entities.Update(emailMessageEntity);
        _unitOfWork.SaveChanges();
    }
}

另一种是像这样的命令装饰,但明确地包装了以前的类一个单独的线程来执行命令:

public class SendAsyncEmailMessageHandler 
    : IHandleCommands<SendEmailMessageCommand>
{
    public SendAsyncEmailMessageHandler(ISendMail mailSender, 
        ILogExceptions exceptionLogger)
    {
        // save constructor args to private readonly fields
    }

    public void Handle(SendEmailMessageCommand command)
    {
        var program = new SendAsyncEmailMessageProgram
            (command, _mailSender, _exceptionLogger);
        var thread = new Thread(program.Launch);
        thread.Start();
    }

    private class SendAsyncEmailMessageProgram
    {
        internal SendAsyncEmailMessageProgram(
            SendEmailMessageCommand command
            , ISendMail mailSender
            , ILogExceptions exceptionLogger)
        {
            // save constructor args to private readonly fields
        }

        internal void Launch()
        {
            // get new instances of DbContext and query processor
            var uow = MyServiceLocator.Current.GetService<IUnitOfWork>();
            var qp = MyServiceLocator.Current.GetService<IProcessQueries>();
            var handler = new SendEmailMessageHandler(qp, _mailSender, 
                uow as ICommandEntities, uow, _exceptionLogger);
            handler.Handle(_command);
        }
    }
}

有一段时间simpleinjector是在喊我,告诉我,它发现的2个实现IHandleCommands<SendEmailMessageCommand> 我发现了以下工作,但不能确定它是否是最好的/最佳方式。 我想明确地登记这一个接口使用异步执行:

container.RegisterManyForOpenGeneric(typeof(IHandleCommands<>), 
    (type, implementations) =>
    {
        // register the async email handler
        if (type == typeof(IHandleCommands<SendEmailMessageCommand>))
            container.Register(type, implementations
                .Single(i => i == typeof(SendAsyncEmailMessageHandler)));

        else if (implementations.Length < 1)
            throw new InvalidOperationException(string.Format(
                "No implementations were found for type '{0}'.",
                    type.Name));
        else if (implementations.Length > 1)
            throw new InvalidOperationException(string.Format(
                "{1} implementations were found for type '{0}'.",
                    type.Name, implementations.Length));

        // register a single implementation (default behavior)
        else
            container.Register(type, implementations.Single());

    }, assemblies);

我的问题:这是正确的方式,或者是有什么好? 例如,我想重用Simpleinjector所有其他类型的抛出,而不必明确地把他们丢在回调现有例外。

更新回复史蒂芬的答案

我已经更新了我的问题更加明确。 我已经这样实现了它的原因是因为作为操作的一部分,命令更新一个System.Nullable<DateTime>所谓财产SentOnUtc在DB实体后MailMessage已成功发送。

ICommandEntitiesIUnitOfWork都由一个实体框架中实现DbContext class.The DbContext每HTTP上下文注册,使用这里所描述的方法 :

container.RegisterPerWebRequest<MyDbContext>();
container.Register<IUnitOfWork>(container.GetInstance<MyDbContext>);
container.Register<IQueryEntities>(container.GetInstance<MyDbContext>);
container.Register<ICommandEntities>(container.GetInstance<MyDbContext>);

该默认行为RegisterPerWebRequest在simpleinjector维基扩展方法是注册一个瞬态的实例时HttpContext为null(这将在新推出的线程)。

var context = HttpContext.Current;
if (context == null)
{
    // No HttpContext: Let's create a transient object.
    return _instanceCreator();
...

这就是为什么启动方法使用Service Locator模式获得的单个实例DbContext ,然后直接传送到同步命令处理程序的构造函数。 为了使_entities.Update(emailMessageEntity)_unitOfWork.SaveChanges()线工作,两者都必须使用相同的DbContext实例。

注意:理想情况下,发送电子邮件应该由一个独立的投票工作人员来处理。 这个命令基本上是一个队列结算所。 在DB中EmailMessage实体已经全部发送电子邮件所需的信息。 这个命令只是抓住从数据库中未发送的一个,将其发送,然后记录动作的日期时间。 这样的命令可以通过查询从一个不同的进程/应用程序来执行,但我不会接受这个问题这样回答。 现在,我们需要在某种HTTP请求事件的触发它揭开序幕这个命令。

Answer 1:

确实有更简单的方法来做到这一点。 例如,而不是注册BatchRegistrationCallback正如你在上次的代码段一样,你可以利用的OpenGenericBatchRegistrationExtensions.GetTypesToRegister方法。 该方法由内部使用RegisterManyForOpenGeneric方法,并允许您将它们发送到之前过滤返回类型RegisterManyForOpenGeneric过载:

var types = OpenGenericBatchRegistrationExtensions
    .GetTypesToRegister(typeof(IHandleCommands<>), assemblies)
    .Where(t => !t.Name.StartsWith("SendAsync"));

container.RegisterManyForOpenGeneric(
    typeof(IHandleCommands<>), 
    types);

但我认为这将是更好地使你的设计进行一些更改。 当你改变你的异步命令处理程序到一个通用的装饰,你完全完全消除问题。 这样一个普通的装饰看起来是这样的:

public class SendAsyncCommandHandlerDecorator<TCommand>
    : IHandleCommands<TCommand>
{
    private IHandleCommands<TCommand> decorated;

    public SendAsyncCommandHandlerDecorator(
         IHandleCommands<TCommand> decorated)
    {
        this.decorated = decorated;
    }

    public void Handle(TCommand command)
    {
        // WARNING: THIS CODE IS FLAWED!!
        Task.Factory.StartNew(
            () => this.decorated.Handle(command));
    }
}

请注意,这是装饰,因为有缺陷的原因,我将在后面解释,但我们与此去教育的缘故。

使得这类通用的,可以重复使用这种类型的多个命令。 因为这种类型是通用的,在RegisterManyForOpenGeneric将跳过这个(因为它不能猜测通用型)。 这使您可以按如下所示注册的装饰:

container.RegisterDecorator(
    typeof(IHandleCommands<>), 
    typeof(SendAsyncCommandHandler<>));

在你的情况然而,你不希望这个装饰周围的所有处理包裹(如前面的注册一样)。 有一个RegisterDecorator重载函数取一个判断,它允许你指定应用这个装饰:

container.RegisterDecorator(
    typeof(IHandleCommands<>), 
    typeof(SendAsyncCommandHandlerDecorator<>),
    c => c.ServiceType == typeof(IHandleCommands<SendEmailMessageCommand>));

施加这个谓词,则SendAsyncCommandHandlerDecorator<T>将仅被施加到IHandleCommands<SendEmailMessageCommand>处理程序。

另一种选择(我喜欢)是注册的一个封闭的通用版本SendAsyncCommandHandlerDecorator<T>版本。 这样您就不必指定谓词:

container.RegisterDecorator(
    typeof(IHandleCommands<>), 
    typeof(SendAsyncCommandHandler<SendEmailMessageCommand>));

但是正如我指出的,对于给定装饰的代码是有缺陷的 ,因为你应该总是建立在一个新的线程一个新的依赖关系图,并从不依赖通过从线程线程(原来的装饰一样)。 这个在这篇文章中的更多信息: 如何在多线程应用程序依赖注入工作 。

因此,答案其实是比较复杂的,因为这一般装饰真的应该是取代了原来的命令处理器(或者甚至可能是装饰包裹的处理程序链)的代理。 这个代理必须能够建立一个新的对象图在一个新的线程。 此代理应该是这样的:

public class SendAsyncCommandHandlerProxy<TCommand>
    : IHandleCommands<TCommand>
{
    Func<IHandleCommands<TCommand>> factory;

    public SendAsyncCommandHandlerProxy(
         Func<IHandleCommands<TCommand>> factory)
    {
        this.factory = factory;
    }

    public void Handle(TCommand command)
    {
        Task.Factory.StartNew(() =>
        {
            var handler = this.factory();
            handler.Handle(command);
        });
    }
}

虽然简单的喷油器没有内置在解决支持Func<T>的工厂, RegisterDecorator方法是例外。 这样做的原因是,这将是非常乏味的无框架的支持来注册Func键依赖装饰。 换句话说,注册时SendAsyncCommandHandlerProxyRegisterDecorator方法,简单的进样器将自动注入Func<T>委托,它可以创建装饰类型的新实例。 由于代理只refences一(单)工厂(和无状态),我们甚至可以把它注册为单例:

container.RegisterSingleDecorator(
    typeof(IHandleCommands<>), 
    typeof(SendAsyncCommandHandlerProxy<SendEmailMessageCommand>));

显然,可以混合使用该注册与其他RegisterDecorator注册。 例:

container.RegisterManyForOpenGeneric(
    typeof(IHandleCommands<>),
    typeof(IHandleCommands<>).Assembly);

container.RegisterDecorator(
    typeof(IHandleCommands<>),
    typeof(TransactionalCommandHandlerDecorator<>));

container.RegisterSingleDecorator(
    typeof(IHandleCommands<>), 
    typeof(SendAsyncCommandHandlerProxy<SendEmailMessageCommand>));

container.RegisterDecorator(
    typeof(IHandleCommands<>),
    typeof(ValidatableCommandHandlerDecorator<>));

该登记包装用任何命令处理程序TransactionalCommandHandlerDecorator<T>任选地与异步代理装饰它,并总是与包装它ValidatableCommandHandlerDecorator<T> 这允许你做验证同步(在同一个线程),并在验证成功时的命令处理上一个新的线程,旋转,在该线程事务中运行。

因为你的一些依赖注册每个Web请求,这意味着在没有web请求,这是他们的方式,这是在简单的喷油器实现(好像是这样,他们将得到一个新的(瞬态)的实例抛出一个异常当你开始一个新的线程来运行代码)。 当你实现你的EF多个接口DbContext ,这意味着简单的喷油器会为每个构造注入接口创建一个新的实例,如你所说,这将是一个问题。

你需要重新配置DbContext ,因为一个纯粹的每个Web请求不会做。 有几种解决方案,但我认为最好是让一个混合PerWebRequest / PerLifetimeScope实例。 你需要在每终生范围扩展包这一点。 另外请注意,也是一个扩展包的每个Web请求 ,所以你不必使用任何自定义代码。 当你这样做,你可以定义下列注册:

container.RegisterPerWebRequest<DbContext, MyDbContext>();
container.RegisterPerLifetimeScope<IObjectContextAdapter,
    MyDbContext>();

// Register as hybrid PerWebRequest / PerLifetimeScope.
container.Register<MyDbContext>(() =>
{
    if (HttpContext.Current != null)
        return (MyDbContext)container.GetInstance<DbContext>();
    else
        return (MyDbContext)container
            .GetInstance<IObjectContextAdapter>();
});

更新简单喷油器2现在有生活方式的明确概念,这使得先前的注册更加容易。 因此,下面的登记劝:

var hybrid = Lifestyle.CreateHybrid(
    lifestyleSelector: () => HttpContext.Current != null,
    trueLifestyle: new WebRequestLifestyle(),
    falseLifestyle: new LifetimeScopeLifestyle());

// Register as hybrid PerWebRequest / PerLifetimeScope.
container.Register<MyDbContext, MyDbContext>(hybrid);

由于简单的注射器只允许登记一次类型(它不支持键控登记),所以不可能同时具有PerWebRequest生活方式,以及一个PerLifetimeScope生活方式来注册MyDbContext。 所以我们要骗一点,所以我们做两个注册(每一个生活方式),并选择不同的服务类型(的DbContext和IObjectContextAdapter)。 服务类型并不重要,但MyDbContext必须实现/从服务类型(随时实现虚拟接口上的继承MyDbContext如果这是方便)。

除了这两个注册,我们需要一个第三注册,映射,这使我们能够获得适当的生活方式了。 这是Register<MyDbContext>其中回来基于该操作是否是HTTP请求或不内部执行适当的实例。

AsyncCommandHandlerProxy将不得不开始新的生命周期范围内,这是做如下:

public class AsyncCommandHandlerProxy<T>
    : IHandleCommands<T>
{
    private readonly Func<IHandleCommands<T>> factory;
    private readonly Container container;

    public AsyncCommandHandlerProxy(
        Func<IHandleCommands<T>> factory,
        Container container)
    {
        this.factory = factory;
        this.container = container;
    }

    public void Handle(T command)
    {
        Task.Factory.StartNew(() =>
        {
            using (this.container.BeginLifetimeScope())
            {
                var handler = this.factory();
                handler.Handle(command);
            }            
        });    
    }    
}

需要注意的是容器添加为依赖性AsyncCommandHandlerProxy

现在,任何MyDbContext时所解决的实例HttpContext.Current为null,将获得每终身范围实例,而不是一个新的瞬态的实例。



文章来源: Simpleinjector: Is this the right way to RegisterManyForOpenGeneric when I have 2 implementations and want to pick one?