Blazor Wasm Signalr ve Mediatr ile Gerçek Zamanlı Güncelleme

In: Genel


Web uygulamasıyla ilgili en büyük sorunlardan biri gerçek zamanlı güncellemedir: trafiğin çoğu istemciden sunucuyadır, bu nedenle sunucu istemciye “sizin için yeni bir mesaj var” gibi bir şey söyleyemez. Neyse ki bunun için pek çok teknik çözüm var: sunucu-gönderme olayları, uzun yoklama, web soketi… .net dünyasında çok şanslıyız çünkü hangi çözümün seçileceğini soyutlamaya yardımcı olan bir proje var (bazıları sadece mevcut bazı web tarayıcılarında veya yalnızca bazı web sunucularında): SinyalR.

Bu blog yazısında, bir Blazor WASM GUI’sinin SignalR ve MediatR (tercih ettiğim diğer OSS projem) ile canlı yeniden yüklenmesinin nasıl uygulanacağını açıklayacağım.

Kurmak

Yeni bir proje ile başlayacağız (projeyi kontrol edebilirsiniz burada)

dotnet new blazorwasm -ho -o .

Bu, bir ASPNET Core arka ucu tarafından barındırılan bir Blazor WASM projesi oluşturur.

Şimdi istemci (Blazor WASM) projenize SignalR/MediatR bağımlılığı ekleyin

dotnet add Client package Microsoft.AspNetCore.SignalR.Client
dotnet add MediatR

Ve yalnızca MediatR sunucuya çünkü SignalR varsayılan olarak dahil edilir (ASPNET bağımlılıklarını her hafta değiştirirler belki ülkenizde farklıdır)

dotnet add package MediatR

SignalR kurulum sunucu tarafı

SignalR “hub” ile çalışır: sunucu bir hub oluşturur, istemci ona abone olur ve ardından sunucu mesajları hub’a iletir. İlk adım, sunucuda bir hub oluşturmaktır:

    public class HubNotificationHandler : Hub
    {
    }

Şimdi SignalR’yi ASPNET Core ara yazılımıyla bağlamanız gerekiyor, Startup.cs’de hizmeti ekleyin

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    services.AddRazorPages();
    services.AddSignalR();
    services.AddMediatR(this.GetType().Assembly);
    services.AddResponseCompression(opts =>
    {
        opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
            new[] { "application/octet-stream" });
    });
}

Ve oluşturduğumuz hub’a gelen aboneliği ileten uç noktayı ekleyin

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<NotificationHub>("/notifications");
    });
}

Diyelim ki bu sunucu tarafı MediatR bildirimim var:


[ApiController]
[Route("[controller]")]
public class HomeController : ControllerBase
{
    private static int Counter = 0;
    private MediatR.IMediator _mediator;

    public HomeController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpPost("increment")]
    public async Task Post()
    {
        int val = Interlocked.Increment(ref Counter);
        await _mediator.Publish(new CounterIncremented(val));

    }
}

Bir curl çağrısı ile tetikleyebilirsiniz

curl -X POST -d "" http://localhost:5000/home/increment

Sunucudan istemciye bildirim gönderme

Yapmak istediğim şu: MediatR’da bazı bildirimler gönderildiğinde: bunları Hub’a gönderin. Daha sonra istemcide bu bildirimi alır ve ona abone olan bileşene iletir.

İlk önce hub’a bildirim göndermem gerekiyor, bu yüzden hub’ımı bu şekilde değiştiriyorum

public class HubNotificationHandler : INotificationHandler<CounterIncremented>
{
    private readonly IHubContext<NotificationHub> _hubContext;

    public HubNotificationHandler(IHubContext<NotificationHub> hubContext)
    {
        _hubContext = hubContext;
    }

    public async Task Handle(CounterIncremented notification, CancellationToken cancellationToken)
    {
        await SendNotification(notification);
    }

    private async Task SendNotification(SerializedNotification notification)
    {
        await _hubContext.Clients.All.SendAsync("Notification", notification);
    }
}
  • İstemciye göndermek istediğim her tür bildirim için INotificationHandler’ı uygulamam gerekecek. MediatR’da joker karakter işleyicisi yapamıyoruz ama burada iyi bir şey: Ne tür olayların gönderildiğini açıkça söylemek istiyorum.

