I am building an ASP.NET Core application with Angular, and I am trying to implement a basic, one-on-one chat functionality using SignalR.
Since I want to allow one-on-one chatting and make chat messages persistent at some point, I'd like to be able to map User Id to SignalR Connection Id and send messages directly to a user based on their Id.
Now, all the examples I've seen use code within a Hub, which makes sense since Hub keeps track of Clients and their connection ids. But other logic that I'll have starts and ends inside my Controller, of course, and I can't call a hub directly from a controller.
Since SignalR is supposed to be relying on Identity by default, his is what I've tried so far:
[Route("send")]
[HttpPost]
public async Task<IActionResult> SendRequest([FromBody] Chat.Models.Message message)
{
var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)); // I'd like to register this (current) userId with SignalR connection Id somehow
var sender = new ChatSender { Id= userId, Name = user.FullName };
await _hubContext.Clients.All.SendAsync("ReceiveMessage", sender, message.MessageText); // can't extend this, nor access hub directly
//var recipient = _hubContext.Clients.User(message.To); // message.To is a Guid of a recepient
//await recipient.SendAsync("ReceiveMessage", sender, message.MessageText);
return Ok();
}
The code above works as a broadcast, but if I replace the _hubContext.Clients.All.SendAsync with two commented out lines below, it does nothing.
Any suggestions?
SignalR mapping User Id to Connection Id
To map the user id /name with the connection id, you need to use the Hub's OnConnectedAsync method to get the userid/username and the connection id, then insert them into database. Code like below:
[Authorize]
public class ChatHub : Hub
{
private readonly ApplicationDbContext _context;
public ChatHub(ApplicationDbContext context)
{
_context = context;
}
public override async Task OnConnectedAsync()
{
//get the connection id
var connectionid = Context.ConnectionId;
//get the username or userid
var username = Context.User.Identity.Name;
var userId = Guid.Parse(Context.User.FindFirstValue(ClaimTypes.NameIdentifier));
//insert or updatde them into database.
var CId = _context.UserIdToCId.Find(userId);
CId.ConnectionId = connectionid;
_context.Update(CId);
await _context.SaveChangesAsync();
await base.OnConnectedAsync();
}
map User Id to SignalR Connection Id and send messages directly to a
user based on their Id.
You can pass your receiver id or name from your client to the SendRequest method, according to the receiver id or name to find the signalr connection id from database. After find the receiver's connection id, then use the following code to send message:
await _hubContext.Clients.Client("{receiver connection id}").SendAsync("ReceiveMessage", message);
the more code you can refer to:
public class HomeController : Controller
{
private readonly IHubContext<ChatHub> _hubContext;
private readonly ApplicationDbContext _context;
public HomeController( IHubContext<ChatHub> hubContext, ApplicationDbContext context)
{
_hubContext = hubContext;
_context = context;
}
...
/// <summary>
///
/// </summary>
/// <param name="receiver">receiver id or name</param>
/// <param name="message">message </param>
/// <returns></returns>
[Route("send")]
[HttpPost]
public async Task<IActionResult> SendRequest([FromBody] string receiver, Message message)
{
//1. according to the receiver id or name to find the signalr connection id
//To map the user id /name with the connection id, you need to use the Hub's OnConnectedAsync method to get the userid/username and the connection id.
//then insert them into database
//2. After find the receiver's connection id, then use the following code to send message.
await _hubContext.Clients.Client("{receiver connection id}").SendAsync("ReceiveMessage", message);
return Ok();
}
Note When the receiver is disconnected,remember to delete the receiver's connection id from database, avoid sending error.
Besides, you can refer to How can I make one to one chat system in Asp.Net.Core Mvc Signalr? to know more.
Related
I use controllers and signalr in my chat microservice. I think all actions that need to invoke methods on clients through hub (like joining group, sending message, leaving group, reading message, deleting message) can be implemented in controllers or hub. But I haven't founded explanations with pros and cons about these approaches and have seen many articles where both of them are used. And I don't understand when I should create hub endpoint and when controller endpoint with hub usage inside.
Could someone explain please which approach is better and why?
Or maybe the best practice is something else like invoke hub methods in mediatr INotificationHandler?
It's example of joining to group chat.
Method in controller with hub usage (with this approach the class ChatHub will be empty):
public ChatMemberController(IMediator sender, IMapper mapper, IHubContext<ChatHub, IChatHub> chatHub, IConnectionIdProvider connectionIdProvider)
{
_sender = sender;
_mapper = mapper;
_chatHub = chatHub;
_connectionIdProvider = connectionIdProvider;
}
public async Task<IActionResult> Create([Required][FromBody] CreateChatMemberDto dto)
{
// business logic
CreateChatMemberCommand command = new CreateChatMemberCommand(_mapper.Map<UseCases.Dto.CreateChatMemberDto>(dto));
int chatMemberId = await _sender.Send(command);
// hub method
string groupName = GetGroupName(dto.ChatId);
await _chatHub.Clients.Group(groupName).JoinGroup(dto.UserId, dto.ChatId);
// join hub group
List<string> connectionIds = _connectionIdProvider.GetConnectionIds(dto.UserId);
foreach (var connectionId in connectionIds)
{
await _chatHub.Groups.AddToGroupAsync(connectionId, groupName);
}
return new ObjectResult(chatMemberId) { StatusCode = StatusCodes.Status201Created };
}
Method in hub:
public async Task<int> JoinGroup([Range(1, int.MaxValue)] int chatId)
{
if (!int.TryParse(this.Context.UserIdentifier, out int currentUserId))
throw new Exception("UserIdentifier is not integer.");
// business logic
CreateChatMemberCommand command = new CreateChatMemberCommand(new CreateChatMemberDto { ChatId = chatId, UserId = currentUserId });
int chatMemberId = await _sender.Send(command);
// hub method
string groupName = this.GetGroupName(chatId);
await this.Clients.Group(groupName).JoinGroup(currentUserId, chatId);
// join hub group
await this.Groups.AddToGroupAsync(this.Context.ConnectionId, groupName);
return chatMemberId;
}
it would be better if your application doesn't rely on signalR, I recommend designing your application to perform its primary functions using traditional API, this is because the connection to the SignalR hub is less reliable and harder to secure compared to the traditional request-response type.
it is advisable to use SignalR to send notifications not for critical tasks such as data persistence. Having a separate API makes it easier to have a backup plan in case the SignalR connection fails.
I'm thinking that perhaps my entire idea of how to approach this is wrong, so let me explain what I'm trying to do.
I have a UserId that is a property contained within my JWT token.
On many of my REST endpoints, I need to read that UserId to use it within my DB queries.
I implemented a filter which intercepts all of my calls and decodes my JWT and assigns the UserId value into a static Globals class that I had created.
I just realised now though, that that class is GLOBAL. As in, the values are actually shared across the entire server for anybodies REST requests.
I intended for the value to essentially just be transiently available for the duration of each individual request.
How can I change my implementation so that I can globally access the UserId contained in the JWT token for the current request.
My suggestion is to make some kind of abstraction e.g ICurrentUser and make an implementation, which will take UserId from HttpContext.
// Define in Domain/Application project
public interface ICurrentUser
{
public string? Id { get; set; }
}
// Implement in ASP.NET project
public class CurrentUser : ICurrentUser
{
public CurrentUser(IHttpContextAccessor contextAccessor)
{
var user = contextAccessor.HttpContext?.User;
if (user == null)
{
return;
}
Id = user.FindFirstValue(ClaimTypes.NameIdentifier) ?? user.FindFirstValue(JwtClaimTypes.Subject);
}
public string? Id { get; set; }
}
Also, don't forget to add .AddHttpContextAccessor() call for you services
If you want something to be available for the duration of an individual request I would recommend using a service registered as scoped see Scoped Services
But lets start from the beginning. First implement a service itself like:
public UserService : IUserService
{
private readonly IHttpContextAccessor _accessor;
/// inject the `IHttpContextAccessor` to access the actual
/// request / token / headers etc.
public UserService(IHttpContextAccessor accessor)
{
_accessor = accessor;
}
public async Task<string> GetUserIdAsync()
{
var userId = await GetUserIdFromTokenAsync();
return userId;
}
private Task<string> GetUserIdFromTokenAsync()
{
/// Add your logic here to get or parse the
/// user id from the token or do some other stuff to get the user id.
/// ... or get the user id from the current User object claim
/// depends on your auth settings `_accessor.HttpContext?.User`
var token = _accessor... // from headers?
return userId;
}
}
/// Always use an interface to make it well testable and mockable for unit tests
public interface IUserService
{
Task<string> GetUserIdAsync();
}
Then in your dependency injection part (Startup.cs or Program.cs depends which tempate you have selected).
/// register the `IHttpContextAccessor` to be able to inject it.
services.AddHttpContextAccessor();
/// register your `UserService` as scoped!
services.AddScoped<IUserService, UserService>();
Now you can use this in all your services and controllers (which are at least also registered as scoped). This will resolve the service per request.
/// In a data service
class YourDataService
{
private readonly IUserService _userService;
/// Inject the `IUserService` wherever you need it now to
/// receive the current user Id.
public YourDataService(IUserService service)
{
_userService = service
}
public async Task DoYourQueryStuffAsync()
{
var userId = await _userService.GetUserIdAsync();
/// Your application logic with the provided userId
///
}
}
/// The same applies for a controller
[ApiController]
[Route("values")]
public class ValuesController : ControllerBase
{
private readonly IUserService _userService;
/// Inject the `IUserService` wherever you need it now to
/// receive the current user Id.
public ValuesController(IUserService service)
{
_userService = service
}
[Authorized]
[HttpGet]
public async Task<IActionResult> Query()
{
var userId = await _userService.GetUserIdAsync();
/// Your application logic with the provided userId
///
var queryresult = await ...
return Ok(queryresult);
}
}
Notes at the end:
Do not fall into the trap to consume scoped services from a singleton service this is not working because singletons are persistent without the request context.
Documentation links:
ASP.net Core Dependency Injection
UserId in Bearer
I have an MVC application on .NET core 2.2. I want to send instant messages to users on a specified date. When I send it to the Hub with Javascript, it can be processed. But I cannot check the date from the database and trigger the Hub. I built the structure that will check every minute from DB from the address below. In this case, the connection state appears as a disconnect in the HubConnection section. I'm following the instructions at https://learn.microsoft.com/th-th/aspnet/core/signalr/background-services?view=aspnetcore-2.2.
Hub Class Content:
public class TestHub: Hub
{
public async Task SendMessage(string message)
{
await Clients.All.SendAsync("ReceiveMessage", message);
}
}
public class Worker : BackgroundService
{
private HubConnection _connection;
_connection = new HubConnectionBuilder().WithUrl(myUrl).Build();
_connection.StartAsync();
//I will do database operations here
_connection.InvokeAsync("SendMessage", "test"); //At this point, my connection state is in a disconnected state and my parameter cannot be passed to my "SendMessage" method on the hub.
}
I can't send a notification message to a specific user from the controller.
My controller:
public class SomeController : ControllerBase
{
private readonly IHubContext<NotificationHub> _hubContext;
public SomeController(IHubContext<NotificationHub> hubContext)
{
_hubContext = hubContext;
}
public async Task SomeMethod()
{
//it works
await _hubContext.Clients.All.SendAsync("sendMessage", msg);
//it doesn't work
await _hubContext.Clients.User(userId).SendAsync("sendMessage", msg);
//it also doesn't work
await _hubContext.Clients.Client(userId).SendAsync("sendMessage", msg);
}
}
Unfortunately, IHubContext has some imperfections. In the Clients property is available only Clients. All - that is, we can send a message only to all clients and there are no such properties as Other or Caller that are available in the hub class. Also, we cannot get a connection id from IHubContext.
Is there some else method to do it? Or is it possible only with SQL Dependency?
I have a signalr hub with a function like this
public void SendSurveyNote(int surveyId,List<string> users){}
Here I want to add all users of the list into a group "Survey_" + surveyId Then sending the group a message. But I only have the user id but joining a group requires a connection id. So how could I manage that.
Also I wonder would it be a performance issue to send each user a message without a group?
I call the function above when I add a new Survey like this
private static HubConnection hubConnection = new HubConnection(ConfigurationManager.AppSettings["BaseUrl"]);
private static IHubProxy hubProxy = hubConnection.CreateHubProxy("myHub");
await hubConnection.Start();
hubProxy.Invoke("SendSurveyNote", model.Id, users);
thanks
You have access to the connection ID within Context. You'll want to establish groups within OnConnected. Observe the following implementation on your hub, where we will call it MyHub. We'll also group by Context.User.Identity.Name to establish a unique group per user, but this could be any value you wish to group by.
public class MyHub: Hub
{
public override Task OnConnected()
{
Groups.Add(Context.ConnectionId, Context.User.Identity.Name)
return base.OnConnected();
}
}
See Working with Groups in SignalR for more information