C
C#3d ago
bati

Cannot access a disposed context instance.

I'm receiving the ObjectDisposedException about a DbContext being disposed while using IDbContextFactory<> to initialize my database context. After searching online for potential ways for me to approach resolving this, none of them seem to apply for my case. Code:
logger.LogInformation("{Availability} Guild {Name} has just been created.", $"{(gatewayEvent.Guild.IsT0 ? "A" : "Una")}vailable", gatewayEvent.Guild.IsT0 ? gatewayEvent.Guild.AsT0.Name : gatewayEvent.Guild.AsT1.ID.ToString());

if (gatewayEvent.Guild.IsT1) return Result.FromSuccess();

var guild = gatewayEvent.Guild.AsT0;
var guildModel = new GuildModel(guild, configuration["PREFIX"]!);

await using (var databaseContext = await database.CreateDbContextAsync(ct))
{
var dbGuild = await databaseContext.Guilds.SingleByIdAsync(guild.ID, ct);

if (dbGuild is null) // Only add actual new guilds to the database as this fires on ALL guilds during startup
{
databaseContext.Guilds.Add(guildModel);
await databaseContext.SaveChangesAsync(ct);
} else guildModel = dbGuild;
}

using (var entry = memoryCache.CreateEntry(CacheKey.StringKey($"Guild:{guild.ID}")))
entry.Value = guildModel;

return Result.FromSuccess();
logger.LogInformation("{Availability} Guild {Name} has just been created.", $"{(gatewayEvent.Guild.IsT0 ? "A" : "Una")}vailable", gatewayEvent.Guild.IsT0 ? gatewayEvent.Guild.AsT0.Name : gatewayEvent.Guild.AsT1.ID.ToString());

if (gatewayEvent.Guild.IsT1) return Result.FromSuccess();

var guild = gatewayEvent.Guild.AsT0;
var guildModel = new GuildModel(guild, configuration["PREFIX"]!);

await using (var databaseContext = await database.CreateDbContextAsync(ct))
{
var dbGuild = await databaseContext.Guilds.SingleByIdAsync(guild.ID, ct);

if (dbGuild is null) // Only add actual new guilds to the database as this fires on ALL guilds during startup
{
databaseContext.Guilds.Add(guildModel);
await databaseContext.SaveChangesAsync(ct);
} else guildModel = dbGuild;
}

using (var entry = memoryCache.CreateEntry(CacheKey.StringKey($"Guild:{guild.ID}")))
entry.Value = guildModel;

return Result.FromSuccess();
-# Exception Trace is attached as .txt file.
17 Replies
Sossenbinder
Sossenbinder3d ago
Hmm, you're using a pooled dbcontext from what I can read from the stacktrace I suspect the disposal behaviour is connected to that Maybe you can shrink the potential error area by trying a non pooled context first
bati
batiOP3d ago
Yeah I do have pooling toggled on for my database config
bati
batiOP3d ago
Here's after disabling pooling:
bati
batiOP3d ago
Found the trace differences just now
With Pooling

at void Microsoft.EntityFrameworkCore.DbContext.Microsoft.EntityFrameworkCore.Infrastructure.IResettableService.ResetState()
at void Microsoft.EntityFrameworkCore.Internal.DbContextPool<TContext>.Return(IDbContextPoolable context)
at void Microsoft.EntityFrameworkCore.Internal.DbContextLease.Release()
at void Microsoft.EntityFrameworkCore.Internal.DbContextLease.ContextDisposed()
at void Microsoft.EntityFrameworkCore.DbContext.Dispose()

=================================================================================

Without Pooling

at async Task Microsoft.EntityFrameworkCore.DbContext.Microsoft.EntityFrameworkCore.Infrastructure.IResettableService.ResetStateAsync(CancellationToken cancellationToken)
at async ValueTask Microsoft.EntityFrameworkCore.Internal.DbContextPool<TContext>.ReturnAsync(IDbContextPoolable context, CancellationToken cancellationToken)
at async ValueTask Microsoft.EntityFrameworkCore.DbContext.DisposeAsync()
With Pooling

at void Microsoft.EntityFrameworkCore.DbContext.Microsoft.EntityFrameworkCore.Infrastructure.IResettableService.ResetState()
at void Microsoft.EntityFrameworkCore.Internal.DbContextPool<TContext>.Return(IDbContextPoolable context)
at void Microsoft.EntityFrameworkCore.Internal.DbContextLease.Release()
at void Microsoft.EntityFrameworkCore.Internal.DbContextLease.ContextDisposed()
at void Microsoft.EntityFrameworkCore.DbContext.Dispose()

