Handlebars.Net
2.1.6
Prefix Reserved
dotnet add package Handlebars.Net --version 2.1.6
NuGet\Install-Package Handlebars.Net -Version 2.1.6
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
paket add Handlebars.Net --version 2.1.6
#r "nuget: Handlebars.Net, 2.1.6"
// Install Handlebars.Net as a Cake Addin #addin nuget:?package=Handlebars.Net&version=2.1.6 // Install Handlebars.Net as a Cake Tool #tool nuget:?package=Handlebars.Net&version=2.1.6
Handlebars.Net
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:
- Handlebars.Net.Extension.Json (Adds
System.Text.Json.JsonDocument
support) - Handlebars.Net.Extension.NewtonsoftJson (Adds
Newtonsoft.Json
support) - Handlebars.Net.Helpers (Additional helpers in the categories: 'Constants', 'Enumerable', 'Math', 'Regex', 'String', 'DateTime', 'Url' , 'DynamicLinq', 'Humanizer', 'Json', 'Random', 'Xeger' and 'XPath'.)
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 ICompiledHandlebarsConfiguration;
var helpers = configuration?.Helpers;
const string name = "lateHelper";
if (helpers?.TryGetValue(name, out var @ref) ?? false)
{
@ref.Value = new DelegateReturnHelperDescriptor(name, (c, a) => 42);
}
});
var _0_template = "{{registerLateHelper}}";
var _0 = handlebars.Compile(_0_template);
var _1_template = "{{lateHelper}}";
var _1 = handlebars.Compile(_1_template);
var result = _1(null);
Assert.Equal("", result); // `lateHelper` is not registered yet
_0(null);
result = _1(null);
Assert.Equal("42", result);
}
Compatibility feature toggles
Compatibility feature toggles defines a set of settings responsible for controlling compilation/rendering behavior. Each of those settings would enable certain feature that would break compatibility with canonical Handlebars.
By default all toggles are set to false
.
Legend
- Areas
Compile-time
: takes affect at the time of template compilationRuntime
: takes affect at the time of template rendering
RelaxedHelperNaming
If true
enables support for Handlebars.Net helper naming rules.
This enables helper names to be not-valid Handlebars identifiers (e.g. {{ one.two }}
).
Such naming is not supported in Handlebarsjs and would break compatibility.
Areas
Compile-time
Example
[Fact]
public void HelperWithDotSeparatedName()
{
var source = "{{ one.two }}";
var handlebars = Handlebars.Create();
handlebars.Configuration.Compatibility.RelaxedHelperNaming = true;
handlebars.RegisterHelper("one.two", (context, arguments) => 42);
var template = handlebars.Compile(source);
var actual = template(null);
Assert.Equal("42", actual);
}
HtmlEncoder
Used to switch between the legacy Handlebars.Net and the canonical Handlebars rules (or a custom implementation).
For Handlebars.Net 2.x.x HtmlEncoderLegacy
is the default.
HtmlEncoder
Implements the canonical Handlebars rules.
HtmlEncoderLegacy
Will not encode:
= (equals)
` (backtick)
' (single quote)
Will encode non-ascii characters �
, �
, ...
Into HTML entities (<
, â
, ß
, ...).
Areas
Runtime
Example
[Fact]
public void UseCanonicalHtmlEncodingRules()
{
var handlebars = Handlebars.Create();
handlebars.Configuration.TextEncoder = new HtmlEncoder();
var source = "{{Text}}";
var value = new { Text = "< �" };
var template = handlebars.Compile(source);
var actual = template(value);
Assert.Equal("< �", actual);
}
Performance
Compilation
Compared to rendering, compiling is a fairly intensive process. While both are still measured in millseconds, compilation accounts for the most of that time by far. So, it is generally ideal to compile once and cache the resulting function to be re-used for the life of your process.
Rendering
Nearly all time spent in rendering is in the routine that resolves values against the model. Different types of objects have different performance characteristics when used as models.
Model Types
- The absolute fastest model is a
IDictionary<string, object>
(microseconds). - The next fastest is a POCO (typically a few milliseconds for an average-sized template and model), which uses traditional reflection and is fairly fast.
- Rendering starts to get slower (into the tens of milliseconds or more) on dynamic objects.
- The slowest (up to hundreds of milliseconds or worse) tend to be objects with custom type implementations (such as
ICustomTypeDescriptor
) that are not optimized for heavy reflection.
Future roadmap
TBD
Contributing
Pull requests are welcome! The guidelines are pretty straightforward:
- Only add capabilities that are already in the Mustache / Handlebars specs
- Avoid dependencies outside of the .NET BCL
- Maintain cross-platform compatibility (.NET/Mono; Windows/OSX/Linux/etc)
- Follow the established code format
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. 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. |
.NET Core | netcoreapp1.0 was computed. netcoreapp1.1 was computed. netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard1.3 is compatible. netstandard1.4 was computed. netstandard1.5 was computed. netstandard1.6 was computed. netstandard2.0 is compatible. netstandard2.1 is compatible. |
.NET Framework | net451 is compatible. net452 was computed. net46 was computed. net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen30 was computed. tizen40 was computed. tizen60 was computed. |
Universal Windows Platform | uap was computed. uap10.0 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETFramework 4.5.1
- System.ValueTuple (>= 4.5.0)
-
.NETStandard 1.3
- Microsoft.CSharp (>= 4.7.0)
- NETStandard.Library (>= 1.6.1)
- System.ComponentModel.TypeConverter (>= 4.3.0)
- System.Diagnostics.Contracts (>= 4.3.0)
- System.ValueTuple (>= 4.5.0)
-
.NETStandard 2.0
- Microsoft.CSharp (>= 4.7.0)
-
.NETStandard 2.1
- Microsoft.CSharp (>= 4.7.0)
-
net6.0
- No dependencies.
NuGet packages (194)
Showing the top 5 NuGet packages that depend on Handlebars.Net:
Package | Downloads |
---|---|
Handlebars.Net.Helpers.Core
Core functionality for Handlebars.Net.Helpers |
|
EntityFrameworkCore.Scaffolding.Handlebars
Allows customization of classes reverse engineered from an existing database using the Entity Framework Core toolchain with Handlebars templates. |
|
SyncSoft.App.HandlebarsDotNet
An app framework for SyncSoft Inc. |
|
Microsoft.SemanticKernel.PromptTemplates.Handlebars
Semantic Kernel Handlebars Prompt Template Engine |
|
Handlebars.Net.Extension.Json
System.Text.Json extension for Handlebars.Net |
GitHub repositories (30)
Showing the top 5 popular GitHub repositories that depend on Handlebars.Net:
Repository | Stars |
---|---|
microsoft/semantic-kernel
Integrate cutting-edge LLM technology quickly and easily into your apps
|
|
bitwarden/server
Bitwarden infrastructure/backend (API, database, Docker, etc).
|
|
chocolatey/choco
Chocolatey - the package manager for Windows
|
|
dotnetcore/Util
Util是一个.Net平台下的应用框架,旨在提升中小团队的开发能力,由工具类、分层架构基类、Ui组件,配套代码生成模板,权限等组成。
|
|
scriban/scriban
A fast, powerful, safe and lightweight scripting language and engine for .NET
|
Version | Downloads | Last updated |
---|---|---|
2.1.6 | 4,298,103 | 4/3/2024 |
2.1.5 | 109,762 | 4/1/2024 |
2.1.4 | 11,185,640 | 3/4/2023 |
2.1.3 | 415,085 | 2/15/2023 |
2.1.2 | 7,633,945 | 4/7/2022 |
2.1.1 | 294,210 | 3/26/2022 |
2.1.1-beta-1 | 10,925 | 3/4/2022 |
2.1.0 | 1,508,163 | 1/24/2022 |
2.0.10 | 795,928 | 12/22/2021 |
2.0.9 | 1,353,018 | 8/1/2021 |
2.0.8 | 609,527 | 6/2/2021 |
2.0.7 | 415,243 | 3/26/2021 |
2.0.6 | 30,605 | 3/21/2021 |
2.0.5 | 301,659 | 3/6/2021 |
2.0.4 | 4,300,172 | 1/7/2021 |
2.0.3 | 320,563 | 12/11/2020 |
2.0.2 | 20,813 | 12/8/2020 |
2.0.1 | 2,595 | 12/7/2020 |
2.0.0 | 18,604 | 12/6/2020 |
2.0.0-rc-2 | 2,397 | 12/2/2020 |
2.0.0-rc-1 | 1,709 | 12/2/2020 |
2.0.0-preview-2 | 13,253 | 11/6/2020 |
2.0.0-preview-1 | 2,230 | 10/26/2020 |
1.11.5 | 1,624,934 | 10/10/2020 |
1.11.4 | 3,983 | 10/10/2020 |
1.10.1 | 11,425,867 | 3/23/2019 |
1.10.0 | 7,228 | 3/22/2019 |
1.9.5 | 5,710,181 | 6/16/2018 |
1.9.4 | 99,455 | 5/26/2018 |
1.9.3 | 108,478 | 5/2/2018 |
1.9.0 | 3,444,543 | 5/16/2017 |
1.8.0 | 313,513 | 10/9/2016 |
1.7.1 | 207,914 | 6/4/2016 |
1.7.0 | 16,154 | 5/20/2016 |
1.6.8 | 11,054 | 5/18/2016 |
1.6.7 | 3,911 | 5/15/2016 |
1.6.6 | 2,695 | 5/14/2016 |
1.6.4 | 83,861 | 2/25/2016 |
1.6.3 | 8,203 | 2/22/2016 |
1.6.2 | 2,774 | 2/22/2016 |
1.6.1 | 6,492 | 2/7/2016 |
1.6.0 | 5,905 | 1/29/2016 |
1.5.3 | 6,036 | 1/26/2016 |
1.5.2 | 3,674 | 1/20/2016 |
1.5.1 | 3,685 | 1/5/2016 |
1.4.8 | 6,546 | 11/27/2015 |
1.4.6 | 4,011 | 11/24/2015 |
1.4.4 | 40,624 | 8/9/2015 |
1.4.3 | 2,906 | 8/8/2015 |
1.4.2 | 8,027 | 8/4/2015 |
1.4.0 | 29,245 | 7/20/2015 |
1.3.5 | 3,837 | 7/10/2015 |
1.3.3 | 83,961 | 5/18/2015 |
1.3.2 | 20,249 | 4/11/2015 |
1.3.1 | 2,907 | 4/2/2015 |
1.3.0 | 2,719 | 3/31/2015 |
1.2.10 | 7,459 | 3/17/2015 |
1.2.9 | 27,614 | 3/6/2015 |
1.2.8 | 2,818 | 2/27/2015 |
1.2.7 | 2,681 | 2/27/2015 |
1.2.6 | 6,266 | 2/15/2015 |
1.2.5 | 2,862 | 2/15/2015 |
1.2.4 | 2,882 | 2/13/2015 |
1.2.3 | 2,848 | 2/9/2015 |
1.2.2 | 2,822 | 2/8/2015 |
1.2.1 | 2,768 | 2/7/2015 |
1.2.0 | 3,060 | 1/18/2015 |
1.1.7 | 2,694 | 1/16/2015 |
1.1.6 | 2,658 | 1/16/2015 |
1.1.5 | 2,735 | 1/14/2015 |
1.1.4 | 2,718 | 1/10/2015 |
1.1.3 | 2,678 | 1/10/2015 |
1.1.2 | 2,749 | 1/9/2015 |
1.1.1 | 2,662 | 1/8/2015 |
1.1.0 | 3,068 | 1/7/2015 |
1.0.6 | 2,881 | 1/2/2015 |
1.0.5 | 3,014 | 1/2/2015 |
1.0.4 | 2,913 | 1/2/2015 |
1.0.3 | 2,999 | 12/20/2014 |
1.0.2 | 3,108 | 11/30/2014 |
1.0.0-beta9 | 2,597 | 6/29/2014 |
1.0.0-beta8 | 2,443 | 6/22/2014 |
1.0.0-beta6 | 2,501 | 6/21/2014 |
1.0.0-beta5 | 2,449 | 6/15/2014 |
1.0.0-beta4 | 2,389 | 5/27/2014 |
1.0.0-beta3 | 2,398 | 5/26/2014 |
1.0.0-beta2 | 2,385 | 5/17/2014 |
1.0.0-beta18 | 2,742 | 11/20/2014 |
1.0.0-beta17 | 2,637 | 11/17/2014 |
1.0.0-beta16 | 2,576 | 10/15/2014 |
1.0.0-beta12 | 2,415 | 9/18/2014 |
1.0.0-beta11 | 2,442 | 8/31/2014 |
1.0.0-beta1 | 2,519 | 5/3/2014 |
1.0.0-alpha5 | 2,409 | 5/13/2014 |
1.0.0-alpha4 | 2,434 | 5/11/2014 |
1.0.0-alpha2 | 3,852 | 5/10/2014 |