Eighty

Simple and fast HTML generation

HTML templating systems are great but they sure are complex. ASP.NET’s Razor, for example, is a whole new programming language! While Razor does happen to have a large chunk of C# embedded within it, and it works by generating and then compiling C# code, it’s still a separate language with a separate syntax, separate abstraction techniques, separate compiler tooling, a separate file type, and separate (and usually inferior) editor support. All this for a task as simple and common as generating HTML!

This overhead can be worth it if you’re building a complex web application, but for simple tools such as report generators or email batch mailers Razor is unwieldy. Many people in these situations resort to generating their own HTML, either by building strings manually or by imperatively building tags using .NET’s supplied XML manipulation APIs. But there’s a whole world of possible designs out there, and there’s a lot of space in between “complex templating language” and “build strings by hand”.

Eighty

Eighty (as in eigh-ty-M-L) is my attempt at striking a balance between these two extremes: not so abstract as to constitute a separate programming language, but not so concrete that you have to manipulate XML tags or strings manually. It’s a simple embedded domain-specific language which piggybacks on C#’s syntax, enabling you to write code resembling the HTML you’re generating. Rather than embedding C# into an HTML generator, Eighty embeds an HTML generator into C#.

Here’s an example from the readme:

var html = article(@class: "readme")._(
    h1(id: "Eighty")._("Eighty"),
    p_(
        "Eighty (as in ",
        i_("eigh-ty-M-L"),
        ") is a simple HTML generation library."
    )
);

Eighty is organised around the Html class, being an immutable chunk of HTML which knows how to render itself using its Write(TextWriter) method. Html defines a large collection of static methods (designed to be imported with using static), with names like h1 and p, which create Html values representing their respective tags, with a collection of children which are smaller Html values.

Eighty adopts some simple conventions for its HTML-esque domain-specific language:

Eighty vs Razor

Of course, C# code will only ever look a bit like HTML. Razor code looks much more like HTML than this! This can be a drawback when you’re working with designers who want to read and write HTML — I’m planning to write a tool to convert HTML text into an Eighty expression to partially ease this pain point. But Eighty has two big advantages which make it simpler and easier than Razor to program with:

  1. It plugs into your existing system. You don’t require any extra tools to work with Eighty: if you can compile C#, you can use Eighty.
  2. Programming with Eighty is just programming. Html instances are plain old immutable CLR objects, so you can use all your favourite techniques for abstraction and code reuse.

To illustrate the second point, here are some examples of how you might emulate some of Razor’s programming constructs using Eighty. In many of these cases Eighty does a better job than Razor of allowing abstraction and code reuse, because Eighty is embedded within C# rather than layered on top of C#.

In Razor, each view file you write declares a model type — the type of object it expects you to pass in to direct the generation of HTML. You use the @model directive at the top of your file, and then you can access members of the model in your Razor code.

@model ExampleModel

<h1>@Model.Title</h1>

One important disadvantage of Razor’s @model construct is that it is dynamically checked. The controller’s View method takes an object for the model parameter. You get a runtime error, without any feedback from the compiler, if you pass in a model whose type doesn’t match the view’s expected model type.

Since Eighty is embedded within C#, there’s no special syntax to declare the type of data a function depends on. You can just use a plain old parameter.

Html Example(ExampleModel model)
    => h1_(model.Title);

Since a template is a regular C# method, it’s much easier to run in a unit test harness than Razor. You can just call the method and make assertions about the generated HTML, either by looking at the string directly or by parsing it and traversing the resultant DOM.

Eighty includes an IHtmlRenderer<TModel> interface, which captures this pattern of parameterising a chunk of HTML by a model, but its use is optional — it’s used primarily by Eighty’s ASP.NET integration packages.

Control flow

Razor allows you to mix markup with C#’s control flow constructs such as foreach and if. Here’s a simple example of populating a ul based on a list of values:

<ul>
    @foreach (var item in Model.Items)
    {
        if (item.Visible)
        {
            <li>@item.Value</li>
        }
    }
</ul>

With Eighty, it’s a question of building different Html values. You can use LINQ’s high-level functional looping constructs:

return ul_(
    model.Items
        .Where(item => item.Visible)
        .Select(item => li_(item.Value))
);

Or you can write your own loop and build a list:

var lis = new List<Html>();
foreach (var item in model.Items)
{
    if (item.Visible)
    {
        lis.Add(li_(item.Value));
    }
}
return ul_(lis);

Mixing markup with C# is not a problem, because markup is C#.

Partials and Helpers

Razor’s two main tools for code reuse are partial views and helpers. For the purposes of this article, they’re roughly equivalent. Partial views can be returned directly from a controller but their model type is checked at runtime, whereas helpers’ parameters are checked by the compiler but they can only be invoked from within a Razor view.

Eighty handles both of these uses in the simplest of ways: calling a function. If I want to include an HTML snippet in more than one place, I can just extract it into a method returning an Html object. Transliterating an example from the MVC documentation:

Html MakeNote(string content)
    => div(@class: "note")._(
        p_(
            strong_("Note"),
            Raw("&nbsp;&nbsp; "),
            content
        )
    );

