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.
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
// 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
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
// 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
// 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
// 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
// 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
// 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-sideProduction issue I've seen: A team's hub method threw a generic
Exceptionthat included a SQL connection string in the message (from an inner exception). SignalR sent this to the client as a hub error. OnlyHubExceptionmessages are forwarded to clients — all other exceptions should be caught, logged server-side, and rethrown asHubExceptionwith 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 callsClients.All.PatientAdmitted(patient). InterfaceIClinicalDashboardClientdefines 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;IHubContextinjects the hub into application services so any code can push to clients. Lifecycle events (OnConnectedAsync,OnDisconnectedAsync) manage connection state. UseHubExceptionfor client-visible errors — other exceptions log server-side only. Return values from hub methods and useIAsyncEnumerablefor server-to-client streaming.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.