注:この回答はEntity FrameworkDbContext
の.DataContext
ISession
Ian の言葉を借りて始めましょう:DbContext
アプリケーション全体に単一のものを用意するのは悪い考えです。これが意味を持つ唯一の状況は、シングル スレッド アプリケーションと、その単一のアプリケーション インスタンスによってのみ使用されるデータベースがある場合です。はDbContext
スレッドセーフではなく、DbContext
データをキャッシュするため、すぐに古くなります。これにより、複数のユーザー/アプリケーションがそのデータベースで同時に作業しているときに、あらゆる種類の問題が発生します (これはもちろん非常に一般的です)。しかし、あなたはすでにそれを知っていると思います.そして、それを必要とする人に新しいインスタンスを(つまり、一時的なライフスタイルで)注入しない理由を知りたいだけですDbContext
. DbContext
(単一の、またはスレッドごとのコンテキストでさえも悪い理由の詳細については、この回答を読んでください)。
まず、を一時的なものとして登録するDbContext
こともできますが、通常は、特定のスコープ内でそのような作業単位の単一のインスタンスを保持する必要があります。Web アプリケーションでは、Web 要求の境界でこのようなスコープを定義することが実用的です。したがって、Per Web Request ライフスタイルです。これにより、一連のオブジェクト全体を同じコンテキスト内で動作させることができます。つまり、これらは同じビジネス トランザクション内で動作します。
一連の操作を同じコンテキスト内で動作させるという目標がない場合、その場合は一時的なライフスタイルで問題ありませんが、注意すべき点がいくつかあります。
- すべてのオブジェクトが独自のインスタンスを取得するため、システムの状態を変更するすべてのクラスを呼び出す必要があります
_context.SaveChanges()
(そうしないと、変更が失われます)。これはコードを複雑にする可能性があり、コードに 2 番目の責任 (コンテキストを制御する責任) を追加する可能性があり、単一責任の原則に違反します。
- [によってロードおよび保存された] エンティティ
DbContext
は、別のクラスのコンテキスト インスタンスでは使用できないため、そのようなクラスのスコープを決して離れないようにする必要があります。これにより、コードが非常に複雑になる可能性があります。これらのエンティティが必要な場合は、id で再度読み込む必要があり、パフォーマンスの問題が発生する可能性があるからです。
DbContext
が実装されているためIDisposable
、作成されたすべてのインスタンスを破棄したい場合があります。これを行う場合、基本的に 2 つのオプションがあります。を呼び出した直後に同じメソッドでそれらを破棄する必要がありますcontext.SaveChanges()
が、その場合、ビジネス ロジックは外部から渡されたオブジェクトの所有権を取得します。2 番目のオプションは、HTTP 要求の境界で作成されたすべてのインスタンスを破棄することですが、その場合でも、これらのインスタンスをいつ破棄する必要があるかをコンテナーに知らせるために、ある種のスコープが必要です。
別のオプションは、 a をまったく注入しないDbContext
ことです。代わりに、新しいインスタンスを作成できる a を注入しDbContextFactory
ます (私は過去にこのアプローチを使用していました)。このようにして、ビジネス ロジックはコンテキストを明示的に制御します。次のように見える場合:
public void SomeOperation()
{
using (var context = this.contextFactory.CreateNew())
{
var entities = this.otherDependency.Operate(
context, "some value");
context.Entities.InsertOnSubmit(entities);
context.SaveChanges();
}
}
これのプラス面は、DbContext
明示的に の寿命を管理し、これを簡単に設定できることです。また、特定のスコープで単一のコンテキストを使用することもできます。これには、単一のビジネス トランザクションでコードを実行したり、同じDbContext
.
欠点は、from メソッドをメソッドに渡さなければならないことですDbContext
(メソッド インジェクションと呼ばれます)。ある意味では、このソリューションは「スコープ」アプローチと同じですが、スコープはアプリケーション コード自体で制御されることに注意してください (おそらく何度も繰り返されます)。作業単位の作成と破棄を担当するのはアプリケーションです。は依存関係グラフが構築された後に作成されるためDbContext
、コンストラクター インジェクションは視野外であり、あるクラスから別のクラスにコンテキストを渡す必要がある場合は、メソッド インジェクションを延期する必要があります。
メソッド インジェクションはそれほど悪くはありませんが、ビジネス ロジックがより複雑になり、より多くのクラスが関与するようになると、メソッドからメソッドへ、およびクラスからクラスへと渡す必要があり、コードが非常に複雑になる可能性があります (私が見たこれは過去に)。ただし、単純なアプリケーションの場合、このアプローチは問題なく機能します。
このファクトリ アプローチには欠点があるため、大規模なシステムには別のアプローチが役立ちます。それは、コンテナーまたはインフラストラクチャ コード /コンポジション ルートに作業単位を管理させる方法です。これがあなたの質問のスタイルです。
コンテナーやインフラストラクチャーにこれを処理させることで、UoW インスタンスを作成、(オプションで) コミット、および破棄する必要があるためにアプリケーション コードが汚染されることはありません。これにより、ビジネス ロジックがシンプルかつクリーンに保たれます (単一の責任のみ)。このアプローチにはいくつかの問題があります。たとえば、インスタンスをコミットして破棄しましたか?
作業単位の破棄は、Web 要求の最後で実行できます。しかし、多くの人は、これが作業単位をコミットする場所でもあると誤って想定しています。ただし、アプリケーションのその時点では、作業単位を実際にコミットする必要があるかどうかを確実に判断することはできません。たとえば、ビジネス層のコードがコールスタックの上位でキャッチされた例外をスローした場合は、絶対にコミットしたくありません。
本当の解決策は、何らかのスコープを明示的に管理することですが、今回はコンポジション ルート内で行います。コマンド/ハンドラー パターンの背後にあるすべてのビジネス ロジックを抽象化すると、これを可能にする各コマンド ハンドラーをラップできるデコレーターを作成できるようになります。例:
class TransactionalCommandHandlerDecorator<TCommand>
: ICommandHandler<TCommand>
{
readonly DbContext context;
readonly ICommandHandler<TCommand> decorated;
public TransactionCommandHandlerDecorator(
DbContext context,
ICommandHandler<TCommand> decorated)
{
this.context = context;
this.decorated = decorated;
}
public void Handle(TCommand command)
{
this.decorated.Handle(command);
context.SaveChanges();
}
}
これにより、このインフラストラクチャ コードを 1 回記述するだけで済みます。ICommandHandler<T>
堅実な DI コンテナーを使用すると、そのようなデコレーターを構成して、一貫した方法ですべての実装をラップすることができます。