Obtaining SignalR Hub without losing its clients from "other place in code" - c#

I have app for one person that has to display data on webpage from external device connected via usb port
Before, I gave user button "Start listening" that sent http request to backend which started listening on port (and blocked app, but it was fine because it is supposed to be used by exactly 1 person at time) until it received not-error response (SerialDataReceivedEvent/SerialErrorReceivedEvent)
and SerialData was returned from that request and displayed on page
I have to rewrite this using SignalR, so I quickly came with naive solution like this:
public class DeviceReaderHub : Hub
{
private readonly IConfiguration _config;
// this is static because listening that port (SerialPort) has to stay open
private static DeviceReader_Helper _service;
public DeviceReaderHub(IConfiguration config)
{
_config = config;
if (_service == null)
{
_service = new DeviceReader_Helper();
_service.Open(_config["DeviceInfo:Port"]);
}
_service.DataReceived_Delegate = SendMessage;
_service.ErrorReceived_Delegate = SendErrorMessage;
}
public async Task SendMessage(string message)
{
await Clients.All.SendAsync("onRead", message);
}
public async Task SendErrorMessage(string message)
{
await Clients.All.SendAsync("onRead", $"error = {message}");
}
public async override Task OnConnectedAsync()
{
await Clients.All.SendAsync("onConnected");
await base.OnConnectedAsync();
}
}
but I received reality-check very quickly - I cannot do it like that because DeviceReaderHub is disposed.
Exception thrown: 'System.ObjectDisposedException' in Microsoft.AspNetCore.SignalR.Core.dll
I thought about obtaining new DeviceReaderHub hub whenever SerialDataReceivedEvent or SerialErrorReceivedEvent is being Invoked
but I have not idea how can I get instance of the hub without losing connected clients

SignalR hubs are transient objects, which means that a new hub instance is used for each method call on the hub from the client. According to this Microsoft Docs:
Don't store state in a property on the hub class. Every hub method call is executed on a new hub instance.
So you should not use the hub to do anything other than receiving and handling requests from the client. To send messages to the client outside of the hub, SignalR provides the HubContext<T> class which is available from Dependency Injection. For example:
public class DeviceReader_Helper {
private readonly IHubContext<DeviceReaderHub> _hubContext;
// you can obtain hubContext either from constructor DI, or service locator pattern with an IServiceProvider
public DeviceReader_Helper(IHubContext<DeviceReaderHub> hubContext) {
_hubContext = hubContext;
}
public async Task SendMessage(string message) {
await _hubContext.Clients.All.SendAsync("onRead", message);
}
}
For more information see this. As for your concern that
but I have not idea how can I get instance of the hub without losing connected clients
Clients can be connected without a hub instance. Hubs are only used to receive messages from the client, and are not necessary to keep a client connected to the server.

Related

How can an Azure EventProcessor access a SignalR HubContext?

I have a .net core 3.0 web application. In Startup.cs, I register an EventProcessor (from Microsoft.Azure.EventHubs.Processor) that listens to an Azure EventHub for events. I do it like this:
await eventProcessorHost.RegisterEventProcessorAsync<TwinChangesEventHandler>();
I'm interested in device twin changes in an IoT Hub that's connected to the EventHub.
So, in the EventProcessor, I want to access the SignalR IHubContext interface (from Microsoft.AspNetCore.SignalR - the new version of SignalR) to be able to notify connected browsers of a device twin property change. My problem is that the EventProcessor can't get a handle to IHubContext. How can I get it?
I see online that people are using dependency injection but because my EventProcessor is created by RegisterEventProcessorAsync() like I showed above, its default constructor is ALWAYS called and NOT the one with IHubContext as a parameter! Even if I use a factory to create the EventProcessor and call RegisterEventProcessorFactoryAsync() in Startup.cs, I can't get the IHubContext handle in the factory, because the call is not originating from a controller. It either originates from Startup.ConfigureServices() or a callback from whenever something happens in the EventHub, which is not a controller method. I'm really stuck, so any help would be much appreciated. Does anyone know the answer to this?
You can add your Factory and processor to services
.ConfigureServices((hostContext, services) =>
{
...
services.AddSingleton<IEventProcessorFactory, EventProcessorFactory>();
services.AddSingleton<IEventProcessor, TwinChangesEventHandler>();
...
});
public class EventProcessorFactory : IEventProcessorFactory
{
private readonly IEventProcessor _fluxEventProcessor;
public EventProcessorFactory(IEventProcessor fluxEventProcessor)
{
_fluxEventProcessor = fluxEventProcessor;
}
public IEventProcessor CreateEventProcessor(PartitionContext context)
{
return _fluxEventProcessor;
}
}
Then in your handler you can have access to the injected hub
public class TwinChangesEventHandler : IEventProcessor
{
private readonly IHubContext<MyHub> _myHubContext;
public TwinChangesEventHandler(IHubContext<MyHub> myHubContext)
{
_myHubContext= myHubContext;
}
...
async Task IEventProcessor.ProcessEventsAsync(PartitionContext context, IEnumerable<EventData> messages)
{
foreach (var eventData in messages)
{
await _myHubContext.Clients.All.SendAsync("Update", eventData);
}
//Call checkpoint every 5 minutes, so that worker can resume processing from 5 minutes back if it restarts.
if (_checkpointStopWatch.Elapsed > TimeSpan.FromMinutes(5))
{
await context.CheckpointAsync();
_checkpointStopWatch.Restart();
}
}
}

