Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
olegsAtQualtero opened this issue Jul 3, 2024 · 4 comments
Closed

Comments

@olegsAtQualtero
Copy link

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

Environment

  • .NET version: Migrating from 6 to 8
  • Npgsql version: 8.0.3
  • Npgsql.Json.NET: 8.0.3
  • Npgsql.NodaTime: 8.0.3
  • Npgsql.EntityFrameworkCore.PostgreSQL: 8.0.4
  • Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime: 8.0.4
  • NodaTime: 3.1.11
  • Microsoft.EntityFrameworkCore: 8.0.6

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

  • The application uses separate read and write contexts per tenant.
  • Connection strings are resolved at runtime based on tenant configuration.

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

@NinoFloris NinoFloris transferred this issue from npgsql/npgsql Jul 3, 2024
@roji
Copy link
Member

roji commented Jul 3, 2024

Duplicate of #3204

@roji roji marked this as a duplicate of #3204 Jul 3, 2024
@roji
Copy link
Member

roji commented Jul 3, 2024

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 #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 roji closed this as not planned Won't fix, can't repro, duplicate, stale Jul 3, 2024
@roji
Copy link
Member

roji commented Jul 3, 2024

@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
Copy link
Author

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);
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants