How to send message to only caller client in SignalR? - signalr

Below is my SignalR Hub class code.
public class ChatHub : Hub
{
public void Send(string name, string message)
{
// Call the addNewMessageToPage method to update clients.
Clients.All.addNewMessageToPage(name, message);
}
public async void webAPIRequest()
{
HttpClient client = new HttpClient();
HttpResponseMessage response = await client.GetAsync("https://jsonplaceholder.typicode.com/posts");
//Clients.All.addWebAPIResponseToPage(response);
Clients.Caller.addWebAPIResponseToPage(response);
await Task.Delay(1000);
response = await client.GetAsync("http://www.google.com");
Clients.Caller.addWebAPIResponseToPage(response);
//Clients.All.addWebAPIResponseToPage(response);
await Task.Delay(1000);
response = await client.GetAsync("https://jsonplaceholder.typicode.com/posts?userId=1");
//Clients.All.addWebAPIResponseToPage(response);
Clients.Caller.addWebAPIResponseToPage(response);
}
}
As per my understanding ,
Clients.Caller.addWebAPIResponseToPage(response);
sends message only to caller client , whereas
Clients.All.addWebAPIResponseToPage(response);
sends the message to all the clients.
Is my understanding correct ?
If No , then what method needs to be called to send message only to caller client.

Yes your understanding is correct. Read it here
https://learn.microsoft.com/en-us/aspnet/signalr/overview/guide-to-the-api/hubs-api-guide-server#selectingclients
You can use caller, you can provide current user connection id and send message to that or I have seen a group called self in some places which keeps user logged in from various devices and send message to that.
For example if you are logged in on a desktop and on mobile as well then you will have two connection IDs but you are same user. You can add this user to a self_username_unique_group_name kind of group and then send a message to that group which will be sent to all devices where user is connected.
You can also manage connection IDs for a single user in a separate table and send message to all of those connection IDs if you want.
Too much flexibility and magic
Enjoy

I found this to work quite well where ConnectionMapping is described in https://learn.microsoft.com/en-us/aspnet/signalr/overview/guide-to-the-api/mapping-users-to-connections
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped<SomeService>();
services.AddScoped<SessionService>();
services.AddScoped<ProgressHub>();
}
}
public class SomeService
{
ProgressHub _hub;
public SomeService(ProgressHub hub)
{
_hub = hub;
}
private async Task UpdateProgressT(T value)
{
_hub.Send(value);
}
}
public class ProgressHub : Hub
{
private readonly static ConnectionMapping<string> _connections = new ConnectionMapping<string>();
private readonly IHubContext<ProgressHub> _context;
private readonly SessionService _session;
public ProgressHub(IHubContext<ProgressHub> context, SessionService session)
{
_context = context;
_session = session;
}
public override Task OnConnectedAsync()
{
_connections.Add(_session.SiteId, Context.ConnectionId);
return base.OnConnectedAsync();
}
public override Task OnDisconnectedAsync(Exception exception)
{
_connections.Remove(_session.SiteId, Context.ConnectionId);
return base.OnDisconnectedAsync(exception);
}
public async Task Send(object data)
{
foreach (var connectionId in _connections.GetConnections(_session.SiteId))
{
await _context.Clients.Client(connectionId).SendAsync("Message", data);
}
}
}
public class SessionService
{
private readonly ISession _session;
public SessionService(IHttpContextAccessor accessor)
{
_session = accessor.HttpContext.Session;
if (_session == null) throw new ArgumentNullException("session");
}
public string SiteId
{
get => _session.GetString("SiteId");
set => _session.SetString("SiteId", value);
}
}

Related

Azure Service Bus not all messages received in hosted service web app

