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

 
 
  • Gérald Barré

In the previous post, I covered inlining a stylesheet into a page. This reduces the number of requests needed to load the page, which in turn reduces the overall load time. The TagHelper automatically replaces the tag with the file's content at runtime, keeping the HTML source clean. In this post, we'll create Tag Helpers to inline CSS, JavaScript, and image files. By the end, you'll be able to use this code to inline your resources:

HTML
<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 base class for all tag helpers to handle file loading. If you've read the previous post, this will look familiar. The class resolves the file on disk, reads its content, and stores it in a memory cache to avoid repeated disk reads. For script and style tags, we need the file content as text. For image tags, we need it encoded as base64. Here's the base class:

C#
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:

C#
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:

C#
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 differs slightly because the file content is encoded as base64. It also needs the image's MIME type, such as image/jpeg for JPEG files or image/xml+svg for SVG files. Rather than hard-coding every possible extension, you can reuse FileExtensionContentTypeProvider, which is part of ASP.NET Core and already used by the static file provider. It covers the MIME types for the most common file extensions.

C#
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, declare them in _ViewImports.cshtml. Other registration options are 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]

In the end, you should see that your resources are embedded in the page like in the screenshot at the beginning of the post.

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

Follow me:
Enjoy this blog?