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 portallocalhost- Development admin accesssystem.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 patternhttps://myapp.com/{tenant}- Path patternhttps://{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:
- User visits
myapp.com(no tenant identifier) - App cannot resolve tenant
- User redirected to
/select-tenant - User selects/enters tenant
- 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.com→tenant1acmecorp.myapp.com→acmecorpadmin.myapp.com→ No tenant (if matchesownerHost)
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/dashboard→tenant1/acmecorp/orders→acmecorp
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:
- Check host/subdomain
- If not found, check route parameter
- If not found, check authentication claims
- 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:
- Middleware order -
app.UseMultiTenant()before controllers - Strategy configuration matches URL pattern
- Tenant exists in database
ownerHostnot interfering with resolution
Seeing Wrong Tenant's Data
Symptom: User sees another tenant's data
Check:
- Tenant filter applied to DbContext queries
- Entity has
TenantIdproperty IEntityFilterProviderregistered- Not using
[AllowWithoutTenant]inappropriately
Performance Issues
Symptom: Slow tenant resolution
Solutions:
- Enable distributed cache
- Use in-memory store for read-heavy scenarios
- Index
Identifiercolumn in tenants table - Reduce tenant info queries
Next Steps
- Tenant Resolution - Deep dive into resolution strategies
- Tenant Management - CRUD operations and lifecycle
- Data Isolation - Schema and database-per-tenant patterns