Activities of "bmulinari"

Hi,

Thanks for the clarification! Maybe this detail of setting Applications["MVC"].RootUrl should be in the documentation, since although not required for the subdomain tenant resolver to work, it's certainly something that should be done for a complete implementation.

Hi there. We have configured ABP’s built-in subdomain tenant resolver using AddDomainTenantResolver(...), and inbound tenant resolution works as expected. Requests to tenant subdomains correctly resolve CurrentTenant.

However, we noticed that Account module emails, like the password reset email sent when a tenant user uses “Forgot password”, do not use the tenant subdomain URL. The generated reset link points to the configured host/base URL, so the link does not work correctly for tenant users in our subdomain setup.

We found that setting the MVC application root URL to the tenant-aware template (replacing it's default value of configuration["App:SelfUrl"]) appears to solve it:

Configure<AppUrlOptions>(options =>
{
    options.Applications["MVC"].RootUrl = "https://{0}.example.com";
});

Can you confirm whether configuring options.Applications["MVC"].RootUrl this way is the correct approach for tenant-aware Account email links? We could not find anything related to this in the documentation, so we want to make sure we are not doing anything we shouldn't that could cause unwanted side effects.

Thanks!

Problem

When using LeptonX with Blazorise <Select>, validation runs but the visible select control does not show the validation state.

A required Blazorise text input correctly shows:

  • red outline
  • validation message

A required Blazorise select on the same form does not show:

  • red outline
  • validation message

This appears to happen because Blazorise applies validation state and feedback to the native <select>, but LeptonX hides that native element and renders a .custom-select-display proxy. The proxy does not mirror is-invalid, aria-invalid, or aria-describedby, and the validation feedback is not shown visually.

Repro

@page "/leptonx-select-validation-repro"
@using System.ComponentModel.DataAnnotations

<PageTitle>LeptonX Select Validation Repro</PageTitle>

<Card Width="Width.Is25">
    <CardHeader>
        <Heading Size="HeadingSize.Is4">LeptonX Select Validation Repro</Heading>
    </CardHeader>
    <CardBody>
        <Form>
            <Validations @ref="ValidationsRef"
                         Mode="ValidationMode.Auto"
                         Model="@Model"
                         ValidateOnLoad="false">
                <Validation>
                    <Field>
                        <FieldLabel>Required Text *</FieldLabel>
                        <TextInput @bind-Value="@Model.RequiredText">
                            <Feedback>
                                <ValidationError />
                            </Feedback>
                        </TextInput>
                    </Field>
                </Validation>

                <Validation>
                    <Field>
                        <FieldLabel>Required Select *</FieldLabel>
                        <Select TValue="string" @bind-Value="@Model.RequiredSelect">
                            <ChildContent>
                                <SelectItem Value="@string.Empty"></SelectItem>
                                <SelectItem Value="@("one")">Option one</SelectItem>
                                <SelectItem Value="@("two")">Option two</SelectItem>
                            </ChildContent>
                            <Feedback>
                                <ValidationError />
                            </Feedback>
                        </Select>
                    </Field>
                </Validation>
            </Validations>

            <Button Color="Color.Primary" Clicked="ValidateAsync">
                Validate
            </Button>
        </Form>
    </CardBody>
</Card>

@code {
    private Validations ValidationsRef { get; set; } = new();

    private ReproModel Model { get; } = new();

    private Task ValidateAsync() => ValidationsRef.ValidateAll();

    private sealed class ReproModel
    {
        [Required(ErrorMessage = "The Required Text field is required.")]
        public string? RequiredText { get; set; }

        [Required(ErrorMessage = "The Required Select field is required.")]
        public string? RequiredSelect { get; set; } = string.Empty;
    }

}

Awesome, thank you.

Hi there. Any updates on this?

There seems to be a bug in the way permissions are checked on toolbar buttons when the required policy is specified in the AddButton method, where we will get a spam of unnecessarily permissions checks on every minimal interaction with the page, like on every key press, for instance.

  • Steps to reproduce the issue:
  • While in debug mode, navigate to Administration -> Identity management -> Users (just an example, this seems to occur on any page with toolbar buttons)
  • Click New User or Edit on an existing user and start typing in any field
  • Verify in the console output that for every character typed a set of permission checks happen:
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Import
[15:26:29 DBG] Found in the cache: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Import
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:R,pk:admin,n:AbpIdentity.Users.Import
[15:26:29 DBG] Found in the cache: pn:R,pk:admin,n:AbpIdentity.Users.Import
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Export
[15:26:29 DBG] Found in the cache: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Export
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:R,pk:admin,n:AbpIdentity.Users.Export
[15:26:29 DBG] Found in the cache: pn:R,pk:admin,n:AbpIdentity.Users.Export
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Create
[15:26:29 DBG] Found in the cache: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Create
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:R,pk:admin,n:AbpIdentity.Users.Create
[15:26:29 DBG] Found in the cache: pn:R,pk:admin,n:AbpIdentity.Users.Create
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Import
[15:26:29 DBG] Found in the cache: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Import
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:R,pk:admin,n:AbpIdentity.Users.Import
[15:26:29 DBG] Found in the cache: pn:R,pk:admin,n:AbpIdentity.Users.Import
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Export
[15:26:29 DBG] Found in the cache: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Export
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:R,pk:admin,n:AbpIdentity.Users.Export
[15:26:29 DBG] Found in the cache: pn:R,pk:admin,n:AbpIdentity.Users.Export
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Create
[15:26:29 DBG] Found in the cache: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Create
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:R,pk:admin,n:AbpIdentity.Users.Create
[15:26:29 DBG] Found in the cache: pn:R,pk:admin,n:AbpIdentity.Users.Create
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users
[15:26:29 DBG] Found in the cache: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:R,pk:admin,n:AbpIdentity.Users
[15:26:29 DBG] Found in the cache: pn:R,pk:admin,n:AbpIdentity.Users
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Import
[15:26:29 DBG] Found in the cache: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Import
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:R,pk:admin,n:AbpIdentity.Users.Import
[15:26:29 DBG] Found in the cache: pn:R,pk:admin,n:AbpIdentity.Users.Import
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Export
[15:26:29 DBG] Found in the cache: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Export
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:R,pk:admin,n:AbpIdentity.Users.Export
[15:26:29 DBG] Found in the cache: pn:R,pk:admin,n:AbpIdentity.Users.Export
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Create
[15:26:29 DBG] Found in the cache: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Create
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:R,pk:admin,n:AbpIdentity.Users.Create
[15:26:29 DBG] Found in the cache: pn:R,pk:admin,n:AbpIdentity.Users.Create
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Import
[15:26:29 DBG] Found in the cache: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Import
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:R,pk:admin,n:AbpIdentity.Users.Import
[15:26:29 DBG] Found in the cache: pn:R,pk:admin,n:AbpIdentity.Users.Import
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Export
[15:26:29 DBG] Found in the cache: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Export
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:R,pk:admin,n:AbpIdentity.Users.Export
[15:26:29 DBG] Found in the cache: pn:R,pk:admin,n:AbpIdentity.Users.Export
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Create
[15:26:29 DBG] Found in the cache: pn:U,pk:3a1beb3d-8133-1e8d-48ee-2869932f2b57,n:AbpIdentity.Users.Create
[15:26:29 DBG] PermissionStore.GetCacheItemAsync: pn:R,pk:admin,n:AbpIdentity.Users.Create
[15:26:29 DBG] Found in the cache: pn:R,pk:admin,n:AbpIdentity.Users.Create
[15:26:32 DBG] Get dynamic claims cache for user: 3a1beb3d-8133-1e8d-48ee-2869932f2b57
  • Workaround we found for our own custom pages:

Default implementation where we observe the problem above, with the required permissions being specified as a parameter of the AddButton method:

private ValueTask SetToolbarItemsAsync()
{
    Toolbar.AddButton(L["Page:Button:ButtonA"],
        async () =>
        {
            await SomeActionAsync();
        },
        IconName.Sync,
        Blazorise.Color.Primary,
        requiredPolicyName: MyPermissions.MyPage.Default);

    Toolbar.AddButton(L["Page:Button:ButtonB"],
        async () =>
        {
            await SomeOtherActionAsync();
        },
        IconName.Download,
        Blazorise.Color.Secondary,
        requiredPolicyName: MyPermissions.MyPage.Default);

    return ValueTask.CompletedTask;
}

As a workaround, when we wrap the permission check around the AddButton method while removing it as a parameter, we no longer observe the permission check spam:

private async ValueTask SetToolbarItemsAsync()
{
    if (await PermissionChecker.IsGrantedAsync(MyPermissions.MyPage.Default))
    {
        Toolbar.AddButton(L["Page:Button:ButtonA"],
            async () =>
            {
                await SomeActionAsync();
            },
            IconName.Sync,
            Color.Primary);
        Toolbar.AddButton(L["Page:Button:ButtonB"],
            async () =>
            {
                await SomeOtherActionAsync();
            },
            IconName.Download,
            Color.Secondary);
    }
}

[maliming] said: hi

the distributed handler creates a separate UoW to run the migrations in with _unitOfWorkManager.Begin(requiresNew: true), which queries the database before the original UoW commits. Result: tenantConfiguration.ConnectionStrings.Default is null, so migrations don't run when they should.

The TenantCreatedEto event should be published when a tenant is created. Which means the new tenant is already in the database.

Thanks.

As I understand, the tenant technically is already in the database, but the transaction is not yet committed at that point, correct? Since if my TenantCreatedEto handler throws, the transaction is rolled back and it's like the entity was never added.

So, since the transaction is not yet committed, when MyAppTenantDatabaseMigrationHandler starts a completely new UoW with requiresNew=true before applying the migrations, it will not "see" the connection string I just set to the tenant in the previous handler, causing the migrations to not be applied. That is the problem.

We are testing a service to automatically provision the database and user for a tenant whenever a tenant is created, ensuring proper tenant isolation at a database level. Although the provisioning is working as we expect, the events that should be triggered after a tenant is created are not.

The current implementation consists of two handlers that listen to TenantCreatedEto:

1. Local handler (order -1): Runs first. Provisions database and sets connection string to the new tenant:

// Inside TenantDatabaseProvisioner.ProvisionAsync()
await CreateDatabaseAsync(hostConnectionString, databaseName);
await CreateUserAsync(hostConnectionString, userName, password);
await GrantPrivilegesAsync(hostConnectionString, databaseName, userName);

var tenantConnectionString = BuildTenantConnectionString(hostConnectionString, databaseName, userName, password);

var tenant = await tenantRepository.FindByIdAsync(tenantId);
tenant.SetDefaultConnectionString(tenantConnectionString);
await tenantRepository.UpdateAsync(tenant, true);  // Still within the original UoW

2. Default MyAppTenantDatabaseMigrationHandler: This is default ABP code to run migrations/seeding:

private async Task MigrateAndSeedForTenantAsync(Guid tenantId, string adminEmail, string adminPassword)
{
    using (_currentTenant.Change(tenantId))
    {
        // Creates a NEW Unit of Work
        using (var uow = _unitOfWorkManager.Begin(requiresNew: true, isTransactional: false))
        {
            var tenantConfiguration = await _tenantStore.FindAsync(tenantId);

            // THIS CHECK FAILS - configuration/connection string is still null here, so migrations don't run
            if (tenantConfiguration?.ConnectionStrings != null &&
                !tenantConfiguration.ConnectionStrings.Default.IsNullOrWhiteSpace())
            {
                foreach (var migrator in _dbSchemaMigrators)
                {
                    await migrator.MigrateAsync();
                }
            }
            await uow.CompleteAsync();
        }

        // Seed data
        // ...
    }
}

Problem: the distributed handler creates a separate UoW to run the migrations in with _unitOfWorkManager.Begin(requiresNew: true), which queries the database before the original UoW commits. Result: tenantConfiguration.ConnectionStrings.Default is null, so migrations don't run when they should.

  • Are there built-in mechanisms for this tenant database provisioning scenario?
  • What would be the recommended approach for running migrations when the connection string is set dynamically during tenant creation?

Thanks!

Hi there.

Any way to get this behavior in the permissions modal?

Hi there. In our application we implemented a page that utilizes the default ABP app services IIdentityUserAppService and IIdentityRoleAppService to list users and roles. Therefore, this page requires the permissions "AbpIdentity.Users" and "AbpIdentity.Roles" to work correctly.

So, to avoid unexpected permission errors when setting up the roles for our application, we need to implement a proper permission dependency when defining our own permissions, as explained in the documentation here and here.

With this in mind, we added the dependencies to our permission definition:

public class MyModulePermissionDefinitionProvider : PermissionDefinitionProvider
{
    public override void Define(IPermissionDefinitionContext context)
    {
        var myModuleGroupDefinition = context.AddGroup(MyModulePermissions.GroupName, L("Permission:MyModule"));
        var myModuleGroup = myModuleGroupDefinition.AddPermission(MyModulePermissions.Home, L("Permission:MyModule:Home"), MultiTenancySides.Tenant);

        var reportsPermissions = myModuleGroup.AddChild(MyModulePermissions.Reports.Default, L("Permission:MyModule:Reports"), MultiTenancySides.Tenant);
        reportsPermissions.AddChild(MyModulePermissions.Reports.ReportManagement, L("Permission:MyModule:Reports:ReportManagement"), MultiTenancySides.Tenant)
            .RequirePermissions("AbpIdentity.Users", "AbpIdentity.Roles"); // this is what we added
    }

    private static LocalizableString L(string name)
    {
        return LocalizableString.Create<MyModuleResource>(name);
    }
}

What I expected to happen: for my custom permission MyModulePermissions.Reports.ReportManagement to always require the permissions "AbpIdentity.Users" and "AbpIdentity.Roles", in a way that they should be automatically selected in the permissions pop-up when my custom permission is selected, or at least give me an error and not allow me to save until the dependent permissions are also selected, making the permission dependencies clear and mandatory when setting up the permissions for users or roles.

What actually happens: in the permissions pop-up I can select my custom permission MyModulePermissions.Reports.ReportManagement and save without any action or feedback that my custom permission depends on other permissions. From my tests, all RequirePermissions does is consider my custom permission not granted unless the other required permissions are also granted, which is very confusing since my menu entry won't appear for the user even when they have that permission granted (but not its dependencies).

Am I missing something or is this the expected behavior? If so, is there a way to implement permission dependencies in the way I described?

Thanks!

Showing 1 to 10 of 37 entries
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on September 28, 2026, 11:44
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.