How to buffer messages on signal hub and send them when the right client appears?

I have two type of clients connecting my signalR server (ASP.NET Core). Some of them are senders, and some of them are receivers. I need to route messages from senders to the receivers, which is not a problem, but when there is no receivers, I need to somehow buffer messages and not lose them (probably the best is ConcurrentQueue in some kind of a singleton class) but when the first receiver connect, the message buffer needs to start dequeue. Which is the best approach for this?
I created singleton class that wraps arround ConcurrentQueue collection and I enqueue and dequeue messages there. Also I have a separate singleton class which persist collection of the receivers connectionIDs. And I implemented event in this second class that fires event when first receiver connects after the list of receivers was empty but maybe this is not a good approach, I don't know how to use id in Hub, because there is more than one instance of a signalR hub.
Second approach is to mark persistance class as controller and inject the ContextHub and message buffer in this class and dequeue buffer from there and directly send messages to the receivers???
If I understood well, you want to defer SignalR message sending by using something like a synchronized call in some IHostedService. Here is what I managed to achieve so far.
Your approach that consists in using a ConcurrentQueue that contains invokable Action delegates to handle the concurrent hub calls is the right one. As you mention, it has to be injected as a singleton.
So here the Queues class:
public class Queues {
public ConcurrentQueue<Action<IHubContext<MyHub, IMyEvents>>> MessagesQueue { get; set; }
}
Now we need to capture the ConnectionId of the caller so a call can get an answer later. SendMessage enqueue the necessary action delegate to perform a call against a hub instance as a parameter.
As an example SendMessage will trigger an answer back to the caller, and BroadcastMessage will send a message to all clients.
Using a captured hub instance instead would lead to an exception here because the hub will be disposed, quickly. That's why it would be injected later in another class. Have a look on SendMessage_BAD
Here is the MyHub class and the corresponding IMyEvents interface:
public interface IMyEvents {
void ReceiveMessage(string myMessage);
}
public class MyHub : Hub<IMyEvents> {
Queues queues;
public MyHub(Queues queues) {
this.queues = queues;
}
public void SendMessage(string message) {
var callerId = Context.ConnectionId;
queues.MessagesQueue.Enqueue(hub => hub.Clients.Client(callerId).ReceiveMessage(message));
}
// This will crash
public void SendMessage_BAD(string message) {
this.callerId = Context.ConnectionId;
queues.MessagesQueue.Enqueue(_ => this.Clients.Client(callerId).ReceiveMessage(message));
}
public void BroadcastMessage(string message) {
queues.MessagesQueue.Enqueue(hub => hub.Clients.All.ReceiveMessage(message));
}
}
Now, using a naive approach, this code will trigger the message sending a deferred way. (At work, a timer ensure a regular cadence, and the class is an IHostedService but it is does not appear here). This class has to be injected as a singleton.
Here the DeferredMessageSender class:
public class DeferredMessageSender {
Queues queues;
IHubContext<MyHub, IMyEvents> hub;
public DeferredMessageSender(Queues queues, IHubContext<MyHub, IMyEvents> hub) {
this.queues = queues;
this.hub = hub;
}
public void GlobalSend() {
while(queues.MessagesQueue.TryDequeue(out var evt)) {
evt.Invoke(hub);
}
}
}
Hope it helps.

how to send a binary file to SignalR client from server in dotnet core

