Activities of "maliming"

Hi,

With that setup only the offline check runs, so the license code is all the test host needs.

I'll leave this question open. Post the result here when you have it.

Thanks

Hi,

OpenIddict throws this error when all registered encryption certificates are outside their validity period.

Your snippet matches the older microservice template. The AuthServer Dockerfile in that template creates the pfx file during the image build:

RUN dotnet dev-certs https -v -ep /app/authserver.pfx -p <your-passphrase>

A new certificate created by this command is valid for 365 days. Please check if your Dockerfile still has this line, and check the NotBefore and NotAfter dates of the pfx file that the AuthServer loads.

I suggest you create your own certificate with the lifetime you need and mount it into the container, instead of creating it during the image build. Use an RSA certificate with a key of at least 2048 bits, and the pfx file must contain the private key. If the certificate has a Key Usage extension, it needs both Digital Signature and Key Encipherment, because you use the same file for signing and encryption. Your code loads the file at startup, so restart the AuthServer after you replace it.

For the monitoring and the alerts, you can add a health check. It reads the certificate that OpenIddict uses and returns Degraded when it expires within 30 days.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Server;

namespace MyCompanyName.MyProjectName;

public class OpenIddictCertificateHealthCheck : IHealthCheck
{
    private static readonly TimeSpan WarningThreshold = TimeSpan.FromDays(30);

    private readonly IOptionsMonitor<OpenIddictServerOptions> _options;

    public OpenIddictCertificateHealthCheck(IOptionsMonitor<OpenIddictServerOptions> options)
    {
        _options = options;
    }

    public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        var options = _options.CurrentValue;

        var signing = GetExpiration(options.SigningCredentials[0].Key);
        var encryption = GetExpiration(options.EncryptionCredentials[0].Key);
        var expiration = signing < encryption ? signing : encryption;
        var remaining = expiration - DateTime.UtcNow;

        var data = new Dictionary<string, object>
        {
            { "expiration", expiration.ToString("u") },
            { "remainingDays", (int)remaining.TotalDays }
        };

        if (remaining <= TimeSpan.Zero)
        {
            return Task.FromResult(HealthCheckResult.Unhealthy($"The OpenIddict certificate expired on {expiration:u}.", data: data));
        }

        if (remaining <= WarningThreshold)
        {
            return Task.FromResult(HealthCheckResult.Degraded($"The OpenIddict certificate expires in {(int)remaining.TotalDays} days.", data: data));
        }

        return Task.FromResult(HealthCheckResult.Healthy($"The OpenIddict certificate expires on {expiration:u}.", data));
    }

    private static DateTime GetExpiration(SecurityKey key)
    {
        return key is X509SecurityKey x509SecurityKey
            ? x509SecurityKey.Certificate.NotAfter.ToUniversalTime()
            : DateTime.MaxValue;
    }
}

Register it in ConfigureServices:

context.Services.AddHealthChecks()
    .AddCheck<OpenIddictCertificateHealthCheck>("openiddict-certificate");

Then add the endpoint to your existing UseConfiguredEndpoints call in OnApplicationInitialization:

app.UseConfiguredEndpoints(endpoints =>
{
    endpoints.MapHealthChecks("/health/certificate");
});

Your monitoring system can call this endpoint and send the alert. For Degraded the endpoint returns HTTP 200 with Degraded in the response body, so the alert has to check the body. It also has to fire on any non-200 response or when the endpoint is unreachable. The endpoint returns 503 or 500 when the certificate is no longer valid.

For the pipeline, this command returns exit code 1 when the certificate expires within 30 days:

openssl pkcs12 -in authserver.pfx -passin pass:<your-passphrase> -nokeys | openssl x509 -noout -dates -checkend 2592000

It prints both dates, but the exit code checks the expiry date only. Run it against the pfx file that the AuthServer will load.

Before deployment, start the new image with the certificate and call the health check endpoint. It has to return Healthy. Also test a login against it, because the health check does not issue a token.

When you replace the certificate, register the new one together with the old one and restart the AuthServer. The new certificate must already be valid at that time. OpenIddict then uses the one with the later expiry date. Keep the old one registered until the tokens issued with it have expired.

