Introducing CinderBlockHtml - A C# DSL for Building HTML with Composable Building Blocks

Date: 2025-07-07 | build | cinderblockhtml | create | csharp | dsl | fsharp | html |

DISCLOSURE: If you buy through affiliate links, I may earn a small commission. (disclosures)

I've been exploring modern C# and how I might build webapps with it using Simple Scalable Systems. I like building with the HAM Stack (Hypermedia on a Modulith) which usually means doing a lot of server-side rendered HTML.

In F# I've been using Falco.Markup in CloudSeed for this HTML generation but I've noticed C# doesn't have a DSL option that's as ergonomic.

So I decided to build CinderBlockHtml to port the experience of Falco.Markup from F# to C#.

What is CinderBlockHtml?

GitHub: https://github.com/SIRHAMY/CinderBlockHtml

CinderBlockHtml is an HTML DSL aiming to make it simple to construct server side HTML with flexibility, performance, and robustness.

It's all C# code so you get access to all the type safety and code constructs you're used to:

  • Return HTML partials as results of functions
  • Use loops / LINQ to build up lists
  • Type-safe inputs (props for React fans) for building HTML "components"

Example:

using CinderBlockHtml;

// Simple element with text content
var greeting = Elem.Div([Attr.Class("container")],
    [Text.Encoded("Hello World")]
);

// Convert to HTML string
var html = greeting.RenderToString();
// Output: <div class="container">Hello World</div>

Why use CinderBlockHtml?

I get it - DSLs are bespoke and can get confusing.

There are many other ways to produce server side HTML in dotnet with tools like Razor, RazorLight, Scriban, Eighty, DV8, HtmlTags, etc.

The main sticking point is that CinderBlockHtml is:

  • Simple
  • Readable
  • Composable

Some people like the templating paradigm:

  • Build template
  • Compile template
  • Parse HTML out with inputs

But I find this to be cumbersome, inefficient, and often not as type safe as I'd like. Moreover composing multiple templates together with partials gets even more cumbersome as you keep track of different templates and their compilation states.

HTML DSLs allow you to build HTML with the same ergonomics you like your programming language for (assuming you like your programming language).

Among the HTML DSLs, CinderBlockHtml is:

  • The easiest to read (IMO)
  • Competitively performant - not as fast as raw strings or Eighty but with much more readable (and maintainable) syntax (IMO)

For examples of the different syntaxes of each, you can check out examples in CinderBlockHtml.Benchmarks.

How does CinderBlockHtml Perform?

Performance is rarely the top factor in choosing a code library but it is something to keep in mind as it may be a deal breaker if it's too slow.

CinderBlockHtml is very competitive with other HTML DSLs and templating libraries only falling behind raw strings and Eighty's performance-focused design.

As always, all benchmarks have asterisks so you will need to run your own benchmarks on your own workloads to understand how they'll actually perform in prod.

CinderBlockHtml.Benchmarks currently benchmarks 6 methods of HTML generation across 3 scenarios.

(If you'd like to add a scenario or generation method, please make a PR!)

Hello World Benchmark

Render a simple hello world html page using const strings to simulate a small amount of dynamism.

| Method          | Mean          | Error       | StdDev      | Gen0   | Gen1   | Allocated |
|---------------- |--------------:|------------:|------------:|-------:|-------:|----------:|
| CinderBlockHtml | 1,227.1095 ns |  20.6712 ns |  19.3358 ns | 0.3643 | 0.0019 |    5720 B |
| RawStringHtml   |     0.3551 ns |   0.0539 ns |   0.0504 ns |      - |      - |         - |
| HtmlTagsHtml    | 1,605.5206 ns |  29.2699 ns |  27.3791 ns | 0.4921 | 0.0057 |    7728 B |
| ScribanHtml     | 7,604.5052 ns | 150.2625 ns | 242.6459 ns | 2.1973 | 0.1831 |   35083 B |
| RazorLightHtml  | 3,448.5997 ns |  67.6905 ns | 101.3160 ns | 0.2899 |      - |    4679 B |
| EightyHtml      |   616.2327 ns |   6.6551 ns |   5.8996 ns | 0.0858 |      - |    1352 B |

List Benchmark

Render a list of 100 items to html to simulate a long dynamic html page.

| Method          | Mean      | Error     | StdDev    | Gen0   | Gen1   | Allocated |
|---------------- |----------:|----------:|----------:|-------:|-------:|----------:|
| CinderBlockHtml | 16.919 us | 0.3377 us | 0.7622 us | 6.0425 | 0.7324 |  92.78 KB |
| RawStringHtml   |  5.033 us | 0.1007 us | 0.1737 us | 3.1052 | 0.2747 |  47.64 KB |
| HtmlTagsHtml    | 21.675 us | 0.4301 us | 0.6437 us | 7.6599 | 1.8921 | 117.68 KB |
| ScribanHtml     | 47.962 us | 0.9554 us | 2.2521 us | 7.3242 | 0.9766 | 116.53 KB |
| RazorLightHtml  | 19.060 us | 0.2875 us | 0.2689 us | 4.5166 | 0.4883 |  69.58 KB |
| EightyHtml      | 16.646 us | 0.2312 us | 0.2049 us | 2.4719 | 0.1831 |  37.97 KB |

Nested Benchmark

Render 100 items in nested divs to simulate pages with lots of nested elements, similar to complex pages with components.

| Method          | Mean     | Error    | StdDev   | Gen0    | Gen1   | Allocated  |
|---------------- |---------:|---------:|---------:|--------:|-------:|-----------:|
| CinderBlockHtml | 51.77 us | 0.704 us | 0.659 us | 14.0991 | 2.8076 |  216.75 KB |
| RawStringHtml   | 55.81 us | 1.082 us | 1.111 us | 64.5142 |      - |  988.14 KB |
| HtmlTagsHtml    | 67.02 us | 1.337 us | 2.376 us | 15.8691 | 5.9814 |     244 KB |
| ScribanHtml     | 74.24 us | 1.443 us | 3.619 us | 70.8008 | 5.1270 | 1086.29 KB |
| RazorLightHtml  | 63.25 us | 1.222 us | 1.358 us | 66.7725 | 4.5166 | 1025.32 KB |
| EightyHtml      | 28.67 us | 0.543 us | 0.603 us |  3.7842 | 0.0305 |   58.31 KB |

Next

I'll be continuing to hack on this as I build with it in more of my side projects.

Please let me know if you have any feedback or suggestions!

Shoutout to Pim Brouwers (creator of Falco and Falco.Markup) for building the original library. I really enjoy the og syntax and this port is largely a 1:1 mapping of that syntax.

If you liked this post you might also like:

Want more like this?

The best way to support my work is to like / comment / share for the algorithm and subscribe for future updates.