SignalR .Net 7 Можно ли абстрагировать концентратор, чтобы использовать его на многих страницах Blazor?

У меня есть хаб, который отлично работает. На данный момент есть 3 события:

  • ReceiveMessage (взято непосредственно из примеров приложения чата)
  • RefreshPage (используется, когда одна страница делает недействительной другую страницу)
  • NotificationsUpdated (значок с цифрой количества уведомлений, которые у вас есть)

Прямо сейчас страница чата работает, как и ожидалось, и я перехватываю каждое из трех событий на ней, чтобы доказать мне, что все они работают (и они работают). Однако я знаю, что некоторым страницам сайта не нужно будет подключаться к этому. концентратор, в то время как другие будут подключаться и должны обрабатывать только 1 или 2 события. Скорее всего, в конце концов я удалю событие ReceiveMessage чата.

На данный момент я использую HubConnectionBuilder вот так:

protected override async Task OnInitializedAsync()
{
  hubConnection = new HubConnectionBuilder()
    .WithUrl(NavigationManager.ToAbsoluteUri("/hub/ping"), options =>
    {
      options.AccessTokenProvider = async () => await GetAccessTokenValueAsync();
    })
    .WithAutomaticReconnect()
    .Build();

  hubRegistrations.Add(hubConnection.OnMessageReceived(OnMessageReceivedAsync));
  hubRegistrations.Add(hubConnection.OnRefreshPage(OnRefreshPageAsync));
  hubRegistrations.Add(hubConnection.OnNotificationsUpdate(OnNotificationsUpdateAsync));

  await hubConnection.StartAsync();
}

И как пример одного из событий:

public async Task OnMessageReceivedAsync(string user, string message) =>
  await InvokeAsync(() =>
  {
    var encodedMessage = $"{user}: {message}";
    Messages.Add(encodedMessage);
    StateHasChanged();
  });

И много дублированного кода, если мне нужно реализовать это на другой странице (конечно, если мне не нужно реализовывать все 3 события, меньше, но не намного меньше.)

[Inject]
public required NavigationManager NavigationManager { get; set; }

[Inject]
public required IAccessTokenProvider TokenProvider { get; set; }

[Inject]
public required StateManager State { get; set; }

private HubConnection hubConnection;
private readonly HashSet<IDisposable> hubRegistrations = new();