We had a solution for sending a file to SignalR client using .Net
We have now moved to .Net Core
In previous .net solution, we used to Hub context via GlobalHost.ConnectionManager
var myHub = GlobalHost.ConnectionManager.GetHubContext<MyHub>();
myHub.Clients.Client(connectionId).doStuffWithFile(fileByteArray, fileName);
where in the client side, function doStuffWithFile would be triggered with the two arguments.
In new .Net Core solution I created a Hub class by deriving from Hub. I added a method of Send to send a file to specific client and not broadcasting it to every one
public class MyHub : Hub
{
private static string _connectionId;
public override Task OnConnectedAsync()
{
_connectionId = Context.ConnectionId;
return Task.CompletedTask;
}
public override Task OnDisconnectedAsync(Exception exception)
{
_connectionId = Context.ConnectionId;
//// remove Connection Id
return base.OnDisconnectedAsync(exception);
}
public async Task Send(byte[] fileByteArray, string fileName)
{
await Clients.Client(_connectionId).InvokeAsync("doStuff", fileByteArray, fileName);
}
}
However, I do not have any mechanism in .Net core such as GlobalHost or ConnectionManager to get HubContext to send the file.
On the client side:
static void Main(string[] args)
{
var connection = new HubConnectionBuilder()
.WithUrl("http://localhost:25786/file")
.WithConsoleLogger()
.Build();
connection.On<byte[], string>("doStuff", DoStuff);
connection.StartAsync().ContinueWith(
task =>
{
if (task.IsFaulted)
{
Console.WriteLine("Connection faulty");
}
});
Console.ReadLine();
}
private static void DoStuff(byte[] data, string name)
{
File.WriteAllBytes(#"c:\Projects\" + name, data);
}
I tried to create a new instance of MyHub to invoke the Send method, but simply it does not work. Could you please advise me how to do this?
This is not a direct answer to your question but hopefully it will help you find a solution.
Storing connection Id in a static class variable is not correct. It will change each time a new client connects and you won't have any control over which client you are sending to. From the code you provided it is not clear how you know which client to send the file to. Note that you also set the _connectionId when the client is disconnected so it is likely that you will try to send data to the connection you know has been closed. I actually think you will want to pass the target connection id or the user to the hub Send method. I think you may not have enough context in the hub method itself to resolve the connection id but since connection id is a SignalR concept it might be hard to access it outside SignalR components. This is why it might be easier to use the user instead of connection Id (i.e. Clients.User(...) instead of Clients.Client(...)).
GlobalHost no longer exists in the new SignalR. Instead you can inject IHubContext<THub> to the class you want to invoke the method from and use InvokeAsync. Here is an example of invoking a hub method from an Mvc Controller.

How to return a value from a c# SignalR client

I have a hub that manages many worker processes. I want to build a UI that lets me connect to this hub and retrieve the processing log from any of these worker processes. Essentially this will be a client wanting to obtain a string from another client. I have been able to get the request from client A sent to client B, but i dont know how to return anything in that response.
I have a simple method in the hub
public void GetRunLog(int runid)
{
JobRunLog log = null;
JobRunClient client = this.GetClientByRunID(runid);
if(client != null)
{
var rawlog = Clients.Client(client.ConnectionID).GetRunLog();
log = JsonConvert.DeserializeObject<JobRunLog>(rawlog);
Clients.Client(Context.ConnectionId).GetRunLog(log);
}
}
This request gets picked up by the client, but I dont know how to make it return a value so that var rawlog actually contains something. For the moment, this is the best workaround i could come up with.
myHubProxy.On("GetRunLog", (uiconnectionid) =>
{
string connectionid = uiconnectionid;
myHubProxy.Invoke("ReturnRunLog", run.ID, run.Log, connectionid).ContinueWith(task => {});
});
This will then make the worker client send the log back in a separate request with a reference to the client that had requested the log, but it isnt actually returning a respnonse to the initial request. I cant see a way to make this happen. Rather than use Invoke, how would i just return the object directly to the method on the hub that initiated the request?
Unfortunatelly Hub doesn't keeps it's state:
Because instances of the Hub class are transient, you can't use them
to maintain state from one method call to the next. Each time the
server receives a method call from a client, a new instance of your
Hub class processes the message. To maintain state through multiple
connections and method calls, use some other method such as a
database, or a static variable on the Hub class, or a different class
that does not derive from Hub.
Try to move the logic into a separate class and store the instance object in a static dictionary related to the connection id (don't forget to clean it). Whenewer call comes to the Hub it repoints it to a appropriate instance,
here is the simplified sample
public class TestingLogHub : Hub
{
public static readonly Dictionary<string, TestInstance> Instances =
new Dictionary<string, TestInstance>();
public void SetParameter(string value)
{
Instances[Context.ConnectionId].ContinueWith(value);
}
...
}
public class TestInstance : IDisposable
{
public TestInstance(string basePath, IHubContext host, string connectionId)
{...
}
public void ContinueWith(string value)
{
if (_nextAction == null)
{
FinishExecution();
}
else
{
try
{
_nextAction(value);
}
catch (Exception exception)
{
Error(exception.Message);
FinishExecution();
}
}
}
public void RequestParameterFor(Action<string> action, string parameter, string defaultValue = null)
{
_nextAction = action;
_host.Clients.Client(_connectionId).requestParameter(parameter, defaultValue??GetRandomText());
}
}
So when Instance is started it's doing some work, but at the moment it requires some input it executes RequestParameterFor that set's the next function to be executed into an instance state and waits for the next call of ContinueWith.
it is a bit generic example, in your case you can send back an object and provide it to an instance, and maybe dispose the instance at the end of that request, if that was the only required call

Unable to track connected users using Signalr hub class

I have created a class that inherits the Signalr Hub class and it runs on startup. When a connection is made, there are some custom headers sent from the client that I use to generate a user object. I want to store these in memory on the server so that I can retrieve the list and display them in a UI. Someone can then use this UI to see the user info and perform interactions with this connection. I have setup a hub class in an ASP MVC project and i am using a console app for the client. I can connect fine and the server can communicate back, but the property that I use in the hub class to keep track of the connected users is reset to null every time a request is made to the hub class.
public class JobRunHandler : Hub
{
private List<JobRunClient> RunningJobs { get; set; }
public JobRunHandler()
{
if(this.RunningJobs == null) this.RunningJobs = new List<JobRunClient>();
}
public override Task OnConnected()
{
JobRunClient runclient = new JobRunClient()
{
ConnectionID = Context.ConnectionId,
Someotherstuff = this.GetHeaderInt(Context.Headers["Someotherstuff"])
};
this.RunningJobs.Add(runclient);
return base.OnConnected();
}
public override Task OnReconnected()
{
var existingClient = this.GetConnectingClient();
if (existingClient == null)
{
JobRunClient runclient = new JobRunClient()
{
ConnectionID = Context.ConnectionId,
Someotherstuff = this.GetHeaderInt(Context.Headers["Someotherstuff"])
};
this.RunningJobs.Add(runclient);
}
return base.OnReconnected();
}
public override Task OnDisconnected(bool stopCalled)
{
this.RemoveClient(Context.ConnectionId);
return base.OnDisconnected(stopCalled);
}
public void TestMethod()
{
Clients.All.ping();
var client = this.GetConnectingClient();
}
}
I have put break points in every method so i know when it runs. The client never disconnects or triggers reconnect, so there is no issue with the connection being broken. The client connects and the OnConnected() method triggers and the value is added to this.RunningJobs. The client then calls the TestMethod() which works, but when i check this.RunningJobs it is empty.
When Clients.All.ping(); runs it does actually send a ping back to the client. So the connection is made successfully, the server maintains the connection and i can send a ping back to the client when a separate method is called, but for some reason the property is being reset and I dont know why. I can use redis for this if I have to, but I have seen others use this strategy and its not been an issue.
Here is the client code I have created to test this (the console app)
var hubConnection = new HubConnection("http://localhost:2497/");
hubConnection.Credentials = CredentialCache.DefaultNetworkCredentials;
IHubProxy myHubProxy = hubConnection.CreateHubProxy("JobRunHandler");
myHubProxy.On("ping", () => Console.Write("Recieved ping \n"));
hubConnection.Headers.Add("Someotherstuff", "1");
hubConnection.Start().Wait();
while(true)
{
myHubProxy.Invoke("BroadcastCompletion").ContinueWith(task =>
{
if (task.IsFaulted)
{
Console.WriteLine("!!! There was an error opening the connection:{0} \n", task.Exception.GetBaseException());
}
}).Wait();
Console.WriteLine("Broadcast sent to the server.\n");
Thread.Sleep(4000);
}
The hub is transient. SignalR creates a hub instance each time a hub method is invoked so you cannot store any state in an instance property between the calls.
I changed the property being used for this to a ConcurrentDictionary and this seems to be doing the trick. It allows me to store the client connections across all connections. I used the following code for this.
private static readonly ConcurrentDictionary<string, JobRunClient> RunningJobs = new ConcurrentDictionary<string, JobRunClient>();

Categories