SignalR group registration server methods not being hit - c#

I've followed this guide, ASP.NET SignalR Hubs API Guide (How to manage group membership from the Hub class) and yet am unable to get my server side ShipmentHub methods to execute.
My ShipmentHub class looks like this:
public class ShipmentHub : Hub
{
IShipmentLogic shipmentLogic;
public ShipmentHub(IShipmentLogic shipmentLogic)
{
this.shipmentLogic = shipmentLogic;
}
public void CreateShipment(IEnumerable<Shipment> shipments)
{
// Clients.All.createShipment(shipments.OrderByDescending(s => s.CreatedDate));
Clients.Group(shipments.FirstOrDefault().ShipmentId)
.createShipment(shipments.OrderByDescending(s => s.CreatedDate));
}
public async Task WatchShipmentId(string shipmentId)
{
await Groups.Add(Context.ConnectionId, shipmentId);
Clients.Group(shipmentId).createShipment(shipmentLogic.Get(shipmentId, true));
}
public Task StopWatchingShipmentId(string shipmentId)
{
return Groups.Remove(Context.ConnectionId, shipmentId);
}
}
My client, more or less, looks like this:
var shipmentHub = $.connection.shipmentHub;
$.connection.hub.logging = true;
$.connection.hub.start();
var shipmentId = "SHP-W-GE-100122";
if (previousShipmentId) {
shipmentHub.server.stopWatchingShipmentId(previousShipmentId);
}
if (shipmentId.length) {
previousShipmentId = shipmentId;
shipmentHub.server.watchShipmentId(shipmentId);
}
In the SignalR client logs I see that these are being called:
SignalR: Invoking shipmenthub.WatchShipmentId
SignalR: Invoking shipmenthub.StopWatchingShipmentId
SignalR: Invoking shipmenthub.WatchShipmentId
And, aside from just the logs, these methods are being hit:
proxies['shipmentHub'].server = {
createShipment: function (shipments) {
return proxies['shipmentHub'].invoke.apply(proxies['shipmentHub'], $.merge(["CreateShipment"], $.makeArray(arguments)));
},
stopWatchingShipmentId: function (shipmentId) {
return proxies['shipmentHub'].invoke.apply(proxies['shipmentHub'], $.merge(["StopWatchingShipmentId"], $.makeArray(arguments)));
},
watchShipmentId: function (shipmentId) {
return proxies['shipmentHub'].invoke.apply(proxies['shipmentHub'], $.merge(["WatchShipmentId"], $.makeArray(arguments)));
}
};
And, as a final note, before I added the Watch and StopWatching methods, everything else worked (i.e., CreateShipment would call the Client.All.createShipment method without issue).

You need to wait for the connection to the server to be established before you can start calling methods on the server from the client. hub.start() returns a promise, here is the basic pattern for doing something once that promise is resolved.
var shipmentHub = $.connection.shipmentHub;
$.connection.hub.logging = true;
$.connection.hub.start().done(talkToServer);
var talkToServer=function(){
var shipmentId = "SHP-W-GE-100122";
if (previousShipmentId) {
shipmentHub.server.stopWatchingShipmentId(previousShipmentId);
}
if (shipmentId.length) {
previousShipmentId = shipmentId;
shipmentHub.server.watchShipmentId(shipmentId);
}
}

The issue is due to the parameterized constructor in ShipmentHub. According to Dependency Injection in SignalR:
By default, SignalR expects a hub class to have a parameterless constructor. However, you can easily register a function to create hub instances, and use this function to perform DI. Register the function by calling GlobalHost.DependencyResolver.Register.
So, you need to modify your Startup.Configuration(IAppBuilder app) method to resolve the dependency for you:
GlobalHost
.DependencyResolver
.Register(
typeof(ShipmentHub),
() => new ShipmentHub(new ShipmentLogic()));

Related

Is there SignalR alternative with "return value to server" functionality?

