Thursday, June 20, 2024

SignalR ve Garnet ile Anlık Kullanıcıları İzleme

Üzerinde çalıştığım kişisel bir projede sistem üzerindeki anlık kullanıcı sayısını yönetim panelinde görmek istiyorum. Normal olmayan şartlarda ve en cahil halimle bir endpoint yazıp sunucuya her bir dakikada request atarım ve sunucu da anlık kullanıcı sayısını bana response olarak dönerdi. Ama bu hiç mantıklı değil, sunucuya ciddi anlamda yük bindirmiş oluruz(olmaz ama öyle varsayalım). O zaman çözüm SignalR. En iyi çözüm mü bilmiyorum ama onunla problemimi çözebilirim diye düşünüyorum. 

SignalR, Microsoft tarafından geliştirilmiş real-time iletişim sağlayan bir kütüphane. Bu iletişim server ile client tarayıcısı arasında gerçekleşmekte. SignalR'ın yaptığı şey server ile client arasında kalıcı bir bağlantı oluşturmak. Yani bu bağlantı bir kere oluşuyor sonrasında sürekli olarak çalışmaya devam ediyor. Burada önemli husus server'in herhangi bir istek alma zorunluluğunun olmaması. Duruma göre server üzerinde yapılan bir işlem sonucu client'a veri gönderilebilir. Bu işleme server push denmekte. Kısacası tek bir bağlantı üzerinden server-client arasında iletişim kurabiliyoruz. SignalR, bu iletişimi sağlarken şartlar için en uygun olduğunu düşündüğü bir transport metodu kullanıyor. Performans açısından kullanılabilecek en uygun tranport metodu ise SignalR için Websockets diyebiliriz. Ancak SignalR bu seçimi server-client teknolojilerine bağlı olarak yapar. Örneğin, client oldukça eski yani 2010 öncesi bir tarayıcı sürümü kullanıyorsa bu durumda SignalR transport metodu olarak Long Polling kullanacaktır. HTML5 destekli tarayıcılar WebSockets ve Server-Sent Events kullanımına olanak sağlıyor. Bu noktada çok fazla detaya girmeyeceğim ama ayrıntılı olarak bilgi veren bazı yazılar buldum onları aşağıya ekliyorum:
SignalR'ı genel olarak kullanabileceğimiz uygulama alanları: chat, notifications, real-time oyunlar ve yapmak istediğim anlık kullanıcıları sistem üzerinde görme yani genellersek sistemi canlı izleme. 

SignalR bir protocol değil bir kütüphane, implementasyon hazır olarak yapılmış bu kütüphanede. Yani bizim WebSockets için bir geliştirme yapmamıza gerek kalmıyor. SignalR o kısımları bizim için hallediyor. Bize kalan kısım ise onun sunduğu metotları uygun şekilde kullanmak. 

Şimdi probleme dönelim. Anlık kullanıcı sayısını yönetim panelinde görmek istiyorum. Yazdığım projenin Asp.Net Web API kısmı sunucu tarafında çalışacak, admin paneli olarak kullanacağım uygulama ise Blazor WASM yani istemci tarafında çalışacak bir uygulama.

Ayrıca gerekli değerleri geçici olarak depolamak için Garnet kullanacağım. Garnet, Microsoft tarafından geliştirilmiş remote cache-store bir redis alternatifi. Ayrıntılı bilgiye buradan ulaşabilirsiniz Welcome to Garnet | Garnet (microsoft.github.io).

Bu uygulamalara SignalR'ı ekleyelim. Öncelikle API tarafında Program.cs tarafında SignalR'ı projeye dahil ediyorum:
...
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddSignalR();

var app = builder.Build();

...
Hub temelinde bir sınıf oluşturmamız lazım. Sınıfın ismi TrackHub olsun. Bu Hub sınıfı bir API olarak client'lar ile server arasında iletişim kurmamızı sağlayacak. Identity katmanında Hubs klasörü oluşturdum. Bu klasörde hub sınıflarım yer alacak. TrackHub.cs dosyası olacak şekilde ilk sınıfımı ekledim:
using Application.Contracts.Hubs;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;