Inside a .net web app, I set up a hosted service to receive messages from an Azure Service Bus topic. The problem is that not all messages are received, only an arbitrary amount (e.g. of 20 messages only 12 are received). The rest of them ended up in the dead letter queue. This happens when the messages are send simultaneously.
I tried the following steps to solve this:
Increased the amount of maximum concurrent calls, which helped but didn't provide a guarantee
Added a prefetch count
I also tried to send messages via the functionality in the service bus resource in Azure. 500 messages, no interval time --> didn't work (for all messages). 500 messages, 1s interval time, all messages were received.
I just don't understand why the receiver is not recieving all of the messages.
I want to build a event-driven architecture and cannot make it a gamble if all messages will be processed.
Startup.cs
...
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IServiceBusTopicSubscription,ServiceBusSubscription>();
services.AddHostedService<WorkerServiceBus>();
}
...
WorkerService.cs
public class WorkerServiceBus : IHostedService, IDisposable
{
private readonly ILogger<WorkerServiceBus> _logger;
private readonly IServiceBusTopicSubscription _serviceBusTopicSubscription;
public WorkerServiceBus(IServiceBusTopicSubscription serviceBusTopicSubscription,
ILogger<WorkerServiceBus> logger)
{
_serviceBusTopicSubscription = serviceBusTopicSubscription;
_logger = logger;
}
public async Task StartAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Starting the service bus queue consumer and the subscription");
await _serviceBusTopicSubscription.PrepareFiltersAndHandleMessages().ConfigureAwait(false);
}
public async Task StopAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Stopping the service bus queue consumer and the subscription");
await _serviceBusTopicSubscription.CloseSubscriptionAsync().ConfigureAwait(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual async void Dispose(bool disposing)
{
if (disposing)
{
await _serviceBusTopicSubscription.DisposeAsync().ConfigureAwait(false);
}
}
}
ServiceBusSubscription.cs
public class ServiceBusSubscription : IServiceBusTopicSubscription
{
private readonly IConfiguration _configuration;
private const string TOPIC_PATH = "test";
private const string SUBSCRIPTION_NAME = "test-subscriber";
private readonly ILogger _logger;
private readonly ServiceBusClient _client;
private readonly IServiceScopeFactory _scopeFactory;
private ServiceBusProcessor _processor;
public ServiceBusBookingsSubscription(IConfiguration configuration,
ILogger<ServiceBusBookingsSubscription> logger,
IServiceScopeFactory scopeFactory)
{
_configuration = configuration;
_logger = logger;
_scopeFactory = scopeFactory;
var connectionString = _configuration.GetConnectionString("ServiceBus");
var serviceBusOptions = new ServiceBusClientOptions()
{
TransportType = ServiceBusTransportType.AmqpWebSockets
};
_client = new ServiceBusClient(connectionString, serviceBusOptions);
}
public async Task PrepareFiltersAndHandleMessages()
{
ServiceBusProcessorOptions _serviceBusProcessorOptions = new ServiceBusProcessorOptions
{
MaxConcurrentCalls = 200,
AutoCompleteMessages = false,
PrefetchCount = 1000,
};
_processor = _client.CreateProcessor(TOPIC_PATH, SUBSCRIPTION_NAME, _serviceBusProcessorOptions);
_processor.ProcessMessageAsync += ProcessMessagesAsync;
_processor.ProcessErrorAsync += ProcessErrorAsync;
await _processor.StartProcessingAsync().ConfigureAwait(false);
}
private async Task ProcessMessagesAsync(ProcessMessageEventArgs args)
{
_logger.LogInformation($"Received message from service bus");
_logger.LogInformation($"Message: {args.Message.Body}");
var payload = args.Message.Body.ToObjectFromJson<List<SchedulerBookingViewModel>>();
// Create scoped dbcontext
using var scope = _scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<dbContext>();
// Process payload
await new TestServiceBus().DoThings(payload);
await args.CompleteMessageAsync(args.Message).ConfigureAwait(false);
}
private Task ProcessErrorAsync(ProcessErrorEventArgs arg)
{
_logger.LogError(arg.Exception, "Message handler encountered an exception");
_logger.LogError($"- ErrorSource: {arg.ErrorSource}");
_logger.LogError($"- Entity Path: {arg.EntityPath}");
_logger.LogError($"- FullyQualifiedNamespace: {arg.FullyQualifiedNamespace}");
return Task.CompletedTask;
}
public async ValueTask DisposeAsync()
{
if (_processor != null)
{
await _processor.DisposeAsync().ConfigureAwait(false);
}
if (_client != null)
{
await _client.DisposeAsync().ConfigureAwait(false);
}
}
public async Task CloseSubscriptionAsync()
{
await _processor.CloseAsync().ConfigureAwait(false);
}
}
So this is how we solved the problem. It was related to the message lock duration, which is set for the Azure Resource in the portal. Previous Value: 30s. New Value: 3min.

HttpClientFactory HttpClient Cannot access a disposed object

