ASP.NET Core 6 SignalR hub store data - c#

I'm working on a simple chat using SignalR. At the moment I'm trying associate a users connection id with their identity user. I want to do this to prevent a user from impersonating another user by manually calling the hub functions.
My hub looks something like this:
public static class MessageContext
{
public static string RECEIVE = "ReceiveMessage";
public static string REGISTER = "Register";
public static string SEND = "SendMessage";
}
public class ChatHub : Hub
{
public const string HUBURL = "/api/ChatSignal";
Dictionary<string, string> _userContext;
public ChatHub()
{
_userContext = new Dictionary<string, string>();
}
public override Task OnConnectedAsync()
{
var ConnectionId = Context.ConnectionId;
var Username = Context.User.Identity.Name;
_userContext.Add(ConnectionId, Username);
Groups.AddToGroupAsync(ConnectionId, Username);
return base.OnConnectedAsync();
}
public async Task SendAll(string user, string message)
{
var ConnectionId = Context.ConnectionId;
message = HttpUtility.HtmlEncode(message);
await Clients.All.SendAsync(MessageContext.RECEIVE, _userContext[ConnectionId], message);
}
public Task SendMessage(string sender, string receiver, string message)
{
var ConnectionId = Context.ConnectionId;
message = HttpUtility.HtmlEncode(message);
return Clients.Group(receiver).SendAsync(MessageContext.RECEIVE, _userContext[ConnectionId], message);
}
public override async Task OnDisconnectedAsync(Exception e)
{
var ConnectionId = Context.ConnectionId;
var Username = Context.User.Identity.Name;
_userContext.Remove(ConnectionId);
await base.OnDisconnectedAsync(e);
}
}
My problem is that after when I call these functions, the dictionary gets set to null. After looking around for a while I found on MSDN that, hubs are "Transient", so each hub method call is executed on a new hub instance. This is a problem if I want to save ConnectionId:Identity.Name.
How can I use a dictionary to store this data for each hub instance?

To fix the null issue remove the initialization of _userContext in the constructor.
And change the line Dictionary<string, string> _userContext; -> private static ConcurrentDictionary<string, string> _userContext = new ConcurrentDictionary<string, string>();
This is how your dictionary state will be retained across different hub instances. And ConcurrentDictionary will make it thread-safe.
But it is not a very scaleable solution. If you are really making a production-grade chat application, try to use something like Redis Cache for such state management.

Related

Azure Service IMessageSender with DI

I am using .net core project .
I implemented DI like :
builder.Services.AddSingleton<IMessageSender>(x => new
MessageSender(Environment.GetEnvironmentVariable("ServiceBusConnection"),"queueName"));
var serviceprovider = builder.Services.BuildServiceProvider();
ServiceBusUtils.Configure(serviceprovider.GetService<IMessageSender>());
Also, I have a Utility class :
public static class ServiceBusUtils
{
private static IMessageSender _messageSender;
public static void Configure(IMessageSender messageSender)
{
_messageSender = messageSender;
}
public static async Task<bool> SendMessage(ExecuteMessage<Message> message ,string queueName,string Id)
{
var message = new Message(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(Message)))
{
SessionId = Id,
};
await _messageSender.SendAsync(message);
return true;
}
}
But I cannot set the queuename with the parameter.
I want to use implementation like
ServiceBusUtils.SendMessage(message,"quename");
You have to register named clients for using with multiple queue names. The following code will give you an idea.
public void ConfigureServices(IServiceCollection services)
{
var queueNames = new string[] { "q1", "q2" };
var queueFactory = new Dictionary<string, IMessageSender>();
foreach (var queueName in queueNames)
{
queueFactory.Add(queueName, new MessageSender(Environment.GetEnvironmentVariable("ServiceBusConnection"),queueName));
}
services.AddSingleton(queueFactory);
services.AddSingleton<IServiceBus,ServiceBus>();
}
public interface IServiceBus
{
public Task SendMessage(string message, string queue);
}
public class ServiceBus : IServiceBus
{
private readonly Dictionary<string, IMessageSender> _queueFactory;
public ServiceBus(Dictionary<string, IMessageSender> queueFactory)
{
_queueFactory = queueFactory;
}
public async Task SendMessage(string message, string queue)
{
var messageSender = _queueFactory[queue];
await messageSender.SendAysnc(message);
}
}
You can pass the queue name to your send message method. This will resolve your MessageSender instance for the named queue.
_serviceBus.SendMessage("message","q1");

Tenant session is lost in IRealTimeNotifier

