Skip to content
bitzorcas
中EN

Reference

Menu Catalog, Persistence, and Seeding

Deep reference for the global SysModule row, compile-time metadata, IEntitySet store, seed natural key, nine shipped rows, and dual-ORM boundary.

Last updated

Menu persistence centers on one global table, SysModule. Its global scope, compatibility fields, and seed write-back rules must be understood before extending it safely.

1. Physical model

ParentIdParentId

parent SysModule
Id / Code / Name

child SysModule
Code / LinkUrl / OrderSort

grandchild SysModule
IsMenu / Enabled / Scope

UX_SysModule_Code

IX_SysModule_ParentId

ParentId expresses the self-reference only by application convention. There is no database foreign key or cascade declaration in this module.

2. Field semantics

FieldCurrent meaningConstraint
Idpersistence identifierinherited from EntityBase
ParentIdparent row Id; null/0 is rootlength 36, normal index
Codemodule code shared with Authorizationlength 50, unique, nullable
Namemanagement/navigation labellength 50, nullable
LinkUrlclient or API relative entrylength 100, nullable
Area/Controller/Actionlegacy MVC discovery fields2000 each, nullable
Iconicon codelength 100, nullable
OrderSortsibling display orderrequired integer
Descriptionmanagement descriptionlength 100, nullable
IsMenuincluded in navigation projectionrequired Boolean
Enabledincluded in any tree queryrequired Boolean
Scopelegacy Web=0, Host=1 platform scoperequired integer

Scope is currently stored and returned but is not used by MenuTreeBuilder to filter a login platform.

3. Compile-time ORM metadata

Key parts of the real model shape
// ① This is a global catalog: no IsTenant metadata and no BizEntityBase.
[BitzTable("SysModule", Description = "API/控制器/动作注册表")]
[BitzIndex("UX_SysModule_Code", "Code", IsUnique = true)]
[BitzIndex("IX_SysModule_ParentId", "ParentId")]
public sealed class MenuModuleCatalogRecord : EntityBase
{
// ② Parent relationships are Id values without a database FK.
[BitzColumn(Length = 36)]
public string? ParentId { get; set; }
// ③ Authorization shares Code, so treat it as a stable business key.
[BitzColumn(Length = 50)]
public string? Code { get; set; }
}

The generator emits the model manifest at compile time. SqlSugar and EF Core adapters consume the same metadata; Menu Infrastructure references neither concrete ORM package.

4. Store port

IMenuStore groups enabled/detail/child reads, visible-module-code reads, and insert/update/soft-delete writes. It does not expose IQueryable or let Menu consume Authorization persistence rows.

Application handlers

IMenuStore

MenuStore

IEntitySet

IAuthorizationAssignmentReader

SqlSugar adapter

EF Core adapter

5. Query behavior

GetAllEnabledAsync applies !IsDeleted && Enabled and then orders in memory by OrderSort. GetChildrenAsync does not require Enabled, so deletion can still find disabled descendants.

Read through narrow ports
// ① Do not inject a concrete DbContext or SqlSugarClient into business code.
var enabled = await menuStore.GetAllEnabledAsync(cancellationToken);
// ② The catalog is global; tenant visibility is filtered in another stage.
var visibleCodes = await menuStore.GetVisibleModuleCodesAsync(
currentUser.User.Roles,
cancellationToken);
// ③ Code joins owners; Id serves only catalog parent relationships.
var visible = enabled.Where(row => visibleCodes.Contains(row.Code!));

6. Update and delete semantics

Update uses one UpdateWhereAsync to write ParentId, Name, Code, LinkUrl, Icon, OrderSort, IsMenu, Enabled, and Scope. It does not update legacy MVC fields or Description.

Delete is a soft delete that sets IsDeleted=true. Neither method checks affected rows, so an update after a concurrent delete can still appear successful.

7. Seed step

MenuModuleSeedStep declares:

  • Order 220;
  • SeedId sys_module;
  • CSV 220-sys_module.csv;
  • WhereColumns Code;
  • match predicate row.Code == source.Code;
  • ORM-neutral EntitySetCsvSeedStepBase<T>.
Upsert by stable Code
// ① Code is both the unique database key and seed natural key.
protected override string[] WhereColumns => [nameof(MenuModuleCatalogRecord.Code)];
// ② Every supported adapter must translate this predicate.
protected override Expression<Func<MenuModuleCatalogRecord, bool>> Match(
MenuModuleCatalogRecord source)
=> row => row.Code == source.Code;
// ③ Copy does not change Code, preserving the cross-owner identity.
protected override void Copy(MenuModuleCatalogRecord source, MenuModuleCatalogRecord target)
=> target.Name = source.Name;

The final snippet is a focused teaching extract; the real Copy also updates parent, routes, icon, order, description, flags, Scope, and soft deletion.

8. Nine shipped rows

OrderCodeLinkUrl
0operations/api/operations
1platformbilling/api/platform-billing
2files/api/files
3notifications/api/notifications
4webhooks/api/webhooks
5catalog/api/catalog
6tickets/api/tickets
7chat/api/chat
8sandbox/api/notes

Every row is a root, menu-enabled, enabled, and Scope=0. The asset does not prove that every link has a same-named page or a matching target permission.

9. Changing Code

Authorization role-module-permission relations use Code as ModuleId. Renaming it can strand existing grants; the seeder also treats a new Code as a new row, not a rename.

A production migration must coordinate catalog uniqueness, authorization relations, cache invalidation, rollback, and compatibility aliases. Editing only the CSV is not a safe migration.

10. ParentId integrity

There is no foreign key, parent-existence check, cycle check, or depth limit. An orphan, self-parent, or A→B→A cycle can be stored. Orphans disappear from root projections; a reachable cycle can overflow recursion.

Remediation belongs in layers: transactional parent/self/ancestor validation on writes, plus defensive visited/depth checks on reads.

11. Test evidence and gaps

Architecture tests fix ORM neutrality, Store-only query handlers, fail-closed defaults, owner-local metadata, and generated manifests. Integration parity covers insert, read, visible codes, and soft deletion on both ORMs.

No focused test currently fixes Code rename, parent cycles, orphans, unique conflicts, affected-row handling, or coordinated seed/Authorization migrations.

12. Inspection commands

Terminal window
# Model, indexes, and Store update fields.
rg -n "BitzTable|BitzIndex|UpdateWhereAsync" src/Platform/Menu -g '*.cs'
# Expect one header plus nine rows.
wc -l src/Platform/Menu/*Infrastructure/Seeders/Assets/220-sys_module.csv
# Concrete ORM dependencies should be absent.
rg -n "SqlSugar|EntityFrameworkCore" src/Platform/Menu -g '*.cs' -g '*.csproj'

Module overview · Management and cache

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%