"Cannot access a disposed object" crash in SignalR - c#

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

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

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

Related

Scoped service added is producing null error over the same request within middleware

I'm creating a piece of custom middleware that is going to used for telemetry purposes. This middleware is going to have a service that it uses to keep track of the different logs generated by my API, and once the request response is triggered it'll send off the traces to AppInsights.
The Invoke method of the middleware looks like this:
public async Task InvokeAsync(HttpContext context)
{
_telemetryService.InitNewEvent();
_telemetryService.Add("path", context.Request.Path, null); // Add works here
await _next(context);
_telemetryService.Add("statusCode", context.Response.StatusCode, null); // Add throws null error here
_telemetryService.SendEvent(context.Response);
}
Telemetry Service Excerpt
public class TelemetryService
{
private TelemetryClient _telemetryClient;
private List<TelemetryTrace> _currentTelemetryEventData;
private Dictionary<string, string> _finalisedTelemetryEventData;
private string _eventName;
private string _apiName = "pmapi";
public TelemetryService(TelemetryClient telemetryClient)
{
_telemetryClient = telemetryClient;
}
public void InitNewEvent()
{
_currentTelemetryEventData = new List<TelemetryTrace>();
Console.WriteLine("New telemetry event inited");
}
public void Add(string key, object data, SeverityLevel? severityLevel)
{
_currentTelemetryEventData.Add(new TelemetryTrace
{
Key = key,
Data = data,
SeverityLevel = severityLevel
});
}
}
A new event, which is just a dictionary, is inited by the middleware and a log it added to it. The middleware then awaits the response from the pipeline, during this time the API controller will add other logs to the event, and then the status code is added to the event and the event is sent when the pipeline returns to the middleware.
However I've noticed that if I add the telemetryService as a scoped or transient service the dictionary that holds the log data is set back to null after await _next(context) has been called.
This makes sense for transient, but with the definition of scoped being Scoped objects are the same within a request, but different across different requests. I expected that my dictionary state would be kept. But that only occurs with a singleton. Why isn't this happening the way I'm expecting?

Dependency Injection not resolving fast enough for use when a service relies on another service

I am injecting two services into my dot net core web api, the main service relies on data in the helper service. The helper service populates this data in the constructor, however when the main service goes to use this data it is not ready because the constructor of the helper service has not finished by the time it is needed.
I thought DI and the compiler would resolve and chain these services properly so the helper service would not be used until it was fully instantiated.
How I tell the main service to wait until the helper service is fully resolved and instantiated?
Generic sample code of what I am doing. I call the DoSomething() in MainSerice the HelperService calls out to an external API to get some data, that data is needed in the MainService.
StartUp.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHelperService, HelperService);
services.Scoped<IMainService, MainService);
}
MainService.cs
public class MainService : IMainService
{
private readonly IHelperServuce _helper;
public MainService(IHelperService HelperService)
{
_helper = HelperService;
}
public void DoSomething()
{
string helperParameter = _helper.Param1; //This fails because the constructor of HelperService has not finished
}
}
HelperService.cs
public class HelperService : IHelperService
{
public HelperService()
{
GetParamData();
}
private async void GetParamData()
{
var response = await CallExternalAPIForParameters(); //This may take a second.
Params1 = response.value;
}
private string _param1;
public string Param1
{
get
{
return _param1;
}
private set
{
_param1 = value;
}
}
}
You are not awaiting the async method GetParamData() data in the constructor. That is, ofcourse, not possible. Your constructor should only initialize simple data. You could fix this by, instead of using a property to return, you could also return a Task from a method called (for example) Task<string> GetParam1(). Which could cache the string value.
for example:
public class HelperService : IHelperService
{
private string _param1;
// note: this is not threadsafe.
public async Task<string> GetParam1()
{
if(_param1 != null)
return _param1;
var response = await CallExternalAPIForParameters(); //This may take a second.
_params1 = response.value;
return _param1;
}
}
You could even return a ValueTask<string> because most of the calls can be executed synchronously.
Pass a lambda to the helper service that initializes the variable in your main service, as in...
Helper service.getfirstparam( (response) ->
{ firstparam = response.data;});
While (firstparam == null)
sleep
// now do your processing

Call Client method outside SignalR Project

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

Autofac same injected instance not received when manually resolving an InstancePerRequest type

