Click here to Skip to main content
15,886,199 members
Articles / Web Development / ASP.NET / ASP.NET Core

MVC 6 Dynamic Navigation Menu from Database

Rate me:
Please Sign up or sign in to vote.
3.45/5 (15 votes)
27 Aug 2019CPOL4 min read 59.2K   3K   31   12
MVC 6 .NET Core Dynamic navigation menu from the database

Table of Contents

Goal

Several years ago, I had to load navigation menu from database and create menu control using web forms, so the main idea of loading the menu data from database was filtering according to the user roles. As an end result, we'll have data filtered by role. Here we've to do it using ASP.NET Core 2.2 MVC application.

Introduction

I came across this requirement in MVC 6 .NET Core to generate a Role based navigation menu dynamically from the database so it can be used to explore the website and admin panel for management to allocate roles, permissions and other maintenance of the application. In this system, number of roles are limited so role based authorization can be a fit.

Components Used

Here are the components that you'll need to build and test the demo code provided.

Create Web Project

Create your Web Application in Visual Studio 2019.

Select the language as C# and Project type as Web then Select the first template, ASP.NET Core Web Application and click Next.

Provide a project name and select the physical path and then click on Create.

Select the Web Application (Model-View-Controller), then Click on the Change button on the right under Authentication. After that, select the Individual User Accounts, ok to close popup and then Create.

Now the project has been setup and can be run but we don't have any database created based on our model so first, we need to change our connection string in appsettings.json file. I'll be using the localhost as my server with Windows authentication, following is my connection string.

JavaScript
"DefaultConnection": "Server=localhost;Database=DynamicMenu;
Trusted_Connection=True;MultipleActiveResultSets=true"

But if we create the database at this level, we'll have only Identity tables like shown below:

But in our case, we need two more tables which we'll create using code first by defining their entities and then add them to our context class.

C#
[Table(name: "AspNetRoleMenuPermission")]
public class RoleMenuPermission
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public Guid Id { get; set; }

    [ForeignKey("ApplicationRole")]
    public string RoleId { get; set; }

    [ForeignKey("NavigationMenu")]
    public Guid NavigationMenuId { get; set; }

    public NavigationMenu NavigationMenu { get; set; }
}

[Table(name: "AspNetNavigationMenu")]
public class NavigationMenu
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public Guid Id { get; set; }

    public string Name { get; set; }

    [ForeignKey("ParentNavigationMenu")]
    public Guid? ParentMenuId { get; set; }

    public virtual NavigationMenu ParentNavigationMenu { get; set; }

    public string ControllerName { get; set; }

    public string ActionName { get; set; }

    [NotMapped]
    public bool Permitted { get; set; }
}

public class ApplicationDbContext : IdentityDbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {

    }

    public DbSet<RoleMenuPermission> RoleMenuPermission { get; set; }

    public DbSet<NavigationMenu> NavigationMenu { get; set; }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
    }
}

Migrations

Now we need to run the migrations and then update the database, Enable-Migrations command has been obsolete, so we need to delete everything from Migrations folder and then run add migration command.

add-migration InitialVersion

It'll create some files in the Migrations folder then we need to run update-database command if your connection string is correct, then it'll create your database like below:

For more details on Seeding data, you can check my other article:

For the current scenario, our seed will have all the navigational menu items, Users, Roles and permissions. So it'll be a bit more complex.

Now we have our database in place with all the entities, so let's run the application in Development environment and it'll insert the seed data in the database.

Data Service

We'll create a data service to communicate with the database, it's extremely simple, it has one main function GetMenuItemsAsync which returns Navigation menu view model after filtering by role.

C#
public class DataAccessService : IDataAccessService
{
     private readonly ApplicationDbContext _context;

     public DataAccessService(ApplicationDbContext context)
     {
         _context = context;
     }

     public async Task<List<NavigationMenuViewModel>> 
                       GetMenuItemsAsync(ClaimsPrincipal principal)
     {
         var isAuthenticated = principal.Identity.IsAuthenticated;
         if (!isAuthenticated)
             return new List<NavigationMenuViewModel>();

         var roleIds = await GetUserRoleIds(principal);
         var data = await (from menu in _context.RoleMenuPermission
                           where roleIds.Contains(menu.RoleId)
                           select menu)
                           .Select(m => new NavigationMenuViewModel()
                           {
                               Id = m.NavigationMenu.Id,
                               Name = m.NavigationMenu.Name,
                               ActionName = m.NavigationMenu.ActionName,
                               ControllerName = m.NavigationMenu.ControllerName,
                               ParentMenuId = m.NavigationMenu.ParentMenuId,
                           }).Distinct().ToListAsync();

         return data;
     }

     private async Task<List<string>> GetUserRoleIds(ClaimsPrincipal ctx)
     {
         var userId = GetUserId(ctx);
         var data = await (from role in _context.UserRoles
                           where role.UserId == userId
                           select role.RoleId).ToListAsync();

         return data;
     }

     private string GetUserId(ClaimsPrincipal user)
     {
         return ((ClaimsIdentity)user.Identity).FindFirst(ClaimTypes.NameIdentifier)?.Value;
     }
}

We need to register this service in the Startup.cs as well so Dependency Injection can serve it. It can be registered like this:

C#
services.AddScoped<IDataAccessService, DataAccessService>();

Navigation Menu

We'll use View Component for loading navigational menu as a partial view:

C#
public class NavigationMenuViewComponent : ViewComponent
{
    private readonly IDataAccessService _dataAccessService;

