Skip to content

jamesgober/dotnet-webkit-views

Repository files navigation

JG.WebKit.Views

Enterprise-grade, high-performance template engine for .NET 8+

.NET NuGet NuGet Downloads License CI

JG.WebKit.Views is a blazing-fast, feature-rich template engine designed for production workloads. Built from the ground up for .NET 8+, it provides compiled delegate rendering, comprehensive syntax support, and enterprise-ready features like hot-reload, caching, and extensibility.

Features

  • High Performance - Compiled delegate rendering with zero-allocation HTML escaping
  • Rich Syntax - Variables, conditionals, loops, partials, layouts, helpers
  • Extensible - Custom helpers, template providers, and hooks
  • Hot Reload - FileSystemWatcher integration for instant template updates
  • Smart Caching - Template compilation caching with granular invalidation
  • Secure - HTML escaping by default, XSS protection built-in
  • Flexible - File system or in-memory providers, perfect for testing
  • Well Documented - Full XML docs, comprehensive guides, real-world examples

Quick Start

Installation

dotnet add package JG.WebKit.Views

Basic Usage

using JG.WebKit.Views;
using JG.WebKit.Views.Providers;

// Set up the engine
var provider = new FileTemplateProvider("./templates");
var options = new ViewEngineOptions();
var engine = ViewEngineFactory.Create(provider, options);

// Prepare data
var data = new Dictionary<string, object?>
{
    ["title"] = "Welcome",
    ["user"] = new { Name = "John", Email = "john@example.com" }
};
var context = new TemplateContext(data);

// Render
var html = await engine.RenderAsync("index", context);

Template Syntax

Variables

{{ title }}                    <!-- HTML-escaped output -->
{{{ rawHtml }}}               <!-- Raw/unescaped output -->
{{ user.name }}               <!-- Dot notation -->
{{ items[0] }}                <!-- Array indexing -->
{{ user.profile.settings }}   <!-- Deep nesting -->

Conditionals

