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 CreatedAtorijinal değerlerini korur (yeniden üretilmez). Bu, idempotency ve duplicate detection için önemlidir.
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:
[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.
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.
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.
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ı:
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).