5

ここで説明するコマンドパターンとここ説明するクエリパターンで単純なインジェクターを使用します。コマンドの1つに、2つのハンドラー実装があります。1つ目は、同期的に実行される「通常の」実装です。

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

もう1つはコマンドデコレータに似ていますが、前のクラスを明示的にラップして、別のスレッドでコマンドを実行します。

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>。私は次のことがうまくいくことを発見しましたが、それが最良/最適な方法であるかどうかはわかりません。非同期実装を使用するために、この1つのインターフェースを明示的に登録したいと思います。

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によってスローされた既存の例外を、コールバックで明示的にスローするのではなく、他のすべての実装に再利用したいと思います。

スティーブンの答えへの返信を更新する

質問をより明確にするために更新しました。私がこのように実装した理由は、操作の一部として、コマンドが正常に送信された後にdbエンティティでSystem.Nullable<DateTime>呼び出されたプロパティを更新するためです。SentOnUtcMailMessage

ICommandEntitiesIUnitOfWorkは両方ともエンティティフレームワーククラスによって実装されDbContextます。これは、ここで説明するメソッドDbContextを使用して、httpコンテキストごとに登録されます。

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

RegisterPerWebRequestsimpleinjector wikiのextensionメソッドのデフォルトの動作は、HttpContextがnullの場合に一時インスタンスを登録することです(これは新しく起動されたスレッドにあります)。

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

これが、Launchメソッドがサービスロケーターパターンを使用しての単一インスタンスを取得しDbContext、それを同期コマンドハンドラーコンストラクターに直接渡す理由です。_entities.Update(emailMessageEntity)と行が機能する_unitOfWork.SaveChanges()には、両方が同じDbContextインスタンスを使用している必要があります。

注:理想的には、電子メールの送信は別のポーリングワーカーが処理する必要があります。このコマンドは基本的にキュークリアリングハウスです。db内のEmailMessageエンティティには、電子メールの送信に必要なすべての情報がすでに含まれています。このコマンドは、データベースから未送信のコマンドを取得して送信し、アクションの日時を記録します。このようなコマンドは、別のプロセス/アプリからポーリングすることで実行できますが、この質問に対するそのような回答は受け付けません。今のところ、ある種のhttpリクエストイベントがそれをトリガーしたときに、このコマンドを開始する必要があります。

4

1 に答える 1

9

これを行うには、確かにもっと簡単な方法があります。たとえば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);
        });
    }
}

Simple Injectorには、Func<T>ファクトリを解決するための組み込みのサポートはありませんが、RegisterDecoratorメソッドは例外です。この理由は、フレームワークのサポートなしでデコレータをFunc依存関係に登録するのは非常に面倒だからです。つまり、メソッドにを登録するSendAsyncCommandHandlerProxyと、Simple Injectorは、装飾されたタイプの新しいインスタンスを作成できるデリゲートをRegisterDecorator自動的に注入します。Func<T>プロキシは(シングルトン)ファクトリのみを参照する(そしてステートレスである)ため、シングルトンとして登録することもできます。

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 Injectorで実装される方法です(ケースのように)コードを実行するために新しいスレッドを開始するとき)。EFで複数のインターフェースを実装しているのでDbContext、これは、Simple Injectorがコンストラクターによって注入されたインターフェースごとに新しいインスタンスを作成することを意味し、あなたが言ったように、これは問題になります。

DbContext純粋なWebごとのリクエストでは機能しないため、を再構成する必要があります。いくつかの解決策がありますが、PerWebRequest/PerLifetimeScopeのハイブリッドインスタンスを作成するのが最善だと思います。これには、 PerLifetimeScope拡張パッケージが必要です。また、 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 Simple Injector 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);

Simple Injectorではタイプの登録が1回しか許可されていないため(キー付き登録はサポートされていません)、MyDbContextをPerWebRequestライフスタイルとPerLifetimeScopeライフスタイルの両方に登録することはできません。したがって、少しごまかす必要があるので、2つの登録(ライフスタイルごとに1つ)を作成し、異なるサービスタイプ(DbContextとIObjectContextAdapter)を選択します。MyDbContextがそのサービスタイプを実装/継承する必要があることを除いて、サービスタイプはそれほど重要ではありません(MyDbContextこれが便利な場合は、ダミーインターフェイスを自由に実装してください)。

これらの2つの登録に加えて、適切なライフスタイルを取り戻すことができる3番目の登録であるマッピングが必要です。これは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

これで、がnullのMyDbContext場合に解決されるHttpContext.Currentインスタンスは、新しい一時インスタンスではなく、ライフタイムスコープごとのインスタンスを取得します。

于 2012-04-24T20:08:18.377 に答える