Persistence依存のドメインイベント - サービス、リポジトリ、またはUIをどこで提起するか?
-
28-10-2019 - |
質問
私のASP.NET MVC3 / Nhibernateアプリケーションには、ドメインオブジェクトに関連するさまざまなイベントを発射して処理する必要があります。たとえば、an Order
オブジェクトには、ようなイベントがある場合があります OrderStatusChanged
また NoteCreatedForOrder
. 。ほとんどの場合、これらのイベントは電子メールが送信されるため、MVCアプリに残すことはできません。
Udi Dahan'sを読みました ドメインイベント そして、この種のことを行う方法についての他の数十の考えであり、私はイベントメッセージを処理するNServiceBusベースのホストを使用することにしました。概念実証テストをいくつか行ったことがありますが、これはうまく機能しているようです。
私の質問はです どのアプリケーションレイヤーが実際にイベントを上げる必要があります. 。問題のオブジェクトが正常に持続するまで、イベントを発射したくありません(永続性が失敗した場合、メモが作成されたというメールを送信できません)。
別の懸念は、場合によっては、イベントが集計ルートの下にあるオブジェクトに結び付けられることです。上記の例では、a Note
に追加することで保存されます Order.Notes
注文の収集と保存。これは、どのイベントが解雇されるべきかを評価するのが難しいという点で問題を引き起こします。 Order
保存されます。更新されたコピーを保存する前に、オブジェクトの現在のコピーを取得し、違いを探す必要がないようにしたいと思います。
UIがこれらのイベントを提起するのは適切ですか?どのイベントが発生したかを知っており、サービスレイヤーがオブジェクトを保存しても正常に発生した後にのみ発射できます。コントローラーがドメインイベントを発射することについて何かが間違っているようです。
リポジトリは、正常に持続した後にイベントを発射する必要がありますか?
イベントを完全に分離する必要がある場合は、リポジトリストアに
Event
その後、ポーラーサービスによって拾われるオブジェクトと それから NServiceBusのイベントになりました(またはPoller Serviceから直接処理されます)?これを行うためのより良い方法はありますか?たぶん、オブジェクトが持続した後にのみサービスレイヤーによって発射されるイベントをキューアップするドメインオブジェクトを使用するのでしょうか?
アップデート: サービスレイヤーがありますが、比較プロセスを実行して、特定の集計ルートが保存されている場合にどのようなイベントを起動すべきかを判断するのは面倒で過度に思えます。これらのイベントの一部は粒状であるため(「注文ステータス変更」など)、オブジェクトのDBコピーを取得し、プロパティを比較してイベントを作成し、新しいオブジェクトを保存してから、イベントをNServiceBusに送信する必要があると思います。保存操作が正常に完了しました。
アップデート
私がしたことは、以下に投稿した答えに続いて、下の方法)、私のドメインエンティティに組み込むことでした EventQueue
a List<IDomainEvent>
. 。次に、ドメインに変更されたイベントを追加しました。これにより、ドメイン内でロジックを維持することができました。これは、エンティティ内で何が起こっているかに基づいてイベントを発射しているため適切だと思います。
次に、サービスレイヤーにオブジェクトを保持すると、そのキューを処理し、実際にイベントをサービスバスに送信します。当初、私はアイデンティティPKSを使用するレガシーデータベースを使用することを計画していたので、エンティティのIDを入力するためにこれらのイベントを後処理しなければなりませんでしたが、最終的にはAに切り替えることにしました。 Guid.Comb
そのステップをスキップできるPK。
解決 4
ソリューションは実装に基づいていることが判明しました これらの拡張メソッド nhibernateセッションオブジェクトで。
私はおそらく質問の文言について少し不明確でした。建築上の問題の全体的な理由は、手動でプロクシストしていない限り、あらゆる種類の陰謀を経験しない限り、Nhibernateオブジェクトが常に同じ状態にあるという事実でした。それは、どのプロパティが変更されたか、したがってどのようなイベントを発射するかを判断するためにやりたくなかったことでした。
プロパティセッターでこれらのイベントを発射することは、最終的に失敗する可能性のある操作でイベントを発射することを避けるために、変更が続いた後にのみイベントが発生する必要があるため、機能しません。
だから私がしたことは、リポジトリベースにいくつかの方法を追加することでした:
public bool IsDirtyEntity(T entity)
{
// Use the extension method...
return SessionFactory.GetCurrentSession().IsDirtyEntity(entity);
}
public bool IsDirtyEntityProperty(T entity, string propertyName)
{
// Use the extension method...
return SessionFactory.GetCurrentSession().IsDirtyProperty(entity, propertyName);
}
その後、私のサービスで Save
方法、私はこのようなことをすることができます(ここでnservicebusを使用していることを覚えておいてください。しかし、Udi Dahanのドメインイベントの静的クラスを使用している場合、同様に機能します):
var pendingEvents = new List<IMessage>();
if (_repository.IsDirtyEntityProperty(order, "Status"))
pendingEvents.Add(new OrderStatusChanged()); // In reality I'd set the properties of this event message object
_repository.Save(order);
_unitOfWork.Commit();
// If we get here then the save operation succeeded
foreach (var message in pendingEvents)
Bus.Send(message);
場合によっては Id
エンティティの保存まで設定されない場合があります(私は使用しています Identity
整数列)、イベントオブジェクトのプロパティを入力するためにIDを取得するためにトランザクションをコミットした後に実行する必要がある場合があります。これは既存のデータであるため、Hiloタイプのクライアントが割り当てたIDに簡単に切り替えることはできません。
他のヒント
私の解決策は、ドメイン層とサービス層の両方でイベントを上げることです。
あなたのドメイン:
public class Order
{
public void ChangeStatus(OrderStatus status)
{
// change status
this.Status = status;
DomainEvent.Raise(new OrderStatusChanged { OrderId = Id, Status = status });
}
public void AddNote(string note)
{
// add note
this.Notes.Add(note)
DomainEvent.Raise(new NoteCreatedForOrder { OrderId = Id, Note = note });
}
}
あなたのサービス:
public class OrderService
{
public void SubmitOrder(int orderId, OrderStatus status, string note)
{
OrderStatusChanged orderStatusChanged = null;
NoteCreatedForOrder noteCreatedForOrder = null;
DomainEvent.Register<OrderStatusChanged>(x => orderStatusChanged = x);
DomainEvent.Register<NoteCreatedForOrder>(x => noteCreatedForOrder = x);
using (var uow = UnitOfWork.Start())
{
var order = orderRepository.Load(orderId);
order.ChangeStatus(status);
order.AddNote(note);
uow.Commit(); // commit to persist order
}
if (orderStatusChanged != null)
{
// something like this
serviceBus.Publish(orderStatusChanged);
}
if (noteCreatedForOrder!= null)
{
// something like this
serviceBus.Publish(noteCreatedForOrder);
}
}
}
ドメインイベントは...ドメインで提起する必要があります。それが彼らがドメインイベントである理由です。
public void ExecuteCommand(MakeCustomerGoldCommand command)
{
if (ValidateCommand(command) == ValidationResult.OK)
{
Customer item = CustomerRepository.GetById(command.CustomerId);
item.Status = CustomerStatus.Gold;
CustomerRepository.Update(item);
}
}
(そして、顧客クラスでは、以下):
public CustomerStatus Status
{
....
set
{
if (_status != value)
{
_status = value;
switch(_status)
{
case CustomerStatus.Gold:
DomainEvents.Raise(CustomerIsMadeGold(this));
break;
...
}
}
}
Raise Methodは、イベントストアにイベントを保存します。また、ローカル登録されたイベントハンドラーを実行することもできます。
あなたは必要だったようですね サービスレイヤー. 。サービスレイヤーは、フロントエンドまたはコントローラーとビジネスレイヤーまたはドメインモデルの間にある別の抽象化です。アプリケーションへのAPIのようなものです。その後、コントローラーはサービスレイヤーにのみアクセスできます。
その後、ドメインモデルと対話するサービスレイヤーの責任になります
public Order GetOrderById(int id) {
//...
var order = orderRepository.get(id);
//...
return order;
}
public CreateOrder(Order order) {
//...
orderRepositroy.Add(order);
if (orderRepository.Submitchanges()) {
var email = emailManager.CreateNewOrderEmail(order);
email.Send();
}
//...
}
次のような「マネージャー」オブジェクトで終わるのが一般的です OrderManager
POCOSに対処するために注文とサービスレイヤーと対話する。
UIがこれらのイベントを提起するのは適切ですか?どのイベントが発生したかを知っており、サービスレイヤーがオブジェクトを保存しても正常に発生した後にのみ発射できます。コントローラーがドメインイベントを発射することについて何かが間違っているようです。
いいえ。新しいアクションが追加され、開発者が気付いていない場合、または電子メールが解雇されるべきであることを忘れている場合、問題が発生します。
リポジトリは、正常に持続した後にイベントを発射する必要がありますか?
いいえ。リポジトリの応答性は、データアクセスよりも抽象化を提供することです。
これを行うためのより良い方法はありますか?たぶん、オブジェクトが持続した後にのみサービスレイヤーによって発射されるイベントをキューアップするドメインオブジェクトを使用するのでしょうか?
はい、これはサービスレイヤーによって処理されるべきだと思われます。
ドメインイベントがある場合は、うまく機能します コマンド, 、あなたに送られました サービスレイヤー. 。その後、コマンドに従ってエンティティを更新し、単一のトランザクションで対応するドメインイベントを上げることができます。
エンティティ自体を移動している場合 ui と サービスレイヤー, 、明示的ではなく、エンティティの状態の下に隠されているため、どのドメインイベントが発生したかを判断することは非常に難しくなります(そして時には不可能です)。
デビッド・グレンが言ったように、あなたはドメインサービスを持っていると思います。
私が遭遇した問題は、サービスレイヤーが変更されたオブジェクトを保存する前にいくつかのオブジェクトのコピーをプルして、古いバージョンと新しいバージョンを比較し、どのイベントを起動するかを決定する必要があることです。
ドメインサービスには、レジスタNeworder、CreateNoteForOrder、ChangeOrderStatusなど、ドメインエンティティでやりたいことを明確に述べる方法が含まれている必要があります。
public class OrderDomainService()
{
public void ChangeOrderStatus(Order order, OrderStatus status)
{
try
{
order.ChangeStatus(status);
using(IUnitOfWork unitOfWork = unitOfWorkFactory.Get())
{
IOrderRepository repository = unitOfWork.GetRepository<IOrderRepository>();
repository.Save(order);
unitOfWork.Commit();
}
DomainEvents.Publish<OrderStatusChnaged>(new OrderStatusChangedEvent(order, status));
}
}
}