Thomas
Thomas

Reputation: 12117

"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 }));
            }
        }
    }

Upvotes: 3

Views: 7075

Answers (4)

Ekus
Ekus

Reputation: 1880

I run into this error after adding overrides in my ClientHub class for OnConnectedAsync and OnDisconnectedAsync, e.g.:

public override Task OnDisconnectedAsync(Exception exception)
{
    // omitted: update list of clients in DB...
    Clients.Groups("...").SendAsync("ClientsChange", clients); // this line was failing because it was not awaited since my method was not async
}

The solution was to make it async and await the nested call referring to Clients:

public override async Task OnDisconnectedAsync(Exception exception)
{
    // omitted: update list of clients in DB...
    await Clients.Groups("...").SendAsync("ClientsChange", clients); // works now!
}

Upvotes: 0

ronenfe
ronenfe

Reputation: 2415

They changes something in the framework causing a bug.

Code that worked before the change:

 private ApplicationUser GetCurrentUser()
 {
    var userName = Context.User.Identity.GetUserName();
    var user = _userManager.FindByName<ApplicationUser, string> 
   (userName);
   return user
 }

Working code:

    private async Task<ApplicationUser> GetCurrentUserAsync()
    {
        // Use HttpContext.Current.GetOwinContext() to get the OWIN context in ASP.NET 4.8
        var userManager = HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>();
    
        // Get the current user's username from the authenticated identity
        var userName = Context.User.Identity.GetUserName();
    
        // Use FindByNameAsync to find the user by username asynchronously
        var user = await userManager.FindByNameAsync(userName);
        return user
    }

Upvotes: 0

Bruce
Bruce

Reputation: 23

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

Upvotes: 2

teroplut
teroplut

Reputation: 131

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

Upvotes: 7

Related Questions