417.1 Multi‐Tenancy of ABP framework applications - chempkovsky/CS82ANGULAR GitHub Wiki
- 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
andVolo.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).
- 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
Click to show the picture
- 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.
- 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 hasDirectory.Build.props
-file too. So to switch betweenmodeling
andbuild
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
- Modify
Tenant.cs
-file of theVolo.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.
- Repeat the steps of the 403 Modify DBContext for the first entity (PhoneType).
OnModelCreating
must be as follows:
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
}
- 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 generatesTenantDto
-name. But the calss with the same name is declared in theVolo.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 changeTenantDto
toTenantExDto
. - In the
rupbes.firstapp.Application.Contracts.csproj
-rpoject we create theTm
-folder and define Dto as follows: - The second page of the wizard will be as follows:
Click to show the picture
- The fourth page of the wizard will be as follows:
Click to show the picture
- 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
-
Repeat the steps of the 408 Typescript Classes for the First View of ABP framework.
-
Repeat the steps of the 409 Navigation aware typescript components for the First View of ABP framework.
-
We do not generate classes for Add, Update and Delete forms
-
UI List properties is as follows:
Click to show the picture
- UI Form properties is as follows:
Click to show the picture
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: