Skip to main content

07 - Tag Helpers

Tag Helpers

  • Tag Helpers enable server-side code to participate in creating and rendering HTML elements in Razor files.
  • There are many built-in Tag Helpers for common tasks - such as creating forms, links, loading assets and more - and even more available in public GitHub repositories and as NuGet packages.
  • Tag Helpers are authored in C#, and they target HTML elements based on element name, attribute name, or parent tag.
  • Tag helpers do not replace Html helpers – there is not a Tag Helper for every Html helper.

Why Tag Helpers?

  • Plain HTML – no server-side help, you must manually construct URLs, form actions, and manage input names for model binding.
  • Html helpers@Html.TextBoxFor(m => m.Name, new { @class = "form-control" }) – generates correct HTML but is hard to read, designers and front-end tools can't work with it easily.
  • Tag Helpers – look like normal HTML (<input asp-for="Name" class="form-control" />), but get server-side processing. Best of both worlds.

Registering Tag Helpers

Tag helpers are registered in _ViewImports.cshtml using @addTagHelper:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
  • The wildcard * imports all tag helpers from the specified assembly.
  • _ViewImports.cshtml applies hierarchically – a file in /Views/ applies to all views, one in /Views/Home/ applies only to that folder.
  • Use @removeTagHelper to exclude specific tag helpers.
  • Use @tagHelperPrefix to require a prefix (e.g., th:) so tag helpers are visually distinct from plain HTML:
@tagHelperPrefix th:

Then use: <th:input asp-for="Name" />

<form>

  • asp-action
  • asp-all-route-data
  • asp-antiforgery
  • asp-area
  • asp-controller
  • asp-fragment
  • asp-route
  • asp-route-<parameter name>
<form
asp-controller="Account"
asp-action="Login"
asp-route-returnurl="@ViewData["ReturnUrl"]"
method="post"
class="form-horizontal">

Transforms to

<form method="post" class="form-horizontal" action="/Account/Login">
  • asp-controller="<Controller Name>"
  • asp-action="<Action Name>" The form target is constructed from controller and action tag helpers.
  • asp-route="<Route Name from routing table>" The form target is constructed using the routing table.
  • asp-route-<parameter name>="<value>" Parameter name is added to the form target (as query string or route parameter).
  • asp-antiforgery="true/false" Generates an anti-forgery token as a hidden field in the form. Usually controlled by [ValidateAntiForgeryToken] attribute in Controller. This protects against CSRF (Cross-Site Request Forgery) attacks – where a malicious site submits a form to your app using the user's authenticated session. The token ensures the form submission originated from your own site.
  • asp-all-route-data Give a dictionary for all route/target parameters.
  • asp-area Use area in route/target (usually not needed to specify explicitly).
  • asp-fragment Add #<value> to route/target.

<div>

asp-validation-summary – display validation summary in this div

  • All – property and model
  • ModelOnly – only model
  • None - none
<div asp-validation-summary="ModelOnly" class="text-danger"></div>

<input>

  • asp-action, asp-all-route-data, asp-area, asp-controller, asp-fragment, asp-route
  • asp-for="<ModelExpression>" Generates id and name attributes – these are critical for model binding on POST (the model binder matches input names to model properties). Sets the type attribute – based on model type and data annotations. Sets up client-side validation. asp-for="Property1" becomes in generated code m => m.Property1.
  • asp-format="<format>" Use to format value. <input asp-for="SomeNumber" asp-format="{0:N4}"/>
  • Input tag helper does not handle collections and templates – use Html.XxxxxFor.

Input type is based on .NET type

  • .NET type - Input Type
  • Bool - type="checkbox"
  • String - type="text"
  • DateTime - type="datetime-local"
  • Byte - type="number"
  • Int - type="number"
  • Single, Double - type="number"

Or use data annotations

  • [EmailAddress] - type="email"
  • [Url] - type="url"
  • [HiddenInput] - type="hidden"
  • [Phone] - type="tel"
  • [DataType(DataType.Password)] - type="password"
  • [DataType(DataType.Date)] - type="date"
  • [DataType(DataType.Time)] - type="time"

<span>

asp-validation-for

Display validation error (if there is one for this model property) in this span.