namespace Identity.Hubs
{
    [Authorize]
    public class TrackHub : Hub<ITrackHub>
    {
        public override async Task OnConnectedAsync()
        {
            var httpContext = Context.GetHttpContext();
            if (httpContext != null)
            {
                var userName = httpContext.Request.Query["username"];
            }

            await base.OnConnectedAsync();
        }
    }
}
Authorize ile yetki gerekliliği sağladık bu sınıf için, sonuçta herkes bu hub'a bağlanamaz. Aynı zamanda Hub sınıfının kendisi strongly typed olmadığı için kendi strongly typed Hub'ımızı oluşturmak için bize interface kullanma imkanı sağlıyor. Bu sayede mesaj gönderirken hangi parametrelerin kullanılacağına dair bir kural oluşturmuş oluyoruz. Diğer türlü belli bir kurala bağlı olmayacağımız için eğer hata yaparsak mesaj gönderirken bunu runtime esnasında anlayabilmemiz mümkün oluyor.
namespace Application.Contracts.Hubs
{
    public interface ITrackHub
    {
        Task ReceiveActiveUserCounter(int counter);
    }
}

httpContext'i ise hangi kullanıcının hub'a bağlandığını anlamak amacıyla kullandım. Yazının devamında blazor wasm tarafında hub'a bağlanırken username'i parametre olarak göndereceğiz.

Ayrıca bir tane TrackBackgroundService adında bir Background Service oluşturdum. IHostedService olan background servisimiz arkaplanda concurrent olarak bizim belirlediğimiz süreye göre SignalR kullanarak mesaj gönderecek. Ama burada şöyle bir durum olacak. Eğer kullanıcı yönetim panelinden girdiyse bu kullanıcı veya kullanıcılara mesaj gönderilecek. Mesaj olarak göndereceğimiz şey ise Blazor Wasm uygulamasından giriş yapan kullanıcı bilgileri olacak. Background Service sayesinde anlık bir kontrol yapmış olacağız. Ancak background service kullanmamız gerekmiyor olabilir bu senaryoda ama nasıl kullanılacağını görmek amacıyla güzel bir pratik olur dedim.

using Application.Contracts.Hubs;
using Identity.Hubs;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Hosting;

namespace Identity.Services.Hubs
{
    public class TrackBackgroundService : BackgroundService
    {
        private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(10);
        private readonly IHubContext<TrackHub, ITrackHub> _context;

        public TrackBackgroundService(IHubContext<TrackHub, ITrackHub> context)
        {
            _context = context;
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            using var timer = new PeriodicTimer(Timeout);

            while (!stoppingToken.IsCancellationRequested &&
                await timer.WaitForNextTickAsync(stoppingToken))
            {
                //await _context.Clients.Client(userService.GetUserId()).ReceiveActiveUserCounter(12);
                // mesaj gönder
            }
        }
    }
}
Hosted service olduğu için kayıt işlemini aşağıdaki gibi yapıyoruz:
            });

            services.AddHostedService<TrackBackgroundService>();


            return services;
        }
    }
Oluşturduğumuz TrackHub'a gelecek olan request'leri alabilmek için kullanacağımız endpoint'i Program.cs dosyasında aşağıdaki gibi bildiriyorum:
app.UseHttpsRedirection();

app.MapHub<TrackHub>("track");

app.UseCors("all");
Tabi production'da CORS "all" falan diye kalmasın kendinize göre ayarlayın.

