122

In the latest version of Asp.Net SignalR, was added a new way of sending a message to a specific user, using the interface "IUserIdProvider".

public interface IUserIdProvider
{
   string GetUserId(IRequest request);
}

public class MyHub : Hub
{
   public void Send(string userId, string message)
   {
      Clients.User(userId).send(message);
   }
}

My question is: How do I know to whom I am sending my message? The explanation of this new method is very superficial. And the draft Statement of SignalR 2.0.0 with this bug and does not compile. Has anyone implemented this feature?

More Info : http://www.asp.net/signalr/overview/signalr-20/hubs-api/mapping-users-to-connections#IUserIdProvider

Hugs.

Igor
  • 3,573
  • 4
  • 33
  • 55
  • 1
    You need to look into Authentication and Authorization with SignalR. The UserId will be part of the IPrincipal provider. – Gjohn Jan 19 '14 at 20:40

6 Answers6

175

SignalR provides ConnectionId for each connection. To find which connection belongs to whom (the user), we need to create a mapping between the connection and the user. This depends on how you identify a user in your application.

In SignalR 2.0, this is done by using the inbuilt IPrincipal.Identity.Name, which is the logged in user identifier as set during the ASP.NET authentication.

However, you may need to map the connection with the user using a different identifier instead of using the Identity.Name. For this purpose this new provider can be used with your custom implementation for mapping user with the connection.

Example of Mapping SignalR Users to Connections using IUserIdProvider

Lets assume our application uses a userId to identify each user. Now, we need to send message to a specific user. We have userId and message, but SignalR must also know the mapping between our userId and the connection.

To achieve this, first we need to create a new class which implements IUserIdProvider:

public class CustomUserIdProvider : IUserIdProvider
{
     public string GetUserId(IRequest request)
    {
        // your logic to fetch a user identifier goes here.

        // for example:

        var userId = MyCustomUserClass.FindUserId(request.User.Identity.Name);
        return userId.ToString();
    }
}

The second step is to tell SignalR to use our CustomUserIdProvider instead of the default implementation. This can be done in the Startup.cs while initializing the hub configuration:

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        var idProvider = new CustomUserIdProvider();

        GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), () => idProvider);          

        // Any connection or hub wire up and configuration should go here
        app.MapSignalR();
    }
}

Now, you can send message to a specific user using his userId as mentioned in the documentation, like:

public class MyHub : Hub
{
   public void Send(string userId, string message)
   {
      Clients.User(userId).send(message);
   }
}
starball
  • 20,030
  • 7
  • 43
  • 238
