I am having a weird problem in one of my microservice web api. My async GET methods throw a Cannot access a disposed object exception for my DbContext except for the very first time they are invoked. I tried looking online for an answer but nothing worked. I made sure my methods are not async void and I await the necessary calls. Since my POST and DELETE methods work fine, I am fairly certain that the real culprit is the IMapper instance. I think it might always point to the first instance of the DbContext and that is why the works the first time but not the ones after. Any help or pointers would be gladly appreciated
Here are some snapshots of the code.
Startup.cs
...
// Add AutoMapper
services.AddAutoMapper(new Assembly[] { typeof(AutoMapperProfile).GetTypeInfo().Assembly });
// Add DbContext using NoSQL Server Provider
services.AddDbContext<ProfileDbContext>(options =>
options.UseMongoDb(Configuration.GetConnectionString("TeamJobProfilesDatabase")));
...
MyController.cs
// GET api/profiles
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<ProfilesListViewModel>> GetAll()
{
return Ok(await Mediator.Send(new GetAllProfilesQuery()));
}
// GET api/profiles/{id}
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ProfileViewModel>> Get(int id)
{
return Ok(await Mediator.Send(new GetProfileQuery { Id = id }));
}
GetAllProfilesQueryHandler.cs
public class GetAllProfilesQueryHandler : IRequestHandler<GetAllProfilesQuery, ProfilesListViewModel>
{
private readonly ProfileDbContext _context;
private readonly IMapper _mapper;
public GetAllProfilesQueryHandler(ProfileDbContext context, IMapper mapper)
{
_context = context;
_mapper = mapper;
}
public async Task<ProfilesListViewModel> Handle(GetAllProfilesQuery request, CancellationToken cancellationToken)
{
return new ProfilesListViewModel
{
Profiles = await _context.Profiles.ProjectTo<ProfileLookupModel>(_mapper.ConfigurationProvider).ToListAsync(cancellationToken)
};
}
}
ProfileDbContext.cs
[MongoDatabase("profileDb")]
public class ProfileDbContext : DbContext
{
public ProfileDbContext(DbContextOptions<ProfileDbContext> options)
: base(options)
{
}
public DbSet<Domain.Entities.Profile> Profiles { get; set; }
}
Exception message
{ "error": [ "Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling Dispose() on the context, or wrapping the context in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.\r\nObject name: 'ProfileDbContext'." ], "stackTrace": " at Microsoft.EntityFrameworkCore.DbContext.CheckDisposed()\r\n at Microsoft.EntityFrameworkCore.DbContext.get_InternalServiceProvider()\r\n at Microsoft.EntityFrameworkCore.DbContext.get_ChangeTracker()\r\n at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompilationContextFactory.get_TrackQueryResults()\r\n at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompilationContextFactory.Create(Boolean async)\r\n at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](QueryModel queryModel)\r\n at Blueshift.EntityFrameworkCore.MongoDB.Storage.MongoDbDatabase.<>c__DisplayClass11_0
1.<CompileAsyncQuery>b__0(QueryContext queryContext)\r\n at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query)\r\n at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression)\r\n at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable
1.System.Collections.Generic.IAsyncEnumerable.GetEnumerator()\r\n at System.Linq.AsyncEnumerable.Aggregate_[TSource,TAccumulate,TResult](IAsyncEnumerable1 source, TAccumulate seed, Func
3 accumulator, Func2 resultSelector, CancellationToken cancellationToken) in D:\\a\\1\\s\\Ix.NET\\Source\\System.Interactive.Async\\Aggregate.cs:line 118\r\n at Profile.Application.Profiles.Queries.GetAllProfiles.GetAllProfilesQueryHandler.Handle(GetAllProfilesQuery request, CancellationToken cancellationToken) in C:\\Users\\Adam\\Repositories\\TeamJob\\TeamJob\\src\\Services\\Profile\\Profile.Application\\Profiles\\Queries\\GetAllProfiles\\GetAllProfilesQueryHandler.cs:line 24\r\n at MediatR.Pipeline.RequestPostProcessorBehavior
2.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate1 next)\r\n at MediatR.Pipeline.RequestPreProcessorBehavior
2.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate1 next)\r\n at MediatR.Pipeline.RequestPreProcessorBehavior
2.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate1 next)\r\n at Profile.API.Controllers.ProfilesController.GetAll() in C:\\Users\\Adam\\Repositories\\TeamJob\\TeamJob\\src\\Services\\Profile\\Profile.API\\Controllers\\ProfilesController.cs:line 19\r\n at lambda_method(Closure , Object )\r\n at Microsoft.Extensions.Internal.ObjectMethodExecutorAwaitable.Awaiter.GetResult()\r\n at Microsoft.AspNetCore.Mvc.Internal.ActionMethodExecutor.AwaitableObjectResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)\r\n at System.Threading.Tasks.ValueTask
1.get_Result()\r\n at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeActionMethodAsync()\r\n at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeNextActionFilterAsync()\r\n at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)\r\n at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)\r\n at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync()\r\n at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextExceptionFilterAsync()" }
The problem is in Mediator.Send method.
Mediator
class stores request handlers in a static ConcurrentDictionaryand when the
Send
method is invoked, it usesGetOrAdd
method on that dictionary.This means, if the request handler instance doesn't already exist in the dictionary, it creates a new instance (using
Activator
) and adds it to the dictionary, but if the request handler instance already exists in the dictionary, it uses the existing one (and this is the cause of your problem).So, what exactly is causing your error?
The
_requestHandlers
dictionary isstatic
, which means it lives through multiple requests, i.e. doesn't get disposed/garbage collected at the end of the request. YourProfileDbContext
, when registered usingAddDbContext
method, has a scoped lifetime, which means it is created once per request (and disposed at the end of the request). This means you can end up in a situation where_requestHandlers
dictionary contains an instance ofGetAllProfilesQueryHandler
that has a reference on an exposed instance ofProfileDbContext
.Here is what happens:
Mediator.Send(new GetProfileQuery { Id = id })
gets called.Mediator.Send(new GetProfileQuery { Id = id })
doesn't findGetAllProfilesQueryHandler
in_requestHandlers
dictionary, so it instantiates it and also resolves itsProfileDbContext
dependency._context
field (ProfileDbContext
) in yourGetAllProfilesQueryHandler
indsance gets disposed (because it has ascoped
lifetime), but the_requestHandlers
dictionary (containingGetAllProfilesQueryHandler
instance) doesn't get disposed (because it is static).Mediator.Send(new GetProfileQuery { Id = id })
gets called again.Mediator.Send(new GetProfileQuery { Id = id })
findsGetAllProfilesQueryHandler
instance in_requestHandlers
dictionary and uses the existing instance, whose_context
field is disposed at the end of the previous request.GetAllProfilesQueryHandler
tries to access the disposed_context
field, and gets the "Cannot access a disposed object" error.Possible solution
Don't let
Mediator.Send
resolveGetAllProfilesQueryHandler
s dependencies.Maybe pass
IServiceProvider serviceProvider
to yourGetAllProfilesQueryHandler
and let it resolve its dependencies as needed:Edit:
As @Lucian Bargaoanu pointed out in comments, you can resolve handlers through DI, as in https://github.com/jbogard/MediatR.Extensions.Microsoft.DependencyInjection