Repository navigation
Issue Configuring EnableDynamicJson() on Data Source with a Multi-Tenant API Leveraging Postgres Row-Level Security #3239
Description
We're encountering an issue with our multi-tenant API that uses PostgreSQL with row-level security and JSONB columns. We're trying to configure Npgsql to handle JSONB serialization while maintaining our multi-tenant setup, but we're running into problems with excessive IServiceProvider instances being created.
Environment
- .NET Core 8
- Entity Framework Core 8.0.7
- Npgsql 8.0.4
- PostgreSQL 16.3
Repository With Reproducible Issue
A repository where you can reproduce the issue can be found here: https://github.com/MichaelCharles/ExampleMultiTenancyAPIWithRowLevelSecurity
Current Setup
Our API uses a multi-tenant architecture with the following key features:
- PostgreSQL database with row-level security
- JWT-based tenant identification
- JSONB columns for flexible data storage
We determine the user's tenant through middleware that reads a JWT token and sets the tenant in HttpContext.Items. In the DbContext, we read this tenant and use it to set the username in the connection string, which Postgres then uses for row-level security.
The connection string is derived as follows,
private static string GetConnectionString(
IConfiguration configuration,
IHttpContextAccessor? httpContextAccessor
)
{
var host = configuration.GetValue<string>("Database:Host");
var database = configuration.GetValue<string>("Database:Name");
string? user = configuration.GetValue<string>("Database:Username");
string? password = configuration.GetValue<string>("Database:Password");
var httpContext = httpContextAccessor?.HttpContext;
if (httpContext != null && httpContext.Items.ContainsKey("TenantId"))
{
string? tenantId = httpContext.Items["TenantId"] as string;
user = $"user{tenantId}";
}
return $"Host={host}; Database={database}; Username={user}; Password={password};";
}Since we have JSONB columns which are serialized into C# classes, we need to explicitly enable dynamic JSON. We're currently doing that globally like so,
NpgsqlConnection.GlobalTypeMapper.EnableDynamicJson();But this results in a warning saying that global setting of EnableDynamicJson() has been deprecated since .NET 7.
The Problem
We need to explicitly configure JSONB serialization. The recommended approach is to use NpgsqlDataSourceBuilder, but this is causing issues with our multi-tenant setup. If I create the data source and set it during startup, we lose the ability to flexibly change the connection string per request to leverage row-level security. If I create the data source within our DbContext, after about 20 requests I'm hit with a warning that Entity Framework is managing too many instances of IServiceProvider.
Attempted Solutions
- Per-Context Configuration
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var dataSourceBuilder = new NpgsqlDataSourceBuilder(GetConnectionString());
dataSourceBuilder.EnableDynamicJson();
optionsBuilder.UseNpgsql(dataSourceBuilder.Build());
}This causes the "More than twenty 'IServiceProvider' instances" error after multiple API calls.
- Startup Configuration
var config = services.BuildServiceProvider().GetRequiredService<IConfiguration>();
var httpContextAccessor = services
.BuildServiceProvider()
.GetRequiredService<IHttpContextAccessor>();
var dataSourceBuilder = new NpgsqlDataSourceBuilder(
GetConnectionString(config, httpContextAccessor)
);
dataSourceBuilder.EnableDynamicJson();
var dataSource = dataSourceBuilder.Build();
services.AddDbContext<AppDbContext>(options =>
{
options.UseNpgsql(dataSource);
});Configuring NpgsqlDataSourceBuilder during app startup works but prevents dynamic tenant-specific connection strings, breaking row-level security. The connection string is set once during startup, when no HttpContext is available, and so rather than being set to the per-tenant database user, it gets set to the default user.
Question
I'm looking for a way to,
- Use the recommended
NpgsqlDataSourceBuilderapproach - Allow setting a different connection string per request based upon the "TenantId" set in the HttpContext to allow for row-level security
- Avoid creating excessive
IServiceProviderinstances
Is there a recommended approach to handle this specific setup (multi-tenancy with row-level security and JSONB columns) with the latest Npgsql and EF Core versions?
Activity
roji commentedon Sep 30, 2024
Sorry for the very late response; this question has come up before, I'll try to put some time into documenting the recommendations (see npgsql/doc#367).
If different tenants require different connection strings (because each tenant uses a different PG user), that means you need to instantiate a new NpgsqlDataSource per tenant; the general idea is outlined in npgsql/npgsql#4523. You basically want to have a singleton registry of data sources in DI, with the key being the tenant ID; when you need to do something with the database, you get the registry, resolve the data source with the request's tenant ID (or create a new one if needed), and use that.
I know this may sound complicated, but it happens to be exactly how things work under the hood when you're not using a data source; when you open a connection with a connection string (the old, pre-data source way), Npgsql internally looks up a data source based on that connection string; it effectively has your tenant data source registry inside, as a global/static thing. To do things in the better, new way with data source, you have to take over managing that registry in DI. npgsql/npgsql#4523 is about possibly providing some helpful infrastructure to make this easier, but there's nothing you can't already do today.