Sniz.HtmlTemplateEngine 1.1.0

dotnet add package Sniz.HtmlTemplateEngine --version 1.1.0
                    
NuGet\Install-Package Sniz.HtmlTemplateEngine -Version 1.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Sniz.HtmlTemplateEngine" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Sniz.HtmlTemplateEngine" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Sniz.HtmlTemplateEngine" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Sniz.HtmlTemplateEngine --version 1.1.0
                    
#r "nuget: Sniz.HtmlTemplateEngine, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Sniz.HtmlTemplateEngine@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Sniz.HtmlTemplateEngine&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Sniz.HtmlTemplateEngine&version=1.1.0
                    
Install as a Cake Tool

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}}");
// → &lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;

// 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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.

Version Downloads Last Updated
1.1.0 52 6/4/2026
1.0.2 98 5/31/2026

Added support for untyped models:
- JsonElement
- Dictionary<string, object>
- ExpandoObject

Added null-safe property access support.
Improved template rendering reliability.