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
Htmlhelpers – there is not a Tag Helper for everyHtmlhelper.
Why Tag Helpers?
- Plain HTML – no server-side help, you must manually construct URLs, form actions, and manage input names for model binding.
Htmlhelpers –@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.cshtmlapplies hierarchically – a file in/Views/applies to all views, one in/Views/Home/applies only to that folder.- Use
@removeTagHelperto exclude specific tag helpers. - Use
@tagHelperPrefixto 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-actionasp-all-route-dataasp-antiforgeryasp-areaasp-controllerasp-fragmentasp-routeasp-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-dataGive a dictionary for all route/target parameters.asp-areaUse area in route/target (usually not needed to specify explicitly).asp-fragmentAdd#<value>to route/target.
<div>
asp-validation-summary – display validation summary in this div
All– property and modelModelOnly– only modelNone- none
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input>
asp-action,asp-all-route-data,asp-area,asp-controller,asp-fragment,asp-routeasp-for="<ModelExpression>"Generatesidandnameattributes – these are critical for model binding on POST (the model binder matches input names to model properties). Sets thetypeattribute – based on model type and data annotations. Sets up client-side validation.asp-for="Property1"becomes in generated codem => 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 propertyasp-itemsspecifies 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-hostSpecify host to use in generated link (default is relative to current host)asp-protocolSpecify 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.cshtmlextension)model– passes a model object to the partialfor– binds to a model expression (likeasp-for), passes both the value and the model expression metadataview-data– passes aViewDataDictionaryto the partial
<cache>
Server-side output caching for expensive view fragments:
<cache expires-after="@TimeSpan.FromMinutes(5)">
@await Component.InvokeAsync("TopProducts")
</cache>
expires-after–TimeSpanafter which the cache expiresexpires-on– absoluteDateTimeOffsetexpires-sliding– sliding expirationTimeSpanvary-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-versionasp-fallback-srcIfasp-fallback-testis negative (CDN unavailable) then fall back to this locationasp-fallback-testJavaScript expression to testasp-fallback-src-exclude,asp-fallback-src-include,asp-src-exclude,asp-src-includeComma 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>
<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-versionasp-fallback-hrefasp-fallback-href-excludeasp-fallback-href-includeasp-fallback-test-classasp-fallback-test-propertyasp-fallback-test-valueasp-href-excludeasp-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
Htmlhelpers. - Some functionality is only possible with
Htmlhelpers. Htmlhelpers 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:
- 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. - How does
asp-forwork and why is it important? —asp-forgenerates theidandnameattributes from the model property for model binder mapping on POST. It also sets the correcttypeattribute and configures client-side validation. - What does
asp-antiforgeryprotect 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. - How does
asp-append-versionhelp 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). - How do you create a custom Tag Helper? — Create a class inheriting from
TagHelper, overrideProcessorProcessAsync, and register it in_ViewImports.cshtmlwith@addTagHelper. - 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.