SignalR and Hubs

A hub is a class whose public methods clients can call, and whose Clients property pushes messages back out over a WebSocket.

Syntaxapp.MapHub<ChatHub>("/hubs/chat");

A web page usually gets data by asking for it. SignalR turns that round. Code on the server calls a method on a connected browser, and the browser runs it. The alternative is polling: the page asks "anything new?" every few seconds, and the answer is almost always no. Polling wastes requests and still arrives late.

The hub

A hub is a class you write that inherits from Hub. Its public methods are the ones a client may call by name. A client here is any connected program: a browser, a phone app, another .NET process.

Inside a hub method the Clients property goes the other way. Clients.All.SendAsync("ReceiveMessage", user, text) asks every connected client to run its own handler called ReceiveMessage with those two values. That name is a plain string, so nothing checks it when you build. A typo just means nobody reacts. SendAsync is a shorthand: it packs its arguments into an array and calls SendCoreAsync, which is the only method on the underlying interface. The two are the same call.

Two lines wire it up. builder.Services.AddSignalR() registers the services, and app.MapHub<ChatHub>("/hubs/chat") puts the hub at a URL.

Transports and negotiation

A transport is how the bytes actually travel. SignalR prefers a WebSocket, a two-way connection that stays open so either side can send at any time. First the client sends an HTTP POST to /hubs/chat/negotiate, and the reply is JSON holding a connection id and an availableTransports list. If WebSockets are unavailable, SignalR falls back to server-sent events, then to long polling, in that order.

Example

Example · csharp
using Microsoft.AspNetCore.SignalR;

var builder = WebApplication.CreateBuilder(args);

// Registers the services a hub needs. MapHub fails at startup without it.
builder.Services.AddSignalR();

var app = builder.Build();

// The address clients connect to. ChatHub is declared below.
app.MapHub<ChatHub>("/hubs/chat");

app.Run();

// A hub is an ordinary class. Its public methods are callable by clients.
public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
    {
        // Runs "ReceiveMessage" on every connected client, the caller included.
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}

When to use it

  • Push a build's status to an open dashboard instead of refreshing the page every five seconds.
  • Show a new chat message to everyone in the room the moment it is sent.
  • Tell a browser that the file it uploaded has finished processing.

More examples

The same send, written three ways

All three methods do the identical thing. You will meet the SendCoreAsync form when the client method name or the argument list is built at run time, and the expression-bodied form all over other people's hubs. The one shape to avoid is calling SendAsync as a bare statement without await or return: the hub method can finish first and the send then fails.

Example · csharp
public class ChatHub : Hub
{
    // Shorthand. SendAsync is an extension method on the client proxy.
    public async Task SendShorthand(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }

    // The same call written out. SendAsync packs its arguments into this
    // array and calls SendCoreAsync, the only method on IClientProxy.
    public async Task SendLonghand(string user, string message)
    {
        await Clients.All.SendCoreAsync(
            "ReceiveMessage",
            new object?[] { user, message });
    }

    // Expression-bodied form. Returning the Task is fine, because SignalR
    // awaits what the method returns. Dropping it on the floor is not.
    public Task SendExpressionBodied(string user, string message)
        => Clients.All.SendAsync("ReceiveMessage", user, message);
}

A strongly typed hub

Reach for Hub<T> once a hub has more than two or three client methods, because the string names are the usual source of silent breakage. Mapping is unchanged: app.MapHub<ChatHub>("/hubs/chat"). Note that SendAsync is no longer available on a Hub<T>; the interface methods replace it entirely.

Example · csharp
// The client's methods, written as a C# interface. One place to spell them.
public interface IChatClient
{
    Task ReceiveMessage(string user, string message);
}

// Hub<T> says: my clients implement T.
public class ChatHub : Hub<IChatClient>
{
    // No string name anywhere. A misspelling is now a compile error.
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.ReceiveMessage(user, message);
    }
}

Sending from outside a hub

Most pushes do not start with a client calling a hub. They start with an HTTP request, a queue message or a timer. Ask for IHubContext<THub> wherever you can take a constructor or parameter injection and send from there. There is no caller in that situation, so Clients.Caller and Clients.Others do not exist on an IHubContext.

Example · csharp
// In Program.cs, after builder.Services.AddSignalR().
app.MapPost("/alerts", async (string text, IHubContext<ChatHub> hub) =>
{
    // The same Clients API, used outside any hub method.
    await hub.Clients.All.SendAsync("ReceiveMessage", "system", text);
    return Results.Accepted();
});

Discussion

  • Be the first to comment on this lesson.