MVC 6 动态导航菜单(来自数据库)
MVC 6 .NET Core 动态导航菜单来自数据库
目录
目标
几年前,我不得不从数据库加载导航菜单并使用 Web Forms 创建菜单控件,因此从数据库加载菜单数据的主要思想是根据用户角色进行过滤。最终结果是,我们将获得按角色过滤的数据。在这里,我们必须使用 ASP.NET Core 2.2 MVC 应用程序来完成此操作。
引言
我在 MVC 6 .NET Core 中遇到了这个需求,需要从数据库动态生成基于角色的导航菜单,以便用于浏览网站和管理面板,为应用程序分配角色、权限和其他维护工作。在此系统中,角色数量有限,因此基于角色的授权可能很合适。
使用的组件
以下是构建和测试提供的演示代码所需的组件。
- 如果您没有 Professional 或 Enterprise 版本,请下载最新的 Visual Studio 2019 Community 版。
- 我使用的是 SQL Server Developer Edition 17.9.1,您可以从以下链接下载。
创建 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 日:初始版本