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:
- Tags are created using (lower-case) methods like
p()
andi()
. - Attributes are passed as optional named arguments:
a(href: "benjamin.pizza", @class: "website-link")
. I can’t force you to name your arguments — you could pass them positionally — but that’s not a good idea. - A tag’s children are introduced using the
_
character, which can appear at the end of a method name or as a method name all by itself.a(href: "benjamin.pizza")._("Visit my website")
creates ana
tag with anhref
attribute and some text inside it;p_("a paragraph of text")
represents ap
tag with some text but no attributes. I chose_
because it’s the least noisy character that can be used as an identifier in C#. - Strings can be implicitly converted to
Html
and are interpreted as HTML text. Text is HTML-encoded by default. You can opt out of this using theRaw
method.
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:
- 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.
- 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#.
Models
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.
Example(ExampleModel model)
Html => 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_(
.Items
model.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)
{
.Add(li_(item.Value));
lis}
}
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:
MakeNote(string content)
Html => div(@class: "note")._(
p_(
strong_("Note"),
Raw(" "),
content)
);
SomeHtmlContainingANote()
Html => 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:
RepeatFiveTimes(Html 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.
Layouts
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(
: "stylesheet",
rel: "text/css",
type: "default.css"
href),
Css(),
script(
: "text/javascript",
type: "jquery-3.3.1.min.js"
src),
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:
- You mustn’t forget a
using
statement, or callDispose
more than once, or Twenty will output malformed HTML. Eighty, on the other hand, will never generate bad HTML (notwithstanding the use ofRaw
). - There’s no
Html
object — you can’t pass around chunks of HTML as first class values. This makes code reuse and abstraction somewhat more difficult. HtmlBuilder
is not re-entrant. You can’t use the sameHtmlBuilder
from multiple threads.- There’s no
async
API, because there’s no way to callDispose
asynchronously.
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.
- Each call to
TextWriter
’sWrite
method is comparatively expensive, so rather than write individual snippets of HTML into theTextWriter
directly, Eighty builds up a 4kB buffer and empties it out into theTextWriter
when it fills up. The code to fill this buffer is a little fiddly, because you don’t know how long your input string is going to be after HTML-encoding it, so the HTML encoder has to write the encoded HTML in chunks. I toyed with a hand-written encoder, but I wanted to interoperate with ASP.NET’s pluggableHtmlEncoder
, so I ended up calling that class’s low-level API.- The buffer is managed by a mutable struct which is stored on the stack and passed by reference because mutable structs must never be copied. However, the async version cannot be a struct because
async
methods copy theirthis
variable into a field behind the scenes. My first version of the code used the same mutable struct for both paths, which caused me some head-scratching when theasync
version didn’t work! - There’s a fun and dangerous hack in Twenty’s codebase to allow storing a reference to one of these stack-allocated structs in a field. This is safe as long as the reference in the field doesn’t live longer than the stack location to which it refers, but you don’t get any compile-time feedback about this (I just have to program carefully and hope I don’t make a mistake). This hack makes critical use of C# 7’s “
ref
return types”, so it wouldn’t have been possible a couple of years ago.
- The buffer is managed by a mutable struct which is stored on the stack and passed by reference because mutable structs must never be copied. However, the async version cannot be a struct because
- Calling an
async
method is comparatively expensive, even if it never goes async, because of the wayasync
methods are translated by the compiler into code which builds and then executes a state machine. In the case of Eighty’s frequently-calledWriteRawImpl
method, it’s predictable whether a call will complete synchronously (that is, without calling the underlyingTextWriter
’sWriteAsync
method). I split theasync
method into two parts — a fast wrapper which synchronously returns aTask
and anasync
method which is only called when necessary — and got a ~15% speedup in my end-to-end benchmarks. Html
values make use ofImmutableArray
s to store their children.ImmutableArray
is a thin wrapper over a regular array, so if you have aT[]
you should be able to turn it into anImmutableArray
in-place without copying the contents, as long as you’re careful never to modify the original array after freezing it. There are several places in Eighty where this is a safe optimisation, butImmutableArray
doesn’t have a public API to do this. However, sinceImmutableArray<T>
is a struct with a single privateT[]
field, its runtime representation is the same asT[]
’s. This makes it possible to unsafely coerce aT[]
to anImmutableArray<T>
with no runtime cost.- I’ve opened an issue in the
corefx
repo proposing an officially-supported API for this use case.
- I’ve opened an issue in the
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!