Hub Methods — Server Methods, Client Invocations and Groups

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);
    }
}
Note: 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.
Tip: Use groups for topic-based subscriptions — clients interested in updates for post #42 join group “post-42”. When post #42 is updated, send to 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.
Warning: 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.

🧠 Test Yourself

A REST API controller publishes a post and wants to notify all connected Angular clients in real time. The notification must come from inside the controller action, not from within a Hub method. How is this achieved?