Table of Contents

DbContext Lifetime

A Discord bot is long-lived and handles many concurrent gateway events. DbContext is neither thread-safe nor designed to live for the bot's lifetime — its change tracker accumulates tracked entities with every operation and grows unbounded if the context is never disposed.

The right pattern: IDbContextFactory

Register a context factory rather than a context instance:

services.AddDbContextFactory<MyBotContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Bot")));

Then resolve the factory (typically via constructor injection) and create a short-lived context per unit of work — one per gateway event, one per command, one per background task iteration:

await using var db = await factory.CreateDbContextAsync();

db.Guilds.Add(new GuildEntity { Id = guildId, Name = name, OwnerId = ownerId });
await db.SaveChangesAsync();
// db is disposed here — change tracker is discarded

await using disposes the context (and its change tracker) as soon as the work is done. Because each context is independent, concurrent gateway handlers can each hold their own context without sharing state.

What goes wrong with a long-lived context

  • Memory leak — the change tracker accumulates every entity it has ever seen until the context is disposed.
  • Stale data — EF Core returns cached entities from the first-level cache rather than re-querying, which can cause your handlers to see outdated state.
  • Thread-safety violationsDbContext is not thread-safe; concurrent access to a shared instance causes unpredictable failures.

See also

  • Getting Started — end-to-end setup including the factory registration and a complete first-write example.