Hypertext Markup Language is the first programming language many of us learn. Like chess, it’s easy to learn but can take a lifetime to master. Its declarative syntax is ideal and ubiquitous across many different ecosystems, and C# embraces it with ASP.NET. In this post, we’ll investigateHtmlContentBuilder, look at its API, and consider a potential DSL built on top of the builder.

What Is HtmlContentBuilder

Building HTML in code has always been tricky. How do we distinguish a tag from an intended literal? Improper handling of user input can lead to incorrect syntax, and at its worst, it could lead to security vulnerabilities. Let’s take a look at an example string.

<h1>Hello, Developers!</h1>

Experienced developers will understand that we have a string value inside of an h1 tag. What about the following example?

<code><h1>Hello, Code!</h1></code>

The problem starts to get a little more difficult because in this case, everything inside of our code tag should have been encoded.

<code>&lt;h1&gt;Hello, World!&lt;/h1&gt;</code>

ASP.NET has introduced a set of classes that allow us to work with HTML content in C#. These classes include HtmlContentBuilder, HtmlString, HtmlFormattableString. All of them implement the IHtmlContent interface. The most interesting class is HtmlContentBuilder, which gives us the ability to work with HTML structures.

The HtmlContentBuilder class allows us to Append, Clear, CopyTo, MoveTo, and WriteTo efficiently.

Appending

HtmlContentBuilder provides multiple Append methods. They primarily differ in whether they encode the content passed into them.

IHtmlContentBuilder Append(string unencoded)
IHtmlContentBuilder AppendHtml(IHtmlContent htmlContent)
IHtmlContentBuilder AppendHtml(string encoded)

It’s important to know that appending to the builder is not finalized immediately. Let’s take a look at the internal implementation of Append.

public IHtmlContentBuilder Append(string unencoded)
{
    if (!string.IsNullOrEmpty(unencoded))
    {
        Entries.Add(unencoded);
    }
    return this;
}

HtmlContentBuilder stores values in an Entries collection. Storage of entries will become clearer when we look at Move and Copy functions.

Usage of Append methods is straightforward, with more fine-grained functionality coming in the form of extension methods found in HtmlContentBuilderExtensions.

var builder = new HtmlContentBuilder();
// AppendFormat is from HtmlContentBuilderExtensions
builder.AppendFormat("<html><h1>{0}</h1></html>", "Hello, Khalid!");

Moving Elements

Entries in an HtmlContentBuilder instance need to be some flavor of IHtmlContent. The storage of these entries allows us to move elements in and out of HTML containers.

public void MoveTo(IHtmlContentBuilder destination)
{
    if (destination == null)
    {
        throw new ArgumentNullException(nameof(destination));
    }

    for (var i = 0; i < Entries.Count; i++)
    {
        var entry = Entries[i];

        string entryAsString;
        IHtmlContentContainer entryAsContainer;
        if ((entryAsString = entry as string) != null)
        {
            destination.Append(entryAsString);
        }
        else if ((entryAsContainer = entry as IHtmlContentContainer) != null)
        {
            // Since we're moving, do a deep flatten.
            entryAsContainer.MoveTo(destination);
        }
        else
        {
            // Only string, IHtmlContent values can be added to the buffer.
            destination.AppendHtml((IHtmlContent)entry);
        }
    }

    Entries.Clear();
}

Copying Elements

Copying is a necessary function for any templating approach. Looking at the implementation, we can see how a copy happens.

public void CopyTo(IHtmlContentBuilder destination)
{
    if (destination == null)
    {
        throw new ArgumentNullException(nameof(destination));
    }

    for (var i = 0; i < Entries.Count; i++)
    {
        var entry = Entries[i];

        string entryAsString;
        IHtmlContentContainer entryAsContainer;
        if ((entryAsString = entry as string)  != null)
        {
            destination.Append(entryAsString);
        }
        else if ((entryAsContainer = entry as IHtmlContentContainer) != null)
        {
            // Since we're copying, do a deep flatten.
            entryAsContainer.CopyTo(destination);
        }
        else
        {
            // Only string, IHtmlContent values can be added to the buffer.
            destination.AppendHtml((IHtmlContent)entry);
        }
    }
}

Using HtmlContentBuilder

For all intents and purposes, HtmlContentBuilder is an internal implementation detail for ASP.NET MVC, Razor Pages, and Blazor. We likely will interact with HtmlString more often than HtmlContentBuilder. That doesn’t mean we are restricted to using this class in an ASP.NET application exclusively. Here is an example in a console application.

using Microsoft.AspNetCore.Html;
using static System.Console;
using static System.Text.Encodings.Web.HtmlEncoder;

namespace ConsoleApp16
{
    class Program
    {
        static void Main(string[] args)
        {
            var builder = new HtmlContentBuilder();
            builder.AppendFormat("<html><h1>{0}</h1></html>", "Hello, Khalid!");
            builder.WriteTo(Out, Default);
        }
    }
}

To get our results, we need a TextWriter instance and the encoder for our HTML. Running the WriteTo method, we see the expected output of HTML.

<html><h1>Hello, Khalid!</h1></html>

Not that exciting, but what we can do is wrap HTML elements in a custom domain-specific language (DSL). Here is the same output but with the HTML encapsulated in classes.

using System.IO;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Html;
using static System.Console;
using static System.Text.Encodings.Web.HtmlEncoder;

namespace ConsoleApp16
{
    class Program
    {
        static void Main(string[] args)
        {
            new Html(
                new H1("Hello, World!")
            )
            .Write(Out, Default);
        }
    }

    public class Html
    {
        private readonly H1 h1;

        public Html(H1 h1)
        {
            this.h1 = h1;
        }

        public void Write(TextWriter writer, HtmlEncoder encoder)
        {
            var builder = new HtmlContentBuilder();
            builder.AppendFormat("<html>{0}</html>", h1.ToString());
            builder.WriteTo(writer, encoder);
        }
    }

    public class H1
    {
        public H1(string content)
        {
            Content = content;
        }

        private string Content { get; set; }

        public IHtmlContent ToString()
        {
            return new HtmlString($"<h1>{Content}</h1>");
        }
    }
}

We get the same expected output.

<html><h1>Hello, World!</h1></html>

Conclusion

Using the classes of HtmlContentBuilder, HtmlString, and HtmlFormattableString give us the ability to build and manipulate HTML structures in C#. It’s important that these classes are meant to build new HTML structures, and cannot parse existing HTML. Leveraging these classes, we could build a custom DSL that lets us write HTML using C# syntax while getting all the benefits of encoding.

Please let me know what you think by leaving a comment below.