Inlining a Stylesheet, a JavaScript, or an image file using a TagHelper in ASP.NET Core

In the previous post, I've written about inlining a Stylesheet file in the page. This allow you to reduce the number of requests required to load the page, and so to reduce the loading time of the page. The TagHelper automatically replace the tag by the content of the file at runtime, so the html document stays clean in the source code. In this post, we'll create new Tag Helpers to be able to inline CSS, JS and image files. At the end of the post, you'll be able to use this code to inline your resources:

<inline-style href="css/site.css" />
<inline-script src="js/site.js" />
<inline-img src="images/banner1.svg" />

Let's see how to create this tag helper!

First, we need a common class for all the tag helpers to handle the loading of the content of the file. There is no surprised if you have read the previous post. This class resolves the file on the disk, read its content, and store it in a memory cache to not overuse the disk. For script and style, we need the text of the file. For image tags, we need the content as base64. Here's the base class:

public abstract class InlineTagHelper : TagHelper
{
    private const string CacheKeyPrefix = "InlineTagHelper-";

    private readonly IHostingEnvironment _hostingEnvironment;
    private readonly IMemoryCache _cache;

    protected InlineTagHelper(IHostingEnvironment hostingEnvironment, IMemoryCache cache)
    {
        _hostingEnvironment = hostingEnvironment;
        _cache = cache;
    }

    private async Task<T> GetContentAsync<T>(ICacheEntry entry, string path, Func<IFileInfo, Task<T>> getContent)
    {
        var fileProvider = _hostingEnvironment.WebRootFileProvider;
        var changeToken = fileProvider.Watch(path);

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

        var file = fileProvider.GetFileInfo(path);
        if (file == null || !file.Exists)
            return default(T);

        return await getContent(file);
    }

    protected Task<string> GetFileContentAsync(string path)
    {
        return _cache.GetOrCreateAsync(CacheKeyPrefix + path, entry =>
        {
            return GetContentAsync(entry, path, ReadFileContentAsStringAsync);
        });
    }

    protected Task<string> GetFileContentBase64Async(string path)
    {
        return _cache.GetOrCreateAsync(CacheKeyPrefix + path, entry =>
        {
            return GetContentAsync(entry, path, ReadFileContentAsBase64Async);
        });
    }

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

    private static async Task<string> ReadFileContentAsBase64Async(IFileInfo file)
    {
        using (var stream = file.CreateReadStream())
        using (var writer = new MemoryStream())
        {
            await stream.CopyToAsync(writer);
            writer.Seek(0, SeekOrigin.Begin);
            return Convert.ToBase64String(writer.ToArray());
        }
    }
}

The hardest part is done! We can create the InlineScriptTagHelper class:

public class InlineScriptTagHelper : InlineTagHelper
{
    [HtmlAttributeName("src")]
    public string Src { get; set; }

    public InlineScriptTagHelper(IHostingEnvironment hostingEnvironment, IMemoryCache cache)
        : base(hostingEnvironment, cache)
    {
    }

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        var fileContent = await GetFileContentAsync(Src);
        if (fileContent == null)
        {
            output.SuppressOutput();
            return;
        }

        output.TagName = "script";
        output.Attributes.RemoveAll("src");
        output.TagMode = TagMode.StartTagAndEndTag;
        output.Content.AppendHtml(fileContent);
    }
}

The InlineStyleTagHelper is very similar:

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

    public InlineStyleTagHelper(IHostingEnvironment hostingEnvironment, IMemoryCache cache)
        : base(hostingEnvironment, cache)
    {
    }

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        var fileContent = await GetFileContentAsync(Href);
        if (fileContent == null)
        {
            output.SuppressOutput();
            return;
        }

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

The InlineImageTagHelper is just a little different because the content of the file is encoded as base64. Also, we need to get the mime type of the image, e.g. image/jpeg for a jpeg file, image/xml+svg for an svg file, and so on. Instead of hard-coding all the possibilities in the tag helper, you can reuse the class FileExtensionContentTypeProvider which is part of ASP.NET Core as it is used by the static file provider. It contains the mime types of the most common file extensions.

public class InlineImgTagHelper : InlineTagHelper
{
    private static readonly FileExtensionContentTypeProvider s_contentTypeProvider = new FileExtensionContentTypeProvider();

    [HtmlAttributeName("src")]
    public string Src { get; set; }

    public InlineImgTagHelper(IHostingEnvironment hostingEnvironment, IMemoryCache cache)
        : base(hostingEnvironment, cache)
    {
    }

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        var fileContent = await GetFileContentBase64Async(Src);
        if (fileContent == null)
        {
            output.SuppressOutput();
            return;
        }

        if (!s_contentTypeProvider.TryGetContentType(Src, out var contentType))
        {
            contentType = "application/octet-stream";
        }

        output.TagName = "img";
        var srcAttribute = $"data:{contentType};base64,{fileContent}";

        output.Attributes.RemoveAll("src");
        output.Attributes.Add("src", srcAttribute);
        output.TagMode = TagMode.SelfClosing;
        output.Content.AppendHtml(fileContent);
    }
}

To use the TagHelpers, you need to declare it in the _ViewImports.cshtml. There are other ways to include the Tag Helper as explain in the documentation, but this is the most common.

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

At the end, you should see that your resources are directly in the page:

Enjoy this blog? Buy Me A Coffee Donate with PayPal

Leave a reply