Skip to content

Commit e709aa9

Browse files
authored
Merge pull request #514 from Handlebars-Net/feature/shared-env
Introduce `SharedEnvironment`
2 parents 76bf589 + a69939f commit e709aa9

File tree

11 files changed

+175
-42
lines changed

11 files changed

+175
-42
lines changed

README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,56 @@ public void DateTimeFormatter(IHandlebars handlebars)
253253
#### Notes
254254
- Formatters are resolved in reverse order according to registration. If multiple providers can provide formatter for a type the last registered would be used.
255255

256+
### Shared environment
257+
258+
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.
259+
260+
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`.
261+
262+
Templates compiled in `SharedEnvironment` will share the same configuration.
263+
264+
#### Limitations
265+
266+
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.
267+
268+
#### Example
269+
270+
```c#
271+
[Fact]
272+
public void BasicSharedEnvironment()
273+
{
274+
var handlebars = Handlebars.CreateSharedEnvironment();
275+
handlebars.RegisterHelper("registerLateHelper",
276+
(in EncodedTextWriter writer, in HelperOptions options, in Context context, in Arguments arguments) =>
277+
{
278+
var configuration = options.Frame
279+
.GetType()
280+
.GetProperty("Configuration", BindingFlags.Instance | BindingFlags.NonPublic)?
281+
.GetValue(options.Frame) as ICompiledHandlebarsConfiguration;
282+
283+
var helpers = configuration?.Helpers;
284+
285+
const string name = "lateHelper";
286+
if (helpers?.TryGetValue(name, out var @ref) ?? false)
287+
{
288+
@ref.Value = new DelegateReturnHelperDescriptor(name, (c, a) => 42);
289+
}
290+
});
291+
292+
var _0_template = "{{registerLateHelper}}";
293+
var _0 = handlebars.Compile(_0_template);
294+
var _1_template = "{{lateHelper}}";
295+
var _1 = handlebars.Compile(_1_template);
296+
297+
var result = _1(null);
298+
Assert.Equal("", result); // `lateHelper` is not registered yet
299+
300+
_0(null);
301+
result = _1(null);
302+
Assert.Equal("42", result);
303+
}
304+
```
305+
256306
### Compatibility feature toggles
257307

258308
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.

source/Handlebars.Test/BasicIntegrationTests.cs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
using HandlebarsDotNet.Features;
1313
using HandlebarsDotNet.IO;
1414
using HandlebarsDotNet.PathStructure;
15+
using HandlebarsDotNet.Runtime;
1516
using HandlebarsDotNet.ValueProviders;
1617

1718
namespace HandlebarsDotNet.Test
@@ -56,6 +57,42 @@ public void BasicPath(IHandlebars handlebars)
5657
var result = template(data);
5758
Assert.Equal("Hello, Handlebars.Net!", result);
5859
}
60+
61+
[Fact]
62+
public void BasicSharedEnvironment()
63+
{
64+
var handlebars = Handlebars.CreateSharedEnvironment();
65+
handlebars.RegisterHelper("registerLateHelper",
66+
(in EncodedTextWriter writer, in HelperOptions options, in Context context, in Arguments arguments) =>
67+
{
68+
var configuration = options.Frame
69+
.GetType()
70+
.GetProperty("Configuration", BindingFlags.Instance | BindingFlags.NonPublic)?
71+
.GetValue(options.Frame) as ICompiledHandlebarsConfiguration;
72+
73+
if(configuration == null) return;
74+
75+
var helpers = configuration.Helpers;
76+
77+
const string name = "lateHelper";
78+
if (helpers.TryGetValue(name, out var @ref))
79+
{
80+
@ref.Value = new DelegateReturnHelperDescriptor(name, (c, a) => 42);
81+
}
82+
});
83+
84+
var _0_template = "{{registerLateHelper}}";
85+
var _0 = handlebars.Compile(_0_template);
86+
var _1_template = "{{lateHelper}}";
87+
var _1 = handlebars.Compile(_1_template);
88+
89+
var result = _1(null);
90+
Assert.Equal("", result); // `lateHelper` is not registered yet
91+
92+
_0(null);
93+
result = _1(null);
94+
Assert.Equal("42", result);
95+
}
5996

6097
[Theory]
6198
[ClassData(typeof(HandlebarsEnvGenerator))]
@@ -110,7 +147,7 @@ public void PathUnresolvedBindingFormatter(IHandlebars handlebars)
110147
}
111148

112149
[Theory, ClassData(typeof(HandlebarsEnvGenerator))]
113-
public void CustcomDateTimeFormat(IHandlebars handlebars)
150+
public void CustomDateTimeFormat(IHandlebars handlebars)
114151
{
115152
var source = "{{now}}";
116153

@@ -420,6 +457,8 @@ public void BasicPropertyOnArray(IHandlebars handlebars)
420457
[Theory, ClassData(typeof(HandlebarsEnvGenerator))]
421458
public void AliasedPropertyOnArray(IHandlebars handlebars)
422459
{
460+
if(handlebars.IsSharedEnvironment) return;
461+
423462
var source = "Array is {{ names.count }} item(s) long";
424463
handlebars.Configuration.UseCollectionMemberAliasProvider();
425464
var template = handlebars.Compile(source);
@@ -452,6 +491,8 @@ public void CustomAliasedPropertyOnArray(IHandlebars handlebars)
452491
[Theory, ClassData(typeof(HandlebarsEnvGenerator))]
453492
public void AliasedPropertyOnList(IHandlebars handlebars)
454493
{
494+
if(handlebars.IsSharedEnvironment) return;
495+
455496
var source = "Array is {{ names.Length }} item(s) long";
456497
handlebars.Configuration.UseCollectionMemberAliasProvider();
457498
var template = handlebars.Compile(source);

source/Handlebars.Test/Handlebars.Test.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>netcoreapp2.1;netcoreapp3.1</TargetFrameworks>
4+
<TargetFrameworks>netcoreapp3.1</TargetFrameworks>
55
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT'">$(TargetFrameworks);net452;net46;net461;net472</TargetFrameworks>
66
<ProjectGuid>6BA232A6-8C4D-4C7D-BD75-1844FE9774AF</ProjectGuid>
77
<RootNamespace>HandlebarsDotNet.Test</RootNamespace>

source/Handlebars.Test/HandlebarsEnvGenerator.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public class HandlebarsEnvGenerator : IEnumerable<object[]>
1010
private readonly List<IHandlebars> _data = new()
1111
{
1212
Handlebars.Create(),
13+
Handlebars.CreateSharedEnvironment(),
1314
Handlebars.Create(new HandlebarsConfiguration().Configure(o => o.Compatibility.RelaxedHelperNaming = true)),
1415
Handlebars.Create(new HandlebarsConfiguration().UseWarmUp(types =>
1516
{
Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections;
23
using System.Collections.Generic;
34
using System.Diagnostics;
@@ -16,72 +17,84 @@ namespace HandlebarsDotNet.Collections
1617
public class CascadeIndex<TKey, TValue, TComparer> : IIndexed<TKey, TValue>
1718
where TComparer : IEqualityComparer<TKey>
1819
{
20+
private readonly TComparer _comparer;
1921
public IReadOnlyIndexed<TKey, TValue> Outer { get; set; }
20-
private readonly DictionarySlim<TKey, TValue, TComparer> _inner;
22+
private DictionarySlim<TKey, TValue, TComparer> _inner;
2123

2224
public CascadeIndex(TComparer comparer)
25+
: this(null, comparer)
2326
{
24-
Outer = null;
25-
_inner = new DictionarySlim<TKey, TValue, TComparer>(comparer);
2627
}
2728

28-
public int Count => _inner.Count + OuterEnumerable().Count();
29+
public CascadeIndex(IReadOnlyIndexed<TKey, TValue> outer, TComparer comparer)
30+
{
31+
_comparer = comparer;
32+
Outer = outer;
33+
}
34+
35+
public int Count => (_inner?.Count ?? 0) + OuterEnumerable().Count();
2936

3037
[MethodImpl(MethodImplOptions.AggressiveInlining)]
3138
public void AddOrReplace(in TKey key, in TValue value)
3239
{
33-
_inner.AddOrReplace(key, value);
40+
(_inner ??= new DictionarySlim<TKey, TValue, TComparer>(_comparer)).AddOrReplace(key, value);
3441
}
3542

3643
public void Clear()
3744
{
3845
Outer = null;
39-
_inner.Clear();
46+
_inner?.Clear();
4047
}
4148

4249
public bool ContainsKey(in TKey key)
4350
{
44-
return _inner.ContainsKey(key) || (Outer?.ContainsKey(key) ?? false);
51+
return (_inner?.ContainsKey(key) ?? false)
52+
|| (Outer?.ContainsKey(key) ?? false);
4553
}
4654

4755
[MethodImpl(MethodImplOptions.AggressiveInlining)]
4856
public bool TryGetValue(in TKey key, out TValue value)
4957
{
50-
if (_inner.TryGetValue(key, out value)) return true;
51-
return Outer?.TryGetValue(key, out value) ?? false;
58+
value = default;
59+
return (_inner?.TryGetValue(key, out value) ?? false)
60+
|| (Outer?.TryGetValue(key, out value) ?? false);
5261
}
5362

5463
public TValue this[in TKey key]
5564
{
5665
get
5766
{
58-
if (_inner.TryGetValue(key, out var value)) return value;
59-
if (Outer?.TryGetValue(key, out value) ?? false) return value;
60-
throw new KeyNotFoundException($"{key}");
67+
if (TryGetValue(key, out var value)) return value;
68+
Throw.KeyNotFoundException($"{key}");
69+
return default; // will never reach this point
6170
}
6271

63-
set => _inner.AddOrReplace(key, value);
72+
set => AddOrReplace(key, value);
6473
}
6574

6675
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
6776
{
68-
var enumerator = _inner.GetEnumerator();
69-
while (enumerator.MoveNext())
70-
{
71-
yield return enumerator.Current;
72-
}
77+
foreach (var pair in InnerEnumerable()) yield return pair;
78+
foreach (var pair in OuterEnumerable()) yield return pair;
79+
}
7380

74-
foreach (var pair in OuterEnumerable())
81+
private IEnumerable<KeyValuePair<TKey, TValue>> InnerEnumerable()
82+
{
83+
if(_inner == null) yield break;
84+
85+
var outerEnumerator = _inner.GetEnumerator();
86+
while (outerEnumerator.MoveNext())
7587
{
76-
yield return pair;
88+
if (_inner.ContainsKey(outerEnumerator.Current.Key)) continue;
89+
yield return outerEnumerator.Current;
7790
}
7891
}
79-
92+
8093
private IEnumerable<KeyValuePair<TKey, TValue>> OuterEnumerable()
8194
{
82-
var outerEnumerator = Outer?.GetEnumerator();
83-
if (outerEnumerator == null) yield break;
84-
95+
if(Outer == null) yield break;
96+
97+
using var outerEnumerator = Outer.GetEnumerator();
8598
while (outerEnumerator.MoveNext())
8699
{
87100
if (_inner.ContainsKey(outerEnumerator.Current.Key)) continue;
@@ -90,5 +103,10 @@ private IEnumerable<KeyValuePair<TKey, TValue>> OuterEnumerable()
90103
}
91104

92105
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
106+
107+
private static class Throw
108+
{
109+
public static void KeyNotFoundException(string message, Exception exception = null) => throw new KeyNotFoundException(message, exception);
110+
}
93111
}
94112
}

source/Handlebars/Configuration/HandlebarsConfigurationAdapter.cs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ namespace HandlebarsDotNet
1818
{
1919
internal class HandlebarsConfigurationAdapter : ICompiledHandlebarsConfiguration
2020
{
21+
// observers are in WeakCollection, need to keep reference somewhere
2122
private readonly List<object> _observers = new List<object>();
2223

2324
public HandlebarsConfigurationAdapter(HandlebarsConfiguration configuration)
@@ -33,6 +34,7 @@ public HandlebarsConfigurationAdapter(HandlebarsConfiguration configuration)
3334
new CollectionFormatterProvider(),
3435
new ReadOnlyCollectionFormatterProvider()
3536
}.AddMany(configuration.FormatterProviders);
37+
configuration.FormatterProviders.Subscribe(FormatterProviders);
3638

3739
ObjectDescriptorProviders = CreateObjectDescriptorProvider(UnderlingConfiguration.ObjectDescriptorProviders);
3840
ExpressionMiddlewares = new ObservableList<IExpressionMiddleware>(configuration.CompileTimeConfiguration.ExpressionMiddleware)
@@ -78,7 +80,7 @@ public HandlebarsConfigurationAdapter(HandlebarsConfiguration configuration)
7880
public IAppendOnlyList<IHelperResolver> HelperResolvers { get; }
7981
public IIndexed<string, HandlebarsTemplate<TextWriter, object, object>> RegisteredTemplates { get; }
8082

81-
private ObservableIndex<PathInfoLight, Ref<TDescriptor>, IEqualityComparer<PathInfoLight>> CreateHelpersSubscription<TDescriptor, TOptions>(IIndexed<string, TDescriptor> source)
83+
private ObservableIndex<PathInfoLight, Ref<TDescriptor>, PathInfoLight.PathInfoLightEqualityComparer> CreateHelpersSubscription<TDescriptor, TOptions>(IIndexed<string, TDescriptor> source)
8284
where TOptions : struct, IOptions
8385
where TDescriptor : class, IDescriptor<TOptions>
8486
{
@@ -89,7 +91,7 @@ private ObservableIndex<PathInfoLight, Ref<TDescriptor>, IEqualityComparer<PathI
8991
equalityComparer
9092
);
9193

92-
var target = new ObservableIndex<PathInfoLight, Ref<TDescriptor>, IEqualityComparer<PathInfoLight>>(equalityComparer, existingHelpers);
94+
var target = new ObservableIndex<PathInfoLight, Ref<TDescriptor>, PathInfoLight.PathInfoLightEqualityComparer>(equalityComparer, existingHelpers);
9395

9496
var observer = ObserverBuilder<ObservableEvent<TDescriptor>>.Create(target)
9597
.OnEvent<DictionaryAddedObservableEvent<string, TDescriptor>>(
@@ -130,14 +132,8 @@ private ObservableList<IObjectDescriptorProvider> CreateObjectDescriptorProvider
130132
}
131133
.AddMany(descriptorProviders);
132134

133-
var observer = ObserverBuilder<ObservableEvent<IObjectDescriptorProvider>>.Create(objectDescriptorProviders)
134-
.OnEvent<AddedObservableEvent<IObjectDescriptorProvider>>((@event, state) => { state.Add(@event.Value); })
135-
.Build();
135+
descriptorProviders.Subscribe(objectDescriptorProviders);
136136

137-
_observers.Add(observer);
138-
139-
descriptorProviders.Subscribe(observer);
140-
141137
return objectDescriptorProviders;
142138
}
143139
}

source/Handlebars/EqualityComparers/PathInfoLight.PathInfoLightEqualityComparer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace HandlebarsDotNet
55
{
66
public readonly partial struct PathInfoLight
77
{
8-
internal sealed class PathInfoLightEqualityComparer : IEqualityComparer<PathInfoLight>
8+
internal readonly struct PathInfoLightEqualityComparer : IEqualityComparer<PathInfoLight>
99
{
1010
private readonly PathInfo.TrimmedPathEqualityComparer _comparer;
1111

source/Handlebars/Handlebars.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,19 @@ public static IHandlebars Create(HandlebarsConfiguration configuration = null)
2424
configuration = configuration ?? new HandlebarsConfiguration();
2525
return new HandlebarsEnvironment(configuration);
2626
}
27-
28-
27+
28+
/// <summary>
29+
/// Creates shared Handlebars environment that is used to compile templates that share the same configuration
30+
/// <para>Runtime only changes can be applied after object creation!</para>
31+
/// </summary>
32+
/// <param name="configuration"></param>
33+
/// <returns></returns>
34+
public static IHandlebars CreateSharedEnvironment(HandlebarsConfiguration configuration = null)
35+
{
36+
configuration ??= new HandlebarsConfiguration();
37+
return new HandlebarsEnvironment(new HandlebarsConfigurationAdapter(configuration));
38+
}
39+
2940
/// <summary>
3041
/// Creates standalone instance of <see cref="Handlebars"/> environment
3142
/// </summary>

source/Handlebars/HandlebarsEnvironment.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using HandlebarsDotNet.Compiler;
55
using HandlebarsDotNet.Decorators;
66
using HandlebarsDotNet.Helpers;
7-
using HandlebarsDotNet.Helpers.BlockHelpers;
87
using HandlebarsDotNet.IO;
98
using HandlebarsDotNet.ObjectDescriptors;
109
using HandlebarsDotNet.Runtime;
@@ -37,8 +36,11 @@ public HandlebarsEnvironment(HandlebarsConfiguration configuration)
3736
internal HandlebarsEnvironment(ICompiledHandlebarsConfiguration configuration)
3837
{
3938
CompiledConfiguration = configuration ?? throw new ArgumentNullException(nameof(configuration));
39+
Configuration = CompiledConfiguration.UnderlingConfiguration;
40+
IsSharedEnvironment = true;
4041
}
41-
42+
43+
public bool IsSharedEnvironment { get; }
4244
public HandlebarsConfiguration Configuration { get; }
4345
internal ICompiledHandlebarsConfiguration CompiledConfiguration { get; }
4446
ICompiledHandlebarsConfiguration ICompiledHandlebars.CompiledConfiguration => CompiledConfiguration;
@@ -115,6 +117,12 @@ private HandlebarsTemplate<TextWriter, object, object> CompileViewInternal(strin
115117
};
116118
}
117119

120+
public IHandlebars CreateSharedEnvironment()
121+
{
122+
var configuration = CompiledConfiguration ?? new HandlebarsConfigurationAdapter(Configuration);
123+
return new HandlebarsEnvironment(configuration);
124+
}
125+
118126
public HandlebarsTemplate<TextWriter, object, object> Compile(TextReader template)
119127
{
120128
using var container = AmbientContext.Use(_ambientContext);

0 commit comments

Comments
 (0)