Activities of "maliming"

Hi Aníbal,

Thanks for the detailed report. You're right that this looks like a license/module entitlement issue on our side rather than something for you to work around in the generated project.

We'll check and adjust the Personal license entitlement for the Volo.Abp.OperationRateLimiting.* packages on our end. I'll get back to you on this thread once it's done.

Thanks,

Answer

Hi,

Glad you spotted the difference — this is actually by design.

ABP runs an online license check only when ASPNETCORE_ENVIRONMENT=Development (or the debugger is attached). In any other environment (Staging, Production, etc.) only the offline check runs, which just needs the AbpLicenseCode value from configuration.

The online check expects the abp login token file (%UserProfile%\.abp\cli\access-token.bin on Windows, ~/.abp/cli/access-token.bin on Linux) to be present in the process's user home directory at runtime. That's why logging in on the dev PC and publishing didn't help — that file lives under your dev user profile, it's not part of the project, so dotnet publish doesn't include it. And even if you copied it into the Function, the token is bound to the developer machine identity, so it's not the right thing to ship to Azure anyway.

Recommendation for the Azure Function:

  1. Set ASPNETCORE_ENVIRONMENT to Staging or Production in the Function App's Environment variables — never Development for a deployed environment.
  2. Make sure AbpLicenseCode is also set there (the long base64 string from your local appsettings.secrets.json).

With those two settings, only the offline check runs and it will pass without needing any abp login state on the server.

Treat Development as something only your local dev machine ever uses — it assumes there is an interactive developer with an active abp login session.

Thanks

Hi,

ABP doesn't ship a dedicated "event-driven notification management" module that wires events to templates with a host-level on/off switch. But every building block you need is already in the framework, and composing them takes only a thin coordinator class. Here is the pattern we recommend.

1. Define your notification events as constants

public static class AppNotificationEvents
{
    public const string UserRegistered = "App.Notifications.UserRegistered";
    public const string OrderPlaced    = "App.Notifications.OrderPlaced";
}

2. Register a host-only setting per event (the on/off switch)

WithProviders("G") (= GlobalSettingValueProvider.ProviderName) restricts the setting to the host level, so tenants cannot override it.

public class AppNotificationSettingDefinitionProvider : SettingDefinitionProvider
{
    public override void Define(ISettingDefinitionContext context)
    {
        context.Add(new SettingDefinition(
                $"{AppNotificationEvents.UserRegistered}.EmailEnabled",
                defaultValue: "true",
                isVisibleToClients: true)
            .WithProviders(GlobalSettingValueProvider.ProviderName));

        context.Add(new SettingDefinition(
                $"{AppNotificationEvents.OrderPlaced}.EmailEnabled",
                defaultValue: "true",
                isVisibleToClients: true)
            .WithProviders(GlobalSettingValueProvider.ProviderName));
    }
}

3. Register a text template per event

public class AppEmailTemplateDefinitionProvider : TemplateDefinitionProvider
{
    public override void Define(ITemplateDefinitionContext context)
    {
        context.Add(new TemplateDefinition(
                AppNotificationEvents.UserRegistered,
                layout: StandardEmailTemplates.Layout,
                localizationResource: typeof(AppResource))
            .WithVirtualFilePath("/Emailing/Templates/UserRegistered.tpl", isInlineLocalized: true));
    }
}

Add the Text Template Management module (Volo.Abp.TextTemplateManagement.*) to your solution. With it installed, host admins can edit the rendered content of every template through the UI; ITemplateRenderer checks the DB first and falls back to your .tpl file, so updates take effect without redeploy.

4. The coordinator: setting check → render → send

This is the single place every notification flows through.

public class NotificationDispatcher : IDomainService, ITransientDependency
{
    protected ITemplateRenderer TemplateRenderer { get; }
    protected ISettingProvider SettingProvider { get; }
    protected IEmailSender EmailSender { get; }

    public NotificationDispatcher(
        ITemplateRenderer templateRenderer,
        ISettingProvider settingProvider,
        IEmailSender emailSender)
    {
        TemplateRenderer = templateRenderer;
        SettingProvider = settingProvider;
        EmailSender = emailSender;
    }

    public virtual async Task DispatchEmailAsync(
        string eventCode, string to, string subject, object model, string? cultureName = null)
    {
        var enabled = await SettingProvider.GetAsync<bool>($"{eventCode}.EmailEnabled");
        if (!enabled)
        {
            return;
        }

        var body = await TemplateRenderer.RenderAsync(eventCode, model, cultureName);
        await EmailSender.QueueAsync(to, subject, body);
    }
}

QueueAsync pushes the email into the ABP background job queue, so the calling event handler isn't blocked by SMTP.

