417.1 Multi‐Tenancy of ABP framework applications - chempkovsky/CS82ANGULAR GitHub Wiki

Note
  • Abp developers say in the paragraph A note about separate database per tenant approach in open source version:
    • "While ABP fully supports this option, managing connection strings of tenants from the UI is not available in open source version. You need to have Saas module (PRO). Alternatively you can implement this feature yourself by customizing the tenant management module and tenant application service to create and migrate the database on the fly."
  • The most important for us is a following phrase: While ABP fully supports this option.
  • "customizing the tenant management module and tenant application service" is a hand coding.
    • There are some "How-to" blogs concerning "migration the database on the fly". So we are not going to discuss this aspect.
  • What we are going to talk about is of how to generate UI for Volo.Abp.TenantManagement.Tenant and Volo.Abp.TenantManagement.TenantConnectionString.
    • After that we will add new Tenant using default APB UI, we will add default connection-string for the newly created Tenant using the generated UI and run rupbes.firstapp.DbMigrator.csproj console app to see what happenes.
    • For those how do not want to belong to a group of "normal heroes who always take the detour" we recommend to create new tenant right now using default APB UI, directly insert the row into AbpTenantConnectionStrings-table in the database and run DbMigrator console app (sorry, about being captain-obvious).
Click to show the picture

project structure

  • The second important note about IMultiTenant is the following fact: "ABP automatically filters entities for the current tenant when you query from database". Telling the truth, it is not a fact, it's a real headache. Every table with a TenantId column should have a short list of rows. A full table scan containing tens of millions of records is unacceptable fun.
TenantManagement module source
  • To get the source of the TenantManagement module we use abp cli command.
  • Run cmd.exe-command line terminal first
  • In command line terminal change the current directory
d:
cd D:\Development\rupbes.firstapp
  • In command line terminal change the current directory run abp cli command
abp add-source-code Volo.Abp.TenantManagement --add-to-solution-file
  • It is important to note that the generated TenantManagement-module has Directory.Build.props-file too. So to switch between modeling and build modes we have to change both files:
D:\Development\rupbes.firstapp\Directory.Build.props
  • and
D:\Development\rupbes.firstapp\modules\Volo.Abp.TenantManagement\Directory.Build.props
Entity
  • Modify Tenant.cs-file of the Volo.Abp.TenantManagement.Domain.csproj-rpject as follows:
Click to show the code
using JetBrains.Annotations;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Volo.Abp.Auditing;
using Volo.Abp.Domain.Entities.Auditing;

namespace Volo.Abp.TenantManagement;

public class Tenant : FullAuditedAggregateRoot<Guid>, IHasEntityVersion
{
#if (!NOTMODELING)
    [Display(Description = "Row id", Name = "Id of the Tenant", Prompt = "Enter Tenant Id", ShortName = "Tenant Id")]
    public override Guid Id { get; protected set; }
#endif

#if (!NOTMODELING)
    [Display(Description = "Tenant Name", Name = "Name of the Tenant", Prompt = "Enter Tenant Name", ShortName = "Tenant Name")]
    [StringLength(64, MinimumLength = 3, ErrorMessage = "Invalid")]
#endif
    public virtual string Name { get; protected set; }

#if (!NOTMODELING)
    [Display(Description = "Tenant Normalized Name", Name = "Normalized Name of the Tenant", Prompt = "Enter Normalized Tenant Name", ShortName = "Normalized Tenant Name")]
    [StringLength(64, MinimumLength = 3, ErrorMessage = "Invalid")]
#endif
    public virtual string NormalizedName { get; protected set; }

#if (!NOTMODELING)
    [Display(Description = "Tenant Entity Version", Name = "Entity Version of the Tenant", Prompt = "Enter Entity Version", ShortName = "Tenant Entity Version")]
#endif
    public virtual int EntityVersion { get; protected set; }

    public virtual List<TenantConnectionString> ConnectionStrings { get; protected set; }

    protected Tenant()
    {

    }

    protected internal Tenant(Guid id, [NotNull] string name, [CanBeNull] string normalizedName)
        : base(id)
    {
        SetName(name);
        SetNormalizedName(normalizedName);

        ConnectionStrings = new List<TenantConnectionString>();
    }

