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?
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<string, string>(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<ModuleDto> 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<Module, ModuleDto>(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.
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?
Hi, I've deployed boilerplate projects (HttpApi.Host and IdentityServer) to Azure VM.
Both are placed under "Default Web Site" as applications. While I was able to resolve a path issues for HttpApi.Host application and it is displayed properly in browser, I still get a broken markup for IdentityServer application (as shown in the screenshot below): probably, it has to do with incorrect application paths for bundle creation - they are considered for web root folder, not for application folder. Could you please help me to fix this? Are there other places in the solution I might need to check and make the changes for setting correct root folder (i.e. application folder is a root folder, not a web site root folder).