Skip to main content
Bildirim servisi src/DiyanetCleanArchitecture.Infrastructure.Services.Notification projesindedir. Admin panelindeki kullanıcılara gerçek-zamanlı bildirim push etmek için Server-Sent Events (SSE) kullanır. Transport-agnostic bir broadcaster arayüzü etrafında kuruludur.

Broadcaster arayüzü

public interface IAdminNotificationBroadcaster
{
    Task PublishAsync(NotificationEnvelope notification, CancellationToken cancellationToken = default);
    SubscriptionHandle Subscribe(Guid userId, IReadOnlyList<int> roleIds);
    void Unsubscribe(SubscriptionHandle handle);
}
Default implementasyon InMemoryAdminNotificationBroadcaster’dır (singleton, tek API instance için). Her SSE bağlantısı bir Channel<NotificationEnvelope> tutar.

Rol-hedefli yayın

PublishAsync, NotificationEnvelope.TargetRoleId alanına göre filtre uygular: null ise tüm personele, dolu ise yalnızca o role sahip abonelere yazar. Yazım bekletmezDropOldest ile en eski mesaj düşürülür:
public Task PublishAsync(NotificationEnvelope notification, CancellationToken cancellationToken = default)
{
    foreach (var sub in _subs.Values)
    {
        if (notification.TargetRoleId is int target && !sub.RoleIds.Contains(target))
            continue;

        sub.Writer.TryWrite(notification);
    }
    return Task.CompletedTask;
}
Abonelik kanalı kapasite 50 ve DropOldest ile sınırlıdır — yavaş okuyan client backpressure yaratmasın diye:
var channel = Channel.CreateBounded<NotificationEnvelope>(new BoundedChannelOptions(50)
{
    FullMode     = BoundedChannelFullMode.DropOldest,
    SingleReader = true,
    SingleWriter = false,
});
Kapasite 50 yeterlidir: sayfa açılışında REST list endpoint’i tam state’i tazelediği için, SSE yalnızca “canlı” delta’ları taşır.

NotificationEnvelope — transport modeli

Broadcaster, Domain entity’si yerine ince bir transport tipi taşır (Infrastructure’ın Application/Domain’e bağımlı kalmaması için):
public sealed class NotificationEnvelope
{
    public Guid     Id           { get; init; }
    public string?  Code         { get; init; }
    public int      CategoryId   { get; init; }
    public string   Category     { get; init; }
    public int      SeverityId   { get; init; }
    public string   Severity     { get; init; }
    public int?     TargetRoleId { get; init; }   // null = tüm personel
    public string   Title        { get; init; }
    public string   Body         { get; init; }
    public string?  ActionUrl    { get; init; }
    public string?  Payload      { get; init; }
    public DateTime? CreatedAt   { get; init; }
}

HMAC stream-token

EventSource (tarayıcı SSE API’si) Authorization: Bearer header gönderemez. Bu nedenle frontend, mevcut Keycloak token’ı ile kısa-ömürlü bir capability token alır ve SSE bağlantısını query param ile açar. Token üretici HmacStreamTokenService’tir:
public interface IAdminNotificationStreamTokenService
{
    string Issue(Guid userId, IReadOnlyList<int> roleIds);
    StreamTokenPrincipal? Validate(string token);
}
Token formatı base64url(payload).base64url(hmac) — JWT’ye benzer ama header yok (tek algoritma, HMAC-SHA256). Payload {"sub":"...","roles":[1,2],"exp":...} taşır. Doğrulamada imza FixedTimeEquals ile sabit-zamanlı karşılaştırılır ve exp kontrol edilir.
StreamTokenSecret boş ya da 32 karakterden kısa ise servis runtime’da rastgele 64 byte anahtar üretir ve uyarı loglar. Bu yalnızca geliştirme/test için kabul edilebilir; multi-instance prod’da paylaşılan secret zorunludur (aksi halde her instance farklı anahtarla token üretir/doğrular).
Bu token yalnızca SSE stream’ini açar; REST endpoint’lerini açamaz — sızıntı riskini azaltır (Keycloak access token kullanılmaz).

Application tarafı — AdminNotification aggregate

Bildirim oluşturmanın tek giriş noktası Application katmanındaki IAdminNotificationService.DispatchAsync’tir. Domain event handler’lar (MediatR INotificationHandler) bunu inject eder:
public interface IAdminNotificationService
{
    Task<Guid> DispatchAsync(AdminNotification notification, CancellationToken cancellationToken = default);
}
İmplementasyon, AdminNotification aggregate’ini aynı UnitOfWork ile DB’ye yazar; SaveEntitiesAsync sonrası NotificationEnvelope üretip broadcaster’a push eder (bağlı client’lar anında alır). Örnek tetikleyiciler: NotifyAdminsUserInvitedDomainEventHandler, NotifyAdminsEtkinlikKontenjanDolduDomainEventHandler, NotifyAdminsCitizenAccountDeactivatedDomainEventHandler.

Heartbeat

SSE bağlantısının canlı kalması için periyodik comment-line gönderilir. Frekans StreamHeartbeatSeconds (default 25 sn) ile ayarlanır ve nginx proxy_read_timeout (default 60 sn) altında kalmalıdır. Controller bunu dar bir INotificationStreamOptions abstraction’ı üzerinden okur.

Multi-instance — henüz desteklenmiyor

InMemoryBroadcaster: false ayarı multi-instance (Redis backplane) senaryosu içindir ama Redis impl’i henüz yoktur. Startup’ta bilinçli olarak NotSupportedException fırlatılır (fail-fast):
if (settings.InMemoryBroadcaster)
    services.AddSingleton<IAdminNotificationBroadcaster, InMemoryAdminNotificationBroadcaster>();
else
    throw new NotSupportedException(
        "Services:Notification.InMemoryBroadcaster=false için Redis backplane impl'i henüz eklenmedi. ...");

Config — Services:Notification

{
  "Services": {
    "Notification": {
      "InMemoryBroadcaster": true,
      "StreamHeartbeatSeconds": 25,
      "StreamTokenSecret": "dev-only-stream-token-secret-change-in-prod-min-32-chars",
      "StreamTokenLifetimeSeconds": 300
    }
  }
}
InMemoryBroadcaster
bool
default:"true"
true → in-memory; false → (NYI) Redis backplane, startup’ta exception.
StreamHeartbeatSeconds
int
default:"25"
SSE heartbeat frekansı (proxy timeout altında kalmalı).
StreamTokenSecret
string
HMAC anahtarı, min 32 karakter. Boş → runtime rastgele (sadece dev).
StreamTokenLifetimeSeconds
int
default:"300"
Stream-token ömrü (saniye).

DI kaydı

public static IServiceCollection AddNotificationService(this IServiceCollection services, IConfiguration configuration)
{
    services.Configure<AdminNotificationSettings>(o =>
        configuration.GetSection("Services:Notification").Bind(o));

    var settings = new AdminNotificationSettings();
    configuration.GetSection("Services:Notification").Bind(settings);

    if (settings.InMemoryBroadcaster)
        services.AddSingleton<IAdminNotificationBroadcaster, InMemoryAdminNotificationBroadcaster>();
    else
        throw new NotSupportedException(/* Redis backplane NYI */);

    services.AddSingleton<IAdminNotificationStreamTokenService, HmacStreamTokenService>();
    services.AddSingleton<INotificationStreamOptions, NotificationStreamOptions>();

    return services;
}

Domain Events

Bildirimleri tetikleyen domain event’ler ve handler’lar.

Caching

Multi-instance senaryosunda kullanılan Redis backplane deseni.