Şimdilik API tarafında SignalR için gerekli eklemeler yapıldı. Sıra Garnet'e geldi, zaten yukarıda bahsettim biraz. İlk olarak API tarafında Garnet için gerekli implementasyonu yapmaya başlayalım. Infrastructure'da Identity katmanında Microsoft.Garnet paketini indirdim. Application katmanında Contracts içine IGarnetCacheService adında bir interface oluşturalım:
namespace Application.Contracts.Identity
{
    public interface IGarnetCacheService
    {
        Task<string> GetValueAsync(string key);
        Task<bool> SetValueAsync(string key, string value);
        Task AppendListAsync(string key, string element);
	Task<bool> RemoveFromList(string key, string element);
        Task Clear(string key);
        void ClearAll();
    }
}
Bu interface'in implementasyonunu yazmadan önce bir Garnet'in kurulumunu yapın. Aslında tam olarak kurulum denemez, sadece bir executable file. microsoft/garnet Buradan repo clone alıp kendiniz de build edebilirsiniz veya yine aynı linkten Garnet'in son release sürümünü indirebilirsiniz. İndirdikten sonra sadece garnet-server dosyasını çalıştırmanız yeterli. Sonrasında ekrana şöyle bir şey gelecek:
 
Ayrıca client tarafında Garnet Server'i test etmek için redis-cli indirebilirsiniz. Bu sayede pratik şekilde Garnet server'ı kullanabiliyorsunuz.  Garnet'in dokümantasyonunda gördüğüm kadarıyla redis-cli ile kullanabilmeniz mümkün.

Garnet server'a bağlanabilmek için 127.0.0.1 adresini ve 6379 portunu kullanmamız gerek. Bu bilgileri appsettings.json dosyasına ekleyelim:
	"AllowedHosts": "*",
  "Garnet": {
    "Address": "127.0.0.1",
    "Port": 6379,
    "UseTLS": false
  }
}
Sonrasında API tarafında bu değerleri alabilmek için uygun bir DTO oluşturdum infrastructure katmanında:
namespace Identity.Models.DTO
{
    public class GarnetConfiguration
    {
        public int Port { get; set; }
        public string Address { get; set; } = string.Empty;
        public bool UseTLS { get; set; }
    }
}
Service kayıt sınıfında aşağıdaki configurasyonu ve gerekli IGarnetCacheService'i singleton service olarak ekledim:
            services.Configure<GarnetConfiguration>(configuration.GetSection("Garnet"));
            services.AddSingleton<IGarnetCacheService, GarnetCacheService>();
GarnetCacheService'i GarnetClient'a göre ve oluşturduğumuz interface'e uygun şekilde implement edelim:
using Application.Contracts.Identity;
using Garnet.client;
using Identity.Models.DTO;
using Microsoft.Extensions.Options;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

namespace Identity.Services
{
    public class GarnetCacheService : IGarnetCacheService
    {
        private readonly GarnetConfiguration _g;

        public GarnetCacheService(IOptions<GarnetConfiguration> garnetConfiguration)
        {
            _g = garnetConfiguration.Value;
        }

        public async Task Clear(string key)
        {
            using var db = new GarnetClient(_g.Address, _g.Port, GetSslOpts());

            await db.ConnectAsync();

            if (await Contains(db, key))
            {
                await db.KeyDeleteAsync(key);
            }
        }

        public async void ClearAll()
        {
            using var db = new GarnetClient(_g.Address, _g.Port, GetSslOpts());

            await db.ConnectAsync();
            
            await db.ExecuteForStringResultAsync("FLUSHDB");
        }

        public async Task<string> GetValueAsync(string key)
        {
            using var db = new GarnetClient(_g.Address, _g.Port, GetSslOpts());

            await db.ConnectAsync();

            return await db.StringGetAsync(key);
        }

        public async Task<bool> SetValueAsync(string key, string value)
        {
            using var db = new GarnetClient(_g.Address, _g.Port, GetSslOpts());

            await db.ConnectAsync();

            await db.StringSetAsync(key, value);


            string check = await db.StringGetAsync(key);

            if (check == value)
            {
                return true;
            }
            return false;
        }

        public async Task AppendListAsync(string key, string element)
        {
            using var db = new GarnetClient(_g.Address, _g.Port, GetSslOpts());

            await db.ConnectAsync();

            await db.ListRightPushAsync(key, element);
        }