I'm trying to have a transaction ID which will be used for logging throughout the request. Trasaction ID is a property in AuditContext, which in turn will be a singleton per request. I've the below code in Global.asax.cs
builder.RegisterType<AuditContext>().As<IAuditContext>().InstancePerRequest();
....
GlobalContainer = builder.Build();
config.DependencyResolver = new AutofacWebApiDependencyResolver(GlobalContainer);
Transaction ID is set in a base api class.
public BaseApiController(IAuditContext auditContext, IDispatcher dispatcher = null)
{
auditContext.TransactionID = Guid.NewGuid().ToString();
}
AuditContext object with the correct transaction ID is injected, in below constructor injection usage.
public class SapEventLogger : ISapEventLogger
{
private IAuditContext auditContext;
public SapEventLogger(IAuditContext auditContext)
{
this.auditContext = auditContext; //this auditContext object has correct transaction ID
}
}
In case of any exception I want to retrieve the transaction ID and log it. But when I try to manually resolve, I get a new AuditContext object in which the transaction ID is null.
public class ServiceExceptionHandler : ExceptionHandler
{
public override void Handle(ExceptionHandlerContext context)
{
using (var scope = GlobalContainer.BeginLifetimeScope(Autofac.Core.Lifetime.MatchingScopeLifetimeTags.RequestLifetimeScopeTag))
{
var auditContext = scope.Resolve<FDP.Services.Common.IAuditContext>();
transactionID = auditContext.TransactionID; //Transaction ID is null here
}
}
}
Not sure why a new object is created when resolving AuditContext manually. Am I missing something?
With the BeginLifetimeScope call you're manually creating a second request lifetime rather than using the existing request lifetime. Two request lifetimes equals two instances.
The solution to this is outlined in the Autofac docs in more detail but the short version is to get the request lifetime from the request message in the handler context:
// Get the request lifetime scope so you can resolve services.
var requestScope = context.Request.GetDependencyScope();
// Resolve the service you want to use.
var auditContext = requestScope.GetService(typeof(IAuditContext)) as IAuditContext;
// Do the rest of the work in the filter.
auditContext.DoWork();

SignalR group registration server methods not being hit

I've followed this guide, ASP.NET SignalR Hubs API Guide (How to manage group membership from the Hub class) and yet am unable to get my server side ShipmentHub methods to execute.
My ShipmentHub class looks like this:
public class ShipmentHub : Hub
{
IShipmentLogic shipmentLogic;
public ShipmentHub(IShipmentLogic shipmentLogic)
{
this.shipmentLogic = shipmentLogic;
}
public void CreateShipment(IEnumerable<Shipment> shipments)
{
// Clients.All.createShipment(shipments.OrderByDescending(s => s.CreatedDate));
Clients.Group(shipments.FirstOrDefault().ShipmentId)
.createShipment(shipments.OrderByDescending(s => s.CreatedDate));
}
public async Task WatchShipmentId(string shipmentId)
{
await Groups.Add(Context.ConnectionId, shipmentId);
Clients.Group(shipmentId).createShipment(shipmentLogic.Get(shipmentId, true));
}
public Task StopWatchingShipmentId(string shipmentId)
{
return Groups.Remove(Context.ConnectionId, shipmentId);
}
}
My client, more or less, looks like this:
var shipmentHub = $.connection.shipmentHub;
$.connection.hub.logging = true;
$.connection.hub.start();
var shipmentId = "SHP-W-GE-100122";
if (previousShipmentId) {
shipmentHub.server.stopWatchingShipmentId(previousShipmentId);
}
if (shipmentId.length) {
previousShipmentId = shipmentId;
shipmentHub.server.watchShipmentId(shipmentId);
}
In the SignalR client logs I see that these are being called:
SignalR: Invoking shipmenthub.WatchShipmentId
SignalR: Invoking shipmenthub.StopWatchingShipmentId
SignalR: Invoking shipmenthub.WatchShipmentId
And, aside from just the logs, these methods are being hit:
proxies['shipmentHub'].server = {
createShipment: function (shipments) {
return proxies['shipmentHub'].invoke.apply(proxies['shipmentHub'], $.merge(["CreateShipment"], $.makeArray(arguments)));
},
stopWatchingShipmentId: function (shipmentId) {
return proxies['shipmentHub'].invoke.apply(proxies['shipmentHub'], $.merge(["StopWatchingShipmentId"], $.makeArray(arguments)));
},
watchShipmentId: function (shipmentId) {
return proxies['shipmentHub'].invoke.apply(proxies['shipmentHub'], $.merge(["WatchShipmentId"], $.makeArray(arguments)));
}
};
And, as a final note, before I added the Watch and StopWatching methods, everything else worked (i.e., CreateShipment would call the Client.All.createShipment method without issue).
You need to wait for the connection to the server to be established before you can start calling methods on the server from the client. hub.start() returns a promise, here is the basic pattern for doing something once that promise is resolved.
var shipmentHub = $.connection.shipmentHub;
$.connection.hub.logging = true;
$.connection.hub.start().done(talkToServer);
var talkToServer=function(){
var shipmentId = "SHP-W-GE-100122";
if (previousShipmentId) {
shipmentHub.server.stopWatchingShipmentId(previousShipmentId);
}
if (shipmentId.length) {
previousShipmentId = shipmentId;
shipmentHub.server.watchShipmentId(shipmentId);
}
}
The issue is due to the parameterized constructor in ShipmentHub. According to Dependency Injection in SignalR:
By default, SignalR expects a hub class to have a parameterless constructor. However, you can easily register a function to create hub instances, and use this function to perform DI. Register the function by calling GlobalHost.DependencyResolver.Register.
So, you need to modify your Startup.Configuration(IAppBuilder app) method to resolve the dependency for you:
GlobalHost
.DependencyResolver
.Register(
typeof(ShipmentHub),
() => new ShipmentHub(new ShipmentLogic()));

Categories

Resources