Sign in to track progress and bookmarks.
Built-in Tag Helpers cover forms, links, and caching. But what about your own components — an <alert> tag that renders Bootstrap alerts, a <gravatar> tag that generates profile images, or an <authorize> tag that hides content from unauthorized users? Custom Tag Helpers let you create your own HTML elements with server-side intelligence.
A Custom Tag Helper is a C# class that inherits from TagHelper and overrides the Process() or ProcessAsync() method. When the Razor engine encounters a matching HTML element, it invokes your class to transform, augment, or completely replace the rendered HTML output.
The class name determines the HTML tag. ASP.NET Core converts PascalCase to kebab-case:
AlertTagHelper → <alert>
GravatarImageTagHelper → <gravatar-image>
EmailSendButtonTagHelper → <email-send-button>
Or use [HtmlTargetElement("custom-name")] to override the convention.
// TagHelpers/AlertTagHelper.cs
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace MyApp.TagHelpers
{
// Targets: <alert type="success">Message here</alert>
[HtmlTargetElement("alert")]
public class AlertTagHelper : TagHelper
{
// Public properties become HTML attributes automatically
public string Type { get; set; } = "info"; // alert type: success, danger, warning, info
public bool Dismissible { get; set; } = true; // show close button?
public string Icon { get; set; } // optional Font Awesome icon
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
// Change the rendered tag from <alert> to <div>
output.TagName = "div";
output.Attributes.SetAttribute("class",
$"alert alert-{Type}{(Dismissible ? " alert-dismissible fade show" : "")}");
output.Attributes.SetAttribute("role", "alert");
// Get the inner content written between <alert>...</alert>
var childContent = await output.GetChildContentAsync();
// Build the inner HTML
var html = "";
if (!string.IsNullOrEmpty(Icon))
html += $"";
html += childContent.GetContent();
if (Dismissible)
html += "";
output.Content.SetHtmlContent(html);
}
}
}
<!-- Usage in Razor Views — Clean, semantic HTML -->
<alert type="success" icon="fas fa-check">Product saved successfully!</alert>
<alert type="danger" dismissible="false">Critical error occurred.</alert>
<alert type="warning">Your session will expire in 5 minutes.</alert>
<!-- Renders as: -->
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="fas fa-check me-2"></i>Product saved successfully!
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
// TagHelpers/GravatarTagHelper.cs
using System.Security.Cryptography;
using System.Text;
[HtmlTargetElement("gravatar")]
public class GravatarTagHelper : TagHelper
{
public string Email { get; set; }
public int Size { get; set; } = 80;
public string Default { get; set; } = "identicon"; // mp, identicon, monsterid, retro
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "img";
// Generate MD5 hash of email (Gravatar standard)
var emailHash = MD5.HashData(Encoding.ASCII.GetBytes(Email.Trim().ToLower()));
var hashString = Convert.ToHexStringLower(emailHash);
output.Attributes.SetAttribute("src",
$"https://www.gravatar.com/avatar/{hashString}?s={Size}&d={Default}");
output.Attributes.SetAttribute("alt", $"Avatar for {Email}");
output.Attributes.SetAttribute("class", "rounded-circle");
output.Attributes.SetAttribute("width", Size.ToString());
output.Attributes.SetAttribute("height", Size.ToString());
output.TagMode = TagMode.SelfClosing;
}
}
<!-- Usage -->
<gravatar email="@user.Email" size="48" />
<gravatar email="dev@toolliyo.com" size="120" default="retro" />
// TagHelpers/AuthorizeViewTagHelper.cs
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
[HtmlTargetElement("authorize-view")]
public class AuthorizeViewTagHelper : TagHelper
{
[ViewContext] // Injects current request context
[HtmlAttributeNotBound] // Prevents this from being an HTML attribute
public ViewContext ViewContext { get; set; }
public string Roles { get; set; } // Comma-separated roles: "Admin,Manager"
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = null; // Don't render the <authorize-view> tag itself
var user = ViewContext.HttpContext.User;
if (!user.Identity?.IsAuthenticated ?? true)
{
output.SuppressOutput(); // User not logged in — hide everything
return;
}
if (!string.IsNullOrEmpty(Roles))
{
var requiredRoles = Roles.Split(',', StringSplitOptions.TrimEntries);
if (!requiredRoles.Any(role => user.IsInRole(role)))
{
output.SuppressOutput(); // User doesn't have required role
return;
}
}
// User is authorized — render child content as-is
}
}
<!-- Usage: Only admins see the delete button -->
<authorize-view roles="Admin">
<button class="btn btn-danger">Delete All Records</button>
</authorize-view>
<!-- Only authenticated users see this -->
<authorize-view>
<a href="/dashboard">My Dashboard</a>
</authorize-view>
<!-- Views/_ViewImports.cshtml -->
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers <!-- Built-in -->
@addTagHelper *, MyApp <!-- YOUR custom Tag Helpers -->
[HtmlTargetElement] for custom element names that don't follow PascalCase conventionProcessAsync when accessing child content (GetChildContentAsync())Process for simple transformations without async operations[ViewContext] to access HttpContext, User, RouteData, and ModelStateSuppressOutput() to conditionally hide content instead of rendering empty tagsQ: "Can you explain the difference between Process() and ProcessAsync() in a Tag Helper?"
Answer: "Process() is synchronous — use it for simple transformations where you modify attributes, change the tag name, or set content without any I/O. ProcessAsync() is the async variant — it's required when you call output.GetChildContentAsync() to access the content nested between the opening and closing tags. In practice, I default to ProcessAsync because most real-world Tag Helpers need to inspect or modify their child content, and the async pattern prevents thread blocking in the ASP.NET Core pipeline."
Quizzes linked to this course—pass to earn certificates.
On this page
1. WHAT Are Custom Tag Helpers? 2. HOW to Create Custom Tag Helpers 3. Registration & Setup 4. Best Practices 5. Interview Mastery