We are trying to achieve a scenario where user can subscribe to notifications via channels.
For example, user can subscribe to notifications via Email, SMS, Real Time Notifications, etc.
For now, in order to achieve email notifications, I am implementing the IRealTimeNotifier in this way:
public class EmailNotifier : IRealTimeNotifier, ITransientDependency
{
private readonly IEmailSender _emailSender;
private readonly ISettingManager _settingManager;
public IAbpSession AbpSession { get; set; }
public EmailNotifier(IEmailSender emailSender, ISettingManager settingManager, IAbpSession abpSession)
{
this._emailSender = emailSender;
this._settingManager = settingManager;
this.AbpSession = NullAbpSession.Instance;
}
public async Task SendNotificationsAsync(UserNotification[] userNotifications)
{
List<Task> sendEmails = new List<Task>();
string fromAddress = this._settingManager.GetSettingValueForTenant(EmailSettingNames.Smtp.UserName, Convert.ToInt32(this.AbpSession.TenantId));
foreach (UserNotification notification in userNotifications)
{
notification.Notification.Data.Properties.TryGetValue("toAddresses", out object toAddresses);
notification.Notification.Data.Properties.TryGetValue("subject", out object sub);
notification.Notification.Data.Properties.TryGetValue("body", out object content);
notification.Notification.Data.Properties.TryGetValue("toAddresses", out object toAddresses);
List<string> toEmails = toAddresses as List<string> ;
string subject = Convert.ToString(sub);
string body = Convert.ToString(content);
toEmails.ForEach(to =>
{
sendEmails.Add(this._emailSender.SendAsync(fromAddress, to, subject, body));
});
}
await Task.Run(() =>
{
sendEmails.ForEach(async task =>
{
try
{
await task.ConfigureAwait(false);
}
catch (Exception)
{
// TODO #1: Add email to background job to be sent later.
// TODO #2: Add circuit breaker to email sending mechanism
/*
Email sending is failing because of two possible reasons.
1. Email addresses are wrong.
2. SMTP server is down.
3. Break the circuit while the SMTP server is down.
*/
// TODO #3 (Optional): Notify tenant admin about failure.
// TODO #4: Remove throw statement for graceful degradation.
throw;
}
});
});
}
}
The problem is with the IAbpSession, whether I inject it via property or constructor, at the time of execution of this notifier, the response has already been returned and the TenantId in this session is null, so the email is being sent with host configurations and not tenant configuration.
Similarly, I need to implement IRealTimeNotifier for SMS. I think I can reuse the SignalRRealTimeNotifier from ABP, but the tenantId in session is being set to null.
This is where the publisher is being called:
public class EventUserEmailer : IEventHandler<EntityCreatedEventData<Event>>
{
public ILogger Logger { get; set; }
private readonly IEventManager _eventManager;
private readonly UserManager _userManager;
private readonly IAbpSession _abpSession;
private readonly INotificationPublisher _notiticationPublisher;
public EventUserEmailer(
UserManager userManager,
IEventManager eventManager,
IAbpSession abpSession,
INotificationPublisher notiticationPublisher)
{
_userManager = userManager;
_eventManager = eventManager;
_notiticationPublisher = notiticationPublisher;
_abpSession = abpSession;
Logger = NullLogger.Instance;
}
[UnitOfWork]
public virtual void HandleEvent(EntityCreatedEventData<Event> eventData)
{
// TODO: Send email to all tenant users as a notification
var users = _userManager
.Users
.Where(u => u.TenantId == eventData.Entity.TenantId)
.ToList();
// Send notification to all subscribed uses of tenant
_notiticationPublisher.Publish(AppNotificationNames.EventCreated);
}
}
Can anybody recommend a better way? Or point anything out that we are doing wrong here.
I have not thought about how to handle DI of these particular notifiers yet. For testing purposes, I have given a named injection in my module like this:
IocManager.IocContainer.Register(
Component.For<IRealTimeNotifier>()
.ImplementedBy<EmailNotifier>()
.Named("Email")
.LifestyleTransient()
);
Current ABP version: 3.7.1
This is the information I have until now. If anything is needed, you can ask in comments.
// Send notification to all subscribed uses of tenant
_notificationPublisher.Publish(AppNotificationNames.EventCreated);
If you publish notifications like this, ABP's default implementation enqueues a background job.
There is no session in a background job.
You can get the tenantId like this:
var tenantId = userNotifications[0].Notification.TenantId;
using (AbpSession.Use(tenantId, null))
{
// ...
}

Send message to specific user in signalr

