I am playing around with signalR. First it worked fine, but when I moved the code where I call the client to another class (using IHubContext) no message is triggered at the client anymore.
For example I made a simple hub:
public class EventHub : Hub
{
public EventHub()
{
Do();
}
private async Task Do()
{
while (true)
{
await Task.Delay(2000);
await Clients.All.foo2("BAR2");
}
}
}
But I try the same in another class it is not working anymore.
public class EventHubTester
{
private readonly Lazy<IHubContext> context = new Lazy<IHubContext>(() => GlobalHost.ConnectionManager.GetHubContext<EventHub>());
public EventHubTester()
{
Do();
}
private async Task Do()
{
while (true)
{
await Task.Delay(2000);
await context.Value.Clients.All.foo("BAR");
}
}
}
I see in the browser log, that foo2 is triggered, foo is not. Any ideas? I am using signalR 2.2.
Regards,
Sebastian
EDIT:
I found the problem. I made a very small example and tested both codes from the post. Then I recognized that it is working fine. So I removed all code from my application step by step until I found the solution:
I use unity for dependency injection and tried two approaches:
DOESNT WORK
HubConfiguration configuration = new HubConfiguration
{
Resolver = new UnitySignalRDependencyResolver(UnityContainer)
};
app.MapSignalR(configuration);
WORKS
GlobalHost.DependencyResolver = new UnitySignalRDependencyResolver(UnityContainer);
app.MapSignalR(configuration);
If somebody can explain me the reason, I would be really happy.
Related
I'm trying to write a notification system between a server and multiple clients using gRPC server streaming in protobuf-net.grpc (.NET Framework 4.8).
I based my service off of this example. However, if I understand the example correctly, it is only able to handle a single subscriber (as _subscriber is a member variable of the StockTickerService class).
My test service looks like this:
private readonly INotificationService _notificationService;
private readonly Channel<Notification> _channel;
public ClientNotificationService(INotificationService notificationService)
{
_notificationService = notificationService;
_notificationService.OnNotification += OnNotification;
_channel = Channel.CreateUnbounded<Notification>();
}
private async void OnNotification(object sender, Notification notification)
{
await _channel.Writer.WriteAsync(notification);
}
public IAsyncEnumerable<Notification> SubscribeAsync(CallContext context = default)
{
return _channel.AsAsyncEnumerable(context.CancellationToken);
}
INotificationService just has an event OnNotification, which is fired when calling its Notify method.
I then realized that System.Threading.Channels implements the Producer/Consumer pattern, but I need the Publisher/Subscriber pattern. When trying it out, indeed only one of the clients gets notified, instead of all of them.
It would also be nice if the server knew when a client disconnects, which seems impossible when returning _channel.AsAsyncEnumerable.
So how can I modify this in order to
serve multiple clients, with all of them being notified when OnNotification is called
and log when a client disconnects?
For 1, you'd need an implementation of a publisher/subscriber API; each call to SubscribeAsync will always represent a single conversation between gRPC endpoints, so you'll need your own mechanism for broadcasting that to multiple consumers. Maybe RX is worth investigating there
For 2, context.CancellationToken should be triggered by client-disconnect
Many thanks to Marc Gravell
I rewrote the NotificationService like this, using System.Reactive.Subjects (shortened) - no need for an event, use an Action instead:
public class NotificationService<T>
{
private readonly Subject<T> _stream = new Subject<T>();
public void Publish(T notification)
{
_stream.OnNext(notification);
}
public IDisposable Subscribe(Action<T> onNext)
return _stream.Subscribe(onNext);
}
}
My updated ClientNotificationService, which is exposed as a code-first gRPC service:
public class ClientNotificationService : IClientNotificationService
{
private readonly INotificationService<Notification> _notificationService;
public ClientNotificationService(INotificationService<Notification> notificationService)
{
_notificationService = notificationService;
}
public async IAsyncEnumerable<Notification> SubscribeAsync(CallContext context = default)
{
try
{
Channel<Notification> channel = Channel.CreateUnbounded<Notification>(
new UnboundedChannelOptions { SingleReader = true, SingleWriter = true });
CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(context.CancellationToken);
using (_notificationService.Subscribe(n => channel.Writer.WriteAsync(n, cts.Token)))
{
await foreach (Notification notification in channel.AsAsyncEnumerable(cts.Token))
{
yield return notification;
}
}
}
finally
{
// canceled -> log, cleanup, whatever
}
}
}
Note: Solution provided by OP on question section.
I am working on a SignalR Clinet-Server connection. My server is WebApi Core 2.1 and my client is WPF .NET Framework 4.7.2.
On the client side I have a singleton hub service with one Instance to recive messages from server:
using System.Collections.ObjectModel;
using Microsoft.AspNetCore.SignalR.Client;
public class HubService
{
//singleton
public static HubService Instance { get; } = new HubService();
public ObservableCollection<string> Notifications { get; set; }
public async void Initialize()
{
this.Notifications = new ObservableCollection<string>();
var hubConnection = new HubConnectionBuilder()
.WithUrl(UrlBuilder.BuildEndpoint("Notifications"))
.Build();
hubConnection.On<string>("ReciveServerUpdate", update =>
{
//todo
});
await hubConnection.StartAsync();
}
}
i initialize it as singleton:
public MainWindowViewModel()
{
HubService.Instance.Initialize();
}
While I'm debugging, on MainWindowViewModel im hitting that HubService.
On Server side its look like this.
Hub:
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
public class NotificationsHub : Hub
{
public async Task GetUpdateForServer(string call)
{
await this.Clients.Caller.SendAsync("ReciveServerUpdate", call);
}
}
Im trigering send message in this way in my controller's methods:
[HttpPost]
public async Task<IActionResult> PostTask([FromBody] Task task)
{
if (!this.ModelState.IsValid)
{
return this.BadRequest(this.ModelState);
}
this.taskService.Add(task);
//here im calling sending message. When im debugging
//i see one connection from my WPF with unique ConnectionId
await this.notificationsHub.Clients.All.SendAsync("ReciveServerUpdate", "New Task in database!");
return this.Ok(task);
}
As I wrote before, while I'm debugging my WebApi, in Clients I have exactly one connection from my WPF. When I turn off WPF, connection count = 0 so connections works perfectly.
But when I call SendAsync(), I'm not reciving any information in WPF in hubConnection.On. Funny thing, yesterday it works perfectly.
So, is my thinking about making HubService as static singleton is right? If its, why i cant recive messages from WebApi by SignalR when my WPF is connected to it?
I asked something similiar yesterday but i found a solution for it. Yesterday, my methods works, i could hit hubConnection.On when i get any message from WebApi. My question from yestarday.
EDIT
Injection of HUb to controller:
private readonly ITaskService taskService;
private readonly IHubContext<NotificationsHub> notificationsHub;
public TaskController(ITaskService taskService, IHubContext<NotificationsHub> notificationsHub)
{
this.taskService = taskService;
this.notificationsHub = notificationsHub;
}
And Startup.cs only SignalR things (i deleted other things not related to signal):
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddSignalR();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseSignalR(routes => routes.MapHub<NotificationsHub>("/Notifications"));
}
EDIT2
Here is connection that i can get it, when my client WPF will register his connection:
I tried your code with all kinds of clients (wpf/console/even with a browser), it always works fine for me. The hubConnection.On<string>("ReciveServerUpdate", update => {//todo}); always be invoked when I send a request to PostTask.
I'm not sure why (sometimes) it doesn't work for you somehow . However, when SignalR client has connected to server but gets no message from server, there're possible two reasons:
Your PostTask([FromBody] Task task) action method is not executed. Let's say this is an ApiController method, if the browser posts a request with a Content-Type of application/www-x-form-urlencoded by accident, the invocation of Clients.All.SendAsync(..., ...); won't be executed at all.
The handler of SigalR client (hubConnection.On<>(method,handler)) must has exactly the same argument list as the invocation in order to receive messages. We must be very careful when dealing with this.
Finally, it's better to add a reference to Microsoft.Extensions.Logging.Console
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.2.*" />
so that we could enable logging to troubleshoot :
var hubConnection = new HubConnectionBuilder()
.WithUrl(UrlBuilder.BuildEndpoint("Notifications"))
.ConfigureLogging(logging =>{
logging.AddConsole(); // enable logging
})
.Build();
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);
}
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.
I'm trying to hit my MVC5 SignalR Hub via a separate, tiny client application, to no avail.
Some background:
I have a regular ASP.NET application using SingalR 1.10, that I can hit with my client. Code:
ASP.NET Hub:
namespace SignalrTest
{
public class ScanHub : Hub
{
public void SendScan(string data, string xmlData)
{
Clients.All.broadcastMessage(data, xmlData);
}
}
}
Client:
connection = new HubConnection("http://localhost:2446/");
hubProxy = connection.CreateHubProxy("ScanHub");
connection.Start();
........
private static async Task RunAsync()
{
object[] param = new object[2];
param[0] = _Data;
param[1] = _xmlData;
await hubProxy.Invoke("SendScan", param);
}
and again, that's working fine. My MVC Hub is identical to the other (I've made sure to change the client HubConnection address), and I have my Startup.cs as:
[assembly: OwinStartupAttribute(typeof(SignalrTest.Startup))]
namespace SignalrTest
{
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
app.MapSignalR();
}
}
}
running my client, it fires off with no errors, but I get no response or any indication that anything has occurred on the MVC side.
Can anyone see where I'm going wrong with the MVC app? I'm unclear on whether I need to alter the routing. I'm happy to post any other code that would help resolve my issue. Thankyou in advance.
Are you really using SignalR 1.1? SignalR 1.1 doesn't use OWIN startup classes, and the MapSignalR method shouldn't even compile.
Try throwing your connection on the .NET client into an async method like so and doing a quick test if your connection is good or not.
private async void Connect()
{
connection = new HubConnection("http://localhost:2446/");
hubProxy = connection.CreateHubProxy("ScanHub");
await connection.Start();
//If using WPF you can test like so if not use whatever output method you prefer to see if it connects
if (Connection.State == Microsoft.AspNet.SignalR.Client.ConnectionState.Connected)
{
MessageBox.Show("Connected!");
}
else
{
MessageBox.Show("Fail!");
}
}