8

Simple Injectorを使用して、注入された依存関係の存続期間を管理しています(この場合UnitOfWork)。サービスやコマンドハンドラーではなく、別のデコレーターを使用して保存と破棄を行うことで、ビジネスロジックを作成する際のコードが非常に簡単になります。レイヤー(このブログ投稿で概説されているアーキテクチャに従います)。

上記は、Simple Injector MVC NuGetパッケージとコンポジションルートコンテナーの構築中に次のコードを使用することで完全に(そして非常に簡単に)機能します。グラフに複数の依存関係が存在する場合、同じインスタンスがすべてに注入されます。エンティティフレームワークモデルコンテキスト。

private static void InitializeContainer(Container container)
{
    container.RegisterPerWebRequest<IUnitOfWork, UnitOfWork>();
    // register all other interfaces with:
    // container.Register<Interface, Implementation>();
}

ここで、いくつかのバックグラウンドスレッドを実行し、スレッドに関するSimple Injectorのドキュメントから、コマンドを次のようにプロキシできることを理解する必要があります。

public sealed class TransactionCommandHandlerDecorator<TCommand>
    : ICommandHandler<TCommand>
{
    private readonly ICommandHandler<TCommand> handlerToCall;
    private readonly IUnitOfWork unitOfWork;

    public TransactionCommandHandlerDecorator(
        IUnitOfWork unitOfWork, 
        ICommandHandler<TCommand> decorated)
    {
        this.handlerToCall = decorated;
        this.unitOfWork = unitOfWork;
    }

    public void Handle(TCommand command)
    {
         this.handlerToCall.Handle(command);
         unitOfWork.Save();
    }
}

ThreadedCommandHandlerProxy:

