Dynamic Menus in ASP.NET Core MVC

How to Implement Dynamic Menus in ASP.NET Core MVC

In this article, I will explain How to Implement a Dynamic Menus in ASP.NET Core MVC with an Example. Please read our previous article discussing How to Implement a Cascading Dropdown List in ASP.NET Core MVC using jQuery Ajax with an Example.

What are Dynamic Menus in Web Applications?

Dynamic Menus are navigational elements in web applications that are generated and displayed based on data retrieved from a database rather than being hard-coded. Dynamic menus are crucial for applications where the menu structure changes frequently or needs to reflect real-time data, such as e-commerce platforms, content management systems, and portals.

Why Do We Need Dynamic Menus in Web Application?

Dynamic Menus are navigation menus that are generated at runtime from data stored in a database rather than hardcoded into views. They offer several benefits:

  • Changes in the database automatically reflect in the menu structure.
  • Updating or rearranging categories and links doesn’t require code changes, just data modifications.
  • As your content grows, you can easily add more levels or categories, which will reflect automatically.
  • Dynamic menus allow you to display content, products, or links that are most relevant at any given time.
  • Menus can be built based on user roles, permissions, or preferences.
Real-Time Ecommerce Example: Building Dynamic Menus

We will build a three-level menu structure for an e-commerce application. Each menu level corresponds to:

  • Category
  • Sub-Category
  • Sub-Sub-Category

Note: Sub‑Category and Sub‑Sub‑Category are optional.

  • Product Association: Products can belong to a category, sub‑category, or sub‑sub‑category. If a category has child categories, products cannot be added directly to that (parent) category. Only leaf categories (categories with no children) can have products.
  • Initial Page Load: On the first load, random products are shown with the complete category menu and “View More” options.
  • Filtering: When a user clicks a category, only its associated products are loaded.
Step-by-Step Implementation

Let us proceed and implement this example step by step. We will use the following technology stacks:

  • ASP.NET Core MVC
  • Entity Framework Core Code First Approach
  • SQL Server
  • Bootstrap (For Responsive and Professional UI)

We will create the following Dynamic Menu:

How to Implement a Dynamic Menus in ASP.NET Core MVC with an Example

Setting Up the ASP.NET Core MVC Application

Create a new ASP.NET Core MVC project and install the necessary NuGet packages. Open Visual Studio and create a new ASP.NET Core Web Application using the Model-View-Controller Project template, giving the project name DynamicMenusApp. Once you create the Project, please install the following NuGet Packages.

  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools

You can install these two packages using Visual Studio Package Manager Console by executing the below commands:

  • Install-Package Microsoft.EntityFrameworkCore.SqlServer
  • Install-Package Microsoft.EntityFrameworkCore.Tools
Creating the Models:

We will use the Code First approach with EF Core to create two main models:

Category Model:

So, create a class file named Category.cs within the Models folder and copy and paste the following code. The Category model represents a navigation node. Since our menu is hierarchical, the Category model is self‑referencing. Each category can have an optional parent and a collection of child categories. Only leaf categories (i.e., those with no children) are allowed to have products.

using System.ComponentModel.DataAnnotations;
namespace DynamicMenusApp.Models
{
    public class Category
    {
        public int Id { get; set; }

        [Required]
        public string Name { get; set; }

        // Self-referencing relationship: Parent Category (nullable)
        public int? ParentCategoryId { get; set; }
        public virtual Category ParentCategory { get; set; }

        // Collection of child categories (if any)
        public virtual ICollection<Category> ChildCategories { get; set; } = new List<Category>();

        // Products can be linked to a category (only allowed if this category is a leaf node)
        public virtual ICollection<Product> Products { get; set; } = new List<Product>();
    }
}
Product Model:

