Open Closed

Tenant has many subtenats (Tenants as Host) #5514


User avatar
0
n.uerkmez created
  • ABP Framework version: v7.3.0
  • UI Type: Angular
  • Database System: EF Core (SQL Server, Oracle, MySQL, PostgreSQL, etc..)
  • Tiered (for MVC) or Auth Server Separated (for Angular): yes
    • Exception message and full stack trace:
  • Steps to reproduce the issue:

ABP multitenancy;

  1. Can a tenant have multiple tenants in ABP Commercial? If not, what solution is the best practice for this use case? For example, Tenant-1 has 2 sub tenants. -Host -- Tenant-1 -------- Sub Tenant-01 --------Sub Tenant-02

  2. How can I override the routing path for Auto API Controllers? I mean that all API endpoint in the swagger must be generated with tenant-id by default as follows;

GetAsync(Guid id) GET /api/app/{tenant-id}/book/{id} GetListAsync() GET /api/app/{tenant-id}/book CreateAsync(CreateBookDto input) POST /api/app/{tenant-id}/book UpdateAsync(Guid id, UpdateBookDto input) PUT /api/app/{tenant-id}/book/{id} DeleteAsync(Guid id) DELETE /api/app/{tenant-id}/book/{id} GetEditorsAsync(Guid id) GET /api/app/{tenant-id}/book/{id}/editors CreateEditorAsync(Guid id, BookEditorCreateDto input) POST /api/app/{tenant-id}/book/{id}/editor


