Activities of "alexander.nikonov"

  • ABP Framework version: v2.9
  • UI type: Angular
  • Tiered (MVC) or Identity Server Separated (Angular): Identity Server Separated

Hi, our business model requires one user to belong to several tenants. So we have created accounts for the same user, where each account has the same user name, but different tenant id.

  1. we need user to login once and next time his tenant changed, the user does not need to re-enter his credentials again - he re-logins automatically (SSO), for instance, after selecting the tenant from dropdown list in UI; Could you please suggest the solution?
  2. is there a way to get the list of all tenants of all user accounts using ABP framework?
  3. how to get the list of tenants where user name is the same?
  • ABP Framework version: v2.9
  • UI type: Angular
  • Tiered (MVC) or Identity Server Seperated (Angular): Identity Server Seperated

Hi. To extend the existing ABP tenants / users functionality in the system, we had to create corresponding tables / entities which relate 1:1 to ABP one:

 //Our Tenant class
 using AbpTenant = Volo.Saas.Tenant;

 namespace XXX.Tenants {
      public class Tenant : LogEntity {
      ...
      public Guid AbpId { get; set; }
      public AbpTenant AbpTenant { get; set; }`
      ...
      
 //OnModelCreating   
        ...
        builder.Entity<Tenant>()
            .HasOne(x => x.AbpTenant)
            .WithOne()
            .HasPrincipalKey<Volo.Saas.Tenant>(x => x.Id)
            .HasForeignKey<Tenant>(x => x.AbpId);
        ...
        builder.Entity<Tenant>(b => {
            b.ToTable("OUR_TENANT");
            b.ConfigureByConvention();
            b.HasKey(x => x.Id);
            b.Property(x => x.Id).HasColumnName("C_TENANT").IsRequired().ValueGeneratedNever();
            ...
            b.Property(x => x.AbpId).HasColumnName("C_ABP_TENANT").IsRequired();
            ...

We had to create own client-side infrastructure in Angular app as well, to process these composite entities:

//Angular Tenants model

export interface State {
    tenants: Response;
    tenantsLookup: Common.LookupResponse<number>;
}

export interface Response {
    items: Tenants.TenantWithNavigationProperties[];
    totalCount: number;
}

export interface TenantsQueryParams extends ABP.PageQueryParams {
    filterText?: string;
    idMin?: number;
    idMax?: number;
    shortName?: string;
    fullName?: string;
    companyId?: number;
    masterId?: number;
    abpId?: string;
    isMaster?: boolean;
}

export interface AbpTenant {
    id: string;
    name: string;
    editionId: string;
}

export interface AbpTenantCreateDto {
    name: string;
    editionId: string;
    adminEmailAddress: string;
    adminPassword: string;
}

export interface AbpTenantUpdateDto {
    name: string;
    editionId: string;
}

export interface TenantWithNavigationProperties {
    id: number;
    shortName: string;
    fullName: string;
    comment: string;
    companyId: number;
    masterId: number;
    abpId: string;
    isMaster: boolean;
    abpTenant: AbpTenant;
}

export interface TenantCreateDto {
    id: number;
    shortName: string;
    fullName: string;
    comment: string;
    companyId: number;
    masterId?: number;
    abpId?: string;
    isMaster: boolean;
    abpTenant: AbpTenantCreateDto;
}

export interface TenantUpdateDto {
    id: number;
    shortName: string;
    fullName: string;
    comment: string;
    companyId: number;
    masterId?: number;
    abpId: string;
    isMaster: boolean;
    abpTenant: AbpTenantUpdateDto;
}

To circumvent potential issues with compatibility, we have decided to handle CRUD operations using two repositories - ABP ITenantRepository and IIdentityUserRepository. Unfortunately, it raised a major transaction issue: row lock when trying to create (not tested thouroughly on other operations, but sure the issue exists there as well) a new tenant. We have tried different approaches (including using ITenantAppService directly instead of ITenantRepository) to resolve it, but none of them worked:

            //using ITenantAppService
            using var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: true);
            var abpTenantDto = await _abpTenantAppService.CreateAsync(input.AbpTenant);
            var tenant = ObjectMapper.Map<CreateTenantDto, Tenant>(input);
            tenant.AbpId = abpTenantDto.Id; //causes row lock SOMETIMES
            var newTenant = await _tenantRepository.InsertAsync(tenant);
            await uow.CompleteAsync(); //this operations hangs SOMETIMES because of row lock
            return ObjectMapper.Map<Tenant, TenantDto>(newTenant);
            
            //using ITenantRepository
            using var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: true);
            var abpTenant = await _abpTenantManager.CreateAsync(input.AbpTenant.Name, input.AbpTenant.EditionId);
            input.AbpTenant.MapExtraPropertiesTo(abpTenant);
            var newAbpTenant = await _abpTenantRepository.InsertAsync(abpTenant);
            var tenant = ObjectMapper.Map<CreateTenantDto, Tenant>(input);
            tenant.AbpId = abpTenant.Id; //causes row lock ALWAYS
            var newTenant = await _tenantRepository.InsertAsync(tenant);
            await uow.CompleteAsync(); //this operations hangs ALWAYS because of row lock
            return ObjectMapper.Map<Tenant, TenantDto>(newTenant);
            

Now we use two separate commits (ABP tenant, then - our tenant) as a workaround (deleting the first entry if first commit failed), which of course is not good at all and is just a temporary solution:

            #region ABP tenant commit
            using var abpTenantUow = _unitOfWorkManager.Begin(requiresNew: true);
            Tenant newTenant = null;
            var abpTenant = await _abpTenantManager.CreateAsync(input.AbpTenant.Name, input.AbpTenant.EditionId);
            input.AbpTenant.MapExtraPropertiesTo(abpTenant);
            var newAbpTenant = await _abpTenantRepository.InsertAsync(abpTenant);
            await abpTenantUow.CompleteAsync();
            #endregion ABP tenant commit

            #region Tenant commit
            using var tenantUow = _unitOfWorkManager.Begin(requiresNew: true);
            var tenant = ObjectMapper.Map<CreateTenantDto, Tenant>(input);
            tenant.AbpId = abpTenant.Id;
            newTenant = await _tenantRepository.InsertAsync(tenant);
            tenantUow.Failed += async (sender, args) =>
            {
                using var abpTenantDeleteUow = _unitOfWorkManager.Begin(requiresNew: true);
                await _abpTenantRepository.HardDeleteAsync(abpTenant);
                await abpTenantDeleteUow.CompleteAsync();
            };
            await tenantUow.CompleteAsync();
            #endregion Tenant commit

Here is the sessions screenshot displaying the row lock when trying to use one transaction:

Could you please help us to resolve transaction issue in the first place and also suggest how to handle two-tenant-approach in the most correct way on both back-end and front-end side?

Saying honestly, not a very good solution, imho. Usually DTO is just a bunch of fields and should not contain some functionality. Handling all the logic in OnModelCreating looks fine and logical. So - since it's not possible to get new data straight from update object - I think I will have to re-read the entity.

OK, thanks, now I know the property values not changed, but parameter values instead. But what do you mean by DTO normalization? Carry on some operations on DTOs? But if so - it's easier just to re-read the entity to get correct up-to-date values.

I wonder why UpdateAsync returns INPUT data, even if the input data has been changed? For instance, I have the space truncating rule for my entity:

b.Property(x => x.Domain)
    .HasConversion(new ValueConverter&lt;string, string&gt;(v => v.Trim(), v => v.Trim()))
    .HasColumnName("C_DOMAIN").HasMaxLength(DbConsts.DomainMaxLength);
                

Despite this fact, the method returns INPUT data which does not have truncation:

public async Task&lt;ModuleDto&gt; UpdateAsync(ModuleKeyDto id, UpdateModuleDto input)
{
    var module = await _moduleRepository.GetAsync(m => m.ApplicationId == id.ApplicationId && m.ModuleId == id.ModuleId);
    if (module == null)
    {
        throw new UserFriendlyException("Module not found", ErrorCodes.NotFound);
    }
    ObjectMapper.Map(input, module);
    var updatedModule = await _moduleRepository.UpdateAsync(module); // updatedModule container non-truncated value that was present in module!!!
    await CurrentUnitOfWork.SaveChangesAsync();
    return ObjectMapper.Map&lt;Module, ModuleDto&gt;(updatedModule);
}

Looks like a bug? I can re-read the entity and it then will look allright, but don't want to make an extra trip to DB.

I see now... But is dotnet-ef tool the same thing as Microsoft.EntityFrameworkCore.Tools package? So basically I can use PM commands add-migration / script-migration -from 0 and so on instead? I just need to leave only ABP-related tables inside CentralToolsMigrationsDbContext.OnModelCreating method.

I've tried that (I used EntityFrameworkCore project as a base and added the Microsoft.EntityFrameworkCore.Design reference as asked by the tool). After running migration tool, I'm getting

Unable to create an object of type 'XXXXDbContext' //this is the class inherited from AbpDbContext<XXXXDbContext>

Hi,

we are not planning to use DB migration for our project tables - we have predefined DB tables structure, which will be changed by applying SQL scripts and the code just must be in-sync with it. At the same time, we understand DB migration mechanism is used in ABP Framework solution to create default tables (ABP[XXX], IDENTITYSERVER[XXX]) - so when ABP Framework gets updated, these tables might be updated accordingly.

Could you please suggest the best approach to use in the solution? Is it possible to go without DbMigration-related projects at all? Or it needs to be some selective migration? How to set it up?

I hope it will be easy... :) Step 1) Create a boilerplate project, as described in the abp doc; Step 2) Create the folder on a remote machine for publishing IdentityServer project, set it up as an IIS application under Default Web Site and assign a separate app pool to it: Step 3) Publish IdentityServer project to that remote machine using the publish profile as below (please replace XXX / YYY / ZZZ and some other dummy values with relevant values):

IdentityServer_to_RemoteMachine.pubxml

<?xml version="1.0" encoding="utf-8"?>
<!--
This file is used by the publish/package process of your Web project. You can customize the behavior of this process
by editing this MSBuild file. In order to learn more about this please visit https://go.microsoft.com/fwlink/?LinkID=208121. 
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <WebPublishMethod>MSDeploy</WebPublishMethod>
    <PublishProvider>AzureVirtualMachine</PublishProvider>
    <LastUsedBuildConfiguration>Debug</LastUsedBuildConfiguration>
    <LastUsedPlatform>Any CPU</LastUsedPlatform>
    <SiteUrlToLaunchAfterPublish>http://XXX.cloudapp.azure.com/YYY.IdentityServer</SiteUrlToLaunchAfterPublish>
    <LaunchSiteAfterPublish>True</LaunchSiteAfterPublish>
    <ExcludeApp_Data>False</ExcludeApp_Data>
    <ProjectGuid>073c361e-b8f4-49f5-93cc-72a3ff49c026</ProjectGuid>
    <MSDeployServiceURL>XXX.cloudapp.azure.com:8172</MSDeployServiceURL>
    <DeployIisAppPath>Default Web Site\YYY.IdentityServer</DeployIisAppPath>
    <RemoteSitePhysicalPath />
    <SkipExtraFilesOnServer>False</SkipExtraFilesOnServer>
    <MSDeployPublishMethod>WMSVC</MSDeployPublishMethod>
    <EnableMSDeployBackup>True</EnableMSDeployBackup>
    <UserName>YOUR_USERNAME</UserName>
    <_SavePWD>True</_SavePWD>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
    <SelfContained>false</SelfContained>
  </PropertyGroup>
</Project>

IdentityServer_to_RemoteMachine.pubxml.user

<?xml version="1.0" encoding="utf-8"?>
<!--
This file is used by the publish/package process of your Web project. You can customize the behavior of this process
by editing this MSBuild file. In order to learn more about this please visit https://go.microsoft.com/fwlink/?LinkID=208121. 
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <TimeStampOfAssociatedLegacyPublishXmlFile />
    <EncryptedPassword>YOUR_ENCRYPTEDPASSWORD</EncryptedPassword>
  </PropertyGroup>
  <ItemGroup>
    <DestinationConnectionStrings Include="Default">
      <Value>YOUR_CONNECTIONSTRING</Value>
    </DestinationConnectionStrings>
  </ItemGroup>
</Project>

Step 3) When the published IdentityServer website is being run, you (hopefully) will see the same broken UI as reported by me earlier;

I have not tried to deploy to local IIS server yet - in fact, it's even not set up, because we supposed to use remote DEV machine for running our backend. BTW, when I run this app via IIS Express - I can't see the 'Lepton.Global' bundle loading at all. I've tried to figure out what this bundle was about, playing around AbpBundlingOptions in my IdentityServerModule and hoping to override LeptonThemeBundles.Scripts.Global / LeptonThemeBundles.Styles.Global bundles - but seems like those are not bundle names...

Showing 131 to 140 of 141 entries
Made with ❤️ on ABP v9.1.0-rc.1. Updated on January 17, 2025, 14:13