Skip to main content

Your .NET app can handle a new request with an old tenant’s state.

Updated 2 min readMarkdown
Your .NET app can handle a new request with an old tenant’s state.

You replace AddDbContext with AddDbContextPool to reduce context-creation overhead.

A small performance improvement. What could go wrong?

In a multi-tenant application, the answer is surprisingly serious: a reused context can carry tenant-specific state into another request if you don’t explicitly refresh it. learn.microsoft

The subtle trap

Imagine your context contains this property:

public int TenantId { get; set; }

And your global query filter uses it:

modelBuilder.Entity<Order>()
    .HasQueryFilter(order => order.TenantId == TenantId);

Now consider this sequence:

  1. Tenant A’s request receives a context with TenantId = 101.
  2. The request finishes, and the context returns to the pool.
  3. Tenant B’s request receives that same context instance.
  4. Your application fails to overwrite TenantId.

The filter can now use Tenant A’s ID while serving Tenant B.

Pooling isn’t automatically unsafe. The mistake is assuming a reused context has fresh application-defined state. Microsoft explicitly calls out tenant IDs as state that needs special handling with pooled contexts. learn.microsoft

Why the obvious fix doesn’t work

“I’ll set the tenant in OnConfiguring.”

With context pooling, OnConfiguring runs only when that context instance is first created—not every time it is borrowed from the pool. It is therefore the wrong place for state that must change between requests. learn.microsoft

EF Core resets its internal context state. That does not mean it reinitializes your custom tenant property for the next caller. learn.microsoft

The safer pattern

Use a scoped factory that:

  • Resolves the tenant from trusted authentication data.
  • Obtains a context from the pooled factory.
  • Assigns the current tenant ID before returning the context.

The essential step looks like this:

public AppDbContext CreateDbContext()
{
    var context = _pooledFactory.CreateDbContext();

    // Validated tenant identity for this scope.
    context.TenantId = _tenantId;

    return context;
}

Microsoft documents this scoped-factory approach for multi-tenant applications using context pooling. Don’t accept an arbitrary client-supplied tenant ID as proof of authorization. learn.microsoft

The engineering lesson

Reuse changes the lifecycle assumptions your code can safely make.

A new request does not necessarily mean a new object.

Before enabling pooling, audit every piece of request-specific state attached to your context—not just the database queries. And remember: DbContext pooling and database connection pooling are separate mechanisms. You don’t need the former to benefit from the latter. learn.microsoft

#DotNet #EFCore #AspNetCore #MultiTenancy #SoftwareEngineering

2 views

No comments yet. Be the first to comment.