Skip to content
bitzorcas
中EN

Tutorial

Menu Management, Ordering, and Cache Consistency

Deep guide to create, update, delete, sort, and toggle paths, typed errors, invalidation order, multi-instance sync, and consistency gaps.

Last updated

All five write use cases follow “read/validate → Store write → local tag removal → sync event,” but current business validation and atomicity remain limited.

1. Write path

Sync notifierLocal cacheMenuStoreCommand handlerAuthorization pipelineGenerated endpointSync notifierLocal cacheMenuStoreCommand handlerAuthorization pipelineGenerated endpointauthenticated commandmenus.menu create/update/deleteauthorizedinsert/update/soft-deleteRemoveByTag("menus")Cache / menus / tenant 0 / Updated

All write endpoints authenticate by default, and every command implements IAuthorizedRequest.

2. CreateMenu

Create checks only nonblank Name and Code and normalizes blank ParentId to null. It does not validate the parent, URL, Scope, order range, duplicate Code, or icon format.

Create a global menu
// ① Code is the stable key shared by Menu and Authorization.
var command = new CreateMenu.Command(
ParentId: null,
Name: "Orders",
Code: "orders",
LinkUrl: "/orders",
Icon: "shopping-cart",
OrderSort: 20,
IsMenu: true,
Enabled: true,
Scope: 0);
// ② Caller needs menus.menu.create; the unique index is the final conflict guard.
var result = await mediator.Send(command, cancellationToken);

Mapping a uniqueness exception depends on the shared exception pipeline; the handler does not return a typed duplicate error itself.

3. UpdateMenu

Update reads by Id and returns Menu.NotFound on a miss, then overwrites nine managed fields. It does not repeat Create’s Name/Code validation or protect Code identity.

It can therefore store a blank Name/Code or make ParentId point to self or a descendant. A production API should use one validator and model reparenting as a separate transactional operation.

4. SortMenu

Sort changes only the selected OrderSort:

Current ordering semantics
public static class MenuErrors
{
public static readonly Error NotFound =
Error.NotFound("Menu.NotFound", "Menu does not exist");
}
// ① A missing row returns Menu.NotFound.
var entity = await menuStore.GetByIdAsync(request.Id, cancellationToken);
if (entity is null)
return Result.Failure(MenuErrors.NotFound);
// ② No sibling moves, and equal values are allowed.
entity.OrderSort = request.OrderSort;
await menuStore.UpdateAsync(entity, cancellationToken);
// ③ All menu projection caches are removed after the write.
await CreateMenu.InvalidateCacheAsync(cacheStore, syncNotifier, cancellationToken);

Equal OrderSort values have no guaranteed secondary ordering, and base row order can differ by database.

5. ToggleMenu

Toggle overwrites one Enabled flag. Reads exclude disabled rows, so disabling a parent hides its reachable descendants without changing their state.

Current behavior supports temporary parent hiding. It does not meet a requirement that disabling a folder persistently disables its whole subtree.

6. DeleteMenu

Delete checks the root, recursively loads direct children, soft-deletes each subtree depth-first, and finally deletes the root.

target node

query direct children

recursively delete each subtree

soft-delete current node

invalidate only after all succeed

No Unit of Work wraps the tree. A mid-run failure leaves partial soft deletion and skips cache invalidation. A cycle prevents normal recursion termination.

7. Typed failures

The explicit application errors are:

ErrorTrigger
Menu.InvalidInputblank Name/Code in Create
Menu.NotFoundmissing Id in detail/update/delete/sort/toggle

There are no dedicated errors for missing parents, duplicate Codes, cycles, concurrent overwrite, or authorization references.

8. Invalidation order

The shared helper removes the tag first, then publishes:

Current cross-instance event
// ① Remove tree, flat, and navigation entries on this instance.
await cacheStore.RemoveByTagAsync(MenuCacheTags.Menus, cancellationToken);
// ② Broadcast Cache type, menus resource, and global tenant "0".
await syncNotifier.NotifyAsync(
LocalResourceTypes.Cache,
"menus",
"0",
LocalResourceAction.Updated,
DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
cancellationToken);

If cache removal or notification fails after persistence, the request fails but the database change does not roll back automatically.

9. Sync consumer

MenuCacheSyncConsumer.ResourceType is generic Cache. Handle does not check event.ResourceId == "menus", so any Cache event routed to it clears the Menu tag.

Non-cancellation exceptions are logged and suppressed. A message may appear consumed even though invalidation failed; there is no module-specific retry/dead-letter evidence.

10. Consistency windows

failure gapfailure gapdelivery gap

DB commit

local invalidation

broadcast

remote invalidation

The 15-minute TTL is a final bound, not a strong consistency guarantee. GA should consider an outbox, versioned catalog snapshots, retry/dead-letter, and cache-staleness telemetry.

11. Concurrency semantics

Store update/delete predicates contain Id only, with no version or old-value check. Concurrent administrators get last-write-wins behavior; Sort and Toggle have no compare-and-swap.

EntityBase may supply audit columns, but this UpdateWhere builder has no explicit concurrency condition. The handbook must not claim optimistic concurrency.

12. Production management guidance

Restrict platform-admin permissions; show cross-tenant impact; make Code read-only by default; detect cycles before reparenting; export subtree and grant references before deletion; verify both database and navigation afterward; and do not blindly replay a non-idempotent Create after an ambiguous failure.

Long term, model create, rename, reparent, reorder, toggle, and delete as distinct operations with explicit validation, audit, and rollback semantics.

13. Test checklist

  • Create blanks and uniqueness conflict;
  • Update blank Code, self-parent, descendant-parent;
  • Sort equal values, concurrent writes, deterministic secondary order;
  • parent/child Toggle projection;
  • Delete partial failure, deep tree, cycle, grant references;
  • local invalidation and publisher failures;
  • remote duplicate, reordered, and lost events;
  • 15-minute TTL fallback.

Current automation does not cover these command and cache-sync semantics.

14. Inspection commands

Terminal window
# All five writes and the shared invalidation helper.
rg -n "InvalidateCacheAsync|InsertAsync|UpdateAsync|DeleteDescendantsAsync" src/Platform/Menu -g '*.cs'
# Transaction, outbox, and concurrency support; expect no material Menu hits.
rg -n "UnitOfWork|Transaction|Outbox|Concurrency|RowVersion" src/Platform/Menu -g '*.cs'

Module overview · Visibility and navigation

100%

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