Фильтры хабов

Последнее обновление: 09.08.2022

Фильтры хабов представляют специальные компоненты, которые выполняют до и после выполнения хабов. В этом плане фильтры хабов напоминают стандартные компоненты 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() в фильтре хаба, который выведет на консоль уведомление:

Фильтры хабов в SignalR в приложении на ASP.NET Core и C#

Подобным образом можно реализовать и другие методы фильтра:

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, который принимает измененные значения параметров, и передаем его в вызов метода хаба.

Фильтры и методы хабов в SignalR в приложении на ASP.NET Core и C#

Регистрация глобальных и локальных фильтров

Чтобы задействовать фильтр, его надо зарегистрировать. Это можно сделать глобально - для всех хабов, либо локально - для определенного хаба. Так, в примере выше применялась глобальная регистрациия. Но можно также применить локальную регистрацию - для определенного класса хабов или сочетать оба вида фильтров:

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-объекта. Но при новом обращении к хабу будут использоваться ранее созданные объекты фильтров.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850