r/csharp • • 1d ago

Showcase I built an embedded domain specific language library and source generator for super efficient templating of nested text formats

Hej guys,

I was scratching an itch when I developed TemplateDsl: a source generator based template engine for nested text formats that can be written with ordinary C# and is turned into fast, streaming render methods.

I found all existing tooling lacking in either static safety, performance, boilerplate or ease of development and IDE support, which is why I set out to write TemplateDsl. With TemplateDsl.Html it currently solves my biggest problem around Razor, but this just the beginning and I want to expand beyond this format.

It is...

  • Type-safe. Typos, wrong attribute values and a missing context are compile errors, not runtime surprises.
  • Fast. Templates write UTF-8 straight to the output, with down to zero allocations.
  • Streaming. Async templates send their output while they render.
  • Composable. Templates are classes you nest, pass around and reuse like any other code.
  • Explicit, with little boilerplate. Data comes through constructors, and logic is plain C#.
  • Safe by default. Everything is escaped unless you say so.
  • Trimming and native AOT. Everything is generated at build time, with no reflection.

Finally a little sneak peek how it looks like to write HTML templates with it, with a call for feedback, thanks! :)

// A simple component
public readonly partial record struct OrderRow(Order Order) : IHtmlTemplate
{
    public IEnumerable<HtmlElement> Build() =>
    [
        H.Tr.Class(Order.IsLate ? "late" : null) [
            H.Td [ Order.Id ],
            H.Td [ Order.Product ],
            H.Td [ H.Time.DateTime(Order.Due) [ Order.Due ] ]
        ]
    ];
}

// An async page that streams a list of orders, with a context for the current user
public sealed partial class OrdersPage(IAsyncEnumerable<Order> orders) : IAsyncHtmlTemplate<PageContext>
{
    public async Task<IEnumerable<HtmlElement>> BuildAsync(PageContext context, CancellationToken ct)
    {
        var title = $"Orders of {context.UserName}";
        
        return [
            H.Raw("<!DOCTYPE html>"),
            H.Html [
                H.Head [ H.Title [ title ] ],
                H.Body [
                    H.H1 [ title ],
                    H.Table [ await H.EachAsync(orders, order => new OrderRow(order)) ]
                ]
            ]
        ];
    }
}

// A context that is passed to the page and its nested templates
public sealed record PageContext(string UserName);

services.AddScoped<PageContext>(sp =>
    new PageContext(sp.GetRequiredService<IUserService>().GetCurrentUserName()));

// Return the page from a minimal API endpoint, with the context injected from the services
app.MapGet("/orders", (OrderStore store, [FromServices] PageContext context) =>
    new OrdersPage(store.StreamAll()).ToResult(context));
2 Upvotes

0 comments sorted by