I have a signalR Server(Console Application) and a client application(Asp.net MVC5)
How I can send message to specific user in OAuth Membership.
Actually I can't resolve sender user from hub request context with.
Context.User.Identity.Name
My Hub
public class UserHub : Hub
{
#region Hub Methods
public void LoggedIn(string userName, string uniqueId, string ip)
{
Clients.All.userLoggedIn(userName, uniqueId, ip);
}
public void LoggedOut(string userName, string uniqueId, string ip)
{
var t = ClaimsPrincipal.Current.Identity.Name;
Clients.All.userLoggedOut(userName, uniqueId, ip);
}
public void SendMessage(string sendFromId, string userId, string sendFromName, string userName, string message)
{
Clients.User(userName).sendMessage(sendFromId, userId, sendFromName, userName, message);
}
#endregion
}
Start hub class(Program.cs)
class Program
{
static void Main(string[] args)
{
string url = string.Format("http://localhost:{0}", ConfigurationManager.AppSettings["SignalRServerPort"]);
using (WebApp.Start(url))
{
Console.WriteLine("Server running on {0}", url);
Console.ReadLine();
}
}
}
Keep connectionId with userName by creating a class as we know that Signalr only have the information of connectionId of each connected peers.
Create a class UserConnection
Class UserConnection{
public string UserName {set;get;}
public string ConnectionID {set;get;}
}
Declare a list
List<UserConnection> uList=new List<UserConnection>();
pass user name as querystring during connecting from client side
$.connection.hub.qs = { 'username' : 'anik' };
Push user with connection to this list on connected mthod
public override Task OnConnected()
{
var us=new UserConnection();
us.UserName = Context.QueryString['username'];
us.ConnectionID =Context.ConnectionId;
uList.Add(us);
return base.OnConnected();
}
From sending message search user name from list then retrive the user connectionid then send
var user = uList.Where(o=>o.UserName ==userName);
if(user.Any()){
Clients.Client(user.First().ConnectionID ).sendMessage(sendFromId, userId, sendFromName, userName, message);
}
DEMO
All of these answers are unnecessarily complex. I simply override "OnConnected()", grab the unique Context.ConnectionId, and then immediately broadcast it back to the client javascript for the client to store and send with subsequent calls to the hub server.
public class MyHub : Hub
{
public override Task OnConnected()
{
signalConnectionId(this.Context.ConnectionId);
return base.OnConnected();
}
private void signalConnectionId(string signalConnectionId)
{
Clients.Client(signalConnectionId).signalConnectionId(signalConnectionId);
}
}
In the javascript:
$(document).ready(function () {
// Reference the auto-generated proxy for the SignalR hub.
var myHub = $.connection.myHub;
// The callback function returning the connection id from the hub
myHub.client.signalConnectionId = function (data) {
signalConnectionId = data;
}
// Start the connection.
$.connection.hub.start().done(function () {
// load event definitions here for sending to the hub
});
});
In order to be able to get "Context.User.identity.Name", you supposed to integrate your authentication into OWIN pipeline.
More info can be found in this SO answer: https://stackoverflow.com/a/52811043/861018
In ChatHub Class Use This for Spacific User
public Task SendMessageToGroup(string groupName, string message)
{
return Clients.Group(groupName).SendAsync("Send", $"{Context.ConnectionId}: {message}");
}
public async Task AddToGroup(string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
await Clients.Group(groupName).SendAsync("Send", $"{Context.ConnectionId} has joined the group {groupName}.");
}
public async Task RemoveFromGroup(string groupName)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
await Clients.Group(groupName).SendAsync("Send", $"{Context.ConnectionId} has left the group {groupName}.");
}

SignalR Mapping user to connectionID with custom authentication

I am trying to implement a chat application. Users should be able to send messages to specific users. In order to do that I need to map usernames to their connectionIDs.
My client is using custom authentication. Username is stored in Session["User"]. Therefore I don't have the username stored in Context.User.Identity.Name, which is where SignalR normally takes the username from.
How else can I get the username of the logged in user so that I can map it to Context.ConnectionID?
Here is some sample implementation of public class ChatHub : Hub I found on the web.
private readonly static ConnectionMapping<string> _connections =
new ConnectionMapping<string>();
public void SendChatMessage(string who, string message)
{
string name = Context.User.Identity.Name;
foreach (var connectionId in _connections.GetConnections(who))
{
Clients.Client(connectionId).addChatMessage(name + ": " + message);
}
}
public override Task OnConnected()
{
string name = Context.User.Identity.Name;
_connections.Add(name, Context.ConnectionId);
return base.OnConnected();
}
Pass your username using query string.
Client
First set query string
For auto generated proxy
$.connection.hub.qs = { 'username' : 'anik' };
For manual proxy
var connection = $.hubConnection();
connection.qs = { 'username' : 'anik' };
then start hub connection
Server
public override Task OnConnected()
{
var username= Context.QueryString['username'];
return base.OnConnected();
}

Why Context.User.Identity.Name is null in Windows Form Hub Context?

I need to pass User.Identity.Name to Windows Form client.
Method
public override Task OnConnected() {
string userName = Context.User.Identity.Name;
string connectionId = Context.ConnectionId;
var user = Users.GetOrAdd(userName, _ => new User {
Name = userName,
ConnectionIds = new HashSet<string>()
});
lock (user.ConnectionIds) {
user.ConnectionIds.Add(connectionId);
if (user.ConnectionIds.Count == 1) {
Clients.Others.userConnected(userName);
}
}
return base.OnConnected();
}
But Context.User.Identity.Name is null? Why? How to solve it?
It looks like you are trying to get the username when connecting to the hub. I solved a similar issue by passing the username from my client. It also sounds like you are making use of the SignalR .NET client. Give this a try
Client
Connection = new HubConnection("http://.../", new Dictionary<string, string>
{
{ "UserName", WindowsIdentity.GetCurrent().Name }
});
Hub
public override Task OnConnected()
{
string userName = Context.QueryString["UserName"]
}

Categories

Resources