私は、大規模な多層コードベースでEF Coreに対して堅牢なコーディングを行う方法を理解しようとしています。私たちはいくつかの問題を経験しており、オンラインのサンプルプロジェクトアーキテクチャのほとんどは非常に単純化されており、洞察はあまり得られません。
私たちが直面する最も一般的な質問は次のとおりです。サービスクラス(ビジネスロジックの実行など)では、呼び出し元(多くの場合、他のサービスクラス)がEntity Frameworkエンティティを渡します。
参考までに、このアプリケーションには、従来のREST API、他のGraphQL API、cronベースのスケジュールタスクコンポーネント、長時間実行されるバッチ計算プロセスなどのコンポーネントがあり、すべてEFを使用してアクセスします。データベース内のデータ。EFCoreのほとんどのアーキテクチャ例は、単純なWebアプリです。
オンラインで提案されているアーキテクチャソリューションは次のとおりです。
一般に、これらの困難な問題の多くを処理する必要がなくなることが示唆されています。ただし、さらに困難な問題(結果整合性など)が多く発生するように感じます。私が間違っている?
ビジネス/ドメインサービスとドメインオブジェクトのEFエンティティに直接触れないでください:リポジトリと作業単位パターンのいずれかを使用して、EFエンティティとドメインオブジェクト間のマッピングレイヤーを実装します。
これにより、最初にEntity Frameworkを使用する利点のほとんどが取り除かれているようです。このアプローチをとるなら、なぜマイクロORMを使用しないのでしょうか。
私は常に好きです NorthWindTraders 多層アプリケーションでEFを適切に使用する方法の明確な例として、独自のUoWまたは永続化レイヤーをロールすることに頼らずに(これよりも面倒です)解決策。
- SaveChangesの呼び出しは誰が担当しますか?
それは誰がデータ操作を行っているかによります。サービスが基になるリポジトリメソッド/クエリオブジェクト/コマンドオブジェクトを呼び出す場合、SaveChanges()
を呼び出すのはそれらのリポジトリメソッド/クエリオブジェクト/コマンドオブジェクトです。
ただし、場合によっては、サービスレイヤーがコマンド/クエリオブジェクトをbeすることがあります。この時点で、それらはコンテキストを処理し、SaveChanges()
を自分で呼び出します。それはあなたのアーキテクチャに大きく依存します。
- サービスクラスは、必要なすべての依存関係が確実に読み込まれるようにする必要がありますか(その場合はどのように行うか)。
「依存関係」とは、「関連するエンティティ」を意味すると思います。サービスがそれらを必要とする(または、コンシューマーがそれらを必要とするときにそれらの存在を確認するタスクが課せられている)場合、まだ持っていない場合、それらをフェッチすることは明らかな論理的な結果です。
- 依存関係がロードされていない場合、サービスクラスは依存関係をロードする必要がありますか、それともサービスクラスが例外をスローする必要がありますか?これらの依存関係を呼び出し元に伝えるより良い方法はありますか?
これはEFではなく、例外管理に関連しています。例外をスローするか、否定的な結果オブジェクトを返すか、または単に(試行する)問題を修正するかは、ユーザーの期待と期待に基づいて、状況に応じて異なります。
これは何度も議論されてきたので、例外処理でSO/SE/Googleを参照することをお勧めします
- データベースコンテキストのライフサイクルはどのように管理する必要がありますか?
Webアプリケーションの場合、スコープ付きの依存関係は通常、それを回避するためのより良い方法です。ほとんどのWeb要求は単一のアトミック操作を実行するため、コンテキストをWeb要求にスコープすることが最も適切です。
非Webアプリケーションの場合は、よりアクティブなスコープ管理が必要になります。たとえば、Windowsサービスでは、コンテキストを直接注入するのではなく、コンテキストファクトリを注入する必要があります。そうしないと、Windowsサービスのランタイム全体で同じコンテキストを使用し続けることになります。経験上、これは対処したくないバグの原因です。
- 取引の責任者は誰ですか?
サービス層は基本的にアトミック操作のスコープであり、トランザクションはアトミック操作のデータベース表現です。したがって、サービスは、トランザクションをいつ使用するかについてショットを呼び出します-必要な場合。
コマンドオブジェクト(またはリポジトリメソッド)がSaveChanges()
を呼び出し、サービスレイヤーがそれらをトランザクションにラップするようにすると、興味深いダイナミックが作成され、自由に変更できます。
つまり、サービスレイヤーは、どの操作(コマンドオブジェクト)が単一のアトミック操作の一部であるかを完全に自由に組み合わせることができます。コマンドオブジェクトを変更したり複雑にする必要はありません。
小さな例:
_public class CommandObject
{
private readonly MyContext myContext;
public CommandObject(MyContext myContext)
{
this.myContext = myContext;
}
public void CreateFoo()
{
myContext.Foos.Add(new Foo());
myContext.SaveChanges();
}
}
public class SeparateFooService
{
private readonly CommandObject commandObject;
public SeparateFooService(CommandObject commandObject)
{
this.commandObject = commandObject;
}
public void CreateTwoFoos()
{
commandObject.CreateFoo();
commandObject.CreateFoo();
}
}
public class TransactionalFooService
{
private readonly MyContext myContext;
private readonly CommandObject commandObject;
public TransactionalFooService(MyContext myContext, CommandObject commandObject)
{
this.myContext = myContext;
this.commandObject = commandObject;
}
public void CreateTwoFoos()
{
using (var transaction = myContext.Database.BeginTransaction())
{
commandObject.CreateFoo();
commandObject.CreateFoo();
transaction.Commit();
}
}
}
_
SeparateFooService
は2つの異なるアクションを実行します。 2番目が失敗しても、1番目は成功します。 TransactionalFooService
は、1つだけではなく、2つまたはゼロのFoo
エンティティを作成します。
興味深い部分は、同じコマンドオブジェクトがどちらの場合でも使用されていることです。つまり、サービス層は、依存関係を必要とせずにトランザクション動作を単独で制御できます。それを意識する。
- サービスクラスはデータベースコンテキストへの参照を持つ必要がありますか?
スコープ付きDIの良い点は、依存関係にオプトインできることです。
everyサービスクラスにはデータベースコンテキストへの参照がありますか?通常はありません。クエリ/コマンドオブジェクトやリポジトリなどの他の依存関係に依存して、コンテキスト処理をそれらに任せることもできます。
ただし、サービスがコンテキストを必要とする場合、たとえば上記のトランザクションの場合、依存関係を追加するだけで、DIフレームワーク(正しく設定されている場合)は、基になるクエリ/コマンドオブジェクトまたはリポジトリが使用する正確なコンテキストへのアクセスを提供します。
[〜#〜] cqrs [〜#〜]しかし、それはさらに多くの困難な問題(たとえば、結果の一貫性)をもたらすように感じます。私が間違っている?
Webアプリケーションでスコープ付きコンテキストを使用する場合、同じコンテキストがWebリクエスト全体で保持されます。非Webアプリケーションでは、もう少しアクティブな処理が必要ですが、DIフレームワークで必要な場所に同じコンテキストを再利用できるようにすることもできます(シングルトンにしたくないので、必要ありません) )。
結果の一貫性に対する懸念がCQRSが非同期処理につながるという解釈に由来するかどうかはわかりません。それは(本質的に)しません。 CQRSは依然として線形の同期コードであり、データストアは、たとえば、すべてを行うモノリシックメソッド(それは良い考えではありませんが、線形同期コードの明確な例です)。
そうは言っても、これを非同期で行うことは可能ですが、CQRSを使用していなかったとしても、これは(同じ潜在的な問題を伴う)オプションでもあります。したがって、CQRSはこれを考慮しません。
ビジネス/ドメインサービスおよびドメインオブジェクトからEFエンティティに直接触れないでください:リポジトリと作業単位パターンのいずれかを使用して、EFエンティティとドメインオブジェクト間のマッピングレイヤーを実装します。
これにより、最初にEntity Frameworkを使用する利点のほとんどが取り除かれているようです。
私は心から同意します。私は書きました 以前の回答 独自のUoW /リポジトリをローリングすることがEFのミラーリングされた不要なラッパーである理由を詳しく説明しました。要約すると、具体的には抽象化できないため、また抽象化する必要がないため、エンティティライブラリではなく、エンティティFrameworkと呼ばれます。メリットは、コストをはるかに上回ることさえありません。
ただし、エンティティをドメインオブジェクト(または、バックエンドがダムリソースストアの場合はDTOのみ)から分離することは、依然として良い考えです。エンティティとドメインモデル/ DTOが異なるである必要があるという意味ではありませんが、separateである必要があります。
これらの個別のクラス間をどのようにマッピングするかは、適切なライブラリを見つけるか、手動で作成するかによって決まります。
ここで考えられる1つのアプローチは Automapper's ProjectTo<TDomainModel>()
です。これは、EFクエリを非エンティティクラスに効果的かつ即座にラップして、コードベースは実際にはエンティティクラスを必要以上に処理しません。
私たちはいくつかの問題を経験しており、オンラインのサンプルプロジェクトアーキテクチャのほとんどは非常に単純化されており、洞察はあまり得られません。
EFディスカッションの問題は、さまざまな人々がさまざまな場所で線を引くこと、および常に明白ではないオンライン会話であるということです。
私の答えは、大規模な多層コードベースでの作業に合わせています。小規模なプロジェクトについて話していたら、もっと単純なアプローチを提唱したでしょう。
サービス層を開発すべきだと思います。
EF --> Service Layer --> Domain Aggregates
必要なビジネスオペレーションに関してドメインアグリゲートを定義します。たとえば、GetInvoiceメソッドは、顧客情報、配送先住所、製品ラインアイテム、数量を含む集計を返します。
そうすれば、他の質問にも自然に答えられるようになると思います。 EF DataContextのライフサイクルはどのように処理されますか?請求書の取得ごとに1つのDataContextオブジェクト。 SaveChangesの呼び出しは誰が担当しますか?サービス層。誰がトランザクションを処理しますか?サービス層。
CQRSやオニオンアーキテクチャのようなものを、組織の原則ではなくレシピとして見る傾向があると思います。CQRSは、単一の責任のもう1つの形です。 CRUDはCQRSのもう1つの形式です。アーキテクチャを意味のある方法で整理すると、アプリケーションは当然これらの整理の原則に従います。