I have the following WebApi constructor:
public MyController(ILogger<MyController> logger,
Channel channel,
AuditManager auditManager,
IMediator mediator)
{
_logger = logger;
_channel = channel;
_channel.EventFinished = async (o, e) =>
{
await mediator.Publish(new EventFinishedNotification(e);
};
_auditManager = auditManager;
}
Basically i'm subscribing to the _channel.EventFinished
event (which fires when one of my end points is called). When this occurs, on the call to mediator.Publish
i get a
Cannot access a disposed object. Object name: 'IServiceProvider'.'
exception. Any ideas why ?
CodePudding user response:
It happens, because you subscribing to IMediator
instance resolved from DI. Here compiler makes private class for delegate after =
and "caches" the instance of IMediator
inside of it.
When your controller method execution ends, DI is disposing container. And if event triggers after that, your method calling mediator.Publish
, which uses Service Locator pattern inside and tries to resolve required services from IServiceProvider
. Because it already disposed, you catch an exception.
There are several ways to handle it:
- Change way how it works and architecture, for example change classic events to in process event raising. (Of course it require additional code)
- Create hosted service for that purposes, which will works with DI and lifetime correctly, for example:
public sealed class ChannelHostedService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly Channel _channel;
// Resolve channel from DI or initialize it in constructor.
public ChannelHostedService(IServiceProvider serviceProvider, Channel channel)
{
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
_channel = channel ?? throw new ArgumentNullException(nameof(channel));
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
// Code for start channel listening.
// Our new subscriber.
_channel.EventFinished = async (o, e) =>
{
// Here we creating new scope from IServiceProvider to handle our event
using (var scope = _serviceProvider.CreateScope())
{
// Mediator resolves from our scope.
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
await mediator.Publish(new EventFinishedNotification(e);
}
};
}
}
Then register it in your DI container in Startup.cs
services.AddHostedService<ChannelHostedService>();
In this example we are injecting singleton IServiceProvider
in singleton BackgroundService
, and creating new service scopes for execution from IServiceProvider
. Obviously, in this case your Channel
should be a singleton.
I didn't see your code, and don't know what is Channel
and why you initializing subscription in controller. But this approach works fine for subscriptions, e.g. when we want to read events from message brokers: RabbitMq, Kafka, etc. We just initializing channel once in DI or in background service constructor, and then it works till our app is down.
Hope it helps.
CodePudding user response:
Ok just for anyone else's benefit it looks like calling the mediator publish from the callback initialised from the constructor as i was doing was the issue. I've wrapped that bit of code in a separate service and that works fine.