Skip to content

Latest commit

 

History

History
483 lines (390 loc) · 12.7 KB

File metadata and controls

483 lines (390 loc) · 12.7 KB

Multi-Tenancy

Sharp-ABP provides advanced multi-tenancy features including map-based tenancy isolation and tenant grouping.

MapTenancy

Map-based tenancy isolation that allows domain-to-tenant binding for seamless tenant resolution.

Installation

# Core MapTenancy
dotnet add package SharpAbp.Abp.MapTenancy

# AspNetCore integration
dotnet add package SharpAbp.Abp.AspNetCore.MapTenancy

Configuration

Configure 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();
    }
}

Usage Example

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();
        }
    }
}

Dynamic Tenant Mapping Management

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();
    }
}

Tenant Code Provider

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.


TenancyGrouping

Tenant grouping isolation allows you to organize tenants into groups and apply group-level configurations and isolation.

Installation

# Core TenancyGrouping
dotnet add package SharpAbp.Abp.TenancyGrouping

# AspNetCore integration
dotnet add package SharpAbp.Abp.AspNetCore.TenancyGrouping

Configuration

Add 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";
        });
    }
}

Usage Example

Define Tenant Groups

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; }
}

Tenant Group Management

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);
    }
}

Group-Level Data Isolation

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
    }
}

Best Practices

1. Tenant Isolation

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;
    }
}

2. Host vs Tenant Operations

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);
    }
}

3. Cross-Tenant Operations

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;
    }
}

4. Tenant Configuration

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);
    }
}