7 Answer(s)
  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Can a tenant have multiple tenants in ABP Commercial?

    No, ABP does not provide such an infrastructure, you need to implement it yourself.

    If not, what solution is the best practice for this use case?

    This is a big topic. I can only give you some simple ideas:

    Use the extended entity system to add ParentTenantId property to Tenant entity: https://docs.abp.io/en/abp/latest/Customizing-Application-Modules-Extending-Entities

    Data isolation between tenants, you can manually close the data filter: https://docs.abp.io/en/abp/latest/Data-Filtering#imultitenant

    How can I override the routing path for Auto API Controllers? I mean that all API endpoint in the swagger must be generated with tenant-id by default as follows;

    You can try:

    [ExposeServices(typeof(IConventionalRouteBuilder))]
    public class MyConventionalRouteBuilder : ConventionalRouteBuilder
    {
        public MyConventionalRouteBuilder(IOptions<AbpConventionalControllerOptions> options) : base(options)
        {
        }
    
        protected override string NormalizeControllerNameCase(string controllerName, ConventionalControllerSetting? configuration)
        {
            var name = base.NormalizeControllerNameCase(controllerName, configuration);
    
            return name += "/{tenant-id}";
        }
    }
    
    [ExposeServices(typeof(IAbpServiceConvention))]
    public class MyAppServiceConvention : AbpServiceConvention
    {
        public MyAppServiceConvention(IOptions<AbpAspNetCoreMvcOptions> options, IConventionalRouteBuilder conventionalRouteBuilder) : base(options, conventionalRouteBuilder)
        {
        }
    
        protected override void ConfigureSelector(ControllerModel controller, ConventionalControllerSetting? configuration)
        {
            RemoveEmptySelectors(controller.Selectors);
    
            var controllerType = controller.ControllerType.AsType();
            var remoteServiceAtt = ReflectionHelper.GetSingleAttributeOrDefault<RemoteServiceAttribute>(controllerType.GetTypeInfo());
            if (remoteServiceAtt != null && !remoteServiceAtt.IsEnabledFor(controllerType))
            {
                return;
            }
    
            if (controller.Selectors.Any(selector => selector.AttributeRouteModel != null))
            {
                foreach (var selector in controller.Selectors)
                {
                    if (selector.AttributeRouteModel != null)
                    {
                        selector.AttributeRouteModel.Template += "/{tenant-id}";
                    }
                }
                return;
            }
    
            var rootPath = GetRootPathOrDefault(controller.ControllerType.AsType());
    
            foreach (var action in controller.Actions)
            {
                ConfigureSelector(rootPath, controller.ControllerName, action, configuration);
            }
        }
    }
    

    You need also to create a custom tenant resolver to get the resolve from route:

    https://docs.abp.io/en/abp/latest/Multi-Tenancy#custom-tenant-resolvers

  • User Avatar
    0
    n.uerkmez created

    Thanks. That is good insight

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    ok

  • User Avatar
    0
    n.uerkmez created

    I have checked this documentation. However, I need help to implement the following use case. https://docs.abp.io/en/abp/latest/Data-Filtering?&_ga=2.210300374.2127151687.1690988301-1686284556.1681286165#entityframework-core

    Could you provide a sample on how to implement complex use-case (not only filter with a boolean value)?

    In my use case, I need to find all sub Tenants by ParentTenantId and then filter the data accordingly.

    List<Guid> subTenants = new List<Guid>(){ SubTenantsGuids};

    I need to use create an expression like tenantId=> subTenants.contains(tenantId)

    protected override Expression<Func<TEntity, bool>> CreateFilterExpression<TEntity>()
    {
        var expression = base.CreateFilterExpression<TEntity>();
    
        if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity)))
        {
            Expression<Func<TEntity, bool>> isTenantFilter =
              e => !IsTenantFilterEnabled || EF.Property<Guid>(e, "TenantId");
            expression = expression == null 
                ? isTenantFilter 
                : CombineExpressions(expression, isTenantFilter);
        }
    
        return expression;
    }
    

    How I can filter by a list of Guid in the section of Expression<Func<TEntity, bool>> isTenantFilter = e => !IsTenantFilterEnabled || EF.Property<Guid>(e, "TenantId"); expression = expression == null ? isTenantFilter : CombineExpressions(expression, isTenantFilter);**

    Thanks in Advance

  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Hi,

    It's easy to do.

    For example:

    var parentTenantId = currentTenant.Id.ToString();
    
    using(currentTenant.Change(null))
    {
        var tenants = await tenantRepository.GetListAsync();
        var subTenants = tenants.where(x => x.GetProperty<string>("ParentTenantId") == parentTenantId).ToList();
    }
    
  • User Avatar
    0
    n.uerkmez created

    Thanks for the prompt answer.

    I would like to create a global filter instead of aligning each Domain Service class like as following.

    namespace MultiTenancyDemo.Products
    {
        public class ProductManager : DomainService
        {
            private readonly IRepository<Product, Guid> _productRepository;
    
            public ProductManager(IRepository<Product, Guid> productRepository)
            {
                _productRepository = productRepository;
            }
    
            public async Task<long> GetProductCountAsync(Guid? tenantId)
            {
                using (CurrentTenant.Change(tenantId))
                {
                    return await _productRepository.GetCountAsync();
                }
            }
        }
    }
    
    

    My question is about how to implement globally

    Do you think the following code may work?

    protected override Expression> CreateFilterExpression()
    {
    var parentTenantId = currentTenant.Id.ToString();
    Guid[] tenantIdsToFilter;
    using(currentTenant.Change(null))
    {
        var tenants = await tenantRepository.GetListAsync();
        var subTenants = tenants.where(x => x.GetProperty<string>("ParentTenantId") == parentTenantId).ToList();
        tenantIdsToFilter = subTenants.Select(t=>t.Id).ToArray(); 
    }
    
    Expression<Func<TEntity, bool>> tenantIdFilterExpression =
        e => !IsActiveFilterEnabled || tenantIdsToFilter.Contains(EF.Property<Guid>(e, "TenantId"));
    .......
    }
    
  • User Avatar
    0
    liangshiwei created
    Support Team Fullstack Developer

    Do you think the following code may work?

    I think it's no problem, you can give it a try.

Made with ❤️ on ABP v8.2.0-preview Updated on March 25, 2024, 15:11