Фильтры хабов представляют специальные компоненты, которые выполняют до и после выполнения хабов. В этом плане фильтры хабов напоминают стандартные компоненты middleware, которые встраиваются в конвейер обработки запроса в ASP.NET Core. Фильтры хабов могут быть полезны, если необходимо перед и/или после выполнения хаба определить некоторую программную логику.
Допустим у нас есть следующий хаб ChatHub:
using Microsoft.AspNetCore.SignalR; namespace SignalRApp { public class ChatHub : Hub { public async Task Send(string username, string message) { await this.Clients.All.SendAsync("Receive", username, message); } } }
Определим для него фильтр.
Функционально фильтры представляют классы, которые реализуют интерфейс IHubFilter. Данный интерфейс определяет три метода:
public interface IHubFilter { ValueTask<object?> InvokeMethodAsync(HubInvocationContext invocationContext, Func<HubInvocationContext, ValueTask<object?>> next); Task OnConnectedAsync(HubLifetimeContext context, Func<HubLifetimeContext, Task> next); Task OnDisconnectedAsync(HubLifetimeContext context, Exception? exception, Func<HubLifetimeContext, Exception?, Task> next); }
Этот интерфейс определяет три метода:
InvokeMethodAsync: вызывается при каждом обращении к хабу. Первый параметр - объект HubInvocationContext
представляет контекст
вызова хаба и хранит всю связанную информацию в виде свойств:
Context
: контекст хаба - объект HubCallerContext
Hub
: вызываемый хаб в виде объекта класса Hub
HubMethod
: вызываемый метод хаба - объект MethodName
HubMethodName
: название вызываемого метода хаба
HubMethodArguments
: аругменты вызываемого метода хаба - объект IReadOnlyList<object?>
ServiceProvider
: провайдер сервисов - объект IServiceProvider
Второй параметр - next
представляет следующий в конвейере фильтр хаба. Если в конвейере больше нет фильтров, то представляет вызов метода хаба.
Метод возвращает результат работы метода хаба.
OnConnectedAsync: вызывается при обращении к методу OnConnectedAsync
хаба. Первый параметр - объект HubLifetimeContext
представляет контекст
вызова метода OnConnectedAsync и имеет следующие свойства:
Context
: контекст хаба - объект HubCallerContext
Hub
: вызываемый хаб в виде объекта класса Hub
ServiceProvider
: провайдер сервисов - объект IServiceProvider
Второй параметр - next
представляет следующий в конвейере фильтр хаба. Если в конвейере больше нет фильтров, то представляет вызов метода хаба.
OnDisconnectedAsync: вызывается при обращении к методу OnDisconnectedAsync
хаба. Первый параметр - объект HubInvocationContext
представляет контекст
вызова метода OnDisconnectedAsync
Второй параметр представляет возможную ошибку, которая может произойти при отключении.
Третий параметр - next
представляет следующий в конвейере фильтр хаба. Если в конвейере больше нет фильтров, то представляет вызов метода хаба.
Например, определим следующий фильтр хаба:
using Microsoft.AspNetCore.SignalR; namespace SignalRApp { public class MyHubFilter : IHubFilter { public async ValueTask<object?> InvokeMethodAsync( HubInvocationContext invocationContext, Func<HubInvocationContext, ValueTask<object?>> next) { // получаем вызываемый метод хаба Console.WriteLine($"Вызов метода {invocationContext.HubMethodName}"); try { return await next(invocationContext); // вызываем следующий фильтр или метод хаба } catch (Exception ex) { Console.WriteLine($"Не удалось вызвать метод {invocationContext.HubMethodName}: {ex.Message}"); throw; } } } }
В данном случае просто логгируем на консоль имя метода хаба, который обрабатывает запрос. После логгирования вызываем метод хаба (так как у нас больше нет фильтров хаба)
await next(invocationContext);
Результат вызова метода хаба собственно и будет представлять возвращаемый результат.
Чтобы задействовать фильтр, его надо зарегистрировать. Для регистрации фильтра изменим код файла Program.cs следующим образом:
using Microsoft.AspNetCore.SignalR; using SignalRApp; // пространство имен класса ChatHub var builder = WebApplication.CreateBuilder(args); // регистрация фильтра builder.Services.AddSignalR(options => options.AddFilter<MyHubFilter>()); var app = builder.Build(); app.UseDefaultFiles(); app.UseStaticFiles(); app.MapHub<ChatHub>("/chat"); app.Run();
Для регистрации фильтра применяется перегрузка метода AddSignalR()
, который в качестве параметра принимает делегат. Параметр этого делегата - объект HubOptions
с помощью метода AddFilter() добавляет фильтр в качестве сервиса. Для добавления фильтра метод AddFilter типизируется типом фильтра.
Для тестирования этого хаба в проекте в папке wwwroot следующую страницу index.html:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>Metanit.com</title> </head> <body> <div> Введите логин:<br /> <input id="userName" type="text" /><br /><br /> Введите сообщение:<br /> <input type="text" id="message" /><br /><br /> <input type="button" id="sendBtn" value="Отправить" disabled="disabled" /> </div> <div id="chatroom"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.js"></script> <script> const hubConnection = new signalR.HubConnectionBuilder() .withUrl("/chat") .build(); document.getElementById("sendBtn").addEventListener("click", function () { const userName = document.getElementById("userName").value; // получаем введенное имя const message = document.getElementById("message").value; hubConnection.invoke("Send", userName, message) // отправка данных серверу .catch(function (err) { return console.error(err.toString()); }); }); // получение данных с сервера hubConnection.on("Receive", function (userName, message) { // создаем элемент <b> для имени пользователя const userNameElem = document.createElement("b"); userNameElem.textContent = `${userName}: `; // создает элемент <p> для сообщения пользователя const elem = document.createElement("p"); elem.appendChild(userNameElem); elem.appendChild(document.createTextNode(message)); // добавляем новый элемент в самое начало // для этого сначала получаем первый элемент const firstElem = document.getElementById("chatroom").firstChild; document.getElementById("chatroom").insertBefore(elem, firstElem); }); hubConnection.start() .then(function () { document.getElementById("sendBtn").disabled = false; }) .catch(function (err) { return console.error(err.toString()); }); </script> </body> </html>
И при обращении к хабу сначала сработает метод InvokeMethodAsync() в фильтре хаба, который выведет на консоль уведомление:
Подобным образом можно реализовать и другие методы фильтра:
using Microsoft.AspNetCore.SignalR; namespace SignalRApp { public class MyHubFilter : IHubFilter { public async ValueTask<object?> InvokeMethodAsync( HubInvocationContext invocationContext, Func<HubInvocationContext, ValueTask<object?>> next) { // получаем вызываемый метод хаба Console.WriteLine($"Вызов метода {invocationContext.HubMethodName}"); return await next(invocationContext); // вызываем следующий фильтр или метод хаба } public Task OnConnectedAsync(HubLifetimeContext context, Func<HubLifetimeContext, Task> next) { Console.WriteLine("Вызов метода OnConnectedAsync"); return next(context); } public Task OnDisconnectedAsync( HubLifetimeContext context, Exception? exception, Func<HubLifetimeContext, Exception, Task> next) { Console.WriteLine("Вызов метода OnDisconnectedAsync"); return next(context, exception!); } } }
Благодаря объекту HubInvocationContext, который передается в метод InvokeMethodAsync() в фильтре хаба, мы можем проинспектировать передаваемые в метод хаба значения и при необходимости изменить их. Например, нам надо исключить отправку определенных слов:
using Microsoft.AspNetCore.SignalR; namespace SignalRApp { public class MyHubFilter : IHubFilter { public async ValueTask<object?> InvokeMethodAsync( HubInvocationContext invocationContext, Func<HubInvocationContext, ValueTask<object?>> next) { // получаем второй параметр метода хаба в переменную message if (invocationContext.HubMethodArguments.Count == 2 && invocationContext.HubMethodArguments[1] is string message) { // заменяем некоторые слова message = message.Replace("йух", "***"); var arguments = invocationContext.HubMethodArguments.ToArray(); arguments[1] = message; // пересоздаем объект HubInvocationContext invocationContext = new HubInvocationContext(invocationContext.Context, invocationContext.ServiceProvider, invocationContext.Hub, invocationContext.HubMethod, arguments); } // передаем этот объект в вызов последующих фильтров или метода хаба return await next(invocationContext); } } }
Так, в методе Send хаба ChatHub второй параметр представлял некоторое сообщение. В данном случае мы получаем это сообщение, заменяем в нем подстроку "йух" на звездочки. Пересоздаем объект HubInvocationContext, который принимает измененные значения параметров, и передаем его в вызов метода хаба.
Чтобы задействовать фильтр, его надо зарегистрировать. Это можно сделать глобально - для всех хабов, либо локально - для определенного хаба. Так, в примере выше применялась глобальная регистрациия. Но можно также применить локальную регистрацию - для определенного класса хабов или сочетать оба вида фильтров:
using Microsoft.AspNetCore.SignalR; using SignalRApp; // пространство имен класса ChatHub var builder = WebApplication.CreateBuilder(args); builder.Services .AddSignalR(options => options.AddFilter<MyHubFilter>()) // глобальная регистрация фильтра .AddHubOptions<ChatHub>(options => options.AddFilter<MyHubFilter>()); // локальная регистрация фильтра var app = builder.Build(); app.UseDefaultFiles(); app.UseStaticFiles(); app.MapHub<ChatHub>("/chat"); app.Run();
Для глобальной установки фильтра применяется перегрузка метода AddSignalR()
.
Для локальной установки фильтра применяется метод AddHubOptions()
, который типизируется типом хаба, к которому будут применться настройки. И также в качестве
параметра метод принимает делегат, в котором через параметр типа HubOptions и его метода AddFilter() добавляется тип фильтра.
Следует учитывать, что сначала выполняются глобальные фильтры, а потом локальные.
Кроме типизации метода AddFilter можно напрямую передать в этот метод объект фильтра:
builder.Services .AddSignalR(options => options.AddFilter(new MyHubFilter())) // глобальная регистрация фильтра .AddHubOptions<ChatHub>(options => options.AddFilter(new MyHubFilter())); // локальная регистрация фильтра
Фильтры внедряются в виде сервисов в приложение. Однако в зависимости от типа регистрации сервис будет иметь определенный тип жизненного цикла. Например, при регистрации в виде типизации метода AddFilter при каждом отдельном обращении к хабу будет создаваться новый объект фильтра:
builder.Services .AddSignalR(options => options.AddFilter<MyHubFilter>()) // глобальная регистрация фильтра .AddHubOptions<ChatHub>(options => options.AddFilter<MyHubFilter>()); // локальная регистрация фильтра
Так, в этом примере регистрируется два фильтра одного типа. И при обращении к хабу будет создавать два разных объекта этого фильтра. При новом обращении к хабу опять будет создаваться два новых объекта фильтров. То есть это будет transient-сервис.
Если же в метод AddFilter передается объект фильтра, то будет создваться один фильтр, который будет существовать на протяжении всей жизни приложения, то есть это будет signleton-сервис:
builder.Services .AddSignalR(options => options.AddFilter(new MyHubFilter())) // глобальная регистрация фильтра .AddHubOptions<ChatHub>(options => options.AddFilter(new MyHubFilter())); // локальная регистрация фильтра
Так, в этом примере регистрируется два фильтра одного типа. И при обращении к хабу будет создавать два разных объекта этого фильтра, два singleton-объекта. Но при новом обращении к хабу будут использоваться ранее созданные объекты фильтров.