Sharp-ABP provides advanced multi-tenancy features including map-based tenancy isolation and tenant grouping.
Map-based tenancy isolation that allows domain-to-tenant binding for seamless tenant resolution.
# Core MapTenancy
dotnet add package SharpAbp.Abp.MapTenancy
# AspNetCore integration
dotnet add package SharpAbp.Abp.AspNetCore.MapTenancyConfigure in appsettings.json:
{
"MapTenancy": {
"TenantMappings": [
{
"Domain": "tenant1.myapp.com",
"TenantId": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
},
{
"Domain": "tenant2.myapp.com",
"TenantId": "8b3c9d45-6e2a-4f8b-9c5d-1a2b3c4d5e6f"
},
{
"Domain": "app.customdomain.com",
"TenantId": "7a4e8f23-9b1c-4d5e-a6f7-3b2c1d0e9f8a"
}
]
}
}Add the module dependency:
[DependsOn(
typeof(AbpMapTenancyModule),
typeof(AbpAspNetCoreMapTenancyModule)
)]
public class YourModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
Configure<AbpMapTenancyOptions>(options =>
{
options.TenantMappings.Add(new TenantMapping
{
Domain = "tenant1.myapp.com",
TenantId = Guid.Parse("3fa85f64-5717-4562-b3fc-2c963f66afa6")
});
options.TenantMappings.Add(new TenantMapping
{
Domain = "tenant2.myapp.com",
TenantId = Guid.Parse("8b3c9d45-6e2a-4f8b-9c5d-1a2b3c4d5e6f")
});
});
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
// Add MapTenancy middleware (before MVC)
app.UseMapTenancy();
app.UseRouting();
app.UseConfiguredEndpoints();
}
}public class TenantAwareService : ITransientDependency
{
private readonly ICurrentTenant _currentTenant;
private readonly ITenantStore _tenantStore;
public TenantAwareService(
ICurrentTenant _currentTenant,
ITenantStore tenantStore)
{
_currentTenant = currentTenant;
_tenantStore = tenantStore;
}
public async Task<string> GetCurrentTenantNameAsync()
{
if (_currentTenant.Id == null)
{
return "Host";
}
var tenant = await _tenantStore.FindAsync(_currentTenant.Id.Value);
return tenant?.Name ?? "Unknown";
}
public Guid? GetCurrentTenantId()
{
return _currentTenant.Id;
}
// Switch tenant context
public async Task<List<Product>> GetTenantProductsAsync(Guid tenantId)
{
using (_currentTenant.Change(tenantId))
{
// Operations here will be in the context of the specified tenant
return await _productRepository.GetListAsync();
}
}
}public class TenantMappingService : ApplicationService
{
private readonly ITenantMappingStore _mappingStore;
public TenantMappingService(ITenantMappingStore mappingStore)
{
_mappingStore = mappingStore;
}
// Add new domain mapping
public async Task AddDomainMappingAsync(string domain, Guid tenantId)
{
await _mappingStore.AddOrUpdateAsync(new TenantMapping
{
Domain = domain,
TenantId = tenantId
});
}
// Remove domain mapping
public async Task RemoveDomainMappingAsync(string domain)
{
await _mappingStore.RemoveAsync(domain);
}
// Get tenant by domain
public async Task<Guid?> GetTenantIdByDomainAsync(string domain)
{
var mapping = await _mappingStore.FindByDomainAsync(domain);
return mapping?.TenantId;
}
// List all mappings for a tenant
public async Task<List<string>> GetTenantDomainsAsync(Guid tenantId)
{
var mappings = await _mappingStore.GetListByTenantIdAsync(tenantId);
return mappings.Select(m => m.Domain).ToList();
}
}IMapTenantCodeProvider resolves a tenant id to the mapping code used by consumers that need a stable tenant code instead of a tenant GUID or tenant name.
For example, SharpAbp.Abp.FileStoring.MapTenancy uses this provider to fill FilePathContext.TenantCode.
public class StorageTenantCodeService : ITransientDependency
{
private readonly IMapTenantCodeProvider _mapTenantCodeProvider;
public StorageTenantCodeService(IMapTenantCodeProvider mapTenantCodeProvider)
{
_mapTenantCodeProvider = mapTenantCodeProvider;
}
public async Task<string?> FindCodeAsync(Guid tenantId)
{
var codeInfo = await _mapTenantCodeProvider.FindByTenantIdAsync(tenantId);
return codeInfo?.Code;
}
}MapTenantCodeInfo contains:
| Property | Description |
|---|---|
TenantId |
Tenant id from the mapping. |
TenantName |
Optional tenant display/name value from the mapping. |
Code |
Tenant mapping code. This is the default tenant path code for FileStoring integration. |
MapCode |
Optional alternate mapping code. FileStoring can opt into this with FilePathTenantCodeSource.MapCode. |
The default provider reads from AbpMapTenancyOptions.
When SharpAbp.Abp.MapTenancyManagement.Domain is installed, DatabaseMapTenantCodeProvider replaces it and reads tenant code information from IMapTenantStore.
Tenant grouping isolation allows you to organize tenants into groups and apply group-level configurations and isolation.
# Core TenancyGrouping
dotnet add package SharpAbp.Abp.TenancyGrouping
# AspNetCore integration
dotnet add package SharpAbp.Abp.AspNetCore.TenancyGroupingAdd the module dependency:
[DependsOn(
typeof(AbpTenancyGroupingModule),
typeof(AbpAspNetCoreTenancyGroupingModule)
)]
public class YourModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpTenancyGroupingOptions>(options =>
{
// Configure grouping options
options.EnableGroupIsolation = true;
options.DefaultGroupName = "Default";
});
}
}public class TenantGroup : AggregateRoot<Guid>
{
public string Name { get; set; }
public string DisplayName { get; set; }
public string Description { get; set; }
public Dictionary<string, object> Properties { get; set; }
public bool IsActive { get; set; }
}public class TenantGroupAppService : ApplicationService
{
private readonly IRepository<TenantGroup, Guid> _groupRepository;
private readonly ITenantGroupManager _tenantGroupManager;
public TenantGroupAppService(
IRepository<TenantGroup, Guid> groupRepository,
ITenantGroupManager tenantGroupManager)
{
_groupRepository = groupRepository;
_tenantGroupManager = tenantGroupManager;
}
// Create tenant group
public async Task<TenantGroupDto> CreateAsync(CreateTenantGroupDto input)
{
var group = new TenantGroup
{
Name = input.Name,
DisplayName = input.DisplayName,
Description = input.Description,
IsActive = true
};
await _groupRepository.InsertAsync(group);
return ObjectMapper.Map<TenantGroup, TenantGroupDto>(group);
}
// Assign tenant to group
public async Task AssignTenantToGroupAsync(Guid tenantId, Guid groupId)
{
await _tenantGroupManager.AssignTenantToGroupAsync(tenantId, groupId);
}
// Get tenants in group
public async Task<List<TenantDto>> GetTenantsInGroupAsync(Guid groupId)
{
var tenants = await _tenantGroupManager.GetTenantsInGroupAsync(groupId);
return ObjectMapper.Map<List<Tenant>, List<TenantDto>>(tenants);
}
// Get tenant's group
public async Task<TenantGroupDto> GetTenantGroupAsync(Guid tenantId)
{
var group = await _tenantGroupManager.GetTenantGroupAsync(tenantId);
return ObjectMapper.Map<TenantGroup, TenantGroupDto>(group);
}
}public class GroupAwareProductService : ApplicationService
{
private readonly IRepository<Product, Guid> _productRepository;
private readonly ICurrentTenantGroup _currentTenantGroup;
public GroupAwareProductService(
IRepository<Product, Guid> productRepository,
ICurrentTenantGroup currentTenantGroup)
{
_productRepository = productRepository;
_currentTenantGroup = currentTenantGroup;
}
// Get products visible to current tenant's group
public async Task<List<ProductDto>> GetGroupProductsAsync()
{
var groupId = _currentTenantGroup.Id;
var queryable = await _productRepository.GetQueryableAsync();
var products = await AsyncExecuter.ToListAsync(
queryable.Where(p => p.TenantGroupId == groupId)
);
return ObjectMapper.Map<List<Product>, List<ProductDto>>(products);
}
// Share product across group
public async Task ShareProductWithGroupAsync(Guid productId, Guid targetGroupId)
{
var product = await _productRepository.GetAsync(productId);
// Create shared product reference
var sharedProduct = new SharedProduct
{
OriginalProductId = productId,
TargetGroupId = targetGroupId
};
// Save shared reference
}
}Always use ABP's tenant resolution mechanisms:
public class TenantAwareRepository<TEntity> : ITransientDependency
where TEntity : class, IEntity, IMultiTenant
{
private readonly IRepository<TEntity> _repository;
private readonly ICurrentTenant _currentTenant;
public async Task<List<TEntity>> GetListAsync()
{
// Automatically filtered by current tenant
return await _repository.GetListAsync();
}
public async Task<TEntity> GetAsync(Guid id)
{
var entity = await _repository.GetAsync(id);
// Additional tenant check
if (entity.TenantId != _currentTenant.Id)
{
throw new UnauthorizedAccessException("Access denied");
}
return entity;
}
}Distinguish between host and tenant operations:
public class AdminService : ApplicationService
{
private readonly ICurrentTenant _currentTenant;
[Authorize(Permissions.SystemAdmin)]
public async Task<List<TenantDto>> GetAllTenantsAsync()
{
// This should only be accessible to host
if (_currentTenant.Id != null)
{
throw new BusinessException("This operation is only available for host");
}
var tenants = await _tenantRepository.GetListAsync();
return ObjectMapper.Map<List<Tenant>, List<TenantDto>>(tenants);
}
public async Task<TenantDto> GetCurrentTenantAsync()
{
if (_currentTenant.Id == null)
{
throw new BusinessException("No tenant context");
}
var tenant = await _tenantRepository.GetAsync(_currentTenant.Id.Value);
return ObjectMapper.Map<Tenant, TenantDto>(tenant);
}
}Use tenant switching carefully:
public class CrossTenantReportService : ApplicationService
{
private readonly ICurrentTenant _currentTenant;
private readonly IRepository<Order> _orderRepository;
[Authorize(Permissions.CrossTenantReports)]
public async Task<ConsolidatedReport> GenerateConsolidatedReportAsync(
List<Guid> tenantIds)
{
var report = new ConsolidatedReport();
foreach (var tenantId in tenantIds)
{
using (_currentTenant.Change(tenantId))
{
var orders = await _orderRepository.GetListAsync();
report.AddTenantData(tenantId, orders);
}
}
return report;
}
}Store tenant-specific configuration:
public class TenantConfigurationService : ApplicationService
{
private readonly ITenantConfigurationProvider _configProvider;
public async Task<T> GetTenantConfigAsync<T>(string key)
{
var value = await _configProvider.GetAsync(key);
return JsonSerializer.Deserialize<T>(value);
}
public async Task SetTenantConfigAsync<T>(string key, T value)
{
var json = JsonSerializer.Serialize(value);
await _configProvider.SetAsync(key, json);
}
}