        public async Task<bool> RemoveFromList(string key, string element)
        {
            using var db = new GarnetClient(_g.Address, _g.Port, GetSslOpts());

            await db.ConnectAsync();

            bool isRemove = int.Parse(await db.ExecuteForStringResultAsync("LREM", new string[] { key, "0", element })) == 1 ? true : false;

            return isRemove;
        }

        private async Task<bool> Contains(GarnetClient db, string key)
        {
            bool exist = int.Parse(await db.ExecuteForStringResultAsync("EXISTS", new string[] { key })) == 1 ? true : false;
            return exist;
        }
        SslClientAuthenticationOptions GetSslOpts() => _g.UseTLS ? new()
        {
            ClientCertificates = [new X509Certificate2("certfile.pfx", "temp")],
            TargetHost = "GarnetTest",
            RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true,
        } : null;
    }
}
Yaptığım implementasyon çalışacak mı emin olmamakla beraber çalışacağını umuyorum. Bu arada  Garnet kullanımı çok muhtemel kötü bir kullanım yöntemi o yüzden sonrasında iyileştirme yapmak gerekecektir.Garnet yeni olduğu için ve ben de ilk defa kullandığım için bu şekilde yazdım daha iyi kavramak amacıyla. 

Oluşturulan cache servisini dependency injection yaparak TrackHub ve oluşturulan background service'te kullanacağım. (Background service'in güncel halini birazdan ekleyeceğim):

using Application.Contracts.Hubs;
using Application.Contracts.Identity;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;


namespace Identity.Hubs
{
    [Authorize]
    public class TrackHub : Hub<ITrackHub>
    {
        private readonly IGarnetCacheService _cache;

        public TrackHub(IGarnetCacheService cache)
        {
            _cache = cache;
        }

        public override async Task OnConnectedAsync()
        {
            var httpContext = Context.GetHttpContext();
            if (httpContext != null)
            {
                string connectionId = Context.ConnectionId;
                string? userName = httpContext.Request.Query["username"];

                if(userName != null)
                {
                    await _cache.AppendListAsync("activeUsers", userName);
                }
            }

            await base.OnConnectedAsync();
        }
    }
}
Şimdi sıra SignalR Hub'ımız düzgün çalışıyor mu ve kullanıcı adı Garnet Server'a ekleniyor mu test etmeye geldi. İlk olarak Blazor Wasm uygulaması olan yönetim paneli projesine öncelikle Microsoft.AspNetCore.SignalR.Client(8.0.6) paketini yükledim. Bu paket sayesinde HubConnection oluşturabileceğim client ile server arasında. HubConnection'ı sağlamak için Dashboard.razor'u seçtim. Bu seçim tamamen size bağlı istediğiniz yerde connection'u sağlayabilirsiniz:
@using AdminUI.Models.Stat
@using Microsoft.AspNetCore.SignalR.Client

@implements IAsyncDisposable

...


@code{
    private HubConnection? hubConnection;
    [Inject]
    LocalStorageService localStorage { get; set; }
    [Inject]
    public IForumStatService forumStatService { get; set; }

    ...

    int activeUserCounter = 0;

    bool isLoaded;

    [CascadingParameter] private Task<AuthenticationState> authenticationStateTask { get; set; }

    protected async override Task OnInitializedAsync()
    {
        var authState = await authenticationStateTask;
        var user = authState.User;
        string username = string.Empty;
        if (user.Identity.IsAuthenticated)
        {
            username = user.Identity.Name;
        }

        ...
        
        string token = await localStorage.GetItem("token");

        hubConnection = new HubConnectionBuilder()
            .WithUrl(
                "https://localhost:7147/track",
                o => {
                    o.AccessTokenProvider = () => Task.FromResult<string?>(token);
                    o.Url = new Uri($"https://localhost:7147/track?username={username}");
                }
            )
            .Build();

        hubConnection.On<int>("ReceiveActiveUserCounter", async message =>
        {
            activeUserCounter = message;
            await InvokeAsync(StateHasChanged);
        });

        await hubConnection.StartAsync();

        isLoaded = true;
    }

    public async ValueTask DisposeAsync()
    {
        if(hubConnection is not null)
        {
            await hubConnection.DisposeAsync();
        }
    }
}

