Skip to main content
Domain event, bir aggregate’in içinde “şu önemli şey oldu” demesinin yoludur. Aggregate metodu state’i değiştirir ve AddDomainEvent(...) ile event’i biriktirir; IUnitOfWork.SaveEntitiesAsync SaveChanges’ten sonra bu event’leri MediatR’a dispatch eder. Bir INotificationHandler<TEvent> onu yakalar (SMS, e-posta, cache drop, vb.). Süreç-arası ihtiyaç varsa handler ayrıca bir integration event yayar (Outbox → RabbitMQ).
Referans gerçek örnekler: Domain/Events/UserOtpGeneratedDomainEvent.cs · Domain/AggregatesModel/UserAggregate/User.cs (AddOtpChallenge) · Application/DomainEventHandlers/UserOtpGenerated/SendOtpSmsDomainEventHandler.cs · Application/DomainEventHandlers/FaqUpdated/InvalidateFaqCacheDomainEventHandler.cs

Genel akış

Reçete

1

Domain event sınıfını tanımla

src/DiyanetCleanArchitecture.Domain/Events/FaqArchivedDomainEvent.cs. DomainEvent (SharedKernel) tabanından türet ve INotification işaretle. Event immutable ve yalnızca veri taşır:
using DiyanetCleanArchitecture.Domain.SharedKernel.SeedWork;
using MediatR;

namespace DiyanetCleanArchitecture.Domain.Events
{
    public class FaqArchivedDomainEvent : DomainEvent, INotification
    {
        public Guid FaqId      { get; }
        public DateTime ArchivedAt { get; }

        public FaqArchivedDomainEvent(Guid faqId, DateTime archivedAt)
        {
            FaqId      = faqId;
            ArchivedAt = archivedAt;
        }
    }
}
DomainEvent tabanı zaten Id (Guid v7), CorrelationID ve CreatedAt sağlar — bunları yeniden tanımlamayın. Konvansiyon: sınıf adı ...DomainEvent ile biter.
2

Aggregate metodunda AddDomainEvent

Event yalnızca aggregate root’un içinden, iş kuralı geçtikten sonra yayılır. User.AddOtpChallenge’taki gerçek deseni izleyin:
// Faq aggregate (örnek)
public void Archive()
{
    if (IsArchived)
        throw new DomainException("FAQ zaten arşivlenmiş.");

    IsArchived = true;
    ArchivedAt = DateTime.UtcNow;

    AddDomainEvent(new FaqArchivedDomainEvent(Id, ArchivedAt.Value));
}
Karşılaştırma — gerçek User örneği:
public void AddOtpChallenge(OtpCode code, OtpType type)
{
    if (CanRequestOtp == false)
        throw new DomainException("Kullanıcı durumu giriş yapmaya uygun değil.");
    // ... challenge oluştur ...
    AddDomainEvent(new UserOtpGeneratedDomainEvent(this, code, type, this.Phone, this.Email));
}
3

Komutu/handler'ı bağla (gerekirse yeni)

Aggregate metodunu bir komut handler’dan çağırın ve mutlaka SaveEntitiesAsync ile kaydedin — event’ler ancak o zaman dispatch edilir:
public class ArchiveFaqCommandHandler : IRequestHandler<ArchiveFaqCommand, IResponseWrapper<bool>>
{
    private readonly IRepository<Faq> _repo;
    private readonly IUnitOfWork _uow;
    // ctor ...

    public async Task<IResponseWrapper<bool>> Handle(ArchiveFaqCommand request, CancellationToken ct)
    {
        var faq = await _repo.SingleOrDefaultAsync(new FaqByIdSpecification(request.FaqId), ct)
                  ?? throw new ApplicationException("FAQ bulunamadı");
        faq.Archive();
        await _repo.UpdateAsync(faq, ct);
        await _uow.SaveEntitiesAsync(ct);   // ← event dispatch tetikleyicisi
        return ResponseWrapper<bool>.Success(true);
    }
}
Yeni komut iskeleti için Playbook’lar overview — Command reçetesi.
4

Domain event handler yaz