So, create a class file named Product.cs within the Models folder and then copy and paste the following code. The Product model represents an item for sale. It includes essential properties along with a foreign key linking it to the associated (leaf) category. However, as per our business rule, products can only be added to a leaf category (one that does not have child categories).

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace DynamicMenusApp.Models
{
    public class Product
    {
        public int Id { get; set; }
        [Required]
        public string Name { get; set; }
        [DataType(DataType.Currency)]
        [Column(TypeName ="decimal(18,2)")]
        public decimal Price { get; set; }
        public string Description { get; set; }
        
        // Foreign key for Category. This category must be a leaf node.
        public int CategoryId { get; set; }
        public virtual Category Category { get; set; }
    }
}

The relationship is defined such that a product can belong to either a category, sub‑category, or sub‑sub‑category.

Creating the DbContext and Seeding Data

We will create a DbContext named EcommerceDbContext that includes DbSets for both Category and Product. In the OnModelCreating method, we will seed sample data representing categories and products. So, first, create a folder named Data in the project root directory. Inside the Data folder, add a class file named EcommerceDbContext.cs and then copy and paste the following code:

using DynamicMenusApp.Models;
using Microsoft.EntityFrameworkCore;

namespace DynamicMenusApp.Data
{
    public class EcommerceDbContext : DbContext
    {
        public EcommerceDbContext(DbContextOptions<EcommerceDbContext> options)
            : base(options)
        {
        }

