Updating the Menu System to ASP.NET Core 3.1 with Microsoft Identity
The menu example needs an upgrade. Today, we update our menu system to work with ASP.NET Core 3.1 and Microsoft Identity's Claims and Entity Framework Core
No matter what web system you build, you will always need a navigation or menu system of some kind for your users which I demonstrated in the past with my Menu System series.
I've had readers using this technique in their apps (BTW, thank you all) and recently had a reader ask about updating it since Core 3.1 is now considered an LTS.
After looking over the series of posts, I examined the date.
2017?!?! Wow! Three years ago.
That means my code example on Github was getting a little stale.
So yeah, it was definitely time for an update.
The Menu System Series was posted back in 2017 and includes the following posts:
Complete Overhaul: What changed since 2017?
The goal for this project was to build a generic menu system to be used across multiple systems.
The only thing changing was the menu data. With menu data, you can easily present a horizontal, vertical, or even hierarchical menu for your users on the front-end.
In the Menu System posts, I used the following:
- ASP.NET MVC 5.2
- HtmlHelpers
- Entity Framework 6
- Microsoft Identity 2.2
With the latest ASP.NET Core 3.1, we'll replace a majority of technologies in this post with some 3.1 goodness including:
- ASP.NET Core MVC 3.1
- ViewComponents
- Entity Framework Core 3.1
- Migrations
- Microsoft Identity 3.1
This is a dense post with a lot of new techniques, so let's dive into it.
Database Recap
While the database structure hasn't changed, there were some enhancements for authorization (refresher: authentication is identifying who the user is and authorization is what resources does the user have access to).
Database Schema
We still have our four tables to augment Identity for authorization purposes.
- AspNetMenu - Contains our menu items for our application
- AspNetRoleMenu - Junction table to connect roles to menus
- MenuPermission - Store the permissions for each AspNetRoleMenu Id; what permissions do they have for this menu?
- Permission - Lookup table of permissions with Id/Name (i.e. 1=Create, 2=Update)
With these tables, we can cover any type of authorization necessary by using either:
- AspNetRoleClaims - Authorizations based on Roles
- AspNetUserClaims - Authorizations based on a specific user
The good news about these tables is when a user is successfully authenticated, it automatically loads the AspNetUserClaims into the profile making it extremely easy for us to authorize resources to the user.
With the database taken care of, we can now focus on extending Identity to access these tables.
Extending Microsoft Identity
Microsoft Identity has grown a lot over the three years and has become easier to integrate into your existing applications for authentication and authorization.
If you look back at the menu integrated with claims, you'll notice I included additional classes to make Identity work. With the latest version in Core 3.1, it's even easier.
When extending Microsoft Identity, only include the classes you want changed. For example, we are extending the IdentityRole (ApplicationRole) and IdentityUser (ApplicationUser). We won't have a need to touch anything else. The untouched Identity classes under the covers work as expected by default.
The only two Identity classes requiring a change is the ApplicationRole and ApplicationUser.
Identity\ApplicationRole.cs
public class ApplicationRole : IdentityRole<string> { public ApplicationRole() { } public ApplicationRole(string name) : this() { this.Name = name; RoleMenus = new HashSet<ApplicationRoleMenu>(); }
public ICollection<ApplicationRoleMenu> RoleMenus { get; set; } }
Identity\ApplicationUser.cs
public class ApplicationUser: IdentityUser<string> { }
You'll notice we have a new collection called RoleMenus so we need to define this class.
Identity\ApplicationRoleMenu.cs
[Table("AspNetRoleMenu")] public class ApplicationRoleMenu { public ApplicationRoleMenu() { Permissions = new HashSet<MenuPermission>(); }
[Key, Column(Order=1), DatabaseGenerated(DatabaseGeneratedOption.Identity)] public virtual int Id { get; set; }
[Column(Order = 2)] public virtual string RoleId { get; set; }
[Column(Order = 3)] public virtual int MenuId { get; set; }
public virtual ApplicationRole Role { get; set; } public virtual MenuItem MenuItem { get; set; } public ICollection<MenuPermission> Permissions { get; set; }
}
Along with the RoleMenus, we have our MenuItem, MenuPermission, and Permission classes to include.
Models\MenuItem.cs
[Table("AspNetMenu")] public class MenuItem { [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")] public MenuItem() { Children = new HashSet<MenuItem>(); RoleMenus = new HashSet<ApplicationRoleMenu>(); }
[Key, Required, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; }
[Required] [StringLength(50)] public string Title { get; set; }
[StringLength(100)] public string Description { get; set; }
public int? ParentId { get; set; }
[StringLength(50)] public string Icon { get; set; }
[StringLength(50)] public string Url { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] public virtual ICollection<MenuItem> Children { get; set; }
public virtual MenuItem ParentItem { get; set; }
public virtual ICollection<ApplicationRoleMenu> RoleMenus { get; set; }
}
Identity\MenuPermission.cs
public class MenuPermission { [Key, Column(Order = 1)] public virtual int RoleMenuId { get; set; }
[Key, Column(Order = 2)] public virtual int PermissionId { get; set; }
public virtual Permission Permission { get; set; } public virtual ApplicationRoleMenu RoleMenu { get; set; } }
Identity\Permission.cs
public class Permission { [Key, Required] public int Id { get; set; }
[Required] [StringLength(50)] public string Name { get; set; }
public virtual ICollection<MenuPermission> MenuPermissions { get; set; } }
Our ApplicationDbContext is next. Our onModelCreating
method creates the relationships tying these entities together through common properties, but branching off from the ApplicationRole table.
Identity\ApplicationDbContext.cs
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string> { public DbSet<MenuItem> MenuItems { get; set; } public DbSet<MenuPermission> MenuPermissions { get; set; } public DbSet<Permission> Permissions { get; set; } public DbSet<ApplicationRoleMenu> RoleMenus { get; set; }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); // Additional tables.
builder.Entity<MenuItem>(item => { item.ToTable("AspNetMenu"); item.HasMany(y => y.Children) .WithOne(r => r.ParentItem) .HasForeignKey(u => u.ParentId);
item.HasMany(t => t.RoleMenus) .WithOne(u => u.MenuItem) .HasForeignKey(r => r.MenuId) .OnDelete(DeleteBehavior.NoAction); });
builder.Entity<ApplicationRoleMenu>(roleMenu => { roleMenu.ToTable("AspNetRoleMenu");
roleMenu.HasOne(o => o.Role) .WithMany(u => u.RoleMenus) .HasForeignKey(e => e.RoleId) .OnDelete(DeleteBehavior.NoAction);
roleMenu.HasOne(o => o.MenuItem) .WithMany(u => u.RoleMenus) .HasForeignKey(e => e.MenuId) .OnDelete(DeleteBehavior.NoAction); });
builder.Entity<MenuPermission>(mp => { mp.ToTable("MenuPermission");
mp.HasKey(l => new {l.RoleMenuId, l.PermissionId});
mp.HasOne(o => o.Permission) .WithMany(i => i.MenuPermissions) .IsRequired();
mp.HasOne(o => o.RoleMenu) .WithMany(i => i.Permissions) .IsRequired(); });
builder.Entity<Permission>(mp => { mp.ToTable("Permission");
mp.HasKey(l => l.Id);
mp.HasMany(o => o.MenuPermissions) .WithOne(i => i.Permission) .HasForeignKey(y => y.PermissionId); }); } }
As an optional step, you may want to seed the database with default values (as I've done in the project with .HasData()
).
Adding Migrations
Once we have our database defined through our ApplicationDbContext using a Code-First model, we can now build the database in 3 steps.
- Package Manager Console - Open the Package Manager Console under View > Other Windows
- Add a Migration - Type:
Add-Migration Initial
(or whatever you want to call the first version of your migration). This will create your first migration in the Migrations folder including the code to create the tables defined in your ApplicationDbContext. - Create the Database - Type
Update-Database
to create your database. IMPORTANT: Confirm you have the correct connection string in yourappsettings.json
.
At this point, you should have a database structure ready to go (and also seeded if you added the HasData method).
If you made a mistake and want to reset everything, the process is simple.
- Drop it! - In the Package Manager Console, type:
Drop-Database
. This reads the connectionstring in the appsettings.json and removes the database. - Remove Migrations - Type:
Remove-Migration
and this will remove your Migrations folder with everything in it.
Make your changes to your Identity entities and re-run steps 2-3 above to see if everything worked properly.
Upgrading from HtmlHelpers
I thought HtmlHelpers were really awesome because they hide business logic from Views.
Next, TagHelpers appeared when ASP.NET Core 1.0 was released. I had some fun with those as well (i.e. Tag Helpers for Image Layouts and Bootstrap 4 with ASP.NET Core TagHelpers).
The latest technology in ASP.NET Core I'm focusing on are ViewComponents. ViewComponents are a mix between partials and TagHelpers. They are specifically meant to render a chunk of HTML and use a version of controllers to render the content.
Since we are just rendering HTML, ViewComponents makes the most sense.
For this demonstration, I made a Horizontal Menu View Component (ViewComponents\HorizontalMenuViewComponent.cs
and Views\Shared\Components\HorizontalMenu\Default.cshtml
).
ViewComponent\HorizontalMenuViewComponent.cs
public class HorizontalMenuViewComponent: ViewComponent { private readonly IMenuService _service;
public HorizontalMenuViewComponent(IMenuService service) { _service = service; }
public IViewComponentResult Invoke() { var menuViewModel = new MenuViewModel { MenuItems = _service.GetMenuByUser(User) };
return View(menuViewModel); } }
As you can see, the ViewComponent is powered by the MenuService which is dependency injected. It also looks at the user's claims as to what menu each user has authorization to view.
Services\MenuService.cs
public class MenuService : IMenuService { private readonly ApplicationDbContext _context; private readonly UserManager<ApplicationUser> _manager;
public MenuService(ApplicationDbContext context, UserManager<ApplicationUser> manager) { _context = context; _manager = manager; }
public List<MenuItem> GetMenuByUser(IPrincipal user) { if (user == null) { return new List<MenuItem>(); }
var principal = user as ClaimsPrincipal;
var id = _manager.GetUserId(principal);
var viewableItems = principal.Claims .Where(e => e.Value == "View") .Select(item => item.Type) .ToList();
var result = _context.MenuItems .Where(item => viewableItems.Any(u => item.Id.ToString() == u)) .ToList();
return result; } }
Since the User already contains claims when they log in, these claims are automatically carried around with the user's identity so you can check these claims for other authorizations against other system resources.
This technique could be used to enable or disable a toolbar buttons, pages, or even grids. The security can expand from feature to feature by adding additional claims for specific functions throughout your app.
Currently, I have the following authorizations defined in the Permission
table: View, Create, Update, Delete, Upload, and Publish.
Final Notes
A couple of notes regarding this approach:
- If you have a lot of permissions, you may want to move towards a more practical approach like using aggregated permissions with enumerated types. Each menu item "claim" will contain a number in the value spot allowing you to check the number against permissions.
- With this technique, you have the ability to authorize by role or by individual. This makes your application's security extremely flexible.
- While the Horizontal Menu is a simple ViewComponent, it is completely self contained and portable. The only thing it requires is an Identity (which is always available if they are logged in) and a MenuService (which is dependency-injected). The MenuService can be swapped out and replaced with something else making the app more modular.
- You could inherit from the
AuthorizeAttribute
, implement the claims check against the menu URL, and act on it on a page-by-page basis.
Check out the GitHub Repository. I have two users ("Morning Sam...Morning Ralph") seeded when you perform the EF Migration.
- Ralph - He's an Admin [ UN: ralph@gmail.com / PW: Password123! ]
- Sam - He's a super user, but only has access to one menu item [ UN: sam@gmail.com / PW: Password456! ]
Conclusion
We took an older implementation of a menu system and upgraded to it ASP.NET Core 3 using MS Identity with claims, ViewComponents, and Entity Framework Core Migrations.
This post took a little while, but feel it will benefit people looking to implement a menu system, use middleware, seed a database using entity framework, or implement ViewComponents in MVC Core.
If I missed something, let me know.
Do you have a menu system? Can I improve on this? Post your comments below and let's discuss.