<span asp-validation-for="LastName" class="text-danger"></span>

<label>

asp-for Generate label for this model property. The displayed text comes from [Display(Name = "...")] data annotation, or from the property name.

<label asp-for="LastName" class="col-md-2 control-label"></label>
<label class="col-md-2 control-label" for="LastName">LastName</label>

<textarea>

  • asp-for="<ModelExpression>" Generate textarea input for this model property. Id, name, validation

Complete form example

Putting it all together – a form with model, labels, inputs, validation, and submit:

@model LoginViewModel

<form asp-controller="Account" asp-action="Login" method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>

<div class="mb-3">
<label asp-for="Email" class="form-label"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>

<div class="mb-3">
<label asp-for="Password" class="form-label"></label>
<input asp-for="Password" class="form-control" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>

<div class="mb-3">
<input asp-for="RememberMe" class="form-check-input" />
<label asp-for="RememberMe" class="form-check-label"></label>
</div>

<button type="submit" class="btn btn-primary">Log in</button>
</form>

The tag helpers generate proper id, name, type, and validation attributes – all of which the model binder uses on POST to populate LoginViewModel.

<select>, option group <optgroup>

  • asp-for="<ModelExpression>" specifies the model property
  • asp-items specifies option elements (List<SelectListItem>)
<select asp-for="Country" asp-items="Model.Countries"></select>
  • You can generate option list from enums using Html helper
<select asp-for="EnumCountry"
asp-items="Html.GetEnumSelectList<CountryEnum>()">
</select>
  • The HTML <optgroup> element is generated when the view model contains one or more SelectListGroup objects.
