Note: In the hour I spent writing this post and in the process having a second and third look at everything, I found the solution to my problem. I decided to post this anyway, as I did not find the solution online and it might help others.
If you have any comments about my architecture, please do share them! My guts tell me I should not have stumbled across this problem in the first place, but I can't point my finger to a specific detail.
In my .NET 6 application I have a service which needs to access the database, thus use a DbContext
. The service is registered as singleton, but the DbContext
is not, as it should be. Quite a number of similar questions out there are answered with inject IServiceScopeFactory
and create (and dispose) your scope for each operation. example 1, example 2, example from Microsoft.
I try to do exactly that, but it throws ObjectDisposedException
s, so it obviously does not work in my case. The question is: why?
The service(s) I'm talking about are data access services for the rest of the application, the gateway to the database. They are registered as singletons, because they fire events consumed by background services for certain actions.
I moved the context and scope handling into an abstract class, that looks like this (stripped down):
(I obviously renamed all classes, I'm not working on a cat related application, if something does not add up, I might have forgotten to remane something )
public abstract class RepositoryBase
{
private readonly IServiceScopeFactory serviceScopeFactory;
protected RepositoryBase(IServiceScopeFactory serviceScopeFactory)
{
this.serviceScopeFactory = serviceScopeFactory;
}
protected void ExecuteInContext(Action<IAnimalContext> function)
{
using var scope = serviceScopeFactory.CreateScope();
function(scope.ServiceProvider.GetRequiredService<IAnimalContext>());
}
protected T ExecuteInContext<T>(Func<IAnimalContext, T> function)
{
using var scope = serviceScopeFactory.CreateScope();
return function(scope.ServiceProvider.GetRequiredService<IAnimalContext>());
}
}
This is then used like so:
internal class CatRepository : RepositoryBase, ICatRepository
{
public CatRepository(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory)
{ }
public event Action<Guid>? CatRenamed;
public IEnumerable<Cat> AllCats => ExecuteInContext(context => context.Cats);
public void RenameCat(Guid id, string name) => ExecuteInContext(context => {
var cat = context.Cats.FindOrThrow(id);
cat.Name = name;
context.SaveChanges();
CatRenamed?.Invoke(cat.id);
});
If I'm removing the using statements from the ExecuteInContext
functions, it behaves as expected (creating a new scope every time a repository action is called and never disposing the scopes).
Debugging with breakpoints (as well as log messages) in the ExecuteInContext
functions indicate that the actions are executed completely before the scope is disposed.
The stack trace of the exception has no functions or classes written by me in it. When calling CatRepository.AllCats
it looks something like this:
fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
An unhandled exception has occurred while executing the request.
System.ObjectDisposedException: Cannot access a disposed context instance. A common cause of this error is disposing a context instance 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 instance, or wrapping it in a using statement. If you are using dependency injectio
n, you should let the dependency injection container take care of disposing context instances.
Object name: 'LogisticsStateManagementContext'.
at Microsoft.EntityFrameworkCore.DbContext.CheckDisposed()
at Microsoft.EntityFrameworkCore.DbContext.get_ContextServices()
at Microsoft.EntityFrameworkCore.DbContext.get_InternalServiceProvider()
at Microsoft.EntityFrameworkCore.DbContext.get_ChangeTracker()
at Microsoft.EntityFrameworkCore.Query.CompiledQueryCacheKeyGenerator.GenerateCacheKeyCore(Expression query, Boolean async)
at Microsoft.EntityFrameworkCore.Query.RelationalCompiledQueryCacheKeyGenerator.GenerateCacheKeyCore(Expression query, Boolean async)
at Microsoft.EntityFrameworkCore.SqlServer.Query.Internal.SqlServerCompiledQueryCacheKeyGenerator.GenerateCacheKey(Expression query, Boolean async)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1.GetEnumerator()
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.IncludableQueryable`2.GetEnumerator()
at System.Linq.Enumerable.SelectEnumerableIterator`2.MoveNext()
at System.Text.Json.Serialization.Converters.IEnumerableDefaultConverter`2.OnWriteResume(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.JsonCollectionConverter`2.OnTryWrite(Utf8JsonWriter writer, TCollection value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.JsonConverter`1.TryWrite(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.JsonConverter`1.WriteCore(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.JsonConverter`1.WriteCoreAsObject(Utf8JsonWriter writer, Object value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.JsonSerializer.WriteCore[TValue](JsonConverter jsonConverter, Utf8JsonWriter writer, TValue& value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.JsonSerializer.WriteStreamAsync[TValue](Stream utf8Json, TValue value, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken)
at System.Text.Json.JsonSerializer.WriteStreamAsync[TValue](Stream utf8Json, TValue value, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken)
at System.Text.Json.JsonSerializer.WriteStreamAsync[TValue](Stream utf8Json, TValue value, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Mvc.Formatters.SystemTextJsonOutputFormatter.WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|30_0[TFilter,TFilterAsync](ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object
state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isComplet
ed)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
at Microsoft.AspNetCore.Authorization.Policy.AuthorizationMiddlewareResultHandler.HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authori
zeResult)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
The stack trace is where I found my solution, see answer below.