Learnixo
Back to blog
AI Systemsintermediate

Hub Methods — Calling Between Clients and Server in SignalR

SignalR hub methods in depth: strongly-typed hubs, calling clients from the server, calling the server from clients, hub context injection, and the invocation patterns for real-time clinical dashboards.

Asma Hafeez KhanMay 16, 20265 min read
SignalRASP.NET Core.NETReal-TimeWebSockets
Share:𝕏

Hub Methods Overview

A SignalR Hub is a class that exposes methods the client can call (hub methods) and can call methods on connected clients. Communication is bidirectional.

Server Hub Method:         client calls it    → server executes
Client Method:             server calls it    → client executes

Direction:
  Client → Server:  client.invoke("MethodName", args)
  Server → Client:  await Clients.All.SendAsync("EventName", data)
  Server → Client:  await Clients.Caller.SendAsync(...)    (only the caller)
  Server → Client:  await Clients.Group("ward-4b").SendAsync(...)

Strongly-Typed Hub

C#
// Define the client interface — what methods the server can call on clients
public interface IClinicalDashboardClient
{
    Task PatientAdmitted(PatientAdmittedDto patient);
    Task DrugOrderStatusChanged(DrugOrderStatusDto status);
    Task AlarmTriggered(AlarmDto alarm);
    Task WardCapacityUpdated(WardCapacityDto capacity);
}

// Hub class — TClient = the client interface
public sealed class ClinicalDashboardHub
    : Hub<IClinicalDashboardClient>
{
    // Hub methods — called by clients
    public async Task SubscribeToWard(string wardId)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, $"ward:{wardId}");
        await Clients.Caller.WardCapacityUpdated(
            await GetWardCapacityAsync(wardId));
    }

    public async Task UnsubscribeFromWard(string wardId)
    {
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"ward:{wardId}");
    }

    // Called by JavaScript client: connection.invoke("AcknowledgeAlarm", alarmId)
    public async Task AcknowledgeAlarm(Guid alarmId)
    {
        var doctorId = Context.User?.FindFirstValue(JwtRegisteredClaimNames.Sub);
        // ... process acknowledgment
        // Notify all ward subscribers that alarm was acknowledged
        await Clients.Group($"ward:{wardId}")
            .AlarmTriggered(new AlarmDto(alarmId, "Acknowledged", doctorId!));
    }
}

Hub Lifecycle Events

C#
public sealed class ClinicalDashboardHub : Hub<IClinicalDashboardClient>
{
    private readonly IConnectionTracker _tracker;

    public ClinicalDashboardHub(IConnectionTracker tracker)
        => _tracker = tracker;

    // Called when a client connects
    public override async Task OnConnectedAsync()
    {
        var userId = Context.User?.FindFirstValue(JwtRegisteredClaimNames.Sub)
            ?? throw new HubException("Unauthenticated.");

        await _tracker.RegisterAsync(Context.ConnectionId, userId);
        await base.OnConnectedAsync();
    }

    // Called when a client disconnects (exception = reason, null = normal close)
    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        await _tracker.UnregisterAsync(Context.ConnectionId);
        await base.OnDisconnectedAsync(exception);
    }
}

Calling Clients from Hub Methods

C#
// Clients.All — every connected client
await Clients.All.AlarmTriggered(alarm);

// Clients.Caller — only the client that called this method
await Clients.Caller.WardCapacityUpdated(capacity);

// Clients.Others — everyone except the caller
await Clients.Others.PatientAdmitted(patient);

// Clients.Client(connectionId) — one specific connection
await Clients.Client(specificConnectionId).DrugOrderStatusChanged(status);

// Clients.Group(groupName) — all clients in a group
await Clients.Group($"ward:{wardId}").PatientAdmitted(patient);

// Clients.GroupExcept(groupName, connectionId) — group minus one connection
await Clients.GroupExcept($"ward:{wardId}", Context.ConnectionId).PatientAdmitted(patient);