    [CanBeNull]
    public virtual string FindDefaultConnectionString()
    {
        return FindConnectionString(Data.ConnectionStrings.DefaultConnectionStringName);
    }

    [CanBeNull]
    public virtual string FindConnectionString(string name)
    {
        return ConnectionStrings.FirstOrDefault(c => c.Name == name)?.Value;
    }

    public virtual void SetDefaultConnectionString(string connectionString)
    {
        SetConnectionString(Data.ConnectionStrings.DefaultConnectionStringName, connectionString);
    }

    public virtual void SetConnectionString(string name, string connectionString)
    {
        var tenantConnectionString = ConnectionStrings.FirstOrDefault(x => x.Name == name);

        if (tenantConnectionString != null)
        {
            tenantConnectionString.SetValue(connectionString);
        }
        else
        {
            ConnectionStrings.Add(new TenantConnectionString(Id, name, connectionString));
        }
    }

    public virtual void RemoveDefaultConnectionString()
    {
        RemoveConnectionString(Data.ConnectionStrings.DefaultConnectionStringName);
    }

    public virtual void RemoveConnectionString(string name)
    {
        var tenantConnectionString = ConnectionStrings.FirstOrDefault(x => x.Name == name);

        if (tenantConnectionString != null)
        {
            ConnectionStrings.Remove(tenantConnectionString);
        }
    }

    protected internal virtual void SetName([NotNull] string name)
    {
        Name = Check.NotNullOrWhiteSpace(name, nameof(name), TenantConsts.MaxNameLength);
    }

    protected internal virtual void SetNormalizedName([CanBeNull] string normalizedName)
    {
        NormalizedName = normalizedName;
    }
}

  • It is important to note, that at build time (i.e. <DefineConstants>NOTMODELING</DefineConstants>) Tenant-class stays unchanged.
Modify DBContext
Click to show the code
    protected override void OnModelCreating(ModelBuilder builder)
    {
#if (!NOTMODELING)
        builder.Entity<Tenant>().HasKey(p => p.Id);
        builder.Entity<PhbkFile>().HasKey(p => new { p.FileId, p.FlNm });
        builder.Entity<PhbkPhone>().HasKey(p => p.Id);
        builder.Entity<PhbkEmployee>().HasKey(p => p.Id);
        builder.Entity<PhbkDivision>().HasKey(p => p.Id);
        builder.Entity<PhbkPhoneType>().HasKey(p => p.Id);
        builder.Entity<PhbkEnterprise>().HasKey(p => p.Id);
        builder.Entity<PhbkPhoneMtm>().HasKey(p => p.Id);
        builder.Entity<PhbkEmpToPhn>().HasKey(p => new { p.EmpIdRef, p.PhnIdRef });
#endif
        base.OnModelCreating(builder);
        /* Include modules to your migration db context */
        builder.ConfigurePermissionManagement();
        builder.ConfigureSettingManagement();
        builder.ConfigureBackgroundJobs();
        builder.ConfigureAuditLogging();
        builder.ConfigureFeatureManagement();
        builder.ConfigureIdentity();
        builder.ConfigureOpenIddict();
        builder.ConfigureTenantManagement();
        builder.ConfigureBlobStoring();
        /* Configure your own tables/entities inside here */
        //builder.Entity<YourEntity>(b =>
        //{
        //    b.ToTable(firstappConsts.DbTablePrefix + "YourEntities", firstappConsts.DbSchema);
        //    b.ConfigureByConvention(); //auto configure for the base class props
        //    //...
        //});
        builder.ConfigurefirstappDbContext();
#if (!NOTMODELING)
        builder.Entity<PhbkDivision>().HasOne(d => d.Enterprise)
                .WithMany(m => m.Divisions)
                .HasForeignKey(d => d.EntrprsIdRef)
                .HasPrincipalKey(p => p.Id)
                .IsRequired(true)
                .OnDelete(DeleteBehavior.NoAction);
        builder.Entity<PhbkEmployee>().HasOne(d => d.Division)
                .WithMany(m => m.Employees)
                .HasForeignKey(d => d.DivisionIdRef)
                .HasPrincipalKey(p => p.Id)
                .IsRequired(true)
                .OnDelete(DeleteBehavior.NoAction);
        builder.Entity<PhbkPhone>().HasOne(d => d.PhoneType)
                .WithMany(m => m.Phones)
                .HasForeignKey(d => d.PhoneTypeIdRef)
                .HasPrincipalKey(p => p.Id)
                .IsRequired(true)
                .OnDelete(DeleteBehavior.NoAction);
        builder.Entity<PhbkPhone>().HasOne(d => d.Employee)
                .WithMany(m => m.Phones)
                .HasForeignKey(d => d.EmployeeIdRef)
                .HasPrincipalKey(p => p.Id)
                .IsRequired(true)
                .OnDelete(DeleteBehavior.NoAction);
        builder.Entity<PhbkPhoneMtm>().HasOne(d => d.PhoneType)
                .WithMany(m => m.PhoneMtms)
                .HasForeignKey(d => d.PhoneTypeIdRef)
                .HasPrincipalKey(p => p.Id)
                .IsRequired(true)
                .OnDelete(DeleteBehavior.NoAction);
        builder.Entity<PhbkEmpToPhn>().HasOne(d => d.PhoneMtm)
                .WithMany(m => m.EmpToPhns)
                .HasForeignKey(d => d.PhnIdRef)
                .HasPrincipalKey(p => p.Id)
                .IsRequired(true)
                .OnDelete(DeleteBehavior.NoAction);
        builder.Entity<PhbkEmpToPhn>().HasOne(d => d.Employee)
                .WithMany(m => m.EmpToPhns)
                .HasForeignKey(d => d.EmpIdRef)
                .HasPrincipalKey(p => p.Id)
                .IsRequired(true)
                .OnDelete(DeleteBehavior.NoAction);
#endif

    }
