2.4.0
Performance
Rendering plain .NET objects and string-heavy templates got substantially faster this release, across three targeted changes to the hot rendering path:
- ~10–33% faster object rendering, less allocation —
ObjectDescriptor's member accessor is now pre-bound to its described type instead of re-resolving through a shared type-keyed lookup on every access, andboolproperty reads return cached boxed instances instead of allocating a fresh box per read. (#652)Benchmark Case Before After Δ RenderSimple object 856.85 ns 575.79 ns −32.8% RenderNested object, rows=20 24.52 us 18.94 us −22.7% RenderList N=100, object 27.48 us 22.77 us −17.1% - ~10–20% faster string-heavy rendering — HTML encoders now bulk-write clean runs of text via
SearchValues<char>(net8.0+) or a plain scan (netstandard) instead of oneTextWriter.Write(char)call per character, falling back to the original per-character path only where escaping is actually needed. Output is byte-for-byte identical. (#651)Benchmark Case Before After Δ RenderSimple dictionary 509.96 ns 399.20 ns −21.7% RenderList N=100, object 27.48 us 22.05 us −19.8% RenderToString clean 13.95 us 10.97 us −21.4% - Zero-allocation
{{#each}}iteration — the boxed-integer cache used for iterator indexes grew from 20 to 1024 entries, eliminating a 24-byte-per-item allocation that was the dominant remaining allocation source in list rendering (e.g. 23.5 KB → 0 B for a 1000-item{{#each}}). (#653)
Combined, typical object-rendering and list-rendering templates should see meaningfully lower latency and near-zero allocation on the common paths; dictionary/expando-backed templates benefit from the encoder work but were otherwise already efficient.
New features
- Nullable Reference Types — the entire public API surface is now annotated for Nullable Reference Types. Binary- and runtime-compatible (annotations are compile-time metadata only); projects without
<Nullable>enable</Nullable>are unaffected. Nullable-enabled consumers get compiler-checked null contracts on the public API, and extensibility interfaces (IPartialTemplateResolver,ITextEncoder,IMemberAccessor,IHelperResolver,IFormatterProvider,IObjectDescriptorProvider,IHelperDescriptor<T>,ViewEngineFileSystem) gained nullability annotations that may surface mismatch warnings (e.g. CS8767) in existing implementations until updated. (#642, @TheConstructor) System.Text.Json.JsonElementsupport — first-class support forJsonElement(e.g. the result ofJsonSerializer.Deserialize<object>(json)) in templates: nested member access,{{#each}}iteration over both JSON objects and arrays, and correct{{#if}}/{{#unless}}truthiness — bringing it to parity with the existing NewtonsoftJObject/JTokensupport. (#657)- Multi-dimensional array support — true C# multi-dimensional arrays (e.g.
int[,]) can now be indexed via path expressions ({{grid.[0].[1]}}) and iterated with{{#each}}, which walks the outer-most dimension and yields row/slab slices for the rest. Jagged arrays and existingIList/IEnumerablebehavior are unaffected. (#649) else ifchaining for block helpers —{{else name args}}...{{/outer}}now works for any block helper, not just{{#if}}, and chains recursively to any depth, e.g.{{#StringEqualityBlockHelper @value 'dog'}}...{{else StringEqualityBlockHelper @value 'cat'}}...{{else}}...{{/StringEqualityBlockHelper}}. (#648)
Fixes
- Properties whose only implementation is a C# 8+ default interface member (declared and bodied on an interface, not overridden by the concrete class) are now resolved correctly by both
{{PropertyName}}lookup and{{#each this}}enumeration, instead of being silently skipped. (#658, fixes #601) {{#*inline "name" ...}}no longer throws when passed hash arguments or extra positional arguments, matching Handlebars.js'sinlinedecorator behavior. (#647, fixes #560)- Corrected the nullable annotation on
Try*out-parameters for concrete reference types (introduced in #642) from[MaybeNullWhen(false)] out Tto[NotNullWhen(true)] out T?, matching BCL convention (e.g.Uri.TryCreate) and giving a stronger compiler guarantee against unchecked dereferences. Affects ~20Try*methods acrossIObjectDescriptorProvider/ObjectDescriptor,IFormatterProvider,DynamicMemberAccessor,TypeExtensions,BindingContext,PathResolver, andBlockAccumulatorContext. Compile-time-only change, not binary breaking. (#655, fixes #654) - Resolved a SonarCloud reliability regression (A→B) surfaced by #642's diff against two pre-existing mutable-array-exposure smells;
Closure.Ais nowinternalandPathInfo.Segmentscarries an explicit suppression. (#656)
Compatibility notes
- All changes in this release are binary-compatible. The Nullable Reference Types annotations and the
NotNullWhen/MaybeNullWhencorrection are compile-time metadata only. - If you implement
IPartialTemplateResolver,ITextEncoder,IMemberAccessor,IHelperResolver,IFormatterProvider,IObjectDescriptorProvider,IHelperDescriptor<T>, or derive fromViewEngineFileSystem, and build with<Nullable>enable</Nullable>, you may see new nullability-mismatch warnings until your implementation's annotations are updated to match. HandlebarsConfiguration.FileSystemis now declared nullable (ViewEngineFileSystem?), matching its actual default.- Built-in collection formatters now throw
ArgumentNullException(with parameter name) instead of a rawNullReferenceExceptionfor null/mismatched values.
Contributors
Full Changelog: 2.2.0...2.4.0