PreConfigure<OpenIddictServerBuilder>(builder =>
{
    builder.AddSigningCertificate(oldCertificate);
    builder.AddEncryptionCertificate(oldCertificate);
    builder.AddSigningCertificate(newCertificate);
    builder.AddEncryptionCertificate(newCertificate);
});

Thanks

Hi,

An ABP Blazor Server app is a regular ASP.NET Core app, so you host it the same way. Use IIS on Windows. On Linux, run it with Kestrel behind Nginx or Apache. Tomcat is a Java server and doesn't run ASP.NET Core apps.

Here is our IIS guide for Blazor Server. You can switch the ABP version, database and tiered options at the top of the page:

https://abp.io/docs/latest/solution-templates/layered-web-application/deployment/deployment-iis?UI=BlazorServer

The publish, DbMigrator and appsettings steps in that guide are the same on Linux. For the Nginx or Apache part, follow the Microsoft guide. It covers both:

https://learn.microsoft.com/en-us/aspnet/core/blazor/host-and-deploy/server/

Three things to check when you deploy:

  1. Enable WebSockets. On IIS, install the WebSocket Protocol feature. On Nginx or Apache, the proxy has to pass the WebSocket upgrade to the app.
  2. The app needs its OpenIddict certificate file in production. The IIS guide creates authserver.pfx, but the current templates load openiddict.pfx with the password from AuthServer:CertificatePassPhrase. Check the file name your project uses, then make sure that file is in the published folder and the password matches. In a tiered solution that is the AuthServer folder. More details are here:

https://abp.io/docs/latest/deployment/configuring-openiddict

  1. If Nginx or Apache handles HTTPS and sends plain HTTP to the app, configure forwarded headers in the app and trust your proxy's address:

https://abp.io/docs/10.7/deployment/forwarded-headers

Thanks

Hi,

ABP-LIC-0020 only means AbpLicenseCode is missing from the configuration. The license check reads it from IConfiguration, so an in-memory provider is enough:

var builder = WebApplication.CreateEmptyBuilder(new WebApplicationOptions
{
    EnvironmentName = Environments.Development
});

builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
    ["AbpLicenseCode"] = "<your-license-code>"
});

With the license code in place, the test host starts without abp login and without internet access. CreateEmptyBuilder is fine. It just doesn't load appsettings.secrets.json, so the code was never there.

The online check runs only in two cases:

  • the test process has the ASPNETCORE_ENVIRONMENT=Development environment variable
  • a debugger is attached

EnvironmentName on the builder doesn't count. In these two cases the Windows user that runs the tests must be logged in and must be able to reach https://commercial.abp.io. Logging in from ABP Studio or with abp login is the same thing.

On CI, don't set ASPNETCORE_ENVIRONMENT=Development for the test run. Then the license code is all you need, and there is no token file to copy.

One note about logs: if the license code is malformed, the error message contains the code itself. Check the output before you share it.

Please create a new question for the ABP Studio AI topics (context limit, plans, credits). The Studio team will check it.

Thanks

Hi,

Starting with 10.4.0, Volo.Abp.Account.Pro.Public.Application uses Volo.Abp.Imaging.SkiaSharp instead of Volo.Abp.Imaging.ImageSharp. So upgrading to 10.4.0 or later (10.7.0 is the latest stable) removes the ImageSharp dependency that comes from Account Pro. After the upgrade, run dotnet list package --include-transitive for your solution (including the AuthServer and HttpApi.Host projects) to make sure nothing else still pulls in SixLabors.ImageSharp.

If you need to stay on 10.3.0 for now, you can add a direct reference to the patched version:

<PackageReference Include="SixLabors.ImageSharp" Version="4.1.2" />

Add it to every project where dotnet list package --include-transitive shows SixLabors.ImageSharp, then run the command again to make sure it resolves to 4.1.2.

Note that ImageSharp 4.1.2 checks for a Six Labors license at build time. Without one, Debug builds show a warning and Release builds fail. You can set the license with the SixLaborsLicenseKey / SixLaborsLicenseFile MSBuild properties or by adding a sixlabors.lic file. See https://sixlabors.com/pricing/

Also, in 10.3.0 Account Pro only calls ImageSharp when AbpProfilePictureOptions.EnableImageCompression is enabled, and it's off by default. If you enabled it, test profile picture upload after the change and check the server logs for compression errors.