I am implementing a service for posting data to an external RestAPI.
What I did as below:
Service definition:
public class ExternalOutputService : IExternalOutputService
{
private readonly HttpClient _httpClient;
public ExternalOutputService(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task<object> Send(object data, string baseAddress, string uri)
{
try
{
HttpResponseMessage response = await _httpClient.PostAsJsonAsync(uri, data);
response.EnsureSuccessStatusCode();
}
catch (Exception ex) {
Console.Write(ex.Message);
}
return response.Content;
}
}
Add services.AddHttpClient<IExternalOutputService, ExternalOutputService>(); in Startup
Use the injected the service and call the Send method.
public class ConfigurableOutput
{
private readonly IExternalOutputService _externalOutputService;
public ConfigurableOutput(IExternalOutputService externalOutputService)
{
_externalOutputService = externalOutputService;
}
public override async Task<object> Run(object input)
{
await _externalOutputService.Send(input.data, "URI address");
}
}
But when I run it and hit the httpclient send line, it would throw an exception with 'Cannot access a disposed object'
Anyone has idea or advice?
Hi guys, I finally find the issue.
In another DI extension class, the class has already been registered.
context.Services.AddTransient<IExternalOutputService, ExternalOutputService>();
So removed this line and only keeps
services.AddHttpClient<IExternalOutputService, ExternalOutputService>();
It is all good now.

Cannot access a disposed object. with SignalR and Timer Manager

I wanna make my function send data as a real time (every 2 seconds or once there is change in the database table ) but the problem is there is Exception keep appread in my below code.
The exception details are:
'Cannot access a disposed object.
public class MyHub : Hub
{
private readonly IRepository<MyTable, long> _repository;
private readonly IUnitOfWorkManager _unitOfWorkManager;
public HCHub(IUnitOfWorkManager unitOfWorkManager,IRepository<MyTable, long> repository)
{
_repository = repository;
_unitOfWorkManager = unitOfWorkManager;
}
public void Get(TestDto testDto)
{
try {
using (var unitOfWork = _unitOfWorkManager.Begin())
{
var result= _repository.GetDbContext().Set<MyTable>()
.Include(x => x.list)
.ThenInclude(x => x.list2)
.ThenInclude(x => x.obj).ToList();
new TimerManager(async () =>
await Clients.All.SendAsync("listen", result) //<====== in this Line the exception occured
);
}
}
catch(Exception ex)
{
throw new UserFriendlyException(ex.InnerException.Message.ToString());
}
}
and TimerManager Code is
public class TimerManager
{
private Timer _timer;
private AutoResetEvent _autoResetEvent;
private Action _action;
public DateTime TimerStarted { get; }
public TimerManager(Action action)
{
_action = action;
_autoResetEvent = new AutoResetEvent(false);
_timer = new Timer(Execute, _autoResetEvent, 1000, 2000);
TimerStarted = DateTime.Now;
}
public void Execute(object stateInfo)
{
_action();
if ((DateTime.Now - TimerStarted).Seconds > 60)
{
_timer.Dispose();
}
}
}
So the problem is in Timer Manager or in myHub or the way that I'm simulate the realtime data by TimerManager is not acceptable ?!
Once you exit the hub method you aren't guaranteed to be able to access the Clients property. If you want to do something like that, you should inject an IHubContext<THub> into your Hubs constructor and use that instead. You can read more about IHubContext in https://learn.microsoft.com/aspnet/core/signalr/hubcontext?view=aspnetcore-3.1#get-an-instance-of-ihubcontext

Calling Web-Api from a SignalR Hub

I am creating a WebApi server with integrated SignalR Hubs. For simplicity's sake I am using a Controller which is operating on a List.
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
public static List<string> Source { get; set; } = new List<string>();
public static int counter = 0;
private IHubContext<ValuesHub, IValuesClient> hubContext;
public ValuesController(IHubContext<ValuesHub, IValuesClient> hub)
{
Source.Add("bla" + counter);
counter++;
Source.Add("bla" + counter);
counter++;
this.hubContext = hub;
}
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return Source;
}
// GET api/values/x
[HttpGet("{id}")]
public ActionResult<string> Get(int id)
{
return Source[id];
}
// POST api/values
[HttpPost]
public void Post([FromBody] string value)
{
Source.Add(value);
}
// PUT api/values/x
[HttpPut("{id}")]
public void Put(int id, [FromBody] string value)
{
Source[id] = value;
}
// DELETE api/values/x
[HttpDelete("{id}")]
public void Delete(int id)
{
var item = Source[id];
Source.Remove(item);
Console.WriteLine("Outgoing message!");
hubContext.Clients.All.ReceiveMessage("Message incoming", "Blaaaaa");
}
}
}
My Hub doesn't do anything special yet:
public interface IValuesClient
{
Task ReceiveMessage(string value, string message);
Task ReceiveMessage(string message);
}
public class ValuesHub : Hub<IValuesClient>
{
// private static ValuesController ctrl = Glo
public override async Task OnConnectedAsync()
{
await Groups.AddToGroupAsync(Context.ConnectionId, "Users");
Console.WriteLine("Client connected - Client-Id: {0}", Context.ConnectionId);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, "Users");
Console.WriteLine("Client disconnected - Client-Id: {0}", Context.ConnectionId);
Console.WriteLine("Disconnection due to: {0}", exception);
await base.OnDisconnectedAsync(exception);
}
public async Task MessageToAll(string user, string message)
{
Console.WriteLine("SendMessage - User: {0} - Message: {1}", user, message);
await Clients.All.ReceiveMessage(user, message);
}
public async Task MessageToCaller(string message)
{
Console.WriteLine("SendMessageToCaller: {0}", message);
await Clients.Caller.ReceiveMessage(message);
}
}
}
Also for simplicity's sake I will not go into detail why I want to achieve this, but I want the server to wait for a certain amount of time and then delete the according value, after a disconnection is detected. Let's say I want to simply delete the first element in my Source list.
How would I access the according Controller-functions from inside my OnDisconnectedAsync function?
One idea I came up with is to create a HttpClient inside my Hub and let the Hub act as a client here by calling e. g. DELETE: http://localhost:5000/api/values/0. I have to admit this sounds like a rather horrible approach, though.
So If I understand your problem is that you are having is that you want to access the methods on the controller from your hubs?
If this is the case - It seems to me that you have a fundamental design flaw. I would create a service that handles all the things your controller is doing, and then inject this service directly into the hub. Then you can use that service directly in the hub on the overrides and operate on your list . If this is unclear I can Provide an example.
public class ValuesHub : Hub<IValuesClient>
{
IListService _listService;
public ValuesHub (IListService listService)
{
_listService = listService;
}
public override async Task OnConnectedAsync()
{
await Groups.AddToGroupAsync(Context.ConnectionId, "Users");
Console.WriteLine("Client connected - Client-Id: {0}", Context.ConnectionId);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, "Users");
Console.WriteLine("Client disconnected - Client-Id: {0}", Context.ConnectionId);
Console.WriteLine("Disconnection due to: {0}", exception);
//Call your methods here.
_listService.RemoveFirstElement();
await base.OnDisconnectedAsync(exception);
}
public async Task MessageToAll(string user, string message)
{
Console.WriteLine("SendMessage - User: {0} - Message: {1}", user, message);
await Clients.All.ReceiveMessage(user, message);
}
public async Task MessageToCaller(string message)
{
Console.WriteLine("SendMessageToCaller: {0}", message);
await Clients.Caller.ReceiveMessage(message);
}
}
}
Thats your hub - See service example below
public class ListService : IListService
{
public void RemoveFirstElement()
{
//Delete Your Element here
}
}
public interface IListService
{
void RemoveFirstElement();
}
And then your startup.cs
services.AddSingleton<IListService,ListService>();

