Agent skill
dotliquid
Guidance for DotLiquid template engine for .NET. USE FOR: safe user-generated templates, email templates, CMS content rendering, sandboxed template execution, report generation from data models. DO NOT USE FOR: Razor-based server-side views (use ASP.NET Razor), logic-heavy templates requiring full C# (use Scriban or Razor), compiled template performance-critical paths.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/dotliquid
Metadata
Additional technical details for this skill
- author
- Tyler-R-Kendrick
- version
- 1.0.0
- displayName
- DotLiquid
SKILL.md
DotLiquid
Overview
DotLiquid is a .NET port of the Liquid template engine originally created by Shopify. It provides a secure, sandboxed template rendering system where templates cannot execute arbitrary code, making it ideal for user-generated content, email templates, and CMS rendering. Templates use {{ variable }} for output and {% tag %} for control flow.
DotLiquid supports two rendering modes: by default, it uses a safe model where only explicitly registered types and properties are accessible from templates. This prevents untrusted templates from accessing sensitive data or calling arbitrary methods.
Install via NuGet:
dotnet add package DotLiquid
Basic Template Rendering
Parse a template string and render it with a hash of values. Templates are compiled once and can be rendered multiple times with different data.
using DotLiquid;
// Simple variable substitution
var template = Template.Parse("Hello, {{ name }}! You have {{ count }} new messages.");
var output = template.Render(Hash.FromAnonymousObject(new { name = "Alice", count = 5 }));
// Output: "Hello, Alice! You have 5 new messages."
// Reuse the compiled template with different data
var output2 = template.Render(Hash.FromAnonymousObject(new { name = "Bob", count = 12 }));
Control Flow and Iteration
DotLiquid supports if/elsif/else, unless, for loops, and case/when for branching logic inside templates.
using DotLiquid;
var invoiceTemplate = Template.Parse(@"
Invoice #{{ invoice.number }}
Date: {{ invoice.date | date: '%B %d, %Y' }}
{% for item in invoice.items %}
{{ item.name }} - {{ item.quantity }} x ${{ item.price }} = ${{ item.total }}
{% endfor %}
{% if invoice.discount > 0 %}
Discount: -${{ invoice.discount }}
{% endif %}
Total: ${{ invoice.grand_total }}
{% if invoice.paid %}
Status: PAID
{% else %}
Status: UNPAID - Due by {{ invoice.due_date | date: '%m/%d/%Y' }}
{% endif %}
");
var result = invoiceTemplate.Render(Hash.FromAnonymousObject(new
{
invoice = new
{
number = "INV-2025-001",
date = DateTime.Now,
items = new[]
{
new { name = "Widget A", quantity = 3, price = 10.00, total = 30.00 },
new { name = "Widget B", quantity = 1, price = 25.00, total = 25.00 }
},
discount = 5.00,
grand_total = 50.00,
paid = false,
due_date = DateTime.Now.AddDays(30)
}
}));
Custom Filters
Filters transform output values. Register custom filters by creating a static class with static methods that DotLiquid calls during rendering.
using DotLiquid;
public static class CustomFilters
{
public static string Truncate(string input, int length)
{
if (string.IsNullOrEmpty(input) || input.Length <= length)
return input;
return input[..length] + "...";
}
public static string Currency(decimal input, string symbol = "$")
{
return $"{symbol}{input:N2}";
}
public static string Pluralize(int count, string singular, string plural)
{
return count == 1 ? singular : plural;
}
}
// Register filters globally
Template.RegisterFilter(typeof(CustomFilters));
var template = Template.Parse(
"{{ description | truncate: 50 }} - {{ price | currency }} " +
"({{ qty }} {{ qty | pluralize: 'item', 'items' }})");
var output = template.Render(Hash.FromAnonymousObject(new
{
description = "A very long product description that should be truncated for display",
price = 29.99m,
qty = 3
}));
Registering Safe Types (Drop Classes)
For security, DotLiquid does not expose CLR objects directly. Use Drop classes or register safe types to control what template authors can access.
using DotLiquid;
using System.Collections.Generic;
public class ProductDrop : Drop
{
private readonly Product _product;
public ProductDrop(Product product)
{
_product = product;
}
public string Name => _product.Name;
public decimal Price => _product.Price;
public string Category => _product.Category;
// Methods accessible in templates
public string FormattedPrice() => $"${_product.Price:N2}";
}
// Alternatively, register types as safe for direct access
Template.RegisterSafeType(typeof(Product), new[] { "Name", "Price", "Category" });
// Using Drop in a template
var template = Template.Parse("{{ product.name }} costs {{ product.formatted_price }}");
var hash = Hash.FromAnonymousObject(new { product = new ProductDrop(myProduct) });
var output = template.Render(hash);
Custom Tags
Create custom tags by inheriting from Tag for block-level template constructs.
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using DotLiquid;
public class HighlightTag : Tag
{
private string _cssClass = "highlight";
public override void Initialize(string tagName, string markup, List<string> tokens)
{
base.Initialize(tagName, markup, tokens);
var match = Regex.Match(markup.Trim(), @"class:\s*""(\w+)""");
if (match.Success)
_cssClass = match.Groups[1].Value;
}
public override void Render(Context context, TextWriter result)
{
result.Write($"<span class=\"{_cssClass}\">");
RenderAll(NodeList, context, result);
result.Write("</span>");
}
}
// Register the custom tag
Template.RegisterTag<HighlightTag>("highlight");
// Use in template: {% highlight class: "important" %}This is highlighted{% endhighlight %}
DotLiquid vs Other Template Engines
| Feature | DotLiquid | Scriban | Handlebars.NET | Razor |
|---|---|---|---|---|
| Sandboxed execution | Yes (default) | Optional | No | No |
| User-generated templates | Excellent | Good | Good | Dangerous |
| Full C# expressions | No | Yes | No | Yes |
| Logic-free philosophy | Yes | No | Yes | No |
| Performance | Good | Excellent | Good | Excellent |
| Template syntax | Liquid/Shopify | Liquid-like | Mustache | HTML+C# |
Integrating with Dependency Injection
Wrap template rendering in a service for clean DI integration.
using System.Threading.Tasks;
using DotLiquid;
public interface ITemplateRenderer
{
string Render(string templateSource, object model);
}
public class LiquidTemplateRenderer : ITemplateRenderer
{
public LiquidTemplateRenderer()
{
Template.RegisterFilter(typeof(CustomFilters));
}
public string Render(string templateSource, object model)
{
var template = Template.Parse(templateSource);
var hash = Hash.FromAnonymousObject(model);
return template.Render(hash);
}
}
// Registration
// builder.Services.AddSingleton<ITemplateRenderer, LiquidTemplateRenderer>();
Best Practices
- Cache compiled
Templateinstances by callingTemplate.Parseonce and reusing the result across renders, since parsing is the most expensive step. - Use
Dropclasses for domain models instead ofRegisterSafeTypewhen you need to control exactly which properties and methods are exposed to templates. - Register custom filters in a startup path (e.g., application initialization) rather than per-request, since
Template.RegisterFilteris a global static operation. - Validate template syntax before storing user templates by wrapping
Template.Parsein a try/catch forSyntaxExceptionand returning errors to the user. - Use
Hash.FromAnonymousObjectfor simple data but switch toHash.FromDictionarywhen building data dynamically from multiple sources. - Prefer DotLiquid naming conventions (snake_case in templates mapped to PascalCase in C#) by configuring
Template.NamingConvention = new RubyNamingConvention(). - Limit template execution time in user-facing scenarios by setting
Template.DefaultMaxIterationsto prevent infinite loops in user-authored templates. - Avoid embedding business logic in templates -- keep templates focused on presentation and move calculations into the model or filter layer.
- Use the
includetag with registered file systems for template composition instead of duplicating template fragments. - Test templates with edge cases including null values, empty collections, and missing keys to ensure templates degrade gracefully without crashing.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?