Thanks

Hi,

This is a bug in the CMS Kit Pro Blazor admin UI. The edit page doesn't keep the page status (and the layout name) when it loads the page, so every save sends Draft back. The MVC UI has Save as draft and Publish buttons, but they were never added to the Blazor UI.

We'll fix this in the next patch release. For now, you can replace the page edit component.

  1. Add these two files to your Blazor.Client project, for example under Pages/Cms:

MyPageUpdate.razor

@using Blazorise.Markdown
@using Volo.Abp.AspNetCore.Components.Web
@using Volo.Abp.AspNetCore.Components.Web.Theming.Layout
@using Volo.Abp.BlazoriseUI.Components.ObjectExtending
@using Volo.Abp.DependencyInjection
@using Volo.CmsKit.Admin.Pages
@using Volo.CmsKit.Localization
@using Volo.CmsKit.Pro.Admin.Blazor.Pages.CmsKit
@inherits PageUpdate
@attribute [ExposeServices(typeof(PageUpdate))]
@attribute [Dependency(ReplaceServices = true)]
@inject AbpBlazorMessageLocalizerHelper<CmsKitResource> LocalizerHelper

<PageHeader Title="@L["Pages"]" BreadcrumbItems="@BreadcrumbItems">
</PageHeader>

@if (EditingPage != null)
{
    <Card>
        <CardBody>
            <Form>
                <Validations @ref="@ValidationsRef" Model="@EditingPage" ValidateOnLoad="false">
                    <Validation MessageLocalizer="@LocalizerHelper.Localize">
                        <Field>
                            <FieldLabel>@L["Title"] *</FieldLabel>
                            <TextInput @bind-Value="EditingPage.Title" Autofocus="true">
                                <Feedback>
                                    <ValidationError/>
                                </Feedback>
                            </TextInput>
                        </Field>
                    </Validation>
                    <Validation MessageLocalizer="@LocalizerHelper.Localize">
                        <Field>
                            <FieldLabel>@L["Slug"] *</FieldLabel>
                            <Tooltip Text="@L["PageSlugInformation"]">
                                <TextInput @bind-Value="EditingPage.Slug" Autofocus="false">
                                    <Feedback>
                                        <ValidationError/>
                                    </Feedback>
                                </TextInput>
                            </Tooltip>
                        </Field>
                    </Validation>
                    <ExtensionProperties TEntityType="UpdatePageInputDto" TResourceType="CmsKitResource" Entity="@EditingPage" LH="@LocalizerHelper" ModalType="ExtensionPropertyModalType.EditModal" />
                </Validations>

                <Tabs @bind-SelectedTab="@SelectedTab" RenderMode="TabsRenderMode.LazyLoad">
                    <Items>
                        <Tab Name="content">@L["Content"]</Tab>
                        <Tab Name="script">@L["Script"]</Tab>
                        <Tab Name="style">@L["Style"]</Tab>
                    </Items>
                    <Content>
                        <TabPanel Name="content">
                            <DynamicWidgetMarkdown @ref="@MarkdownRef" @bind-Value="@EditingPage.Content"></DynamicWidgetMarkdown>
                        </TabPanel>
                        <TabPanel Name="script">
                            <Markdown @bind-Value="@EditingPage.Script" LineNumbers="true" HideIcons="@HideIcons" AutoDownloadFontAwesome="false" UploadImage="false">
                                <Toolbar>
                                    <MarkdownToolbarButton Action="MarkdownAction.Bold"/>
                                </Toolbar>
                            </Markdown>
                        </TabPanel>
                        <TabPanel Name="style">
                            <Markdown @bind-Value="@EditingPage.Style" LineNumbers="true" HideIcons="@HideIcons" AutoDownloadFontAwesome="false" UploadImage="false">
                                <Toolbar>
                                    <MarkdownToolbarButton Action="MarkdownAction.Bold"/>
                                </Toolbar>
                            </Markdown>
                        </TabPanel>
                    </Content>
                </Tabs>
            </Form>
        </CardBody>
        <CardFooter>
            <Button Color="Color.Primary" Outline Class="me-1" Clicked="@SaveAsDraftAsync">@L["SaveAsDraft"]</Button>
            <Button Color="Color.Primary" Clicked="@PublishAsync">@L["Publish"]</Button>
        </CardFooter>
    </Card>
}

