⚖️ Domain Events در مقابل Integration Events
ممکن است قبلاً دربارهٔ Integration Events شنیده باشید و اکنون این سؤال برایتان پیش آمده باشد که تفاوت آنها با Domain Events در چیست.
از نظر معنایی (Semantically)، هر دو یکی هستند:
نمایانگر چیزیاند که در گذشته اتفاق افتاده است.
اما هدف (Intent) آنها متفاوت است — و درک این تفاوت اهمیت زیادی دارد.
🧩 Domain Events:
• درون یک Domain واحد منتشر و مصرف (Consume) میشوند.
• از طریق یک in-memory message bus ارسال میشوند.
• میتوانند بهصورت synchronous یا asynchronous پردازش شوند.
🔗 Integration Events:
• توسط زیرسیستمها (مانند microservices یا Bounded Contexts) مصرف میشوند.
• از طریق message broker و روی queue ارسال میشوند.
• بهصورت کاملاً asynchronous پردازش میشوند.
💡 بنابراین، اگر نمیدانید چه نوع Eventی باید منتشر کنید، به هدف (Intent) و اینکه چه کسی باید آن را مدیریت کند فکر کنید.
در واقع، Domain Events میتوانند برای تولید Integration Events نیز مورد استفاده قرار گیرند - رویدادهایی که از مرز (boundary) Domain خارج میشوند.
🏗 پیادهسازی Domain Events
روشی که من برای پیادهسازی Domain Events ترجیح میدهم، ایجاد یک abstraction به نام IDomainEvent و پیادهسازی آن از MediatR.INotification است.
مزیت این کار این است که میتوان از قابلیت publish-subscribe در MediatR برای انتشار یک Notification به یک یا چند Handler استفاده کرد.
using MediatR;
public interface IDomainEvent : INotification
{
}
اکنون میتوانید یک Domain Event مشخص (Concrete) را پیادهسازی کنید.
در هنگام طراحی Domain Events، چند نکتهٔ مهم را باید در نظر بگیرید 👇
⚙️ نکات کلیدی طراحی Domain Events:
🔹️Immutability (تغییرناپذیری) :
چون Domain Event یک واقعیت (Fact) است، باید غیرقابلتغییر باشد.
🔹️Fat vs Thin Domain Events :
چقدر اطلاعات لازم دارید؟ (Eventها را بیش از حد سنگین یا بیش از حد سبک طراحی نکنید.)
🔹️نامگذاری با زمان گذشته :
برای نام Event از past tense استفاده کنید.
🔸 مثال:
public class CourseCompletedDomainEvent : IDomainEvent
{
public Guid CourseId { get; init; }
}
🚀 Raising Domain Events
پس از اینکه Domain Eventهای خود را ایجاد کردید، باید بتوانید آنها را از درون Domain فراخوانی (raise) کنید.
روش پیشنهادی من این است که یک کلاس پایه به نام Entity بسازید،
چون فقط Entityها مجازند Domain Event ایجاد کنند.
برای کپسولهسازی (Encapsulation) بهتر، متد RaiseDomainEvent را میتوان protected تعریف کرد تا فقط از داخل کلاس یا زیرکلاسها قابل فراخوانی باشد.
در این پیادهسازی، ما Domain Eventها را در یک لیست داخلی (internal collection) نگهداری میکنیم تا هیچ بخش دیگری از سیستم نتواند مستقیماً به آن دسترسی داشته باشد.
متد GetDomainEvents یک snapshot از لیست داخلی برمیگرداند،
و متد ClearDomainEvents برای پاکسازی لیست داخلی استفاده میشود.
public abstract class Entity : IEntity
{
private readonly List<IDomainEvent> _domainEvents = new();
public IReadOnlyList<IDomainEvent> GetDomainEvents()
{
return _domainEvents.ToList();
}
public void ClearDomainEvents()
{
_domainEvents.Clear();
}
protected void RaiseDomainEvent(IDomainEvent domainEvent)
{
_domainEvents.Add(domainEvent);
}
}
اکنون Entityهای شما میتوانند از این کلاس پایه ارثبری کرده و Domain Eventها را raise کنند 👇
public class Course : Entity
{
public Guid Id { get; private set; }
public CourseStatus Status { get; private set; }
public DateTime? CompletedOnUtc { get; private set; }
public void Complete()
{
Status = CourseStatus.Completed;
CompletedOnUtc = DateTime.UtcNow;
RaiseDomainEvent(new CourseCompletedDomainEvent { CourseId = this.Id });
}
}
✅ حالا تنها کاری که باقی مانده، انتشار (Publish) کردن Domain Eventها است.