65.9K
CodeProject 正在变化。 阅读更多。
Home

MVC 6 动态导航菜单(来自数据库)

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.45/5 (15投票s)

2019 年 8 月 26 日

CPOL

4分钟阅读

viewsIcon

64531

downloadIcon

3123

MVC 6 .NET Core 动态导航菜单来自数据库

目录

目标

几年前,我不得不从数据库加载导航菜单并使用 Web Forms 创建菜单控件,因此从数据库加载菜单数据的主要思想是根据用户角色进行过滤。最终结果是,我们将获得按角色过滤的数据。在这里,我们必须使用 ASP.NET Core 2.2 MVC 应用程序来完成此操作。

引言

我在 MVC 6 .NET Core 中遇到了这个需求,需要从数据库动态生成基于角色的导航菜单,以便用于浏览网站和管理面板,为应用程序分配角色、权限和其他维护工作。在此系统中,角色数量有限,因此基于角色的授权可能很合适。

使用的组件

以下是构建和测试提供的演示代码所需的组件。

创建 Web 项目

在 Visual Studio 2019 中创建您的 Web 应用程序。

将语言选择为 C#,项目类型选择为 Web,然后选择第一个模板 ASP.NET Core Web 应用程序,然后单击“下一步”。

提供一个项目名称并选择物理路径,然后单击“创建”。

选择 Web 应用程序 (Model-View-Controller),然后单击右侧“身份验证”下方的“更改”按钮。之后,选择“Individual User Accounts”,点击“确定”关闭弹出窗口,然后点击“创建”。

现在项目已设置好并且可以运行,但我们还没有根据模型创建数据库,因此首先我们需要在 `appsettings.json` 文件中更改连接字符串。我将使用本地主机作为我的服务器,并使用 Windows 身份验证,以下是我的连接字符串。

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

但是,如果我们在此级别创建数据库,我们只会得到 `Identity` 表,如下所示:

但在我们的例子中,我们需要另外两个表,我们将通过定义它们的实体来创建它们,然后将它们添加到我们的 `context` 类中。

    [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

现在我们需要运行迁移,然后更新数据库,**Enable-Migrations 命令已被弃用**,因此我们需要删除 `Migrations` 文件夹中的所有内容,然后运行 `add migration` 命令。

add-migration InitialVersion

这将在 `Migrations` 文件夹中创建一些文件,然后我们需要运行 `update-database` 命令,如果您的连接字符串正确,它将像下面这样创建您的数据库:

有关填充数据的更多详细信息,您可以查看我的另一篇文章。

对于当前场景,我们的种子数据将包含所有导航菜单项、用户、角色和权限。因此,它会更复杂一些。

现在我们的数据库已经就绪,并且所有实体都已创建,所以让我们在开发环境中运行应用程序,它将把种子数据插入数据库。

数据服务

我们将创建一个数据服务来与数据库通信,它非常简单,有一个主要函数 `GetMenuItemsAsync`,该函数在按角色过滤后返回导航菜单视图模型。

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;
     }
}

我们也需要将此服务注册到 `Startup.cs` 中,以便依赖注入可以为其提供服务。可以这样注册:

services.AddScoped<IDataAccessService, DataAccessService>();

导航菜单

我们将使用 View Component 来加载导航菜单作为部分视图。

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);
    }
}

在 Views 的 Shared 文件夹中创建一个 `Components` 文件夹。然后在 Components 中,我们可以创建一个 `NavigationMenu` 文件夹,然后创建一个 `Default.cshtml` 视图文件。这里,这种层次结构对于它正常工作非常重要。

这是部分视图 HTML,在这里我们将菜单限制为仅 2 级,它可以通过递归扩展到 N 级,但为了在此处限制,我们将不使用它。

@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>

现在我们将创建一个名为 `Administration` 的控制器,其中包含两个操作:`Roles` 和 `Users`。

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()
     {
            ........
     }
}

在控制器之后,我们将为这些操作创建视图,以便我们可以分别显示 `Roles` 和 `Users` 的列表。

让我们再次启动应用程序,它看起来会是这样,对于任何访问者,页面看起来是这样的,但它会根据分配给用户的角色加载额外的菜单项。

让我们用用户 admin@test.com 登录。现在页面看起来像这样,根据他们分配的角色,管理员可以看到额外的菜单项。

所以,这是登录后使用部分视图绘制的菜单。

下一步

现在我们有一个问题,如果有人知道页面的 URL,例如 https:///administration/roles,他们仍然可以访问该页面。接下来,我们将看看如何进行基于角色的授权。

结论

通过迁移创建数据库并在开发环境中启动项目后,我们已经实现了从数据库创建导航菜单的目标。已登录用户会看到根据其角色的菜单项。源代码已附上。我鼓励您运行并查看。如果您有任何问题/建议,欢迎大家发表评论。感谢阅读。

历史

  • 2019 年 8 月 26 日:初始版本
© . All rights reserved.