ASP.NET Core tag helpers - with great power comes great responsibility

Posted on Monday, 09 May 2016

I recently watched a Build 2016 talk by N. Taylor Mullen where Taylor demonstrated the power of ASP.NET Core MVC's new tag helpers. Whilst I've been keeping up to date with the changes and improvements being made to Razor, there were a couple times my jaw dropped as Taylor talked about points that were completely new to me. These points really highlighted how powerful the Razor engine is becoming - but as Ben Parker said in Spiderman, "With great power comes great responsibility".

This post serves as a review of how powerful the Razor tag engine is, but also a warning of potential pitfalls you may encounter as your codebase grows.

The power of ASP.NET Core MVC's tag engine

For those of you that haven't been keeping up to date with the changes in ASP.NET Core MVC, one of the new features included within Razor are Tag Helpers. At their essence, tag helpers allow you to replace Razor's jarring syntax with a more natural HTML-like syntax. If we take moment to compare a new tag helper to the equivalent Razor function you'll see the difference (remember, you can still use the HTML helpers, Tag helpers do not replace them and will happily work side by side in the same view).

// Before - HTML Helpers
@Html.ActionLink("Click me", "MyController", "MyAction", { @class="my-css-classname", data_my_attr="my-attribute"}) 

// After - Tag Helpers
<a asp-controller="MyController" asp-action="MyAction" class="my-css-classname" my-attr="my-attribute">Click me</a>

Whilst both of these will output the same HTML, it's clear to see how much more natural the tag helper syntax looks and feels. Infact, with data prefix being optional when using data attributes in HTML, you could mistake the tag helper for HTML (more on this later).

Building your own tag helpers

It goes without saying that we're able to create our own tag helpers, and this is where they get extremely powerful. Let's start by creating a tag helper from start to finish. The follow example is a trivial example, but if you stick stick with me hopefully you'll see why I chose this example as we near the end. So let's begin by creating a tag helper that automatically adds a link-juice preserving rel="nofollow" attribute to links outbound links:

public class NoFollowTagHelper : TagHelper
{
    // Public properties becomes available on our custom tag as an attribute.
    public string Href { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = "a"; // Specify our tag output name
        output.TagMode = TagMode.StartTagAndEndTag; // The type of tag we wish to create

        output.Attributes["href"] = Href;
        if (!output.Attributes["href"].Value.ToString().Contains("josephwoodward.co.uk"))
        {
            output.Attributes["rel"] = "nofollow";
        }

        base.Process(context, output);
    }
}

Before continuing, it's worth noting that our derived class (NoFollowTagHelper) is what will become our custom tag helper name; Razor will add hyphens between the uppercase character and then lowercase the string. It will also remove the word TagHelper from the class name if it exists.

Before we can use our tag helper we need to tell Razor where to find it.

Loading our tag helper

To load our tag helper we need to add it to our _ViewImports.cshtml file. The _ViewImport's sole purpose is to reference our assemblies relating to the views to save us littering our views with references to assemblies. Using the _ViewImport we can do this in one place, much like we used to specify custom HTLM Helpers in the Web.config in previous versions of ASP.NET MVC.

// _ViewImports.cshtml
@using TagHelperDemo
@using TagHelperDemo.Models
@using TagHelperDemo.ViewModels.Account
@using TagHelperDemo.ViewModels.Manage
@using Microsoft.AspNet.Identity
@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"
@addTagHelper "*, TagHelperDemo" // Reference our assembly containing our tag helper here.

The asterisk will load all tag helpers within the TagHelperDemo assembly. If you wish to only load a single tag helper you can specify it like so:

// _ViewImports.cshtml
...
@addTagHelper "ImageLoaderTagHelper, TagHelperDemo"

Using our tag helper

Now that we've created our tag helper and referenced it, any references to elements will be transformed into no follow anchor links if the href is going to an external domain:

// Our custom tag helper
<no-follow href="http://outboundlink.com">Thanks for visiting</no-follow>
<no-follow href="/about">About</no-follow>

<!-- The transformed output -->
<a href="http://outboundlink.com" rel="nofollow">Thanks for visiting</a>
<a href="/about">About</a>

But wait! There's more!

Ok, so creating custom no-follow tags isn't ideal and is quite silly when we can just type normal HTML, so let's go one step further. With the new tag helper syntax you can actually transform normal HTML tags too! Let's demonstrate this awesomeness by modifiyng our nofollow tag helper:

[HtmlTargetElement("a", Attributes = "href")]
public class NoFollowTagHelper : TagHelper
{
    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        var href = output.Attributes["href"];
        if (!href.Value.ToString().Contains("josephwoodward.co.uk"))
        {
            output.Attributes["rel"] = "nofollow";
        }

        base.Process(context, output);
    }
}

As you'll see, we've removed some redundant code and added the HtmlTargetElement attribute. This attribute is what allows us to target existing HTML elements and add additional functionality. Now, if we look at our Razor code ALL of our anchors have been processed by our NoFollowTagHelper class and only those with outbound links have been transformed:

<!-- Before -->
<a href="http://outboundlink.com">Thanks for visiting</a>
<a href="/about">About</a>

