0

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 ObjectDisposedExceptions, 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.

Ardor
  • 154
  • 11

1 Answers1

0

The stack trace indicates, that the exception occurs during JSON serialization. It is not visible from my code examples, but the IEnumerable returned is actually an IQueryable. This means the database is only called once it is enumerated, which in this particular case only happens when I try to serialize the data to send it elsewhere. At that point the context is already disposed, but not GC'd, since the IQueryable has a reference to it.

I knew of this behaviour of IQueryable, just did not see the problem here, as the return value is IEnumerable<Cat>. I basically masked the reference to a context for myself and let it escape the scope in which the context exists.

So the general answer to my question is: Make sure no references to the context leave the scope.

The concrete answer in my example above would be to change this:

public IEnumerable<Cat> AllCats => ExecuteInContext(context => context.Cats);

to this

public IEnumerable<Cat> AllCats => ExecuteInContext(context => context.Cats.ToArray());

so that the enumeration is happening inside the scope where the context exists.

Ardor
  • 154
  • 11