How to Build a Database-Driven Navigation Menu in ASP.NET Core 10 Using C# and Role-Based Permissions (2026)
Build an ASP.NET Core database-driven navigation menu by loading a hierarchy of SQL Server menu definitions and rendering it with a Razor View Component. Resolve destination keys to known application routes and check the current user’s policy before displaying protected links. Enforce the same policy on the destination endpoint.
How does a role-aware navigation tree work?
Each database row has an ID, optional parent ID, label, destination key, sort order and enabled flag. A group provides structure, Home points to the dashboard, and Reports points to a route protected by the reports.read policy. The app applies that policy to the endpoint independently of whether the menu displays the link.
| Layer | Responsibility |
|---|---|
| SQL Server | Store the hierarchy, labels and order |
| Destination catalog | Resolve approved keys to application paths |
| View Component | Apply the current caller’s authorization and build the tree |
| Controller | Enforce access on the HTTP request |
A parent group with no visible children disappears. An unknown destination is omitted. A database label remains a string that Razor encodes. This sample does not treat database values as arbitrary redirect destinations or as permissions that can grant access.
Install the project and seed SQL Server
Use the .NET 10 SDK and an empty SQL Server development database. Apply the seed script as a deployment account. Give the app identity read access to dbo.MenuNode and set ConnectionStrings__Navigation through environment configuration or secrets. Use a separate identity for navigation edits.
The self-referencing foreign key protects against missing parent records in the stored table. It does not enforce a complete cycle-free tree. Validate hierarchy updates in your admin workflow, including cycles and maximum depth. In the renderer, rows reachable from root are traversed within a depth cap. An enabled node under a disabled parent cannot be reached.
Implement MVC and the View Component
The component fetches the raw definitions asynchronously and then calls IAuthorizationService for the current user. The cache stores shared definitions for two minutes. The rendered tree and authorization decisions are calculated for each request. The example maps a small, fixed set of destination keys. Larger applications can use an approved route-name catalog and LinkGenerator.
The nested lists preserve the relationship between groups and children, and the active link receives aria-current. Keep the nav landmark labeled. If you add collapsible groups, use keyboard-accessible controls, expanded state and logical focus order. Check menu wrapping at narrow widths before adding custom styles.
Test both visibility and route access
Run on http://localhost:5096 in Development. Use the developer JWT tool for the appropriate audience. Generate one token with role Analyst and one authenticated token without it. Use curl with the Authorization header when testing. Direct browser navigation needs a browser-compatible authentication scheme to carry credentials.
- Open the root without a token. Expect Home and no Reports link. Request /reports directly and expect 401.
- Open the root with a token lacking Analyst. Expect the Reports group to remain hidden. The direct route should return 403.
- Open /reports with the Analyst token. Expect 200, a Reports link and aria-current for that link.
- Disable the parent in SQL and wait for the two-minute cache TTL. Confirm its subtree disappears. The Analyst still has route access because that access belongs to the endpoint policy.
- Insert an unknown destination key in the development fixture. Check that it does not generate an external or unrecognized link.
Cache, tenant and production design
If tenants have different menu definitions, apply tenant scope in the SQL query and cache key. Reapply authorization per caller after the cache lookup. In a deployment with several replicas, use coordinated invalidation when editors expect an immediate change. The sample can issue duplicate reads on concurrent cache misses. Measure contention before adding coalescing and preserve request cancellation.
Keep the database topology bounded. The query reads at most 201 rows and rejects a configuration beyond 200. Have a validation and rollback path for navigation changes. If the menu is critical and the database can be unavailable, define a small public fallback and monitor failures. Never grant access through the fallback.
Troubleshooting and FAQ
| Symptom | Check |
|---|---|
| Reports is visible but clicking returns 403 | Compare the component’s policy, token role mapping and endpoint policy |
| A menu edit is delayed | Check the cache TTL and replica invalidation |
| A group never appears | Check its parent, whether it has visible children and its depth |
| A label appears as text containing markup | Keep Razor encoding and fix the stored label |
Can I store full URLs? The sample uses known destination keys. If you support external destinations, define an explicit scheme and host policy and review it independently.
Is hiding a menu item enough to protect a page? The route enforces authorization for direct requests. Hiding the item improves the interface for users without access.
See ASP.NET Core View Components, policy-based authorization, and the ASP.NET PB JWT tutorial.
Comments
Post a Comment