I have a self-hosted application akin to a scenario wherein I have a method that continuously broadcasts to groups (whether somebody has "joined" or not). Something like:
var aTimer = new System.Timers.Timer(2000);
aTimer.Elapsed += (sender, e) =>
{
// broadcast to listeners whether they are listening or not
IHubConnectionContext _clients = GlobalHost.ConnectionManager.GetHubContext<ChatHub>().Clients;
_clients.Group("group1FixedName").showMessage("Some message for group 1 only");
_clients.Group("group2FixedName").showMessage("Some message for group 2 only");
// etc
};
aTimer.Start();
I had recently upgraded to version 1.1.0 from beta1. I started to observe that the "remove" method doesn't work as the "client" (web browser) is still receiving messages from the "other" group even if I initiated a "leave". Note that "leaving" the group doesn't mean closing the web browser. It's still in the same page (Single Page Application) and leaving/joining a group is triggered by a selection (combo box for example).
Code in hub:
public Task Leave(string groupName)
{
return Groups.Remove(Context.ConnectionId, groupName)
.ContinueWith(z => Clients.Caller.showCallerMessage("You are now leaving " + groupName));
}
Code in javascript client to "leave the group":
chat.server.leave("group1FixedName");
Code in javascript client to "join the group":
chat.server.join("group1FixedName");
Code in Hub for joining:
public Task Join(string groupName)
{
return Groups.Add(Context.ConnectionId, groupName)
.ContinueWith(z => Clients.Caller.showCallerMessage("You are now listening to " + groupName));
}
Is there something wrong with my implementation here?
This is bug #2040 introduced in 1.1.0beta.
Team is working on fixing it.
Related
I am currently using SignalR in my .NET framework project to send updates to the client for a long running process. There can be many processes running simultaneously and the client will subscribe to any one of the process using an unique ID. I am using Groups to identify the clients who are subscribed to a particular process. If a client subscribes to a process in middle, I must send all the previous messages to that client. The code goes something like this
public class ProgressHub : Hub
{
public async Task SubscribeToProgress(string id)
{
foreach (var message in GetPreviousMessages(id)) // Getting all the previous messages
{
await Clients.Caller.SendMessage(message); // Sending Messages to the current caller alone
}
await Groups.Add(Context.ConnectionId, id); // Added the current client to a group to be used further
}
}
The client listens to Send Message
The above code snippet is not working (No messages in the network tab).
I tried many things
await Clients.Client(Context.ConnectionId).SendMessage(message);
await Clients.All.SendMessage(message); // Just to check if it works
all the above without await, but nothing seems to work.
After fiddling around a bit, I was able to come up with this
public class ProgressHub : Hub
{
public async Task SubscribeToProgress(string id)
{
await Groups.Add(Context.ConnectionId, id); // Adding client to the group first
foreach (var message in GetPreviousMessages(id))
{
await Clients.Group(id).SendMessage(message); // Sending messages to the group all together
}
}
}
But this has an undesirable side effect of sending the older messages to client who are already connected. Sure, I can exclude the other connectionIDs and send out the message, but this seems like an hack. Logically speaking, the first snippet should have worked just fine.
are you add configuration in Program.cs ?
using SignalRChat.Hubs;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddSignalR();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.MapHub<ChatHub>("/chatHub");
app.Run();
and you can read this reference :
Microsoft
I have setup a SignalR hub which has the following method:
public void SomeFunction(int SomeID)
{
try
{
Thread.Sleep(600000);
Clients.Caller.sendComplete("Complete");
}
catch (Exception ex)
{
// Exception Handling
}
finally
{
// Some Actions
}
m_Logger.Trace("*****Trying To Exit*****");
}
The issue I am having is that SignalR initiates and defaults to Server Sent Events and then hangs. Even though the function/method exits minutes later (10 minutes) the method is initiated again ( > 3 minutes) even when the sendComplete and hub.stop() methods are initiated/called on the client prior. Should the user stay on the page the initial "/send?" request stays open indefinitely. Any assistance is greatly appreciated.
To avoid blocking the method for so long, you could use a Taskand call the client method asynchronously.
public void SomeFunction(Int32 id)
{
var connectionId = this.Context.ConnectionId;
Task.Delay(600000).ContinueWith(t =>
{
var message = String.Format("The operation has completed. The ID was: {0}.", id);
var context = GlobalHost.ConnectionManager.GetHubContext<SomeHub>();
context.Clients.Client(connectionId).SendComplete(message);
});
}
Hubs are created when request arrives and destroyed after response is sent down the wire, so in the continuation task, you need to create a new context for yourself to be able to work with a client by their connection identifier, since the original hub instance will no longer be around to provide you with the Clients method.
Also note that you can leverage the nicer syntax that uses async and await keywords for describing asynchronous program flow. See examples at The ASP.NET Site's SignalR Hubs API Guide.
I am doing my first tests with SignalR. I am toying with chat messages, but that's only a first step to replace all the polling from client to server which I have today on my site.
I have a lot of scenarios where I want to notify certain users either by their login or by their ID. The idea is that I am adding each user to two groups as soon as he connects. I do this in OnConnected and that event is called.
When I send a chat message, I have two modes: either public or personal. If it is personal the sender is notified and the recipient should be notified. The sender gets a message but the group never does. It seems to be impossible to found out how many members a group has.
Any ideas what's going wrong here?
public class GlobalHub:Hub
{
private Users user;
private void AuthenticateUser()
{
var ydc = new MyDataContext();
user = ydc.Users.First(u => u.Login == HttpContext.Current.User.Identity.Name);
}
public override Task OnConnected()
{
var ydc = new MyDataContext();
user = ydc.Users.First(u => u.Login == HttpContext.Current.User.Identity.Name);
Groups.Add(Context.ConnectionId, user.Login);
Groups.Add(Context.ConnectionId, user.ID.ToString());
return base.OnConnected();
}
public void SendChatMessage(string message, string recipient)
{
AuthenticateUser();
var cm = ChatController.AddChatMessage(user.Login, user.ID, recipient, tmessage);
if (recipient != "")
{
Clients.Caller.NewMessage(cm);
Clients.Group(recipient).NewMessage(cm);
}
else
{
Clients.All.NewMessage(cm);
}
}
}
It looks like that Groups.Add does not immediately join the connection to the group, but instead returns a Task, that needs to be started. Try returning the result of Groups.Add as result of OnConnectedMethod.
See also more detailed explanation at: https://stackoverflow.com/a/15469038/174638
I'm hoping to use SignalR to provide updates to the client, the updates are going to come from a message table which is updated when things happen across the application..
My problem is that the application will have around 500-600 concurrent users and I cant have all them having a connection to the database and constantly polling against the table..
What id like to do is have a single thing{?} polling the table and then updating the hubs rather than each connection polling.. I was thinking of using a singleton for this? so maybe when the application starts something is created that will then do all the work really..
My question is - say I had a singleton that had an event which was fired every time there was an update.. what would the performance be like for say 500 controllers subscribing to this event?
Also.. if there is a better way to do this then pleases say.. this is my first and only idea sadly!
any help would be fantastic!
EDIT: the data is bring provided by a legacy application and I have no control over how the data is entered so database polling will be needed.
ste.
I'd rather not to poll the database as it would be wasteful. I would approach this problem by opening only one single point of entry for my data (an HTTP API, etc) and then broadcast the update to all connected clients through the SignalR Hub. Brad Wilson has a super cool presentation which demonstrate this approach:
Brad Wilson - Microsoft’s Modern Web Stack, Starring ASP.NET Web API
Here is a code sample for this approach which uses ASP.NET Web API technology for data entry. It uses in-memory dictionary for data store but the data storage technique is not the concern here:
// This hub has no inbound APIs, since all inbound communication is done
// via the HTTP API. It's here for clients which want to get continuous
// notification of changes to the ToDo database.
[HubName("todo")]
public class ToDoHub : Hub { }
public abstract class ApiControllerWithHub<THub> : ApiController
where THub : IHub {
Lazy<IHubContext> hub = new Lazy<IHubContext>(
() => GlobalHost.ConnectionManager.GetHubContext<THub>()
);
protected IHubContext Hub {
get { return hub.Value; }
}
}
public class ToDoController : ApiControllerWithHub<ToDoHub> {
private static List<ToDoItem> db = new List<ToDoItem> {
new ToDoItem { ID = 0, Title = "Do a silly demo on-stage at NDC" },
new ToDoItem { ID = 1, Title = "Wash the car" },
new ToDoItem { ID = 2, Title = "Get a haircut", Finished = true }
};
private static int lastId = db.Max(tdi => tdi.ID);
// Lines removed for brevity
public HttpResponseMessage PostNewToDoItem(ToDoItem item) {
lock (db) {
// Add item to the "database"
item.ID = Interlocked.Increment(ref lastId);
db.Add(item);
// Notify the connected clients
Hub.Clients.addItem(item);
// Return the new item, inside a 201 response
var response = Request.CreateResponse(HttpStatusCode.Created, item);
string link = Url.Link("apiRoute", new { controller = "todo", id = item.ID });
response.Headers.Location = new Uri(link);
return response;
}
}
// Lines removed for brevity
}
The full source code for the application which Brad demoed is also available: https://github.com/bradwilson/ndc2012.
The other option, which you don't prefer, is make your database to fire notifications as soon as data is changed. Then, you can pick that up and broadcast it through SignalR. Here is an example:
Database Change Notifications in ASP.NET using SignalR and SqlDependency
Sorry that this solution is not signalR, but maybe you can get ideas from it.
Here is the full example for download on GitHub
I am using MassTransit request and response with SignalR. The web site makes a request to a windows service that creates a file. When the file has been created the windows service will send a response message back to the web site. The web site will open the file and make it available for the users to see. I want to handle the scenario where the user closes the web page before the file is created. In that case I want the created file to be emailed to them.
Regardless of whether the user has closed the web page or not, the message handler for the response message will be run. What I want to be able to do is have some way of knowing within the response message handler that the web page has been closed. This is what I have done already. It doesnt work but it does illustrate my thinking. On the web page I have
$(window).unload(function () {
if (event.clientY < 0) {
// $.connection.hub.stop();
$.connection.exportcreate.setIsDisconnected();
}
});
exportcreate is my Hub name. In setIsDisconnected would I set a property on Caller? Lets say I successfully set a property to indicate that the web page has been closed. How do I find out that value in the response message handler. This is what it does now
protected void BasicResponseHandler(BasicResponse message)
{
string groupName = CorrelationIdGroupName(message.CorrelationId);
GetClients()[groupName].display(message.ExportGuid);
}
private static dynamic GetClients()
{
return AspNetHost.DependencyResolver.Resolve<IConnectionManager>().GetClients<ExportCreateHub>();
}
I am using the message correlation id as a group. Now for me the ExportGuid on the message is very important. That is used to identify the file. So if I am going to email the created file I have to do it within the response handler because I need the ExportGuid value. If I did store a value on Caller in my hub for the web page close, how would I access it in the response handler.
Just in case you need to know. display is defined on the web page as
exportCreate.display = function (guid) {
setTimeout(function () {
top.location.href = 'GetExport.ashx?guid=' + guid;
}, 500);
};
GetExport.ashx opens the file and returns it as a response.
Thank you,
Regards Ben
I think a better bet would be to implement proper connection handling. Specifically, have your hub implementing IDisconnect and IConnected. You would then have a mapping of connectionId to document Guid.
public Task Connect()
{
connectionManager.MapConnectionToUser(Context.ConnectionId, Context.User.Name);
}
public Task Disconnect()
{
var connectionId = Context.ConnectionId;
var docId = connectionManager.LookupDocumentId(connectionId);
if (docId != Guid.Empty)
{
var userName = connectionManager.GetUserFromConnectionId(connectionId);
var user = userRepository.GetUserByUserName(userName);
bus.Publish( new EmailDocumentToUserCommand(docId, user.Email));
}
}
// Call from client
public void GenerateDocument(ClientParameters docParameters)
{
var docId = Guid.NewGuid();
connectionManager.MapDocumentIdToConnection(Context.ConnectionId, docId);
var command = new CreateDocumentCommand(docParameters);
command.Correlationid = docId;
bus.Publish(command);
Caller.creatingDocument(docId);
}
// Acknowledge you got the doc.
// Call this from the display method on the client.
// If this is not called, the disconnect method will handle sending
// by email.
public void Ack(Guid docId)
{
connectionManager.UnmapDocumentFromConnectionId(connectionId, docId);
Caller.sendMessage("ok");
}
Of course this is from the top of my head.