8

複数のサービスがあり、それぞれがSimpleInjectorIoCコンテナーを使用してコンストラクターに注入UnitOfWorkれています。

現在、各インスタンスが個別のオブジェクトであることがわかりUnitOfWorkます。これは、Entity Frameworkを使用していて、すべての作業単位で同じコンテキスト参照が必要なため、問題があります。

UnitOfWork解決リクエストごとに同じインスタンスがすべてのサービスに注入されるようにするにはどうすればよいですか?コマンドが完了すると、外部のUnitOfWorコマンドハンドラデコレータによって保存されます。

これは共通のライブラリであり、MVCフォームとWindowsフォームの両方で使用されることに注意してください。可能であれば、両方のプラットフォーム用の汎用ソリューションがあると便利です。

コードは以下のとおりです。

// snippet of code that registers types
void RegisterTypes()
{
    // register general unit of work class for use by majority of service layers
    container.Register<IUnitOfWork, UnitOfWork>();

    // provide a factory for singleton classes to create their own units of work 
    // at will
    container.RegisterSingle<IUnitOfWorkFactory, UnitOfWorkFactory>();

    // register logger
    container.RegisterSingle<ILogger, NLogForUnitOfWork>();

    // register all generic command handlers
    container.RegisterManyForOpenGeneric(typeof(ICommandHandler<>),
        AppDomain.CurrentDomain.GetAssemblies());

    container.RegisterDecorator(typeof(ICommandHandler<>),
        typeof(TransactionCommandHandlerDecorator<>));

    // register services that will be used by command handlers
    container.Register<ISynchronisationService, SynchronisationService>();
    container.Register<IPluginManagerService, PluginManagerService>();
}

以下の行の望ましい結果は、構築されたオブジェクトグラフ全体で共有UnitOfWorkインスタンスを持つオブジェクトを作成することです。

var handler = Resolve<ICommandHandler<SyncExternalDataCommand>>();

これが私のサービスです:

public class PluginManagerService : IPluginSettingsService
{
    public PluginManagerService(IUnitOfWork unitOfWork)
    {
        this.unitOfWork = unitOfWork;
    }

    private readonly unitOfWork;

    void IPluginSettingsService.RegisterPlugins()
    {
       // manipulate the unit of work
    }
}

public class SynchronisationService : ISynchronisationService
{
    public PluginManagerService(IUnitOfWork unitOfWork)
    {
        this.unitOfWork = unitOfWork;
    }

    private readonly unitOfWork;

    void ISynchronisationService.SyncData()
    {
       // manipulate the unit of work
    }
}

public class SyncExternalDataCommandHandler
    : ICommandHandler<SyncExternalDataCommand>
{
    ILogger logger;
    ISynchronisationService synchronisationService;
    IPluginManagerService pluginManagerService;

    public SyncExternalDataCommandHandler(
        ISynchronisationService synchronisationService, 
        IPluginManagerService pluginManagerService, 
        ILogger logger)
    {
        this.synchronisationService = synchronisationService;
        this.pluginManagerService = pluginManagerService;
        this.logger = logger;
    }

    public void Handle(SyncExternalDataCommand command)
    {
        // here i will call both services functions, however as of now each
        // has a different UnitOfWork reference internally, we need them to 
        // be common.
        this.synchronisationService.SyncData();
        this.pluginManagerService.RegisterPlugins();
    }
}
4

1 に答える 1

20

どの登録が必要かは、アプリケーションの種類によって異なります。2 つの異なるフレームワーク (MVC と WinForms) について話しているため、両方の登録が異なります。

MVC アプリケーション (または Web アプリケーション全般) の場合、最も一般的なのは、Web 要求ごとに作業単位を登録することです。たとえば、次の登録は、単一の Web 要求中に作業単位をキャッシュします。

container.Register<IUnitOfWork>(() =>
{
    var items = HttpContext.Current.Items;

    var uow = (IUnitOfWork)items["UnitOfWork"];

    if (uow == null)
    {
        items["UnitOfWork"] = uow = container.GetInstance<UnitOfWork>();
    }

    return uow;
});

この登録の欠点は、作業単位が破棄されないことです (必要な場合)。コンテナーに拡張メソッドを追加する Simple Injector 用の拡張パッケージがありRegisterPerWebRequestます。これにより、Web 要求の最後でインスタンスが確実に破棄されるようになります。このパッケージを使用すると、次の登録を行うことができます。

container.RegisterPerWebRequest<IUnitOfWork, UnitOfWork>();

これは次へのショートカットです。

container.Register<IUnitOfWork, UnitOfWork>(new WebRequestLifestyle());

一方、Windows フォーム アプリケーションは通常、シングル スレッドです (1 人のユーザーがそのアプリケーションを使用します)。フォームごとに単一の作業単位があり、それがフォームを閉じて破棄されることは珍しくないと思いますが、コマンド/ハンドラー パターンを使用することで、よりサービス指向のアプローチを取る方がよいと思います。つまり、プレゼンテーション レイヤーを変更せずに、ビジネス レイヤーを WCF サービスに移行できるように設計するとよいということです。これは、コマンドにプリミティブと (その他の) DTOのみを含めることで実現できます。そのため、Entity Framework エンティティをコマンドに格納しないでください。これにより、コマンドのシリアル化が非常に難しくなり、後で驚きにつながるからです。

