Updated on

To send a SignalR message to one client, call Clients.Client(connectionId).SendAsync("methodName", data) from inside the hub. The connection id comes from Context.ConnectionId, which SignalR assigns when the client connects.

That works for a single connection. For everything a user has open at once, Clients.User(userId) addresses every connection belonging to that user, and Clients.Group(name) addresses a set of connections we manage ourselves.

We build on the project from our SignalR Angular Tutorial: Real-Time Charts in ASP.NET Core article, which implements a real-time chart with ASP.NET Core SignalR on the server and Angular on the client side. That article’s hub pushes messages to every connected client; here we modify the same project so the hub can answer just one of them.

To download the source code, visit our Real-Time Charts With SignalR And Angular repository.

How Do We Send a SignalR Message to a Specific Client?

Three properties on the hub’s Clients object cover it, and the right one depends on what “a specific client” means.

Clients.Client(connectionId) targets one connection. The id comes from Context.ConnectionId inside the hub, and SendAsync("methodName", data) invokes the named handler on that client alone.

Clients.User(userId) targets a person rather than a connection, and reaches every connection they have open: two browser tabs and a phone all receive it. SignalR identifies the user from the NameIdentifier claim on the connection’s ClaimsPrincipal, so this only works once the application authenticates and sets that claim.

Clients.Group(groupName) targets a set we define. Connections join and leave with Groups.AddToGroupAsync() and Groups.RemoveFromGroupAsync(), a connection can belong to several groups, and SignalR keeps the membership.

Groups are the general answer. Connection ids are ephemeral and user ids need authentication, but a group is ours to define and manage. Microsoft’s SignalR documentation calls groups “the recommended way to send to a connection or multiple connections because the groups are managed by the application”.

Each client connecting to a SignalR hub has a unique connection id. We can retrieve this using the Context.ConnectionId property of the hub context. Using this, we can send messages just to that particular client:

public async Task BroadcastToConnection(string data, string connectionId)    
    => await Clients.Client(connectionId).SendAsync("broadcasttoclient", data);

By default, SignalR uses the ClaimTypes.NameIdentifier from the ClaimsPrincipal associated with the connection as the user identifier. We can send messages to a particular user using this value: 

public async Task BroadcastToUser(string data, string userId)     
    => await Clients.User(userId).SendAsync("broadcasttouser", data);

Remember that when we are sending messages to a user, they will be sent to all connections associated with that user and not just any particular connection. However, sending messages to individual users requires our application to authenticate users and set the NameIdentifier claim in ClaimsPrincipal. Only then can the connections be mapped to specific users. Our JWT authentication in ASP.NET Core Web API article shows one way to set that claim.

A SignalR group is a collection of connections associated with a name. We can send messages to all connections in a group using the group name. Groups are the recommended way to send messages to multiple connections as it is easy to manage the groups in our application based on the application’s logic. A connection can become a member of multiple groups. Connections can be added to or removed from groups via the AddToGroupAsync() and RemoveFromGroupAsync() methods respectively:

public async Task AddToGroup(string groupName)     
    => await Groups.AddToGroupAsync(Context.ConnectionId, groupName); 
        
public async Task RemoveFromGroup(string groupName)     
    => await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);

Then, we can send messages to the group using the group name:

public async Task BroadcastToGroup(string groupName) => await Clients.Group(groupName)
        .SendAsync("broadcasttogroup", $"{Context.ConnectionId} has joined the group {groupName}.");

Groups are a good choice when we want to implement notifications specific to particular user groups, roles, etc in our application. 

How Do We Get a SignalR Client’s ConnectionId?

Inside a hub method, Context.ConnectionId is the id of the connection that invoked it. That is the only place SignalR hands it out.

Server-side code outside the hub (a controller, a background service) has no direct access to it. Reaching a specific connection from there means either recording the id when the connection is established, or addressing the client by user or group instead through IHubContext<THub>.

Client-side code has the same problem in reverse. The connection id is not exposed to the client by default, so the article’s approach is to add a hub method that returns Context.ConnectionId and call it once the connection starts.

The important property is that connection ids do not survive. A reconnect produces a new one, so a stored id becomes stale silently and messages sent to it go nowhere.

That is the argument for groups and user ids over stored connection ids in anything long-lived.

Pushing from outside the hub is its own topic, and we cover it in calling a SignalR hub from a controller.

Implementing Client-Specific Messages Using SignalR

In the previous section, we discussed how we can send messages to individual connections, users, and groups. Now let’s straight away implement client-specific messages in our SignalR app. We are going to do that by modifying the applications that we created in the linked article.

In that article, we implemented an ASP.NET Core SignalR server that uses a timer to send real-time data to all connected clients. Then, we implemented an Angular chart in the client app which consumes this data. On clicking the chart, we send a message from the client to the server, which in turn pushes a message to all connected clients.

Here, we are going to modify the last step in such a way that once we click the Angular chart on the client app, it additionally sends the connectionId to the server, which helps the server to identify this client. Then, the SignalR hub can send a message back to just this client.

To implement this, first, let’s modify the ChartHub class in our server-side project:

public class ChartHub : Hub
{
    public async Task BroadcastChartDataToClient(List<ChartModel> data, string connectionId) =>
        await Clients.Client(connectionId).SendAsync("broadcastchartdata", data);

    public string GetConnectionId() => Context.ConnectionId;
}

