SkillAgentSearch skills...

Handlebars.Net

A real .NET Handlebars engine

Install / Use

/learn @Handlebars-Net/Handlebars.Net
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Handlebars.Net

CI Nuget performance


Quality Gate Status Reliability Rating Security Rating

Bugs Code Smells Coverage


Stack Exchange questions GitHub issues questions GitHub issues help wanted


Blistering-fast Handlebars.js templates in your .NET application.

Handlebars.js is an extension to the Mustache templating language created by Chris Wanstrath. Handlebars.js and Mustache are both logicless templating languages that keep the view and the code separated like we all know they should be.

Check out the handlebars.js documentation for how to write Handlebars templates.

Handlebars.Net doesn't use a scripting engine to run a Javascript library - it compiles Handlebars templates directly to IL bytecode. It also mimics the JS library's API as closely as possible.

Install

dotnet add package Handlebars.Net

Extensions

The following projects are extending Handlebars.Net:

Usage

string source =
@"<div class=""entry"">
  <h1>{{title}}</h1>
  <div class=""body"">
    {{body}}
  </div>
</div>";

var template = Handlebars.Compile(source);

var data = new {
    title = "My new post",
    body = "This is my first post!"
};

var result = template(data);

/* Would render:
<div class="entry">
  <h1>My New Post</h1>
  <div class="body">
    This is my first post!
  </div>
</div>
*/

Registering Partials

string source =
@"<h2>Names</h2>
{{#names}}
  {{> user}}
{{/names}}";

string partialSource =
@"<strong>{{name}}</strong>";

Handlebars.RegisterTemplate("user", partialSource);

var template = Handlebars.Compile(source);

var data = new {
  names = new [] {
    new {
        name = "Karen"
    },
    new {
        name = "Jon"
    }
  }
};

var result = template(data);

/* Would render:
<h2>Names</h2>
  <strong>Karen</strong>
  <strong>Jon</strong>
*/

Registering Helpers

Handlebars.RegisterHelper("link_to", (writer, context, parameters) => 
{
    writer.WriteSafeString($"<a href='{context["url"]}'>{context["text"]}</a>");
});

string source = @"Click here: {{link_to}}";

var template = Handlebars.Compile(source);

var data = new {
    url = "https://github.com/rexm/handlebars.net",
    text = "Handlebars.Net"
};

var result = template(data);

/* Would render:
Click here: <a href='https://github.com/rexm/handlebars.net'>Handlebars.Net</a>
*/

This will expect your views to be in the /Views folder like so:

Views\layout.hbs                |<--shared as in \Views            
Views\partials\somepartial.hbs   <--shared as in  \Views\partials
Views\{Controller}\{Action}.hbs 
Views\{Controller}\{Action}\partials\somepartial.hbs 

Registering Block Helpers

Handlebars.RegisterHelper("StringEqualityBlockHelper", (output, options, context, arguments) => 
{
    if (arguments.Length != 2)
    {
        throw new HandlebarsException("{{#StringEqualityBlockHelper}} helper must have exactly two arguments");
    }

    var left = arguments.At<string>(0);
    var right = arguments[1] as string;
    if (left == right) options.Template(output, context);
    else options.Inverse(output, context);
});

var animals = new Dictionary<string, string>() 
{
	{"Fluffy", "cat" },
	{"Fido", "dog" },
	{"Chewy", "hamster" }
};

var template = "{{#each this}}The animal, {{@key}}, {{#StringEqualityBlockHelper @value 'dog'}}is a dog{{else}}is not a dog{{/StringEqualityBlockHelper}}.\r\n{{/each}}";
var compiledTemplate = Handlebars.Compile(template);
string templateOutput = compiledTemplate(animals);

/* Would render
The animal, Fluffy, is not a dog.
The animal, Fido, is a dog.
The animal, Chewy, is not a dog.
*/

Registering Decorators

[Fact]
public void BasicDecorator(IHandlebars handlebars)
{
    string source = "{{#block @value-from-decorator}}{{*decorator 42}}{{@value}}{{/block}}";

    var handlebars = Handlebars.Create();
    handlebars.RegisterHelper("block", (output, options, context, arguments) =>
    {
        options.Data.CreateProperty("value", arguments[0], out _);
        options.Template(output, context);
    });
    
    handlebars.RegisterDecorator("decorator", 
        (TemplateDelegate function, in DecoratorOptions options, in Context context, in Arguments arguments) =>
    {
        options.Data.CreateProperty("value-from-decorator", arguments[0], out _);
    });
    
    var template = handlebars.Compile(source);
    
    var result = template(null);
    Assert.Equal("42", result);
}

For more examples see DecoratorTests.cs

Known limitations:

  • helpers registered inside of a decorator will not override existing registrations

Register custom value formatter

In case you need to apply custom value formatting (e.g. DateTime) you can use IFormatter and IFormatterProvider interfaces:

public sealed class CustomDateTimeFormatter : IFormatter, IFormatterProvider
{
    private readonly string _format;

    public CustomDateTimeFormatter(string format) => _format = format;

    public void Format<T>(T value, in EncodedTextWriter writer)
    {
        if(!(value is DateTime dateTime)) 
            throw new ArgumentException("supposed to be DateTime");
        
        writer.Write($"{dateTime.ToString(_format)}");
    }

    public bool TryCreateFormatter(Type type, out IFormatter formatter)
    {
        if (type != typeof(DateTime))
        {
            formatter = null;
            return false;
        }

        formatter = this;
        return true;
    }
}

[Fact]
public void DateTimeFormatter(IHandlebars handlebars)
{
    var source = "{{now}}";

    var format = "d";
    var formatter = new CustomDateTimeFormatter(format);
    handlebars.Configuration.FormatterProviders.Add(formatter);

    var template = handlebars.Compile(source);
    var data = new
    {
        now = DateTime.Now
    };
    
    var result = template(data);
    Assert.Equal(data.now.ToString(format), result);
}

Notes

  • Formatters are resolved in reverse order according to registration. If multiple providers can provide formatter for a type the last registered would be used.

Shared environment

By default Handlebars will create standalone copy of environment for each compiled template. This is done in order to eliminate a chance of altering behavior of one template from inside of other one.

Unfortunately, in case runtime has a lot of compiled templates (regardless of the template size) it may have significant memory footprint. This can be solved by using SharedEnvironment.

Templates compiled in SharedEnvironment will share the same configuration.

Limitations

Only runtime configuration properties can be changed after the shared environment has been created. Changes to Configuration.CompileTimeConfiguration and other compile-time properties will have no effect.

Example

[Fact]
public void BasicSharedEnvironment()
{
    var handlebars = Handlebars.CreateSharedEnvironment();
    handlebars.RegisterHelper("registerLateHelper", 
        (in EncodedTextWriter writer, in HelperOptions options, in Context context, in Arguments arguments) =>
        {
            var configuration = options.Frame
                .GetType()
                .GetProperty("Configuration", BindingFlags.Instance | BindingFlags.NonPublic)?
                .GetValue(options.Frame) as ICompiledHan
View on GitHub
GitHub Stars1.5k
CategoryDevelopment
Updated3d ago
Forks236

Languages

C#

Security Score

100/100

Audited on Mar 20, 2026

No findings