4

使用簡單的進樣器with the command pattern described herethe query pattern described here。對於其中一個命令,我有2個處理程序實現。第一個是「正常」的實現,同步執行:Simpleinjector:當我有2個實現並且想要選擇一個時,這是註冊ManyForOpenGeneric的正確方法嗎?

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

對於一陣簡單的噴射器對我大喊,告訴我它找到了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爲所有其他實現拋出的現有異常,而不必在回調中顯式拋出它們。

更新回覆史蒂芬的答案

我已經更新了我的問題要更加明確。我以這種方式實現它的原因是,作爲操作的一部分,該命令在成功發送MailMessage後更新db實體上名爲SentOnUtcSystem.Nullable<DateTime>屬性。

ICommandEntitiesIUnitOfWork都由一個實體框架DbContext class.The DbContext每HTTP上下文註冊實現,使用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爲空(它將在新啓動的線程中)時註冊一個瞬態實例。

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

注意:理想情況下,發送電子郵件應該由單獨的投票工作人員處理。這個命令基本上是一個隊列交換所。數據庫中的EmailMessage實體已經具有發送電子郵件所需的全部信息。這個命令只是從數據庫中抓取一個未發送的命令,發送它,然後記錄操作的DateTime。這樣的命令可以通過從不同的進程/應用進行輪詢來執行,但我不會接受這個問題的答案。現在,當某種類型的http請求事件觸發它時,我們需要啓動此命令。

+0

看看[這個答案](http://stackoverflow.com/a/11899759/264697)。它談到了以更可靠的方式執行異步命令。 – Steven 2012-08-10 11:21:55

回答

8

確實有更簡單的方法來做到這一點。例如,您可以使用OpenGenericBatchRegistrationExtensions.GetTypesToRegister方法,而不像您在上一段代碼中那樣註冊BatchRegistrationCallback。該方法由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>)); 

,對於給定裝飾的代碼是有缺陷的,因爲你應該總是建立在一個新的線程一個新的依賴關係圖,從不傳從線程到線程的依賴關係(原始裝飾器所做的)。有關這篇文章中的更多信息:How to work with dependency injection in multi-threaded applications

所以答案實際上是比較複雜的,因爲這一般裝飾真的應該是取代了原來的命令處理器(或者甚至可能是裝飾包裹的處理程序鏈)的代理。該代理必須能夠在新線程中建立新的對象圖。此代理應該是這樣的:

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依賴關係註冊裝飾器將非常繁瑣。換言之,與RegisterDecorator方法註冊SendAsyncCommandHandlerProxy時,簡單的進樣器將自動注入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請求時會拋出異常,這是它們在Simple注入器(就像當你啓動一個新線程來運行代碼一樣)。由於您正在使用EF DbContext實現多個接口,這意味着簡單注入器將爲每個注入構造函數的接口創建一個新實例,正如您所說,這將是一個問題。

您需要重新配置DbContext,因爲純Per Web Request不會。有幾種解決方案,但我認爲最好是製作一個混合的PerWebRequest/PerLifetimeScope實例。爲此,您需要使用Per Lifetime Scope擴展包。另請注意,這也是Per Web Request的擴展包,因此您不必使用任何自定義代碼。當你這樣做,你可以定義下列註冊:

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

UPDATE 簡單的噴油器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生活方式MyDbContext,AND PerLifetimeScope生活方式。所以我們必須作弊一點,所以我們進行兩次註冊(每種生活方式一次)並選擇不同的服務類型(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的依賴。

現在,當HttpContext.Current爲空時解析的任何MyDbContext實例將獲得Per Lifetime Scope實例,而不是新的瞬態實例。

+0

你可以擴展句子「因爲這種類型是通用的,RegisterManyForOpenGeneric將跳過這個(因爲它不能猜測泛型)。」? – qujck 2013-01-16 07:07:56

+1

@qujck:'RegisterManyForOpenGeneric'擴展方法只是一個方便的幫助器,它搜索具體(非泛型)類型,並通過調用'container.Register ()'來註冊每個類型。遇到泛型時(例如'MyDecorator '),它會被跳過。它不知道註冊這個,因爲這個T可能是任何東西。如果你想映射開放的泛型類型,有一個擴展方法:'RegisterOpenGeneric'。 – Steven 2013-01-16 09:18:07

相關問題