Skip to main content

Multi-Tenancy Configuration

Configure PearDrop multi-tenancy behavior through appsettings.json and service registration.

Complete Configuration Reference

{
"ConnectionStrings": {
"PearDrop-Multitenancy": "Server=localhost,1433;Database=MyApp;User Id=sa;Password=...;TrustServerCertificate=True;"
},
"PearDrop": {
"modules": {
"multitenancy": {
"defaultTenantId": "00000000-0000-0000-0000-000000000000",
"defaultIdentifier": "dbo",
"defaultName": "Default Tenant",
"ownerHost": "admin.myapp.com",
"tenantSiteUrl": "https://{tenant}.myapp.com",
"noTenantRedirectPath": "/no-tenant",
"doNotUseStore": false
}
}
}
}

Configuration Options

defaultTenantId

Type: Guid
Default: 00000000-0000-0000-0000-000000000000

The default tenant ID used when no tenant context is available.

"defaultTenantId": "550e8400-e29b-41d4-a716-446655440000"

When used:

  • System admin operations
  • Background jobs without tenant context
  • Migration scripts
  • Health check endpoints

defaultIdentifier

Type: string
Default: "dbo"

URL-friendly identifier for the default tenant.

"defaultIdentifier": "system"

Use cases:

  • Subdomain: system.myapp.com
  • Route: /system/dashboard
  • Fallback when no tenant found

defaultName

Type: string
Default: "Default Tenant"

Display name for the default tenant.

"defaultName": "System Administrator"

Shown in:

  • Admin UI
  • Reports
  • Audit logs

ownerHost

Type: string
Default: ""

Host/domain reserved for system administrators (no tenant context).

"ownerHost": "admin.myapp.com"

Behavior:

  • Requests to admin.myapp.com → No tenant resolution
  • User sees all tenants (if authorized)
  • Cross-tenant operations allowed

Examples:

  • admin.myapp.com - Admin portal
  • localhost - Development admin access
  • system.internal.com - Internal admin

tenantSiteUrl

Type: string
Default: ""

URL pattern for tenant sites. Use {tenant} placeholder for identifier.

"tenantSiteUrl": "https://{tenant}.myapp.com"

Used for:

  • Generating tenant-specific links
  • Email templates with tenant URLs
  • API responses with tenant endpoints

Examples:

  • https://{tenant}.myapp.com - Subdomain pattern
  • https://myapp.com/{tenant} - Path pattern
  • https://{tenant}.example.com/app - Subdomain + path

noTenantRedirectPath

Type: string
Default: "/redirect-test"

Redirect path when no tenant context is found and request requires one.

"noTenantRedirectPath": "/select-tenant"

Scenario:

  1. User visits myapp.com (no tenant identifier)
  2. App cannot resolve tenant
  3. User redirected to /select-tenant
  4. User selects/enters tenant
  5. User redirected to tenant1.myapp.com

doNotUseStore

Type: boolean
Default: false

Disable database tenant store (use in-memory or custom store instead).

"doNotUseStore": true

When to use:

  • Testing with fixed tenants
  • Custom tenant store implementation
  • Static tenant configuration

With in-memory store:

builder.Services.AddMultiTenant<PearDropTenantInfo>()
.WithInMemoryStore(options =>
{
options.Tenants.Add(new PearDropTenantInfo
{
Id = "tenant1-id",
Identifier = "tenant1",
Name = "Tenant One"
});
options.Tenants.Add(new PearDropTenantInfo
{
Id = "tenant2-id",
Identifier = "tenant2",
Name = "Tenant Two"
});
});

Service Registration

Basic Setup

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

// Register PearDrop multitenancy
builder.Services.AddPearDropMultitenancy(builder.Configuration);

// Configure Finbuckle with resolution strategy
builder.Services.AddMultiTenant<PearDropTenantInfo>()
.WithHostStrategy(); // Use host/subdomain resolution

var app = builder.Build();