=================================================================================

Without Pooling

at async Task Microsoft.EntityFrameworkCore.DbContext.Microsoft.EntityFrameworkCore.Infrastructure.IResettableService.ResetStateAsync(CancellationToken cancellationToken)
at async ValueTask Microsoft.EntityFrameworkCore.Internal.DbContextPool<TContext>.ReturnAsync(IDbContextPoolable context, CancellationToken cancellationToken)
at async ValueTask Microsoft.EntityFrameworkCore.DbContext.DisposeAsync()
Sehra
Sehra3d ago
did you create a service scope?
Sossenbinder
Sossenbinder3d ago
Hm, interesting, looking at https://github.com/dotnet/efcore/blob/main/src/EFCore/Internal/DbContextLease.cs#L111 it does seem like the stacktrace you're going down would only happen for a context which does use pooling. The only way the DbContextLease does use an instance with a pool is from within a pooled factory https://github.com/dotnet/efcore/blob/main/src/EFCore/Infrastructure/PooledDbContextFactory.cs#L69
bati
batiOP3d ago
I'll go ahead and send how I'm injecting the dbcontext factory
webApplicationBuilder.Services.AddPooledDbContextFactory<SionDbContext>((s, o) =>
{
#if MIGRATION
const string connectionString = "raw connection string for now";
#else
var configuration = s.GetRequiredService<IConfigurationRoot>();
var connectionString = new MySqlConnectionStringBuilder
{
Server = configuration["MARIADB_SERVER"] ?? "localhost",
Port = uint.Parse(configuration["MARIADB_PORT"] ?? "3306"),
// Pooling = true,
Database = configuration["MARIADB_NAME"],
UserID = configuration["MARIADB_USERNAME"] ?? "root",
Password = configuration["MARIADB_PASSWORD"]
}.ConnectionString;
#endif

o.UseMySql(connectionString, new MariaDbServerVersion(ServerVersion.AutoDetect(connectionString)));
o.UseLoggerFactory(s.GetRequiredService<ILoggerFactory>());
o.EnableDetailedErrors();
});
webApplicationBuilder.Services.AddPooledDbContextFactory<SionDbContext>((s, o) =>
{
#if MIGRATION
const string connectionString = "raw connection string for now";
#else
var configuration = s.GetRequiredService<IConfigurationRoot>();
var connectionString = new MySqlConnectionStringBuilder
{
Server = configuration["MARIADB_SERVER"] ?? "localhost",
Port = uint.Parse(configuration["MARIADB_PORT"] ?? "3306"),
// Pooling = true,
Database = configuration["MARIADB_NAME"],
UserID = configuration["MARIADB_USERNAME"] ?? "root",
Password = configuration["MARIADB_PASSWORD"]
}.ConnectionString;
#endif

o.UseMySql(connectionString, new MariaDbServerVersion(ServerVersion.AutoDetect(connectionString)));
o.UseLoggerFactory(s.GetRequiredService<ILoggerFactory>());
o.EnableDetailedErrors();
});
Ohh I need to use AddDbContextFactory<>() instead
Sehra
Sehra3d ago
can you register your responder with DI and ask for a SionDbContext in the constructor?
bati
batiOP3d ago
As in register the context?
Sehra
Sehra3d ago
Sion.Discord.Responders.GuildCreateResponder that one
bati
batiOP3d ago
That already is registered from another service
Sehra
Sehra3d ago
but registered as IResponder<Something>
bati
batiOP3d ago
Also, my error has been resolved by removing pooling
Sehra
Sehra3d ago
ok, cause i was looking at https://github.com/Remora/Remora.Discord/blob/main/Backend/Remora.Discord.Gateway/Services/ResponderDispatchService.cs#L303 and it creates a service scope and resolve responders, so accepting a SionDbContext in your constructor could work
GitHub
Remora.Discord/Backend/Remora.Discord.Gateway/Services/ResponderDis...
A data-oriented C# Discord library, focused on high-performance concurrency and robust design. - Remora/Remora.Discord
bati
batiOP3d ago
Not with a factory
Sehra
Sehra3d ago
no, there wouldn't be a need for factory in that case, it's more for apps that don't deal with service scopes so the lifetime would be handled by the service scope remora creates, and it would be disposed when your responder return
bati
batiOP3d ago
Ah I see where you're coming from

Did you find this page helpful?