Custom Tag Helpers in dotnet MVC
Ok, so every project you come across, especially when you get people logging in with various roles and responsibilities, you eventually get the request to have things conditionally show or hide based on those roles. Traditionally, I would have just used some good old if
statements in the View
(what fun is that). I poked around for a cooler solution and came across Tag Helpers
.
I had seen this before: <a class="nav-link" asp-controller="Account" asp-action="SignOut">Logout</a>
so I thought it was worth a try. What I wanted was a way to explicitly hide or show a menu item, a button, a link or any DOM element really for certain users.
Since I already had the user and role management pieces sorted out, and some constants
for these roles, I figured I could pass them in to my tag helper
to denote which one(s) to show or hide for.
PermissionConstants.cs
public static class PermissionConstants
{
public static class Job
{
public const string Create = "JobCreate";
public const string Read = "JobRead";
public const string Update = "JobUpdate";
public const string Deactivate = "JobDeactivate";
}
public static class User
{
public const string Create = "UserCreate";
public const string Read = "UserRead";
public const string Update = "UserUpdate";
public const string Deactivate = "UserDeactivate";
public const string ManageRoles = "UserManageRoles";
}
}
The above roles
were associated with a user and acquired by doing a _userService.GetCurrentUserPermissionsAsync()
, of course with some proper caching so we weren’t hammering a database with every instance of the tag helper.
ShowForTagHelper.cs
[HtmlTargetElement(Attributes="show-for-permission")]
[HtmlTargetElement(Attributes="hide-for-permission")]
public class ShowForTagHelper : TagHelper
{
private readonly IUserService _userService;
public ShowForTagHelper(IUserService userService)
{
_userService = userService;
}
/// <summary>
/// Show this html element for a particular permission type
/// </summary>
[HtmlAttributeName("show-for-permission")]
public string ShowForPermission { get; set; }
/// <summary>
/// Hide this html element for a particular permission type
/// </summary>
[HtmlAttributeName("hide-for-permission")]
public string HideForPermission { get; set; }
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
base.Process(context, output);
var currentUserPermissionsAll = await _userService.CurrentUserPermissionsAsync();
if (currentUserPermissionsAll != null && currentUserPermissionsAll.Any())
{
var currentUserPermissions = currentUserPermissionsAll.Where(x => x.Value).Select(p => p.Key);
bool showForUser = (ShowForPermission != null) ? currentUserPermissions
.Intersect(_getPermissionsList(ShowForPermission)).Any() : true;
bool hideForUser = (HideForPermission != null) ? currentUserPermissions
.Intersect(_getPermissionsList(HideForPermission)).Any() : false;
if (!showForUser || hideForUser)
{
output.SuppressOutput();
//since at this point the output is suppressed, it doesn't pay to continue checking...
}
}
else
{
// if attribute exists, but user has no roles: hide
output.SuppressOutput();
}
}
private IEnumerable<string> _getPermissionsList(string permAttr)
{
return permAttr.Split(',');
}
}
A quick unpacking of the above:
_userService
is injected into the constuctor- two distinct attributes defined
HtmlTargetElement
decorator associates the attributes to the helperHtmlAttributeName
associates the attributes to to concrete properties
- override the
ProcessAsync
method to do your thingoutput.SuppressOutput()
is the magic that hides this from ever being rendered
The last thing we need to do before we actually use this in markup is to register the namespace in the _ViewImports.cshtml
:
ViewImports.cshtml
@using YourNamespace
@using StackExchange.Profiling
@addTagHelper *, MiniProfiler.AspNetCore.Mvc
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, SmartBreadcrumbs
@addTagHelper *, YourNamespace.Helpers
Yes, I know, there are a few other tag helpers
registered, but for good reason: I love the MiniProfiler
and use it on my local/dev/test instances as a sanity check and the SmartBreadcrumbs
is handy for generating a breadcrumb nav by controller
and action
auto-magically..
SomeMarkup.cshtml
<!-- other markup, then an action link that only shows for people who have
the roles needed to view or update a Job -->
<a class="nav-link" asp-area="" asp-controller="Jobs" asp-action="Index"
show-for-permission="@(PermissionConstants.Jobs.Read +","+PermissionConstants.Jobs.Update)">Profile</a>
<!-- more arbitrary markup, then an entire div hidden based on role -->
<div show-for-permission="@(PermissionConstants.User.Read +","+PermissionConstants.User.Update)">
<!-- cool things only a user with User read/update perms can see or do -->
</div>
Hopefully the above shows you a real world case for using a custom TagHelper
. Not only does it seem pretty clean and heavily reusable, but honestly it feels like it separates the concern of hiding or showing content based on a users permissions to a separate handler (almost AOP-esq, don’t quote me on that tho!).