Inlining a stylesheet using a TagHelper in ASP.NET Core

 
 
  • Gérald Barré

When you have a very small stylesheet, it can be more efficient to inline it directly in the page. This avoids an extra network request that would otherwise block page layout until the browser receives the response. If the stylesheet is tiny, you may not need the browser to cache it at all. For instance, on this website, the main stylesheet is 2kB, and the full page with the inlined stylesheet is about 11kB. That is small enough to justify inlining. Of course, manually pasting the minified CSS into the HTML is not practical: the markup becomes unreadable, and formatting the document introduces unwanted indentation or line breaks. The process should be automated. Using a tag helper, it can look like this:

HTML
<inline-style href="css/site.min.css" />

Let's see how to create this tag helper!

First, let's create the structure of the Tag Helper. A tag helper is simply a class that inherits from TagHelper.

C#
public class InlineStyleTagHelper : TagHelper
{
    [HtmlAttributeName("href")]
    public string Href { get; set; }

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        // TODO
    }
}

To use the TagHelper, you need to declare it in _ViewImports.cshtml. There is another way to include Tag Helpers as explained in the documentation, but this is the most common approach.

HTML
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, MyWebApp [Replace MyWebApp with the name of the assembly that contains the TagHelper]

Now we will read the file from disk and write its content to the output. To resolve the full file path from a relative path, you need the web root. You can use IHostingEnvironment.WebRootFileProvider for this. Tag Helpers support dependency injection, so you can inject IHostingEnvironment via the constructor. Here is what it looks like:

C#
public class InlineStyleTagHelper : TagHelper
{
    public InlineStyleTagHelper(IHostingEnvironment hostingEnvironment)
    {
        HostingEnvironment = hostingEnvironment;
    }

    [HtmlAttributeName("href")]
    public string Href { get; set; }

    private IHostingEnvironment HostingEnvironment { get; }

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        var path = Href;
        IFileProvider fileProvider = HostingEnvironment.WebRootFileProvider;
        IFileInfo file = fileProvider.GetFileInfo(path);
        if (file == null)
            return;

        string fileContent = await ReadFileContent(file);
        if (fileContent == null)
        {
            output.SuppressOutput();
            return;
        }

        // Generate the output
        output.TagName = "style"; // Change the name of the tag from inline-style to style
        output.Attributes.RemoveAll("href"); // href attribute is not needed anymore
        output.Content.AppendHtml(fileContent);
    }

    private static async Task<string> ReadFileContent(IFileInfo file)
    {
        using (var stream = file.CreateReadStream())
        using (var textReader = new StreamReader(stream))
        {
            return await textReader.ReadToEndAsync();
        }
    }
}

#Caching the file in memory

That works! However, it is not optimal for performance: the file is read from disk on every request. A better approach is to cache the file content in memory, while making sure the cache is invalidated whenever the file changes on disk. ASP.NET Core provides everything needed for this:

  • You can cache a value using the IMemoryCache
  • You can handle the expiration using an IChangeToken

Conveniently, IFileProvider exposes a Watch method that returns exactly this kind of token, so the implementation is straightforward:

C#
public class InlineStyleTagHelper : TagHelper
{
    public InlineStyleTagHelper(IHostingEnvironment hostingEnvironment, IMemoryCache cache)
    {
        HostingEnvironment = hostingEnvironment;
        Cache = cache;
    }

    [HtmlAttributeName("href")]
    public string Href { get; set; }

    private IHostingEnvironment HostingEnvironment { get; }
    private IMemoryCache Cache { get; }

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        var path = Href;

        // Get the value from the cache, or compute the value and add it to the cache
        var fileContent = await Cache.GetOrCreateAsync("InlineStyleTagHelper-" + path, async entry =>
        {
            IFileProvider fileProvider = HostingEnvironment.WebRootFileProvider;
            IChangeToken changeToken = fileProvider.Watch(path);

            entry.SetPriority(CacheItemPriority.NeverRemove);
            entry.AddExpirationToken(changeToken);

            IFileInfo file = fileProvider.GetFileInfo(path);
            if (file == null || !file.Exists)
                return null;

            return await ReadFileContent(file);
        });

        if (fileContent == null)
        {
            output.SuppressOutput();
            return;
        }

        output.TagName = "style";
        output.Attributes.RemoveAll("href");
        output.Content.AppendHtml(fileContent);
    }
}

Once set up, you should see the CSS inlined directly in the page:

Demo of a stylesheet inlined by the TagHelperDemo of a stylesheet inlined by the TagHelper

The tag helper works as expected. You can apply the same technique to JavaScript files or small images embedded as base64.

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?