3.3.2 / Angular
We are extending HangfireDashboard
- adding custom tab for handling recurring jobs. As a base, we are about to use existing Github extension (RecurringJobAdmin
) which is plumbed like this:
private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration)
{
context.Services.AddHangfire(globalConfig =>
{
globalConfig
.UseStorage(new OracleStorage(configuration.GetConnectionString("Default")))
.UseRecurringJobAdmin(typeof(CentralToolsApplicationModule).Assembly);
});
}
However here is where the problem is: this extension uses own UI styles and Vue as JS Framework. We don't want to keep using it as a Nuget package. Instead, we would like to have own Module - with the same UI as other pages and get rid of Vue (in favor of Angular or without it). Here's what it looks like:
The questions are:
RecurringJobAdmin
extension uses just a sole Vue.js file to handle this; So is there an easier solution? What we would need though is 'change detection' on Admin page and of course data exchange between client and server part;3.3.1 / Angular
Hi ABP team.
I created solution as an ABP module. At this moment i would like to check for permissions which were granted for a user's roles. I'm using IsGrantedAsync
method of IPermissionStore
interface. But this method returns negative result every time. I'm using "* .HttpApi.Host" project to run and test my solution.
Also I've found out that information about user isn't complete: the user's roles are absent in CurrentUser
member of ApplicationService
object, but access token contains this data.
Could you please suggest what I did wrong and how it can be fixed?
I would like to add a custom provider name like "Q", what am I supposed to do in this case and how to make IPermissionStore
interface methods work with a new provider name?
After upgrading to the ABP version where login workflow changed, our published Azure app stopped logging user in - it just redirects back to login page. We don't know what settings need to be changed.
Here are the details:
some hand-made SSL certificate is installed
configurations are as follows:
`
const baseUrl = 'https://xxxxxx.azure.com/CentralTools';
export const environment = {
production: false,
application: {
baseUrl,
name: 'CentralTools'
},
oAuthConfig: {
issuer: 'https://xxxxxx.azure.com/identityserver',
redirectUri: baseUrl,
clientId: 'CentralTools_App',
dummyClientSecret: '1q2w3e*',
scope: 'CentralTools AuditLogging offline_access',
strictDiscoveryDocumentValidation: true,
timeoutFactor: 0.9, // default value is 0.75 - Timeout for updating access_token
responseType: 'code', // This is parameter is required in order to get new access_token via refresh token
showDebugInformation: true,
requireHttps: true
},
apis: {
default: {
url: 'https://xxxxxx.azure.com/httpapihost'
},
AuditLogging: {
url: 'https://xxxxxx.azure.com/auditlogging'
}
},
localization: {
defaultResourceName: 'CentralTools'
}
};
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
app.UseCookiePolicy(new CookiePolicyOptions
{
MinimumSameSitePolicy = SameSiteMode.Lax
});
var env = context.GetEnvironment();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAbpRequestLocalization();
app.UseHsts();
app.UseHttpsRedirection();
if (!env.IsDevelopment())
{
app.UseErrorPage();
}
app.UseCors(DefaultCorsPolicyName);
app.UseCorrelationId();
app.UseVirtualFiles();
app.UseRouting();
app.UseAuthentication();
if (MultiTenancyConsts.IsEnabled)
{
app.UseMultiTenancy();
}
app.UseIdentityServer();
app.UseAuthorization();
app.UseAuditing();
app.UseConfiguredEndpoints();
}
{
"App": {
"SelfUrl": "https://xxxxxx.azure.com/IdentityServer",
"CorsOrigins": "https://xxxxxx.azure.com,https://localhost/CentralTools"
},
...
}
We want to work with some service inside AutoMapper profile. Below is just the idea which does not work. Please suggest how to implement such DI:
private void ConfigureAutoMapper(ServiceConfigurationContext context)
{
context.Services.AddSingleton(provider => new AutoMapper.MapperConfiguration(cfg =>
{
//TODO: implement
cfg.AddProfile(new CentralToolsApplicationAutoMapperProfile
(
provider.GetService<IBlobContainer<FileContainer>>()
));
}).CreateMapper());
}
public class CentralToolsApplicationAutoMapperProfile : AutoMapper.Profile
{
//TODO: implement
public CentralToolsApplicationAutoMapperProfile()
{
CreateMap(...);
CreateMap(...);
}
}
public class MyCustomUserMapper : IObjectMapper<User, UserDto>, ITransientDependency
approach seems not to be useful for our case.
Hi, we can see that meanwhile token lifetime is not prolonged per each request in UI, there is a fixed lifetime that is configured (1 year by default for now, as far as I remember). We want to provide a specific token life, like 15 mins and to prolong token lifetime per each request by this value. How would you recommend to do that?
Hi, we are using Tenants functionality and noticed it's only possible to create a tenant in UI by a user logged in under a 'null'-tenant (super tenant?) We need to create tenants by a user, logged in under ANOTHER tenant - we have a custom tree-like structure of tenants in our system.
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.
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.