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 toHttpContext
, 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.