LowlandTech.TinyTools 2026.1.4

Version Build Status Docs Status Documentation License: MIT Twitter: wendellmva

A lightweight template engine for .NET with minimal dependencies. Designed for data composition, not view rendering.

🏠 Homepage


Why TinyTemplateEngine?

Most .NET templating solutions—RazorEngine, RazorLight, and similar—are built around one core assumption:

Templates are views, and views are primarily HTML.

That assumption becomes a liability once your problem is data composition, not UI rendering.

The Razor Problem

Razor excels at MVC-style view rendering, but it introduces friction when used as a general-purpose templating engine:

  • HTML-first design
    Razor tightly couples templates to HTML and view concepts, even when the output is not a web page.

  • Compile-time complexity
    Runtime compilation, Roslyn dependencies, caching layers, and AppDomain constraints add overhead for problems that don't require them.

  • Control-flow leakage
    Logic (@if, @foreach, helpers) creeps into templates, blurring the line between data preparation and data projection.

  • Poor fit for non-visual outputs
    Generating JSON, YAML, Markdown, config files, prompts, emails, or documents feels unnatural and verbose.

After migrating from RazorEngine to RazorLight, the core issue remained:
the templating model itself was working against the use case.

A Different Assumption

TinyTemplateEngine starts from a different premise:

A template is a projection of data — not a view, not a page, and not an application.

That shift enables a simpler and more predictable model.

What TinyTemplateEngine Optimizes For

What TinyTemplateEngine Optimizes For

  • Data-first templating
    Templates exist to merge structured data into text—nothing more.

  • Minimal surface area
    No compilation step, no HTML bias, no runtime code execution.

  • Explicit separation of concerns

    • Data is prepared outside the template

    • Templates only describe shape and placement

  • Format-agnostic output
    Works equally well for:

    • Text

    • Markdown

    • JSON / YAML

    • Config files

    • Prompts

    • Emails

    • Code generation

  • Predictable behavior
    No hidden execution model, no side effects, no magic.

When to Use TinyTemplateEngine

Use it when:

  • You are merging data into templates, not rendering views

  • You want templates that are safe, readable, and boring

  • You care more about composition and transformation than UI

  • Razor's power is getting in the way, not helping

If you need a full view engine, Razor is still the right tool.
If you need a small, deterministic templating engine, TinyTemplateEngine exists because that gap was real.

Install

dotnet add package LowlandTech.TinyTools

Usage

Basic String Interpolation

// Simple property interpolation with {PropertyName} syntax
var template = "Hello {FirstName} {LastName}!";
var model = new { FirstName = "John", LastName = "Smith" };

var result = template.Interpolate(model);
// Output: "Hello John Smith!"

Dictionary Interpolation

var template = "Welcome to {City}, {Country}!";
var data = new Dictionary<string, string>
{
    { "City", "Amsterdam" },
    { "Country", "Netherlands" }
};

var result = template.Interpolate(data);
// Output: "Welcome to Amsterdam, Netherlands!"

TinyTemplateEngine (Advanced)

var engine = new TinyTemplateEngine();
var context = new ExecutionContext();
context.Set("Name", "Alice");
context.Set("IsPremium", true);
context.Set("Items", new[] { "Item 1", "Item 2", "Item 3" });

var template = """
    Hello ${Context.Name}!
    
    @if (Context.IsPremium) {
    You have premium access.
    } else {
    Upgrade to premium for more features.
    }
    
    Your items:
    @foreach (var item in Context.Items) {
    - ${item}
    }
    """;

var result = engine.Render(template, context);

Using with Models

var engine = new TinyTemplateEngine();
var context = new ExecutionContext
{
    Model = new Customer
    {
        FirstName = "Jane",
        LastName = "Doe",
        Orders = new List<Order>
        {
            new Order { OrderNumber = "ORD-001", Total = "99.99" }
        }
    }
};

var template = """
    Dear ${Context.Model.FirstName} ${Context.Model.LastName},
    
    @foreach (var order in Context.Model.Orders) {
    Order #${order.OrderNumber} - Total: $${order.Total}
    }
    """;

var result = engine.Render(template, context);

Null Coalescing

var template = """
    Name: ${Context.Name ?? "Guest"}
    Title: ${Context.Title ?? "No title provided"}
    """;

Conditional Logic with Else-If

var template = """
    @if (Context.Score >= 90) {
    Grade: A
    } else if (Context.Score >= 80) {
    Grade: B
    } else if (Context.Score >= 70) {
    Grade: C
    } else {
    Grade: F
    }
    """;

Features