Html SomeHtmlContainingANote()
    => article_(
        p_("This is some opening paragraph text"),
        MakeNote("My test note content"),
        p_("This is some following text")
    );

This is the best of both worlds: types are checked by the compiler as usual, but the returned Html value is a perfectly good standalone chunk of HTML, and can be rendered separately if necessary.

Html values being ordinary C# values, Eighty actually supports more types of reuse than Razor does. For example, you can pass a chunk of HTML as an argument, which is not easy to do with Razor:

Html RepeatFiveTimes(Html html)
    => _(Enumerable.Repeat(html, 5));

Since Html values are immutable, you can safely share them between different HTML documents, across different threads, etc. Sharing parts of your HTML document that don’t change can be an important optimisation.

Razor lets you define a shared layout page, which acts as a template for the other pages in your application. For example, you might put the html and body tags in a layout page, and use the built in RenderBody helper to render the concrete page’s body inside the body tag. This is also where global navs and the like are defined.

One way to handle global layouts and sections in Eighty would be to define an abstract base class. Each section becomes an abstract method, allowing individual pages to fill in their own HTML for those sections.

abstract class Layout
{
    public Html GetHtml()
        => doctypeHtml_(
            head(
                link(
                    rel: "stylesheet",
                    type: "text/css",
                    href: "default.css"
                ),
                Css(),
                script(
                    type: "text/javascript",
                    src: "jquery-3.3.1.min.js"
                ),
                Js()
            ),
            body(
                Body()
            )
        );

    protected abstract Html Css();
    protected abstract Html Js();
    protected abstract Html Body();
}

Then, inheriting a layout is as easy as inheriting a class.

class DashboardPage : Layout
{
    private DashboardModel _model;

    public Dashboard(DashboardModel model)
    {
        _model = model;
    }

    protected override Html Css()
        => /* Dashboard-specific CSS */;

    protected override Html Js()
        => /* Dashboard-specific scripts */;

    protected override Html Body()
        => /* The body of the dashboard page */;
}

Twenty

Eighty comes bundled with a second HTML generation library called Twenty. Twenty is harder to use correctly than Eighty, and its API is more verbose, but it’s faster.

HTML tags have to be balanced: every opening tag has to have a matching closing tag and vice versa. While an Html value is being written to a TextWriter, Eighty manages the stack of currently-open tags using the call stack. Each tag writes its opening tag, tells its children to write themselves, and then writes its closing tag. This is possible because Html is an ordinary reference type; the objects you build with methods like p() and h1() are tree-shaped objects representing a DOM of statically-unknown size.

Twenty instead takes an imperative view of HTML generation. Each tag method writes an opening tag to the TextWriter immediately, and returns an IDisposable which writes out the closing tag when it’s disposed. You, the programmer, use C#’s using statement to ensure that the Dispose method is called as soon as the children have been written. The structure of your HTML document is still visible in the code, but it’s present in the nesting of using statements, rather than by the structure of a tree-shaped object.

class MyHtmlBuilder : HtmlBuilder
{
    protected override void Build()
    {
        using (article(@class: "readme"))
        {
            using (h1(id: "Eighty"))
                Text("Eighty");
            using (p())
            {
                Text("Eighty (as in ");
                using (i())
                    Text("eigh-ty-M-L");
                Text(") is a simple HTML generation library.");
            }
        }
    }
}

Perhaps this is a bit of an abuse of IDisposable, and the using syntax is comparatively noisy, but this trick allows Twenty to operate quickly and without generating any garbage while still making for a reasonable DSL. Compared to Eighty, Twenty does lose out on some flexibility and safety:

Given Twenty’s limitations, my advice is to write your markup using Html, and convert it to HtmlBuilder if you see that building Html values is a performance bottleneck.

Performance

Eighty is pretty fast. I wrote a benchmark testing how long it takes to spit out around 30kB of HTML (with some encoding characters thrown in for good measure) while running in an in-memory hosted MVC application. Eighty’s synchronous code path does this around three times faster than Razor, and Twenty runs about 30% faster than that — so, four times faster than Razor.

What have I done to make Eighty fast? Honestly, not a huge amount. There are a only few interesting optimisations in Eighty’s codebase.

I’m not sure exactly why Razor is slower by comparison. My guess is that Razor’s template compiler just tends to generate comparatively slow C# code — so there’s probably some room for improvement — but I would like to investigate this more.


HTML generators are an example of a problem where the spectrum of possible solutions is very broad indeed. Just within the C# ecosystem there exists a menagerie of different templating languages, as well as imperative object-oriented APIs like TagBuilder and streaming APIs like XmlWriter. Even Eighty and Twenty, two implementations of the same idea, are substantially different. You can often find yourself somewhere quite interesting if you appreach a common problem from a different direction than the established solutions. What parts of the library ecosystem do you think you could do with a fresh perspective?

Eighty is available on Nuget, along with some helpers to integrate Eighty with MVC and ASP.NET Core. API docs are hosted on this very domain, and the code’s all on GitHub where contributions and bug reports are very welcome!