Skip to main content

IntegrationEvent base

Cross-service event’lerin temel sınıfı (src/BuildingBlocks/.../Contracts.Events/IntegrationEvent.cs). Tüm event POCO’ları bundan türer; INotification’dır:
public class IntegrationEvent : INotification
{
    public IntegrationEvent()
    {
        Id = Guid.CreateVersion7(DateTime.UtcNow);   // time-ordered v7
        CreatedAt = DateTime.UtcNow;
    }

    [JsonConstructor]
    public IntegrationEvent(Guid id, DateTime createdAt)
    {
        Id = id;
        CreatedAt = createdAt;
    }

    [JsonInclude] public Guid Id { get; private init; }
    [JsonInclude] public DateTime CreatedAt { get; private init; }
}
[JsonConstructor] + [JsonInclude] private init’e izin verir: receiver tarafında deserialize edilirken Id ve CreatedAt orijinal değerlerini korur (yeniden üretilmez). Bu, idempotency ve duplicate detection için önemlidir.

IBusEvent ve IBusMessage

MediatorExtension bir domain event’i in-process mı yoksa bus’a mı göndereceğine IBusEvent marker’ı ile karar verir:
// Bus'a yayınlanan tüm event'lerin marker'ı
public interface IBusEvent : INotification, IBusMessage
{
    string GetEventKey();   // topic routing key üretimi için
}

public interface IBusMessage { }   // bus üzerinden taşınan tüm tipler için marker
MassTransitEventBus.PublishAsync routing key’i şöyle belirler: event IBusEvent ise GetEventKey(), değilse tip adı. RoutingKeyPrefix (default integration.event) ile birleştirilir:
string routingKey = @event is IBusEvent busEvent ? busEvent.GetEventKey() : @event.GetType().Name;
context.SetRoutingKey($"{_options.RoutingKeyPrefix}.{routingKey}");

Örnek: UserCreatedIntegrationEvent

[MessageUrn("urn:event:user-created:v1")]
public class UserCreatedIntegrationEvent : IntegrationEvent
{
    public Guid UserId { get; init; }
    public string? FullName { get; init; }
    public string Phone { get; init; } = string.Empty;
    public string Email { get; init; } = string.Empty;

    public UserCreatedIntegrationEvent() { }
    public UserCreatedIntegrationEvent(Guid userId, string? fullName, string phone, string email)
    {
        UserId = userId; FullName = fullName; Phone = phone; Email = email;
    }
}
[MessageUrn("urn:event:...:v1")] cross-service deserialization için stabil bir identity sağlar — tip adı/namespace değişse bile mesajlar eşleşir. Versiyon son ek (:v1) şema evrimi içindir.
CacheInvalidationIntegrationEvent de aynı kalıbı kullanır (urn:event:cache-invalidation:v1); ayrıntı: Multi-Instance Senkron.

Handler kontratı: IIntegrationEventHandler<T>

Üretici servis event’i Contracts.Events’ten import eder; tüketici servis handler’ı implement eder:
public interface IIntegrationEventHandler<in TIntegrationEvent> : IIntegrationEventHandler
    where TIntegrationEvent : IntegrationEvent
{
    Task Handle(TIntegrationEvent @event);

    Task IIntegrationEventHandler.Handle(IntegrationEvent @event) => Handle((TIntegrationEvent)@event);
}

// Tipsiz handler — keyed DI registration için.
public interface IIntegrationEventHandler { Task Handle(IntegrationEvent @event); }

Kayıt: AddMassTransitSubscription<T, TH>

Event/handler çiftini iki işle birden register eder (BuildingBlocks.EventBus.MassTransit.RabbitMq/EventBusBuilderExtensions.cs):
public static IEventBusBuilder AddMassTransitSubscription<T, TH>(this IEventBusBuilder builder)
    where T : IntegrationEvent
    where TH : class, IIntegrationEventHandler<T>
{
    // 1) Keyed handler kaydı + EventTypes mapping (RabbitMQ binding'i bunu kullanır)
    EventBusBuilderExtensions.AddSubscription<T, TH>(builder);
    // 2) MassTransit consumer'ı DI'a ekle
    builder.Services.AddScoped<IntegrationEventConsumer<T>>();
    return builder;
}
Application katmanındaki kullanım (Application/DependencyInjection.cs):
builder.Services.AddMassTransitRabbitMqEventBus(builder.Configuration, x =>
    {
        x.AddEntityFrameworkOutbox<DiyanetCleanArchitectureDbContext>(o => { /* ... */ });
    })
    .AddMassTransitSubscription<CacheInvalidationIntegrationEvent, CacheInvalidationIntegrationEventHandler>();
UserCreatedIntegrationEvent ve CitizenCreatedIntegrationEvent şu an yayın-only: yayınlanır ve outbox/RabbitMQ’ya gider, ama bu projede dinleyen consumer henüz yoktur. Cross-service tüketici çıktığında AddMassTransitSubscription ile kayıt eklenir.

IntegrationEventConsumer<T> — keyed handler çözümü

Tek bir generic consumer tüm tipler için çalışır; gelen mesaj tipine kayıtlı tüm handler’ları keyed DI’dan resolve eder ve sırayla çağırır:
public class IntegrationEventConsumer<T>(IServiceProvider serviceProvider) : IConsumer<T>
    where T : IntegrationEvent
{
    public async Task Consume(ConsumeContext<T> context)
    {
        var handlers = serviceProvider.GetKeyedServices<IIntegrationEventHandler>(typeof(T));
        foreach (var handler in handlers)
            if (handler is IIntegrationEventHandler<T> integrationHandler)
                await integrationHandler.Handle(context.Message);
    }
}
Aynı event’e birden çok handler kaydedilebilir (keyed services), ama RabbitMQ tarafında consumer tipi tektir — binding’ler distinct event tipleri üzerinden kurulur.

İdempotent handler

Mesaj en az bir kez (at-least-once) teslim edilebilir: retry, redelivery veya broker yeniden teslimi sonucu aynı event iki kez gelebilir. Bu yüzden handler’lar idempotent yazılmalı:
  • Outbox DuplicateDetectionWindow (30 dk) MassTransit Inbox tarafında tekrarları yakalar (bkz. Outbox Pattern).
  • Yine de handler kendi tarafında güvende olmalı: event.Id ile işlenmişlik kontrolü, upsert semantiği, veya doğal idempotent işlem (örn. RemoveByTagLocallyAsync tekrar çağrılması zararsızdır).

İlgili

Outbox Pattern

Transactional outbox + Inbox duplicate detection.

RabbitMQ Topology

Exchange, routing key, queue, retry.

Domain Events

Domain event’ten integration event yayma.

Multi-Instance Senkron

CacheInvalidationIntegrationEvent örneği.