We add a BroadcastChartDataToClient() method that takes connectionId as an additional parameter. This way, we can find the client using the connectionId and send a message just to that client. The name differs from the BroadcastChartData() method of the original project on purpose: SignalR does not support hub method overloads, and two methods sharing one name throw at MapHub time.

One thing is worth saying out loud about this signature. The hub takes the connection id from the client, so any client that guesses or captures an id can address the owner of that connection. For a chart demo that is fine, and as a general pattern it is not: Clients.Caller removes the parameter altogether when the target is whoever called the method, and accepting a connection id from a client is only safe when the server has some reason to trust it.

Additionally, we  add a new GetConnectionId() method, which returns the connectionId of the client.

Next, let’s modify our Angular app. We need to change the SignalRService to get the connectionId and pass it while sending the message to the server:

export class SignalrService {
  public data: ChartModel[];
  public connectionId: string;
  public broadcastedData: ChartModel[];

  private hubConnection: signalR.HubConnection
    public startConnection = () => {
      this.hubConnection = new signalR.HubConnectionBuilder()
                              .withUrl('https://localhost:5001/chart')
                              .withAutomaticReconnect()
                              .build();
      this.hubConnection
        .start()
        .then(() => console.log('Connection started'))
        .then(() => this.getConnectionId())
        .catch(err => console.log('Error while starting connection: ' + err))
    }

    ...

    private getConnectionId = () => {
      this.hubConnection.invoke('getconnectionid')
      .then((data) => {
        console.log(data);
        this.connectionId = data;
      });
    }

    public broadcastChartData = () => {
      const data = this.data.map(m => {
        const temp = {
          data: m.data,
          label: m.label
        }
        return temp;
      });

      this.hubConnection.invoke('broadcastchartdatatoclient', data, this.connectionId)
      .catch(err => console.error(err));
    }

    ...
}

In the startConnection() method, we call the getConnectionId() method, which invokes our hub method to return the connectionId. Once we get this value, we can set it as a property of the class. Later, when we invoke the broadcastchartdatatoclient hub method, we pass the connectionId so that our SignalR hub can identify the client using it.

We use invoke() rather than send() throughout, and the difference is worth one sentence: invoke() waits for the hub method to complete and can return a value, while send() is fire-and-forget. Our getConnectionId() round trip only works because invoke() returns a value.

We also add withAutomaticReconnect() to the builder chain, and it matters more here than it usually would. A reconnect is a new connection with a new id, so the value we stored is dead the moment the client comes back. The onreconnected callback receives the new id, so assigning it there is the whole fix. Our SignalR automatic reconnect article covers the retry schedule and the rest of the reconnect callbacks.

One note on the hub address. withUrl('https://localhost:5001/chart') is a development URL written by hand; in anything deployed it comes from configuration rather than from the source file.

That’s it. We have implemented client-specific message sending in SignalR.

Testing

Now it’s time to test the changes that we have made. For that, we need to run both the server application and the client app. To see client-specific behavior in action, let’s run two instances of the client app. Once two client instances are up and running, let’s click the chart on any one instance:

sending client-specific messages using signalR

We can see that the SignalR hub sends a message back to just that client instance. Remember that before implementing this change, when we clicked the chart on any client instance, the SignalR hub was used to send a message to all connected client instances.

Now that we have learned how to send messages to specific clients, we can create a lot of cool features using this technique. 

Which Clients Property Should We Use in SignalR?

Start from the audience, not from the API.

For a reply to whoever just called the hub method, Clients.Caller is simpler and safer than passing a connection id around: SignalR already knows who called.

For a broadcast, Clients.All reaches everyone connected, and Clients.Others reaches everyone except the caller, which is what most “someone did something” notifications actually want.

For one person, Clients.User is almost always the right choice over Clients.Client, because a person is not a connection. Microsoft’s SignalR documentation notes that “A single user in SignalR can have multiple connections to an app.” Reserve Clients.Client for cases where the specific connection is genuinely the target: the tab that made the request, a device being paired.

For anything with a shape (a room, a document, a tenant, a role), use groups. They cost one call to join, they survive as long as we maintain them, and they turn “who should receive this” into a question we answer at join time rather than at send time.

PropertySends toUse it for
Clients.AllEvery connected clientBroadcasts (a global notice, a shared tick)
Clients.CallerThe connection that invoked this hub methodAcknowledging or answering a request
Clients.OthersEveryone except the caller"Someone else did something" notifications
Clients.Client(id)One connectionThe case this article covers
Clients.Clients(ids)A specific list of connectionsA hand-picked set
Clients.User(userId)Every connection belonging to one userReaching a person, not a tab
Clients.Users(userIds)Every connection of several usersA hand-picked set of people
Clients.Group(name)Every connection in a groupRooms, channels, tenants
Clients.Groups(names)Several groupsOverlapping audiences
Clients.GroupExcept(name, ids)A group, minus named connectionsA room excluding the sender
Clients.OthersInGroup(name)A group, minus the callerThe common chat-room case

The same reasoning transfers to other front ends. Our real-time charts with Blazor WebAssembly and SignalR article builds the same chart against the same kind of hub.

Conclusion

We have learned the following topics in this article:

  • The concept of connections, users, and groups in SignalR
  • How to send messages from SignalR hub to specific connections, users, and groups
  • How to modify our SignalR app to send client-specific messages

Tested with .NET 10.0.10.