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

Note
  • Please read the article first
  • We generated UI for Volo.Abp.TenantManagement.Tenant-entity
  • Now we are going to generate code for Volo.Abp.TenantManagement.TenantConnectionString-entity
Entity
  • Modify TenantConnectionString.cs-file of the Volo.Abp.TenantManagement.Domain.csproj-project as follows:
Click to show the code
using JetBrains.Annotations;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Volo.Abp.Domain.Entities;

namespace Volo.Abp.TenantManagement;

public class TenantConnectionString : Entity
{
#if (!NOTMODELING)
    [Description("IsNot_TenantId")]
    [Display(Description = "Tenant id", Name = "Id of the Tenant", Prompt = "Enter Tenant Id", ShortName = "Tenant Id")]
#endif
    public virtual Guid TenantId { get; protected set; }

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

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

#if (!NOTMODELING)
    public Tenant Tnnt { get; set; } = null!;
#endif


    protected TenantConnectionString()
    {

    }

    public TenantConnectionString(Guid tenantId, [NotNull] string name, [NotNull] string value)
    {
        TenantId = tenantId;
        Name = Check.NotNullOrWhiteSpace(name, nameof(name), TenantConnectionStringConsts.MaxNameLength);
        SetValue(value);
    }

    public virtual void SetValue([NotNull] string value)
    {
        Value = Check.NotNullOrWhiteSpace(value, nameof(value), TenantConnectionStringConsts.MaxValueLength);
    }

    public override object[] GetKeys()
    {
        return new object[] { TenantId, Name };
    }
}

  • It is important to note, that at build time (i.e. <DefineConstants>NOTMODELING</DefineConstants>) TenantConnectionString-class stays unchanged.
  • It is important to note, that public Tenant Tnnt { get; set; } = null!; property was added to the class. We make a hint for CS82ANGULAR about one-to-many relation.
  • It is important to note, that [Description("IsNot_TenantId")] attribute was added to the TenantId-property of the class
    • CS82ANGULAR does not analyze the class inheritance, instead it checks the properties with a predefined name like:
      • TenantId, ConcurrencyStamp and so on
    • On the one hand, TenantConnectionString-class does not implement IMultiTenant-interface, on the other it has TenantId-property.
      • using attributes like [Description("IsNot_TenantId")] or [Description("IsNot_ConcurrencyStamp")] (and so on), we make a hint for CS82ANGULAR that TenantId is not for implementing IMultiTenant-interface
Modify DBContext
Click to show the picture

project structure

  • With DbContext wizard we define the foreigth key
Click to show the picture

project structure

  • OnModelCreating must be as follows:
Click to show the code
    protected override void OnModelCreating(ModelBuilder builder)
    {
#if (!NOTMODELING)
        builder.Entity<TenantConnectionString>().HasKey(p => new { p.TenantId, p.Name });
        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);
        builder.Entity<TenantConnectionString>().HasOne(d => d.Tnnt)
                .WithMany(m => m.ConnectionStrings)
                .HasForeignKey(d => d.TenantId)
                .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 TenantConnectionString-entity. The wizard generates TenantConnectionStringDto-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 TenantConnectionStringDto to TenantConnectionStringExDto.
  • 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

  • We define the foreign key:
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.
    • Even though the TenantManagement module may have a web service for TenantConnectionString, we generate our own copy of the classes and interfaces: [ITenantConnectionStringExDtoRepo.cs, TenantConnectionStringExDtoRepo.cs, ITenantConnectionStringExDtoService, TenantConnectionStringExDtoService.cs, TenantConnectionStringExDtoWebApiController.cs]
Click to show the picture

project structure

  • It is important to note that in the updateone-method of the TenantConnectionStringExDtoService-class we had to replace one line of code:
    • we commented the code resultEntity.Value = viewToUpdate.Value;
    • we added resultEntity.SetValue(viewToUpdate.Value);
      • in the article we introduce ChangeVals-method which used to change properties.
      • Abp developers use SetXXX()-methods to modify properties
      • as we mentioned above, we leave Abp-classes to be unchanged at build time
    • you do not need to remember of which code you must change. Visual Studio shows it.
Click to show the code
        [Authorize(firstappPermissions.TenantConnectionStringExDto.Edit)]
        public async Task<TenantConnectionStringExDto?> updateone(TenantConnectionStringExDto viewToUpdate)
        {
            // using(repo.EnableTracking()) 
            {
                var appQr = (await repo.GetQueryableAsync()) // .TenantConnectionStrings
                    .Where(p => p.TenantId == viewToUpdate.TenantId)
                    .Where(p => p.Name == viewToUpdate.Name)
                    ;
                TenantConnectionString resultEntity = await AsyncExecuter.FirstOrDefaultAsync(appQr);
                if(resultEntity == null) {
                    return null;
                }

                //resultEntity.Value =  viewToUpdate.Value;
                resultEntity.SetValue(viewToUpdate.Value);
                await repo.UpdateAsync(resultEntity);
                await UnitOfWorkManager.Current.SaveChangesAsync();
                var appQrEx =   (await repo.GetQueryableAsync()) // .TenantConnectionStrings
                    .Where(p => p.TenantId == viewToUpdate.TenantId)
                    .Where(p => p.Name == viewToUpdate.Name)
                    .Select(itm => new TenantConnectionStringExDto() {
                             TenantId = itm.TenantId
                            , Name = itm.Name
                            , Value = itm.Value
                    });
                return await AsyncExecuter.FirstOrDefaultAsync(appQrEx);
            } // using(repo.EnableTracking()) { ... }
        }
Typescript Classes
Click to show the picture

project structure

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

project structure

  • After generating typescript classes for TenantConnectionString we must to regenerate the following classes for Tenant:
    • services (014400-AbpWebApiService.json-batch)
    • rv-form
    • rlist-form
    • rdlist-form
    • this is a scipt (not a batch) 01980-R-lazy.routes.ts/abp.r-lazy.routes.ts.t4
    • this is a scipt (not a batch) 02060-Rdl-lazy.routes.ts/abp.rdl-lazy.routes.ts.t4

Here is a result:

Click to show the picture

project structure

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