skip to content
Khalid Abuhakmeh logo Khalid Abuhakmeh

Building HTML with C#

How do you build HTML in C#? Use ASP.NET Core's HtmlContentBuilder and IHtmlContent to build encoded HTML safely, plus a small DSL example.

5 min read Updated

To build HTML in C#, use ASP.NET Core’s HtmlContentBuilder (or HtmlString), which implements IHtmlContent and encodes anything you don’t explicitly mark as HTML. Updated: October 2026.

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 investigate HtmlContentBuilder, 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. (If encoding is new to you, read the difference between HTML and URL encoding in .NET.) 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);
}
}
}

How Do You Use HtmlContentBuilder Outside ASP.NET Core Views?

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;
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.

Terminal window
<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 C# 12 primary constructors).

using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Html;
using static System.Console;
using static System.Text.Encodings.Web.HtmlEncoder;
new Html(
new H1("Hello, World!")
)
.Write(Out, Default);
public class Html(H1 h1)
{
public void Write(TextWriter writer, HtmlEncoder encoder)
{
var builder = new HtmlContentBuilder();
builder.AppendFormat("<html>{0}</html>", h1.Render());
builder.WriteTo(writer, encoder);
}
}
public class H1(string content)
{
public IHtmlContent Render()
{
return new HtmlString($"<h1>{content}</h1>");
}
}

We get the same expected output.

Fix from the original post: the H1 class used to declare public IHtmlContent ToString(). That hides object.ToString() with a different return type, which doesn’t compile. It’s now Render(). Also note that new HtmlString($"<h1>{content}</h1>") doesn’t encode content, so a production DSL should run it through HtmlEncoder first.

Terminal window
<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.

Comments

Related Posts