🚀 نحوهٔ انتشار (Publish) Domain Eventها با EF Core
یک راهحل زیبا برای انتشار Domain Eventها استفاده از EF Core است.
از آنجا که EF Core مانند یک Unit of Work عمل میکند،
میتوانید از آن برای جمعآوری تمام Domain Eventهای موجود در تراکنش فعلی و انتشار آنها استفاده کنید.
من دوست ندارم این فرآیند را پیچیده کنم،
بنابراین به سادگی متد SaveChangesAsync را override میکنم تا بعد از ذخیره شدن تغییرات در پایگاه داده، Domain Eventها منتشر شوند.
البته میتوانید از interceptor نیز استفاده کنید.
public class ApplicationDbContext : DbContext
{
public override async Task<int> SaveChangesAsync(
CancellationToken cancellationToken = default)
{
// چه زمانی باید Domain Eventها را منتشر کنید؟
//
// 1. قبل از فراخوانی SaveChangesAsync
// - Domain Eventها بخشی از همان تراکنش هستند
// - تضمین immediate consistency
// 2. بعد از فراخوانی SaveChangesAsync
// - Domain Eventها در تراکنش جداگانه اجرا میشوند
// - eventual consistency
// - احتمال خطا در Handlerها
var result = await base.SaveChangesAsync(cancellationToken);
await PublishDomainEventsAsync();
return result;
}
}
مهمترین تصمیمی که در این مرحله باید بگیرید این است که چه زمانی Domain Eventها منتشر شوند.
من شخصاً ترجیح میدهم بعد از فراخوانی SaveChangesAsync آنها را منتشر کنم،
یعنی پس از آنکه تغییرات در پایگاه داده ذخیره شدند.
این کار البته معایب و مزایایی دارد 👇
✅ Eventual consistency:
چون پیامها پس از پایان تراکنش اصلی پردازش میشوند.
⚠️ ریسک ناسازگاری دادهها (Database inconsistency): چون ممکن است اجرای Domain Eventها با خطا مواجه شود.
با eventual consistency میتوانم کنار بیایم،
اما ریسک inconsistency در پایگاه داده موضوع مهمی است.
برای حل این مشکل میتوان از الگوی Outbox استفاده کرد 💡
در این الگو، تغییرات در پایگاه داده بهصورت اتمیک (Atomic) به همراه Domain Eventها (به عنوان پیامهای Outbox) ذخیره میشوند.
سپس Domain Eventها به صورت ناهمزمان (asynchronously) توسط یک background job پردازش میشوند.
اگر میخواهید بدانید داخل متد PublishDomainEventsAsync چه اتفاقی میافتد 👇
private async Task PublishDomainEventsAsync()
{
var domainEvents = ChangeTracker
.Entries<Entity>()
.Select(entry => entry.Entity)
.SelectMany(entity =>
{
var domainEvents = entity.GetDomainEvents();
entity.ClearDomainEvents();
return domainEvents;
})
.ToList();
foreach (var domainEvent in domainEvents)
{
await _publisher.Publish(domainEvent);
}
}
🧩 نحوهٔ Handle کردن Domain Eventها
با تمام زیرساختی که تا اینجا ایجاد کردیم،
اکنون آمادهایم که Handler مربوط به Domain Eventها را پیادهسازی کنیم.
خوشبختانه این مرحله سادهترین بخش کار است ✨
کافی است کلاسی بسازید که <INotificationHandler<Tرا پیادهسازی کند
و نوع Domain Event خود را به عنوان پارامتر generic مشخص نمایید.
در مثال زیر، یک Handler برای CourseCompletedDomainEvent داریم
که پس از وقوع Domain Event، یک CourseCompletedIntegrationEvent منتشر میکند
تا سایر سیستمها را مطلع سازد 👇
public class CourseCompletedDomainEventHandler
: INotificationHandler<CourseCompletedDomainEvent>
{
private readonly IBus _bus;
public CourseCompletedDomainEventHandler(IBus bus)
{
_bus = bus;
}
public async Task Handle(
CourseCompletedDomainEvent domainEvent,
CancellationToken cancellationToken)
{
await _bus.Publish(
new CourseCompletedIntegrationEvent(domainEvent.CourseId),
cancellationToken);
}
}