-1

I have a fairly large application where one handler will call another handler. I have read how this is not something we should be doing:

However, refactoring the handlers so that we don't have them calling each other would be a large undertaking.

That being said, I would like to simplify a bit how handlers use Mediator to call the other handlers.

Here is an example of what a handlers would look like:

private readonly ISender _mediator;
private readonly UserHelper _userHelper;

public Handler(ISender mediator, UserHelper userHelper) {
  _mediator = mediator;
  _userHelper = userHelper;
}

public async Task<CreateUserResponse> Handle(CreateUserRequest request) {
      var response = await _mediator.Send(new CustomBizLogicRequest {
        OrgId = request.OrganizationId,
      });
 // ...

Using this approach works, but I would like to change it to use a helper class that would take care of using Mediator. Something like:

public class UserHelper {
  private readonly ISender _mediator;

  public UserHelper(ISender mediator) {
    _mediator = mediator;
  }

  public async Task<Result<CustomBizLogicResponse>> CustomBizLogicAsync(int orgId) =>
    await _mediator.Send(new CustomBizLogicRequest {
      OrgId = orgId,
    });

This way we could simplify the call from the Handler to use the injected `UserHelper:

      response = await _userHelper.CustomBizLogicAsync(request.OrganizationId);

When I try to use the helper method, I get:

Cannot resolve 'MediatR.IRequestHandler`2[CustomBizLogicRequest,CustomBizLogicResponse]]' from root provider because it requires scoped service 'DbContext'.

The handler for CustomBizLogic expects DbContext, which I registered as:

services.AddDbContext<DbContext>(opt => {
  //...
});

I did find this answer for a similar issue, but that one is a BackgroundService, instead of a web host, so I think the lifetime for DbContext would be different.

I'm confused why calling the hander from another handler using Mediator directly works, but moving the call to the helper class somehow changes the scope.

Da Ta
  • 45
  • 3
  • See following : https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/?force_isolation=true – jdweng Oct 27 '22 at 16:55
  • 1
    Worth adding more code to the question, for example code which shows how `UserHelper` is created – Evk Oct 28 '22 at 06:45
  • As requested, I added code to show how `UserHelper` gets created, which is just constructor injection. – Da Ta Oct 31 '22 at 16:06

1 Answers1

2

The instance created over the DI has the same scope (IServiceProvider)

The one difference between a BackgroundSerivce and aspnet-controllers is that aspnet creates a new ServiceScope for every http-request, This ServiceScope is used to create the Controller instance itself and the DbContext too.

The result is that all services registered as scoped have the same lifetime as the http-request.

Because DbContext is a Scoped service, it requires not to be created from the root scope or else it would be effectively a singleton. DbContext should be disposed/dereferenced after every process/usage/request.

For a BackgroundService and all other locations that require to create or get scopaed services from the DI or require to create new instances with DI support, need to create and dispose a new IServiceScope.

Create a new scope in the constructore or start method:

_scope = serviceProvider.CreateScope();

After you can request registered scoped services over it:

var ctx = _scope.ServiceProvider.GetRequiredService<MyDbContext>();

You can unregistred instances too:

var helper = ActivatorUtilities.CreateInstance<UserHelper>(_scope.ServiceProvider);

After the process or background services stops, dispose _scope and all scoped services including the single instance of MyDbContext will get disposed.