public CountryViewModelGroup()
{
var NorthAmericaGroup =
new SelectListGroup { Name = "NA" };
var EuropeGroup =
new SelectListGroup { Name = "EU" };

Countries = new List<SelectListItem>{
new SelectListItem{
Value = "MEX",
Text = "Mexico",
Group = NorthAmericaGroup
},
new SelectListItem{
Value = "FR",
Text = "France",
Group = EuropeGroup
},
...

<select> multi-select

The Select Tag Helper will automatically generate the multiple = "multiple" attribute if the property specified in the asp-for attribute is an IEnumerable

public class CountryViewModelIEnumerable
{
public IEnumerable<string> CountryCodes { get; set; }

public List<SelectListItem> Countries { get; } = new List<SelectListItem>
{
new SelectListItem { Value = "MX", Text = "Mexico" },
new SelectListItem { Value = "CA", Text = "Canada" },
...

Collections

public class ToDoItem
{
public string Name { get; set; }
public bool IsDone { get; set; }
}
@model List<ToDoItem>
@for (int i = 0; i < Model.Count; i++)
{
<tr>
<td>
<label asp-for="@Model[i].Name"></label>
</td>
<td>
<input asp-for="@Model[i].IsDone" />
</td>
</tr>
}

<a>

  • asp-action, asp-all-route-data, asp-area, asp-controller, asp-fragment, asp-route, asp-route-<parameter name>
  • asp-host Specify host to use in generated link (default is relative to current host)
  • asp-protocol Specify protocol to use (default is current protocol)

<img>

asp-append-version="<true/false>"

Enable cache busting. Generates file version hash and appends it to source.

<img src="~/images/logo.png" asp-append-version="true" />

Generated: <img src="/images/logo.png?v=Kl_dqr9NVtnBdseFDBJc..." /> When the file changes, the hash changes, forcing browsers to re-download.

<partial>

Renders a partial view. Replaces @Html.PartialAsync() and @await Html.RenderPartialAsync().

<partial name="_LoginPartial" />
<partial name="_ProductCard" model="item" />
<partial name="_Filters" for="FilterModel" />
  • name – the partial view name (with or without .cshtml extension)
  • model – passes a model object to the partial
  • for – binds to a model expression (like asp-for), passes both the value and the model expression metadata
  • view-data – passes a ViewDataDictionary to the partial

<cache>

Server-side output caching for expensive view fragments:

<cache expires-after="@TimeSpan.FromMinutes(5)">
@await Component.InvokeAsync("TopProducts")
</cache>
  • expires-afterTimeSpan after which the cache expires
  • expires-on – absolute DateTimeOffset
  • expires-sliding – sliding expiration TimeSpan
  • vary-by-query – vary cache by query string parameters

<script>

<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-...">
</script>
  • asp-append-version
  • asp-fallback-src If asp-fallback-test is negative (CDN unavailable) then fall back to this location
  • asp-fallback-test JavaScript expression to test
  • asp-fallback-src-exclude, asp-fallback-src-include, asp-src-exclude, asp-src-include Comma separated list of sources to include or exclude

Note: In modern projects, assets are typically bundled locally rather than loaded from CDNs with fallbacks. The fallback approach is shown here for reference.

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
asp-fallback-href="~/lib/css/bootstrap.min.css"
asp-fallback-test-class="visually-hidden"
asp-fallback-test-property="position"
asp-fallback-test-value="absolute"/>

<link
rel="stylesheet"
href="~/css/site.min.css"
asp-append-version="true"/>
  • asp-append-version
  • asp-fallback-href
  • asp-fallback-href-exclude
  • asp-fallback-href-include
  • asp-fallback-test-class
  • asp-fallback-test-property
  • asp-fallback-test-value
  • asp-href-exclude
  • asp-href-include

<environment>

  • Names="comma_separated_list" ASP.NET Core pre-defines following environments – Development, Staging, Production. Useful for branching in cshtml files.
<environment names="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment names="Staging,Production">
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true"/>
</environment>

View Components

View Components are an architectural pattern for encapsulating reusable UI logic that is too complex for a partial view but doesn't warrant a full controller. They combine a C# class (logic) with a Razor view (rendering).

public class TopProductsViewComponent : ViewComponent
{
private readonly IProductService _productService;

public TopProductsViewComponent(IProductService productService)
{
_productService = productService;
}

public async Task<IViewComponentResult> InvokeAsync(int count = 5)
{
var products = await _productService.GetTopProductsAsync(count);
return View(products);
}
}

Invoke using tag helper syntax:

<vc:top-products count="10"></vc:top-products>

Or using Html helper:

@await Component.InvokeAsync("TopProducts", new { count = 10 })
  • View Components support dependency injection (constructor injection).
  • The view is placed in Views/Shared/Components/<ComponentName>/Default.cshtml.
  • PascalCase class name becomes kebab-case in tag helper: TopProducts<vc:top-products>.

Html helpers

  • Tag helpers are there in addition to Html helpers.
  • Some functionality is only possible with Html helpers.
  • Html helpers generate full HTML tags – harder to read cshtml files, designers can't modify easily.
  • Four main categories of helpers
    • Output
    • Input
    • Form
    • Link

Output

  • DisplayFor
  • DisplayForModel
  • DisplayNameFor
  • DisplayNameForModel
  • DisplayTextFor
  • LabelFor
  • LabelForModel

Input

  • EditorFor
  • TextAreaFor
  • TextBoxFor
  • DropDownListFor
  • EnumDropDownListFor
  • ListBoxFor
  • RadioButtonFor
  • HiddenFor
  • CheckBoxFor
  • PasswordFor

Form

  • BeginForm
  • BeginRouteForm
  • EndForm
  • AntiForgeryToken
  • HiddenFor

Link

  • ActionLink
  • RouteLink

EditorFor

  • EditorFor(expression)
  • EditorFor(expression, additionalViewData)
  • EditorFor(expression, templateName)
  • EditorFor(expression, templateName, additionalViewData)
  • EditorFor(expression, templateName, htmlFieldName)
  • EditorFor(expression, templateName, htmlFieldName, additionalViewData)

Expression - lambda

Html helpers, example

<dt>
@Html.DisplayNameFor(model => model.FirstName)
</dt>
<dd>
@Html.DisplayFor(model => model.FirstName)
</dd>

...

@Html.EditorFor(
model => model.Participant.FirstName,
new { htmlAttributes = new { @class = "form-control" } }
)

@Html.ValidationMessageFor(
model => model.Participant.FirstName,
"",
new { @class = "text-danger" }
)

@Html.ActionLink("Show items", "Show", new { id = 1},
htmlAttributes: new { @class = "btn btn-primary", role = "button" })

Creating Custom Tag Helpers

Convert this:

<email domain="eesti.ee">Support</email>

to

<a href="support@eesti.ee">support@eesti.ee</a>

Add first implementation

using Microsoft.AspNetCore.Razor.TagHelpers;

namespace WebApp.TagHelpers;

// target <email> tag
public class EmailTagHelper: TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.TagName = "a"; // replace original tag
}
}

Add support in _ViewImports.cshtml

@using WebApp
@using WebApp.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, WebApp

Attribute and Content

Switch over to async version...

using Microsoft.AspNetCore.Razor.TagHelpers;

namespace WebApp.TagHelpers;

// target <email> tag
public class EmailTagHelper : TagHelper
{
// pascalcase to kebab-case
public string Domain { get; set; }

public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
// replace original tag
output.TagName = "a";

// get the original tag helper content
var content = await output.GetChildContentAsync();

var address = (content.GetContent() + "@" + Domain).ToLower();

output.Attributes.SetAttribute("href", "mailto:" + address);
output.Content.SetContent(address);
}
}

Targeting with [HtmlTargetElement]

By default, a tag helper targets the HTML element matching its class name (minus the TagHelper suffix). Use [HtmlTargetElement] to target by attribute or to target multiple elements:

[HtmlTargetElement("div", Attributes = "bold")]
[HtmlTargetElement("span", Attributes = "bold")]
public class BoldTagHelper : TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Attributes.RemoveAll("bold");
output.PreContent.SetHtmlContent("<strong>");
output.PostContent.SetHtmlContent("</strong>");
}
}