        public DbSet<Category> Categories { get; set; }
        public DbSet<Product> Products { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            // Parent Categories
            modelBuilder.Entity<Category>().HasData(
                new Category { Id = 1, Name = "Electronics" },
                new Category { Id = 2, Name = "Clothing" },
                new Category { Id = 3, Name = "Books" },
                new Category { Id = 4, Name = "Home & Kitchen" },
                new Category { Id = 5, Name = "Beauty & Personal Care" }
            );

            // ----- Electronics (Parent: 1) -----
            // Sub-Categories for Electronics
            modelBuilder.Entity<Category>().HasData(
                new Category { Id = 6, Name = "Mobile Phones", ParentCategoryId = 1 },
                new Category { Id = 7, Name = "Laptops", ParentCategoryId = 1 },
                new Category { Id = 8, Name = "Televisions", ParentCategoryId = 1 },
                new Category { Id = 9, Name = "Cameras", ParentCategoryId = 1 }
            );
            // Sub‑Sub-Categories for Mobile Phones
            modelBuilder.Entity<Category>().HasData(
                new Category { Id = 10, Name = "Android Phones", ParentCategoryId = 6 },
                new Category { Id = 11, Name = "iOS Phones", ParentCategoryId = 6 }
            );

            // ----- Clothing (Parent: 2) -----
            // Sub-Categories for Clothing
            modelBuilder.Entity<Category>().HasData(
                new Category { Id = 12, Name = "Men", ParentCategoryId = 2 },
                new Category { Id = 13, Name = "Women", ParentCategoryId = 2 },
                new Category { Id = 14, Name = "Kids", ParentCategoryId = 2 }
            );
            // Sub‑Sub-Categories for Men's Clothing
            modelBuilder.Entity<Category>().HasData(
                new Category { Id = 15, Name = "Shirts", ParentCategoryId = 12 },
                new Category { Id = 16, Name = "Trousers", ParentCategoryId = 12 }
            );

            // ----- Books (Parent: 3) -----
            // Sub-Categories for Books
            modelBuilder.Entity<Category>().HasData(
                new Category { Id = 17, Name = "Fiction", ParentCategoryId = 3 },
                new Category { Id = 18, Name = "Non-Fiction", ParentCategoryId = 3 },
                new Category { Id = 19, Name = "Children's Books", ParentCategoryId = 3 }
            );
            // Sub‑Sub-Categories for Fiction
            modelBuilder.Entity<Category>().HasData(
                new Category { Id = 20, Name = "Classics", ParentCategoryId = 17 },
                new Category { Id = 21, Name = "Modern Fiction", ParentCategoryId = 17 }
            );

            // ----- Home & Kitchen (Parent: 4) -----
            // Sub-Categories for Home & Kitchen
            modelBuilder.Entity<Category>().HasData(
                new Category { Id = 22, Name = "Furniture", ParentCategoryId = 4 },
                new Category { Id = 23, Name = "Appliances", ParentCategoryId = 4 },
                new Category { Id = 24, Name = "Decor", ParentCategoryId = 4 }
            );
            // Sub‑Sub-Categories for Furniture
            modelBuilder.Entity<Category>().HasData(
                new Category { Id = 25, Name = "Sofas", ParentCategoryId = 22 },
                new Category { Id = 26, Name = "Dining Tables", ParentCategoryId = 22 }
            );

            // ----- Beauty & Personal Care (Parent: 5) -----
            // Sub-Categories for Beauty & Personal Care
            modelBuilder.Entity<Category>().HasData(
                new Category { Id = 27, Name = "Skincare", ParentCategoryId = 5 },
                new Category { Id = 28, Name = "Hair Care", ParentCategoryId = 5 }
            );
            // Sub‑Sub-Categories for Skincare and Hair Care
            modelBuilder.Entity<Category>().HasData(
                new Category { Id = 29, Name = "Moisturizers", ParentCategoryId = 27 },
                new Category { Id = 30, Name = "Sunscreens", ParentCategoryId = 27 },
                new Category { Id = 31, Name = "Shampoo", ParentCategoryId = 28 },
                new Category { Id = 32, Name = "Conditioner", ParentCategoryId = 28 }
            );

            // --------------------------
            // Seed Products for leaf categories (only categories with no children)

            // Electronics:
            // Android Phones (CategoryId = 10)
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 1, Name = "Samsung Galaxy S21", Description = "Latest Android smartphone", Price = 799.99m, CategoryId = 10 },
                new Product { Id = 2, Name = "OnePlus 9", Description = "High performance Android smartphone", Price = 729.99m, CategoryId = 10 }
            );
            // iOS Phones (CategoryId = 11)
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 3, Name = "iPhone 13", Description = "Latest iOS smartphone", Price = 999.99m, CategoryId = 11 },
                new Product { Id = 4, Name = "iPhone 13 Pro", Description = "Premium iOS smartphone", Price = 1099.99m, CategoryId = 11 }
            );
            // Laptops (CategoryId = 7)
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 5, Name = "Dell XPS 13", Description = "Ultra-portable laptop", Price = 1199.99m, CategoryId = 7 },
                new Product { Id = 6, Name = "MacBook Air", Description = "Lightweight Apple laptop", Price = 999.99m, CategoryId = 7 }
            );
            // Televisions (CategoryId = 8)
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 7, Name = "Sony Bravia 55 inch", Description = "Smart TV with 4K resolution", Price = 699.99m, CategoryId = 8 },
                new Product { Id = 8, Name = "LG OLED", Description = "High-end OLED television", Price = 1299.99m, CategoryId = 8 }
            );
            // Cameras (CategoryId = 9)
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 9, Name = "Canon EOS R5", Description = "Professional mirrorless camera", Price = 3899.99m, CategoryId = 9 },
                new Product { Id = 10, Name = "Nikon Z7", Description = "High-resolution mirrorless camera", Price = 2999.99m, CategoryId = 9 }
            );

            // Clothing:
            // Men's Shirts (CategoryId = 15)
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 11, Name = "Men's Casual Shirt", Description = "Comfortable casual shirt", Price = 29.99m, CategoryId = 15 }
            );
            // Men's Trousers (CategoryId = 16)
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 12, Name = "Men's Formal Trousers", Description = "Elegant formal trousers", Price = 39.99m, CategoryId = 16 }
            );
            // Women (CategoryId = 13)
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 13, Name = "Women's Summer Dress", Description = "Light and breezy summer dress", Price = 49.99m, CategoryId = 13 }
            );
            // Kids (CategoryId = 14)
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 14, Name = "Kids' T-Shirt", Description = "Colorful kids T-shirt", Price = 14.99m, CategoryId = 14 }
            );

            // Books:
            // Classics (CategoryId = 20)
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 15, Name = "The Great Gatsby", Description = "Classic novel by F. Scott Fitzgerald", Price = 10.99m, CategoryId = 20 }
            );
            // Modern Fiction (CategoryId = 21)
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 16, Name = "1984", Description = "Dystopian novel by George Orwell", Price = 8.99m, CategoryId = 21 }
            );
            // Non-Fiction (CategoryId = 18)
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 17, Name = "Sapiens", Description = "A Brief History of Humankind by Yuval Noah Harari", Price = 14.99m, CategoryId = 18 }
            );
            // Children's Books (CategoryId = 19)
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 18, Name = "Harry Potter", Description = "Fantasy novel series by J.K. Rowling", Price = 29.99m, CategoryId = 19 }
            );

            // Home & Kitchen:
            // Sofas (CategoryId = 25)
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 19, Name = "Modern Sofa", Description = "Comfortable modern sofa", Price = 499.99m, CategoryId = 25 }
            );
            // Dining Tables (CategoryId = 26)
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 20, Name = "Dining Table", Description = "Spacious dining table", Price = 299.99m, CategoryId = 26 }
            );
            // Appliances (CategoryId = 23)
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 21, Name = "Microwave Oven", Description = "Compact microwave oven", Price = 89.99m, CategoryId = 23 },
                new Product { Id = 22, Name = "Refrigerator", Description = "Energy-efficient refrigerator", Price = 599.99m, CategoryId = 23 }
            );
            // Decor (CategoryId = 24)
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 23, Name = "Wall Art", Description = "Abstract wall art painting", Price = 59.99m, CategoryId = 24 },
                new Product { Id = 24, Name = "Table Lamp", Description = "Stylish table lamp", Price = 29.99m, CategoryId = 24 }
            );

            // Beauty & Personal Care:
            // Moisturizers (CategoryId = 29)
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 25, Name = "Hydrating Moisturizer", Description = "Nourishing face moisturizer", Price = 19.99m, CategoryId = 29 }
            );
            // Sunscreens (CategoryId = 30)
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 26, Name = "Broad Spectrum Sunscreen", Description = "Protects against UVA/UVB rays", Price = 15.99m, CategoryId = 30 }
            );
            // Shampoo (CategoryId = 31)
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 27, Name = "Nourishing Shampoo", Description = "Gentle cleansing shampoo", Price = 9.99m, CategoryId = 31 }
            );
            // Conditioner (CategoryId = 32)
            modelBuilder.Entity<Product>().HasData(
                new Product { Id = 28, Name = "Smoothing Conditioner", Description = "Conditioner for soft, manageable hair", Price = 9.99m, CategoryId = 32 }
            );
        }
    }
}
Modify AppSettings.json File:

Please modify the appsettings.json file as follows. Here, we have added the connection string which DbConext will use.

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=ECommerceDB;Trusted_Connection=True;TrustServerCertificate=True;"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}
Modify the Program Class:

Please modify the Program class as follows. We have registered the required services and middleware components to the request processing pipeline.

using DynamicMenusApp.Data;
using Microsoft.EntityFrameworkCore;
namespace DynamicMenusApp
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // Add services to the container.
            builder.Services.AddControllersWithViews();

            // Configure SQL Server DbContext
            builder.Services.AddDbContext<EcommerceDbContext>(options =>
                options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
            );

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (!app.Environment.IsDevelopment())
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseRouting();
            app.UseAuthorization();

            app.MapControllerRoute(
                name: "default",
                pattern: "{controller=Home}/{action=Index}/{id?}");

            app.Run();
        }
    }
}
Database Migrations

Open the Package Manager Console in Visual Studio, and then execute the following commands:

  • Add-Migration Mig1
  • Update-Database

This should create the ECommerceDB with the required Categories and Products tables, as shown in the image below.

How to Implement Dynamic Menus in ASP.NET Core MVC

Create a View Model

First, create a folder named ViewModels in the Project root directory. Inside the ViewModels folder, add a class file named CategoryProductsViewModel.cs and copy and paste the following code. The Index view will use this View Model to display the products.

using DynamicMenusApp.Models;
namespace DynamicMenusApp.ViewModels
{
    public class CategoryProductsViewModel
    {
        public Category Category { get; set; }
        public List<Product> Products { get; set; }
    }