これを行う場合、コマンド ハンドラーの実行を開始する前に新しい作業単位を作成し、そのハンドラーの実行中に同じ作業単位を再利用し、ハンドラーが正常に完了したときにそれをコミットする (そして常に破棄する) と便利です。 . これは、 Per Lifetime Scope ライフスタイルの典型的なシナリオです。コンテナーに拡張メソッドを追加する拡張パッケージがあります。RegisterLifetimeScopeこのパッケージを使用すると、次の登録を行うことができます。

container.RegisterLifetimeScope<IUnitOfWork, UnitOfWork>();

これは次へのショートカットです。

container.Register<IUnitOfWork, UnitOfWork>(new LifetimeScopeLifestyle());

ただし、登録は話の半分にすぎません。2 番目の部分は、作業単位の変更をいつ保存するかを決定することです。ライフタイム スコープ ライフスタイルを使用する場合は、そのようなスコープの開始位置と終了位置を決定します。コマンドの実行前にライフタイム スコープを明示的に開始し、コマンドの実行が終了したら終了する必要があるため、これを行う最善の方法は、コマンド ハンドラーをラップできるコマンド ハンドラー デコレーターを使用することです。したがって、Forms アプリケーションの場合、通常、有効期間のスコープを管理する追加のコマンド ハンドラー デコレーターを登録します。この場合、このアプローチは機能しません。次のデコレータを見てください。ただし、正しくないことに注意してください。

private class LifetimeScopeCommandHandlerDecorator<T>
    : ICommandHandler<T>
{
    private readonly Container container;
    private readonly ICommandHandler<T> decoratedHandler;

    public LifetimeScopeCommandHandlerDecorator(...) { ... }

    public void Handle(T command)
    {
        using (this.container.BeginLifetimeScope())
        {
            // WRONG!!!
            this.decoratedHandler.Handle(command);
        }
    }
}

ライフタイム スコープが開始される前に装飾されたコマンド ハンドラーが作成されるため、このアプローチは機能しません。

この問題を次のように解決しようとする誘惑に駆られるかもしれませんが、それも正しくありません。

using (this.container.BeginLifetimeScope())
{
    // EVEN MORE WRONG!!!
    var handler = this.container.GetInstance<ICommandHandler<T>>();

    handler.Handle(command);
}

ライフタイム スコープのコンテキスト内でを要求するICommandHandler<T>と、実際にはそのスコープに が注入IUnitOfWorkされますが、コンテナは (再び) で装飾されたハンドラーを返しLifetimeScopeCommandHandlerDecorator<T>ます。したがって、呼び出しhandler.Handle(command)は再帰呼び出しになり、スタック オーバーフロー例外が発生します。

問題は、ライフタイム スコープを開始する前に、依存関係グラフが既に構築されていることです。したがって、グラフの残りの部分の作成を延期して、依存関係グラフを壊す必要があります。アプリケーションの設計をきれいに保つためにこれを行う最善の方法は、デコレータをプロキシに変更し、ラップするはずの型を作成するファクトリをそこに注入することです。これは次のようLifetimeScopeCommandHandlerProxy<T>になります。

// This class will be part of the Composition Root of
// the Windows Forms application
private class LifetimeScopeCommandHandlerProxy<T> : ICommandHandler<T>
{
    // Since this type is part of the composition root,
    // we are allowed to inject the container into it.
    private Container container;
    private Func<ICommandHandler<T>> factory;

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

    public void Handle(T command)
    {
        using (this.container.BeginLifetimeScope())
        {
            var handler = this.factory();

            handler.Handle(command);        
        }
    }
}

デリゲートを注入することにより、インスタンスが作成される時間を遅らせることができ、これを行うことにより、依存関係グラフ (の残り) の構築を遅らせることができます。ここでの秘訣は、(もちろん) 自分自身を再び注入するのではなく、ラップされたインスタンスを注入するような方法でこのプロキシ クラスを登録することです。Simple Injector はFunc<T>、ファクトリをデコレーターに注入することをサポートしているため、単純RegisterDecoratorに、この場合はRegisterSingleDecorator拡張メソッドも使用できます。

デコレーター (およびこのプロキシー) が (明らかに) 登録される順序が重要であることに注意してください。このプロキシは新しいライフタイム スコープを開始するため、作業単位をコミットするデコレータをラップする必要があります。つまり、より完全な登録は次のようになります。

container.RegisterLifetimeScope<IUnitOfWork, UnitOfWork>();

container.RegisterManyForOpenGeneric(
    typeof(ICommandHandler<>),
    AppDomain.CurrentDomain.GetAssemblies());

// Register a decorator that handles saving the unit of
// work after a handler has executed successfully.
// This decorator will wrap all command handlers.
container.RegisterDecorator(
    typeof(ICommandHandler<>),
    typeof(TransactionCommandHandlerDecorator<>));

// Register the proxy that starts a lifetime scope.
// This proxy will wrap the transaction decorators.
container.RegisterSingleDecorator(
    typeof(ICommandHandler<>),
    typeof(LifetimeScopeCommandHandlerProxy<>));

プロキシとデコレータを逆に登録すると、依存関係グラフの残りの部分とTransactionCommandHandlerDecorator<T>は異なるIUnitOfWorkものに依存することになり、そのグラフの作業単位に加えられたすべての変更がコミットされないことを意味します。つまり、アプリケーションは機能しなくなります。したがって、常にこの登録を注意深く確認してください。

幸運を。

于 2012-06-09T14:55:55.517 に答える