Feature Syntax Example
Variable Interpolation ${Context.xxx} ${Context.Model.Name}
Null Coalescing ${expr ?? "default"} ${Context.Title ?? "Untitled"}
Pipe Helpers ${expr \| helper} ${Context.Name \| upper}
Conditionals @if (condition) { } @if (Context.IsActive) { ... }
Else-If Chains } else if (condition) { } else if (Context.Role == "admin") { ... }
Negation @if (!condition) { } @if (!Context.IsExpired) { ... }
Iteration @foreach (var x in collection) { } @foreach (var item in Context.Items) { ... }
Comments @* comment *@ @* TODO: Fix this *@
Comparison Operators >, >=, <, <=, ==, != @if (Context.Age >= 21) { ... }

Pipe Helpers

Transform values using the pipe syntax: ${Context.Value | helper} or ${Context.Value | helper:argument}

String Helpers

Helper Example Output
upper ${Context.Name \| upper} JOHN
lower ${Context.Name \| lower} john
capitalize ${Context.Name \| capitalize} John
camelcase ${Context.Name \| camelcase} firstName
pascalcase ${Context.Name \| pascalcase} FirstName
trim ${Context.Text \| trim} hello
truncate:N ${Context.Desc \| truncate:20} This is a long te...
replace:old,new ${Context.Path \| replace:old,new} Replaces text
padleft:N,char ${Context.Id \| padleft:5,0} 00042
padright:N,char ${Context.Name \| padright:10,.} John......

Date Helpers

Helper Example Output
format:pattern ${Context.Date \| format:yyyy-MM-dd} 2024-06-15
date ${Context.Date \| date} 2024-06-15
date:pattern ${Context.Date \| date:dd-MMM-yyyy} 15-Jun-2024

Number Helpers

Helper Example Output
number ${Context.Value \| number} 1,234
format:N2 ${Context.Price \| format:N2} 1,234.57
format:C ${Context.Price \| format:C} $1,234.57
format:P0 ${Context.Rate \| format:P0} 86%
round:N ${Context.Pi \| round:2} 3.14
floor ${Context.Value \| floor} 3
ceiling ${Context.Value \| ceiling} 4

Collection Helpers

Helper Example Output
count ${Context.Items \| count} 5
first ${Context.Items \| first} First item
last ${Context.Items \| last} Last item
join:separator ${Context.Tags \| join:, } a, b, c
reverse ${Context.Word \| reverse} olleH

Conditional Helpers

Helper Example Output
default:value ${Context.Name \| default:Guest} Guest if null
ifempty:value ${Context.Title \| ifempty:N/A} N/A if empty
yesno ${Context.Active \| yesno} Yes or No
yesno:yes,no ${Context.Active \| yesno:On,Off} On or Off

Chaining Helpers

Helpers can be chained together:

${Context.Name | trim | upper | truncate:20}
${Context.Items | first | upper}
${Context.Date | format:MMMM | upper}

Template Services (Extensibility)

The core library stays tiny by design. Complex features like advanced pluralization or calculations are provided through Template Services—simple functions you register with string keys.

How It Works

Two Ways to Register Services:

1. Simple Functions (Quick & Easy)

// Inline lambda - perfect for simple transformations
context.RegisterService("pluralize", input => input?.ToString()?.Pluralize());

// Use in templates
var template = "We have ${Context.Services('pluralize')('customer')}";
// Output: "We have customers"

2. ITemplateService (IoC/DI)

// Implement the interface
public class HumanizerService : ITemplateService
{
    public string Name => "pluralize";
    
    public object? Transform(object? input)
    {
        return input?.ToString()?.Pluralize();
    }
}

// Register (simple)
context.RegisterService(new HumanizerService());

// Or with dependency injection (ASP.NET Core)
services.AddSingleton<ITemplateService, HumanizerService>();

// In controller
public MyController(IEnumerable<ITemplateService> services)
{
    var context = new ExecutionContext();
    context.RegisterServices(services);  // Registers all IoC services
}

Example: Pluralization with Humanizer

// Install: dotnet add package Humanizer.Core
using Humanizer;

var context = new ExecutionContext();

// Register pluralization service
context.RegisterService("pluralize", input => 
    input?.ToString()?.Pluralize() ?? "");

context.RegisterService("singularize", input => 
    input?.ToString()?.Singularize() ?? "");

// Use in template
var template = "We have 5 ${Context.Services('pluralize')('customer')}.";
var result = engine.Render(template, context);
// Output: "We have 5 customers."

Example: Calculations with NCalc

// Install: dotnet add package NCalc
using NCalc;

