Simpleinjector:这是RegisterManyForOpenGeneric正确的方法时,我有2实现,想挑一个? [英] Simpleinjector: Is this the right way to RegisterManyForOpenGeneric when I have 2 implementations and want to pick one?

本文介绍了Simpleinjector:这是RegisterManyForOpenGeneric正确的方法时,我有2实现,想挑一个?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

使用简单的注射器这里并介绍的命令模式这里描述的查询模式。对于命令之一,我有2个处理器实现。第一个是一个正常执行,其执行同步

Using simple injector with the command pattern described here and the query pattern described here. For one of the commands, I have 2 handler implementations. The first is a "normal" implementation that executes synchronously:

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();
    }
}

另外就是像一个指挥装饰,但明确地包装了previous类在一个单独的线程来执行命令:

The other is like a command decorator, but explicitly wraps the previous class to execute the command in a separate thread:

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大喊大叫我,告诉我它找到 IHandleCommands&LT的2实现; SendEmailMessageCommand&GT; 。我发现了以下工作,但不能确定它是否是最好的/最佳方式。我想明确地注册这个一个接口使用异步执行:

For a while simpleinjector was yelling at me, telling me that it found 2 implementations of IHandleCommands<SendEmailMessageCommand>. I found that the following works, but not sure whether it is the best / optimal way. I want to explicitly register this one interface to use the Async implementation:

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所有其他类型的抛出现有例外?明确回调。

My question: is this the right way, or is there something better? For example, I'd like to reuse the existing exceptions thrown by Simpleinjector for all other implementations instead of having to throw them explicitly in the callback.

更新回复史蒂芬的答案

我已经更新了我的问题要更加明确。我已经实现这种方式的原因是因为作为操作的一部分,命令更新一个 System.Nullable&LT; D​​ateTime的&GT; 属性名为 SentOnUtc MAILMESSAGE 发送成功后,一个数据库实体。

I have updated my question to be more explicit. The reason I have implemented it this way is because as part of the operation, the command updates a System.Nullable<DateTime> property called SentOnUtc on a db entity after the MailMessage is successfully sent.

ICommandEntities IUnitOfWork 都是由实体框架实施的DbContext class.The 的DbContext 每HTTP上下文注册,使用的这里所描述的方法的:

The ICommandEntities and IUnitOfWork are both implemented by an entity framework DbContext class.The DbContext is registered per http context, using the method described here:

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

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

The default behavior of the RegisterPerWebRequest extension method in the simpleinjector wiki is to register a transient instance when the HttpContext is null (which it will be in the newly launched thread).

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实例。

This is why the Launch method uses the service locator pattern to get a single instance of DbContext, then passes it directly to the synchronous command handler constructor. In order for the _entities.Update(emailMessageEntity) and _unitOfWork.SaveChanges() lines to work, both must be using the same DbContext instance.

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

NOTE: Ideally, sending the email should be handled by a separate polling worker. This command is basically a queue clearing house. The EmailMessage entities in the db already have all of the information needed to send the email. This command just grabs an unsent one from the database, sends it, then records the DateTime of the action. Such a command could be executed by polling from a different process / app, but I will not accept such an answer for this question. For now, we need to kick off this command when some kind of http request event triggers it.

推荐答案

确实有更简单的方法来做到这一点。例如,而不是注册一个 BatchRegistrationCallback 为你上code片段一样,你可以利用的 OpenGenericBatchRegistrationExtensions.GetTypesToRegister 方法。这种方法是由 RegisterManyForOpenGeneric 方法在内部使用,并允许您过滤返回类型,您将其发送到 RegisterManyForOpenGeneric 过载:

There are indeed easier ways to do this. For instance, instead of registering a BatchRegistrationCallback as you did in your last code snippet, you can make use of the OpenGenericBatchRegistrationExtensions.GetTypesToRegister method. This method is used internally by the RegisterManyForOpenGeneric methods, and allows you to filter the returned types before you send them to an RegisterManyForOpenGeneric overload:

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

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

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

But I think it would be better to make a few changes to your design. When you change your async command handler to a generic decorator, you completely remove the problem altogether. Such a generic decorator could look like this:

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));
    }
}

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

Note that this decorator is flawed because of reasons I'll explain later, but let's go with this for the sake of education.

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