// Clients.Clients(IReadOnlyList<string> connectionIds) — specific connections
await Clients.Clients(connectionIds).AlarmTriggered(alarm);

IHubContext — Calling Clients from Outside a Hub

C#
// Inject IHubContext<THub, TClient> into any service to push to clients
public sealed class DrugOrderService
{
    private readonly IHubContext<ClinicalDashboardHub, IClinicalDashboardClient> _hub;
    private readonly DrugOrderRepository _repo;

    public DrugOrderService(
        IHubContext<ClinicalDashboardHub, IClinicalDashboardClient> hub,
        DrugOrderRepository repo)
        => (_hub, _repo) = (hub, repo);

    public async Task DispenseOrderAsync(Guid orderId, CancellationToken ct)
    {
        var order = await _repo.GetByIdAsync(orderId, ct);
        // ... dispense logic

        // Notify relevant ward after dispense
        await _hub.Clients
            .Group($"ward:{order.WardId}")
            .DrugOrderStatusChanged(new DrugOrderStatusDto(
                orderId, "Dispensed", DateTime.UtcNow));
    }
}

Return Values from Hub Methods

C#
// Hub methods can return values to the caller (SignalR 6+)
public async Task<WardCapacityDto> GetWardCapacity(string wardId)
{
    var capacity = await _wardService.GetCapacityAsync(wardId);
    return new WardCapacityDto(wardId, capacity.TotalBeds, capacity.OccupiedBeds);
}

// JavaScript client:
// const capacity = await connection.invoke("GetWardCapacity", "4B");
// console.log(capacity.occupiedBeds);

Streaming from Hub to Client

C#
// IAsyncEnumerable<T> — server streams data to client
public async IAsyncEnumerable<INRReadingDto> StreamINRReadings(
    Guid patientId,
    [EnumeratorCancellation] CancellationToken ct)
{
    while (!ct.IsCancellationRequested)
    {
        var reading = await _vitals.GetLatestINRAsync(patientId, ct);
        if (reading is not null)
            yield return new INRReadingDto(reading.Value, reading.Timestamp);

        await Task.Delay(TimeSpan.FromSeconds(30), ct);
    }
}

// JavaScript:
// const stream = connection.stream("StreamINRReadings", patientId);
// stream.subscribe({ next: reading => updateUI(reading) });

Hub Exceptions

C#
// Throw HubException to send an error to the client
public async Task AcknowledgeAlarm(Guid alarmId)
{
    var alarm = await _repo.GetByIdAsync(alarmId, CancellationToken.None);
    if (alarm is null)
        throw new HubException($"Alarm {alarmId} not found.");

    if (alarm.IsAcknowledged)
        throw new HubException("Alarm already acknowledged.");

    // ... process
}

// HubException message is sent to the client
// Other exceptions: message is NOT sent (security) — log them server-side

Production issue I've seen: A team's hub method threw a generic Exception that included a SQL connection string in the message (from an inner exception). SignalR sent this to the client as a hub error. Only HubException messages are forwarded to clients — all other exceptions should be caught, logged server-side, and rethrown as HubException with a safe user-facing message.


Red Flag / Green Answer

Red Flag: "We call Clients.All.SendAsync("PatientAdmitted", patient) — using raw string method names."

String method names have no compile-time checking. A typo in the server or client silently breaks the communication. Both sides can mismatch and no error is thrown at build time.

Green Answer:

Strongly-typed hub: Hub<IClinicalDashboardClient>. Server calls Clients.All.PatientAdmitted(patient). Interface IClinicalDashboardClient defines all client methods. Compile-time checking on both server and client-side type generation.


Key Takeaway

Strongly-typed hubs (Hub<TClient>) provide compile-time checking for all client method calls. Hub methods are called by clients; IHubContext injects the hub into application services so any code can push to clients. Lifecycle events (OnConnectedAsync, OnDisconnectedAsync) manage connection state. Use HubException for client-visible errors — other exceptions log server-side only. Return values from hub methods and use IAsyncEnumerable for server-to-client streaming.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.