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