// Register calculation service
context.RegisterService("calc", input =>
{
    var expr = new Expression(input?.ToString() ?? "0");
    return expr.Evaluate();
});

// Use in template
var template = "Total: $${Context.Services('calc')('19.99 * 5 * 1.08')}";
var result = engine.Render(template, context);
// Output: "Total: $107.9460"

Service Not Found

If a service isn't registered, you get a clear error message:

var template = "${Context.Services('unknown')('test')}";
// Output: "{unknown not registered}"

Real-World Example: Invoice Generation

// Register services
context.RegisterService("pluralize", input => input?.ToString()?.Pluralize() ?? "");
context.RegisterService("calc", input =>
{
    var expr = new Expression(input?.ToString() ?? "0");
    var result = expr.Evaluate();
    return result is double d ? d.ToString("F2") : result;
});

// Template
var template = """
    Invoice
    -------
    Items: ${Context.Services('calc')('5')} ${Context.Services('pluralize')('widget')}
    Subtotal: $${Context.Services('calc')('19.99 * 5')}
    Tax: $${Context.Services('calc')('19.99 * 5 * 0.08')}
    Total: $${Context.Services('calc')('19.99 * 5 * 1.08')}
    """;

// Output:
// Invoice
// -------
// Items: 5 widgets
// Subtotal: $99.95
// Tax: $8.00
// Total: $107.95

Why This Approach?

Core stays tiny - Zero unnecessary dependencies
Pay for what you use - Only add services you need
Simple - Services are just functions, no complex interfaces
Testable - Easy to mock in unit tests
Flexible - Create any transformation you need
IoC-friendly - Full dependency injection support

Services are simple functions that transform data—nothing more, nothing less.

Advanced: IoC/DI Integration

For production applications with ASP.NET Core or other DI containers, see: 📖 IoC Integration Guide

  • Implement ITemplateService for full DI support
  • Inject services from IoC container
  • Access dependencies (ILogger, IConfiguration, etc.)
  • Production-ready patterns and best practices

Use Cases

  • 📧 Email/Letter Templates - Personalized communications
  • 💻 Code Generation - Generate boilerplate code from models
  • ⚙️ Configuration Files - Environment-specific configs
  • 📄 Documentation - Auto-generate docs from metadata
  • 🧾 Invoices/Reports - Dynamic document generation

Author

👤 wendellmva

🤝 Contributing

Contributions, issues and feature requests are welcome!
Feel free to check issues page.

Show your support

Give a ⭐️ if this project helped you!


This README was generated with ❤️ by readme-md-generator

Showing the top 20 packages that depend on LowlandTech.TinyTools.

Packages Downloads
LowlandTech.Graph
A comprehensive graph database system for .NET with support for nodes, edges, properties, validation rules, and AI-powered RAG capabilities. Includes declarative validation engine with NCalc, vector embeddings with pgvector, and multi-agent system for code generation and reasoning.
3
LowlandTech.Graph
A comprehensive graph database system for .NET with support for nodes, edges, properties, validation rules, and AI-powered RAG capabilities. Includes declarative validation engine with NCalc, vector embeddings with pgvector, and multi-agent system for code generation and reasoning.
0

# 2026.1.3 ## ITemplate System - Self-Validating Code Generation Templates ### New Features * **ITemplate Interface** - Template-first approach to code generation - Self-validating templates with embedded test cases - Each template carries its own `TestDataJson` and `ExpectedContent` - Automatic validation via `Validate()` and `ValidateDetailed()` methods - Type-safe with `DataType` property for compile-time safety * **TemplateBase Abstract Class** - Base implementation for templates - Handles data serialization/normalization automatically - Template rendering via `TinyTemplateEngine` - Detailed validation with diff reporting - Cross-platform line ending normalization * **TemplateRegistry** - Centralized template management - `Register()` / `Get()` for manual template registration - `DiscoverFromAssembly()` - Auto-discover templates via reflection - `DiscoverFromCallingAssembly()` - Discover from calling code - `RenderBatch()` - Render multiple templates in one call - `ValidateAll()` - Validate all registered templates at once * **TemplateResult Record** - Rich rendering output - `Content` - The rendered template content - `Path` - Dynamic output path (supports variable interpolation) - `Namespace` - Generated namespace - `Metadata` - Optional key-value metadata dictionary * **TinyTemplateEngine Enhancements** - Logical AND operator (`&&`) in conditions - Logical OR operator (`||`) in conditions - Ternary expressions: `${Context.Active ? 'Yes' : 'No'}` - Short-circuit evaluation for logical operators - Parenthesized logical expressions ### Documentation * New `Self-Validating-Templates.md` comprehensive guide * Updated `ITemplate-QuickRef.md` with new syntax features * New ITemplate page on documentation website (`/itemplate`) * Template syntax reference cards ### Testing * **Test count: 577 ? 633 tests (+56)** * Fixed skipped test `ItShouldInterpolateFileExtension` * `RenderBatch()` method tests (18 tests) * `DiscoverFromAssembly()` method tests (21 tests) * `ValidateAll()` edge case tests (10 tests) * Null deserialization handling tests (3 tests) * Exception handling in validation tests (4 tests) ### Infrastructure * NCrunch configuration to exclude site folder * VS Code launch configuration for site development * Tasks configuration for npm dev server ### Examples * `ComponentTemplate` - React component generator * `CSharpClassTemplate` - C# class with properties/methods generator --- # 2026.1.1 ## Major Release - Template Services & Architecture Improvements ### New Features * **Template Services Architecture** - Extensible service registration system - Simple function-based services with string keys - Syntax: `${Context.Services('serviceName')(input)}` - Support for both string literals and variable references - IoC-friendly design * **Enhanced Variable Resolver** - Method call support with string arguments - Chained function calls (e.g., `Services('pluralize')('word')`) - Direct delegate invocation - Variable reference support in method calls ### Documentation * Added comprehensive "Why TinyTemplateEngine?" section * Template Services examples with Humanizer and NCalc * Real-world usage examples (invoice generation, etc.) * Sample implementations and best practices ### Architecture * Introduced `ITemplateService` marker interface * Added `TemplateServiceFunc` delegate type * Context service inheritance to child contexts * Error handling with clear "{service} not registered" messages ### Testing * **Comprehensive test coverage expansion** - 1,100 ? 1,293 tests (+193) * Complete test suite for template services * Multi-language pluralization tests * Calculator service tests * Integration tests with real-world scenarios * **TemplateHelpers test coverage:** - `PadLeft` / `PadRight` helper tests - `Round` (decimal/float), `Default`, `IfEmpty` tests - `Floor`, `Ceiling`, `Count`, `First`, `Last`, `Reverse` tests - `Format` helper tests (DateTime, DateTimeOffset, DateOnly, TimeOnly, IFormattable) - `Register` custom helper tests - `YesNo` and `IsEmpty` conditional helper tests * **TinyTemplateEngine test coverage:** - Negation operator tests (`!Context.Value`) - Comparison operator edge cases - `IsTruthy` method branch coverage - `Compare` and `AreEqual` method tests * **InterpolationExtensions test coverage:** - `InterpolateWithEngine<T>` tests - `Interpolate(ExecutionContext)` tests - `Interpolate(List<string>, ExecutionContext)` tests ### Bug Fixes * **Fixed delegate property resolution in VariableResolver** - `ExecutionContext.Get()` method was not being invoked for delegate properties, causing template services to fail silently ### Infrastructure * Upgraded to .NET 8, 9, and 10 multi-targeting * Modern GitHub Actions CI/CD workflows * Cross-platform testing (Ubuntu, Windows, macOS) * Code coverage integration * Source Link support for better debugging * Professional NuGet package metadata ### Dependencies * Tests now include Humanizer.Core (2.14.1) for examples * Tests now include NCalc (1.0.0) for calculator examples * Added Microsoft.SourceLink.GitHub for source debugging ### Breaking Changes * Removed built-in `pluralize` and `singularize` helpers from core - Now available via Template Services pattern - Keeps core library minimal and focused ### Migration Guide **Before (2.0.x):** ```csharp // Built-in helpers (removed) var template = "${Context.EntityName | pluralize}"; ``` **After (2026.1.1):** ```csharp // Register as a service context.RegisterService("pluralize", input => input?.ToString()?.Pluralize()); var template = "${Context.Services('pluralize')(Context.EntityName)}"; ``` # 2.0.2 * add debug verification # 2.0.1 * fix tests # 2.0.0 * upgrade to .net 7 * upgrade dependencies * remove support for legacy framework * move from gitlab to github # 1.6.1 * fix path bug * add support for files and collection of templates # 1.0.5 * make methods testharness virtual * change namespaces for the unittests # 1.0.4 * changed namespaces * add test harness * replace shouldy with fluentassertions # 1.0.3 * add push for github * add release notes * add license * remove csproj backup file * fix bug in test # 1.0.2: * rearrange repository layout * added ci # 1.0.1 * added some more tests # 1.0.0 * created interpolation extension for strings

Version Downloads Last updated
2026.1.4 4 02/04/2026