{{#if loggedIn }}
  Welcome back, {{ user.name }}!
{{#elseif isGuest }}
  Welcome, guest!
{{#else}}
  Please log in.
{{/if}}

<!-- Comparison operators -->
{{#if count > 10 }}Many items{{/if}}
{{#if age >= 18 }}Adult{{/if}}
{{#if status == "active" }}Active{{/if}}
{{#if !isEmpty }}Has content{{/if}}

Loops

{{#each posts as post }}
  <article>
    <h2>{{ post.title }}</h2>
    <p>Item {{ index }} of {{ posts.length }}</p>
  </article>
{{#empty}}
  <p>No posts available.</p>
{{/each}}

<!-- Nested loops -->
{{#each categories as category }}
  <h3>{{ category.name }}</h3>
  {{#each category.items as item }}
    <li>{{ item }}</li>
  {{/each}}
{{/each}}

Partials

<!-- Include a partial -->
{{> header }}

<!-- Partial with variant -->
{{> card "featured" }}

<!-- Partial with context -->
{{> userCard user }}

<!-- In userCard.html -->
<div class="user-card">
  <h3>{{ name }}</h3>
  <p>{{ email }}</p>
</div>

Layouts

<!-- page.html -->
{{#layout "main" }}
<h1>{{ title }}</h1>
<p>Page content here</p>

{{#section "sidebar" }}
  <aside>Custom sidebar</aside>
{{/section}}

<!-- main.html -->
<html>
<head><title>{{ title }}</title></head>
<body>
  <nav>Site navigation</nav>
  <main>
    {{#yield "content" }}
  </main>
  <aside>
    {{#yield-default "sidebar" }}
      <p>Default sidebar</p>
    {{/yield-default}}
  </aside>
</body>
</html>

Helpers

<!-- Built-in helpers -->
{{ date publishedAt "yyyy-MM-dd" }}
{{ truncate description 100 }}
{{ uppercase title }}
{{ lowercase email }}
{{ json data }}

<!-- Utility helpers -->
{{ default title "Untitled Page" }}
{{ ifval user.isAdmin "Admin" "User" }}
{{ concat firstName " " lastName }}
{{ replace slug "-" " " }}
{{ count items }}

<!-- Asset helpers with CDN and versioning -->
<link rel="stylesheet" href="{{ css "styles.css" }}">
<script src="{{ js "app.js" }}"></script>
<img src="{{ img "logo.png" }}" alt="Logo">

Comments

{{-- This is a comment and won't be rendered --}}
{{-- {{ debugVariable }} --}}

Configuration

var options = new ViewEngineOptions
{
    // Template settings
    TemplateExtension = ".html",
    LayoutPath = "_layouts",
    PartialPath = "_partials",
    
    // Security
    AllowRawOutput = false, // Disable {{{ }}} for security
    
    // Performance
    CacheCompiledTemplates = true,
    MaxIncludeDepth = 10,
    
    // Asset handling
    AssetBasePath = "/assets",
    CdnBaseUrl = "https://cdn.example.com",
    AssetVersionHash = "abc123" // For cache busting
};

Advanced Features

Custom Helpers

public class MarkdownHelper : ITemplateHelper
{
    public string Name => "markdown";
    
    public string Execute(object?[] arguments, TemplateContext context)
    {
        if (arguments.Length == 0) return string.Empty;
        var markdown = arguments[0]?.ToString() ?? string.Empty;
        return Markdig.Markdown.ToHtml(markdown);
    }
}

// Register helper
var helpers = new Dictionary<string, ITemplateHelper>
{
    ["markdown"] = new MarkdownHelper()
};
var engine = ViewEngineFactory.Create(provider, options, helpers);

// Use in template
{{ markdown post.content }}

Global Data

var data = new Dictionary<string, object?> { ["page"] = pageData };
var globals = new Dictionary<string, object?>
{
    ["siteName"] = "My Website",
    ["year"] = 2026,
    ["config"] = appConfig
};
var context = new TemplateContext(data, globals);

// Globals available in all templates and partials
{{ siteName }} - {{ year }}

Cache Invalidation

// Invalidate specific template
await engine.InvalidateCacheAsync("blog/post");

// Clear entire cache
await engine.InvalidateAllAsync();

Hot Reload (Development)

var provider = new FileTemplateProvider(
    "./templates",
    enableHotReload: true  // Auto-reload on file changes
);

Performance

JG.WebKit.Views is optimized for production workloads:

  • Template Compilation: 1-2ms for typical templates
  • Cached Rendering: <100μs for simple templates
  • Memory Efficient: Zero allocations in hot paths
  • Fast Expressions: 10-50μs evaluation time
  • Scales Linearly: Tested with 10,000+ item loops

Benchmark: Rendering a complex blog post page (layout + 5 partials + 50 comments):

  • First render (cold): ~5ms
  • Cached renders: ~200μs
  • Memory: <1KB allocations per render

Real-World Examples

Blog Post Page

// Controller
public async Task<IActionResult> Post(string slug)
{
    var post = await _db.Posts.FindAsync(slug);
    
    var data = new Dictionary<string, object?>
    {
        ["post"] = post,
        ["comments"] = await _db.Comments.Where(c => c.PostId == post.Id).ToListAsync(),
        ["relatedPosts"] = await _db.Posts.Where(p => p.Category == post.Category).Take(5).ToListAsync()
    };
    
    var context = new TemplateContext(data);
    var html = await _viewEngine.RenderAsync("blog/post", context);
    
    return Content(html, "text/html");
}

Template: blog/post.html

{{#layout "main" }}
<article class="post">
  <header>
    <h1>{{ post.title }}</h1>
    <time>{{ date post.publishedAt "MMMM dd, yyyy" }}</time>
    <span>By {{ post.author }}</span>
  </header>
  
  <div class="content">
    {{{ post.bodyHtml }}}
  </div>
  
  <footer>
    {{#if post.tags }}
      <div class="tags">
        {{#each post.tags as tag }}
          <span class="tag">{{ tag }}</span>
        {{/each}}
      </div>
    {{/if}}
  </footer>
</article>

<section class="comments">
  <h2>Comments ({{ comments.length }})</h2>
  {{#each comments as comment }}
    {{> comment comment }}
  {{#empty}}
    <p>No comments yet. Be the first!</p>
  {{/each}}
</section>

{{#section "sidebar" }}
  <aside>
    <h3>Related Posts</h3>
    {{#each relatedPosts as related }}
      {{> postCard related }}
    {{/each}}
  </aside>
{{/section}}

Email Templates

// Email service
public async Task SendWelcomeEmail(User user)
{
    var data = new Dictionary<string, object?>
    {
        ["user"] = user,
        ["verificationLink"] = GenerateVerificationLink(user.Id)
    };
    
    var context = new TemplateContext(data);
    var html = await _viewEngine.RenderAsync("emails/welcome", context);
    
    await _emailService.SendAsync(user.Email, "Welcome!", html);
}

Template: emails/welcome.html

{{#layout "email-base" }}
<h1>Welcome, {{ user.name }}!</h1>
<p>Thank you for joining our platform.</p>
<p>Please verify your email address:</p>
<a href="{{ verificationLink }}" class="button">Verify Email</a>

API Response Rendering

// API endpoint with templated responses
[HttpGet("api/widget/{id}")]
public async Task<IActionResult> GetWidget(int id)
{
    var widget = await _db.Widgets.FindAsync(id);
    
    var data = new Dictionary<string, object?> { ["widget"] = widget };
    var context = new TemplateContext(data);
    
    var html = await _viewEngine.RenderStringAsync(
        "<div class='widget'>{{ widget.name }}: ${{ widget.price }}</div>",
        context
    );
    
    return Content(html, "text/html");
}

Documentation

Testing

# Run all tests
dotnet test

# Run with coverage
dotnet test /p:CollectCoverage=true

Test Coverage: 175 tests, 100% pass rate, <1s execution time

Contributing

Contributions welcome! Please read CONTRIBUTING.md for guidelines.

License

Apache License 2.0 - see LICENSE for details.

Support

Acknowledgments

Built with ❤️ for the .NET community. Inspired by Handlebars, Liquid, and Razor syntax.


Version: 1.0.0 | Released: March 6, 2026 | Status: Production Ready

About

Template engine for .NET 8. Three rendering modes: file-based for dev, compiled for production, database-stored for CMS. Layouts, partials, loops, and helpers.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages