Wim Van Houts
Wim Van Houts

Reputation: 614

ASP.NET Core SignalR Clients object disposed exception when calling from a callback

I have the following hub implementation in a new and clean ASP.NET Core 2.1 Web Application. I just updated to the latest version of ASP.NET Core 2.1 and the latest version of Visual Studio 2017.

The class worked and when launched from the debugger, one client connects. I can see this with the debugger, and, I can see this in the client because I log the "userobject" I send after it connected. The client remains connected.

In a second step, I injected the IPbxConnection into the hub, which works as well (I can see a valid object with the debugger). The IPbxConnection implementation will call the OnUserUpdated handler after 5 seconds (I just do this with a timer callback now in the IPbxConnection implementation for testing). This always results in an object disposed exception thrown on the Clients object. How can I send a notification to all clients in this callback? It seems the Clients object does not keep it's state and is only valid during the message handlers... however, I want to push information to the client at all times.

public class PresenceHub : Hub
{
    //Members
    private IPbxConnection _connection; 

    /// <summary>
    /// Constructor, pbx connection must be provided by dependency injection
    /// </summary>        
    public PresenceHub(IPbxConnection connection)
    {
        _connection = connection;
        _connection.OnUserUpdated((e) =>
        {                
            Clients.All.SendAsync("UpdateUser", "updateuserobject");
        });
        _connection.Connect();
    }

    /// <summary>
    /// Called whenever a user is connected to the hub. We will send him all the user information
    /// </summary>
    public override async Task OnConnectedAsync()
    {            
        await base.OnConnectedAsync();
        await Clients.Caller.SendAsync("AddUser", "userobject");
    }
}

Upvotes: 10

Views: 11339

Answers (4)

user1489673
user1489673

Reputation:

This simple solution works for me. I haven't added anything extra to the start up class.

EDIT After some thought I decided that although this code works, it isn't a good pattern, not least because the static code ends up trying to use fields within a disposed object. A hub is a lightweight, short-lived container and should be used as such. I am therefore moving my long running process from static elements of the Hub into an IHostedService pattern

My hub contains a long running async process defined in a static member. Because Hubs are transient, on some occasions the hub is disposed when the async process tries to send messages. I have added a hub context for Injection into the Hub Constructor

public class IisLogFileHub : Hub
{
    IHubContext<IisLogFileHub> _hubContext = null;

    public IisLogFileHub(IHubContext<IisLogFileHub> hubContext)
    {
        _hubContext = hubContext;
    }
}

At any point in the long-running process, messages can be sent as follows

await _hubContext.Clients.All.SendAsync("ReceiveMessage", msg);

Upvotes: 9

Maarten Kieft
Maarten Kieft

Reputation: 7135

I was facing the same issue a while ago. I though too that the hub was the place to call when you c# backend needs to push a message, but actually this is not the case. The hub is more like a mounting point. Your c# backend should have its own client which connects to the hub as well. So your hub looks like this:

public class PresenceHub : Hub
{
    public async Task Send(Userobject userobject)
    {
        await Clients.Others.SendAsync("AddUser", userobject);
    }


    public override async Task OnConnectedAsync()
    {            
        await base.OnConnectedAsync();
        await Clients.Caller.SendAsync("AddUser", "userobject");
    }
}

and than a custom class / piece of code which calls the hub like this

public class MyCustomCode{
    public PresenceHub(IPbxConnection connection)
    {
        hubConnection = new HubConnectionBuilder().WithUrl("localhost\mysignalrhub").Build();
        hubConnection.StartAsync().Wait();

        _connection = connection;
        _connection.OnUserUpdated((e) =>
        {                
            hubConnection.InvokeAsync("Send", e).Wait();
        });
        _connection.Connect();
    }

}

Some documentation: https://learn.microsoft.com/en-us/aspnet/core/signalr/dotnet-client?view=aspnetcore-2.1

Upvotes: 0

Slizop
Slizop

Reputation: 101

As described in this issue on Github https://github.com/aspnet/SignalR/issues/2424 Hubs are short lived by design and are disposed after every invocation.

The only way I found to access your Clients outside of the current request Scope on your Hub is by injecting the HubContext into a seperate Class.

In your case a Broadcast to all Clients would look something like this

public class HubEventEmitter
{
    private IHubContext<PresenceHub> _hubContext;

    public HubEventEmitter(IPbxConnection connection, IHubContext<PresenceHub> hubContext)
    {
        _hubContext = hubContext;
        _connection.OnUserUpdated((e) =>
        {
            _hubContext.Clients.All.SendAsync("UpdateUser", "updateuserobject");
        });
    }
}

if you wanted to notify only specific Clients you'll have to collect the connectionId from the Context and use it like this

_hubContext.Clients.Client(connectionId).SendAsync("UpdateUser", "updateuserobject");

Upvotes: 10

Wim Van Houts
Wim Van Houts

Reputation: 614

The answer is implictly given by this issue on github https://github.com/aspnet/Docs/issues/6888. It was an "issue" in the documentation, so what I tried to do is for sure not possible (it does not mention how to do it however).

It seems the hub class instance in a whole only lives when a call has been made, and as such you can't use the callbacks.

Upvotes: 0

Related Questions