Marius Schulz
Marius Schulz
Front End Engineer

Tag Helpers in ASP.NET Core MVC

I previously wrote about view components in ASP.NET MVC 6, a new feature introduced as part of the ASP.NET Core web stack. This post is about tag helpers, another feature new to ASP.NET MVC 6. Let's take a look at what tag helpers have to offer.

#An Introduction to Tag Helpers

Taken from the comprehensive introduction to tag helpers from the ASP.NET documentation, here's the definition in a nutshell:

Tag Helpers enable server-side code to participate in creating and rendering HTML elements in Razor files.

Take the built-in ImageTagHelper, for example. It is applied to img tags and appends cache-busting query string parameters to image URLs by rewriting the src attribute. That way, images can be cached aggressively without the risk of serving stale images to the client:

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

As you can see, the above img tag looks like a regular HTML tag with regular src and alt attributes. What's special about it, though, is the asp-append-version attribute, which (in conjunction with the src attribute) makes the ImageTagHelper kick in. Here's the resulting HTML output:

<img
  src="~/images/logo.png?v=kwU2pelqgmu77o8S6rXIu-Xj4bsnX_m-ZDQ9Y1EbWio"
  alt="Logo"
/>

Notice that the asp-append-version attribute is gone — after all, it has no meaning to browsers whatsoever. The image URL within the src attribute now includes the v query string parameter that contains a unique hash representing the current image version. Et voilà, there's our cache busting.

In order for this tag helper to work, we need to make our Razor views aware of it. There's a special file called _ViewImports.cshtml located in the Views folder that Razor knows about (similar to _ViewStart.cshtml, if you will). Create it if it doesn't exist yet and add the following line to opt into using all tag helpers built into ASP.NET MVC 6:

@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"

The way that tag helpers work differs from the approach taken by HTML helpers in previous versions of ASP.NET MVC. It was always cumbersome to add custom attributes or CSS classes to HTML tags rendered via HTML helpers because we were writing that code in C#. Now, this part is trivial since we mostly write HTML when working with tag helpers.

#Writing a Custom Tag Helper

Let's say we want to write a custom tag helper for rendering a <time> tag based on a DateTime. Those <time> tags can be used to represent dates and times in a machine-readable format. However, they require a very specific date format that we shouldn't have to repeat over and over again. Here's how we would use our tag helper:

@{
    var exampleDate = new DateTime(2015, 12, 02, 14, 50, 31, DateTimeKind.Utc);
}

<time asp-date-time="@exampleDate" />

The output should be something along the lines of the following:

<time
  datetime="2015-12-02T14:50:31Z"
  title="Wednesday, December 2, 2015 02:50 PM UTC"
>
  December 2, 2015 2:50 PM
</time>

We'll start by creating a custom class that derives from the TagHelper class found in the Microsoft.AspNet.Razor.TagHelpers namespace. We'll also create a property to hold the datetime that's passed in through the asp-date-time attribute:

public class TimeTagHelper : TagHelper
{
    [HtmlAttributeName("asp-date-time")]
    public DateTime DateTime { get; set; }
}

However, we only want to apply our tag helper to <time> tags that specify the asp-date-time attribute, so we'll explicitly restrict it to those using the HtmlTargetElement attribute on the tag helper class:

[HtmlTargetElement("time", Attributes = DateTimeAttributeName)]
public class TimeTagHelper : TagHelper
{
    private const string DateTimeAttributeName = "asp-date-time";

    [HtmlAttributeName(DateTimeAttributeName)]
    public DateTime DateTime { get; set; }
}

To specify our tag helper's behavior, we'll override the Process method and add our datetime manipulation logic inside of it. We're setting both a machine-readable datetime attribute and a human-readable title attribute:

[HtmlTargetElement("time", Attributes = DateTimeAttributeName)]
public class TimeTagHelper : TagHelper
{
    private const string DateTimeAttributeName = "asp-date-time";

    [HtmlAttributeName(DateTimeAttributeName)]
    public DateTime DateTime { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.Attributes["datetime"] = DateTime.ToString("yyyy-MM-dd'T'HH:mm:ss") + "Z";
        output.Attributes["title"] = DateTime.ToString("dddd, MMMM d, yyyy 'at' h:mm tt");
    }
}

Note that we'll also have to add a line to _ViewImports.cshtml for our tag helper to be recognized within Razor views:

@addTagHelper "*, YourTagHelperAssemblyName"

If we now render a <time> tag using this simple version of the tag helper, we get both attributes, but no inner HMTL (no content). Let's extend our tag helper such that it adds a default piece of inner HTML if the <time> tag doesn't define any child content. To do this, we'll await and inspect the GetChildContentAsync method, which means that we'll have to override ProcessAsync instead of Process:

[HtmlTargetElement("time", Attributes = DateTimeAttributeName)]
public class TimeTagHelper : TagHelper
{
    private const string DateTimeAttributeName = "asp-date-time";

    [HtmlAttributeName(DateTimeAttributeName)]
    public DateTime DateTime { get; set; }

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        output.Attributes["datetime"] = DateTime.ToString("yyyy-MM-dd'T'HH:mm:ss") + "Z";
        output.Attributes["title"] = DateTime.ToString("dddd, MMMM d, yyyy 'at' h:mm tt");

        var childContent = await output.GetChildContentAsync();
        if (childContent.IsEmpty)
        {
            output.TagMode = TagMode.StartTagAndEndTag;
            output.Content.SetContent(DateTime.ToString("MMMM d, yyyy h:mm tt"));
        }
    }
}

Now we should get the output we want:

<time
  datetime="2015-12-02T14:50:31Z"
  title="Wednesday, December 2, 2015 02:50 PM UTC"
>
  December 2, 2015 2:50 PM
</time>

#Closing Note

As you've seen, tag helpers can be pretty useful for simple things like adding an additional attribute with a specific format for a given input value. However, I also want to speak a word of caution.

Tag helpers that have been included in _ViewImports.cshtml will automatically be applied to all matching HTML elements. This happens rather implicitly, especially if the HTML elements targeted don't specify attributes which clearly indicate a tag helper.

For this reason, I like to prefix the names of custom tag helper attributes with asp-. If I see an attribute named asp-date-time, it tells me that a tag helper is going to be involved. The unprefixed name date-time, on the other hand, is a lot less clear. Time will tell what best practices will emerge in this area.

So, here you go: tag helpers in ASP.NET MVC 6. Useful helpers, no doubt, but make sure to use them responsibly!