    public class HomeIndexViewModel
    {
        // A collection of top-level categories each with a subset of products
        public List<CategoryProductsViewModel> CategoryProducts { get; set; } = new List<CategoryProductsViewModel>();
    }
}

Here,

  • CategoryProductsViewModel: Holds one Category plus a list of Products.
  • HomeIndexViewModel: Aggregates multiple CategoryProductsViewModel items (one per top-level category).
Create Menu ViewComponent

First, create a folder named ViewComponents in the project root directory. Inside this folder, create a new class file named MenuViewComponent.cs and copy and paste the following code.

using DynamicMenusApp.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace DynamicMenusApp.ViewComponents
{
    public class MenuViewComponent : ViewComponent
    {
        private readonly EcommerceDbContext _context;

        public MenuViewComponent(EcommerceDbContext context)
        {
            _context = context;
        }

        public IViewComponentResult Invoke()
        {
            var categories = _context.Categories
                .AsNoTracking()
                .Include(c => c.ChildCategories)
                    .ThenInclude(child => child.ChildCategories)
                .ToList();

            return View(categories);
        }
    }
}
Create the associated View

Create a view file at Views/Shared/Components/Menu/Default.cshtml. This view should contain the markup for rendering the menus.

@model IEnumerable<DynamicMenusApp.Models.Category>

<ul class="navbar-nav me-auto mb-2 mb-lg-0">
    @foreach (var category in Model.Where(c => c.ParentCategoryId == null))
    {
        if (category.ChildCategories.Any())
        {
            <li class="nav-item dropdown">
                <a class="nav-link dropdown-toggle" href="@Url.Action("Index", "Home", new { categoryId = category.Id })"
                   id="navbarDropdown-@category.Id" role="button" data-bs-toggle="dropdown" aria-expanded="false">
                    @category.Name
                </a>
                <ul class="dropdown-menu" aria-labelledby="navbarDropdown-@category.Id">
                    @foreach (var subCat in category.ChildCategories)
                    {
                        if (subCat.ChildCategories.Any())
                        {
                            <!-- Submenu for deeper levels -->
                            <li class="dropdown-submenu position-relative">
                                <a class="dropdown-item dropdown-toggle" href="@Url.Action("Index", "Home", new { categoryId = subCat.Id })">
                                    @subCat.Name
                                </a>
                                <ul class="dropdown-menu">
                                    @foreach (var subSub in subCat.ChildCategories)
                                    {
                                        <li>
                                            <a class="dropdown-item" href="@Url.Action("Index", "Home", new { categoryId = subSub.Id })">
                                                @subSub.Name
                                            </a>
                                        </li>
                                    }
                                </ul>
                            </li>
                        }
                        else
                        {
                            <li>
                                <a class="dropdown-item" href="@Url.Action("Index", "Home", new { categoryId = subCat.Id })">
                                    @subCat.Name
                                </a>
                            </li>
                        }
                    }
                </ul>
            </li>
        }
        else
        {
            <li class="nav-item">
                <a class="nav-link" href="@Url.Action("Index", "Home", new { categoryId = category.Id })">
                    @category.Name
                </a>
            </li>
        }
    }
</ul>
Creating the Controller

We will modify the HomeController that loads the product listings. On the initial load, a set of random products is shown. When selecting a category, the controller filters and displays only the associated products. So, please modify the Home Controller as follows:

