Jun 5, 2012

Implementing Role Based Menu in ASP.NET MVC 4

In my previous post, I explained how to implement custom role provider, authorization and role based navigation on successful login in asp.net mvc 4. In this post, We’ll implement role based menu.


In default template of asp.net mvc 4.0, Layout.cshtml has following code for menu:

<nav>
                        <ul id="menu">
                            <li>@Html.ActionLink("Home", "Index", "Home")</li>
                            <li>@Html.ActionLink("About", "About", "Home")</li>
                            <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
                        </ul>
                    </nav>

Which is hard-coded for all users. We’ll create partial view for menu to reduce complexity.

Right click on Shared folder in Views > select Add > click View
Enter Name: _Menu and set “Create as a partial view” option true > click Add
It’ll open blank cshtml page.

Define Menu Items:

In normal asp.net webforms app, Sitemap file is used to define structure. Here we’ll define menu details in partial view.

@{
    var menus = new[]
                {
                   new { LinkText="Home", ActionName="Index",ControllerName="Home",Roles="All"  },
                   new { LinkText="About", ActionName="About",ControllerName="Home",Roles="Anonymous"  },
                   new { LinkText="Contact", ActionName="Contact",ControllerName="Home",Roles="Anonymous"  },
                   new { LinkText="Dashboard", ActionName="Index",ControllerName="Dealer",Roles="Dealer"  },
                   new { LinkText="Dashboard", ActionName="Index",ControllerName="Admin",Roles="Administrator"  },
                   new { LinkText="Administration", ActionName="GetUsers",ControllerName="Admin",Roles="Administrator"  },
                   new { LinkText="My Profile", ActionName="GetDealerInfo",ControllerName="Dealer",Roles="Dealer,PublicUser"  },
                   new { LinkText="Products", ActionName="GetProducts",ControllerName="Product",Roles="Dealer,Administrator"  },
                   new { LinkText="Search", ActionName="SearchProducts",ControllerName="Product",Roles="PublicUser,Dealer,Administrator"  },
                   new { LinkText="Purchase History", ActionName="GetHistory",ControllerName="Product",Roles="PublicUser"  },
                   
                };
}  

In above code, An Array of anonymous object having LinkText, ActionName, ControllerName, Roles properties is used.
I’ve given some additional roles which doesn’t belong to user roles like:

All : To display link for both authenticated or anonymous users
Anonymous: To display link for unauthenticated users

Get Role Based Links:

We’ve to get links from above menu structure with following points:
1. If user is not authenticated, Show links having All or Anonymous role.
2. If user is authenticated and has single role, Show links having All or user-role role.
3. If user is authenticated and has multiple roles, show links having All or ANY user-role role.

Note: A user may have multiple roles and in menu structure, a link may have multiple roles.

<ul id="menu">
@if (HttpContext.Current.User.Identity.IsAuthenticated)
{
    String[] roles = Roles.GetRolesForUser();
    var links = from item in menus
                where item.Roles.Split(new String[] { "," }, StringSplitOptions.RemoveEmptyEntries)
                .Any(x => roles.Contains(x) || x == "All")
                select item;
    foreach (var link in links)
    {
        @: <li> @Html.ActionLink(link.LinkText, link.ActionName,link.ControllerName)</li>
    }
}
else{
    var links = from item in menus
                where item.Roles.Split(new String[]{","},StringSplitOptions.RemoveEmptyEntries)
                .Any(x=>new String[]{"All","Anonymous"}.Contains(x))      
                select item;
     foreach ( var link in links){     
         @: <li> @Html.ActionLink(link.LinkText, link.ActionName, link.ControllerName)</li>         
     }
}
</ul> 

In above code, Linq is used to get links for both authenticated and unauthenticated user in partial view. It will get links and makes ul li structure.

Add Menu:

Now, we have to use this partial view in layout page. If we directly call this, It will be called in each request and the entire logic is executed again and again. So we’ll use session to store HTML string of menu and use session to display menu for further request.