5. Hook your business events

Any local or distributed event handler (or just any application service) calls the dispatcher:

public class UserRegisteredEmailHandler
    : ILocalEventHandler<UserRegisteredEto>, ITransientDependency
{
    private readonly NotificationDispatcher _dispatcher;

    public UserRegisteredEmailHandler(NotificationDispatcher dispatcher)
        => _dispatcher = dispatcher;

    public Task HandleEventAsync(UserRegisteredEto eventData)
        => _dispatcher.DispatchEmailAsync(
            AppNotificationEvents.UserRegistered,
            eventData.Email,
            "Welcome",
            new { eventData.UserName, eventData.TenantName });
}

Inside the .tpl file you reference the model fields ({{model.user_name}}, {{model.tenant_name}}, …) using Scriban or Razor depending on which engine you registered.

Summary

  • Static event codes give you a stable contract.
  • Host-only settings give the host admin the per-event email on/off switch you described.
  • TemplateDefinition + the Text Template Management module gives admins a UI to edit template content.
  • NotificationDispatcher is the single chokepoint that checks the switch, renders, and sends — keeps your business code clean.

If you later need admins to add new event codes at runtime (without redeploy), you can promote the static class into a NotificationEvent entity (Code / TemplateName / IsEmailEnabled) and resolve it from DB inside the dispatcher — but most teams don't need that and the static approach is much simpler.

Thanks

Answer

Hi,

The error ABP-LIC-ERROR - License check failed for 'Volo.Abp.OpenIddict.Pro.Domain-v10.0.2.0' means the running process can't find your license code at startup. It's not related to the AI Management module — any ABP commercial module (OpenIddict.Pro, Identity.Pro, etc.) does the same check. That's also why rolling back to the previous code didn't help: the issue is on the deployment side, not in your code.

ABP reads the license code from configuration with the key AbpLicenseCode. In a project generated by ABP Studio, this value lives in appsettings.secrets.json next to each host project (e.g. *.Web.Host, *.AuthServer, *.HttpApi.HostWithIds):

{
  "AbpLicenseCode": "PABMAGkAYwBlAG4Acw...=="
}