API tarafında çalışan TrackHub ile bağlantıyı yetki almak üzere token ile sağlıyorum. Server tarafında ReceiveActiveUserCounter mesajı gönderildikçe client'da ReceiveActiveUserCounter içindeki kod bloğu çalıştırılmış olacak. Dikkat ederseniz Client burada sadece bağlanmak için Hub'a bir kez istek attı sonrasında Server'dan client içindeki metot request olmadan çalıştırılabilecek.

Kullanıcı adını username parametresi olarak gönderiyoruz ve  server tarafında OnConnectedAsync çalıştığında Garnet Server'a activeUsers listesi adı altında eklenmiş oluyor.

Şimdi API ve AdminUI projesini çalıştıralım sonrasında kullanıcı Garnet Server'a eklenmiş mi kontrol edelim. Kontrolü redis-cli üzerinden yaptım:

admin kullanıcısının sisteme bağlandığını görebiliyorum. Fakat burada ben sayfayı yenilersem doğal olarak admin username'i tekrar eklenecek, liste boyutu artacak ve de istediğimiz şekilde çalışmamış olacak.

Bu durumda HashSet kullanmak daha mantıklı olacak. Aynı zamanda performans noktasında en iyi çözümü bu yapıyla sağlayabiliriz çünkü sürekli olarak listeyi kontrol etmek ve tekillik sağlamak optimal bir çözüm değil. İlk olarak dokümantasyonu kontrol ettim. Buradaki bilgiye göre Data Structures | Garnet (microsoft.github.io) cache servise aşağıdaki eklemeyi yaptım. (Interface'i güncellemeyi unutmayın)
    public class GarnetCacheService : IGarnetCacheService
    {
        private readonly GarnetConfiguration _g;

        public GarnetCacheService(IOptions<GarnetConfiguration> garnetConfiguration)
        {
            _g = garnetConfiguration.Value;
        }

        ...

        public async Task AddHashSet(string key, string field, string value)
        {
            using var db = new GarnetClient(_g.Address, _g.Port, GetSslOpts());

            await db.ConnectAsync();

            await db.ExecuteForStringResultAsync("HSET", new string[] { key, field, value });
        }
HSET command'i key adında bir hashset yoksa oluşturmamızı ve ayrıca içerisinde field ve ona karşılık value'yi store etmemizi sağlıyor. Eğer önceden field mevcut ise sadece value değerini güncelliyor.

Bu yapıyı kullanacağız ama nasıl? Birincisi SignalR'da her bağlantı yaptığımız da o client ve server'a özel bir ConnectionId oluşuyor. Her yeni bir bağlantıda bu ConnectionId değişebilir yani kalıcı bir id değil bu. O sebeple bizim asıl belirleyici yani ConnectionId'leri tanıma imkanı veren belirleyicimiz kullanıcı adı. Bu sebeple Garnet üzerinde hash yapısında field tarafında depolanacak veri, username'in kendisi olacak. Value tarafında ise ConnectionId depolanmış olacak. Hub bağlantısı yenilendiğinde ise eğer username'a ait bir eski ConnectionId var ise onun yerine yeni oluşan ConnectionId değeri atanacak.

İlk olarak TrackHub'da OnConnectedAsync metodunu aşağıdaki gibi güncelleyelim:
public override async Task OnConnectedAsync()
{
    var httpContext = Context.GetHttpContext();
    if (httpContext != null)
    {
        string connectionId = Context.ConnectionId;
        string? userName = httpContext.Request.Query["username"];

        if(userName != null)
        {
            await _cache.AddHashSet("activeUsers", userName, connectionId);
        }
    }

    await base.OnConnectedAsync();
}
Şimdi ilk hub bağlantı sonrası redis-cli üzerinden kontrol edelim:
Bir sorun görülmüyor, tekrar hub bağlantısını yenileyelim:
Tam olarak istediğimiz sonucu elde etmiş olduk. Sadece kullanıcı adını kontrol ederek varsa eğer son ConnectionId ile güncellemiş olduk. 

Bu bilgileri yönetim paneli arayüzünde görüntülemek istiyorum. ITrackHub'da zaten sayı almak için gerekli metotu oluşturmuştuk. Ayrıca ben aktif kullanıcı listesini de almak istiyorum. O yüzden Application katmanında ActiveUser adında bir model oluşturdum:
    public class ActiveUser
    {
        public string UserName { get; set; } = string.Empty;
        public string? ConnectionId { get; set; } 
    }
Sonrasında ITrackHub'a ReceiveActiveUsers signature'u ekledim:
    public interface ITrackHub
    {
        Task ReceiveActiveUserCounter(int counter);
        Task ReceiveActiveUsers(string jsonStr);
    }
Test amaçlı BackgroundService'i aşağıda gibi güncelledim:
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            using var timer = new PeriodicTimer(Timeout);

            while (!stoppingToken.IsCancellationRequested &&
                await timer.WaitForNextTickAsync(stoppingToken))
            {

                var exampleList = new List<ActiveUser>() { new ActiveUser { UserName = "test", ConnectionId = "testcon" } };

                var jsonStr = JsonConvert.SerializeObject(exampleList);

                await _context.Clients.All.ReceiveActiveUsers(jsonStr);
            }
        }
Blazor WASM uygulaması tarafında yine aynı şekilde ActiveUserVM adı ile aynı sınıfı oluşturuyorum:
@using AdminUI.Models.Hub
@using AdminUI.Models.Stat
@using Microsoft.AspNetCore.SignalR.Client
@using System.Text.Json.Serialization
@using System.Text.Json

@implements IAsyncDisposable

...


@code{
    private HubConnection? hubConnection;
    [Inject]
    LocalStorageService localStorage { get; set; }
    
    ...

    int activeUserCounter = 0;
    private List<ActiveUserVM>? activeUsers;

    bool isLoaded;

    [CascadingParameter] private Task<AuthenticationState> authenticationStateTask { get; set; }

    protected async override Task OnInitializedAsync()
    {
        var authState = await authenticationStateTask;
        var user = authState.User;
        string username = string.Empty;
        if (user.Identity.IsAuthenticated)
        {
            username = user.Identity.Name;
        }

        ...
        
        hubConnection.On<string>("ReceiveActiveUsers", async message =>
        {
            if(message != null)
            {
                activeUsers = JsonSerializer.Deserialize<List<ActiveUserVM>>(message);
            }
            await InvokeAsync(StateHasChanged);
        });

        await hubConnection.StartAsync();

        isLoaded = true;
    }

   	...
}
Ayrıca bu kullanıcı listesini arayüzde görmek için bir liste arayüzü oluşturalım geçici olarak:
@using AdminUI.Models.Hub
@using AdminUI.Models.Stat
@using Microsoft.AspNetCore.SignalR.Client
@using System.Text.Json.Serialization
@using System.Text.Json

@implements IAsyncDisposable

...
        <div class="row">
            @if(activeUsers != null)
            {
                <ul>
                    @foreach(var activeUser in activeUsers){
                        <li>@activeUser.UserName - @activeUser.ConnectionId</li>
                    }
                </ul>
            }
        </div>
...


@code{
	...
}
Şimdi projeyi çalıştıralım ve bakalım client tarafına mesajımız başarılı bir şekilde geliyor mu kontrol edelim:

Başarılı bir şekilde gelmiş. O zaman aktarımda bir problem yoksa asıl veriyi göndermeye başlayabiliriz. İlk olarak hashset'i uygun şekilde Garnet server'dan çekebilmem lazım:

public interface IGarnetCacheService
{
    ...
    Task<Dictionary<string, string>?> GetAllHashSet(string key);
    ...
}
Implemantasyonunu aşağıdaki gibi HGETALL command'i kullanarak yaptım.
public async Task<Dictionary<string, string>?> GetAllHashSet (string key)
{
    using var db = new GarnetClient(_g.Address, _g.Port, GetSslOpts());
    await db.ConnectAsync();

    string[] hashArray = await db.ExecuteForStringArrayResultAsync("HGETALL", new string[] { key });

    if(hashArray.Length > 0)
    {
        Dictionary<string, string> hash = new Dictionary<string, string>();
        for (int i = 0; i < hashArray.Length; i+=2)
        {
            hash.Add(hashArray[i], hashArray[(i + 1)]);
        }

        return hash;
    }
            
    return null;
}
Daha sonrasında Background Service'da bunu direkt mesaj olarak client'a gönderebiliriz. Ama biz şimdilik onu List<ActiveUser> yapısında gönderelim:
        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            using var timer = new PeriodicTimer(Timeout);

            while (!stoppingToken.IsCancellationRequested &&
                await timer.WaitForNextTickAsync(stoppingToken))
            {

                List<ActiveUser> activeUsers = new List<ActiveUser>();
                Dictionary<string, string>? activeUserHashSet = await _cache.GetAllHashSet("activeUsers");

                if(activeUserHashSet != null)
                {
                    foreach (var activeUser in activeUserHashSet)
                    {
                        activeUsers.Add(new ActiveUser { ConnectionId = activeUser.Value, UserName = activeUser.Key });
                    }
                }

                var jsonStr = JsonConvert.SerializeObject(activeUsers);

                await _context.Clients.All.ReceiveActiveUsers(jsonStr);
            }
        }
