How ASP.NET Core SignalR works

Completed

Servers and the Hub class

The Hub class is a SignalR server concept. It's defined within the Microsoft.AspNetCore.SignalR namespace and is part of the Microsoft.AspNetCore.SignalR NuGet package. ASP.NET Core web apps that target the Microsoft.NET.Sdk.Web SDK don't need to add a package reference for SignalR, because it's already available as part of the shared framework.

A Hub is exposed through a route. For example, the https://www.contoso-pizza.com/hubs/orders route could be used to represent an OrdersHub implementation. Through the various hub APIs, authors can define methods and events.

There are two modalities to expose methods on a hub. You create a subclass of the following types and write methods:

  • Hub: A standard hub.
  • Hub<T>: A strongly typed generic hub.

Example Hub

As a point of reference, consider the following Notification object:

namespace RealTime.Models;

public record Notification(string Text, DateTime Date);

The object can be shared when you use the .NET client SDK, so that the server and client have exactly the same object. Imagine a notification hub:

using Microsoft.AspNetCore.SignalR;
using System;
using System.Threading.Tasks;
using RealTime.Models;

namespace ExampleServer.Hubs;

public sealed class NotificationHub : Hub
{
    public Task NotifyAll(Notification notification) =>
        Clients.All.SendAsync("NotificationReceived", notification);
}

Regarding the difference between methods and events, the method in the preceding hub implementation is NotifyAll, and the event is NotificationReceived. NotificationHub is a subclass of Hub. The NotifyAll method returns a Task, and it accepts a single Notification parameter. The method is expressed as the invocation to SendAsync from Clients.All, which represents all connected clients. The NotificationReceived event is fired, depending on the notification instance.

The IHubContext instance

You fire events from either a Hub or an IHubContext instance. The SignalR hub is the core abstraction for sending messages to clients that are connected to the SignalR server. It's also possible to send messages from other places in your app by using either of the following types:

  • IHubContext<THub>: A context where THub represents a standard hub.
  • IHubContext<THub,T>: A context where THub represents a strongly typed generic hub, and T represents the corresponding type of client.

Important

IHubContext is for sending notifications to clients. It is not used to call methods on the Hub.

An example IHubContext

Considering the previous notification hub implementation, you could use IHubContext<NotificationHub> as follows:

using Microsoft.AspNetCore.SignalR;
using System;
using System.Threading.Tasks;
using RealTime.Models;

namespace ExampleServer.Services;

public sealed class NotificationService(
    IHubContext<NotificationHub> hubContext)
{
    public Task SendNotificationAsync(Notification notification) =>
        notification is not null
            ? hubContext.Clients.All.SendAsync("NotificationReceived", notification)
            : Task.CompletedTask;
}

The preceding C# code relies on IHubContext<NotificationHub> to access the contextual listing of clients, exposing the ability to broadcast notifications. The hubContext primary constructor parameter that's captured in scope is used to fire the "NotificationReceived" event, but it isn't intended to be used to call the hub's NotifyAll method.

Methods

Hub or Hub<T> methods are just like any other C# method. They define a return type, method name, and parameters.

  • The most common return type for a hub method is Task or Task<TResult>, which represents the asynchronous hub operation.
  • The method name is used to call the method from clients. You can customize it by using HubMethodNameAttribute.
  • Parameters are optional, but when they're defined, clients are expected to provide corresponding arguments.

Methods aren't required to fire events, but they often do.

Events

An event can be subscribed to by name from a client. The server is responsible for raising events. Hub, Hub<T>, IHubContext<THub>, and IHubContext<THub, T> events are named and can define up to 10 parameters. Events are fired on the server and handled by interested clients. A client is considered interested when it subscribes to events on its hub's connection. Clients can indirectly trigger events when they call hub methods that fire events as a result of their invocation. Events can't be directly triggered by clients, though, because that's the responsibility of the server.

Event client scopes

You call events from an IClientProxy instance. You implement the IHubClients and IHubCallerClients interfaces from the Clients type. There are many ways to scope to a specific IClientProxy instance. You can target the following scopes from the Hub.Clients property:

Member Details
All All connected clients (such as a broadcast).
AllExcept All connected clients, excluding the specified connections (such as a filtered broadcast).
Caller The connected client that triggered the method (such as an echo).
Client The specified client connection (single connection).
Clients The specified client connections (multiple connections).
Group All connected clients within the specified group.
GroupExcept All connected clients within the specified group, excluding the specified connections.
Groups All connected clients within the specified groups (multiple groups).
Others All connected clients, excluding the client that triggered the method.
OthersInGroup All connected clients within the specified group, excluding the client that triggered the method.
User All connected clients for the specified user (a single user can connect on more than one device).
Users All connected clients for the specified users.

Example scopes