MyPageUpdate.razor.cs

using System.Threading.Tasks;
using Volo.Abp.ObjectExtending;
using Volo.CmsKit.Admin.Pages;
using Volo.CmsKit.Pages;

namespace YourProjectName.Blazor.Client.Pages.Cms;

public partial class MyPageUpdate
{
    protected override async Task GetEditingPageAsync()
    {
        var page = await PageAdminAppService.GetAsync(Id);

        EditingPage = new UpdatePageInputDto
        {
            ConcurrencyStamp = page.ConcurrencyStamp,
            Content = page.Content,
            Script = page.Script,
            Style = page.Style,
            Title = page.Title,
            Slug = page.Slug,
            LayoutName = page.LayoutName,
            Status = page.Status
        };
        page.MapExtraPropertiesTo(EditingPage);
    }

    protected virtual async Task SaveAsDraftAsync()
    {
        EditingPage.Status = PageStatus.Draft;
        await UpdatePageAsync();
    }

    protected virtual async Task PublishAsync()
    {
        EditingPage.Status = PageStatus.Publish;
        await UpdatePageAsync();
    }
}
  1. Register it in the ConfigureServices method of your server Blazor project module as well. The server project doesn't scan the client module, so without this line the server side still renders the old page:
context.Services.AddTransient<Volo.CmsKit.Pro.Admin.Blazor.Pages.CmsKit.PageUpdate>(
    _ => new YourProjectName.Blazor.Client.Pages.Cms.MyPageUpdate());

After this, the edit page shows Save as draft and Publish instead of Submit. Click Publish to save and keep the page published. Save as draft saves it and unpublishes it. New pages are still created as draft, so create the page first, then open it and click Publish.

Thanks

Hi,

10.7.0 is the stable release. It was published on October 6 for both the open source and commercial packages, on NuGet and npm. You can upgrade your solution now.

The docs version selector and the pro-releases page haven't been updated to 10.7 yet. We'll fix both soon.

You can follow the migration guide for the upgrade:

https://abp.io/docs/10.7/release-info/migration-guides/abp-10-7

Thanks

Hi,

Here is the second file, AIManagementBreadcrumbs.cs. Put it next to ModuleBreadcrumbs.cs in your *.Blazor.Client project:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Localization;
using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Layout;
using Volo.Abp.UI.Navigation.Localization.Resource;
using Volo.AIManagement.Blazor.MudBlazor.Pages.AIManagement.Chat;
using Volo.AIManagement.Blazor.MudBlazor.Pages.AIManagement.McpServers;
using Volo.AIManagement.Blazor.MudBlazor.Pages.AIManagement.WorkspaceDataSources;
using Volo.AIManagement.Blazor.MudBlazor.Pages.AIManagement.Workspaces;

namespace MyProjectName.Blazor.Client.Breadcrumbs;

public class MyWorkspaces : Workspaces
{
    [Inject] protected PageLayout PageLayout { get; set; } = default!;
    [Inject] protected IStringLocalizer<AbpUiNavigationResource> LUiNavigation { get; set; } = default!;
    private readonly BreadcrumbState _breadcrumbs = new();

    protected override void OnInitialized()
    {
        base.OnInitialized();
        ApplyBreadcrumbs();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);
        ApplyBreadcrumbs();
    }

    private void ApplyBreadcrumbs()
    {
        if (IsDisposed) return;
        _breadcrumbs.Apply(PageLayout,
            ModuleBreadcrumbs.Item(LUiNavigation["Menu:Administration"]),
            ModuleBreadcrumbs.Item(L["Menu:AIManagement"]),
            ModuleBreadcrumbs.Item(L["Menu:Workspaces"]));
    }
}

public class MyMcpServers : McpServers
{
    [Inject] protected PageLayout PageLayout { get; set; } = default!;
    [Inject] protected IStringLocalizer<AbpUiNavigationResource> LUiNavigation { get; set; } = default!;
    private readonly BreadcrumbState _breadcrumbs = new();