// MUST be early in middleware pipeline
app.UseMultiTenant();

// Rest of middleware
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();
app.AddMultitenancyApi();

app.Run();

Host Strategy (Subdomain)

builder.Services.AddMultiTenant<PearDropTenantInfo>()
.WithHostStrategy();

Resolves:

  • tenant1.myapp.comtenant1
  • acmecorp.myapp.comacmecorp
  • admin.myapp.com → No tenant (if matches ownerHost)

Route Strategy (URL Path)

builder.Services.AddMultiTenant<PearDropTenantInfo>()
.WithRouteStrategy("tenant"); // Route parameter name

Requires route configuration:

app.MapControllerRoute(
name: "tenant_route",
pattern: "{tenant}/{controller}/{action}/{id?}");

Resolves:

  • /tenant1/dashboardtenant1
  • /acmecorp/ordersacmecorp

Authentication Strategy (Claims)

builder.Services.AddMultiTenant<PearDropTenantInfo>()
.WithClaimStrategy("TenantId"); // Claim type

Requires claim in authentication:

var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, userId.ToString()),
new Claim("TenantId", tenantId.ToString())
};

var identity = new ClaimsIdentity(claims, "peardrop");
var principal = new ClaimsPrincipal(identity);

Combined Strategies

Chain multiple strategies with fallback:

builder.Services.AddMultiTenant<PearDropTenantInfo>()
.WithHostStrategy() // Try subdomain first
.WithRouteStrategy("tenant") // Fallback to route
.WithClaimStrategy("TenantId"); // Final fallback to claims

Resolution order:

  1. Check host/subdomain
  2. If not found, check route parameter
  3. If not found, check authentication claims
  4. If not found, use default tenant or redirect

Data Isolation Configuration

Shared Database (Default)

All tenants in one database with automatic tenant filtering:

// No additional configuration needed
builder.Services.AddPearDropMultitenancy(builder.Configuration);

Database:

-- Single database with TenantId column
CREATE TABLE Orders (
Id uniqueidentifier PRIMARY KEY,
TenantId uniqueidentifier NOT NULL, -- Automatic filter
OrderNumber nvarchar(50),
...
);

Schema-Per-Tenant

Configure tenant with custom schema:

var command = new CreateTenantCommand(
identifier: "tenant1",
reference: "CUST-001",
name: "Tenant One");

var result = await commandRunner.ExecuteAsync(command);

// Set schema for tenant
var tenant = await repository.FindOne(
new ByIdSpecification<Tenant>(result.TenantId));

tenant.Value.SetIsolationLevel(
TenantIsolationLevel.Schema,
schemaName: "tenant1schema");

await repository.UnitOfWork.SaveEntitiesAsync();

Database:

-- Separate schema per tenant
CREATE SCHEMA tenant1;
CREATE TABLE tenant1.Orders (...);

CREATE SCHEMA tenant2;
CREATE TABLE tenant2.Orders (...);

Database-Per-Tenant

Configure tenant with dedicated database:

var tenant = await repository.FindOne(
new ByIdSpecification<Tenant>(tenantId));

tenant.Value.SetIsolationLevel(
TenantIsolationLevel.Database,
connectionString: "Server=...;Database=Tenant1DB;...");

await repository.UnitOfWork.SaveEntitiesAsync();

Runtime:

// PearDrop automatically uses tenant-specific connection
var orders = await dbContext.Orders.ToListAsync();
// Connects to Tenant1DB automatically

Environment-Specific Configuration

Development

{
"PearDrop": {
"modules": {
"multitenancy": {
"defaultIdentifier": "dev",
"ownerHost": "localhost",
"tenantSiteUrl": "https://{tenant}.localhost:5000",
"noTenantRedirectPath": "/dev/select-tenant"
}
}
}
}

Staging

