Subtle breaking change when adding DbContextPool Entity Framework Core 6

During the upgrade process of one of our applications from .NET Core 3.1 to .NET 6.0, I stumbled across a very subtle breaking changing when using the AddDbContextPool<TContextService,TContextImplementation>() feature of EF Core. I thought it might be worthwhile to document this, in case someone else is troubled by it too.

Use case

Consider the following code, making use of AddDbContextPool in EF Core.

This code registers a DbContext for different tenants in the DI container, where each tenant supplies its own version of the context (the TContextService on the registration method), as a subclass of AppDbContext, which, in semi-pseudo-code looks as follows:

The multi-tenancy here is merely an example, and the point it illustrates is that the problem arises when one has to deal with multiple isolated variants of the same base DbContexts, but pointing at different SQL DBs. A similar situation arises e.g. when you might have different DB instance per geographical location, and you performing data residency segregation manually in the code.

In the specific above case, the tenants are then registered in a strongly typed fashion as:

At this point you could inject a collection of all the registered AppDbContext, so one per tenant, and use it however you wish.

In EF Core 3.1, when calling AddDbContextPool<AppDbContext, TDbContext>() multiple times, each of the implementations, so each TDbContext, would be registered in the DI container both as itself and against the AppDbContext. This of course makes it possible to inject the collection of base class and receive every implementation, in this case, a different one for each of the tenants.

Breaking change

When upgrading to .NET 6.0, the TenantDbFactory no longer receives all of the implementations, but only the first one, in our example FooDbContext! Now, depending on the scenario, this subtle breaking change may have disastrous consequences, as it does not manifest itself at compile time.

The closer inspection of the EF Core codebase, revealed that this behavior change was introduced, in what appears to be, an unrelated PR. The code was updated from AddScoped(…) to TryAddScoped(…) when registering the TContextService (in our case, the AppDbContext). Since the Try… variant only allows a single registration of a given sort, as a result, only the first tenant context ended up in the DI container, registered against AppDbContext (it was still registered as itself, but, remember that it was not our usage pattern).

Mitigation

The quickest mitigation is to change from using AddDbContextPool<TContextService,TContextImplementation>() to its sibling variant AddDbContextPool<TContext>() and then perform the registration against the common base context (in our example, the AppDbContext) manually. This is shown in the updated RegisterTenant() method below:

This restores the original behavior.