    protected override void OnInitialized()
    {
        base.OnInitialized();
        ApplyBreadcrumbs();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);
        ApplyBreadcrumbs();
    }

    private void ApplyBreadcrumbs()
    {
        if (IsDisposed) return;
        _breadcrumbs.Apply(PageLayout,
            ModuleBreadcrumbs.Item(LUiNavigation["Menu:Administration"]),
            ModuleBreadcrumbs.Item(L["Menu:AIManagement"]),
            ModuleBreadcrumbs.Item(L["Menu:McpServers"]));
    }
}

// Blazor reuses these pages when only the workspace in the URL changes (for example, picking
// an older entry from the browser's back menu), but they only load the workspace once.
// Reloading keeps the page and its breadcrumb on the workspace in the URL.
public class MyChat : Chat
{
    [Inject] protected PageLayout PageLayout { get; set; } = default!;
    [Inject] protected IStringLocalizer<AbpUiNavigationResource> LUiNavigation { get; set; } = default!;
    [Inject] protected NavigationManager ReloadNavigationManager { get; set; } = default!;
    private readonly BreadcrumbState _breadcrumbs = new();
    private string? _initialWorkspaceName;
    private bool _reloading;

    protected override void OnInitialized()
    {
        base.OnInitialized();
        _initialWorkspaceName = WorkspaceName;
        ApplyBreadcrumbs();
    }

    protected override async Task OnParametersSetAsync()
    {
        if (!_reloading && _initialWorkspaceName != null && WorkspaceName != _initialWorkspaceName)
        {
            _reloading = true;
            ReloadNavigationManager.NavigateTo(ReloadNavigationManager.Uri, forceLoad: true);
            return;
        }

        await base.OnParametersSetAsync();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);
        ApplyBreadcrumbs();
    }

    private void ApplyBreadcrumbs()
    {
        if (IsDisposed || _reloading) return;
        _breadcrumbs.Apply(PageLayout,
            ModuleBreadcrumbs.Item(LUiNavigation["Menu:Administration"]),
            ModuleBreadcrumbs.Item(L["Menu:AIManagement"]),
            ModuleBreadcrumbs.Item(L["Menu:Workspaces"], ModuleBreadcrumbs.Url(Culture, "ai-management/workspaces")),
            ModuleBreadcrumbs.Item(WorkspaceName),
            ModuleBreadcrumbs.Item(L["ChatPlayground"]));
    }
}

public class MyWorkspaceDataSources : WorkspaceDataSources
{
    [Inject] protected PageLayout PageLayout { get; set; } = default!;
    [Inject] protected IStringLocalizer<AbpUiNavigationResource> LUiNavigation { get; set; } = default!;
    [Inject] protected NavigationManager ReloadNavigationManager { get; set; } = default!;
    private readonly BreadcrumbState _breadcrumbs = new();
    private Guid? _initialWorkspaceId;
    private bool _reloading;

    protected override void OnInitialized()
    {
        base.OnInitialized();
        _initialWorkspaceId = WorkspaceId;
    }

    protected override async Task OnParametersSetAsync()
    {
        if (!_reloading && _initialWorkspaceId != null && WorkspaceId != _initialWorkspaceId)
        {
            _reloading = true;
            ReloadNavigationManager.NavigateTo(ReloadNavigationManager.Uri, forceLoad: true);
            return;
        }

        await base.OnParametersSetAsync();
        ApplyBreadcrumbs();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);
        ApplyBreadcrumbs();
    }

    private void ApplyBreadcrumbs()
    {
        if (IsDisposed || _reloading || Workspace == null) return;
        _breadcrumbs.Apply(PageLayout,
            ModuleBreadcrumbs.Item(LUiNavigation["Menu:Administration"]),
            ModuleBreadcrumbs.Item(L["Menu:AIManagement"]),
            ModuleBreadcrumbs.Item(L["Menu:Workspaces"], ModuleBreadcrumbs.Url(Culture, "ai-management/workspaces")),
            ModuleBreadcrumbs.Item(Workspace.Name, ModuleBreadcrumbs.Url(Culture, $"ai-management/workspaces/{Uri.EscapeDataString(Workspace.Name)}")),
            ModuleBreadcrumbs.Item(L["DataSources"]));
    }

    protected override void Dispose(bool disposing)
    {
        // This page has no PageHeader, so nothing else clears its items when leaving it.
        _breadcrumbs.Release(PageLayout);
        base.Dispose(disposing);
    }
}

