ASP.NET Core has a superpower that few other frameworks have, largely thanks to the Razor engine. Razor syntax is a mix of HTML and C#, and most Razor syntax implementations will skew heavily towards HTML over C#. However, C# syntax offers the most value in control flow mechanics using if, for, and switch statements. Razor’s power is that even HTML syntax is processed by C# and converted into compiled artifacts. This gives Razor a unique opportunity to do some amazing tricks.

In this post, we’ll see how to use the TagHelpers infrastructure to initialize all tag helper usage across your application and inject necessary shared data.

The TagHelper In Question

Let’s start by writing a simple tag helper that will replace the contents of a span tag when a text attribute is set.

using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace WebApplication2.Models;

[HtmlTargetElement("span")]
public class MyTagHelper: TagHelper
{
    [HtmlAttributeName("text")]
    public string Text { get; set; } = "";

    [HtmlAttributeNotBound] 
    public string Version { get; set; } = "";
    
    public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        output.Content.SetHtmlContent(Text);
        output.Attributes.Add("data-version", Version);
        return Task.CompletedTask;
    }
}

Usage of this tag helper is straightforward. In a Razor view, add the following tag.

<span class="fs-1 d-block" text="Hello, World!">...</span>
<span class="fs-1 d-block"></span> 

As you may have noticed in the MyTagHelper implementation, there is another property of Version, which I decorated with the HtmlAttributeNotBound attribute. This value will be initialized with the tag helper initialization infrastructure.

TagHelper Initializers

We’ll implement the ITagHelperInitializer generic interface, which has an Initialize method that takes an instance of a tag helper and a ViewContext.

public class MyTagHelperInitializer(string defaultText, string version) 
    : ITagHelperInitializer<MyTagHelper>
{
    public void Initialize(MyTagHelper helper, ViewContext context)
    {
        helper.Text = defaultText;
        helper.Version = version;
    }
}

Here, the data passed into our implementation can be used to hydrate all tag helpers of MyTagHelper. This is awesome for multiple reasons.

  • Expensive data can be calculated once and set globally, reducing resource utilization and speeding up page rendering.
  • We have access to the ViewContext, so we can modify and enhance all request/response lifecycle elements if necessary.
  • We have direct access to the tag helper, so the initialization code is as straightforward as possible.
  • The ViewContext gives us access to HttpContext, so we can also handle request-specific values from cookies, user information, etc.
  • Also, we can request other services that are already registered in our services collection.

How do we use this initializer? In Program, add the following line to register our initializer with our services collection.

builder.Services.AddSingleton<
    ITagHelperInitializer<MyTagHelper>
>(new MyTagHelperInitializer("Default Text", "1.0.0"));

In our case, we’re passing in the initial values. However, this type could also take in application configuration and read values from the IConfiguration implementation of an ASP.NET Core application. It’s important to note that this type is registered as a **Singleton **, which means any data passed to it in the constructor is the data for the rest of the application’s lifetime.

When we run our application, the tag helper will result in the following HTML.

<span class="fs-1 d-block" data-version="1.0.0">Hello, World!</span>
<span class="fs-1 d-block" data-version="1.0.0">Default Text</span>

Wow, so easy!

Conclusion

Part of building web experiences is handling requests efficiently and returning responses as quickly as possible. With tag helpers, you can help create more buffer space in your performance budgets by reducing and isolating the work needed to process data for HTML tags. Additionally, this technique of global initialization might also be helpful for test-driven UI tests, as attributes and their data can be added or removed depending on build flags. This approach is exciting, and I hope you try it in your applications.

As always, thanks for reading and sharing my blog posts. Cheers.