Skip to main content

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 IMultiTenant interface
  • โœ… Add Guid? TenantId { get; private set; }
  • โœ… Pass tenantId in 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 IDataFilter internally (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 tenant
  • CurrentTenant.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):

  1. Query String: ?__tenant={tenantId}
  2. HTTP Header: __tenant: {tenantId}
  3. Cookie: __tenant={tenantId}
  4. Subdomain: {tenant}.yourdomain.com
  5. 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 IMultiTenant to tenant-specific entities
  • โœ… Keep host entities without IMultiTenant (shared data)
  • โœ… Let ABP set TenantId automatically
  • โŒ Don't manually filter by TenantId in queries
  • โŒ Don't set TenantId manually 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 using statement
  • โŒ 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