Thanks

Hi,

These MudBlazor pages don't set their breadcrumb items, so LeptonX only shows the page title. The Data Sources page has no page header at all, so it shows nothing. This is a bug and we will fix it in the next release. Your ticket has been refunded.

You can use this workaround for now. It replaces those pages with small subclasses that set the full menu path.

  1. Add two files to your *.Blazor.Client project: ModuleBreadcrumbs.cs below, and AIManagementBreadcrumbs.cs in my next answer (both together are too long for one post). Change the namespace to yours.
  2. Call AddModuleBreadcrumbs() in both your *.Blazor.Client module and your *.Blazor (server) module. The server module doesn't depend on the client module, so without the second call the pages that render on the server still show only the title. After this, the server app logs a warning that the *.Blazor.Client assembly is not in the [DependsOn] chain. You can ignore it, these pages only use [Inject] properties.
using MyProjectName.Blazor.Client.Breadcrumbs;

public override void ConfigureServices(ServiceConfigurationContext context)
{
    context.Services.AddModuleBreadcrumbs();
    // ...
}

A few notes:

  • It also covers the Organization Units page and the two text template content pages. They have the same problem.
  • Chat Playground and Data Sources show Administration > AI Management > Workspaces > WorkspaceName > Chat Playground (or Data Sources).
  • If you go back from one workspace's page to another one through the browser history, Chat Playground and Data Sources reload once, so the page and the breadcrumb both use the workspace in the URL.

ModuleBreadcrumbs.cs:

using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Localization;
using MudBlazor;
using Volo.Abp.AspNetCore.Components.Web.Theming.MudBlazor.Layout;
using Volo.Abp.UI.Navigation.Localization.Resource;
using Volo.Abp.AuditLogging.Blazor.MudBlazor.Pages.AuditLogging;
using Volo.Abp.Identity;
using Volo.Abp.Identity.Pro.Blazor.MudBlazor.Pages.Identity;
using Volo.Abp.LanguageManagement.Blazor.MudBlazor.Pages.LanguageManagement;
using Volo.Abp.TextTemplateManagement.Blazor.MudBlazor.Pages.TextTemplateManagement;
using Volo.AIManagement.Blazor.MudBlazor.Pages.AIManagement.Chat;
using Volo.AIManagement.Blazor.MudBlazor.Pages.AIManagement.McpServers;
using Volo.AIManagement.Blazor.MudBlazor.Pages.AIManagement.WorkspaceDataSources;
using Volo.AIManagement.Blazor.MudBlazor.Pages.AIManagement.Workspaces;

namespace MyProjectName.Blazor.Client.Breadcrumbs;

public static class ModuleBreadcrumbs
{
    public static IServiceCollection AddModuleBreadcrumbs(this IServiceCollection services)
    {
        services.Replace(ServiceDescriptor.Transient<LanguageManagement, MyLanguageManagement>());
        services.Replace(ServiceDescriptor.Transient<LanguageTextManagement, MyLanguageTextManagement>());
        services.Replace(ServiceDescriptor.Transient<AuditLogsManagement, MyAuditLogsManagement>());
        services.Replace(ServiceDescriptor.Transient<TextTemplateManagement, MyTextTemplateManagement>());
        services.Replace(ServiceDescriptor.Transient<TextTemplateContentManagement, MyTextTemplateContentManagement>());
        services.Replace(ServiceDescriptor.Transient<TextTemplateInlineContentManagement, MyTextTemplateInlineContentManagement>());
        services.Replace(ServiceDescriptor.Transient<OrganizationUnitsManagement, MyOrganizationUnitsManagement>());
        services.Replace(ServiceDescriptor.Transient<Workspaces, MyWorkspaces>());
        services.Replace(ServiceDescriptor.Transient<McpServers, MyMcpServers>());
        services.Replace(ServiceDescriptor.Transient<Chat, MyChat>());
        services.Replace(ServiceDescriptor.Transient<WorkspaceDataSources, MyWorkspaceDataSources>());
        return services;
    }

