Michał Turczyn
Michał Turczyn

Reputation: 37367

Ways to persist SignalR connection

I am creating web application, which let's users to communicate with so called chat.

In order to enable such communication, I use SignalR library. I create connection at first visit to my page (main page). So JS code, which creates connection, creates variable used to configure connection.

Then user enters chat room, which is different page, so new JSs are loaded etc. The one, which held connection variable is now unavailable. But now I need that connection to send messages in my chat room.

So this variable must be "transfered" over to next scripts.

So, what would be the way to actually persist connection through whole session on the website?

Upvotes: 1

Views: 2951

Answers (2)

Michał Turczyn
Michał Turczyn

Reputation: 37367

Finally I got my answer.

It was suggested that I should use ASP NET Identity, which is very valid, but I already created simple authentication and users management. It isn't safe and as half good as ASP NET Identity (I looked throught it and got my head around it), but it's just personal project, not production one, so if it evolves I might switch to Identity or even implement one myself ;) But it's not the case.

It required little bit of extra steps, so:

  1. I needed to enable sessions in ASP.NET Core, I used this article for this purpose. Having that, I can persist my user login and provide it to user ID provider for signalR

  2. Adding cutsom user ID provider for SignalR in .NET:

I needed to create such class

public class UserIdProvider : IUserIdProvider
{
  public static readonly string SESSION_LOGIN_KEY = "loggedUser";
  private readonly IHttpContextAccessor _httpContextAccessor;
  
  public UserIdProvider(IHttpContextAccessor httpContextAccessor) 
  {
    _httpContextAccessor = httpContextAccessor;
  }

  public string GetUserId(HubConnectionContext connection)
  {
    var session = _httpContextAccessor.HttpContext.Session;
    session.TryGetValue(SESSION_LOGIN_KEY, out byte[] loginBA);
    if (loginBA == null)
    {
      return null;
    }

    return new string(loginBA.Select(b => (char)b).ToArray());
  }
}

So, after logging I can set login in Session, so it becomes "state" variable, and use it in above class.

Also, one need to add it in ASP services, like below (in Startup.ConfigureServices):

services.AddSingleton<IUserIdProvider, UserIdProvider>();

There also one thing, which still need to be set: in UserIdProvider we need to access Session through HttpContext. In order to use HttpContextwe need to specify it like below (also in Startup.ConfigureServices):

services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

which passes HttpContextAccessor to serices constructors.

After all this, you can access users in SignalR Hub with their logins, which is set in Context.UserIdnentifier

This also enables sending messages to specific user, just by passing their login (frontend client just chooses user), like below:

public async Task SendMessage(string message, string user)
{
  await Clients.User(user).SendAsync("ReceiveMessage", message).ConfigureAwait(false);
}

NOTE There was one problem though. Browsers on computer didn't persist sessions, which I solved by (also in Startup.ConfigureServices):

services.Configure<CookiePolicyOptions>(options =>
{
  // This lambda determines whether user consent for non-essential cookies is needed for a given request.
  options.CheckConsentNeeded = context => false; // <~~~~ This needs to be set to false
  options.MinimumSameSitePolicy = SameSiteMode.None;
});

Without it, you need to be carefull with cookies, if you don't accept them on a site, it won't work, as user's login won't be persisted.

Upvotes: 2

T&#226;n
T&#226;n

Reputation: 1

On the server side, you can send the message to specific user via user id:

var user = await _userManager.GetUserAsync(Context.User);

await Clients.User(user.Id).SendCoreAsync("msg", new object[] { user.Id, user.Email });

On the client side, whenever the connection is started and the hub msg is listened, the user will receive the message:

connection.on('msg', function (...data) {
    console.log('data:', data);
});

By sending message(s) via user id, you don't need to care where the target user is.


public class ChatHub : Hub
{
    private IUserManager<ApplicationUser> _userManager;

    public ChatHub(UserManager<ApplicationUser> userManager)
    {
        _userManager = userManager;
    }

    public async Task GetInfo()
    {
        var user = await _userManager.GetUserAsync(Context.User);

        await Clients.User(user.Id).SendCoreAsync("msg", new object[] { user.Id, user.Email });
    }
}

Upvotes: 1

Related Questions