Hub methods and client invocations are the two directions of SignalR communication. Angular calls Hub methods (like submitting a message or subscribing to a topic). The server calls client methods to push data (like broadcasting a new post notification). IHubContext<THub> is the key service for triggering client invocations from outside Hub classes — from controllers, services, and background workers. This enables the pattern where a REST API write (POST /api/posts/{id}/publish) triggers a SignalR push to all listening Angular clients.
Hub Methods and Client Calls
public class PostHub : Hub
{
// ── Angular calls this to subscribe to a post's updates ───────────────
public async Task SubscribeToPost(int postId)
{
var groupName = $"post-{postId}";
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
// Confirm subscription back to the caller only
await Clients.Caller.SendAsync("SubscribedToPost", postId);
}
public async Task UnsubscribeFromPost(int postId)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"post-{postId}");
}
// ── Angular calls this to submit a live comment ────────────────────────
public async Task SendComment(int postId, string content)
{
var userId = Context.User?.FindFirstValue(ClaimTypes.NameIdentifier);
var username = Context.User?.FindFirstValue(ClaimTypes.Name);
// Broadcast new comment to all subscribers of this post
await Clients.Group($"post-{postId}").SendAsync("NewComment", new
{
PostId = postId,
UserId = userId,
UserName = username,
Content = content,
Timestamp = DateTime.UtcNow,
});
}
// ── Hub client targets ─────────────────────────────────────────────────
// Clients.All — every connected client
// Clients.Caller — only the client that invoked this method
// Clients.Others — all clients EXCEPT the caller
// Clients.Client(id) — one specific client by connectionId
// Clients.Clients(ids) — multiple specific clients
// Clients.Group(name) — all clients in a named group
// Clients.OthersInGroup(name) — group members except the caller
// Clients.User(userId) — all connections for an authenticated user
}
// ── IHubContext — send from outside the Hub ────────────────────────────────
// Inject into services, controllers, background workers
public class PostService(
IPostRepository repo,
IHubContext<PostHub> hubContext) : IPostService
{
public async Task PublishAsync(int id, CancellationToken ct)
{
var post = await repo.PublishAsync(id, ct);
// Push notification to all connected Angular clients
await hubContext.Clients.All.SendAsync("PostPublished", new
{
post.Id,
post.Title,
post.Slug,
post.PublishedAt,
AuthorName = post.Author?.DisplayName,
}, ct);
// Push to post-specific group (clients viewing this post)
await hubContext.Clients.Group($"post-{id}").SendAsync("PostUpdated", new
{
post.Id,
post.IsPublished,
post.PublishedAt,
}, ct);
}
}
IHubContext<THub> is registered as a Singleton and can be injected anywhere — services, controllers, background workers, MediatR handlers. It provides the same Clients.* API as the Hub class. This is the primary way to push real-time updates from your REST API operations: the controller action handles the HTTP request, calls the service, and the service pushes a SignalR notification. The Hub class itself handles client-to-server invocations; IHubContext handles server-to-client pushes from non-Hub code.Clients.Group("post-42") rather than Clients.All. This prevents every connected client from receiving every update, which quickly becomes unsustainable with many posts and many connected users. Groups are in-memory by default (not persisted) — clients rejoin groups on reconnection.SendAsync() is fire-and-forget — it returns when the message is dispatched to the SignalR infrastructure, not when the client has received and processed it. If an Angular client is temporarily disconnected, the message is lost unless you implement message persistence (store undelivered messages and re-send on reconnection). For notifications where delivery reliability matters, combine SignalR push with a REST endpoint the client can poll for missed messages on reconnection.Angular Side (TypeScript Reference)
// ── Angular SignalR connection (TypeScript reference) ─────────────────────
// npm install @microsoft/signalr
// @Injectable({ providedIn: 'root' })
// export class PostHubService {
// private connection: HubConnection;
//
// constructor() {
// this.connection = new HubConnectionBuilder()
// .withUrl(`${environment.apiUrl}/hubs/posts`)
// .withAutomaticReconnect() // auto reconnect on drop
// .build();
//
// // Register client-side method handlers
// this.connection.on('PostPublished', (post: PostSummaryDto) => {
// this.postPublished$.next(post);
// });
//
// this.connection.on('NewComment', (comment: CommentDto) => {
// this.newComment$.next(comment);
// });
// }
//
// async start() {
// await this.connection.start();
// }
//
// async subscribeToPost(postId: number) {
// await this.connection.invoke('SubscribeToPost', postId);
// }
// }
Common Mistakes
Mistake 1 — Broadcasting to Clients.All instead of specific groups (sends to everyone)
❌ Wrong — every post update sent to every connected user; unbounded traffic as connections scale.
✅ Correct — use groups for topic subscription; only send to interested clients.
Mistake 2 — Assuming SendAsync is reliable (messages lost on disconnection)
❌ Wrong — relying on SignalR push for critical notifications without fallback.
✅ Correct — combine SignalR for live push with REST polling for missed messages on reconnection.