Signalr - Associating usernames with connectionIds

Here is my hub:
[HubName("marketWatch")]
public class MarketWatchHub : Hub
{
public override Task OnConnected()
{
SocketCommunicator.Instance.UserConnected(Context.ConnectionId, Context.User.Identity.Name);
return base.OnConnected();
}
public override Task OnDisconnected()
{
SocketCommunicator.Instance.UserDisconnected(Context.ConnectionId);
return base.OnDisconnected();
}
public override Task OnReconnected()
{
// TODO: implement...
return base.OnReconnected();
}
public List<MarketDataResponse> GetAllMarketWatchData()
{
return SocketCommunicator.Instance.MarketDataList;
}
}
And here is the simplified version of SocketCommunicator class:
public class SocketCommunicator
{
private static SocketCommunicator _Instance = new SocketCommunicator();
public static SocketCommunicator Instance
{
get { return SocketCommunicator._Instance; }
}
private Socket socket { get; set; }
private readonly object lockObj = new object();
private IHubContext hubContext;
private List<UserDetail> connectedUsers;
public List<MarketDataResponse> MarketDataList;
private SocketCommunicator() { }
public void UserConnected(string connectionId, string username)
{
lock (lockObj)
{
connectedUsers.Add(new UserDetail() { ConnectionId = connectionId, UserName = username });
}
}
public void UserDisconnected(string connectionId)
{
lock (lockObj)
{
connectedUsers.RemoveAll(ud => ud.ConnectionId == connectionId);
}
}
public void GetMarketData()
{
// Do something and set this.MarketDataList
}
}
When I hit F5 and debug my application it works like a charm. When user logs in, my OnConnected method in my hub is called and when user logs off OnDisconnected method is called. But if user logs in and close his/her browser, OnDisconnected method is not being called. This means in time, my connectedUsers list will contain enormous number of UserDetail objects that are not really connected. How can I avoid this situation? Is there a better way to store user - connection id association?
Thanks in advance,
How long are you waiting for OnDisconnect to get called? It isn't always instantaneous, especially if the client doesn't close 'cleanly' (i.e. closing the browser). It should get called eventually, once the connection times-out.
I have a similar setup and it works fine.

Resources