Skip to content

Multi-Tenancy

Art Admin implements row-level multi-tenancy with automatic query filtering using EF Core global query filters.

Architecture

Request → Token Middleware → Extract TenantId → RequestContext.TenantId

                                          EF Core Global Query Filter
                                          WHERE tenant_id = 'xxx'

Entity Interface

Entities requiring tenant isolation implement ITenantEntity:

csharp
public interface ITenantEntity
{
    string? TenantId { get; set; }
}

[Table("biz_order")]
public class BizOrder : EntityBase, ITenantEntity
{
    public string? TenantId { get; set; }
    public string OrderNo { get; set; } = string.Empty;
    // ...
}

Automatic Filtering

In ArtDbContext, global query filters are applied to all ITenantEntity entities:

csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    foreach (var entityType in modelBuilder.Model.GetEntityTypes())
    {
        if (typeof(ITenantEntity).IsAssignableFrom(entityType.ClrType))
        {
            // Add WHERE tenant_id = @currentTenantId
            modelBuilder.Entity(entityType.ClrType)
                .HasQueryFilter(/* tenant filter expression */);
        }
    }
}

Effect

csharp
// What you write
var orders = await _db.BizOrders.ToListAsync();

// What EF Core actually executes
SELECT * FROM biz_order WHERE tenant_id = 'tenant_001';

Every query is automatically filtered — developers don't need to add WHERE tenant_id = ... manually.

Auto-Fill on Save

When creating new entities, TenantId is automatically filled from RequestContext:

csharp
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
    foreach (var entry in ChangeTracker.Entries<ITenantEntity>())
    {
        if (entry.State == EntityState.Added)
            entry.Entity.TenantId = _currentTenantId;
    }
    return await base.SaveChangesAsync(cancellationToken);
}

Bypass Filtering

For cross-tenant operations (e.g., platform admin):

csharp
// Ignore global filters
var allOrders = await _db.BizOrders
    .IgnoreQueryFilters()
    .ToListAsync();

Tenant Identification

Tenants are identified through a field in the user Token, stored in RequestContext.TenantId. Each request is guaranteed to only access its own tenant data.