TenantCore.EntityFramework.PostgreSql 2.2.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package TenantCore.EntityFramework.PostgreSql --version 2.2.0
                    
NuGet\Install-Package TenantCore.EntityFramework.PostgreSql -Version 2.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="TenantCore.EntityFramework.PostgreSql" Version="2.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TenantCore.EntityFramework.PostgreSql" Version="2.2.0" />
                    
Directory.Packages.props
<PackageReference Include="TenantCore.EntityFramework.PostgreSql" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add TenantCore.EntityFramework.PostgreSql --version 2.2.0
                    
#r "nuget: TenantCore.EntityFramework.PostgreSql, 2.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package TenantCore.EntityFramework.PostgreSql@2.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=TenantCore.EntityFramework.PostgreSql&version=2.2.0
                    
Install as a Cake Addin
#tool nuget:?package=TenantCore.EntityFramework.PostgreSql&version=2.2.0
                    
Install as a Cake Tool

TenantCore.EntityFramework

A robust, extensible multi-tenancy solution for Entity Framework Core with schema-per-tenant isolation on PostgreSQL.

License: MIT NuGet .NET

Features

  • Schema-Per-Tenant Isolation: Complete data separation at the database level
  • Pluggable Tenant Resolution: Multiple built-in resolvers (header, claims, subdomain, path, route, query string, API key)
  • Control Database: Optional centralized tenant metadata storage with status tracking, encrypted credentials, and API key authentication
  • Automatic Migration Management: Apply migrations across all tenant schemas
  • Full Tenant Lifecycle: Provision, archive, restore, and delete tenants
  • Event System: Subscribe to tenant lifecycle events
  • Health Checks: Monitor tenant database health
  • Extensible Architecture: Add custom strategies, resolvers, and seeders

Quick Start

Installation

dotnet add package TenantCore.EntityFramework
dotnet add package TenantCore.EntityFramework.PostgreSql

Basic Setup

// Program.cs
var builder = WebApplication.CreateBuilder(args);

// Configure TenantCore
builder.Services.AddTenantCore<string>(options =>
{
    options.UsePostgreSql(connectionString);
    options.UseSchemaPerTenant(schema =>
    {
        schema.SchemaPrefix = "tenant_";
    });
});

builder.Services.AddTenantCorePostgreSql();
builder.Services.AddHeaderTenantResolver<string>();
builder.Services.AddTenantDbContextPostgreSql<AppDbContext, string>(connectionString);

var app = builder.Build();

// Add tenant resolution middleware
app.UseTenantResolution<string>();

app.Run();

Create a Tenant-Aware DbContext

public class AppDbContext : TenantDbContext<string>
{
    public DbSet<Product> Products => Set<Product>();

    public AppDbContext(
        DbContextOptions<AppDbContext> options,
        ITenantContextAccessor<string> tenantContextAccessor,
        TenantCoreOptions tenantOptions)
        : base(options, tenantContextAccessor, tenantOptions)
    {
    }
}

Provision a New Tenant

app.MapPost("/api/tenants/{tenantId}", async (
    string tenantId,
    ITenantManager<string> tenantManager) =>
{
    await tenantManager.ProvisionTenantAsync(tenantId);
    return Results.Created($"/api/tenants/{tenantId}", new { tenantId });
});

Tenant Resolution

TenantCore includes several built-in tenant resolvers. Multiple resolvers can be registered and will be evaluated in priority order (higher priority values run first).

Resolver Default Priority Registration Helper
Claims 200 AddClaimsTenantResolver<TKey>()
API Key 175 AddApiKeyTenantResolver<TKey>()
Route Value 150 Manual AddScoped
Path 125 AddPathTenantResolver<TKey>()
Header 100 AddHeaderTenantResolver<TKey>()
Subdomain 50 AddSubdomainTenantResolver<TKey>()
Query String 25 Manual AddScoped
builder.Services.AddHeaderTenantResolver<string>("X-Tenant-Id");

Claims-Based (JWT/Authentication)

builder.Services.AddClaimsTenantResolver<string>("tenant_id");

Subdomain-Based

builder.Services.AddSubdomainTenantResolver<string>("example.com");
// tenant1.example.com -> tenant1

Query String-Based

builder.Services.AddScoped<ITenantResolver<string>>(sp =>
    new QueryStringTenantResolver<string>(
        sp.GetRequiredService<IHttpContextAccessor>(),
        "tenant"));
// /api/products?tenant=tenant1 -> tenant1

Route Value-Based

builder.Services.AddScoped<ITenantResolver<string>>(sp =>
    new RouteValueTenantResolver<string>(
        sp.GetRequiredService<IHttpContextAccessor>(),
        "tenantId"));