In _Layout.cshtml page, replace nav tag with following structure:

					<nav>
                         @if (Session["MyMenu"] == null){
                            Session["MyMenu"] = Html.Partial("~/Views/Shared/_Menu.cshtml");
                          } 
                         @Session["MyMenu"]
                    </nav>

We’ve to clear session in login and logout actions.

In Login action, clear it just after setting FormsAuthentication cookies:

 FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
 Session["MyMenu"] = null;

and in LogOff action:

public ActionResult LogOff()
        {
            FormsAuthentication.SignOut();
            Session["MyMenu"] = null;
            return RedirectToAction("Index", "Home");
        }

Output

See following output of above menu structure for different roles in asp.net mvc default template.

role-based-menu-asp-mvc

Hope, It helps. Share your opinion about how you are implementing role based navigation in ASP.NET MVC.

20 comments

  1. I have the following error: The name ‘menus’ does not exist in the current context
    ¿Can anybody help me please?

  2. I am following your solution here to do my project but I am having an issue fitting a dropdown into it. Below is my menu.I am having an issue converting it to the new { LinkText..} as you have. Please can you show me how to change the menu below to fit your example above?

    @Html.ActionLink(“Home”, “Index”, “Home”)
    @Html.ActionLink(“About”, “About”, “Home”)

    Reports

    @Html.ActionLink(“Admin Report”, “Index”, “Admin Report”)
    @Html.ActionLink(“User’s Report”, “Index”, “User’s Report”)

  3. where do you put this?….model?view?controller?
    Get Role Based Links:
    We’ve to get links from above menu structure with following points:
    1. If user is not authenticated, Show links having All or Anonymous role.
    2. If user is authenticated and has single role, Show links having All or user-role role.
    3. If user is authenticated and has multiple roles, show links having All or ANY user-role role.
    Note: A user may have multiple roles and in menu structure, a link may have multiple roles.

    @if (HttpContext.Current.User.Identity.IsAuthenticated)
    {
    String[] roles = Roles.GetRolesForUser();
    var links = from item in menus
    where item.Roles.Split(new String[] { “,” }, StringSplitOptions.RemoveEmptyEntries)
    .Any(x => roles.Contains(x) || x == “All”)
    select item;
    foreach (var link in links)
    {
    @: @Html.ActionLink(link.LinkText, link.ActionName,link.ControllerName)
    }
    }
    else{
    var links = from item in menus
    where item.Roles.Split(new String[]{“,”},StringSplitOptions.RemoveEmptyEntries)
    .Any(x=>new String[]{“All”,”Anonymous”}.Contains(x))
    select item;
    foreach ( var link in links){
    @: @Html.ActionLink(link.LinkText, link.ActionName, link.ControllerName)
    }
    }

    In above code, Linq is used to get links for both authenticated and unauthenticated user in partial view. It will get links and makes ul li structure.

  4. Nice article. Thank you for sharing. I thing you can store generated menu to some static property and it should be more efficient over session (will be generated just once for all users).

  5. Hello,

    I am very new to MVC. I’ve a doubt in this blog u have mentioned ,

    Login action and logoff action. u could u plz bref me where should i really give the codes menthioned under

    @if (HttpContext.Current.User.Identity.IsAuthenticated)

    , login action and log off action. I m stuk with this part.plz help me to proceed.

  6. I keep getting an error that CS0103: The name ‘menus’ does not exist in the current context. Is there something missing in the code? If I paste the _Menu.cshtml directly into the _Layout.cshtml it works fine. I have the session code in place. And then just underneath, outside the I place the code.

  7. Building on the code by Mcshaz, I wanted to store the menus out of the View, so I modified his code as follows:

    Added a static class with the menu definitions
    Added a new method to RollMenu to return the menu items the current user is allowed to use
    Fixed the selection code in the RollMenu.Add() method.

    Modify the namespace to what you are using in your project. (I used Test)

    Add to the top of ViewsShared_Layout.cshtml. This constructs the menu items the current user is allowed to use.

    @{
    Test.ViewModels.RoleMenu menu = new Test.ViewModels.RoleMenu().GetAllowedMenus();
    }

    Farther down in the file, modify the code that displays the menu items:

    @foreach (Test.ViewModels.RoleMenuItem link in menu)
    {
    @Html.ActionLink(link.LinkText, link.ActionName, link.ControllerName)
    }
    @* @Html.ActionLink(“Home”, “Index”, “Home”)
    @Html.ActionLink(“About”, “About”, “Home”)
    @Html.ActionLink(“Contact”, “Contact”, “Home”)
    *@

    Add new class file, ViewModelsRoleMenuItems.cs.
    The public static class AllMenuItems contains the definitions of all top-level menus. Modify this list as you add or change menu items.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using WebMatrix.WebData;
    using System.Web.Security;
    using System.Data.Entity;
    using Test.Models;
    using Test.DAL;
    using System.Collections;

    namespace Test.ViewModels
    {
    public static class AllMenuItems
    {
    public static string temp;
    public static RoleMenuItem[] AllMenus =
    {
    new RoleMenuItem(“Home”, “Index”, “Home”, “Administrator, User”),
    new RoleMenuItem(“About”, “About”, “Home”, “Administrator”),
    new RoleMenuItem(“Contact”, “Contact”, “Home”, “Administrator, User”)
    };
    }

    public class RoleMenuItem : MenuItem
    {
    public RoleMenuItem() { }
    public RoleMenuItem(string linkText, string actionName, string controllerName, string roleNames)
    {
    LinkText = linkText;
    ActionName = actionName;
    ControllerName = controllerName;
    RoleNames = roleNames;
    }
    public string RoleNames
    {
    set
    {
    Roles = Array.ConvertAll(value.Split(new String[] { “,” }, StringSplitOptions.RemoveEmptyEntries), p => p.Trim());
    }
    }
    internal string[] Roles;
    }

    public class MenuItem
    {
    public string LinkText { get; set; }
    public string ActionName { get; set; }
    public string ControllerName { get; set; }
    }

    public class RoleMenu : System.Collections.Generic.IEnumerable
    {
    private readonly List _roleMenuItems = new List();
    private readonly string[] _userRoleNames;
    public readonly bool _isAuthenticated;
    public RoleMenu()
    {
    if (_isAuthenticated = WebSecurity.IsAuthenticated)
    {
    _userRoleNames = Roles.GetRolesForUser();
    }
    }
    public RoleMenu(string userName)
    {
    if (_isAuthenticated = WebSecurity.IsAuthenticated)
    {
    _userRoleNames = Roles.GetRolesForUser(userName);
    }
    }
    public RoleMenu Add(RoleMenuItem menuItem)
    {
    string[] menuRoles = menuItem.Roles;
    IEnumerable userMenuRoles = (_isAuthenticated ? menuRoles.Intersect(_userRoleNames) : new string[] { });
    if (
    menuRoles.Contains(“All”) ||
    (!_isAuthenticated && menuRoles.Contains(“Anonymous”)) ||
    (_isAuthenticated && userMenuRoles.Count()>0 )
    )
    {
    _roleMenuItems.Add(menuItem);
    }
    return this;
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
    return GetEnumerator();
    }
    public System.Collections.Generic.IEnumerator GetEnumerator()
    {
    return _roleMenuItems.GetEnumerator();
    }
    public IEnumerable ItemsForRole(string roleName)
    {
    return _roleMenuItems.Where(r => r.Roles.Contains(roleName));
    }
    ///
    /// Assembles allowed Menu Items for Current User
    ///
    ///
    public RoleMenu GetAllowedMenus()
    {
    _roleMenuItems.Clear();
    foreach (RoleMenuItem mi in AllMenuItems.AllMenus)
    {
    this.Add(mi);
    }
    return this;
    }
    }
    }

  8. great stuff, can you please guide me how to create submenu.Example like for Administrator, in product on mouseover should display “Add new Product” and “list of products”.

  9. Excellent Job. Do you have sample to include option/suboptions, option/suboptions/1option or
    option/suboptions/1option/suboptions?
    Thank you.

  10. Great stuff -there aren’t enough examples of approaches to this ubiquitous implementation problem for MVC. The only comments are – why not move the logic to a model rather than the view (and use strong typing).

    using your code,
    namespace MyNameSpace.ViewModels
    {
    public class RoleMenuItem: MenuItem
    {
    public RoleMenuItem(){}
    public RoleMenuItem(string linkText, string actionName, string controllerName, string roleNames)
    {
    LinkText = linkText;
    ActionName = actionName;
    ControllerName = controllerName;
    RoleNames = roleNames;
    }
    public string RoleNames { set { Roles = value.Split(new String[] { “,” }, StringSplitOptions.RemoveEmptyEntries); } }
    internal string[] Roles;
    }
    public class MenuItem
    {
    public string LinkText { get; set; }
    public string ActionName { get; set; }
    public string ControllerName { get; set; }
    }
    public class RoleMenu : System.Collections.Generic.IEnumerable
    {
    private readonly List _roleMenuItems = new List();
    private readonly string[] _userRoleNames;
    public readonly bool _isAuthenticated;
    public RoleMenu()
    {
    if (_isAuthenticated = WebSecurity.User.Identity.IsAuthenticated)
    {
    _userRoleNames = Roles.GetRolesForUser();
    }
    }
    public RoleMenu(IDataContext context, string userName)
    {
    if (_isAuthenticated = WebSecurity.User.Identity.IsAuthenticated)
    {
    User usr = context.Users.FirstOrDefault(Usr => Usr.UserName == userName) ;
    _userRoleNames = (usr==null)? new string[0]: usr.Roles.Select(r => r.RoleName).ToArray();

    }
    }
    public RoleMenu Add(RoleMenuItem menuItem)
    {
    string[] menuRoles = menuItem.Roles;
    if (
    menuRoles.Contains(“All” ) ||
    (!_isAuthenticated && menuRoles.Contains(“Anonymous”)) ||
    (_isAuthenticated && (menuRoles.Contains(“Authenticated”) || menuRoles.Union(_userRoleNames).Any()))
    )
    {
    _roleMenuItems.Add(menuItem);
    }
    return this;
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
    return GetEnumerator();
    }
    public System.Collections.Generic.IEnumerator GetEnumerator()
    {
    return _roleMenuItems.GetEnumerator();
    }
    public IEnumerable ItemsForRole(string roleName)
    {
    return _roleMenuItems.Where(r => r.Roles.Contains(roleName));
    }
    }
    }

    then implementation in the view becomes:
    @{
    var menu = new RoleMenu().Add(new RoleMenuItem(“Home”, “Index”, “Home”,”All”))
    .Add(new RoleMenuItem(“About”, “About”, “Home”,”All”))
    .Add(new RoleMenuItem(“Part A”, “PartA”, “Home”,”Authenticated”))
    .Add(new RoleMenuItem(“Part B”, “PartB”, “Home”,”Anonymous”))
    .Add…;
    foreach (var link in menu)
    {
    @: @Html.ActionLink(link.LinkText, link.ActionName,link.ControllerName)
    }
    }

  11. Why do you want to store Menu in Session? Why not just make render it everytime?

    Something like:

         @Html.Partial(“~/Views/Shared/_Menu.cshtml”) However, the solution that you have provided is elegant. Thank you very much. I was looking for solution something like this.

    1. It is better to store it in the session so you don’t have to spend resources to process it on every request. With storing it in the session, you only have to pay processing costs once.

Leave a Reply

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