using DynamicMenusApp.Data;
using DynamicMenusApp.Models;
using DynamicMenusApp.ViewModels;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace DynamicMenusApp.Controllers
{
    public class HomeController : Controller
    {
        // Private readonly field for the database context to interact with the database.
        private readonly EcommerceDbContext _context;

        // Constructor that accepts the database context via dependency injection.
        public HomeController(EcommerceDbContext context)
        {
            // Assign the injected context to the private field.
            _context = context; 
        }

        // The main action method for the Index page, with an optional categoryId parameter.
        public IActionResult Index(int? categoryId)
        {
            if (categoryId.HasValue)
            {
                // Create a base query for retrieving products along with their associated category.
                IQueryable<Product> productsQuery = _context.Products
                    .AsNoTracking()
                    .Include(p => p.Category);

                // Retrieve the selected category including its child categories.
                var selectedCategory = _context.Categories
                    .AsNoTracking()
                    .Include(c => c.ChildCategories)
                        .ThenInclude(child => child.ChildCategories)
                    .FirstOrDefault(c => c.Id == categoryId.Value);

                if (selectedCategory != null)
                {
                    // Get all descendant category IDs (child, grandchild, etc.).
                    var descendantIds = GetDescendantCategoryIds(selectedCategory);
                    if (descendantIds.Any())
                    {
                        // Filter products for descendant categories.
                        productsQuery = productsQuery.Where(p => descendantIds.Contains(p.CategoryId));
                    }
                    else
                    {
                        // If the category is a leaf, filter products by the selected category.
                        productsQuery = productsQuery.Where(p => p.CategoryId == categoryId.Value);
                    }
                }

                // Execute the query to get filtered products.
                var filteredProducts = productsQuery.ToList();

                // Return the filtered product list to the FilteredIndex view.
                return View("FilteredIndex", filteredProducts);
            }
            else
            {
                // When no category is selected, display a random subset of products for each top-level category.
                // IMPORTANT: Eagerly load child categories so GetDescendantCategoryIds can find the descendant IDs.
                var topLevelCategories = _context.Categories
                    .AsNoTracking()
                    .Include(c => c.ChildCategories)
                        .ThenInclude(cc => cc.ChildCategories)
                    .Where(c => c.ParentCategoryId == null)
                    .ToList();

                var homeVM = new HomeIndexViewModel();

                foreach (var cat in topLevelCategories)
                {
                    // Retrieve all descendant category IDs for the current top-level category.
                    var catDescendantIds = GetDescendantCategoryIds(cat);
                    catDescendantIds.Add(cat.Id);

                    // Fetch a random subset of 4 products associated with the category or its descendants.
                    var catProducts = _context.Products
                        .AsNoTracking()
                        .Where(p => catDescendantIds.Contains(p.CategoryId))
                        .OrderBy(r => EF.Functions.Random())
                        .Take(4)
                        .ToList();

                    if (catProducts.Any())
                    {
                        homeVM.CategoryProducts.Add(new CategoryProductsViewModel
                        {
                            Category = cat,
                            Products = catProducts
                        });
                    }
                }

                // Return the main view with the HomeIndexViewModel.
                return View(homeVM);
            }
        }

        // Recursive helper method to retrieve all descendant category IDs for a given category.
        private List<int> GetDescendantCategoryIds(Category category)
        {
            // Initialize a list to hold the IDs of descendant categories.
            var ids = new List<int>();

            // Check if the category has any child categories.
            if (category.ChildCategories != null && category.ChildCategories.Any())
            {
                // Iterate over each child category.
                foreach (var child in category.ChildCategories)
                {
                    ids.Add(child.Id); // Add the child's ID to the list.
                    // Recursively call this method to add any further descendant IDs from the child category.
                    ids.AddRange(GetDescendantCategoryIds(child));
                }
            }
            // Return the complete list of descendant category IDs.
            return ids;
        }
    }
}
Main Index View

Modify the Index.cshtml, which is placed under the Views/Home folder, as follows. The Index view expects a HomeIndexViewModel. It loops through each Category and displays a small set of products with a “View More” link that navigates to the filtered listing for that category.

@model DynamicMenusApp.ViewModels.HomeIndexViewModel
@{
    ViewData["Title"] = "Home";
}

