Tutorials ASP.NET Core MVC Mastery

Custom Tag Helpers

On this page

Custom Tag Helpers — Building Your Own Razor Superpower

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.

1. WHAT Are Custom Tag Helpers?

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.

How Tag Helper Naming Works

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.

2. HOW to Create Custom Tag Helpers

Example 1: Bootstrap Alert Tag Helper

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

Example 2: Gravatar Image Tag Helper

// 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" />

Example 3: Role-Based Visibility Tag Helper (Using ViewContext)

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

3. Registration & Setup

<!-- Views/_ViewImports.cshtml -->
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers    <!-- Built-in -->
@addTagHelper *, MyApp                                    <!-- YOUR custom Tag Helpers -->

4. Best Practices

  • Use [HtmlTargetElement] for custom element names that don't follow PascalCase convention
  • Use ProcessAsync when accessing child content (GetChildContentAsync())
  • Use Process for simple transformations without async operations
  • Use [ViewContext] to access HttpContext, User, RouteData, and ModelState
  • Use SuppressOutput() to conditionally hide content instead of rendering empty tags
  • For complex components requiring DB access, prefer View Components over Tag Helpers

5. Interview Mastery

Q: "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."

Questions on this lesson 0

Sign in to ask a question or upvote helpful answers.

No questions yet — be the first to ask!

ASP.NET Core MVC Mastery
Course syllabus
1. Core Framework
MODULE 1: INTRODUCTION & ENVIRONMENT SETUP
2. View Engine
MODULE 2: .NET CORE FUNDAMENTALS
MODULE 3: ASP.NET CORE BASICS
MODULE 4: MVC FUNDAMENTALS
MODULE 5: DATA PASSING TECHNIQUES
MODULE 6: ROUTING
MODULE 7: VIEWS & UI
MODULE 8: ACTION RESULTS
MODULE 9: HTML HELPERS
MODULE 10: TAG HELPERS
MODULE 11: MODEL BINDING
MODULE 12: VALIDATION
MODULE 13: STATE MANAGEMENT
MODULE 14: FILTERS & SECURITY
MODULE 15: ENTITY FRAMEWORK CORE (DEEP DIVE)
MODULE 16: DESIGN PATTERNS
MODULE 17: FILE HANDLING
MODULE 18: ADVANCED ASP.NET CORE
MODULE 19: PERFORMANCE & BEST PRACTICES
MODULE 20: RAZOR PAGES (BONUS)
MODULE 21: REAL-WORLD PROJECTS (🔥 MUST DO)
Toolliyo Assistant
Ask about tutorials, ebooks, training, pricing, mentor services, and support. I use public site content only—not admin or internal tools.

care@toolliyo.com

Need callback? Share your details