public async ValueTask DisposeAsync()
{
  await PHST.DisposeAsync();
  if (hubRegistrations is { Count: > 0 })
  {
    foreach (var disposable in hubRegistrations)
      disposable.Dispose();
  }

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

ВОПРОС

Итак, как кодер, я хочу сделать это максимально простым для всех будущих кодеров и страниц. Я хотел бы сделать это сервисом или синглтоном или чем-то еще. Это то, что я «могу» сделать, или есть лучший способ иметь один концентратор на любом количестве страниц?

Я играл с созданием базового класса, но наследование было бы немного болезненным на других страницах, где уже есть наследование. Я только немного попробовал услугу, которую я могу внедрить, но хотел остановиться и задать вопрос, прежде чем спуститься в кроличью нору.

Я также хотел бы сделать его довольно динамичным, чтобы события могли делать то, что они хотят, с данными. На данный момент каждый из них является динамическим с использованием нескольких методов расширения.

public static IDisposable OnMessageReceived(this HubConnection connection, Func<string, string, Task> handler) =>
  connection.On("ReceiveMessage", handler);

public static IDisposable OnRefreshPage(this HubConnection connection, Func<string, Task> handler) =>
  connection.On("RefreshPage", handler);

public static IDisposable OnNotificationsUpdate(this HubConnection connection, Func<int, Task> handler) =>
  connection.On("NotificationsUpdate", handler);
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
65
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Увидев, что мне не помогли, я продолжил движение вперед. Я не уверен, что сделал это идеально, но мое требование меньшего количества избыточности на каждой странице соответствует этому! Я даже хотел, чтобы имена групп были максимально защищены от идиотов, и я реализовал для этого интерфейс (прокомментировано ниже — см. методы HubGroupXXX и класс HubGroupAdders).

Это будет полный обзор всего, а не только абстракция во вспомогательный класс, на случай, если это кому-то поможет в будущем.

Начнем со стороны помощника.

Сторона клиента

Программа.cs

builder.Services.AddScoped<IPingActionsHubHelper, PingActionsHubHelper>();

IPingActionsHubHelper.cs

public interface IPingActionsHubHelper
{
  /// <summary>
  /// Use this method to do all of the heavy lifting to connect to the hub. It should be run within the OnInitializedAsync method.
  /// It should be followed up by at least one of the AddOnXXXEvents and locally handled.
  /// </summary>
  /// <param name = "groupNames">Chances are your page would need to let the hub know what groups to be in, so they may be signaled later.</param>
  /// <returns></returns>
  Task HubSetup(List<IHubGroupAdder> groupNames = null);

  /// <summary>
  /// After the HubSetup and each of the AddOnXXXEvents, call this to start the hub.
  /// </summary>
  /// <returns></returns>
  Task HubStart();

  HubConnection HubConnection { get; }

  ValueTask DisposeAsync();

  void AddOnReceiveMessageEvent(Action<string, string> action);
  void AddOnRefreshPageEvent(Action<SignalRRefreshTypes> action);
  void AddOnNotificationsUpdateEvent(Action<int, bool> action);
}

PingActionsHelper.cs

public class PingActionsHubHelper : IPingActionsHubHelper
{
  private readonly NavigationManager NavigationManager;
  private readonly IAccessTokenProvider TokenProvider;
  private readonly HashSet<IDisposable> HubRegistrations = new();

  public HubConnection HubConnection { get; private set; }

  public PingActionsHubHelper(IAccessTokenProvider tokenProvider, NavigationManager navigationManager)
  {
    NavigationManager = navigationManager;
    TokenProvider = tokenProvider;
  }

  public async Task HubSetup(List<IHubGroupAdder> groupNames = null) // Run this within the OnInitializedAsync event
  {
    var groupNamesParsed = string.Empty;

    if (groupNames is not null && groupNames.Any())
    {
      groupNamesParsed = string.Join("|", groupNames.Select(x => x.ToString()));
    }

    HubConnection = new HubConnectionBuilder()
      .WithUrl(NavigationManager.ToAbsoluteUri($"/hub/ping?{(string.IsNullOrWhiteSpace(groupNamesParsed) ? "" : groupNamesParsed)}"), options =>
      {
        options.AccessTokenProvider = async () => await GetAccessTokenValueAsync();
      })
    .WithAutomaticReconnect()
    .Build();
  }

  public async Task HubStart()
  {
    await HubConnection.StartAsync();
  }

  #region Events to consume

  public void AddOnReceiveMessageEvent(Action<string, string> action)
  {
    HubRegistrations.Add(HubConnection.On(Core.Constants.PingActionsHub.MethodNames.ReceiveMessage, action));
  }

  public void AddOnRefreshPageEvent(Action<SignalRRefreshTypes> action)
  {
    HubRegistrations.Add(HubConnection.On(Core.Constants.PingActionsHub.MethodNames.RefreshPage, action));
  }

  public void AddOnNotificationsUpdateEvent(Action<int, bool> action)
  {
    HubRegistrations.Add(HubConnection.On(Core.Constants.PingActionsHub.MethodNames.NotificationsUpdate, action));
  }

  #endregion Events to consume

  private async ValueTask<string> GetAccessTokenValueAsync()
  {
    var result = await TokenProvider.RequestAccessToken();
    return result.TryGetToken(out var accessToken) ? accessToken.Value : null;
  }

  public async ValueTask DisposeAsync()
  {
    if (HubRegistrations is { Count: > 0 })
    {
      foreach (var disposable in HubRegistrations)
        disposable.Dispose();
    }

    if (HubConnection is not null)
    {
      await HubConnection.DisposeAsync();
    }
  }
}

Некоторая клиентская страница, где вам нужен концентратор

[Inject]
public IPingActionsHubHelper HubHelper { get; set; }

public bool IsConnected => HubHelper.HubConnection?.State == HubConnectionState.Connected;

protected override async Task OnInitializedAsync()
{
  // Here is an example of adding group(s) to whatever page you are on that is logical
  // You should be adding at least one group so it can be signaled
  // By default, any instance of the PingActionsHub (using the HubHelper) will have the User's Id group
  await HubHelper.HubSetup(new List<IHubGroupAdder>()
  {
    new HubGroupProject(333333),
    new HubGroupPage(Core.SignalRPageNames.FGV, 444444),
    new HubGroupFormulation(999999),
  });

  // This event will most likely never be used, it is really just for the chat page
  HubHelper.AddOnReceiveMessageEvent((string user, string message) =>
  {
    var encodedMessage = $"{user}: {message}";
    Messages.Add(encodedMessage);
    StateHasChanged();
  });

  // Implement this on your page when another page could alater the Project or Formulation and should refresh the page where the hub is instantiated
  HubHelper.AddOnRefreshPageEvent((SignalRRefreshTypes refreshType) =>
  {
    System.Console.WriteLine($"We have caught a Page Refresh of type: {refreshType}");
    // This is where the page would refresh or LoadData or something based perhaps on the refreshType
  });

  // This should really only need to be implemented on the Navbar component (which it is), unless some other location should show
  // how many notifications a user has
  HubHelper.AddOnNotificationsUpdateEvent((int totalNotifications, bool inError) =>
  {
    State.UsersTotalNotifications.SetState(totalNotifications);
    StateHasChanged();
  });

  await HubHelper.HubStart();
}

У меня есть комментарии в коде выше. Но это всего 3 шага:

  1. Вызовите метод HubSetup и передайте любые группы, которые вы хотите поймать
  2. Настройте любые события HubHelper.AddOnXXXEvent для прослушивания и что с ними делать
  3. Вызов метода HubStart

Это из абстракции. Остальной код ниже мой полная реализация, где у меня есть клиентская служба, которая вызывает Контроллер на стороне сервера, который позволяет вызывать концентратор из любого места.


В следующем коде показано, как взаимодействовать со страницей, на которой есть кнопка «Отправить» в этом примере страницы Chat.razor. Он вызывает локальную службу, которая вызывает контроллер серверов, который, в свою очередь, вызывает метод концентраторов.

Клиент.razor.cs

[Inject]
public IPingActionsService PingActionsService { get; set; }

public async Task SendMessage()
{
  await PingActionsService.SendMessage(new PingSendMessageAction
  {
    ToAll = true,
    Message = Message,
    SentFrom = UsersName,
  });

  Message = string.Empty;
  await MessageRef.FocusAsync();
}

IPingActionsService.cs

public interface IPingActionsService
{
  Task<bool> SendMessage(PingSendMessageAction notificationPing);
  Task<bool> SendRefresh(PingSendRefreshAction notificationPing);
  Task<bool> UsersNotifications(PingUsersNotificationsAction notificationPing);
}

Следующий файл просто реализует описанный выше интерфейс для простого вызова контроллера на стороне сервера.

PingActionsService.cs

// Client - PingActionsService.cs
public class PingActionsService : IPingActionsService
{
  private const string BaseUri = "api/ping-actions";
  private readonly IHttpService HttpService;

  public PingActionsService(IHttpService httpService)
  {
    HttpService = httpService;
  }

  public async Task<bool> SendMessage(PingSendMessageAction notificationPing)
  {
    return await HttpService.PostAsJsonAsync<PingSendMessageAction, bool>($"{BaseUri}/send-message", notificationPing);
  }

  public async Task<bool> SendRefresh(PingSendRefreshAction notificationPing)
  {
    return await HttpService.PostAsJsonAsync<PingSendRefreshAction, bool>($"{BaseUri}/refresh-page", notificationPing);
  }

  public async Task<bool> UsersNotifications(PingUsersNotificationsAction notificationPing)
  {
    return await HttpService.PostAsJsonAsync<PingUsersNotificationsAction, bool>($"{BaseUri}/users-notifications", notificationPing);
  }
}

Сторона сервера

IPingActionsService.cs

public interface IPingActionsService
{
  // This is the generic call to determine which one of the more specific calls to make
  Task<bool> HandlePing(PingActionBase ping);

  // These are the calls that can be used within any other service to pass to the UI
  Task<bool> SendMessageToGroups(PingSendMessageAction ping);
  Task<bool> SendRefreshSignalToGroups(PingSendRefreshAction ping);
  Task<bool> UpdateUsersNotifications(PingUsersNotificationsAction ping);
}

PingActionsController.cs

[ApiController]
[Route("api/ping-actions")]
public class PingActionsController : ControllerBase
{
  private readonly IPingActionsService PingActionsService;

  public PingActionsController(IPingActionsService pingActionsService)
  {
    PingActionsService = pingActionsService;
  }

  [HttpPost]
  [Route("ping")]
  public async Task<bool> Ping(PingActionBase notificationPing)
  {
    return await PingActionsService.HandlePing(notificationPing);
  }

  [HttpPost]
  [Route("send-message")]
  public async Task<bool> SendMessage(PingSendMessageAction notificationPing)
  {
    return await PingActionsService.HandlePing(notificationPing);
  }

  [HttpPost]
  [Route("refresh-page")]
  public async Task<bool> SendRefresh(PingSendRefreshAction notificationPing)
  {
    return await PingActionsService.HandlePing(notificationPing);
  }

  [HttpPost]
  [Route("users-notifications")]
  public async Task<bool> UsersNotifications(PingUsersNotificationsAction notificationPing)
  {
    return await PingActionsService.HandlePing(notificationPing);
  }
}

PingActionsService.cs

public class PingActionsService : IPingActionsService
{
  private readonly IHubContext<PingActionsHub, IPingActionsHub> Context;
  private readonly IProjectUsersService ProjectUsersService;

  public PingActionsService(IHubContext<PingActionsHub, IPingActionsHub> context, IProjectUsersService projectUsersService)
  {
    Context = context;
    ProjectUsersService = projectUsersService;
  }

  public async Task<bool> HandlePing(PingActionBase ping)
  {
    bool result;

    if (ping is PingSendMessageAction sendAction)
      result = await SendMessageToGroups(sendAction);
    else if (ping is PingSendRefreshAction refreshAction)
      result = await SendRefreshSignalToGroups(refreshAction);
    else if (ping is PingUsersNotificationsAction notificationAction)
      result = await UpdateUsersNotifications(notificationAction);
    else
      throw new ArgumentOutOfRangeException("PingType selection not valid");

    return result;
  }

  public async Task<bool> SendMessageToGroups(PingSendMessageAction ping)
  {
    if (ping.ToAll)
    {
      await Context.Clients.All.ReceiveMessage(ping.SentFrom, ping.Message);
    }
    else
    {
      var toGroups = ParseGroups(ping);

      foreach (var group in toGroups)
        await Context.Clients.Group(group).ReceiveMessage(ping.SentFrom, ping.Message);
    }

    return true;
  }

  public async Task<bool> SendRefreshSignalToGroups(PingSendRefreshAction ping)
  {
    var toGroups = ParseGroups(ping);

    foreach (var group in toGroups)
      await Context.Clients.Group(group).RefreshPage(ping.RefreshType);

    return true;
  }

  public async Task<bool> UpdateUsersNotifications(PingUsersNotificationsAction ping)
  {
    if ((!ping.UserId.HasValue || ping.UserId.Value <= 0) && (!ping.ProjectId.HasValue || ping.ProjectId <= 0))
      throw new ArgumentOutOfRangeException($"You must specify a UserId or ProjectId when sending an UpdateUsersNotification");

    var groupNames = new List<string>();

    if (ping.UserId.HasValue)
      groupNames.Add($"{Core.Constants.PingActionsHub.GroupNames.User}:{ping.UserId}");

    if (ping.ProjectId.HasValue)
    {
      var projectUsers = await ProjectUsersService.GetProjectUsersAsync(ping.ProjectId.Value);

      foreach (var projectUser in projectUsers)
        groupNames.Add($"{Core.Constants.PingActionsHub.GroupNames.User}:{projectUser.UserId}");
    }

    foreach (var group in groupNames)
      await Context.Clients.Group(group).NotificationsUpdate(ping.TotalNotificadtions, ping.InError);

    return true;
  }

  private static HashSet<string> ParseGroups(PingActionReceivers ping)
  {
    var result = new HashSet<string>();

    foreach (var projectId in ping.ProjectsToSignal.Where(x => x > 0))
      result.Add($"{Core.Constants.PingActionsHub.GroupNames.Project}:{projectId}");

    foreach (var formulationId in ping.FormulationsToSignal.Where(x => x > 0))
      result.Add($"{Core.Constants.PingActionsHub.GroupNames.Formulation}:{formulationId}");

    foreach (var userId in ping.UsersToSignal.Where(x => x > 0))
      result.Add($"{Core.Constants.PingActionsHub.GroupNames.User}:{userId}");

    foreach (var page in ping.PagesToSignal)
      result.Add(page.ToString());

    return result;
  }
}

Ядро — элементы, используемые как клиентским, так и серверным кодом.

HubGroupAdders.cs

public interface IHubGroupAdder
{
  abstract string ToString();
}

public record HubGroupProject : IHubGroupAdder
{
  public int ProjectId { get; init; }

  public HubGroupProject(int projectId) { ProjectId = projectId; }

  public override string ToString() => $"{Core.Constants.PingActionsHub.GroupNames.Project}:{ProjectId}";
}

public record HubGroupFormulation : IHubGroupAdder
{
  public int Formulation { get; init; }

  public HubGroupFormulation(int formulationId) { Formulation = formulationId; }

  public override string ToString() => $"{Core.Constants.PingActionsHub.GroupNames.Formulation}:{Formulation}";
}

public record HubGroupPage : IHubGroupAdder
{
  public SignalRPageNames Page { get; init; }
  public int ProjectId { get; init; }

  public HubGroupPage(SignalRPageNames page, int projectId)
  {
    Page = page;
    ProjectId = projectId;
  }

  public override string ToString() => $"{Page}:{ProjectId}";
}

Константы.cs

public readonly struct PingActionsHub
{
  public readonly struct GroupNames
  {
    public static readonly string Project = "ProjectId";
    public static readonly string Formulation = "FormulationId";
    public static readonly string User = "UserId";
  }

  public readonly struct MethodNames
  {
    public static readonly string ReceiveMessage = "ReceiveMessage";
    public static readonly string RefreshPage = "RefreshPage";
    public static readonly string NotificationsUpdate = "NotificationsUpdate";
  }
}

Enumerations.cs

public enum SignalRPingTypes
{
  Unknown = 0,
  SendMessage = 1,
  Refresh = 2,
  UserNotifications = 3,
}

public enum SignalRPageNames
{
  FGV = 1,
  PAndC,
  PerfTests,
  PM,
}

public enum SignalRRefreshTypes
{
  RefreshOnly = 0,
  SomethingElse = 1, // I think in the future I will need refreshes that only update 1 thing or possibly get a dataset to be used programmatically
}

Другие вопросы по теме