src/DiyanetCleanArchitecture.Application/DomainEventHandlers/FaqArchived/InvalidateFaqCacheDomainEventHandler.cs. INotificationHandler<TEvent> implement edin. Yan etki (cache/SMS/e-posta) burada:
public sealed class InvalidateFaqCacheDomainEventHandler
    : INotificationHandler<FaqArchivedDomainEvent>
{
    private readonly IHybridRequestCache _cache;
    private readonly ILogger<InvalidateFaqCacheDomainEventHandler> _logger;
    // ctor ...

    public async Task Handle(FaqArchivedDomainEvent e, CancellationToken ct)
    {
        try   { await _cache.RemoveByTagAsync(CacheTags.Faqs, ct); }
        catch (Exception ex) { _logger.LogWarning(ex, "[CacheInvalidate] FaqArchived failed: {FaqId}", e.FaqId); }
    }
}
Konvansiyon: handler’lar Application/DomainEventHandlers/<EventName>/<Amaç>DomainEventHandler.cs altında, tek class tek dosya. Aynı event için birden çok yan etki gerekiyorsa (örn. SMS + e-posta) ayrı handler’lar yazın — SendOtpSmsDomainEventHandler ve SendOtpEmailDomainEventHandler aynı UserOtpGeneratedDomainEvent’i bağımsız dinler.
5

(Opsiyonel) Integration event yayını

Olay başka bir instance/servis tarafından da işlenmeliyse (örn. çoklu-instance cache, arama indeksi, harici bildirim), IEventBus ile bir IntegrationEvent yayın. Sözleşme BuildingBlocks.Contracts.Events altında durur:
// src/BuildingBlocks/BuildingBlocks.Contracts.Events/Faq/FaqArchivedIntegrationEvent.cs
public sealed record FaqArchivedIntegrationEvent(Guid FaqId, DateTime ArchivedAt) : IntegrationEvent;
Yayını domain event handler’ında yapın:
public sealed class PublishFaqArchivedIntegrationEventHandler
    : INotificationHandler<FaqArchivedDomainEvent>
{
    private readonly IEventBus _eventBus;
    public Task Handle(FaqArchivedDomainEvent e, CancellationToken ct)
        => _eventBus.PublishAsync(new FaqArchivedIntegrationEvent(e.FaqId, e.ArchivedAt));
}
PublishAsync mesajı aynı transaction içinde MassTransit EF Outbox’a (messaging.outbox_message) yazar; relay onu RabbitMQ topic exchange’ine (integration_event_bus, routing key integration.event.FaqArchivedIntegrationEvent) bırakır.
6

(Opsiyonel) Tüketici + abonelik

Tüketen taraf IIntegrationEventHandler<FaqArchivedIntegrationEvent> implement eder; gerçek örnek CacheInvalidationIntegrationEventHandler’dır. Aboneliği MassTransit konfigürasyonunda AddMassTransitSubscription ile bağlayın ki IntegrationEventConsumer<FaqArchivedIntegrationEvent> kayıtlansın. Retry/DLX davranışı için Outbox Pattern ve RabbitMQ Topolojisi.
7

Test

En az aggregate seviyesinde event üretimini doğrulayın:
[Fact]
public void Archive_raises_FaqArchivedDomainEvent()
{
    var faq = Faq.Create("S", "C");
    faq.Archive();

    faq.DomainEvents.Should().ContainSingle(e => e is FaqArchivedDomainEvent);
}

[Fact]
public void Archive_twice_throws()
{
    var faq = Faq.Create("S", "C");
    faq.Archive();
    var act = () => faq.Archive();
    act.Should().Throw<DomainException>();
}
Handler için DB değişikliği varsa migration ekleyin:
dotnet ef migrations add FaqArchive \
  -p src/DiyanetCleanArchitecture.Infrastructure.EFCore \
  -s src/DiyanetCleanArchitecture.API

Domain event mi, integration event mi?

SoruDomain eventIntegration event
Aynı process içinde mi işlenir?EvetHayır (process-arası)
Taban sınıfDomainEvent : INotificationIntegrationEvent
TetikleyiciAddDomainEventSaveEntitiesAsyncIEventBus.PublishAsync
TeslimatIn-process, transaction içiOutbox → RabbitMQ, at-least-once
Idempotency gerek?HayırEvet (consumer idempotent olmalı)
ÖrnekUserOtpGeneratedDomainEventCacheInvalidationIntegrationEvent
Çoğu durumda yalnızca domain event yeterlidir. Integration event’i ancak birden çok instance/servis olayı bağımsız işlemeliyse ekleyin — gereksiz integration event Outbox ve RabbitMQ yükü demektir.

İlgili

Domain Event'ler

Dispatch mekaniği ve SharedKernel DomainEvent.

Integration Event'ler

IEventBus, sözleşmeler, consumer’lar.

Outbox Pattern

Transaction-güvenli yayın + retry/DLX.

Cache Invalidation

Event’i cache düşürmek için kullanma.