'SignalR multi client method call async return - .Net Core 6
I'm trying to use signalR to trigger processes and get return from clients in real-time. My usage cenario consists of 2 client types (Process Manager and Store) and one server(Hub).
First client type is the "Process Manager". This client will be used to manage processes execution. It will invoke hub's(server) methods asking it to invoke a certain method on an certain connection. The hub will then invoke that method on that connection and "wait" for an answer. The second client type ("Store"), will execute the invoked method and "return" to the hub by invoking a response method. That hub method will then trigger the continuation of the task that was waiting for the return and then response will hit the process manager in the end.
PM -- (invokes)--> HUB -- (invokes)--> STORE -- (invokes)--> HUB -- (return)--> PM
My problem is that I need to invoke multiple methods asynchronously and at this moment, the invoke of a client's method is locking subsequent invocations.
With some research, I manage to write the code below. I invoke two methods, GetLocalIpAddress and GetMachineName. GetLocalIpAddress has a 5 seconds delay added on purpose to simulate if GetMachineName would return first.
By what happened is that GetMachineName is waiting for the GetLocalIpAddress to finish before it returns.
Is it possible to make those calls asynchronously ? What executes first returns first, regardless of other methods ? I need that behavior for different methods and for the same method invoked multiple times. For example, suppose a method that executes a query. Different queries may take longer than others to execute. I want to return data as soon as the method execute regardless of other invokations of the same method that were sent first that are still running.
I don't have much experience with asynchronous code. So please, don't hesitate to point or change anything wrong.
PM Client - Console application
var uri = "https://localhost:7155/SignalR";
HubConnection connection = new HubConnectionBuilder()
.WithUrl(uri, (opts) =>
{
opts.Headers.Add("ID", "ProcessManager");
}).Build();
await connection.StartAsync();
HubRequest(0, "Store0", "GetLocalIpAddress");
HubRequest(1, "Store0", "GetMachineName");
Console.ReadLine();
async void HubRequest(int i, string connectionID, string methodName, string? data = null)
{
Console.WriteLine($"{i} - Request {methodName} from remote client - {connectionID}");
string response = await connection.InvokeAsync<string>("ExecuteOnClient", connectionID, methodName, data);
Console.WriteLine($"{i} - Response {methodName} from remote client: {response} - {connectionID}");
}
Server - Asp.net Core Web App
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();
var app = builder.Build();
app.MapHub<MainHub>("/SignalR");
app.MapGet("/", () => "Hub initialized");
app.Run();
public class MainHub : Hub
{
static Dictionary<string, string> connectionsList = new Dictionary<string, string>();
private static readonly ConcurrentDictionary<Guid, TaskCompletionSource<object>> _responses = new ();
public async override Task OnConnectedAsync()
{
HttpRequest request = Context.GetHttpContext().Request;
string id = request.Headers["ID"].ToString().Trim();
string connectionId = Context.ConnectionId;
if (!id.Equals(("ProcessManager")))
{
id = $"{id}{connectionsList.Where(x => x.Key.Contains("Store")).Count()}";
await Clients.Client(connectionId).SendAsync("AfterConnected", id);
}
connectionsList.Add(id, connectionId);
}
public async override Task OnDisconnectedAsync(Exception exception)
{
string connectionId = Context.ConnectionId;
foreach (var item in connectionsList.Where(kvp => kvp.Value == connectionId).ToList())
connectionsList.Remove(item.Key);
}
public void ClientResponse(Guid guidRequestID, string returnData)
{
TaskCompletionSource<object> tcs;
if (_responses.TryGetValue(guidRequestID, out tcs))
{
// Trigger the task continuation
tcs.TrySetResult(returnData);
}
//else
//{
// Client response for something that isn't being tracked, might be an error
//Test Only
//throw new Exception("Resposta não Esperada.");
//}
}
public async Task<string> ExecuteOnClient(string connectionID, string methodName, string data)
{
// Create an entry in the dictionary that will be used to track the client response
var tcs = new TaskCompletionSource<object>();
var guidRequestID = Guid.NewGuid();
_responses.TryAdd(guidRequestID, tcs);
// Call method on the client passing the identifier
if (!string.IsNullOrWhiteSpace(data))
Clients.Client(connectionsList[connectionID]).SendAsync(methodName, guidRequestID, data);
else
Clients.Client(connectionsList[connectionID]).SendAsync(methodName, guidRequestID);
try
{
// Wait for the client to respond
int timeout = 10000;
var task = tcs.Task;
if (await Task.WhenAny(task, Task.Delay(timeout)) == task)
{
return (string)await task;
}
else
{
//Cancel this task if the client disconnects (potentially by just adding a timeout)
return "TimedOut";//Test
}
}
finally
{
// Remove the tcs from the dictionary so that we don't leak memory
_responses.TryRemove(guidRequestID, out tcs);
}
}
}
Store Client - Console App
string uri = "https://localhost:7155/SignalR";
HubConnection connection = new HubConnectionBuilder()
.WithUrl(uri, (opts) =>
{
opts.Headers.Add("ID", "Store");
}).Build();
connection.On<string>("AfterConnected", (data) =>
{
Console.WriteLine($"Client name - {data}");
});
connection.On<string>("GetLocalIpAddress", (guidRequestID) =>
{
Console.WriteLine($"GetLocalIpAddress - {GetLocalIPAddress()}");
Thread.Sleep(5000);
connection.InvokeAsync("ClientResponse", guidRequestID, GetLocalIPAddress());
});
connection.On<string>("GetMachineName", (guidRequestID) =>
{
Console.WriteLine($"GetMachineName - {System.Environment.MachineName}");
connection.InvokeAsync("ClientResponse", guidRequestID, System.Environment.MachineName);
});
await connection.StartAsync();
Console.ReadKey();
static string GetLocalIPAddress()
{
var host = Dns.GetHostEntry(Dns.GetHostName());
foreach (var ip in host.AddressList)
{
if (ip.AddressFamily == AddressFamily.InterNetwork)
{
return ip.ToString();
}
}
throw new Exception("No network adapters with an IPv4 address in the system!");
}
Solution 1:[1]
There are two options:
- Refactor your hub methods so instead of waiting in one of them for a result it will store some info for calling the client when a result comes in
- Use the MaximumParallelInvocationsPerClient option and set it to a value larger than 1.
The refactor would look something like the following:
private static readonly List<string> _responses = new();
public async Task ExecuteOnClient(string connectionID, string methodName, string data)
{
_responses.TryAdd(Context.ConnectionID);
await Clients.Client(connectionsList[connectionID]).SendAsync(methodName, Context.ConnectionID, data);
}
public void ClientResponse(string originalConnectionID, string returnData)
{
if (_responses.TryRemove(originalConnectionID))
{
await Clients.Client(originalConnectionID).SendAsync("FromClient", returnData);
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 |

