id: multi-tenancy
Multi-Tenancy in SophiChain
ABP Framework Multi-Tenancy Features
id: multi-tenancy
๐ Overviewโ
SophiChain uses ABP Framework's built-in multi-tenancy system to support multiple tenants (organizations) in a single application instance. Data isolation is automatic and transparent.
id: multi-tenancy
๐๏ธ How It Worksโ
IMultiTenant Interfaceโ
ABP provides automatic data filtering for entities that implement IMultiTenant:
public interface IMultiTenant
{
Guid? TenantId { get; }
}
Key Points:
TenantId == nullโ Host data (shared across all tenants)TenantId == {specific-guid}โ Tenant-specific data
id: multi-tenancy
๐ฏ Implementationโ
1. Make Entity Multi-Tenantโ
// Domain/Entities/Wallet.cs
public class Wallet : FullAuditedAggregateRoot<Guid>, IMultiTenant
{
public Guid UserId { get; private set; }
public string Name { get; private set; }
public decimal Balance { get; private set; }
// Required by IMultiTenant
public Guid? TenantId { get; private set; }
protected Wallet()
{
// Required by ABP Framework
}
public Wallet(
Guid id,
Guid userId,
string name,
Guid currencyId,
Guid? tenantId = null) : base(id)
{
UserId = userId;
Name = Check.NotNullOrWhiteSpace(name, nameof(name));
CurrencyId = currencyId;
TenantId = tenantId; // Set from current tenant context
}
}
Rules:
- โ
Add
IMultiTenantinterface - โ
Add
Guid? TenantId { get; private set; } - โ
Pass
tenantIdin constructor (optional) - โ Don't set TenantId manually (ABP handles it)
id: multi-tenancy
2. Automatic Data Filteringโ
ABP automatically filters queries by current tenant:
// Application service
public class AdminWalletAppService : FinanceHubAppService
{
private readonly IWalletRepository _walletRepository;
public virtual async Task<List<WalletDto>> GetAllAsync()
{
// ABP automatically adds: WHERE TenantId = CurrentTenantId
var wallets = await _walletRepository.GetListAsync();
return ObjectMapper.Map<List<Wallet>, List<WalletDto>>(wallets);
}
}
ABP Handles:
- โ
Automatically filters
WHERE TenantId = CurrentTenant.Id - โ Works with all repository methods
- โ Works with EF Core and MongoDB
- โ
Uses
IDataFilterinternally (can be disabled if needed)
Note: Repositories must be injected - they are NOT inherited from base classes.
id: multi-tenancy
3. Accessing Other Tenant's Dataโ
Sometimes you need to access data from different tenants (admin scenarios):
public class AdminWalletAppService : FinanceHubAppService // Inherits CurrentTenant
{
private readonly IWalletRepository _walletRepository;
public virtual async Task<List<WalletDto>> GetAllTenantsWalletsAsync(Guid? targetTenantId)
{
// CurrentTenant available from base ApplicationService
using (CurrentTenant.Change(targetTenantId))
{
var wallets = await _walletRepository.GetListAsync();
return ObjectMapper.Map<List<Wallet>, List<WalletDto>>(wallets);
}
}
public virtual async Task<List<WalletDto>> GetAllWalletsAcrossAllTenantsAsync()
{
// Query all tenants (use with caution!)
using (CurrentTenant.Change(null))
{
var wallets = await _walletRepository.GetListAsync();
return ObjectMapper.Map<List<Wallet>, List<WalletDto>>(wallets);
}
}
}
Note: CurrentTenant is available from ABP base classes (ApplicationService, DomainService, AbpComponentBase)
4. Disabling Tenant Filter (IDataFilter)โ
Use IDataFilter to temporarily disable automatic filtering:
public class AdminWalletAppService : FinanceHubAppService
{
private readonly IWalletRepository _walletRepository;
public virtual async Task<List<WalletDto>> GetAllWalletsAsync()
{
// Disable multi-tenant filter temporarily
using (DataFilter.Disable<IMultiTenant>())
{
var wallets = await _walletRepository.GetListAsync();
return ObjectMapper.Map<List<Wallet>, List<WalletDto>>(wallets);
}
}
}
Note: DataFilter is also available from ABP base classes. See ABP Data Filtering for more details.
Comparison:
CurrentTenant.Change(tenantId)โ Switch to specific tenantCurrentTenant.Change(null)โ Switch to host (null tenant)DataFilter.Disable<IMultiTenant>()โ Disable filtering completely
id: multi-tenancy
๐ Tenant Resolutionโ
ABP resolves current tenant from multiple sources (in order):
- Query String:
?__tenant={tenantId} - HTTP Header:
__tenant: {tenantId} - Cookie:
__tenant={tenantId} - Subdomain:
{tenant}.yourdomain.com - Custom resolver: Implement
ITenantResolveContributor
id: multi-tenancy
๐ฏ Host vs Tenant Dataโ
Host Data (Shared)โ
Some entities should be shared across all tenants:
// This entity is NOT multi-tenant
public class Currency : FullAuditedAggregateRoot<Guid>
{
public string Code { get; private set; } // USD, EUR, BTC
public string Name { get; private set; }
// NO TenantId property
// Available to all tenants
}
Use Cases:
- System settings
- Predefined currencies
- Lookup tables
- Global configuration
Tenant-Specific Dataโ
Data that belongs to a specific tenant:
// This entity IS multi-tenant
public class Wallet : FullAuditedAggregateRoot<Guid>, IMultiTenant
{
public Guid UserId { get; private set; }
public decimal Balance { get; private set; }
public Guid? TenantId { get; private set; } // โ Tenant isolation
}
Use Cases:
- User data
- Transactions
- Wallets
- Invoices
id: multi-tenancy
๐๏ธ Database Strategiesโ
ABP supports multiple multi-tenancy database strategies:
1. Single Database (Default)โ
All tenants share one database with automatic filtering:
Database: SophiChain
โโโ Wallets (TenantId column)
โโโ Transactions (TenantId column)
โโโ Currencies (No TenantId - shared)
Pros: Simple, efficient Cons: All data in one database
2. Database Per Tenantโ
Each tenant has its own database:
Database: SophiChain_Tenant1
โโโ Wallets
โโโ Transactions
Database: SophiChain_Tenant2
โโโ Wallets
โโโ Transactions
Pros: Better isolation, scalability Cons: More complex management
Configuration:
{
"ConnectionStrings": {
"Default": "Server=...;Database=SophiChain;",
"Tenant1": "Server=...;Database=SophiChain_Tenant1;",
"Tenant2": "Server=...;Database=SophiChain_Tenant2;"
}
}
id: multi-tenancy
โ Best Practicesโ
Entity Designโ
- โ
Add
IMultiTenantto tenant-specific entities - โ
Keep host entities without
IMultiTenant(shared data) - โ
Let ABP set
TenantIdautomatically - โ Don't manually filter by
TenantIdin queries - โ Don't set
TenantIdmanually after creation
Repository Usageโ
- โ Always inject repositories (not inherited from base classes)
- โ Use standard repository methods (filtering is automatic)
- โ
Use
CurrentTenant.Change()to access other tenant data - โ
Use
DataFilter.Disable<IMultiTenant>()when needed - โ
Always restore context with
usingstatement - โ Don't bypass tenant filtering unless necessary
Securityโ
- โ Always check permissions before changing tenant context
- โ Log tenant context changes
- โ
Use
ICurrentTenant.Change(null)only in admin scenarios - โ Never expose tenant IDs to unauthorized users
id: multi-tenancy
๐งช Testingโ
public class Wallet_Tests : FinanceHubDomainTestBase // Inherits CurrentTenant
{
[Fact]
public async Task Should_Filter_By_Tenant()
{
var tenant1Id = Guid.NewGuid();
using (CurrentTenant.Change(tenant1Id))
{
var wallet = new Wallet(Guid.NewGuid(), Guid.NewGuid(), "Wallet", Guid.NewGuid());
await _walletRepository.InsertAsync(wallet);
var wallets = await _walletRepository.GetListAsync();
wallets.Count.ShouldBe(1);
}
}
}
id: multi-tenancy
๐ Referencesโ
id: multi-tenancy