Sumant
  • 2,201
  • 1
  • 16
  • 13
  • Hi friend, sorry for the late feedback! But I tried to access the Id that generates CustomUserIdProvider but "OnConnected" method it is not the same. How do I link to a User in the database? Thank you! – Igor May 01 '14 at 01:37
  • 9
    What is MyCustomUserClass? – Danny Bullis Apr 22 '15 at 23:02
  • 4
    "MyCustomUserClass" could be your custom user class which contains the FindUserId method. This is just for example. You could have any method in any class that returns you UserId and use that here. – Sumant Apr 24 '15 at 08:46
  • Excellent answer...i'm gonna give it a try tomorrow :) – Hackerman May 27 '15 at 01:25
  • I have a fully working version of this without the customer userid provider, and I can successfully call a hubContext.Clients.All.newConversation(message) and it works as expected. However if I enable my custom IUserIdProvider in startup as specified above and change the Clients.All to hubContext.Clients.User(userid).newConversation(message) no messages are delivered. Also I noticed that once I enabled the IUserIdProvider the onConnected method never fires in my hub (does without IUserIdProvider). What am I missing here? – xinunix Jun 10 '15 at 15:55
  • @xinunix I suspect there is some error in wiring up your custom userid provider. It would be difficult to guess without having a look at the code. – Sumant Jun 16 '15 at 09:46
  • 6
    Thanks @Sumant, my issue ended up being that b/c I was in a Web API project where I had implemented OAuth 2 with bearer token I had to implement logic to pass the bearer token on query string since it can't be pulled from the headers on that initial signalr connect request. Couldn't just use request.User.Identity.Name – xinunix Jul 02 '15 at 23:50
  • 1
    @xinunix In the Web API project, you could still use `HttpContext.Current.User.Identity.Name`. Did you try that? – Sumant Jul 09 '15 at 10:53
  • hello frnds here i am not able to understand how my Send Method is getting called?? is it require to be a method name as send or it can be any thing?? because if in my code if i am doing this code its working but where the send message is getting called or when it is getting called?? – Liquid Oct 05 '15 at 18:15
  • Do I have to use a string for the user identification? – Ezra Bailey Oct 06 '15 at 13:55
  • @Sumant HttpContext.Current.User is always null for me. Where could be a problem? – Mr. Robot Feb 12 '16 at 16:49
  • @Liquid you can call send from the server (i.e. `Context.Clients.Client(connectionId).send("Message Sent Successfully!");`) and define where to put this as html in your javascript (i.e. `chat.client.send = function (message) {//append somewhere};` It can be named whatever you like, as long is the naming is consistent when you call and define it. – user95227 Feb 17 '16 at 13:18
  • @IamnotBatman silly to ask, but have you enabled "authentication" in your app? – Sumant Feb 19 '16 at 05:17
  • 1
    @Sumant I already resolved that. The problem was that I put `app.MapSignalR();` in global.asax before authentication. – Mr. Robot Feb 19 '16 at 14:46
  • What is the implementation of Send(string userId, string message) on client side ? How userid value is passing from client side ? Is what you show send message to only one user and even not sending message to the sender ? – Mayur Patel Apr 28 '16 at 08:46
  • @MayurPatel client side, js code, remains same - no change there. The above code assumes you have an 'authenticated' user, set in the `Identity` from where it retrieves `request.User.Identity.Name` - which could be user's email or a handle (for example) - basically anything **unique** which helps you identify your user. It will send message only to a single user. This is not a chat example, but is just sending message from a server to a specific client. – Sumant May 26 '16 at 04:41
  • @Sumant GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), () => idProvider); how to do this in vb? – Leonan Milani Mar 08 '18 at 11:28
  • I'd like to add you need to put `app.MapSignalR();` **after** `ConfigureAuth(app);` in Startup.cs file! Then, everything will work like a chram :) – 1_bug Apr 24 '18 at 08:34
  • This is a great answer, but keep in mind that according to [MS Documentation](https://learn.microsoft.com/en-us/aspnet/signalr/overview/security/introduction-to-security) randomly generated connectionId is a security measure, which prevents a malicious user from guessing the connectionId. See [this](https://stackoverflow.com/questions/52194946/how-to-securely-send-a-message-to-a-specific-user) question for more info. – Hooman Bahreini Nov 14 '18 at 07:08
  • Any suggestions how to do this in ASP.NET Core? – bde.dev Jul 11 '20 at 14:12
  • https://stackoverflow.com/questions/51966231/how-to-user-iuseridprovider-in-net-core – Crimson_Hawk Jul 28 '21 at 19:31
48

Here's a start.. Open to suggestions/improvements.

Server

public class ChatHub : Hub
{
    public void SendChatMessage(string who, string message)
    {
        string name = Context.User.Identity.Name;
        Clients.Group(name).addChatMessage(name, message);
        Clients.Group("2@2.com").addChatMessage(name, message);
    }

    public override Task OnConnected()
    {
        string name = Context.User.Identity.Name;
        Groups.Add(Context.ConnectionId, name);

        return base.OnConnected();
    }
}

JavaScript

(Notice how addChatMessage and sendChatMessage are also methods in the server code above)

    $(function () {
    // Declare a proxy to reference the hub.
    var chat = $.connection.chatHub;
    // Create a function that the hub can call to broadcast messages.
    chat.client.addChatMessage = function (who, message) {
        // Html encode display name and message.
        var encodedName = $('<div />').text(who).html();
        var encodedMsg = $('<div />').text(message).html();
        // Add the message to the page.
        $('#chat').append('<li><strong>' + encodedName
            + '</strong>:&nbsp;&nbsp;' + encodedMsg + '</li>');
    };

    // Start the connection.
    $.connection.hub.start().done(function () {
        $('#sendmessage').click(function () {
            // Call the Send method on the hub.
            chat.server.sendChatMessage($('#displayname').val(), $('#message').val());
            // Clear text box and reset focus for next comment.
            $('#message').val('').focus();
        });
    });
});

Testing enter image description here

The Muffin Man
  • 19,585
  • 30
  • 119
  • 191
  • Hi friend, sorry for the late feedback! But how can I prevent the loss of CONNECTIONID? Thank you. – Igor May 01 '14 at 01:38
  • 7
    @lgao I have no idea. – The Muffin Man May 14 '14 at 15:33
  • why this line was required --- Clients.Group("2@2.com").addChatMessage(name, message); ?? – Thomas Jan 01 '15 at 16:23
  • @Thomas I probably included it for the sake of the demo. There has to be another way to broadcast to a specific group as this was hardcoded. – The Muffin Man Jan 01 '15 at 23:32
  • This simple solution solved my issue to send a message to a specific logged user. Its simple, fast and easy to understand. I would upvote this answer several times if i could. – Rafael A. M. S. Mar 31 '15 at 18:03
  • how u r sending message to specific user because who variable not used in code.....would u help me to understand. thanks – Mou Feb 15 '16 at 18:26
  • @Mou `Clients.Group("2@2.com").addChatMessage(name, message);` or look above that line and see how to do it dynamically. – The Muffin Man Feb 15 '16 at 18:31
  • i am talking about this line `Clients.Group(name).addChatMessage(name, message);` this line send message to itself because when i call `SendChatMessage` function from client side and server side function will be called then this line `Context.User.Identity.Name` always return my login name not my friend login name. – Mou Feb 15 '16 at 18:39
  • @Mou That's right, it is just an example of getting chat working.. you need to use the `who` argument instead of `name`. `who` is populated on the client side, you'd have to populate that with the person you want to send to. – The Muffin Man Feb 15 '16 at 18:52
  • @TheMuffinMan good day. may be it is a bit late, but I can't figure out how to populate 'who' varible on server side? could you give a clue how to do this? – Amelina Oct 28 '16 at 10:18
  • @Amelina Unfortunately I'm not sure. I haven't used signalr since I wrote this answer and even then the understanding I had with it was basic. – The Muffin Man Oct 28 '16 at 16:06
  • what is Context here I'm getting nul – Neo Mar 27 '19 at 17:03
8

This is how use SignarR in order to target a specific user (without using any provider):

 private static ConcurrentDictionary<string, string> clients = new ConcurrentDictionary<string, string>();

 public string Login(string username)
 {
     clients.TryAdd(Context.ConnectionId, username);            
     return username;
 }

// The variable 'contextIdClient' is equal to Context.ConnectionId of the user, 
// once logged in. You have to store that 'id' inside a dictionaty for example.  
Clients.Client(contextIdClient).send("Hello!");
Matteo Gariglio
  • 482
  • 5
  • 12
4

For anyone trying to do this in asp.net core. You can use claims.

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

Any identifier can be used, but it must be unique. If you use a name identifier for example, it means if there are multiple users with the same name as the recipient, the message would be delivered to them as well. I have chosen email because it is unique to every user.

Then register the service in the startup class.

services.AddSingleton<IUserIdProvider, CustomEmailProvider>();

Next. Add the claims during user registration.

var result = await _userManager.CreateAsync(user, Model.Password);
if (result.Succeeded)
{
    await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Email, Model.Email));
}