A couple of quick questions to confirm where the gap is:

  1. Open your local solution and check any host project — do you see an appsettings.secrets.json with an AbpLicenseCode value? (If yes, that's the value we need.)
  2. Is the Azure Function project the one being deployed, or one of the standard ABP host projects?

For Azure Functions, appsettings.secrets.json usually doesn't get picked up automatically (it's not part of the default Function host configuration, and even if you publish the file, the Functions runtime won't load it unless you wire it up explicitly). The cleanest fix is to set AbpLicenseCode as an Application Setting on the Function App itself:

  1. Azure Portal → your Function App → Settings → Environment variables (older portals: Configuration → Application settings)
  2. Add a new setting:
    • Name: AbpLicenseCode
    • Value: paste the long base64 string from appsettings.secrets.json
  3. Save and restart the Function App.

ASP.NET Core's environment variables provider exposes this to IConfiguration["AbpLicenseCode"], so the license check will pass without depending on whether the secrets file gets published.

If you can't find AbpLicenseCode anywhere in your local solution, let me know and I'll help you locate / regenerate it.

Thanks

Great

Hi,

One clarification first: the framework does not leave orphaned references behind. Looking at OrganizationUnitAppService.DeleteAsync:

await UserManager.UpdateOrganizationAsync(id, null);   // unlinks all users from this OU
await OrganizationUnitManager.DeleteAsync(id);         // removes members + roles + child OUs, then the OU itself

The first call runs an ExecuteDelete over IdentityUserOrganizationUnit for the OU. The manager then recursively removes child OU memberships/roles before deleting the OU. So the link tables are actively cleaned up — no dangling rows.

The intentional design is "delete = auto-cleanup". Our own MVC and Blazor delete dialogs already wrap the API with this safeguard:

  1. Load the OU and its UserCount first.
  2. If UserCount > 0, force the operator to choose: Unassign the users (default), or move them to another OU they pick from a list.
  3. Submit MoveAllUsersAsync(id, targetOuOrNull) followed by DeleteAsync(id).

You can see this in Volo.Abp.Identity.Pro.Web/Pages/Identity/OrganizationUnits/DeleteModal.cshtml(.cs) and the equivalent Blazor component. Calling DELETE directly from Swagger skips that prompt but lands at the same final state as picking "Unassign" in the UI — there's no orphaned data.

If your business rule is "API calls must explicitly clean up first, otherwise reject", that's a policy on top of the framework. The cleanest approach is to follow the same pattern as our UI from your client (check membership, decide, then call), or enforce it server-side by overriding the Pro app service:

[ExposeServices(typeof(IOrganizationUnitAppService))]
[Dependency(ReplaceServices = true)]
public class MyOrganizationUnitAppService : OrganizationUnitAppService
{
    public MyOrganizationUnitAppService(
        OrganizationUnitManager organizationUnitManager,
        IdentityUserManager userManager,
        IOrganizationUnitRepository organizationUnitRepository,
        IIdentityUserRepository identityUserRepository,
        IIdentityRoleRepository identityRoleRepository,
        IdentityProTwoFactorManager identityProTwoFactorManager)
        : base(organizationUnitManager, userManager, organizationUnitRepository,
               identityUserRepository, identityRoleRepository, identityProTwoFactorManager)
    {
    }

    public override async Task DeleteAsync(Guid id)
    {
        var ou = await OrganizationUnitRepository.GetAsync(id, includeDetails: true);

        var memberCount = await OrganizationUnitRepository.GetMembersCountAsync(ou, includeChildren: true);
        if (memberCount > 0)
        {
            throw new UserFriendlyException(
                $"Cannot delete organization unit '{ou.DisplayName}' because it has {memberCount} assigned user(s). Please unassign or move them first.");
        }

        if (ou.Roles.Any())
        {
            throw new UserFriendlyException(
                $"Cannot delete organization unit '{ou.DisplayName}' because it has assigned role(s). Please remove them first.");
        }

        await base.DeleteAsync(id);
    }
}

GetMembersCountAsync(..., includeChildren: true) covers users attached to any descendant OU as well, so deletion is blocked even if only a child OU has members. ou.Roles comes from the aggregate (an embedded collection on OrganizationUnit), so includeDetails: true is required when loading.

Thanks.

Hi,

Thanks for the detailed report — you found a real framework gap. Confirmed in OrganizationUnitManager.CreateAsync and MoveAsync: neither method validates that the supplied parentId belongs to the current tenant, so calling the API with a ParentId from another tenant goes through.

What actually ends up in the DB is a bit subtle: the new OU's TenantId is correctly set to your current tenant (Tenant A), and because the multi-tenant filter hides Tenant B's OU from the lookup, the Code is calculated as a top-level code (00001). But the ParentId column is stored as-is, pointing at Tenant B's OU id. That cross-tenant FK reference is the real data-integrity issue (and MoveAsync has the same gap).

We just pushed the fix in https://github.com/abpframework/abp/pull/25357 — the manager now validates the parent tenant for both create and move and throws Volo.Abp.Identity:010010 (OrganizationUnitParentTenantMismatch) when they don't match. It will ship in the next ABP release.

For now, you can apply the same check from your application layer by overriding the Pro OrganizationUnitAppService:

[ExposeServices(typeof(IOrganizationUnitAppService))]
[Dependency(ReplaceServices = true)]
public class MyOrganizationUnitAppService : OrganizationUnitAppService
{
    public MyOrganizationUnitAppService(
        OrganizationUnitManager organizationUnitManager,
        IdentityUserManager userManager,
        IOrganizationUnitRepository organizationUnitRepository,
        IIdentityUserRepository identityUserRepository,
        IIdentityRoleRepository identityRoleRepository,
        IdentityProTwoFactorManager identityProTwoFactorManager)
        : base(organizationUnitManager, userManager, organizationUnitRepository,
               identityUserRepository, identityRoleRepository, identityProTwoFactorManager)
    {
    }

    public override async Task<OrganizationUnitWithDetailsDto> CreateAsync(OrganizationUnitCreateDto input)
    {
        await EnsureParentInCurrentTenantAsync(input.ParentId);
        return await base.CreateAsync(input);
    }

    public override async Task MoveAsync(Guid id, OrganizationUnitMoveInput input)
    {
        await EnsureParentInCurrentTenantAsync(input.NewParentId);
        await base.MoveAsync(id, input);
    }

    protected virtual async Task EnsureParentInCurrentTenantAsync(Guid? parentId)
    {
        if (!parentId.HasValue)
        {
            return;
        }

        var parent = await OrganizationUnitRepository.FindAsync(parentId.Value);
        if (parent == null || parent.TenantId != CurrentTenant.Id)
        {
            throw new BusinessException("Identity:CrossTenantOrganizationUnitParent")
                .WithData("ParentId", parentId);
        }
    }
}

Use FindAsync (not GetAsync) so the multi-tenant filter naturally hides cross-tenant parents — you'll get a clean BusinessException either way.

Thanks again for catching this.

Hi,

OpenIddict (and ABP's TokenController.HandleClientCredentialsAsync) only puts the scopes the client explicitly asked for into the access token. The scopes you tick on the application page are an allow-list, not defaults — if the client sends no scope parameter, the issued token has no scope at all, which is why your [Authorize] check fails.

OAuth 2.0 actually allows the authorization server to fall back to a pre-defined default scope (RFC 6749 §3.3 and §4.4.2), but ABP doesn't ship a built-in switch for that. The cleanest way to add it is a small IAbpOpenIddictClaimsPrincipalHandler that, on client_credentials requests with an empty scope, copies the application's oi_scp: permissions into the principal:

using System.Collections.Immutable;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using OpenIddict.Abstractions;
using Volo.Abp.DependencyInjection;
using Volo.Abp.OpenIddict;

public class DefaultClientCredentialsScopeHandler : IAbpOpenIddictClaimsPrincipalHandler, ITransientDependency
{
    public ILogger<DefaultClientCredentialsScopeHandler> Logger { get; set; }
        = NullLogger<DefaultClientCredentialsScopeHandler>.Instance;

    public async Task HandleAsync(AbpOpenIddictClaimsPrincipalHandlerContext context)
    {
        var request = context.OpenIddictRequest;
        if (!string.Equals(request.GrantType, OpenIddictConstants.GrantTypes.ClientCredentials, StringComparison.Ordinal))
        {
            return;
        }

        if (!context.Principal.GetScopes().IsDefaultOrEmpty)
        {
            return; // client requested scopes explicitly, leave it alone
        }

        var clientId = request.ClientId;
        if (string.IsNullOrEmpty(clientId))
        {
            return;
        }

        var applicationManager = context.ScopeServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();
        var scopeManager = context.ScopeServiceProvider.GetRequiredService<IOpenIddictScopeManager>();

        var application = await applicationManager.FindByClientIdAsync(clientId);
        if (application == null)
        {
            return;
        }

        var permissions = await applicationManager.GetPermissionsAsync(application);
        var prefix = OpenIddictConstants.Permissions.Prefixes.Scope;

        var scopes = permissions
            .Where(p => p.StartsWith(prefix, StringComparison.Ordinal))
            .Select(p => p[prefix.Length..])
            .ToImmutableArray();

        if (scopes.IsDefaultOrEmpty)
        {
            return;
        }

        Logger.LogInformation("Injecting configured scopes for {ClientId}: {Scopes}", clientId, string.Join(", ", scopes));

        context.Principal.SetScopes(scopes);

        var resources = new List<string>();
        await foreach (var resource in scopeManager.ListResourcesAsync(scopes))
        {
            resources.Add(resource);
        }
        context.Principal.SetResources(resources);
    }
}

Register it in your module's ConfigureServices:

Configure<AbpOpenIddictClaimsPrincipalOptions>(options =>
{
    options.ClaimsPrincipalHandlers.Add<DefaultClientCredentialsScopeHandler>();
});

One thing to watch out for: register it with Configure, not PreConfigure. PreConfigure runs before the framework's own Configure<AbpOpenIddictClaimsPrincipalOptions> block in AbpOpenIddictAspNetCoreModule, and you'll find the handler gets dropped from the final list. I verified the whole flow locally on rel-10.3 against your entervo_infinite-style client (client_credentials + oi_scp:ConnectorNL permission):

  • Request without scope → token comes back with "scope": "ConnectorNL" and "aud": "ConnectorNLResource" (handler kicks in).
  • Request with scope=ConnectorNL → identical token (handler is a no-op).

We'll also look into making this a first-class option on the OpenIddict module in a future release, so you don't have to maintain the handler yourself.

Thanks

: )

Hi,

Yes, that's expected — both services do register the AccountSettings controller because both IdentityService.HttpApi and AuthServer declare a dependency on AbpAccountAdminHttpApiModule. This is leftover from the older Ocelot template, where the gateway used to route /api/account-admin/... to IdentityService (port 44388). Starting from ABP 8.0 the gateway route was moved to AuthServer, but the dependency on the IdentityService side was kept in the template, so both services still surface the same endpoints in their swagger.

Functionally it doesn't cause any issue:

  • The gateway only routes /api/account-admin/{**catch-all} to the AuthServer cluster, so the controller copy living inside IdentityService is never reached through the gateway.
  • You can simply ignore the duplicate appearing in IdentityService swagger — no code change needed on your side.

If you really want to clean it up so IdentityService no longer exposes those endpoints, you can remove the Account Admin references from the IdentityService projects (IdentityService.HttpApi.csproj / IdentityService.Application.csproj and the matching [DependsOn(...)] entries in their module files). But this is purely cosmetic — keeping them as-is matches the current ABP template and is perfectly fine.

Thanks

Showing 551 to 560 of 12915 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 November 02, 2026, 06:56
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.