// /api/{tenantId}/products -> extracts from route

Path-Based

// By segment index (0-based)
builder.Services.AddPathTenantResolver<string>(segmentIndex: 0);
// /{tenant}/api/products -> tenant

// By path prefix
builder.Services.AddPathTenantResolverWithPrefix<string>("/api");
// /api/{tenant}/products -> tenant

API Key-Based (Requires Control Database)

builder.Services.AddApiKeyTenantResolver<Guid>("X-Api-Key");
// Verifies tenant API key using salted PBKDF2-SHA256 hashing in the control database
// Only returns Active tenants

Custom Resolver

public class MyCustomResolver : ITenantResolver<string>
{
    public int Priority => 100;

    public Task<string?> ResolveTenantAsync(CancellationToken ct = default)
    {
        // Your custom logic here
        return Task.FromResult<string?>("tenant1");
    }
}

// Register
builder.Services.AddScoped<ITenantResolver<string>, MyCustomResolver>();

Migration Management

Automatic Migrations on Startup

options.ConfigureMigrations(migrations =>
{
    migrations.ApplyOnStartup = true;
    migrations.ParallelMigrations = 4;
    migrations.Timeout = TimeSpan.FromMinutes(5);
});

Manual Migration

var tenantManager = serviceProvider.GetRequiredService<ITenantManager<string>>();

// Migrate specific tenant
await tenantManager.MigrateTenantAsync("tenant1");

// Migrate all tenants
await tenantManager.MigrateAllTenantsAsync();

Tenant Lifecycle

Provisioning

await tenantManager.ProvisionTenantAsync("new-tenant");

Archiving

await tenantManager.ArchiveTenantAsync("tenant-to-archive");

Restoring

await tenantManager.RestoreTenantAsync("archived-tenant");

Deletion

// Soft delete (renames schema)
await tenantManager.DeleteTenantAsync("tenant-id", hardDelete: false);

// Hard delete (drops schema)
await tenantManager.DeleteTenantAsync("tenant-id", hardDelete: true);

Tenant Scoping

Use ITenantScopeFactory to temporarily switch tenant context for background jobs or cross-tenant operations:

public class CrossTenantService
{
    private readonly ITenantScopeFactory<string> _scopeFactory;
    private readonly IDbContextFactory<AppDbContext> _dbFactory;

    public CrossTenantService(
        ITenantScopeFactory<string> scopeFactory,
        IDbContextFactory<AppDbContext> dbFactory)
    {
        _scopeFactory = scopeFactory;
        _dbFactory = dbFactory;
    }

    public async Task ProcessAllTenantsAsync(IEnumerable<string> tenantIds)
    {
        foreach (var tenantId in tenantIds)
        {
            // Create a scope for the target tenant
            using var scope = _scopeFactory.CreateScope(tenantId);
            await using var db = await _dbFactory.CreateDbContextAsync();

            // All operations here use the scoped tenant's schema
            var products = await db.Products.ToListAsync();
            // ... process products
        }
    }
}

Data Seeding

Seed initial data when provisioning new tenants:

public class TenantDataSeeder : ITenantSeeder<string>
{
    public int Order => 0; // Lower values run first

    public async Task SeedAsync(
        DbContext context,
        string tenantId,
        CancellationToken cancellationToken = default)
    {
        context.Set<Product>().Add(new Product
        {
            Name = "Welcome Product",
            Description = "Initial product for new tenants"
        });
        await context.SaveChangesAsync(cancellationToken);
    }
}

// Register in DI
builder.Services.AddScoped<ITenantSeeder<string>, TenantDataSeeder>();

Events

Subscribe to tenant lifecycle events:

public class TenantEventHandler : ITenantEventSubscriber<string>
{
    public Task OnTenantCreatedAsync(TenantCreatedEvent<string> @event, CancellationToken ct)
    {
        Console.WriteLine($"Tenant {@event.TenantId} was created");
        return Task.CompletedTask;
    }

    // ... other event handlers
}

builder.Services.AddTenantEventSubscriber<string, TenantEventHandler>();

Health Checks

builder.Services.AddTenantHealthChecks<AppDbContext, string>("tenants");

Control Database (Optional)

The Control Database feature provides centralized tenant metadata storage with support for:

  • Tenant status tracking (Pending, Active, Suspended, Disabled, FlaggedForDelete)
  • Encrypted database credentials
  • API key authentication (salted PBKDF2-SHA256 hashed)
  • Caching for improved performance

Setup