My goal: Pass data do specific client who is connected to server and get results without calling Server method.
I tried use SignalR to do this (because It's very easy tool for me), but I can't get results (now I know why). I am working on ASP.NET Core 3.1.
My question: Is there SignalR alternative with "return value to server" functionality (call method with params on target client and get results)?
SignalR is usually used in a setup where there are multiple clients and a single server the clients connect to. This makes it a normal thing for clients to call the server and expect results back. Since the server usually does not really care about what individual clients are connected, and since the server usually broadcasts to a set of clients (e.g. using a group), the communication direction is mostly used for notifications or broadcasts. Single-target messages are possible but there isn’t a built-in mechanism for a request/response pattern.
In order to make this work with SignalR you will need to have a way for the client to call back the server. So you will need a hub action to send the response to.
That alone doesn’t make it difficult but what might do is that you will need to link a client-call with an incoming result message received by a hub. For that, you will have to build something.
Here’s an example implementation to get you starting. The MyRequestClient is a singleton service that basically encapsulates the messaging and offers you an asynchronous method that will call the client and only complete once the client responded by calling the callback method on the hub:
public class MyRequestClient
{
private readonly IHubContext<MyHub> _hubContext;
private ConcurrentDictionary<Guid, object> _pendingTasks = new ConcurrentDictionary<Guid, object>();
public MyRequestClient(IHubContext<MyHub> hubContext)
{
_hubContext = hubContext;
}
public async Task<int> Square(string connectionId, int number)
{
var requestId = Guid.NewGuid();
var source = new TaskCompletionSource<int>();
_pendingTasks[requestId] = source;
await _hubContext.Clients.Client(connectionId).SendAsync("Square", nameof(MyHub.SquareCallback), requestId, number);
return await source.Task;
}
public void SquareCallback(Guid requestId, int result)
{
if (_pendingTasks.TryRemove(requestId, out var obj) && obj is TaskCompletionSource<int> source)
source.SetResult(result);
}
}
In the hub, you then need the callback action to call the request client to complete the task:
public class MyHub : Hub
{
private readonly ILogger<MyHub> _logger;
private readonly MyRequestClient _requestClient;
public MyHub(ILogger<MyHub> logger, MyRequestClient requestClient)
{
_logger = logger;
_requestClient = requestClient;
}
public Task SquareCallback(Guid requestId, int number)
{
_requestClient.SquareCallback(requestId, number);
return Task.CompletedTask;
}
// just for demo purposes
public Task Start()
{
var connectionId = Context.ConnectionId;
_ = Task.Run(async () =>
{
var number = 42;
_logger.LogInformation("Starting Square: {Number}", number);
var result = await _requestClient.Square(connectionId, number);
_logger.LogInformation("Square returned: {Result}", result);
});
return Task.CompletedTask;
}
}
The Start hub action is only for demo purposes to have a way to start this with a valid connection id.
On the client, you then need to implement the client method and have it call the specified callback method once it’s done:
connection.on('Square', (callbackMethod, requestId, number) => {
const result = number * number;
connection.invoke(callbackMethod, requestId, result);
});
Finally, you can try this out by invoking the Start action by a client:
connection.invoke('Start');
Of course, this implementation is very basic and will need a few things like proper error handling and support for timing out the tasks if the client didn’t respond properly. It would also be possible to expand this to support arbitrary calls, without having you to create all these methods manually (e.g. by having a single callback method on the hub that is able to complete any task).

"Cannot access a disposed object" crash in SignalR

I have a test hub with a timer that sends the date to all clients.
Once a client connects, it crashes with the following error: Cannot access a disposed object.
Here is the error:
System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'MyHub'.
at Microsoft.AspNetCore.SignalR.Hub.CheckDisposed()
at Microsoft.AspNetCore.SignalR.Hub.get_Clients()
Here is the hub code:
public class MyHub : Hub
{
public MyHub()
{
Program.T = new Timer(TickTimer, null, 1000, 1000);
}
private void TickTimer(object State)
{
try
{
var Time = DateTime.UtcNow.ToString(CultureInfo.InvariantCulture);
Console.WriteLine(Time);
Clients.All.SendCoreAsync("update", new object[] { Time });
}
catch (Exception E)
{
Console.WriteLine(E);
throw;
}
}
}
It looks like the Clients object has been disposed of, but I don't understand why.
Edit, here is more information:
The hubs can come from different assemblies, so they are registered dynamically, in the configure section of the asp startup.
Each hub gets decorated with an attribute to identify it and provide a path:
[AttributeUsage(AttributeTargets.Class)]
public class SignalRHub : Attribute
{
public readonly string Route;
public SignalRHubPath(string Route)
{
this.Route = Route;
}
}
And then they are found and registered this way:
private static void RegisterHubs(IApplicationBuilder Application)
{
// find all SignalR hubs
var HubsList = ReflectionHelper.FindType<SignalRHubPath>();
Logging.Info($"Found {HubsList.Count} hubs");
// get a link to the mapper method of the hubroutebuilder.
var MapperMethodInfo = typeof(HubRouteBuilder).GetMethod("MapHub", new[] { typeof(PathString) }, null);
// register them
foreach (var H in HubsList)
{
// get the route attribute
var Route = string.Empty;
var Attributes = Attribute.GetCustomAttributes(H);
foreach (var Attribute in Attributes)
{
if (Attribute is SignalRHubPath A) { Route = A.Route; break; }
}
// register the hub
if (string.IsNullOrEmpty(Route))
{
Logging.Warn($"[Hub] {H.Name} does not have a path, skipping");
}
else
{
Logging.Info($"[Hub] Registering {H.Name} with path {Route}");
// Application.UseSignalR(_ => _.MapHub<Hub>("/" + Route));
// use the mapper method call instead so we can pass the hub type
var Path = new PathString("/" + Route);
Application.UseSignalR(R => MapperMethodInfo.MakeGenericMethod(H).Invoke(R, new object [] { Path }));
}
}
}
Hub lifetime is per request (see Note at https://learn.microsoft.com/en-us/aspnet/core/signalr/hubs?view=aspnetcore-3.1 ) so you get disposed exception because you are accessing a property (Clients) of a disposed object.
When you want to send a message to clients outside a Hub (and you are outside, since reacting to a timer, thus after the .netcore hub lifetime) you should work with a IHubContext (which you can get by DI), have a look at https://learn.microsoft.com/en-us/aspnet/core/signalr/hubcontext?view=aspnetcore-3.1
Hubs are transient:
Don't store state in a property on the hub class. Every hub method call is executed on a new hub instance.
Use await when calling asynchronous methods that depend on the hub staying alive. For example, a method such as Clients.All.SendAsync(...) can fail if it's called without await and the hub method completes before SendAsync finishes.enter link description here

Call Client method outside SignalR Project

I am having concerns about how to use SgnalR in the following scenario:
There is a non-hub service project that runs a time-consuming task periodically.
The clients should be notified about the progress of the running task. After making some research, SignalR seemed to be the right choice for this purpose.
The problem is, I want the Service-Hub-Clients system to be as loosely-coupled as possible. So, I hosted the Hub in IIS and as a SignalR documentation suggests, added a reference to the Hub context in the outside project and called the client method:
hubContext = GlobalHost.ConnectionManager.GetHubContext<TheHub>()
hubContext.Clients.All.progress(n, i);
Client side:
private void InitHub()
{
hubConnection = new HubConnection(ConfigurationManager.AppSettings["hubConnection"]);
hubProxy = hubConnection.CreateHubProxy("TheHub");
hubConnection.Start().Wait();
}
hubProxy.On<int, int>("progress", (total, done) =>
{
task1Bar.Invoke(t => t.Maximum = total);
task1Bar.Invoke(t => t.Value = done);
});
On the client side the method isn't being invoked and after two days of research I can't get it working, although when making a call from the Hub itself, it works fine. I suspect I'm missing some configuration
You can't use the GlobalHost.Connection manager in your Hub class or service, if the caller is going to be any project other than the Web project.
GlobalHost.ConnectionManager.GetHubContext<TheHub>()
You should instead create a service class that would abstract the hub from the callers. The service class should have something like:
// This method is what the caller sees, and abstracts the communication with the Hub
public void NotifyGroup(string groupName, string message)
{
Execute("NotifyGroup", groupName, message);
}
// This is the method that calls the Hub
private void Execute(string methodName, params object[] parameters)
{
using (var connection = new HubConnection("http://localhost/"))
{
_myHub = connection.CreateHubProxy("TheHub");
connection.Start().Wait();
_myHub.Invoke(methodName, parameters);
connection.Stop();
}
}
The last bit which is the hub itself, should be something like:
public void NotifyGroup(string groupName, string message)
{
var group = Clients.Group(groupName);
if (group == null)
{
Log.IfWarn(() => $"Group '{groupName}' is not registered");
return;
}
group.NotifyGroup(message);
}

Call SignalR Client method from normal C# class

I am trying to add SignalR in my MVC project. I need to call a SignalR client method from my class library. I did below code
public class CommomHubManager : ICommomHubManager
{
readonly IHubContext Context;
public CommomHubManager()
{
Context = Helpers.Providers.HubContextProvider<Hubs.Notifications>.HubContext;
}
public Task AddUserToGroup(string groupName)
{
return Task.Factory.StartNew(() => {
Context.Clients.All.addUserToGroup(groupName);
});
}
}
Its not working, but when i try to call an another Hub class method from WebApi its working just fine. I want to know that is it possible to call SignalR Client method from normal C# class?
How to use SignalR hub instance outside of the hubpipleline
var context = GlobalHost.ConnectionManager.GetHubContext<CommomHubManager>();
context.Clients.All.Send("Admin", "stop the chat");
You can find out more in the SignalR documentation.

How do I call a SignalR hub method from the outside?

This is my Hub code:
public class Pusher : Hub, IPusher
{
readonly IHubContext _hubContext = GlobalHost.ConnectionManager.GetHubContext<Pusher>();
public virtual Task PushToOtherInGroup(dynamic group, dynamic data)
{
return _hubContext.Clients.Group(group).GetData(data);
}
}
I want call this method in another project with this code:
var pusher = new Pusher.Pusher();
pusher.PushToOtherInGroup("Test", new {exchangeTypeId, price});
I want call PushToOtherInGroup,when calling the method i don't get any error.but pusher does not work.
This is my Ui Code:
$(function() {
hub = $.connection.pusher;
$.connection.hub.start()
.done(function() {
hub.server.subscribe('newPrice');
console.log('Now connected, connection ID=' + $.connection.hub.id);
})
.fail(function() { console.log('Could not Connect!'); });
});
(function() {
hub.client.GetData = function (data) {
debugger;
};
});
What is my problem?
You can't instantiate and call a hub class directly like that. There is much plumbing provided around a Hub class by the SignalR runtime that you are bypassing by using it as a "plain-old class" like that.
The only way to interact with a SignalR hub from the outside is to actually get an instance of an IHubContext that represents the hub from the SignalR runtime. You can only do this from within the same process, so as long as your other "project" is going to be running in process with the SignalR code it will work.
If your other project is going to be running in another process then what you would want to do is expose a sort of "companion" API which is either another SignalR hub or a regular old web service (with ASP.NET web API) that you can call from this other application to trigger the behavior you want. Whichever technology you choose, you would probably want to secure this so that only your authenticated applications can call it.
Once you decide which approach you're going to take, all you would do to send messages out via the Pusher hub would be:
// Get the context for the Pusher hub
IHubContext hubContext = GlobalHost.ConnectionManager.GetHubContext<Pusher>();
// Notify clients in the group
hubContext.Clients.Group(group).GetData(data);
If you're looking to call a method in your hub from another project then it needs to reside within the same app domain. If it does here's how you can do it:
Call a hub method from a controller's action (don't mind the title, it works for your scenario)
Take a look at this link at the topic of (How to call client methods and manage groups from outside the Hub class).
Code example simply creates a singleton instance of the caller class and pass in the IHubContext into it's constructor. Then you have access to desired context.Clients in caller class's methods:
// This sample only shows code related to getting and using the SignalR context.
public class StockTicker
{
// Singleton instance
private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(() => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>()));
private IHubContext _context;
private StockTicker(IHubContext context)
{
_context = context;
}
// This method is invoked by a Timer object.
private void UpdateStockPrices(object state)
{
foreach (var stock in _stocks.Values)
{
if (TryUpdateStockPrice(stock))
{
_context.Clients.All.updateStockPrice(stock);
}
}
}
The methods within Hub are supposed to be called FROM a CLIENT.
If you want to send something TO a CLIENT - indeed, you have to use hubContext.

Categories

Resources