Consider the following images, which can help you visualize how the hub sends messages to targeted clients. You can expand the images for improved readability.

  • Broadcast to all

    ASP.NET Core SignalR hub sending message with Clients.All syntax.

    All connected clients receive this message, regardless of the group that they might or might not belong to.

  • Isolated user

    ASP.NET Core SignalR hub sending message with Clients.User syntax.

    A single user receives this message, regardless of how many devices they're currently using.

  • Isolated group

    ASP.NET Core SignalR hub sending message with Clients.Group syntax.

    Only clients that belong to a certain group receive this message.

Clients and the HubConnection class

The HubConnection class is a SignalR client concept, which represents the client's connection to the server Hub. It's defined within the Microsoft.AspNetCore.SignalR.Client namespace, and it's part of the Microsoft.AspNetCore.SignalR.Client NuGet package.

You create a HubConnection by using the builder pattern and the corresponding HubConnectionBuilder type. Given the hub's route (or System.Uri), you can create a HubConnection. The builder can also specify additional configuration options, including logging, the desired protocol, authentication token forwarding, and automatic reconnection, among others.

The HubConnection API exposes start and stop functions, which you use to start and stop the connection to the server. Additionally, there are capabilities for streaming, calling hub methods, and subscribing to events.

An example HubConnection creation

To create a HubConnection object from the .NET SignalR client SDK, you use the HubConnectionBuilder type:

using Microsoft.AspNetCore.SignalR.Client;
using System;
using System.Threading.Tasks;
using RealTime.Models;

namespace ExampleClient;

public sealed class Consumer : IAsyncDisposable
{
    private readonly string HostDomain =
        Environment.GetEnvironmentVariable("HOST_DOMAIN");
    
    private HubConnection _hubConnection;

    public Consumer()
    {
        _hubConnection = new HubConnectionBuilder()
            .WithUrl(new Uri($"{HostDomain}/hub/notifications"))
            .WithAutomaticReconnect()
            .Build();
    }

    public Task StartNotificationConnectionAsync() =>
        _hubConnection.StartAsync();

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

Call hub methods

If a client is given a client HubConnection instance that has successfully started, that client can call methods on a hub by using the InvokeAsync or SendAsync extensions. If the hub method returns a Task<TResult>, the result of InvokeAsync<TResult> is of type TResult. If the hub method returns Task, there's no result. Both InvokeAsync and SendAsync require the name of the hub method, and zero to 10 parameters.

  • InvokeAsync: Invokes a hub method on the server by using the specified method name and optional arguments.
  • SendAsync: Invokes a hub method on the server by using the specified method name and optional arguments. This method doesn't wait for a response from the receiver.

An example hub method invocation

When SendNotificationAsync adds a method to the previous Consumer class, SendNotificationAsync delegates out to the _hubConnection and calls the NotifyAll method on the server's hub, depending on the Notification instance.

public Task SendNotificationAsync(string text) =>
    _hubConnection.InvokeAsync(
        "NotifyAll", new Notification(text, DateTime.UtcNow));

Handle events

To handle events, you register a handler with the HubConnection instance. Call one of the HubConnectionExtensions.On overloads when you know the name of the hub method and have zero to eight parameters. The handler can satisfy any of the following Action variations:

Alternatively, you can use the asynchronous handler APIs, which are Func<TResult> where the TResult is a Task variation:

The result of registering an event handler is an IDisposable, which serves as the subscription. To unsubscribe the handler, call Dispose.

An example event registration

By updating the previous Consumer class, you register to an event by providing a handler and calling On:

using Microsoft.AspNetCore.SignalR.Client;
using System;
using System.Threading.Tasks;
using RealTime.Models;

namespace ExampleClient;

public sealed class Consumer : IAsyncDisposable
{
    private readonly string HostDomain =
        Environment.GetEnvironmentVariable("HOST_DOMAIN");
    
    private HubConnection _hubConnection;

    public Consumer()
    {
        _hubConnection = new HubConnectionBuilder()
            .WithUrl(new Uri($"{HostDomain}/hub/notifications"))
            .WithAutomaticReconnect()
            .Build();

        _hubConnection.On<Notification>(
            "NotificationReceived", OnNotificationReceivedAsync);
    }

    private async Task OnNotificationReceivedAsync(Notification notification)
    {
        // Do something meaningful with the notification.
        await Task.CompletedTask;
    }

    // Omitted for brevity.
}

The OnNotificationReceivedAsync method is called when the server's hub instance fires the "NotificationReceived" event.

Contoso Pizza live order updates

The server code for the web application needs to have a Hub implementation, and expose a route to clients. The Hub could use the order object's unique identifier to create a group for tracking. All order status change updates could then be communicated in this group.

The client code would also need to be updated to indicate that the Contoso Pizza application is a Blazor WebAssembly app. You could use either the JavaScript SDK or the .NET client SDK. You would replace the client-side polling functionality with code that builds a HubConnection, and then start the connection to the server. As it navigates to the order tracking page, the code would have to join the order's specific group where the change updates are sent. You'd subscribe to the event for order status changes, and then handle it accordingly.