<!-- After -->
<a href="http://outboundlink.com" rel="nofollow">Thanks for visiting</a>
<a href="/about">About</a>

We've retrospectively changed the output of our HTML without needing to go through our codebase! For those that have worked on large applications and needed to create some kind of consistency between views, you'll hopefully understand how powerful this can be and the potential use cases for it. In fact, this is exactly how ASP.NET uses the tilde (~/) to locate the path an img src path - see for yourself here.

Moving on

So far we've spent the duration of this blog post talking about how powerful ASP.NET Core MVC's Tag Helpers, but with all great powers comes great responsibility - so let's take a moment to look at the downsides of tag helpers and ways we can prevent potential pitfalls as we use them.

The Responsibility

They look just like HTML

When the ASP.NET team first revealed tag helpers to the world there were mixed reactions over the syntax. The powers of Tag Helpers were clear, but some people feel the blurring of lines between HTML and Razor breaks separation of concerns between HTML and Razor. Take the following comments taken from Scott Hanselman's ASP.NET 5 (vNext) Work in Progress - Exploring TagHelpers post demonstrate the feelings of some:

What are the design goals for this feature? I see less razor syntax, but just exchanged for more non-standard html-like markup. I'm still fond of the T4MVC style of referencing controller actions.

This seems very tough to get on-board with. My biggest concern is how do we easily discern between which more obscure attributes are "TagHelper" related vs which ones are part of the HTML spec? When my company hires new devs, I can't rely on the fact that they would realize that "action" is a "server-side" attribute, but then things like "media" and "type" are HTML ... not to mention how hard it would be to tell the difference if I'm trying to maintain code where folks have mixed server-side/html attributes.

This lack of distinction between HTML and Razor quickly becomes apparent when you open a view file in a text editor that doesn't support the syntax highlighting of Tag Helpers that Visual Studio does. Can you spot what's HTML and what's a tag helper the following screenshot?

The solution

Luckily there is a solution to help people discern between HTML and Razor, and that's to force prefixes using the @tagHelperPrefix declaration.

By adding the @tagHelperPrefix declaration to the top of your view file you're able to force prefixes on all of the tags within that current view:

// Index.cshtml
@tagHelperPrefix "helper:"

...

<div>
    <helper:a asp-controller="MyController" asp-action="MyAction" class="my-css-classname" my-attr="my-attribute">Click me</helper:a>
</div>

With a tagHelperPrefix declaration specified in a page, any tag helper that isn't prefixed with the specified prefix will be completely ignored by the Razor engine (note: You also have to prefix the closing tag). If the tag is an enclosing tag that wraps some content, then the body of the tag helper will be emitted:

// Index.cshtml - specify helper prefix
@tagHelperPrefix "helper:"

...

<div>
    // Haven't specified helper: prefix
    <asp-controller="MyController" asp-action="MyAction" class="my-css-classname" my-attr="my-attribute">Click me</a>
</div>

<!-- Output without prefix: -->
<div>
    Click me
</div>

One problem that may arise from this solution is you may forget to add the prefix declaration to your view file. To combat this you can add the prefix declaration to the _ViewImport.cshtml file (which we talked about earlier). As all views automatically inherit from _ViewImport, our prefix rule will cascade down through the rest of our views. As you'd expect, this change will force all tag helpers to require your prefix - even the native .NET MVC tags including any anchors or image HTML tags that feature a tilde:

Unexpected HTML behaviour

Earlier in this article, the second version of NoFollowTagHelper demonstrated how we can harness the power of Tag Helpers to transform any HTML element. Whilst the ability to perform such transformations is a extremely powerful, we're effectively taking HTML, a simple markup language with very little native functionality, and giving it this new power. 

Let me try and explain.

If you were to copy and paste a page of HTML into a .html file and look at it, you'd be confident that there's no magic going on with the markup - effectively what you see is what you get. Now rename that HTML page to .cshtml and put it in an ASP.NET Core MVC application with a number of tag helpers that don't require a prefix and you'll no longer have that same confidence. This lack of confidence can create uncertainty in what was once static HTML. It's the same problem you have when performing DOM manipulations using JavaScript, which is why I prefer to prefix selectors targeted by JavaScript with a 'js', making it clear to the reader that it's being touched by JavaScript (as opposed to selecting DOM elements by classes used styling purposes).

To demonstrate this with an example, what does the following HTML element do?

<img src="/profile.jpg" alt="Profile Picture" />

In any other context it would be a simple profile picture from the root of your application. Considering the lack of classes or Id attributes you'd be fairly confident there's no JavaScript targeting it too.

With Tag Helpers added to the equation this HTML isn't what you expect. When rendered it actually becomes the following:

<img src="http://cdn.com/profile.mobile.jpg" alt="Profile Picture" />

Ultimately, the best way to avoid this unpredictable behaviour is to ensure you use prefixes, or at least be ensure your team are clear as to what tag helpers or are not active.

On closing

Hopefully this post has been valuable in highlighting the good, the bad and the potentially ugly consequences of Tag Helpers in your codebase. They're an extremely powerful addition to the .NET framework, but as with all things - there is potential to shoot yourself in the foot.

Back