Skip to content

Commit a248c92

Browse files
authored
Use FrozenDictionaries in ShapeTables (#15651)
1 parent 74d923c commit a248c92

File tree

13 files changed

+89
-96
lines changed

13 files changed

+89
-96
lines changed

src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/DefaultShapeTableManager.cs

+17-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Concurrent;
3+
using System.Collections.Frozen;
34
using System.Collections.Generic;
45
using System.Linq;
56
using System.Threading.Tasks;
@@ -28,13 +29,13 @@ public class DefaultShapeTableManager : IShapeTableManager
2829
private static readonly object _syncLock = new();
2930

3031
// Singleton cache to hold a tenant's theme ShapeTable
31-
private readonly Dictionary<string, ShapeTable> _shapeTableCache;
32+
private readonly IDictionary<string, ShapeTable> _shapeTableCache;
3233

3334
private readonly IServiceProvider _serviceProvider;
3435
private readonly ILogger _logger;
3536

3637
public DefaultShapeTableManager(
37-
[FromKeyedServices(nameof(DefaultShapeTableManager))] Dictionary<string, ShapeTable> shapeTableCache,
38+
[FromKeyedServices(nameof(DefaultShapeTableManager))] IDictionary<string, ShapeTable> shapeTableCache,
3839
IServiceProvider serviceProvider,
3940
ILogger<DefaultShapeTableManager> logger)
4041
{
@@ -45,20 +46,28 @@ public DefaultShapeTableManager(
4546

4647
public Task<ShapeTable> GetShapeTableAsync(string themeId)
4748
{
48-
// This method is intentionally kept non-async since most calls
49+
// This method is intentionally not awaited since most calls
4950
// are from cache.
5051

5152
if (_shapeTableCache.TryGetValue(themeId ?? DefaultThemeIdKey, out var shapeTable))
5253
{
5354
return Task.FromResult(shapeTable);
5455
}
5556

56-
return BuildShapeTableAsync(themeId);
57+
lock (_shapeTableCache)
58+
{
59+
if (_shapeTableCache.TryGetValue(themeId ?? DefaultThemeIdKey, out shapeTable))
60+
{
61+
return Task.FromResult(shapeTable);
62+
}
63+
64+
return BuildShapeTableAsync(themeId);
65+
}
5766
}
5867

5968
private async Task<ShapeTable> BuildShapeTableAsync(string themeId)
6069
{
61-
_logger.LogInformation("Start building shape table");
70+
_logger.LogInformation("Start building shape table for {Theme}", themeId);
6271

6372
// These services are resolved lazily since they are only required when initializing the shape tables
6473
// during the first request. And binding strategies would be expensive to build since this service is called many times
@@ -125,11 +134,11 @@ private async Task<ShapeTable> BuildShapeTableAsync(string themeId)
125134

126135
var shapeTable = new ShapeTable
127136
(
128-
descriptors: descriptors.ToDictionary(sd => sd.ShapeType, x => (ShapeDescriptor)x, StringComparer.OrdinalIgnoreCase),
129-
bindings: descriptors.SelectMany(sd => sd.Bindings).ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase)
137+
descriptors: descriptors.ToFrozenDictionary(sd => sd.ShapeType, x => (ShapeDescriptor)x, StringComparer.OrdinalIgnoreCase),
138+
bindings: descriptors.SelectMany(sd => sd.Bindings).ToFrozenDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase)
130139
);
131140

132-
_logger.LogInformation("Done building shape table");
141+
_logger.LogInformation("Done building shape table for {Theme}", themeId);
133142

134143
_shapeTableCache[themeId ?? DefaultThemeIdKey] = shapeTable;
135144

src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs

+15-27
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,15 @@ public ShapeDescriptorIndex(
7676

7777
public override IDictionary<string, ShapeBinding> Bindings => _bindings;
7878

79-
public override IEnumerable<Func<ShapeCreatingContext, Task>> CreatingAsync => _creatingAsync;
79+
public override IReadOnlyList<Func<ShapeCreatingContext, Task>> CreatingAsync => _creatingAsync;
8080

81-
public override IEnumerable<Func<ShapeCreatedContext, Task>> CreatedAsync => _createdAsync;
81+
public override IReadOnlyList<Func<ShapeCreatedContext, Task>> CreatedAsync => _createdAsync;
8282

83-
public override IEnumerable<Func<ShapeDisplayContext, Task>> DisplayingAsync => _displayingAsync;
83+
public override IReadOnlyList<Func<ShapeDisplayContext, Task>> DisplayingAsync => _displayingAsync;
8484

85-
public override IEnumerable<Func<ShapeDisplayContext, Task>> ProcessingAsync => _processingAsync;
85+
public override IReadOnlyList<Func<ShapeDisplayContext, Task>> ProcessingAsync => _processingAsync;
8686

87-
public override IEnumerable<Func<ShapeDisplayContext, Task>> DisplayedAsync => _displayedAsync;
87+
public override IReadOnlyList<Func<ShapeDisplayContext, Task>> DisplayedAsync => _displayedAsync;
8888

8989
public override Func<ShapePlacementContext, PlacementInfo> Placement => CalculatePlacement;
9090

@@ -104,27 +104,15 @@ private PlacementInfo CalculatePlacement(ShapePlacementContext ctx)
104104
return info ?? DefaultPlacementAction(ctx);
105105
}
106106

107-
public override IList<string> Wrappers => _wrappers;
107+
public override IReadOnlyList<string> Wrappers => _wrappers;
108108

109-
public override IList<string> BindingSources => _bindingSources;
109+
public override IReadOnlyList<string> BindingSources => _bindingSources;
110110
}
111111

112112
public class ShapeDescriptor
113113
{
114114
public ShapeDescriptor()
115115
{
116-
if (this is not ShapeDescriptorIndex)
117-
{
118-
CreatingAsync = [];
119-
CreatedAsync = [];
120-
DisplayingAsync = [];
121-
ProcessingAsync = [];
122-
DisplayedAsync = [];
123-
Wrappers = [];
124-
BindingSources = [];
125-
Bindings = new Dictionary<string, ShapeBinding>(StringComparer.OrdinalIgnoreCase);
126-
}
127-
128116
Placement = DefaultPlacementAction;
129117
}
130118

@@ -154,19 +142,19 @@ protected PlacementInfo DefaultPlacementAction(ShapePlacementContext context)
154142
public virtual Func<DisplayContext, Task<IHtmlContent>> Binding =>
155143
Bindings[ShapeType].BindingAsync;
156144

157-
public virtual IDictionary<string, ShapeBinding> Bindings { get; set; }
145+
public virtual IDictionary<string, ShapeBinding> Bindings { get; } = new Dictionary<string, ShapeBinding>(StringComparer.OrdinalIgnoreCase);
158146

159-
public virtual IEnumerable<Func<ShapeCreatingContext, Task>> CreatingAsync { get; set; }
160-
public virtual IEnumerable<Func<ShapeCreatedContext, Task>> CreatedAsync { get; set; }
161-
public virtual IEnumerable<Func<ShapeDisplayContext, Task>> DisplayingAsync { get; set; }
162-
public virtual IEnumerable<Func<ShapeDisplayContext, Task>> ProcessingAsync { get; set; }
163-
public virtual IEnumerable<Func<ShapeDisplayContext, Task>> DisplayedAsync { get; set; }
147+
public virtual IReadOnlyList<Func<ShapeCreatingContext, Task>> CreatingAsync { get; set; } = [];
148+
public virtual IReadOnlyList<Func<ShapeCreatedContext, Task>> CreatedAsync { get; set; } = [];
149+
public virtual IReadOnlyList<Func<ShapeDisplayContext, Task>> DisplayingAsync { get; set; } = [];
150+
public virtual IReadOnlyList<Func<ShapeDisplayContext, Task>> ProcessingAsync { get; set; } = [];
151+
public virtual IReadOnlyList<Func<ShapeDisplayContext, Task>> DisplayedAsync { get; set; } = [];
164152

165153
public virtual Func<ShapePlacementContext, PlacementInfo> Placement { get; set; }
166154
public string DefaultPlacement { get; set; }
167155

168-
public virtual IList<string> Wrappers { get; set; }
169-
public virtual IList<string> BindingSources { get; set; }
156+
public virtual IReadOnlyList<string> Wrappers { get; set; } = [];
157+
public virtual IReadOnlyList<string> BindingSources { get; set; } = [];
170158
}
171159

172160
public class ShapeBinding

src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/ShapeAlterationBuilder.cs

+6-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Linq;
43
using System.Threading.Tasks;
54
using Microsoft.AspNetCore.Html;
65
using OrchardCore.DisplayManagement.Implementation;
@@ -57,7 +56,7 @@ public ShapeAlterationBuilder BoundAs(string bindingSource, Func<DisplayContext,
5756

5857
// ShapeDescriptor.Bindings is a case insensitive dictionary.
5958
descriptor.Bindings[_bindingName] = binding;
60-
descriptor.BindingSources.Add(bindingSource);
59+
descriptor.BindingSources = [..descriptor.BindingSources, bindingSource];
6160
});
6261
}
6362