Dto
  • Repeat the steps of the 404 First View (PhoneType). Wizard repository
  • It is important to note that using default DTO-naming is not the case for Tenant-entity. The wizard generates TenantDto-name. But the calss with the same name is declared in the Volo.Abp.TenantManagement.Application.Contracts.csproj-prject. Even though the class created by our wizard has a different namespace, it is much better if different Dto classes have different names. We change TenantDto to TenantExDto.
  • In the rupbes.firstapp.Application.Contracts.csproj-rpoject we create the Tm-folder and define Dto as follows:
  • The second page of the wizard will be as follows:
Click to show the picture

project structure

  • The fourth page of the wizard will be as follows:
Click to show the picture

project structure

Web Api
  • Repeat the steps of the 405 First Web Api Service (PhoneType)
  • Please note that
    • Use only root props for select method-checkbox is ON.
    • we check only select methods
    • Even though the TenantManagement module has a web service for Tenant, we generate our own copy of the classes and interfaces: [ITenantExDtoRepo.cs, TenantExDtoRepo.cs, ITenantExDtoService, TenantExDtoService.cs, TenantExDtoWebApiController.cs]
Click to show the picture

project structure

Typescript Classes
Click to show the picture

project structure

  • UI Form properties is as follows:
Click to show the picture

project structure

Modify route.provider.ts-file as follows:

Click to show the code
      {
        name: 'firstapp::Psn:TenantExDto',
        group: 'firstapp::Psn:TenantExDto',
        order: 18,
        iconClass: 'fas fa-users',
      },      
      {
        path: '/RDLTenantExDto',
        name: 'Tenants Dlg',
        requiredPolicy: 'firstapp.TenantExDto',
        parentName: 'firstapp::Psn:TenantExDto',
        iconClass: 'fas fa-users',
        order: 160,
        layout: eLayoutType.application,
      },
      {
        path: '/TenantExDto',
        name: 'Tenants',
        requiredPolicy: 'firstapp.TenantExDto',
        parentName: 'firstapp::Psn:TenantExDto',
        iconClass: 'fas fa-users',
        order: 170,
        layout: eLayoutType.application,
      }
  • Here is a result:
Click to show the picture

project structure

⚠️ **GitHub.com Fallback** ⚠️