From 5f3e6587c6901f3227a3daf3c2bcb5656fee58ec Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 18 May 2026 16:21:21 +0800 Subject: [PATCH 1/4] docs(suite): document MudBlazor template support Describe how ABP Suite detects the Blazor UI library from the .csproj package references and routes CRUD page generation through the new Frontend.Blazor.MudBlazor.* template tree: - index.md: new Blazor UI library section covering detection, template routing and Template Management UI filtering. - generating-crud-page.md: note explaining that Blazor solutions emit MudBlazor or Blazorise UI automatically based on the resolved library. - customizing-the-generated-code.md: warn that Blazorise and MudBlazor customizations live under separate resource names and are not shared. - editing-templates.md: add Frontend.Blazor.MudBlazor prefix to the template-name conventions list. --- docs/en/suite/customizing-the-generated-code.md | 2 ++ docs/en/suite/editing-templates.md | 5 ++++- docs/en/suite/generating-crud-page.md | 2 ++ docs/en/suite/index.md | 14 ++++++++++++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/en/suite/customizing-the-generated-code.md b/docs/en/suite/customizing-the-generated-code.md index 350405ddb0..18f63d642c 100644 --- a/docs/en/suite/customizing-the-generated-code.md +++ b/docs/en/suite/customizing-the-generated-code.md @@ -84,6 +84,8 @@ As can be seen in the figure above, you can search the related template file, cl ### Customizations on Blazor UI +> **Blazorise vs MudBlazor templates:** Suite ships two parallel template trees for Blazor — `Frontend.Blazor.*` (Blazorise) and `Frontend.Blazor.MudBlazor.*` (MudBlazor) — and picks the right one based on the [Blazor UI library](index.md#blazor-ui-library) detected in your solution. Customized templates are stored under `.suite/customized-templates/` keyed by the full resource name, so a customization made in a MudBlazor solution is never read by a Blazorise solution (or vice versa). If you migrate an existing solution from Blazorise to MudBlazor, you need to re-apply your customizations to the new resource names. + If you have created your application with Blazor UI, you can see the extended classes for _the code-behind classes of your pages_, under the `Pages` folder: ![](../images/custom-code-blazor-folder-structure.png) diff --git a/docs/en/suite/editing-templates.md b/docs/en/suite/editing-templates.md index dd91fee86c..3a6160a215 100644 --- a/docs/en/suite/editing-templates.md +++ b/docs/en/suite/editing-templates.md @@ -31,7 +31,10 @@ There's a naming convention for the template files. * If the template name has `Server` prefix, it's used for backend code like repositories, application services, localizations, controllers, permissions, mappings, unit tests. * If the template name has `Frontend.Angular` prefix, it's used for Angular code generation. The Angular code is being generated via [Angular Schematics](https://angular.dev/tools/cli/schematics). * If the template name has `Frontend.Mvc` prefix, it's used for razor pages, menus, JavaScript, CSS files. -* If the template name has `Frontend.Blazor` prefix, it's used for razor components. +* If the template name has `Frontend.Blazor` prefix (no `.MudBlazor` segment), it's used for Blazorise-based razor components. +* If the template name has `Frontend.Blazor.MudBlazor` prefix, it's used for [MudBlazor](https://mudblazor.com)-based razor components. + +> The template list only shows the variant that matches your solution's [Blazor UI library](index.md#blazor-ui-library) (Blazorise or MudBlazor), so you never see both trees side-by-side and the "outdated" check only flags the active set. ## How do I edit the templates? diff --git a/docs/en/suite/generating-crud-page.md b/docs/en/suite/generating-crud-page.md index 3325e8c03c..fcdbac1e81 100644 --- a/docs/en/suite/generating-crud-page.md +++ b/docs/en/suite/generating-crud-page.md @@ -112,6 +112,8 @@ To create a new entity, make sure the *-New entity-* is selected in the **Entity > **Note:** This option does not generate React UI pages. ABP Suite's UI generation targets Suite-supported UI stacks such as MVC, Blazor and Angular. For React-based solutions, build the UI in the source-owned [React UI](../framework/ui/react/index.md) application. ABP license holders can use [ABP Studio AI Agent](https://abp.io/studio/ai-agent) with predefined AI credits to generate React pages more easily. + > **Blazor UI library:** For Blazor solutions, the generated UI matches the underlying Blazor component library. Solutions that reference `Volo.Abp.MudBlazorUI` produce pages built around `AbpMudExtensibleDataGrid`, `MudDialog`, `MudForm` and the rest of the [MudBlazor](https://mudblazor.com) controls; Blazorise-based solutions keep emitting the original `DataGrid` / `Modal` / `Validations` markup. ABP Suite picks the variant automatically by scanning the Blazor project `.csproj` and shows the resolved value in the solution info tooltip — no extra switch is required. See the [Blazor UI library](index.md#blazor-ui-library) section for details. + * **Excel export**: Creates a button that exports a list of all the data that were added to the entity to an Excel file. * **Bulk delete**: Performs bulk deletion of records based on specified criteria. diff --git a/docs/en/suite/index.md b/docs/en/suite/index.md index d24b36af5a..72bb39c9a1 100644 --- a/docs/en/suite/index.md +++ b/docs/en/suite/index.md @@ -26,3 +26,17 @@ ABP Suite is a complementary tool to the ABP Platform. ABP Suite allows you to b It's a .NET Core Global tool that can be installed from the command line. If you are using [ABP Studio](../studio/index.md), you don't even need to install it because it should already be installed, when you first installed the [ABP Studio](../studio/index.md). By using the ABP Suite, you can generate CRUD pages from the database to the front-end and directly get a kickstart for your application. ABP Suite is actively developed and new features are being added version by version according to the roadmap and your feedback. + +## Blazor UI library + +When the target solution uses any Blazor UI framework (Blazor, BlazorServer, BlazorWebApp or MauiBlazor), ABP Suite detects the underlying Blazor component library by scanning the Blazor project `.csproj` for known package references: + +* A reference to `Volo.Abp.MudBlazorUI`, `Volo.Abp.AspNetCore.Components.*.Theming.MudBlazor` or `*.Blazor.MudBlazor*` selects the **MudBlazor** templates. +* Otherwise ABP Suite falls back to the **Blazorise** templates that have always shipped with Suite. + +The detected value is shown in the *solution info* tooltip of the CRUD Page Generator screen and is reused for two things: + +1. Template routing — `Frontend.Blazor.*` resource names are resolved to `Frontend.Blazor.MudBlazor.*` for MudBlazor solutions, so the generated `.razor` / `.razor.cs` files use `MudCard`, `AbpMudExtensibleDataGrid`, `MudDialog`, `MudForm` and the corresponding Mud input controls instead of the Blazorise `Card`, `DataGrid`, `Modal`, `Validations` markup. +2. Template Management UI — the "Manage Templates" screen and the "outdated templates" check only list the variant that matches the current solution, so the Blazorise and MudBlazor template trees never appear side-by-side. + +Customized templates are stored under `.suite/customized-templates/` keyed by the full resource name, so MudBlazor customizations (`Frontend.Blazor.MudBlazor.*`) are physically isolated from Blazorise customizations (`Frontend.Blazor.*`) and never clash. From 87c102fd0638f09e7dc2bfc8dfbc19990651eb7a Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 18 May 2026 16:33:51 +0800 Subject: [PATCH 2/4] docs(suite): match MudDataGrid switch in suite mud templates --- docs/en/suite/generating-crud-page.md | 2 +- docs/en/suite/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/suite/generating-crud-page.md b/docs/en/suite/generating-crud-page.md index fcdbac1e81..07f1e6ca91 100644 --- a/docs/en/suite/generating-crud-page.md +++ b/docs/en/suite/generating-crud-page.md @@ -112,7 +112,7 @@ To create a new entity, make sure the *-New entity-* is selected in the **Entity > **Note:** This option does not generate React UI pages. ABP Suite's UI generation targets Suite-supported UI stacks such as MVC, Blazor and Angular. For React-based solutions, build the UI in the source-owned [React UI](../framework/ui/react/index.md) application. ABP license holders can use [ABP Studio AI Agent](https://abp.io/studio/ai-agent) with predefined AI credits to generate React pages more easily. - > **Blazor UI library:** For Blazor solutions, the generated UI matches the underlying Blazor component library. Solutions that reference `Volo.Abp.MudBlazorUI` produce pages built around `AbpMudExtensibleDataGrid`, `MudDialog`, `MudForm` and the rest of the [MudBlazor](https://mudblazor.com) controls; Blazorise-based solutions keep emitting the original `DataGrid` / `Modal` / `Validations` markup. ABP Suite picks the variant automatically by scanning the Blazor project `.csproj` and shows the resolved value in the solution info tooltip — no extra switch is required. See the [Blazor UI library](index.md#blazor-ui-library) section for details. + > **Blazor UI library:** For Blazor solutions, the generated UI matches the underlying Blazor component library. Solutions that reference `Volo.Abp.MudBlazorUI` produce pages built around `MudDataGrid`, `MudDialog`, `MudForm` and the rest of the [MudBlazor](https://mudblazor.com) controls; Blazorise-based solutions keep emitting the original `DataGrid` / `Modal` / `Validations` markup. ABP Suite picks the variant automatically by scanning the Blazor project `.csproj` and shows the resolved value in the solution info tooltip — no extra switch is required. See the [Blazor UI library](index.md#blazor-ui-library) section for details. * **Excel export**: Creates a button that exports a list of all the data that were added to the entity to an Excel file. diff --git a/docs/en/suite/index.md b/docs/en/suite/index.md index 72bb39c9a1..ff1258779e 100644 --- a/docs/en/suite/index.md +++ b/docs/en/suite/index.md @@ -36,7 +36,7 @@ When the target solution uses any Blazor UI framework (Blazor, BlazorServer, Bla The detected value is shown in the *solution info* tooltip of the CRUD Page Generator screen and is reused for two things: -1. Template routing — `Frontend.Blazor.*` resource names are resolved to `Frontend.Blazor.MudBlazor.*` for MudBlazor solutions, so the generated `.razor` / `.razor.cs` files use `MudCard`, `AbpMudExtensibleDataGrid`, `MudDialog`, `MudForm` and the corresponding Mud input controls instead of the Blazorise `Card`, `DataGrid`, `Modal`, `Validations` markup. +1. Template routing — `Frontend.Blazor.*` resource names are resolved to `Frontend.Blazor.MudBlazor.*` for MudBlazor solutions, so the generated `.razor` / `.razor.cs` files use `MudCard`, `MudDataGrid`, `MudDialog`, `MudForm` and the corresponding Mud input controls instead of the Blazorise `Card`, `DataGrid`, `Modal`, `Validations` markup. 2. Template Management UI — the "Manage Templates" screen and the "outdated templates" check only list the variant that matches the current solution, so the Blazorise and MudBlazor template trees never appear side-by-side. Customized templates are stored under `.suite/customized-templates/` keyed by the full resource name, so MudBlazor customizations (`Frontend.Blazor.MudBlazor.*`) are physically isolated from Blazorise customizations (`Frontend.Blazor.*`) and never clash. From 5850ad3ede51b81aee21c044aa01decd1750f7ba Mon Sep 17 00:00:00 2001 From: Ma Liming Date: Wed, 27 May 2026 17:44:23 +0800 Subject: [PATCH 3/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/en/suite/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/suite/index.md b/docs/en/suite/index.md index ff1258779e..dfeaca8cb4 100644 --- a/docs/en/suite/index.md +++ b/docs/en/suite/index.md @@ -29,7 +29,7 @@ By using the ABP Suite, you can generate CRUD pages from the database to the fro ## Blazor UI library -When the target solution uses any Blazor UI framework (Blazor, BlazorServer, BlazorWebApp or MauiBlazor), ABP Suite detects the underlying Blazor component library by scanning the Blazor project `.csproj` for known package references: +When the target solution uses any Blazor UI framework (Blazor, BlazorServer, BlazorWebApp or MAUIBlazor), ABP Suite detects the underlying Blazor component library by scanning the Blazor project `.csproj` for known package references: * A reference to `Volo.Abp.MudBlazorUI`, `Volo.Abp.AspNetCore.Components.*.Theming.MudBlazor` or `*.Blazor.MudBlazor*` selects the **MudBlazor** templates. * Otherwise ABP Suite falls back to the **Blazorise** templates that have always shipped with Suite. From 6975e88d633b459b2bba4f5c5df89e77b63c2f18 Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 8 Jun 2026 13:21:47 +0800 Subject: [PATCH 4/4] Use IsDeleted column TypeMapping in soft-delete DbFunction --- .../AbpEfCoreModelBuilderExtensions.cs | 4 +- ...oftDelete_DbFunction_Bool_Literal_Tests.cs | 48 +++++++++++++++++++ .../TestMigrationsDbContext.cs | 11 +++++ .../EntityFrameworkCore/TestAppDbContext.cs | 11 +++++ .../TestApp/Domain/EntityWithIntSoftDelete.cs | 19 ++++++++ 5 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/SoftDelete_DbFunction_Bool_Literal_Tests.cs create mode 100644 framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/EntityWithIntSoftDelete.cs diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Microsoft/Extensions/DependencyInjection/AbpEfCoreModelBuilderExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Microsoft/Extensions/DependencyInjection/AbpEfCoreModelBuilderExtensions.cs index 055c0651cb..4b90d801e6 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Microsoft/Extensions/DependencyInjection/AbpEfCoreModelBuilderExtensions.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Microsoft/Extensions/DependencyInjection/AbpEfCoreModelBuilderExtensions.cs @@ -21,11 +21,11 @@ public static class AbpEfCoreModelBuilderExtensions if (abpEfCoreCurrentDbContext.Context?.DataFilter.IsEnabled() == true) { - // IsDeleted == false + // IsDeleted == false. Use the column's TypeMapping so any ValueConverter is applied to the literal. return new SqlBinaryExpression( ExpressionType.Equal, isDeleted, - new SqlConstantExpression(false, typeof(bool), boolParam.TypeMapping), + new SqlConstantExpression(false, typeof(bool), isDeleted.TypeMapping ?? boolParam.TypeMapping), boolParam.Type, boolParam.TypeMapping); } diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/SoftDelete_DbFunction_Bool_Literal_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/SoftDelete_DbFunction_Bool_Literal_Tests.cs new file mode 100644 index 0000000000..408920ce3e --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DataFiltering/SoftDelete_DbFunction_Bool_Literal_Tests.cs @@ -0,0 +1,48 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Data; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.TestApp.Domain; +using Volo.Abp.TestApp.EntityFrameworkCore; +using Volo.Abp.TestApp.Testing; +using Xunit; + +namespace Volo.Abp.EntityFrameworkCore.DataFiltering; + +// Regression for the soft-delete DbFunction translator hardcoding a bool false literal +// with the bool TypeMapping instead of routing the literal through the IsDeleted +// property's ValueConverter. EntityWithIntSoftDelete maps false->5, true->9, so the +// buggy SQL "is_deleted = 0" matches no row (returns 0) while the correct SQL +// "is_deleted = 5" matches the not-deleted row (returns 1). +public class SoftDelete_DbFunction_Bool_Literal_Tests : TestAppTestBase +{ + private readonly IRepository _repository; + private readonly IDataFilter _softDeleteFilter; + + public SoftDelete_DbFunction_Bool_Literal_Tests() + { + _repository = GetRequiredService>(); + _softDeleteFilter = GetRequiredService>(); + } + + [Fact] + public async Task SoftDelete_Filter_Should_Route_False_Literal_Through_ValueConverter() + { + await _repository.InsertAsync(new EntityWithIntSoftDelete { Name = "kept" }); + await _repository.InsertAsync(new EntityWithIntSoftDelete { Name = "removed", IsDeleted = true }); + + var visible = await _repository.GetListAsync(); + visible.Count.ShouldBe(1); + visible[0].Name.ShouldBe("kept"); + + using (_softDeleteFilter.Disable()) + { + var all = await _repository.GetListAsync(); + all.Count.ShouldBe(2); + } + } +} diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs index 96f4b1a2e1..3b569d8431 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs @@ -27,6 +27,8 @@ public class TestMigrationsDbContext : AbpDbContext public DbSet Categories { get; set; } + public DbSet EntityWithIntSoftDeletes { get; set; } + public DbSet AppEntityWithNavigations { get; set; } public DbSet AppEntityWithNavigationChildOneToMany { get; set; } @@ -104,6 +106,15 @@ public class TestMigrationsDbContext : AbpDbContext b.HasAbpQueryFilter(e => e.Name.StartsWith("abp")); }); + modelBuilder.Entity(b => + { + b.Property(x => x.IsDeleted) + .HasColumnName(EntityWithIntSoftDelete.IsDeletedColumnName) + .HasConversion( + v => v ? EntityWithIntSoftDelete.DeletedProviderValue : EntityWithIntSoftDelete.NotDeletedProviderValue, + i => i == EntityWithIntSoftDelete.DeletedProviderValue); + }); + modelBuilder.Entity(b => { b.ConfigureByConvention(); diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs index 2b95877bed..e5de412d6e 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs @@ -33,6 +33,8 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, public DbSet Categories { get; set; } + public DbSet EntityWithIntSoftDeletes { get; set; } + public DbSet AppEntityWithNavigations { get; set; } public DbSet AppEntityWithNavigationChildOneToMany { get; set; } @@ -129,6 +131,15 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, b.HasAbpQueryFilter(e => e.Name.StartsWith("abp")); }); + modelBuilder.Entity(b => + { + b.Property(x => x.IsDeleted) + .HasColumnName(EntityWithIntSoftDelete.IsDeletedColumnName) + .HasConversion( + v => v ? EntityWithIntSoftDelete.DeletedProviderValue : EntityWithIntSoftDelete.NotDeletedProviderValue, + i => i == EntityWithIntSoftDelete.DeletedProviderValue); + }); + modelBuilder.Entity(b => { b.ConfigureByConvention(); diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/EntityWithIntSoftDelete.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/EntityWithIntSoftDelete.cs new file mode 100644 index 0000000000..a8bcab3331 --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/EntityWithIntSoftDelete.cs @@ -0,0 +1,19 @@ +using System; +using Volo.Abp.Domain.Entities; + +namespace Volo.Abp.TestApp.Domain; + +// Uses a custom bool<->int converter where false maps to a non-zero value, so the +// soft-delete DbFunction translator (which hardcodes a bool false literal with the +// bool TypeMapping) is observably wrong on every provider: the generated SQL +// compares against 0/FALSE instead of the converter's actual provider value (5). +public class EntityWithIntSoftDelete : AggregateRoot, ISoftDelete +{ + public const string IsDeletedColumnName = "is_deleted"; + public const int NotDeletedProviderValue = 5; + public const int DeletedProviderValue = 9; + + public string Name { get; set; } + + public bool IsDeleted { get; set; } +}