Azure SignalR ve Angular ile Gerçek Zamanlı Bildirim

Merhabalar, bu yazımda Azure SignalR Service'i bir Angular ön yüzü ve .NET Core arka ucuyla nasıl konuşturduğumuza bakacağız. Konu basit gibi duruyor ama 'WebSocket'i kim taşıyor?' sorusunun cevabı projeyi büyütünce can sıkmaya başlıyor. Lafı uzatmadan başlayayım.

Modern uygulamalarda kullanıcı sayfayı yenileyerek bilgi beklemiyor; sipariş durumundan canlı uyarılara kadar her şey o anda düşmeli. Klasik yaklaşımda .NET sunucunuz binlerce WebSocket bağlantısını kendi üstünde tutar ve belirli bir noktadan sonra bu işin altından kalkmak zorlaşır. SignalR Service tam burada devreye giriyor.

Azure SignalR nedir, neyi devralıyor?

Azure SignalR Service yönetilen bir WebSocket katmanı. Yani client'lar artık doğrudan sizin sunucunuza değil, Azure'un servisine bağlanıyor. Sizin backend'iniz ise bağlantı tutmak yerine 'şu mesaji şu kullaniciya yolla' diye servise sesleniyor. Kabaca akış şöyle: Angular client SignalR Service'e bağlanır, .NET backend yine SignalR Service'e mesaj iletir, servis de mesajı doğru client'a teslim eder.

Bu mimarinin iki büyük çalışma modu var. Default mode'da hub kodu kendi backend'inizde (App Service, Container, ne olursa) yaşar; servis sadece WebSocket trafiğini taşır. Serverless mode'da ise hub Azure Functions'a taşınır, backend'de açık bir bağlantı kalmaz. Bence çoğu senaryoda default mode yeter; serverless'a ancak gerçekten event-driven bir mimariniz varsa ve sürekli açık bir hub'ı beslemek mantıksızsa geçin.

.NET tarafinda hub

Önce hub'ın kendisi. Group ve connection yönetiminin temel kalıbı şu:

public class NotificationHub : Hub
{
    public override async Task OnConnectedAsync()
    {
        var userId = Context.UserIdentifier ?? Context.ConnectionId;
        Console.WriteLine($"Baglandi: {userId}");
        await base.OnConnectedAsync();
    }

    public Task Subscribe(string channel) =>
        Groups.AddToGroupAsync(Context.ConnectionId, channel);

    public Task Unsubscribe(string channel) =>
        Groups.RemoveFromGroupAsync(Context.ConnectionId, channel);
}

Context.UserIdentifier JWT'den geliyor; bunun için AddAuthentication().AddJwtBearer(...) kurulumunu yapıp Authorize attribute'unu hub'a koymak gerekiyor. Bu sayede mesaj göndermek istediğimde Clients.User(userId) çağrısı tek bir kullanıcının tüm cihazlarına ulaşıyor. Group'lar ise konuya göre abonelik için ideal: 'siparis-guncellemeleri', 'sistem-uyarilari' gibi kanallara client istediği zaman katılır, istediği zaman çıkar.

Program.cs tarafında bağlantı dizesini verip hub'ı haritaya ekliyoruz:

builder.Services.AddSignalR().AddAzureSignalR(options =>
{
    options.ConnectionString = builder.Configuration["Azure:SignalR:ConnectionString"];
});

var app = builder.Build();
app.MapHub<NotificationHub>("/notificationhub").RequireAuthorization();

Angular tarafi

Client kütüphanesi @microsoft/signalr paketinden geliyor. Sade tutmaya çalıştığımız bir bağlantı servisi şöyle:

import * as signalR from '@microsoft/signalr';

const conn = new signalR.HubConnectionBuilder()
  .withUrl('https://api.example.com/notificationhub', {
    accessTokenFactory: () => localStorage.getItem('token') ?? ''
  })
  .withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
  .build();

conn.on('ReceiveNotification', (n) => {
  console.log('Yeni bildirim:', n);
});

await conn.start();
await conn.invoke('Subscribe', 'siparis-guncellemeleri');

accessTokenFactory hem ilk handshake'te hem de token yenilendiğinde tekrar çağrılır; siz sadece güncel JWT'yi döndürmekle yükümlüsünüz. withAutomaticReconnect parametresi ise milisaniye cinsinden tekrar deneme aralıklarını alıyor. Şahsi tercihim ilk denemeyi 0 ms yapmak; geçici bir ağ kopmasında kullanıcı hiç fark etmeden geri bağlanıyor.

Sik karsilasilan tuzaklar

  • JWT'yi Authorization header ile yollamaya çalışmak: Tarayıcı WebSocket handshake'inde özel header geçiremiyor. Bu yüzden accessTokenFactory zorunlu; token query string olarak gidiyor ve TLS olmadan bunu yapmak büyük tehlike.
  • Context.UserIdentifier'i hesaba katmamak: Auth kurulmadıysa bu alan null döner ve Clients.User(...) sessiz sedasız hiçbir şey yapmaz. Önce auth bitsin, sonra mesaj gönderme yazılsın.
  • Group'lara backend'den eklemek: Group abonelikleri connection bazlıdır; client yeniden bağlanınca grup üyeliği kaybolur. Reconnect olayında client kendi gruplarını tekrar invoke etmeli, backend hatırlamaz.
  • Default mode'u serverless ile karistirmak: Default mode'da hub backend'inizdedir, serverless'ta Functions'tadır; bağlantı dizesinin servis modu Azure portalından sabittir. Yanlış mod seçilirse client bağlanır ama mesaj asla teslim edilmez.

Olcek tarafi

Standard SKU birim başına 1000 eşzamanlı bağlantı, günde 1 milyon mesaj veriyor. Daha fazlası gerekiyorsa unit sayısını portaldan artırırsınız; servis arka planda kendi içinde paylaştırma yapar. Backend'inize ek WebSocket yükü gelmediği için kendi instance sayınız mesaj üretim hızına göre büyür, bağlantı sayısına göre değil. Bu da bence SignalR Service'in en güzel yanı.

Kapanis

Bu yazımızda Azure SignalR'ı yönetilen bir WebSocket katmanı olarak nasıl kullandığımıza, hub yazımına, JWT ile auth'a, group ve user hedeflemesine ve iki farklı çalışma moduna baktık. Bana sorarsanız çoğu ekip için default mode + Standard SKU yeterli; serverless'a geçmeden önce gerçekten event-driven bir akışınız olduğundan emin olun. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.