Usage: <div bold>This text is bold</div>

Modify existing tag helpers

Inherit from default AnchorTagHelper, override the Process method and then remove the original Microsoft.AspNetCore.Mvc.TagHelpers.AnchorTagHelper in _ViewImports.cshtml file.

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@removeTagHelper Microsoft.AspNetCore.Mvc.TagHelpers.AnchorTagHelper, Microsoft.AspNetCore.Mvc.TagHelpers

@addTagHelper *, YourProject

Testing custom tag helpers

Custom tag helpers are regular C# classes and can be unit tested:

[Fact]
public async Task EmailTagHelper_GeneratesCorrectLink()
{
var tagHelper = new EmailTagHelper { Domain = "eesti.ee" };

var context = new TagHelperContext(
new TagHelperAttributeList(),
new Dictionary<object, object>(),
"uniqueid");

var output = new TagHelperOutput("email",
new TagHelperAttributeList(),
(useCachedResult, encoder) =>
{
var tagHelperContent = new DefaultTagHelperContent();
tagHelperContent.SetContent("Support");
return Task.FromResult<TagHelperContent>(tagHelperContent);
});

await tagHelper.ProcessAsync(context, output);

Assert.Equal("a", output.TagName);
Assert.Equal("mailto:support@eesti.ee",
output.Attributes["href"].Value);
}

This is valuable for ensuring your custom tag helpers produce correct HTML, especially when they contain business logic.

Docs

https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/authoring?view=aspnetcore-10.0

What to Know for Code Defense

Be prepared to explain:

  1. Why use Tag Helpers instead of plain HTML or Html helpers? — Tag Helpers look like normal HTML, making views readable by designers. They generate correct id, name, type, and validation attributes for model binding.
  2. How does asp-for work and why is it important?asp-for generates the id and name attributes from the model property for model binder mapping on POST. It also sets the correct type attribute and configures client-side validation.
  3. What does asp-antiforgery protect against? — It generates an anti-forgery token to prevent CSRF attacks, where a malicious site submits a form to your app using the user's authenticated session.
  4. How does asp-append-version help with caching? — It generates a file hash appended as a query string. When the file changes, the hash changes, forcing browsers to download the new version (cache busting).
  5. How do you create a custom Tag Helper? — Create a class inheriting from TagHelper, override Process or ProcessAsync, and register it in _ViewImports.cshtml with @addTagHelper.
  6. What is the difference between the <partial> tag helper and View Components?<partial> renders a partial view with data from the parent view. View Components have their own class with logic, support DI, and fetch their own data independently.