Background servisi güncelledikten sonra tekrar uygulamamızı çalıştıralım:

Sıra kullanıcı uygulamasını test etmeye geldi. Yapacağım tek şey server ile client'ı çalışan TrackHub üzerinden birbirine bağlamak. Onun için Nuget Package Manager üzerinden Microsoft.AspNetCore.SignalR.Client paketini kullanıcı uygulamasına öncelikli olarak kuralım. Sonrasında login işlemi yapılan Login.razor.cs dosyasında aşağıdaki eklemeleri yaptım HubConnection için:
using BlazorUI.Contracts;
using Microsoft.AspNetCore.Components;
using BlazorUI.Models.Authentication;
using Microsoft.AspNetCore.SignalR.Client;
using BlazorUI.Services.Common;

namespace BlazorUI.Pages;

public partial class Login
{
    public LoginVM Model { get; set; }

    [Inject]
    public NavigationManager NavigationManager { get; set; }
    public string Message { get; set; }

    [Inject]
    private IAuthenticationService AuthenticationService { get; set; }

    private HubConnection? hubConnection;
    [Inject]
    LocalStorageService localStorage { get; set; }

    public Login()
    {

    }

    ...
    
    protected async Task HandleLogin()
    {
        if (await AuthenticationService.AuthenticateAsync(Model.Email, Model.Password))
        {
            NavigationManager.NavigateTo("/");

            string token = await localStorage.GetItem("token");

            hubConnection = new HubConnectionBuilder()
            .WithUrl(
                "https://localhost:7147/track",
                o => {
                    o.AccessTokenProvider = () => Task.FromResult<string?>(token);
                    o.Url = new Uri($"https://localhost:7147/track?username={Model.Email}");
                }
            )
            .Build();

            await hubConnection.StartAsync();

        }
        Message = "Username/password combination unknown";
    }
}
İlk olarak API ve Admin uygulamasını çalıştıracağım daha sonrasında kullanıcı uygulamasını çalıştıracağım, bu sayede aktif kullanıcılara ekleniyor mu görmüş olacağız:
Başarılı bir şekilde diğer kullanıcı yönetim panelinde aktif kullanıcı listesinde görünür oldu. Yazıyı daha fazla uzatmamak adına burada sonlandırıyorum. Muhtemel gözümden kaçan hatalar, yazım yanlışları olabilir. 

No comments:

Post a Comment