    public static BreadcrumbItem Item(string text, string? href = null)
    {
        return new BreadcrumbItem(text, href, disabled: href == null);
    }

    public static string Url(string? culture, string path)
    {
        return string.IsNullOrEmpty(culture) ? path : $"{Uri.EscapeDataString(culture)}/{path}";
    }
}

// Remembers the items a page put into the shared PageLayout, so it can re-apply them
// when another page's cleanup wiped them, and only clear them if they are still its own.
public class BreadcrumbState
{
    private BreadcrumbItem[] _items = Array.Empty<BreadcrumbItem>();
    private string? _key;

    public void Apply(PageLayout pageLayout, params BreadcrumbItem[] items)
    {
        var key = string.Join("|", items.Select(x => x.Text + ">" + x.Href));
        if (key == _key && IsOwner(pageLayout))
        {
            return;
        }

        _key = key;
        _items = items;
        pageLayout.BreadcrumbItems.Clear();
        foreach (var item in items)
        {
            pageLayout.BreadcrumbItems.Add(item);
        }
    }

    public void Release(PageLayout pageLayout)
    {
        if (IsOwner(pageLayout))
        {
            pageLayout.BreadcrumbItems.Clear();
        }
    }

    private bool IsOwner(PageLayout pageLayout)
    {
        return _items.Length > 0 &&
               pageLayout.BreadcrumbItems.Count == _items.Length &&
               pageLayout.BreadcrumbItems.Zip(_items).All(x => ReferenceEquals(x.First, x.Second));
    }
}

// The previous page's PageHeader resets PageLayout after this page initializes during in-app
// navigation, so the items are applied again after each render (a no-op when unchanged).
public class MyLanguageManagement : LanguageManagement
{
    [Inject] protected PageLayout PageLayout { get; set; } = default!;
    [Inject] protected IStringLocalizer<AbpUiNavigationResource> LUiNavigation { get; set; } = default!;
    private readonly BreadcrumbState _breadcrumbs = new();

    protected override void OnInitialized()
    {
        base.OnInitialized();
        ApplyBreadcrumbs();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);
        ApplyBreadcrumbs();
    }

    private void ApplyBreadcrumbs()
    {
        if (IsDisposed) return;
        _breadcrumbs.Apply(PageLayout,
            ModuleBreadcrumbs.Item(LUiNavigation["Menu:Administration"]),
            ModuleBreadcrumbs.Item(L["Menu:Languages"]),
            ModuleBreadcrumbs.Item(L["Languages"]));
    }
}

public class MyLanguageTextManagement : LanguageTextManagement
{
    [Inject] protected PageLayout PageLayout { get; set; } = default!;
    [Inject] protected IStringLocalizer<AbpUiNavigationResource> LUiNavigation { get; set; } = default!;
    private readonly BreadcrumbState _breadcrumbs = new();

    protected override void OnInitialized()
    {
        base.OnInitialized();
        ApplyBreadcrumbs();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);
        ApplyBreadcrumbs();
    }

    private void ApplyBreadcrumbs()
    {
        if (IsDisposed) return;
        _breadcrumbs.Apply(PageLayout,
            ModuleBreadcrumbs.Item(LUiNavigation["Menu:Administration"]),
            ModuleBreadcrumbs.Item(L["Menu:Languages"]),
            ModuleBreadcrumbs.Item(L["LanguageTexts"]));
    }
}

public class MyAuditLogsManagement : AuditLogsManagement
{
    [Inject] protected PageLayout PageLayout { get; set; } = default!;
    [Inject] protected IStringLocalizer<AbpUiNavigationResource> LUiNavigation { get; set; } = default!;
    private readonly BreadcrumbState _breadcrumbs = new();

    protected override void OnInitialized()
    {
        base.OnInitialized();
        ApplyBreadcrumbs();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);
        ApplyBreadcrumbs();
    }

    private void ApplyBreadcrumbs()
    {
        if (IsDisposed) return;
        _breadcrumbs.Apply(PageLayout,
            ModuleBreadcrumbs.Item(LUiNavigation["Menu:Administration"]),
            ModuleBreadcrumbs.Item(L["Menu:AuditLogging"]));
    }
}

