> ## Documentation Index
> Fetch the complete documentation index at: https://docs.diyanet.pro/llms.txt
> Use this file to discover all available pages before exploring further.

# Yeni Domain Event Ekleme

> Sıfırdan domain event reçetesi: event sınıfı → AddDomainEvent → handler → (opsiyonel) integration event yayını + abonelik + test.

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).

<Info>
  Referans gerçek örnekler:
  `Domain/Events/UserOtpGeneratedDomainEvent.cs` ·
  `Domain/AggregatesModel/UserAggregate/User.cs` (`AddOtpChallenge`) ·
  `Application/DomainEventHandlers/UserOtpGenerated/SendOtpSmsDomainEventHandler.cs` ·
  `Application/DomainEventHandlers/FaqUpdated/InvalidateFaqCacheDomainEventHandler.cs`
</Info>

## Genel akış

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant H as Command Handler
    participant AG as Aggregate
    participant UoW as IUnitOfWork
    participant MED as MediatR
    participant DH as XxxDomainEventHandler
    participant EB as IEventBus (opsiyonel)
    participant MQ as RabbitMQ (Outbox)

    H->>AG: aggregate.DoSomething(...)
    AG->>AG: state değiştir + AddDomainEvent(new XxxDomainEvent(...))
    H->>UoW: SaveEntitiesAsync(ct)
    UoW->>MED: SaveChanges → DispatchDomainEventsAsync
    MED->>DH: Handle(XxxDomainEvent)
    opt cross-service gerekiyorsa
        DH->>EB: PublishAsync(XxxIntegrationEvent)
        EB->>MQ: messaging.outbox_message (aynı TX)
    end
```

## Reçete

<Steps>
  <Step title="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:

    ```csharp theme={null}
    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;
            }
        }
    }
    ```

    <Note>
      `DomainEvent` tabanı zaten `Id` (Guid v7), `CorrelationID` ve `CreatedAt` sağlar — bunları yeniden tanımlamayın. Konvansiyon: sınıf adı `...DomainEvent` ile biter.
    </Note>
  </Step>

  <Step title="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:

    ```csharp theme={null}
    // 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:

    ```csharp theme={null}
    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));
    }
    ```
  </Step>

  <Step title="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:

    ```csharp theme={null}
    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](/playbooks/overview).
  </Step>

  <Step title="Domain event handler yaz">
    `src/DiyanetCleanArchitecture.Application/DomainEventHandlers/FaqArchived/InvalidateFaqCacheDomainEventHandler.cs`. `INotificationHandler<TEvent>` implement edin. Yan etki (cache/SMS/e-posta) burada:

    ```csharp theme={null}
    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); }
        }
    }
    ```

    <Warning>
      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.
    </Warning>
  </Step>

  <Step title="(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:

    ```csharp theme={null}
    // 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:

    ```csharp theme={null}
    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.
  </Step>

  <Step title="(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](/events/outbox-pattern) ve [RabbitMQ Topolojisi](/events/rabbitmq-topology).
  </Step>

  <Step title="Test">
    En az aggregate seviyesinde event üretimini doğrulayın:

    ```csharp theme={null}
    [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:

    ```bash theme={null}
    dotnet ef migrations add FaqArchive \
      -p src/DiyanetCleanArchitecture.Infrastructure.EFCore \
      -s src/DiyanetCleanArchitecture.API
    ```
  </Step>
</Steps>

## Domain event mi, integration event mi?

| Soru                            |              Domain event              |           Integration event           |
| ------------------------------- | :------------------------------------: | :-----------------------------------: |
| Aynı process içinde mi işlenir? |                  Evet                  |         Hayır (process-arası)         |
| Taban sınıf                     |      `DomainEvent : INotification`     |           `IntegrationEvent`          |
| Tetikleyici                     | `AddDomainEvent` → `SaveEntitiesAsync` |        `IEventBus.PublishAsync`       |
| Teslimat                        |       In-process, transaction içi      |    Outbox → RabbitMQ, at-least-once   |
| Idempotency gerek?              |                  Hayır                 | **Evet** (consumer idempotent olmalı) |
| Örnek                           |      `UserOtpGeneratedDomainEvent`     |  `CacheInvalidationIntegrationEvent`  |

<Note>
  Ç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.
</Note>

## İlgili

<CardGroup cols={2}>
  <Card title="Domain Event'ler" icon="diagram-project" href="/events/domain-events">
    Dispatch mekaniği ve SharedKernel DomainEvent.
  </Card>

  <Card title="Integration Event'ler" icon="network-wired" href="/events/integration-events">
    IEventBus, sözleşmeler, consumer'lar.
  </Card>

  <Card title="Outbox Pattern" icon="inbox" href="/events/outbox-pattern">
    Transaction-güvenli yayın + retry/DLX.
  </Card>

  <Card title="Cache Invalidation" icon="bolt" href="/playbooks/cache-invalidation">
    Event'i cache düşürmek için kullanma.
  </Card>
</CardGroup>
