Menu is the platform’s global navigation-catalog owner. It manages the structure and display fields in SysModule and projects flat lists, trees, and client navigation according to a user’s role grants. It does not decide whether a target API can be called, and it is not a feature manager or client router.
1. Current product surface
There are three projects. Contracts contains four output DTOs; Application owns nine use cases and tree policy; Infrastructure owns the global row, IEntitySet store, seed step, and cache-sync consumer.
2. Nine HTTP use cases
| Method and path | Use case | Resource permission |
|---|---|---|
POST /api/menus | CreateMenu | create |
PUT /api/menus/{id} | UpdateMenu | update |
DELETE /api/menus/{id} | DeleteMenu | delete |
PATCH /api/menus/{id}/sort | SortMenu | update |
PATCH /api/menus/{id}/toggle | ToggleMenu | update |
GET /api/menus/{id} | GetMenuDetail | view |
GET /api/menus/flat | GetMenuFlat | view |
GET /api/menus/tree | GetMenuTree | view |
GET /api/menus/navigation | GetNavigation | authentication only; no IAuthorizedRequest |
GenerateEndpointAttribute.RequireAuthorization defaults to true, so navigation still requires a signed-in caller. It skips the menus.menu.view resource decision, not authentication.
3. Permissions and governance
The marker is AppModule("Menu", Code="menus"), with allowed dependencies on Authorization and Identity. Its current permission catalog has four values:
menus.menu.viewmenus.menu.createmenus.menu.updatemenus.menu.deleteMenu has no feature catalog. The old page’s “feature-aware visibility” claim was unsupported; current visibility is decided only by the admin role shortcut or module codes returned by Authorization.
4. Request-to-navigation path
Role name admin is matched case-insensitively. Its presence makes the resolver return null, which means all enabled catalog rows. A non-admin with no role gets an empty navigation result.
5. Correct consumption
// ① The generated endpoint authenticates this query but applies no menu-admin permission.var result = await mediator.Send( new GetNavigation.Query(), cancellationToken);
// ② The result is role-filtered and includes ancestors of visible nodes.if (result.IsFailure) return result.Error;
// ③ Use it only for navigation; every target endpoint enforces its own policy.return result.Value.Select(RenderNavigationNode).ToList();The navigation DTO exposes only Id, Name, Icon, LinkUrl, and Children. Code, Scope, Enabled, and authorization data are not returned.
6. Global, not tenant-scoped
MenuModuleCatalogRecord inherits EntityBase; its table metadata has no IsTenant=true. Structure is shared by all tenants, while tenant differences come from tenant-owned role-to-module relations in Authorization.
Changing a name, link, parent, order, or enabled flag therefore affects every tenant. Tenant-custom menus require an explicit override model and merge policy; the current row must not be treated as tenant data.
7. Seed baseline
MenuModuleSeedStep runs at order 220 and uses Code as its natural key. The asset currently ships nine root nodes: Operations, PlatformBilling, Files, Notifications, Webhooks, Catalog, Tickets, Chat, and Sandbox.
The step updates parent, display data, legacy MVC fields, order, description, flags, Scope, and soft deletion, but it does not replace the matching Code.
8. Current guarantees
- Code has a database unique index.
- ParentId has a normal index.
- The store uses
IEntitySet<MenuModuleCatalogRecord>and stays ORM-neutral. - Authorization relations are read only through
IAuthorizationAssignmentReader. - Queries load only enabled, non-deleted catalog rows.
- Flat, tree, and navigation projections are cached by user and visibility set.
- Successful writes remove the local menu tag and publish a sync notice.
- The API shell supplies a fail-closed unavailable store without database configuration.
9. Current non-guarantees
- Create/update do not validate parent existence, self-parenting, or ancestor cycles.
- Update does not reject blank Name/Code or proactively detect duplicate Code.
- Delete is neither transactional nor protected from referenced authorization data.
- Sort writes one integer and does not rebalance siblings.
- Toggling a parent does not update descendant state.
- Tree recursion has no cycle or depth guard.
- There is no Menu feature, audit timeline, version history, or tenant override.
- Database writes and cache events have no outbox atomicity.
10. When to use Menu
Use it for stable product navigation, module entry points, and legacy controller/action compatibility metadata. A page-local tab, button, or temporary onboarding hint should not become a global SysModule row.
Before adding a node, define its stable Code, parent, target URL, Scope, menu flag, default enabled state, and the roles that receive module visibility.
11. Handbook map
- Catalog, persistence, and seeding
- Visibility, trees, and navigation
- Management, ordering, and cache consistency
- Security, testing, operations, and GA
12. Source inspection
# Inspect all nine command/query endpoint declarations.find src/Platform/Menu -path '*Commands*' -o -path '*Queries*' | sort
# Verify global metadata, seeding, and the Authorization narrow port.rg -n "BitzTable|WhereColumns|IAuthorizationAssignmentReader" src/Platform/Menu -g '*.cs'
# A Menu feature catalog should not be present.rg -n "FeatureCatalog|FeatureDefinition" src/Platform/Menu -g '*.cs'