public class MyTextTemplateManagement : TextTemplateManagement
{
    [Inject] protected PageLayout PageLayout { get; set; } = default!;
    [Inject] protected IStringLocalizer<AbpUiNavigationResource> LUiNavigation { get; set; } = default!;
    private readonly BreadcrumbState _breadcrumbs = new();

    protected override void OnInitialized()
    {
        base.OnInitialized();
        ApplyBreadcrumbs();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);
        ApplyBreadcrumbs();
    }

    private void ApplyBreadcrumbs()
    {
        if (IsDisposed) return;
        _breadcrumbs.Apply(PageLayout,
            ModuleBreadcrumbs.Item(LUiNavigation["Menu:Administration"]),
            ModuleBreadcrumbs.Item(L["Menu:TextTemplates"]));
    }
}

public class MyTextTemplateContentManagement : TextTemplateContentManagement
{
    [Inject] protected PageLayout PageLayout { get; set; } = default!;
    [Inject] protected IStringLocalizer<AbpUiNavigationResource> LUiNavigation { get; set; } = default!;
    private readonly BreadcrumbState _breadcrumbs = new();

    protected override void OnInitialized()
    {
        base.OnInitialized();
        ApplyBreadcrumbs();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);
        ApplyBreadcrumbs();
    }

    private void ApplyBreadcrumbs()
    {
        if (IsDisposed) return;
        if (TemplateDefinitionName == null) return;
        _breadcrumbs.Apply(PageLayout,
            ModuleBreadcrumbs.Item(LUiNavigation["Menu:Administration"]),
            ModuleBreadcrumbs.Item(L["Menu:TextTemplates"], ModuleBreadcrumbs.Url(Culture, "text-templates")),
            ModuleBreadcrumbs.Item(TemplateDefinitionName));
    }
}

public class MyTextTemplateInlineContentManagement : TextTemplateInlineContentManagement
{
    [Inject] protected PageLayout PageLayout { get; set; } = default!;
    [Inject] protected IStringLocalizer<AbpUiNavigationResource> LUiNavigation { get; set; } = default!;
    private readonly BreadcrumbState _breadcrumbs = new();

    protected override void OnInitialized()
    {
        base.OnInitialized();
        ApplyBreadcrumbs();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);
        ApplyBreadcrumbs();
    }

    private void ApplyBreadcrumbs()
    {
        if (IsDisposed) return;
        if (TemplateDefinitionName == null) return;
        _breadcrumbs.Apply(PageLayout,
            ModuleBreadcrumbs.Item(LUiNavigation["Menu:Administration"]),
            ModuleBreadcrumbs.Item(L["Menu:TextTemplates"], ModuleBreadcrumbs.Url(Culture, "text-templates")),
            ModuleBreadcrumbs.Item(TemplateDefinitionName));
    }
}

public class MyOrganizationUnitsManagement : OrganizationUnitsManagement
{
    // The base page already injects PageLayout / LUiNavigation as private members.
    [Inject] protected PageLayout BreadcrumbPageLayout { get; set; } = default!;
    [Inject] protected IStringLocalizer<AbpUiNavigationResource> BreadcrumbUiNavigation { get; set; } = default!;
    private readonly BreadcrumbState _breadcrumbs = new();

    public MyOrganizationUnitsManagement(IOrganizationUnitAppService organizationUnitAppService)
        : base(organizationUnitAppService)
    {
    }

    protected override void OnInitialized()
    {
        base.OnInitialized();
        ApplyBreadcrumbs();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await base.OnAfterRenderAsync(firstRender);
        ApplyBreadcrumbs();
    }

    private void ApplyBreadcrumbs()
    {
        if (IsDisposed) return;
        _breadcrumbs.Apply(BreadcrumbPageLayout,
            ModuleBreadcrumbs.Item(BreadcrumbUiNavigation["Menu:Administration"]),
            ModuleBreadcrumbs.Item(L["Menu:IdentityManagement"]),
            ModuleBreadcrumbs.Item(L["OrganizationUnits"]));
    }
}

Thanks

Hi,

I'll close this question now. If you run into anything else with the Low-Code page or the order editor, feel free to open a new question.

Thanks

Showing 61 to 70 of 12911 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 October 30, 2026, 06:34
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.