npgsql / efcore.pg

Entity Framework Core provider for PostgreSQL
PostgreSQL License
1.56k stars 225 forks source link

Migration to .NET 8: Period Mapping and IServiceProvider Limit Issue #3217

Closed olegsAtQualtero closed 4 months ago

olegsAtQualtero commented 4 months ago

Migration to .NET 8: Period Mapping and IServiceProvider Limit Issue

Environment

Description

I'm migrating multi-tenant services from .NET 6 to 8 and encountering issues with Period mapping and exceeding the IServiceProvider instance limit. Each tenant has a dedicated DB with read-and-write replicas (Domain and ReadModel DBcontexts).

Current Implementation

Based on this response, I updated my code as follows:

Domain Model

public class MyModel
{
    // ...
    public Period? Duration { get; private set; }
}

Service Registration

services.AddDbContext<MyDomainModelContext>(
    options =>
    {
       //Tenant connection string is resolved at runtime
       //A DbContext can serves multiple tenants
        options.UseNpgsql(o => o.UseNodaTime());
    });

DB Context Configuration

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    //resolved tenant connection string from SSM.
    var connectionString = dbConfig.Connection.ConnectionString;

    var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
    dataSourceBuilder.UseNodaTime();
    var dataSource = dataSourceBuilder.Build();

    optionsBuilder.UseNpgsql(dataSource);
}

Issue

While the Period mapping now works, I'm encountering the following error:

More than twenty 'IServiceProvider' instances have been created for internal use by Entity Framework.

Even if I cache NpgsqlDataSourceBuilder instances per connection string, their number can still exceed the limit due to multiple tenants.

Questions

  1. Is there a recommended approach to handle Period mapping in a multi-tenant environment with .NET 8 and Npgsql?
  2. How can I avoid exceeding the IServiceProvider instance limit while maintaining separate contexts for each tenant?
  3. Are there any best practices for configuring Npgsql and EF Core in a multi-tenant scenario that I should be aware of?

Additional Context

Any guidance or suggestions would be greatly appreciated. Thank you!

roji commented 4 months ago

Duplicate of #3204

roji commented 4 months ago

tl;dr that warning just means that you're instantiating more than some fix number of service providers, to help users catch the scenario where they're creating a service provider for each and every DbContext (which would be very bad). If you have a fixed number of tenants, and each tenant has a single NpgsqlDataSource (which, before https://github.com/npgsql/efcore.pg/issues/3086, causes a single EF service provider to be created), then you can safely suppress the warning and just proceed. Just make sure you're not creating a data source every single time you're instantiating a DbContext etc.

roji commented 4 months ago

@NinoFloris turned my attention to this:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    //resolved tenant connection string from SSM.
    var connectionString = dbConfig.Connection.ConnectionString;

    var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
    dataSourceBuilder.UseNodaTime();
    var dataSource = dataSourceBuilder.Build(); // THIS IS BAD

    optionsBuilder.UseNpgsql(dataSource);
}

Since OnConfiguring gets called for each DbContext instance, that means a data source gets built here for each and every context instance; this is a serious problem, as e.g. each data source represents a connection pool (so this effectively kills connection pooling).

You need to create the data source outside of OnConfiguring and pass an already-constructed NpgsqlDataSource to UseNpgsql.

olegsAtQualtero commented 4 months ago

Thanks @roji I wrote a data source resolver registered as a singleton, so now data sources are created only once:

DbContext:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        var connectionString = dbConfig
            .Connection
            .ConnectionString;

        var dataSource = dataSourceResolver.Resolve(connectionString, BuildDataSource);
        optionsBuilder.UseNpgsql(dataSource);
    }

public class NpgsqlDataSourceResolver : INpgsqlDataSourceResolver
{
    private readonly ConcurrentDictionary<string, NpgsqlDataSource> dataSources = new();

    public NpgsqlDataSource Resolve(string connectionString, Func<string, NpgsqlDataSource> factory)
    {
        return dataSources.GetOrAdd(connectionString, factory);
    }
}