Sniz.HtmlTemplateEngine
1.1.0
dotnet add package Sniz.HtmlTemplateEngine --version 1.1.0
NuGet\Install-Package Sniz.HtmlTemplateEngine -Version 1.1.0
<PackageReference Include="Sniz.HtmlTemplateEngine" Version="1.1.0" />
<PackageVersion Include="Sniz.HtmlTemplateEngine" Version="1.1.0" />
<PackageReference Include="Sniz.HtmlTemplateEngine" />
paket add Sniz.HtmlTemplateEngine --version 1.1.0
#r "nuget: Sniz.HtmlTemplateEngine, 1.1.0"
#:package Sniz.HtmlTemplateEngine@1.1.0
#addin nuget:?package=Sniz.HtmlTemplateEngine&version=1.1.0
#tool nuget:?package=Sniz.HtmlTemplateEngine&version=1.1.0
Sniz.HtmlTemplateEngine
A production-grade C# HTML template engine built on a real Lexer → Parser → AST → Renderer pipeline. Inspired by Handlebars, but custom-built for .NET 8 with full extensibility, thread safety, and parse caching.
Features
- ✅ Property binding with deep nesting and null-safe navigation
- ✅ Conditional blocks —
{{#if}},{{else if}},{{else}}, unlimited nesting - ✅ Foreach loops with alias, loop metadata (
@index,@first,@last,@count) - ✅ Parent context navigation (
../Property,../../Property) - ✅ Format specifiers — dates, currency, numeric, upper/lower, custom
- ✅ Arithmetic and string expressions —
{{Qty * Price}},{{First + " " + Last}} - ✅ HTML encoding by default, raw output via
{{{expr}}} - ✅ Custom helpers and formatters
- ✅ Parse cache — templates are parsed once and reused across renders
- ✅ Thread-safe concurrent rendering
Quick Start
var engine = new HtmlTemplateEngine();
var model = new
{
Customer = new { Name = "Nizam", IsActive = true },
CreatedDate = DateTime.Now
};
string html = engine.Render(model, "<h1>Hello, {{Customer.Name}}!</h1>");
// → <h1>Hello, Nizam!</h1>
Usage
1. Property Binding
// Simple
engine.Render(new { Name = "Alice" }, "Hello {{Name}}!");
// Nested
engine.Render(model, "{{Customer.Address.City}}");
// Null-safe (returns empty string instead of throwing)
engine.Render(model, "{{Customer?.Address?.City}}");
// Dictionary
engine.Render(new { Settings = new Dictionary<string,string>{{ "Theme","Dark" }} },
"Theme: {{Settings.Theme}}");
// Collection count
engine.Render(new { Items = new[] { 1, 2, 3 } }, "Total: {{Items.Count}}");
// Resolve a single property directly
string name = engine.ResolveProperty(customer, "Address.City"); // → "Mangaluru"
2. HTML Encoding
// Default — HTML encoded (safe)
engine.Render(new { Value = "<script>alert('xss')</script>" }, "{{Value}}");
// → <script>alert('xss')</script>
// Raw — unescaped output
engine.Render(new { Html = "<b>bold</b>" }, "{{{Html}}}");
// → <b>bold</b>
3. Conditionals
var template = """
{{#if User.IsActive}}
{{#if User.Role == "Admin"}}
<span>Admin Panel</span>
{{else if User.Role == "Editor"}}
<span>Editor Panel</span>
{{else}}
<span>Dashboard</span>
{{/if}}
{{else}}
<span>Account Disabled</span>
{{/if}}
""";
// Supported operators: == != > < >= <= && || !
engine.Render(new { User = new { IsActive = true, Role = "Admin" } }, template);
4. Foreach Loops
// Basic loop — access properties directly
var template = """
{{#foreach Orders}}
<li>{{OrderNumber}}</li>
{{/foreach}}
""";
// With alias
var template = """
{{#foreach Orders as order}}
<li>{{order.OrderNumber}} — {{order.Customer.Name}}</li>
{{/foreach}}
""";
// Loop metadata
var template = """
{{#foreach Items as item}}
{{@index}}. {{item.Name}}
{{#if @first}}<hr>{{/if}}
{{#if @last}}<hr>{{/if}}
({{@count}} total)
{{/foreach}}
""";
5. Nested Loops + Parent Context
var template = """
{{#foreach Categories as category}}
<h2>{{category.Name}}</h2>
{{#foreach category.Products as product}}
<p>
Category: {{../category.Name}}
Product: {{product.Name}}
Customer: {{../../Customer.Name}}
</p>
{{/foreach}}
{{/foreach}}
""";
6. Formatting
// Date
engine.Render(new { Date = DateTime.Now }, "{{Date:yyyy-MM-dd}}");
// Currency
engine.Render(new { Price = 1299.99m }, "{{Price:C}}");
// Numeric
engine.Render(new { Amount = 9876.5 }, "{{Amount:N2}}"); // → 9876.50
// Zero-padded
engine.Render(new { Id = 42 }, "{{Id:0000}}"); // → 0042
// String case
engine.Render(new { Name = "hello" }, "{{Name:U}}"); // → HELLO
engine.Render(new { Name = "HELLO" }, "{{Name:L}}"); // → hello
// Custom formatter
engine.RegisterFormatter("Truncate", v => v?.ToString()?[..10] ?? "");
engine.Render(new { Bio = "Long text here" }, "{{Bio:Truncate}}");
7. Expressions
// Arithmetic
engine.Render(new { Qty = 3, Price = 10.0 }, "{{Qty * Price}}"); // → 30
engine.Render(new { A = 100.0, B = 30.0 }, "{{A - B}}"); // → 70
// String concatenation
engine.Render(new { First = "John", Last = "Doe" }, "{{First + \" \" + Last}}");
// Boolean in conditions
engine.Render(new { Age = 20 }, "{{#if Age >= 18}}Adult{{/if}}");
8. Custom Helpers
// Value helper — {{HelperName(Expression)}}
engine.RegisterHelper("Upper", v => v?.ToString()?.ToUpper());
engine.RegisterHelper("Slugify", v => v?.ToString()?.ToLower().Replace(" ", "-"));
engine.RegisterHelper("Truncate100", v => {
var s = v?.ToString() ?? "";
return s.Length > 100 ? s[..100] + "…" : s;
});
engine.Render(new { Name = "hello world" }, "{{Upper(Name)}}"); // → HELLO WORLD
engine.Render(new { Title = "My Blog Post" }, "{{Slugify(Title)}}"); // → my-blog-post
9. Error Handling
All exceptions include line number, column, and the offending token.
try
{
engine.Render(model, "{{#if User.}}"); // malformed expression
}
catch (TemplateSyntaxException ex)
{
Console.WriteLine(ex.Message);
// → Unexpected token '.' in expression (Line 1, Column 11)
// Token: .
}
// Exception types:
// TemplateParseException — lexer/tokenizer errors
// TemplateSyntaxException — parser structure errors
// PropertyResolutionException — property not found on model
// RenderingException — runtime render errors
10. Template Caching
Templates are parsed once and cached automatically. No extra configuration needed.
var engine = new HtmlTemplateEngine(cacheCapacity: 2000); // default: 1000
engine.Render(model, template); // parsed + cached
engine.Render(model, template); // served from cache — no re-parse
Console.WriteLine(engine.CachedTemplateCount); // → 1
engine.ClearCache();
11. Thread Safety
The engine instance is safe to share across threads. Each Render() call creates its own context stack and renderer internally.
// Shared singleton — safe for concurrent use
var engine = new HtmlTemplateEngine();
Parallel.ForEach(requests, req =>
{
var html = engine.Render(req.Model, req.Template);
});
Requirements
- .NET 8.0+
- No third-party runtime dependencies
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Added support for untyped models:
- JsonElement
- Dictionary<string, object>
- ExpandoObject
Added null-safe property access support.
Improved template rendering reliability.