@@ -80,8 +79,7 @@ public ShapeAlterationBuilder OnCreating(Func<ShapeCreatingContext, Task> action
8079
{
8180
return Configure(descriptor =>
8281
{
83-
var existing = descriptor.CreatingAsync ?? [];
84-
descriptor.CreatingAsync = existing.Concat(new[] { actionAsync });
82+
descriptor.CreatingAsync = [.. descriptor.CreatingAsync ?? [], actionAsync];
8583
});
8684
}
8785

@@ -104,8 +102,7 @@ public ShapeAlterationBuilder OnCreated(Func<ShapeCreatedContext, Task> actionAs
104102
{
105103
return Configure(descriptor =>
106104
{
107-
var existing = descriptor.CreatedAsync ?? [];
108-
descriptor.CreatedAsync = existing.Concat(new[] { actionAsync });
105+
descriptor.CreatedAsync = [.. descriptor.CreatedAsync ?? [], actionAsync];
109106
});
110107
}
111108

@@ -128,8 +125,7 @@ public ShapeAlterationBuilder OnDisplaying(Func<ShapeDisplayContext, Task> actio
128125
{
129126
return Configure(descriptor =>
130127
{
131-
var existing = descriptor.DisplayingAsync ?? [];
132-
descriptor.DisplayingAsync = existing.Concat(new[] { actionAsync });
128+
descriptor.DisplayingAsync = [.. descriptor.DisplayingAsync ?? [], actionAsync];
133129
});
134130
}
135131

@@ -152,8 +148,7 @@ public ShapeAlterationBuilder OnProcessing(Func<ShapeDisplayContext, Task> actio
152148
{
153149
return Configure(descriptor =>
154150
{
155-
var existing = descriptor.ProcessingAsync ?? [];
156-
descriptor.ProcessingAsync = existing.Concat(new[] { actionAsync });
151+
descriptor.ProcessingAsync = [.. descriptor.ProcessingAsync ?? [], actionAsync];
157152
});
158153
}
159154

@@ -176,8 +171,7 @@ public ShapeAlterationBuilder OnDisplayed(Func<ShapeDisplayContext, Task> action
176171
{
177172
return Configure(descriptor =>
178173
{
179-
var existing = descriptor.DisplayedAsync ?? [];
180-
descriptor.DisplayedAsync = existing.Concat(new[] { actionAsync });
174+
descriptor.DisplayedAsync = [.. descriptor.DisplayedAsync ?? [], actionAsync];
181175
});
182176
}
183177

src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/ShapeTable.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ namespace OrchardCore.DisplayManagement.Descriptors
44
{
55
public class ShapeTable
66
{
7-
public ShapeTable(Dictionary<string, ShapeDescriptor> descriptors, Dictionary<string, ShapeBinding> bindings)
7+
public ShapeTable(IDictionary<string, ShapeDescriptor> descriptors, IDictionary<string, ShapeBinding> bindings)
88
{
99
Descriptors = descriptors;
1010
Bindings = bindings;
1111
}
1212

13-
public Dictionary<string, ShapeDescriptor> Descriptors { get; }
14-
public Dictionary<string, ShapeBinding> Bindings { get; }
13+
public IDictionary<string, ShapeDescriptor> Descriptors { get; }
14+
public IDictionary<string, ShapeBinding> Bindings { get; }
1515
}
1616
}

src/OrchardCore/OrchardCore.DisplayManagement/Implementation/DefaultHtmlDisplay.cs

+5-4
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,16 @@ public async Task<IHtmlContent> ExecuteAsync(DisplayContext context)
9797
await shapeDescriptor.DisplayingAsync.InvokeAsync((action, displayContext) => action(displayContext), displayContext, _logger);
9898

9999
// Copy all binding sources (all templates for this shape) in order to use them as Localization scopes.
100-
shapeMetadata.BindingSources = shapeDescriptor.BindingSources.Where(x => x != null).ToList();
101-
if (!shapeMetadata.BindingSources.Any())
100+
shapeMetadata.BindingSources = shapeDescriptor.BindingSources;
101+
102+
if (shapeMetadata.BindingSources.Count == 0)
102103
{
103-
shapeMetadata.BindingSources.Add(shapeDescriptor.BindingSource);
104+
shapeMetadata.BindingSources = [shapeDescriptor.BindingSource];
104105
}
105106
}
106107

107108
// Invoking ShapeMetadata displaying events.
108-
shapeMetadata.Displaying.Invoke(action => action(displayContext), _logger);
109+
shapeMetadata.Displaying.Invoke((action, displayContext) => action(displayContext), displayContext, _logger);
109110

110111
// Use pre-fetched content if available (e.g. coming from specific cache implementation).
111112
if (displayContext.ChildContent != null)

src/OrchardCore/OrchardCore.DisplayManagement/OrchardCoreBuilderExtensions.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Concurrent;
12
using System.Collections.Generic;
23
using Microsoft.AspNetCore.Mvc;
34
using Microsoft.AspNetCore.Mvc.ApplicationParts;
@@ -58,7 +59,7 @@ public static OrchardCoreBuilder AddTheming(this OrchardCoreBuilder builder)
5859
services.AddScoped<IViewLocationExpanderProvider, ThemeViewLocationExpanderProvider>();
5960

6061
services.AddScoped<IShapeTemplateHarvester, BasicShapeTemplateHarvester>();
61-
services.AddKeyedSingleton<Dictionary<string, ShapeTable>>(nameof(DefaultShapeTableManager));
62+
services.AddKeyedSingleton<IDictionary<string, ShapeTable>>(nameof(DefaultShapeTableManager), new ConcurrentDictionary<string, ShapeTable>());
6263
services.AddScoped<IShapeTableManager, DefaultShapeTableManager>();
6364

6465
services.AddScoped<IShapeTableProvider, ShapeAttributeBindingStrategy>();

src/OrchardCore/OrchardCore.DisplayManagement/Shapes/AlternatesCollection.cs

+8-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace OrchardCore.DisplayManagement.Shapes
1111
/// </summary>
1212
public class AlternatesCollection : IEnumerable<string>
1313
{
14-
public static readonly AlternatesCollection Empty = [];
14+
public static AlternatesCollection Empty = [];
1515

1616
private KeyedAlternateCollection _collection;
1717

@@ -25,9 +25,9 @@ public AlternatesCollection(params string[] alternates)
2525
}
2626
}
2727

28-
public string this[int index] => _collection[index];
28+
public string this[int index] => _collection?[index] ?? "";
2929

30-
public string Last => _collection.LastOrDefault() ?? "";
30+
public string Last => _collection?.LastOrDefault() ?? "";
3131

3232
public void Add(string alternate)
3333
{
@@ -99,6 +99,11 @@ public void AddRange(IEnumerable<string> alternates)
9999

100100
private void EnsureCollection()
101101
{
102+
if (this == Empty)
103+
{
104+
throw new NotSupportedException("AlternateCollection can't be changed.");
105+
}
106+
102107
_collection ??= new KeyedAlternateCollection();
103108
}
104109

src/OrchardCore/OrchardCore.DisplayManagement/Shapes/ShapeMetadata.cs

+9-16
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Linq;
43
using System.Text.Json.Serialization;
54
using System.Threading.Tasks;
65
using Microsoft.AspNetCore.Html;
@@ -15,12 +14,6 @@ public class ShapeMetadata
1514

1615
public ShapeMetadata()
1716
{
18-
Wrappers = [];
19-
Alternates = [];
20-
BindingSources = [];
21-
Displaying = [];
22-
Displayed = [];
23-
ProcessingAsync = [];
2417
}
2518

2619
public string Type { get; set; }
@@ -33,45 +26,45 @@ public ShapeMetadata()
3326
public string Prefix { get; set; }
3427
public string Name { get; set; }
3528
public string Differentiator { get; set; }
36-
public AlternatesCollection Wrappers { get; set; }
37-
public AlternatesCollection Alternates { get; set; }
29+
public AlternatesCollection Wrappers { get; set; } = [];
30+
public AlternatesCollection Alternates { get; set; } = [];
3831
public bool IsCached => _cacheContext != null;
3932
public IHtmlContent ChildContent { get; set; }
4033

4134
/// <summary>
4235
/// Event use for a specific shape instance.
4336
/// </summary>
4437
[JsonIgnore]
45-
public IEnumerable<Action<ShapeDisplayContext>> Displaying { get; private set; }
38+
public IReadOnlyList<Action<ShapeDisplayContext>> Displaying { get; private set; } = [];
4639

4740
/// <summary>
4841
/// Event use for a specific shape instance.
4942
/// </summary>
5043
[JsonIgnore]
51-
public IEnumerable<Func<IShape, Task>> ProcessingAsync { get; private set; }
44+
public IReadOnlyList<Func<IShape, Task>> ProcessingAsync { get; private set; } = [];
5245

5346
/// <summary>
5447
/// Event use for a specific shape instance.
5548
/// </summary>
5649
[JsonIgnore]
57-
public IEnumerable<Action<ShapeDisplayContext>> Displayed { get; private set; }
50+
public IReadOnlyList<Action<ShapeDisplayContext>> Displayed { get; private set; } = [];
5851

5952
[JsonIgnore]
60-
public IList<string> BindingSources { get; set; }
53+
public IReadOnlyList<string> BindingSources { get; set; } = [];
6154

6255
public void OnDisplaying(Action<ShapeDisplayContext> context)
6356
{
64-
Displaying = Displaying.Concat(new[] { context });
57+
Displaying = [..Displaying, context];
6558
}
6659

6760
public void OnProcessing(Func<IShape, Task> context)
6861
{
69-
ProcessingAsync = ProcessingAsync.Concat(new[] { context });
62+
ProcessingAsync = [.. ProcessingAsync, context];
7063
}
7164

7265
public void OnDisplayed(Action<ShapeDisplayContext> context)
7366
{
74-
Displayed = Displayed.Concat(new[] { context });
67+
Displayed = [.. Displayed, context];
7568
}
7669

7770
/// <summary>

src/OrchardCore/OrchardCore.DisplayManagement/Theming/ThemeManager.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,13 @@ public async Task<IExtensionInfo> GetThemeAsync()
3636
}
3737
}
3838

39-
themeResults.Sort((x, y) => y.Priority.CompareTo(x.Priority));
40-
4139
if (themeResults.Count == 0)
4240
{
4341
return null;
4442
}
4543

44+
themeResults.Sort((x, y) => y.Priority.CompareTo(x.Priority));
45+
4646
// Try to load the theme to ensure it's present
4747
foreach (var theme in themeResults)
4848
{

0 commit comments

Comments
 (0)