{
"PearDrop": {
"modules": {
"multitenancy": {
"defaultIdentifier": "staging",
"ownerHost": "admin.staging.myapp.com",
"tenantSiteUrl": "https://{tenant}.staging.myapp.com",
"noTenantRedirectPath": "/select-tenant"
}
}
}
}

Production

{
"PearDrop": {
"modules": {
"multitenancy": {
"defaultIdentifier": "system",
"ownerHost": "admin.myapp.com",
"tenantSiteUrl": "https://{tenant}.myapp.com",
"noTenantRedirectPath": "/portal",
"doNotUseStore": false
}
}
}
}

Middleware Order

CRITICAL: Multi-tenancy middleware must run early:

var app = builder.Build();

// 1. Exception handling (optional, can be first)
app.UseExceptionHandler("/error");

// 2. HTTPS redirection
app.UseHttpsRedirection();

// 3. Static files (before multitenancy if tenant-agnostic)
app.UseStaticFiles();

// 4. Routing (MUST be before multitenancy)
app.UseRouting();

// 5. Multi-tenancy (EARLY!)
app.UseMultiTenant();

// 6. Authentication (after multitenancy, may use tenant context)
app.UseAuthentication();

// 7. Authorization
app.UseAuthorization();

// 8. Endpoints
app.MapControllers();
app.AddMultitenancyApi();

app.Run();

Custom Tenant Store

Implement custom tenant store:

public class CustomTenantStore : IMultiTenantStore<PearDropTenantInfo>
{
private readonly IConfiguration config;

public async Task<PearDropTenantInfo?> TryGetAsync(string identifier)
{
// Custom logic to fetch tenant
// Could be: Redis cache, external API, configuration, etc.
var tenantData = await FetchTenantFromExternalSourceAsync(identifier);

if (tenantData == null)
return null;

return new PearDropTenantInfo
{
Id = tenantData.Id.ToString(),
Identifier = tenantData.Slug,
Name = tenantData.DisplayName,
ConnectionString = tenantData.DbConnectionString
};
}

public async Task<PearDropTenantInfo?> TryGetByIdAsync(string id)
{
// Fetch by tenant ID
// ...
}

public Task<IEnumerable<PearDropTenantInfo>> GetAllAsync()
{
// Return all tenants
// ...
}

public Task<bool> TryAddAsync(PearDropTenantInfo tenantInfo)
{
// Add new tenant to your custom store
// ...
}

public Task<bool> TryUpdateAsync(PearDropTenantInfo tenantInfo)
{
// Update tenant
// ...
}

public Task<bool> TryRemoveAsync(string identifier)
{
// Remove tenant
// ...
}
}

// Register
builder.Services.AddMultiTenant<PearDropTenantInfo>()
.WithStore<CustomTenantStore>(ServiceLifetime.Scoped);

Tenant Caching

Reduce database lookups with caching:

// Enable distributed cache
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
});

// Finbuckle uses cache automatically if available
builder.Services.AddMultiTenant<PearDropTenantInfo>()
.WithHostStrategy()
.WithStore<CustomMultiTenantStore>()
.WithPerTenantOptions<MyOptions>((options, tenantInfo) =>
{
// Cached per tenant
options.SomeSetting = tenantInfo.Name;
});

Troubleshooting

Tenant Not Resolved

Symptom: Requests return 404 or redirect to noTenantRedirectPath

Check:

  1. Middleware order - app.UseMultiTenant() before controllers
  2. Strategy configuration matches URL pattern
  3. Tenant exists in database
  4. ownerHost not interfering with resolution

Seeing Wrong Tenant's Data

Symptom: User sees another tenant's data

Check:

  1. Tenant filter applied to DbContext queries
  2. Entity has TenantId property
  3. IEntityFilterProvider registered
  4. Not using [AllowWithoutTenant] inappropriately

Performance Issues

Symptom: Slow tenant resolution

Solutions:

  1. Enable distributed cache
  2. Use in-memory store for read-heavy scenarios
  3. Index Identifier column in tenants table
  4. Reduce tenant info queries

Next Steps