0Pricing
C# Academy · Lesson

Groups, Users & Connection Management

Manage groups, target specific users or connections, and handle connect/disconnect lifecycle events.

Groups, Users & Connection Management is a free C# Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C# Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Why Groups and Users?

In real applications you rarely want to broadcast to ALL clients. SignalR's groups let you target subsets of connections (e.g., a chat room), and users let you address all connections belonging to a specific authenticated user.

Adding Connections to Groups

Call Groups.AddToGroupAsync() with the connection ID and a group name. A connection can belong to multiple groups simultaneously.

public class ChatHub : Hub
{
    public async Task JoinRoom(string roomName)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, roomName);
        await Clients.Group(roomName)
                     .SendAsync("UserJoined", Context.User!.Identity!.Name);
    }

    public async Task LeaveRoom(string roomName)
    {
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomName);
        await Clients.Group(roomName)
                     .SendAsync("UserLeft", Context.User!.Identity!.Name);
    }
}

Sending to Groups

Use Clients.Group(name) to send to all connections in a group. Other targeting options include GroupExcept and Groups (multiple at once).

public async Task SendToRoom(string roomName, string message)
{
    var sender = Context.User!.Identity!.Name;

    // Everyone in the room
    await Clients.Group(roomName)
                 .SendAsync("ReceiveMessage", sender, message);

    // Everyone in the room except the sender
    await Clients.GroupExcept(roomName, Context.ConnectionId)
                 .SendAsync("ReceiveMessage", sender, message);
}

User-Based Targeting

When a user has multiple browser tabs or devices open, they have multiple connections. Clients.User(userId) sends to ALL connections for that user simultaneously.

// Send to all connections of user with ID "user-123"
await Clients.User("user-123")
             .SendAsync("Notification", "Your order has shipped!");

// SignalR uses the NameIdentifier claim by default.
// Context.UserIdentifier returns the current user's ID.
var myId = Context.UserIdentifier;
await Clients.User(myId!).SendAsync("Ack", "Server received your message");

Custom User ID Provider

By default SignalR uses the NameIdentifier claim as the user ID. Implement IUserIdProvider to use a different claim or custom logic.

public class EmailUserIdProvider : IUserIdProvider
{
    public string? GetUserId(HubConnectionContext connection)
    {
        return connection.User?.FindFirst(ClaimTypes.Email)?.Value;
    }
}

// Register:
builder.Services.AddSingleton<IUserIdProvider, EmailUserIdProvider>();

Tracking Connection State

SignalR doesn't persist connection state between disconnects. Track your own mapping of user IDs to connection IDs in a thread-safe dictionary (single server) or Redis (multi-server).

public class ConnectionTracker
{
    private readonly ConcurrentDictionary<string, HashSet<string>> _userConnections
        = new();

    public void AddConnection(string userId, string connectionId)
        => _userConnections.GetOrAdd(userId, _ => new()).Add(connectionId);

    public void RemoveConnection(string userId, string connectionId)
    {
        if (_userConnections.TryGetValue(userId, out var conns))
        {
            conns.Remove(connectionId);
            if (conns.Count == 0) _userConnections.TryRemove(userId, out _);
        }
    }

    public IEnumerable<string> GetConnections(string userId)
        => _userConnections.TryGetValue(userId, out var c) ? c : Enumerable.Empty<string>();
}

Auto-Managing Groups in OnConnected/OnDisconnected

A common pattern: automatically add authenticated users to their personal group in OnConnectedAsync so you can target them by group name without custom tracking.

public override async Task OnConnectedAsync()
{
    // Add to a personal group named by user ID
    var userId = Context.UserIdentifier;
    if (userId is not null)
        await Groups.AddToGroupAsync(Context.ConnectionId, userId);
    await base.OnConnectedAsync();
}

// Now send to a user by their personal group:
await _hub.Clients.Group(userId)
           .SendAsync("Update", payload);

Sending from Background Services with Groups

Using IHubContext you can target groups and users from background services — perfect for async notifications triggered by events in your system.

public class OrderShippedHandler : INotificationHandler<OrderShipped>
{
    private readonly IHubContext<OrderHub> _hub;
    public OrderShippedHandler(IHubContext<OrderHub> hub) => _hub = hub;

    public async Task Handle(OrderShipped notification, CancellationToken ct)
    {
        // Push to the customer's personal group
        await _hub.Clients.Group(notification.CustomerId.ToString())
                  .SendAsync("OrderShipped",
                      new { notification.OrderId, notification.TrackingNumber },
                      ct);
    }
}

Group Membership Persistence

Group membership is not persisted — if the server restarts or a Redis backplane loses state, groups are empty. Re-join groups on reconnect using the client onreconnected event.

// JavaScript client: re-join groups after reconnect
connection.onreconnected(async connectionId => {
    console.log(`Reconnected as ${connectionId}`);
    // Re-join previously joined rooms
    for (const room of joinedRooms) {
        await connection.invoke("JoinRoom", room);
    }
});

Presence: Online/Offline Detection

Track online status by counting connections per user in OnConnected/OnDisconnected and broadcasting presence events to relevant groups.

public override async Task OnConnectedAsync()
{
    var userId = Context.UserIdentifier!;
    var count = _tracker.AddConnection(userId, Context.ConnectionId);
    if (count == 1) // first connection for this user
        await Clients.Others.SendAsync("UserOnline", userId);
    await base.OnConnectedAsync();
}

public override async Task OnDisconnectedAsync(Exception? ex)
{
    var userId = Context.UserIdentifier!;
    var count = _tracker.RemoveConnection(userId, Context.ConnectionId);
    if (count == 0) // no more connections
        await Clients.Others.SendAsync("UserOffline", userId);
    await base.OnDisconnectedAsync(ex);
}

Quick Check

When a user has three browser tabs open, what does Clients.User(userId) do?

Recap: Groups, Users & Connection Management

Key takeaways:

  • Groups.AddToGroupAsync/RemoveFromGroupAsync: manage group membership
  • Clients.Group(name): send to a subset of connections
  • Clients.User(userId): send to all connections of a user
  • Implement IUserIdProvider for custom user ID claims
  • Groups are not persisted — re-join on reconnect
  • Track connection counts in ConcurrentDictionary for presence detection

Frequently asked questions

Is the “Groups, Users & Connection Management” lesson free?

Yes — the full text of “Groups, Users & Connection Management” is free to read here on the web, and the C# Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C# Academy course, upgrade to CoddyKit PRO.

What will I learn in “Groups, Users & Connection Management”?

Manage groups, target specific users or connections, and handle connect/disconnect lifecycle events. You practise C# Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start C# Academy?

No prior experience is required. C# Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Groups, Users & Connection Management” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this C# Academy lesson?

Yes. Every C# Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. SignalR Hubs & Connections
  2. Groups, Users & Connection Management
  3. Strongly Typed Hubs
  4. Scaling with Redis Backplane
← Back to C# Academy