<div class="container mt-4">
    <h2 class="mb-4">Browse by Category</h2>
    @foreach (var catProd in Model.CategoryProducts)
    {
        <div class="mb-5">
            <div class="d-flex align-items-center justify-content-between">
                <h3>@catProd.Category.Name</h3>
                <!-- "View More" links to the same Index action with categoryId -->
                <a asp-action="Index" asp-route-categoryId="@catProd.Category.Id" class="btn btn-sm btn-outline-secondary">
                    View More
                </a>
            </div>
            <hr />
            <div class="row">
                @foreach (var product in catProd.Products)
                {
                    <div class="col-lg-3 col-md-4 col-sm-6 mb-4">
                        <div class="card h-100 shadow-sm">
                            <img src="https://picsum.photos/400/200" class="card-img-top" alt="@product.Name" />
                            <div class="card-body">
                                <h5 class="card-title">@product.Name</h5>
                                <p class="card-text">@product.Description</p>
                            </div>
                            <div class="card-footer d-flex justify-content-between align-items-center">
                                <span class="text-primary fw-bold">$@product.Price</span>
                                <a href="#" class="btn btn-outline-primary btn-sm">View Details</a>
                            </div>
                        </div>
                    </div>
                }
            </div>
        </div>
    }
</div>
Explanation
  • We iterate over Model.CategoryProducts, which represents top-level categories and their small product sets.
  • Each category has a “View More” button linking to Index?categoryId=… so users can see the full listing.
  • The design uses Bootstrap’s grid system for a clean, professional layout.
FilteredIndex View

When a user clicks “View More,” it calls View(“FilteredIndex”, filteredProducts). So, create a view named FilteredIndex.cshtml within the Views/Home folder and copy and paste the following code. This view is used to display the full product listing for the chosen category (and any descendants):

@model IEnumerable<DynamicMenusApp.Models.Product>
@{
    ViewData["Title"] = "Filtered Products";
}

<div class="container mt-4">
    <h2 class="mb-4">Products</h2>
    <div class="row">
        @foreach (var product in Model)
        {
            <div class="col-lg-4 col-md-6 mb-4">
                <div class="card h-100 shadow-sm">
                    <img src="https://picsum.photos/400/200" class="card-img-top" alt="@product.Name" />
                    <div class="card-body">
                        <h5 class="card-title">@product.Name</h5>
                        <p class="card-text">@product.Description</p>
                    </div>
                    <div class="card-footer d-flex justify-content-between align-items-center">
                        <span class="text-primary fw-bold">$@product.Price</span>
                        <a href="#" class="btn btn-outline-primary btn-sm">View Details</a>
                    </div>
                </div>
            </div>
        }
    </div>
</div>

Explanations:

  • This view receives a list of Product objects.
  • It displays all products matching the selected category (sub- and sub-sub-categories).
Updating Site.css

Since Bootstrap 5 does not natively support multi‑level dropdowns, add the following CSS to your site’s CSS file (for example, in site.css) to style the nested menus:

/* Multi-Level Dropdown Styling */
.dropdown-submenu {
    position: relative;
}

.dropdown-submenu > .dropdown-menu {
   top: 0;
   left: 100%;
   margin-top: -1px;
   border-top-left-radius: 0;
   border-bottom-left-radius: 0;
   border-left: none;
}

.dropdown-submenu:hover > .dropdown-menu {
   display: block;
}

/* Optional: Hover highlight for submenu items */
.dropdown-menu li > a:hover {
    background-color: #f8f9fa;
    color: #212529;
}
Modify the _Layout View:

Modify the _Layout.cshtml, which is placed under the Views/Shared folder as follows.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - DynamicMenusApp</title>

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <!-- Your site-wide CSS (which will include custom menu styling) -->
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">DynamicMenusApp</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav"
                        aria-controls="mainNav" aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <!-- The main navigation container -->
                <div class="collapse navbar-collapse" id="mainNav">
                    <!-- Renders the view component-->
                    @await Component.InvokeAsync("Menu")
                </div>
            </div>
        </nav>
    </header>

    <div class="container mt-4">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; @DateTime.Now.Year - DynamicMenusApp
            - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>

    <!-- jQuery and Bootstrap Bundle JS -->
    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    @RenderSection("Scripts", required: false)
</body>
</html>

Now, run the application and test the functionalities. It should work as expected.

In the next article, I will discuss How to Implement Auto Page Refresh in ASP.NET Core MVC Application with an example. In this article, I explain How to Implement Dynamic Menus in ASP.NET Core MVC with an Example. I hope you enjoy this article, How to Implement a Dynamic Menus in ASP.NET Core MVC.

Leave a Reply

Your email address will not be published. Required fields are marked *