C# Academy · Lesson

Groups, Users & Connection Management

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

Lesson 2 of 412 steps

Groups, Users & Connection Management is a free C# Academy lesson on CoddyKit. This is 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, and your progress syncs across the web and the CoddyKit app. The C# Academy course includes 4 lessons in total.

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
Free to start

Learn C# with an AI tutor — free

Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.

Courses
93
Lessons
346

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. 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. The C# Academy course includes 4 lessons in total.

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, so you can start here or from the beginning and move at your own pace. This is lesson 2 of 4.

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