Making this type generic, allows you to reuse this type for multiple commands. Because this type is generic, the RegisterManyForOpenGeneric will skip this (since it can't guess the generic type). This allows you to register the decorator as follows:

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

在你的情况然而,你不希望这个装饰周围的所有处理包裹(如previous注册一样)。有一个 RegisterDecorator 重载需要predicate,允许你指定何时应用这个装饰:

In your case however, you don't want this decorator to be wrapped around all handlers (as the previous registration does). There is an RegisterDecorator overload that takes a predicate, that allows you to specify when to apply this decorator:

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

通过应用这个predicate,在 SendAsyncCommandHandlerDecorator&LT; T&GT; 将只适用于 IHandleCommands&LT; SendEmailMessageCommand&GT; 处理程序。

With this predicate applied, the SendAsyncCommandHandlerDecorator<T> will only be applied to the IHandleCommands<SendEmailMessageCommand> handler.

另一种选择(我preFER)是注册 SendAsyncCommandHandlerDecorator℃的封闭的仿制药; T&GT; 版本。这样可以节省您不必指定predicate:

Another option (which I prefer) is to register a closed generic version of the SendAsyncCommandHandlerDecorator<T> version. This saves you from having to specify the predicate:

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

然而,正如我所提到的,code为给定的装饰是有缺陷的,因为你应该总是建立在一个新的线程一个新的依赖关系图,并决不依赖传球线程线程(其中原始装饰一样)。这个在这篇文章中的更多信息:的如何在多线程应用程序的依赖注入工作。

As I noted however, the code for the given decorator is flawed, because you should always build a new dependency graph on a new thread, and never pass on dependencies from thread to thread (which the original decorator does). More information about this in this article: How to work with dependency injection in multi-threaded applications.

因此​​,答案实际上是更复杂的,因为这个通用装饰真正应该替换原始命令处理程序(或甚至可能一装饰包装的处理程序链)的代理。此代理必须能够建立一个新的对象图中一个新的线程。此代理应该是这样的:

So the answer is actually more complex, since this generic decorator should really be a proxy that replaces the original command handler (or possibly even a chain of decorators wrapping a handler). This proxy must be able to build up a new object graph in a new thread. This proxy would look like this:

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键&LT支持; T&GT; 工厂, RegisterDecorator 方法除外。这样做的原因是,这将是非常乏味的无框架的支持,注册Func键依赖性装饰。换句话说,在注册时的 SendAsyncCommandHandlerProxy RegisterDecorator 方法,简单的喷油器将自动注入 Func键&LT; T&GT; 委托,它可以创建装饰类型的新实例。由于代理只refences一(单)工厂(和无状态),我们甚至可以把它注册为单:

Although Simple Injector has no built-in support for resolving Func<T> factory, the RegisterDecorator methods are the exception. The reason for this is that it would be very tedious to register decorators with Func dependencies without framework support. In other words, when registering the SendAsyncCommandHandlerProxy with the RegisterDecorator method, Simple Injector will automatically inject a Func<T> delegate that can create new instances of the decorated type. Since the proxy only refences a (singleton) factory (and is stateless), we can even register it as singleton:

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&LT; T&GT; ,任选与异步代理装饰它,并始终与 ValidatableCommandHandlerDecorator&LT把它包装; T&GT; 。这允许你做验证同步(在同一个线程),并在验证成功时的命令处理上一个新的线程,旋转,在该线程事务中运行。

This registration wraps any command handler with a TransactionalCommandHandlerDecorator<T>, optionally decorates it with the async proxy, and always wraps it with a ValidatableCommandHandlerDecorator<T>. This allows you to do the validation synchronously (on the same thread), and when validation succeeds, spin of handling of the command on a new thread, running in a transaction on that thread.

由于一些你的依赖正在注册每个Web请求,这意味着<击>,他们将在没有网络请求,这是他们的方式,这是得到一个新的(瞬态)的实例抛出一个异常在简单的喷油器实现的(好像是这样,当你开始一个新的线程来运行code)。当您正在实现与EF多个接口的DbContext ,这意味着简单的喷油器将为每个构造注射界面来创建一个新的实例,如你所说,这将是一个问题。

Since some of your dependencies are registered Per Web Request, this means that they would get a new (transient) instance an exception is thrown when there is no web request, which is they way this is implemented in the Simple Injector (as is the case when you start a new thread to run the code). As you are implementing multiple interfaces with your EF DbContext, this means Simple Injector will create a new instance for each constructor-injected interface, and as you said, this will be a problem.

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

You'll need to reconfigure the DbContext, since a pure Per Web Request will not do. There are several solutions, but I think the best is to make an hybrid PerWebRequest/PerLifetimeScope instance. You'll need the Per Lifetime Scope extension package for this. Also note that also is an extension package for Per Web Request, so you don't have to use any custom code. When you've done this, you can define the following registration:

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现在有生活方式的明确概念,这使得previous注册容易得多。因此,以下注册劝:

UPDATE Simple Injector 2 now has the explicit notion of lifestyles and this makes the previous registration much easier. The following registration is therefore adviced:

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 如果这是方便)继承。

除了这两个注册,我们需要第三登记,映射,这使我们能够得到适当的生活方式回来。这是注册&LT; MyDbContext方式&gt; 这回来基于该操作是否是HTTP请求的内部执行或不正确的实例

Besides these two registrations, we need a third registration, a mapping, that allows us to get the proper lifestyle back. This is the Register<MyDbContext> which gets the proper instance back based on whether the operation is executed inside a HTTP request or not.

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

Your AsyncCommandHandlerProxy will have to start a new lifetime scope, which is done as follows:

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

Note that the container is added as dependency of the AsyncCommandHandlerProxy.

现在,就是解决当 HttpContext.Current 为空的 MyDbContext 实例,将获得每终身范围实例,而不是一个新的瞬态的实例。

Now, any MyDbContext instance that is resolved when HttpContext.Current is null, will get a Per Lifetime Scope instance instead of a new transient instance.

这篇关于Simpleinjector:这是RegisterManyForOpenGeneric正确的方法时,我有2实现,想挑一个?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