// Add control database with PostgreSQL
builder.Services.AddTenantControlDatabase(
    dbOptions => dbOptions.UseNpgsql(controlDbConnectionString),
    options =>
    {
        options.Schema = "tenant_control";
        options.EnableCaching = true;
        options.CacheDuration = TimeSpan.FromMinutes(5);
        options.ApplyMigrationsOnStartup = true;
        options.MigratableStatuses = [TenantStatus.Pending, TenantStatus.Active];
    });

Provisioning with Control Database

When the control database is configured, use the extended provisioning method:

var tenantManager = app.Services.GetRequiredService<TenantManager<AppDbContext, Guid>>();

var request = new CreateTenantRequest(
    TenantSlug: "acme-corp",
    TenantSchema: "tenant_acme",
    TenantApiKey: "sk_live_abc123..."  // Will be hashed with salted PBKDF2-SHA256
);

var tenant = await tenantManager.ProvisionTenantAsync(Guid.NewGuid(), request);
// Creates control DB record (Pending) -> provisions schema -> sets status to Active

Custom Tenant Store (BYO)

Implement your own tenant storage by implementing ITenantStore:

public class MyTenantStore : ITenantStore
{
    // Implement all ITenantStore methods
}

// Register
builder.Services.AddTenantStore<MyTenantStore>(options =>
{
    options.EnableCaching = true;
});

Tenant Record Fields

Field Description
TenantId Unique identifier (Guid)
TenantSlug URL-friendly identifier
Status Tenant status enum
TenantSchema Database schema name
TenantDatabase Optional separate database
TenantDbServer Optional separate server
TenantDbUser Optional database user
TenantDbPasswordEncrypted Encrypted password (Data Protection API)
TenantApiKeyHash Salted PBKDF2-SHA256 hash of API key
CreatedAt / UpdatedAt Timestamps

Configuration Options

builder.Services.AddTenantCore<string>(options =>
{
    // Connection
    options.UsePostgreSql(connectionString);

    // Schema isolation
    options.UseSchemaPerTenant(schema =>
    {
        schema.SchemaPrefix = "tenant_";
        schema.SharedSchema = "public";
        schema.ArchivedSchemaPrefix = "archived_";
    });

    // Migrations
    options.ConfigureMigrations(migrations =>
    {
        migrations.ApplyOnStartup = true;
        migrations.ParallelMigrations = 4;
        migrations.FailureBehavior = MigrationFailureBehavior.ContinueOthers;
        migrations.Timeout = TimeSpan.FromMinutes(5);
    });

    // Behavior
    options.OnTenantNotFound(TenantNotFoundBehavior.Throw);
    options.EnableCaching(TimeSpan.FromMinutes(5));
    options.DisableTenantValidation();
});

Separate Migrations Assembly

When your migrations are in a separate assembly (common in clean architecture):

builder.Services.AddTenantDbContextPostgreSql<AppDbContext, string>(
    connectionString,
    migrationsAssembly: "MyApp.Infrastructure");

Shared Entities

Mark entities that should not be tenant-isolated:

[SharedEntity]
public class GlobalConfiguration
{
    public int Id { get; set; }
    public string Key { get; set; }
    public string Value { get; set; }
}

Or use fluent configuration:

protected override void ConfigureSharedEntities(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<GlobalConfiguration>()
        .ToTable("GlobalConfiguration", "public");
}

Supported Databases

Database Package Status
PostgreSQL TenantCore.EntityFramework.PostgreSql ✅ Supported
SQL Server TenantCore.EntityFramework.SqlServer 🔜 Planned
MySQL TenantCore.EntityFramework.MySql 🔜 Planned

Sample Project

A complete sample Web API is included in the samples/TenantCore.Sample.WebApi directory, demonstrating:

  • Tenant provisioning and management endpoints
  • Tenant-scoped CRUD operations
  • Health check configuration
  • Swagger/OpenAPI integration
  • Optional Control Database integration

Run the sample:

cd samples/TenantCore.Sample.WebApi
dotnet run

To enable the Control Database feature in the sample:

dotnet run -- --TenantCore:UseControlDatabase=true

Requirements

  • .NET 8.0 or .NET 10.0
  • Entity Framework Core 8.x or 10.x
  • PostgreSQL 12+ (for PostgreSQL provider)

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! Please open an issue or submit a pull request on GitHub.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.4.0 0 2/12/2026
2.3.0 23 2/11/2026
2.2.0 73 2/6/2026
2.0.0 89 2/5/2026
1.1.1 72 2/5/2026
1.1.0 66 2/5/2026
1.0.0 69 2/5/2026

v2.2.0: Documentation updates and accuracy improvements