public class ThreadedCommandHandlerProxy<TCommand>
    : ICommandHandler<TCommand>
{
    Func<ICommandHandler<TCommand>> instanceCreator;

    public ThreadedCommandHandlerProxy(
        Func<ICommandHandler<TCommand>> creator)
    {
        this.instanceCreator = creator;
    }

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

ただし、このスレッドのサンプルドキュメントから、ファクトリが使用されていることがわかります。ファクトリをコマンドとサービスレイヤーに導入すると、サービスごとに異なる保存方法があるため、混乱して一貫性がなくなります(1つのコンテナが保存を処理し、他のインスタンス化されたファクトリがサービスは保存と破棄を処理します)-ファクトリがない場合、サービスコードスケルトンがどれほど明確でシンプルであるかがわかります。

public class BusinessUnitCommandHandlers :
    ICommandHandler<AddBusinessUnitCommand>,
    ICommandHandler<DeleteBusinessUnitCommand>
{
    private IBusinessUnitService businessUnitService;
    private IInvoiceService invoiceService;

    public BusinessUnitCommandHandlers(
        IBusinessUnitService businessUnitService, 
        IInvoiceService invoiceService)
    {
        this.businessUnitService = businessUnitService;
        this.invoiceService = invoiceService;
    }

    public void Handle(AddBusinessUnitCommand command)
    {
        businessUnitService.AddCompany(command.name);
    }

    public void Handle(DeleteBusinessUnitCommand command)
    {
        invoiceService.DeleteAllInvoicesForCompany(command.ID);
        businessUnitService.DeleteCompany(command.ID);
    }
}

public class BusinessUnitService : IBusinessUnitService
{
    private readonly IUnitOfWork unitOfWork;
    private readonly ILogger logger;

    public BusinessUnitService(IUnitOfWork unitOfWork, 
        ILogger logger)
    {
        this.unitOfWork = unitOfWork;
        this.logger = logger;
    }

    void IBusinessUnitService.AddCompany(string name)
    {
        // snip... let container call IUnitOfWork.Save()
    }

    void IBusinessUnitService.DeleteCompany(int ID)
    {
        // snip... let container call IUnitOfWork.Save()
    }
}

public class InvoiceService : IInvoiceService
{
    private readonly IUnitOfWork unitOfWork;
    private readonly ILogger logger;

    public BusinessUnitService(IUnitOfWork unitOfWork, 
        ILogger logger)
    {
        this.unitOfWork = unitOfWork;
        this.logger = logger;
    }

    void IInvoiceService.DeleteAllInvoicesForCompany(int ID)
    {
        // snip... let container call IUnitOfWork.Save()
    }
}

上記の問題が発生し始め、ASP .NET PerWebRequestの有効期間に関するドキュメントから理解できるように、次のコードが使用されます。

public T GetInstance()
{
    var context = HttpContext.Current;

    if (context == null)
    {
        // No HttpContext: Let's create a transient object.
        return this.instanceCreator();
    }

    object key = this.GetType();
    T instance = (T)context.Items[key];

    if (instance == null)
    {
        context.Items[key] = instance = this.instanceCreator();
    }
    return instance;
}

上記はHTTPリクエストごとに正常に機能しますが、有効なものがありますHttpContext.Currentが、それを使用して新しいスレッドをスピンアップすると、新しいスレッドThreadedCommandHandlerProxyが作成さHttpContextれ、そのスレッド内に存在しなくなります。

後続の呼び出しごとにはnullになるため、HttpContextサービスコンストラクターに注入されたオブジェクトのすべてのインスタンスは新しく一意になります。これは、オブジェクトがすべてのサービスで同じインスタンスとして正しく共有されるWebリクエストごとの通常のHTTPとは異なります。

したがって、上記を質問に要約すると、次のようになります。

HTTPリクエストから作成されたか、新しいスレッドを介して作成されたかに関係なく、オブジェクトを構築して共通アイテムを注入するにはどうすればよいですか?

UnitOfWorkコマンドハンドラプロキシ内のスレッドによって管理されるための特別な考慮事項はありますか?ハンドラーの実行後に確実に保存および破棄するにはどうすればよいですか?

command-handler / service-layer内で問題が発生し、を保存したくない場合はUnitOfWork、単に例外をスローしますか?もしそうなら、これをグローバルレベルでキャッチすることは可能ですか、それともハンドラデコレータまたはプロキシのtry-内からリクエストごとに例外をキャッチする必要がありますか?catch

ありがとう、

クリス

4

2 に答える 2

7

Web アプリケーションでコマンドを非同期的に実行したい場合は、一歩下がって何を達成しようとしているのかを確認することをお勧めします。バックグラウンド スレッドでハンドラーを開始した直後に、Web アプリケーションがリサイクルされるリスクが常にあります。ASP.NET アプリがリサイクルされると、すべてのバックグラウンド スレッドが中止されます。コマンドを (トランザクション) キューに発行し、バックグラウンド サービスにコマンドを取得させる方がよいでしょう。これにより、コマンドが「失われる」ことがなくなります。また、ハンドラーが成功しなかった場合にコマンドを再実行することもできます。また、厄介な登録を行う必要がなくなることもありますが (どの DI フレームワークを選択しても、登録はおそらく必要です)、これはおそらく副次的な問題です。ハンドラーを非同期で実行する必要がある場合は、

邪魔にならないので、必要なのは次のとおりです。

ご指摘のとおり、(一部の) コマンド ハンドラーを非同期で実行しているため、それらに対して Web 要求ごとのライフスタイルを使用することはできません。Web リクエストごとと「その他」を組み合わせたハイブリッド ソリューションが必要になります。それ以外の何かは、生涯ごとのスコープである可能性が最も高いでしょう。いくつかの理由により、これらのハイブリッド ソリューションには組み込みの拡張機能はありません。まず第一に、多くの人が必要としない非常にエキゾチックな機能です。2 つ目は、任意の 2 つまたは 3 つのライフスタイルを組み合わせることができるため、ハイブリッドの組み合わせは無限にあるということです。最後に、これを自分で登録するのは (かなり) 簡単です。

Simple Injector 2 ではLifestyleクラスが追加され、CreateHybrid任意の 2 つのライフスタイルを組み合わせて新しいライフスタイルを作成できるメソッドが含まれています。次に例を示します。

var hybridLifestyle = Lifestyle.CreateHybrid(
    () => HttpContext.Current != null,
    new WebRequestLifestyle(),
    new LifetimeScopeLifestyle());

このハイブリッド ライフスタイルを使用して、作業単位を登録できます。

container.Register<IUnitOfWork, DiUnitOfWork>(hybridLifestyle);

作業単位を Per Lifetime Scope として登録しているため、特定のスレッドの Lifetime Scope を明示的に作成して破棄する必要があります。最も簡単な方法は、これを に追加することThreadedCommandHandlerProxyです。これは最も堅実な方法ではありませんが、これを行う方法を示す最も簡単な方法です。

コマンド ハンドラー/サービス レイヤー内に問題があり、UnitOfWork を保存したくない場合は、単純に例外をスローしますか?

行う典型的なことは、例外をスローすることです。実際、これは例外の一般的な規則です。

メソッドがその名前が約束することを実行できない場合は、スローします。->

コマンドハンドラーは、それが実行されるコンテキストについて無知であるべきであり、例外をスローする必要があるかどうかを区別することは最も避けたいことです。したがって、投げるのが最善の選択肢です。ただし、バックグラウンド スレッドで実行している場合は、その例外をキャッチすることをお勧めします。キャッチしないと、.NET によって AppDomain 全体が強制終了されるからです。Web アプリケーションでは、これは AppDomain のリサイクルを意味します。これは、Web アプリケーション (または少なくともそのサーバー) が短期間オフラインになることを意味します。

一方、例外情報も失いたくないので、その例外をログに記録する必要があります。おそらく、そのコマンドのシリアル化された表現をその例外とともにログに記録して、渡されたデータを確認できるようにする必要があります。メソッドに追加すると、ThreadedCommandHandlerProxy.Handle次のようになります。

public void Handle(TCommand command)
{
    string xml = this.commandSerializer.ToXml(command);    

    Task.Factory.StartNew(() =>
    {
        var logger = 
            this.container.GetInstance<ILogger>();

        try
        {
            using (container.BeginTransactionScope())
            {
                // must be created INSIDE the scope.
                var handler = this.instanceCreator();
                handler.Handle(command);
            }
        }
        catch (Exception ex)
        {
            // Don't let the exception bubble up, 
            // because we run in a background thread.
            this.logger.Log(ex, xml);
        }
    });
}

ハンドラーを非同期で実行することが最善のアイデアではない可能性があることについて警告しました。ただし、このコマンド/ハンドラー パターンを適用しているため、アプリケーションのコードを 1 行も変更することなく、後でキューイングの使用に切り替えることができます。なんらかのQueueCommandHandlerDecorator<T>方法 (コマンドをシリアル化してキューに送信する) を記述し、コンポジション ルートでの配線方法を変更するだけで、準備完了です (もちろん、実装することを忘れないでください)。キューからコマンドを実行するサービス)。つまり、この SOLID 設計の優れている点は、これらの機能の実装がアプリケーションのサイズに対して一定であるということです。

于 2012-06-15T23:45:03.777 に答える
5

ASP.NET でバックグラウンド スレッドを実行する際の問題の一部は、便利なコマンド ハンドラー デコレーターを使用して解決できます。

public class AspNetSafeBackgroundCommandHandlerDecorator<T>
    : ICommandHandler<T>, IRegisteredObject
{
    private readonly ICommandHandler<T> decorated;

    private readonly object locker = new object();

    public AspNetSafeBackgroundCommandHandlerDecorator(
        ICommandHandler<T> decorated)
    {
        this.decorated = decorated;
    }

    public void Handle(T command)
    {
        HostingEnvironment.RegisterObject(this);

        try
        {
            lock (this.locker)
            {
                this.decorated.Handle(command);
            }
        }
        finally
        {
            HostingEnvironment.UnregisterObject(this);
        }            
    }

    void IRegisteredObject.Stop(bool immediate)
    {
        // Ensure waiting till Handler finished.
        lock (this.locker) { }
    }
}

このデコレーターをコマンド ハンドラーと の間に配置ThreadedCommandHandlerProxyすると、(通常の状態では) そのようなコマンドの実行中に AppDomain がアンロードされないことが保証されます。

于 2012-08-10T10:26:03.863 に答える