Etkinliğimin polimorfik serileştirmesini ve seri durumdan çıkarılmasını işlemek için özel bir Json dönüştürücüye ihtiyacım var. İşte tanımı

public abstract class SerializedNotification : INotification
{
    public string NotificationType
    {
        get
        {
            return this.GetType().Name;
        }
        set{}
    }
}
public class NotificationJsonConverter : JsonConverter<SerializedNotification>
{
    private readonly IEnumerable<Type> _types;

    public NotificationJsonConverter()
    {
        var type = typeof(SerializedNotification);
        _types = AppDomain.CurrentDomain.GetAssemblies()
            .SelectMany(s => s.GetTypes())
            .Where(p => type.IsAssignableFrom(p) && p.IsClass && !p.IsAbstract)
            .ToList();
    }

    public override SerializedNotification Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        if (reader.TokenType != JsonTokenType.StartObject)
        {
            throw new JsonException();
        }

        using (var jsonDocument = JsonDocument.ParseValue(ref reader))
        {   
            if (!jsonDocument.RootElement.TryGetProperty("notificationType", out var typeProperty))
            {
                throw new JsonException();
            }
            var type = _types.FirstOrDefault(x => x.Name == typeProperty.GetString());
            if (type == null)
            {
                throw new JsonException();
            }

            var jsonObject = jsonDocument.RootElement.GetRawText(); 
            var result = (SerializedNotification)JsonSerializer.Deserialize(jsonObject, type, options);
            return result;
        }
    }

    public override void Write(Utf8JsonWriter writer, SerializedNotification value, JsonSerializerOptions options)
    {
        JsonSerializer.Serialize(writer, (object)value, options);
    }
}
  • Perf en iyisi değil, GetRawText(à iyi bir fikir değil, çok fazla bellek ayırabilir. StackOverflow’ta çalışıyorsanız, bu konuda size yardımcı olacak birini bulacaksınız.
  • Bu dönüştürücü aslında basittir: JSON’a alt türde bir alan ekler ve seriyi kaldırırken onu okur. Alt tür listesi sonlu olduğundan ve geliştirici tarafından bilindiğinden (AFAIK) herhangi bir güvenlik sorunu yoktur.

Özel dönüştürücümü SignalR protokolüne şu şekilde bağlarım (Startup.cs’de)

services.AddSignalR()
        .AddJsonProtocol(o => o.PayloadSerializerOptions.Converters.Add(new NotificationJsonConverter()));

İstemci tarafında olayı dinlemeniz gerekir. Microsoft DI, çalışma zamanı dinamik yapılandırmasını desteklemediğinden (app.Build() öğesini bir kez çağırdığınızda hiçbir şeyi değiştiremezsiniz) bileşenlere bildirim göndermek için MediatR kullanamıyorum. Bu yüzden, INotificationHandler’ı uygulayan bileşenleri bulmak için bir tür hizmet bulucu yazmam gerekiyor. Bunu yapmak için bu sınıfı oluşturdum


public static class DynamicNotificationHandlers
{
    private static Dictionary<Type, List<(object,Func<SerializedNotification, Task>)>> _handlers = new Dictionary<Type, List<(object, Func<SerializedNotification, Task>)>>();
    public static void Register<T>(INotificationHandler<T> handler) where T : SerializedNotification
    {
        lock (_handlers)
        {
            var handlerInterfaces = handler
                .GetType()
                .GetInterfaces()
                .Where(x =>
                    x.IsGenericType &&
                    x.GetGenericTypeDefinition() == typeof(INotificationHandler<>))
                .ToList();
            foreach (var item in handlerInterfaces)
            {
                var notificationType = item.GenericTypeArguments.First();
                if(!_handlers.TryGetValue(notificationType,out var handlers)){
                    handlers = new List<(object, Func<SerializedNotification, Task>)>();
                    _handlers.Add(notificationType,handlers);
                }
                handlers.Add((handler, async s => await handler.Handle((T)s, default(CancellationToken))));
            }
        }
    }
    public static void Unregister<T>(INotificationHandler<T> handler) where T : SerializedNotification
    {
        lock (_handlers)
        {
            foreach (var item in _handlers)
            {
                item.Value.RemoveAll(h => h.Item1.Equals(handler));
            }
        }
    }
    public static async Task Publish(SerializedNotification notification)
    {
        try
        {
            var notificationType = notification.GetType();
            if(_handlers.TryGetValue(notificationType, out var filtered)){
                foreach (var item in filtered)
                { 
                    await item.Item2(notification);
                }
            }

        }
        catch (System.Exception e)
        {
            Console.Error.WriteLine(e + " " + e.StackTrace);

            throw;
        }
    }
}
  • Yaptığı şey, belirli bir bildirim türü için tüm uygulamaların bir listesini bellekte tutmasıdır (Eylem olayını anlamam biraz zaman aldı)
  • Tuhaf deneme yakalama, esas olarak SignalR gerçekten utangaç olduğu ve bir hata varsa hiçbir şey söylemediği için burada.

Şimdi buna kaydolmak için bileşenimin bunu yapması gerekiyor (örneğin Index.razor’da):

@page "https://remibou.github.io/"
@using RemiBou.BlogPost.SignalR.Shared
@using MediatR
@implements INotificationHandler<CounterIncremented>
@implements IDisposable

<h1>Hello, world!</h1>

Welcome to your new app.

Current count : @count
@code {
    private int count;
    protected override void OnInitialized()
    {
        DynamicNotificationHandlers.Register(this);
    }
    public async Task Handle(CounterIncremented notification, System.Threading.CancellationToken cancellationToken)
    {
        count = notification.Counter;
        StateHasChanged();
    }

    public void Dispose()
    {
        DynamicNotificationHandlers.Unregister(this);

    }
}
  • Kullanıcı arayüzünü güncellemek için bildirimi dinler
  • Bileşen yok edildiğinde dinlemeyi bırakır: UNUTMAYIN yoksa bileşeniniz sonsuza kadar yaşayacak
  • Bu, bir BaseComponent olarak uygulanabilir

Şimdi, SignalR’nin tüm bunlara son kablolaması, müşterinizin Program.cs dosyasında:

var app = builder.Build();
var navigationManager = app.Services.GetRequiredService<NavigationManager>();
var hubConnection = new HubConnectionBuilder()
            .WithUrl(navigationManager.ToAbsoluteUri("/notifications"))
            .AddJsonProtocol(o => o.PayloadSerializerOptions.Converters.Add(new NotificationJsonConverter()))
            .Build();

hubConnection.On<SerializedNotification>("Notification", async (notificationJson) =>
{
    await DynamicNotificationHandlers.Publish(notificationJson);
});

await hubConnection.StartAsync();
await app.RunAsync();
  • Uygulamanız için hizmet koleksiyonunu elde etmek için şablondan Build ve RunAsync çağrısını ayırmanız gerekir.

Ve işte! Artık bash betiğini başlattığınızda, kullanıcı arayüzü otomatik olarak güncellenir. Tabii ki ilk durumu görüntülemek istiyorsanız, bunu sağlayan bir API oluşturmanız gerekir.

Çözüm

Blazor’un en büyük satış noktasını bir kez daha gördük: Frontend ve backend için aynı toolbelt’i kullanabiliyorum (SignalR, MediatR, Json dönüştürücüler…) ve çok iyi hissettiriyor 🙂 Artık tek satır olmadan gerçek zamanlı bir uygulama oluşturabilirsiniz. javascript’in.

Bu blog gönderisinin tüm kaynak kodu burada mevcuttur: [(https://github.com/RemiBou/remibou.github.io/tree/master/projects/RemiBou.BlogPost.SignalR]((https://github.com/RemiBou/remibou.github.io/tree/master/projects/RemiBou.BlogPost.SignalR)

Bir cevap yazın

Ready to Grow Your Business?

We Serve our Clients’ Best Interests with the Best Marketing Solutions. Find out More

How Can We Help You?

Need to bounce off ideas for an upcoming project or digital campaign? Looking to transform your business with the implementation of full potential digital marketing?

For any career inquiries, please visit our careers page here.
[contact-form-7 404 "Bulunamadı"]