    public NavigationMenuViewComponent(IDataAccessService dataAccessService)
    {
        _dataAccessService = dataAccessService;
    }

    public async Task<IViewComponentResult> InvokeAsync()
    {
        var items = await _dataAccessService.GetMenuItemsAsync(HttpContext.User);

        return View(items);
    }
}

Create a Components folder in the Shared folder in Views. So in components, we can create NavigationMenu folder and then Default.cshtml view file. Here, that hierarchy is very important for it to work.

Here is the partial view HTML, here we'll keep our scope to 2 level menu only it can go to N level with recursion but to keep it limited here, we'll not use it.

HTML
@model List<Mvc.DynamicMenu.Models.NavigationMenuViewModel>
@{
    ViewData["Title"] = "NavigationMenu";
}
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">Dynamic Menu</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" 
 data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" 
 aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarsExampleDefault">
    <ul class="navbar-nav mr-auto">
        <li class="nav-item">
            <a class="nav-link text" asp-area="" 
             asp-controller="Home" asp-action="Index">Home</a>
        </li>
        <li class="nav-item">
            <a class="nav-link text" asp-area="" asp-controller="Home" 
             asp-action="Privacy">Privacy Policy</a>
        </li>

        @*Menu Items from the database*@
        
        @foreach (var item in Model)
        {
            if (item.ParentMenuId == null) //Level one items will have null parent id
            {
                if (!string.IsNullOrWhiteSpace(item.ControllerName))
                {
                    <li class="nav-item active">
                        <a class="nav-link text" asp-area="" 
                         asp-controller="@item.ControllerName" 
                         asp-action="@item.ActionName">@item.Name</a>
                    </li>
                }
                var children = Model.Where(x => x.ParentMenuId == item.Id).ToList();
                if (children != null) //Level one item has children so append them
                {
                    <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle" href="#" id="dropdown01" 
                         data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                         @item.Name</a>
                        <div class="dropdown-menu" aria-labelledby="dropdown01">
                            @foreach (var itm in children)
                            {
                                <a class="dropdown-item" asp-area="" 
                                 asp-controller="@itm.ControllerName" 
                                 asp-action="@itm.ActionName">@itm.Name</a>
                            }
                        </div>
                    </li>
                }
            }
        }
    </ul>
    <partial name="_LoginPartial" />
</div>

Now we'll create a controller named Administration with two actions, Roles and Users.

C#
public class AdministrationController : Controller
{
     private readonly UserManager<IdentityUser> _userManager;
     private readonly RoleManager<IdentityRole> _roleManager;
     private readonly ILogger<AdministrationController> _logger;

     public AdministrationController(
             UserManager<IdentityUser> userManager,
             RoleManager<IdentityRole> roleManager,
             ILogger<AdministrationController> logger)
     {
         _userManager = userManager;
         _roleManager = roleManager;
         _logger = logger;
     }

     public async Task<IActionResult> Roles()
     {
            .......
     }

     public async Task<IActionResult> Users()
     {
            ........
     }
}

After the controller, we'll create views for these actions where we can show the list of Roles and Users respectively.

Let's start the application once again here's how it'll look like, for any visitor the page looks like this but it'll load the additional menu items according to the roles assigned to the user.

Let's login with the user admin@test.com. Now the page looks like below with additional menu items allowed to admin according to their roles.

So here is how that menu has been drawn with a partial view after login.

What Next

Now we have one problem if somebody knows the URL of the page like https://localhost/administration/roles, they can still access the page. So next, we'll see how to do Role based Authorization..

Conclusion

We have achieved our goal of creating navigational menu from database when we create our database through migrations and started our project under Development environment. Logged in User sees menu items according to their roles. The source code is attached. I encourage you to run and see it. All of you are most welcome to post comments if you have any questions/suggestions. Thanks for reading.

History

  • 26th August, 2019: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Pakistan Pakistan
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Rashmi Ranjan Ransingh29-Dec-21 15:52
Rashmi Ranjan Ransingh29-Dec-21 15:52 
GeneralRe: My vote of 5 Pin
dnxit30-Dec-21 2:53
dnxit30-Dec-21 2:53 
QuestionIDataAccessService & ViewComponent Pin
imran_bof8-Oct-20 9:24
imran_bof8-Oct-20 9:24 
QuestionHow do I navigate around this error? Pin
Agbato Adewunmi31-Jul-20 23:00
Agbato Adewunmi31-Jul-20 23:00 
Questionhow to impliment IDataAccessService Pin
Member 1454291027-Apr-20 17:20
Member 1454291027-Apr-20 17:20 
AnswerRe: how to impliment IDataAccessService Pin
dnxit1-Jul-20 23:12
dnxit1-Jul-20 23:12 
QuestionLeft sidebar Navigation menu Pin
Ray_xxi31-Oct-19 8:47
Ray_xxi31-Oct-19 8:47 
AnswerRe: Left sidebar Navigation menu Pin
dnxit3-Nov-19 0:52
dnxit3-Nov-19 0:52 
QuestionNot All Menu Items are Actions Pin
#realJSOP27-Aug-19 3:27
mve#realJSOP27-Aug-19 3:27 
AnswerRe: Not All Menu Items are Actions Pin
dnxit27-Aug-19 6:37
dnxit27-Aug-19 6:37 
GeneralCannot see those images Pin
Michael Chao27-Aug-19 0:40
Michael Chao27-Aug-19 0:40 
Cannot see those images
GeneralRe: Cannot see those images Pin
dnxit27-Aug-19 0:51
dnxit27-Aug-19 0:51 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.