To send message to the specific user.

public class ChatHub : Hub
{
    public async Task SendMessage(string receiver, string message)
    {
        await Clients.User(receiver).SendAsync("ReceiveMessage", message);
    }
}

Note: The message sender won't be notified the message is sent. If you want a notification on the sender's end. Change the SendMessage method to this.

public async Task SendMessage(string sender, string receiver, string message)
{
    await Clients.Users(sender, receiver).SendAsync("ReceiveMessage", message);
}

These steps are only necessary if you need to change the default identifier. Otherwise, skip to the last step where you can simply send messages by passing userIds or connectionIds to SendMessage. For more

Qudus
  • 1,440
  • 2
  • 13
  • 22
  • 1
    Yes, it works! As of 2022, this is by far the best solution. With non-built-In ASP.NET Core Identity authorization the only problem is that UserId which is used by SignalR to send to the specific user by `Clients.User(userId)` is not exist. We just need to assign it to any user's existing unique claim, as in the first code snippet in this answer. – Niksr Dec 17 '22 at 18:37
  • This is the only working (and functional) answer for ASP.NET Core, thanks! – Marco Coppola Mar 24 '23 at 10:41
2

Look at SignalR Tests for the feature.

Test "SendToUser" takes automatically the user identity passed by using a regular owin authentication library.

The scenario is you have a user who has connected from multiple devices/browsers and you want to push a message to all his active connections.

Gustavo Armenta
  • 1,525
  • 12
  • 14
  • Thanks man! But the project version 2.0 is not compiling SignalR here. : (. Unfortunately I can not access to it. – Igor Oct 22 '13 at 17:33
1

Old thread, but just came across this in a sample:

services.AddSignalR()
            .AddAzureSignalR(options =>
        {
            options.ClaimsProvider = context => new[]
            {
                new Claim(ClaimTypes.NameIdentifier, context.Request.Query["username"])
            };
        });
Greg Gum
  • 33,478
  • 39
  • 162
  • 233