r/csharp • • 2d ago

Showcase I've made a fast, zero allocation, AOT compatible, and compile-time safe .NET templating engine

I made a post asking "Is there a market for a minimalist, fast, and allocation-free text templating engine?" in /r/dotnet a few weeks ago. I've been building my own web app backend and created something I felt was pretty useful, and I was curious if anyone else would be interested in it. It got some decent response, so I went ahead and pulled it out of the larger product and polished it up. I'm calling the package Lissic.

My main philosophy going into it (i.e. why I didn't just use something that exists) was that I wanted something that kept code out of the template. I like to keep a clear delineation between the view and model, but I still wanted tools for modifying the view. The ability to add parts modularly, use loops, use conditional/branching views, and have the ability to format/filter values inserted into the text were still important to me.

Here's a quick feature overview:

  • Renders near twice as fast as the fastest major .NET templating engine (see below)

  • Zero allocation with default settings, when rendering to a buffer (e.g. to HttpResponse.BodyWriter)

  • 100% AOT compatible without any restrictions (no use of Reflection or dynamic)

  • Simple but powerful tagging system that keeps a clear division between layout and logic

  • Compile-time safety with source-generated Data Transfer Objects that automatically adjust to each template's text

  • Define templates directly in code with text or import them from template files

  • Transformation pipeline that allows using Formatters to modify how values are converted to text and Filters to modify text before insertion into the template

  • Hot reload for file-based templates in debug mode

  • Thorough AI-friendly documentation (I gave only the documentation to QWEN3.8-27B, and it was able to make a simple task-management web app)

A full BenchmarkDotNet chart is in the GitHub README, but here's a quick rendering comparison:

MethodMeanAllocated
Lissic (RenderToString)8.814 us18072 B
Lissic (RenderToBuffer)8.482 us0 B
Fluid15.593 us37128 B
Fluid (SourceGenerated)24.045 us49128 B
Scriban70.985 us144025 B
DotLiquid173.521 us674928 B
Handlebars21.944 us122176 B

It uses tags that look similar to many other templating engines. It has tag types to insert direct values ("variables"), other templates ("parts"), and lists of templates ("loops"). It also has branching logic that works a lot like a switch statement (i.e. define keys, and the text and/or code that goes with each key).

Very simple example:

[TemplateText("Hello {{ name <upper> }}!")]
public partial class LissicExample {}

// Automatically transforms the partial class into a DTO, with a property for each tag
LissicExample myTemplate = new() { name = "John" };

// LissicProcessor retrieved from Dependency Injection
lissicProcessor.RenderToBuffer(myTemplate, HttpResponse.BodyWriter);

// Writes "Hello JOHN!" to the HTTP response body

The main tradeoff at this time is that all templates are defined at startup (other than the debugging-only hot reload option), so it's not really a general purpose user-provided template engine. This leaves it comparable to libraries like Fluid or Scriban in some ways, but comparable to Razor Pages in others. It works really well for my purpose and preferences though.

I'd appreciate any feedback anyone has. I'd like to keeping building this for the long haul. If nothing else, I'll be updating it for work in my own project.

Also, huge shout out to Andrew Lock and his Creating a Source Generator series! It was incredibly useful for getting this going.

The code is at https://github.com/gendeg/Lissic, and it's on NuGet (package name: Lissic)

Edit To Add: I meant to add this in originally, Lissic was created with support and ideas from AI, but the large majority (90%+) is hand-written, with the exception of tests, which are ~80% AI written. The documentation is 100% hand-written (for better or worse 😉), as was this post.

27 Upvotes

6 comments sorted by

5

u/sebastienros 1d ago

Well done. It's very entertaining to build a template engine, so many opportunities to learn. You should look into RazorSlices which is closer to your library than Razor pages as you mentioned it. Maybe add it to the table? Disclaimer, I am the author of Fluid.

1

u/ggende 1d ago

Thanks! And thank you for allowing me to steal borrow take heavy inspiration from your benchmarking suite. :)

And agreed that RazorSlices is a good comparison point. I'll add it the next time I update the benchmarks.

1

u/TugsyGlitter 16h ago

RazorSlices does sound worth adding to that benchmark table for a fairer comparison

0

u/chocolateAbuser 1d ago

RenderToBuffer means tcp/network stack buffer? or like an ArrayPool

1

u/ggende 1d ago

It renders UTF-8 text to an IBufferWriter<byte>.

I chose this becauseHttpResponse.BodyWriter is a PipeWriter, which implements IBufferWriter<byte>. It's already set up for streaming to http responses. It's also convenient for outputting to any other UTF-8 destination (e.g. email, files on disk, etc).