From a248c92804c20ef89a822f020d64658e4be6c0fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= Date: Thu, 4 Apr 2024 09:13:46 -0700 Subject: [PATCH] Use FrozenDictionaries in ShapeTables (#15651) --- .../Descriptors/DefaultShapeTableManager.cs | 25 +++++++---- .../Descriptors/FeatureShapeDescriptor.cs | 42 +++++++------------ .../Descriptors/ShapeAlterationBuilder.cs | 18 +++----- .../Descriptors/ShapeTable.cs | 6 +-- .../Implementation/DefaultHtmlDisplay.cs | 9 ++-- .../OrchardCoreBuilderExtensions.cs | 3 +- .../Shapes/AlternatesCollection.cs | 11 +++-- .../Shapes/ShapeMetadata.cs | 25 ++++------- .../Theming/ThemeManager.cs | 4 +- .../Extensions/ExtensionManager.cs | 5 ++- .../ShapeFactoryBenchmark.cs | 5 ++- .../MultiSelectShapeDescriptorIndex.cs | 28 ++++++------- .../DefaultShapeTableManagerTests.cs | 4 +- 13 files changed, 89 insertions(+), 96 deletions(-) diff --git a/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/DefaultShapeTableManager.cs b/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/DefaultShapeTableManager.cs index 4f92775e578..c9987c42d31 100644 --- a/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/DefaultShapeTableManager.cs +++ b/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/DefaultShapeTableManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Collections.Frozen; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -28,13 +29,13 @@ public class DefaultShapeTableManager : IShapeTableManager private static readonly object _syncLock = new(); // Singleton cache to hold a tenant's theme ShapeTable - private readonly Dictionary _shapeTableCache; + private readonly IDictionary _shapeTableCache; private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; public DefaultShapeTableManager( - [FromKeyedServices(nameof(DefaultShapeTableManager))] Dictionary shapeTableCache, + [FromKeyedServices(nameof(DefaultShapeTableManager))] IDictionary shapeTableCache, IServiceProvider serviceProvider, ILogger logger) { @@ -45,7 +46,7 @@ public DefaultShapeTableManager( public Task GetShapeTableAsync(string themeId) { - // This method is intentionally kept non-async since most calls + // This method is intentionally not awaited since most calls // are from cache. if (_shapeTableCache.TryGetValue(themeId ?? DefaultThemeIdKey, out var shapeTable)) @@ -53,12 +54,20 @@ public Task GetShapeTableAsync(string themeId) return Task.FromResult(shapeTable); } - return BuildShapeTableAsync(themeId); + lock (_shapeTableCache) + { + if (_shapeTableCache.TryGetValue(themeId ?? DefaultThemeIdKey, out shapeTable)) + { + return Task.FromResult(shapeTable); + } + + return BuildShapeTableAsync(themeId); + } } private async Task BuildShapeTableAsync(string themeId) { - _logger.LogInformation("Start building shape table"); + _logger.LogInformation("Start building shape table for {Theme}", themeId); // These services are resolved lazily since they are only required when initializing the shape tables // 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 BuildShapeTableAsync(string themeId) var shapeTable = new ShapeTable ( - descriptors: descriptors.ToDictionary(sd => sd.ShapeType, x => (ShapeDescriptor)x, StringComparer.OrdinalIgnoreCase), - bindings: descriptors.SelectMany(sd => sd.Bindings).ToDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase) + descriptors: descriptors.ToFrozenDictionary(sd => sd.ShapeType, x => (ShapeDescriptor)x, StringComparer.OrdinalIgnoreCase), + bindings: descriptors.SelectMany(sd => sd.Bindings).ToFrozenDictionary(kv => kv.Key, kv => kv.Value, StringComparer.OrdinalIgnoreCase) ); - _logger.LogInformation("Done building shape table"); + _logger.LogInformation("Done building shape table for {Theme}", themeId); _shapeTableCache[themeId ?? DefaultThemeIdKey] = shapeTable; diff --git a/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs b/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs index a664305d4cd..fe16550b2a5 100644 --- a/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs +++ b/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/FeatureShapeDescriptor.cs @@ -76,15 +76,15 @@ public ShapeDescriptorIndex( public override IDictionary Bindings => _bindings; - public override IEnumerable> CreatingAsync => _creatingAsync; + public override IReadOnlyList> CreatingAsync => _creatingAsync; - public override IEnumerable> CreatedAsync => _createdAsync; + public override IReadOnlyList> CreatedAsync => _createdAsync; - public override IEnumerable> DisplayingAsync => _displayingAsync; + public override IReadOnlyList> DisplayingAsync => _displayingAsync; - public override IEnumerable> ProcessingAsync => _processingAsync; + public override IReadOnlyList> ProcessingAsync => _processingAsync; - public override IEnumerable> DisplayedAsync => _displayedAsync; + public override IReadOnlyList> DisplayedAsync => _displayedAsync; public override Func Placement => CalculatePlacement; @@ -104,27 +104,15 @@ private PlacementInfo CalculatePlacement(ShapePlacementContext ctx) return info ?? DefaultPlacementAction(ctx); } - public override IList Wrappers => _wrappers; + public override IReadOnlyList Wrappers => _wrappers; - public override IList BindingSources => _bindingSources; + public override IReadOnlyList BindingSources => _bindingSources; } public class ShapeDescriptor { public ShapeDescriptor() { - if (this is not ShapeDescriptorIndex) - { - CreatingAsync = []; - CreatedAsync = []; - DisplayingAsync = []; - ProcessingAsync = []; - DisplayedAsync = []; - Wrappers = []; - BindingSources = []; - Bindings = new Dictionary(StringComparer.OrdinalIgnoreCase); - } - Placement = DefaultPlacementAction; } @@ -154,19 +142,19 @@ protected PlacementInfo DefaultPlacementAction(ShapePlacementContext context) public virtual Func> Binding => Bindings[ShapeType].BindingAsync; - public virtual IDictionary Bindings { get; set; } + public virtual IDictionary Bindings { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); - public virtual IEnumerable> CreatingAsync { get; set; } - public virtual IEnumerable> CreatedAsync { get; set; } - public virtual IEnumerable> DisplayingAsync { get; set; } - public virtual IEnumerable> ProcessingAsync { get; set; } - public virtual IEnumerable> DisplayedAsync { get; set; } + public virtual IReadOnlyList> CreatingAsync { get; set; } = []; + public virtual IReadOnlyList> CreatedAsync { get; set; } = []; + public virtual IReadOnlyList> DisplayingAsync { get; set; } = []; + public virtual IReadOnlyList> ProcessingAsync { get; set; } = []; + public virtual IReadOnlyList> DisplayedAsync { get; set; } = []; public virtual Func Placement { get; set; } public string DefaultPlacement { get; set; } - public virtual IList Wrappers { get; set; } - public virtual IList BindingSources { get; set; } + public virtual IReadOnlyList Wrappers { get; set; } = []; + public virtual IReadOnlyList BindingSources { get; set; } = []; } public class ShapeBinding diff --git a/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/ShapeAlterationBuilder.cs b/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/ShapeAlterationBuilder.cs index b047751b09d..58197f423d1 100644 --- a/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/ShapeAlterationBuilder.cs +++ b/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/ShapeAlterationBuilder.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Html; using OrchardCore.DisplayManagement.Implementation; @@ -57,7 +56,7 @@ public ShapeAlterationBuilder BoundAs(string bindingSource, Func action { return Configure(descriptor => { - var existing = descriptor.CreatingAsync ?? []; - descriptor.CreatingAsync = existing.Concat(new[] { actionAsync }); + descriptor.CreatingAsync = [.. descriptor.CreatingAsync ?? [], actionAsync]; }); } @@ -104,8 +102,7 @@ public ShapeAlterationBuilder OnCreated(Func actionAs { return Configure(descriptor => { - var existing = descriptor.CreatedAsync ?? []; - descriptor.CreatedAsync = existing.Concat(new[] { actionAsync }); + descriptor.CreatedAsync = [.. descriptor.CreatedAsync ?? [], actionAsync]; }); } @@ -128,8 +125,7 @@ public ShapeAlterationBuilder OnDisplaying(Func actio { return Configure(descriptor => { - var existing = descriptor.DisplayingAsync ?? []; - descriptor.DisplayingAsync = existing.Concat(new[] { actionAsync }); + descriptor.DisplayingAsync = [.. descriptor.DisplayingAsync ?? [], actionAsync]; }); } @@ -152,8 +148,7 @@ public ShapeAlterationBuilder OnProcessing(Func actio { return Configure(descriptor => { - var existing = descriptor.ProcessingAsync ?? []; - descriptor.ProcessingAsync = existing.Concat(new[] { actionAsync }); + descriptor.ProcessingAsync = [.. descriptor.ProcessingAsync ?? [], actionAsync]; }); } @@ -176,8 +171,7 @@ public ShapeAlterationBuilder OnDisplayed(Func action { return Configure(descriptor => { - var existing = descriptor.DisplayedAsync ?? []; - descriptor.DisplayedAsync = existing.Concat(new[] { actionAsync }); + descriptor.DisplayedAsync = [.. descriptor.DisplayedAsync ?? [], actionAsync]; }); } diff --git a/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/ShapeTable.cs b/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/ShapeTable.cs index dca6c191d5a..e173e2e0b4e 100644 --- a/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/ShapeTable.cs +++ b/src/OrchardCore/OrchardCore.DisplayManagement/Descriptors/ShapeTable.cs @@ -4,13 +4,13 @@ namespace OrchardCore.DisplayManagement.Descriptors { public class ShapeTable { - public ShapeTable(Dictionary descriptors, Dictionary bindings) + public ShapeTable(IDictionary descriptors, IDictionary bindings) { Descriptors = descriptors; Bindings = bindings; } - public Dictionary Descriptors { get; } - public Dictionary Bindings { get; } + public IDictionary Descriptors { get; } + public IDictionary Bindings { get; } } } diff --git a/src/OrchardCore/OrchardCore.DisplayManagement/Implementation/DefaultHtmlDisplay.cs b/src/OrchardCore/OrchardCore.DisplayManagement/Implementation/DefaultHtmlDisplay.cs index 85f3e19c9f6..c067db5a492 100644 --- a/src/OrchardCore/OrchardCore.DisplayManagement/Implementation/DefaultHtmlDisplay.cs +++ b/src/OrchardCore/OrchardCore.DisplayManagement/Implementation/DefaultHtmlDisplay.cs @@ -97,15 +97,16 @@ public async Task ExecuteAsync(DisplayContext context) await shapeDescriptor.DisplayingAsync.InvokeAsync((action, displayContext) => action(displayContext), displayContext, _logger); // Copy all binding sources (all templates for this shape) in order to use them as Localization scopes. - shapeMetadata.BindingSources = shapeDescriptor.BindingSources.Where(x => x != null).ToList(); - if (!shapeMetadata.BindingSources.Any()) + shapeMetadata.BindingSources = shapeDescriptor.BindingSources; + + if (shapeMetadata.BindingSources.Count == 0) { - shapeMetadata.BindingSources.Add(shapeDescriptor.BindingSource); + shapeMetadata.BindingSources = [shapeDescriptor.BindingSource]; } } // Invoking ShapeMetadata displaying events. - shapeMetadata.Displaying.Invoke(action => action(displayContext), _logger); + shapeMetadata.Displaying.Invoke((action, displayContext) => action(displayContext), displayContext, _logger); // Use pre-fetched content if available (e.g. coming from specific cache implementation). if (displayContext.ChildContent != null) diff --git a/src/OrchardCore/OrchardCore.DisplayManagement/OrchardCoreBuilderExtensions.cs b/src/OrchardCore/OrchardCore.DisplayManagement/OrchardCoreBuilderExtensions.cs index 5baf8611587..44c4c3355cf 100644 --- a/src/OrchardCore/OrchardCore.DisplayManagement/OrchardCoreBuilderExtensions.cs +++ b/src/OrchardCore/OrchardCore.DisplayManagement/OrchardCoreBuilderExtensions.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationParts; @@ -58,7 +59,7 @@ public static OrchardCoreBuilder AddTheming(this OrchardCoreBuilder builder) services.AddScoped(); services.AddScoped(); - services.AddKeyedSingleton>(nameof(DefaultShapeTableManager)); + services.AddKeyedSingleton>(nameof(DefaultShapeTableManager), new ConcurrentDictionary()); services.AddScoped(); services.AddScoped(); diff --git a/src/OrchardCore/OrchardCore.DisplayManagement/Shapes/AlternatesCollection.cs b/src/OrchardCore/OrchardCore.DisplayManagement/Shapes/AlternatesCollection.cs index c7bc3a6c8cb..907aa2b8906 100644 --- a/src/OrchardCore/OrchardCore.DisplayManagement/Shapes/AlternatesCollection.cs +++ b/src/OrchardCore/OrchardCore.DisplayManagement/Shapes/AlternatesCollection.cs @@ -11,7 +11,7 @@ namespace OrchardCore.DisplayManagement.Shapes /// public class AlternatesCollection : IEnumerable { - public static readonly AlternatesCollection Empty = []; + public static AlternatesCollection Empty = []; private KeyedAlternateCollection _collection; @@ -25,9 +25,9 @@ public AlternatesCollection(params string[] alternates) } } - public string this[int index] => _collection[index]; + public string this[int index] => _collection?[index] ?? ""; - public string Last => _collection.LastOrDefault() ?? ""; + public string Last => _collection?.LastOrDefault() ?? ""; public void Add(string alternate) { @@ -99,6 +99,11 @@ public void AddRange(IEnumerable alternates) private void EnsureCollection() { + if (this == Empty) + { + throw new NotSupportedException("AlternateCollection can't be changed."); + } + _collection ??= new KeyedAlternateCollection(); } diff --git a/src/OrchardCore/OrchardCore.DisplayManagement/Shapes/ShapeMetadata.cs b/src/OrchardCore/OrchardCore.DisplayManagement/Shapes/ShapeMetadata.cs index 0a1e67706cf..70c3dfd3ba0 100644 --- a/src/OrchardCore/OrchardCore.DisplayManagement/Shapes/ShapeMetadata.cs +++ b/src/OrchardCore/OrchardCore.DisplayManagement/Shapes/ShapeMetadata.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Text.Json.Serialization; using System.Threading.Tasks; using Microsoft.AspNetCore.Html; @@ -15,12 +14,6 @@ public class ShapeMetadata public ShapeMetadata() { - Wrappers = []; - Alternates = []; - BindingSources = []; - Displaying = []; - Displayed = []; - ProcessingAsync = []; } public string Type { get; set; } @@ -33,8 +26,8 @@ public ShapeMetadata() public string Prefix { get; set; } public string Name { get; set; } public string Differentiator { get; set; } - public AlternatesCollection Wrappers { get; set; } - public AlternatesCollection Alternates { get; set; } + public AlternatesCollection Wrappers { get; set; } = []; + public AlternatesCollection Alternates { get; set; } = []; public bool IsCached => _cacheContext != null; public IHtmlContent ChildContent { get; set; } @@ -42,36 +35,36 @@ public ShapeMetadata() /// Event use for a specific shape instance. /// [JsonIgnore] - public IEnumerable> Displaying { get; private set; } + public IReadOnlyList> Displaying { get; private set; } = []; /// /// Event use for a specific shape instance. /// [JsonIgnore] - public IEnumerable> ProcessingAsync { get; private set; } + public IReadOnlyList> ProcessingAsync { get; private set; } = []; /// /// Event use for a specific shape instance. /// [JsonIgnore] - public IEnumerable> Displayed { get; private set; } + public IReadOnlyList> Displayed { get; private set; } = []; [JsonIgnore] - public IList BindingSources { get; set; } + public IReadOnlyList BindingSources { get; set; } = []; public void OnDisplaying(Action context) { - Displaying = Displaying.Concat(new[] { context }); + Displaying = [..Displaying, context]; } public void OnProcessing(Func context) { - ProcessingAsync = ProcessingAsync.Concat(new[] { context }); + ProcessingAsync = [.. ProcessingAsync, context]; } public void OnDisplayed(Action context) { - Displayed = Displayed.Concat(new[] { context }); + Displayed = [.. Displayed, context]; } /// diff --git a/src/OrchardCore/OrchardCore.DisplayManagement/Theming/ThemeManager.cs b/src/OrchardCore/OrchardCore.DisplayManagement/Theming/ThemeManager.cs index 9a37c268b18..28ea22aa2d1 100644 --- a/src/OrchardCore/OrchardCore.DisplayManagement/Theming/ThemeManager.cs +++ b/src/OrchardCore/OrchardCore.DisplayManagement/Theming/ThemeManager.cs @@ -36,13 +36,13 @@ public async Task GetThemeAsync() } } - themeResults.Sort((x, y) => y.Priority.CompareTo(x.Priority)); - if (themeResults.Count == 0) { return null; } + themeResults.Sort((x, y) => y.Priority.CompareTo(x.Priority)); + // Try to load the theme to ensure it's present foreach (var theme in themeResults) { diff --git a/src/OrchardCore/OrchardCore/Extensions/ExtensionManager.cs b/src/OrchardCore/OrchardCore/Extensions/ExtensionManager.cs index 16a14e1bc8a..cc01db9ba35 100644 --- a/src/OrchardCore/OrchardCore/Extensions/ExtensionManager.cs +++ b/src/OrchardCore/OrchardCore/Extensions/ExtensionManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Concurrent; +using System.Collections.Frozen; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -27,7 +28,7 @@ public class ExtensionManager : IExtensionManager private readonly ITypeFeatureProvider _typeFeatureProvider; private readonly IFeaturesProvider _featuresProvider; - private Dictionary _extensions; + private FrozenDictionary _extensions; private List _extensionsInfos; private Dictionary _features; private IFeatureInfo[] _featureInfos; @@ -374,7 +375,7 @@ await modules.ForEachAsync((module) => .Select(f => f.Extension) .ToList(); - _extensions = _extensionsInfos.ToDictionary(e => e.Id, e => loadedExtensions[e.Id]); + _extensions = _extensionsInfos.ToFrozenDictionary(e => e.Id, e => loadedExtensions[e.Id]); _isInitialized = true; } diff --git a/test/OrchardCore.Benchmarks/ShapeFactoryBenchmark.cs b/test/OrchardCore.Benchmarks/ShapeFactoryBenchmark.cs index 0862be7b057..0a6c8f7d7aa 100644 --- a/test/OrchardCore.Benchmarks/ShapeFactoryBenchmark.cs +++ b/test/OrchardCore.Benchmarks/ShapeFactoryBenchmark.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Fluid; @@ -28,8 +29,8 @@ static ShapeFactoryBenchmark() _templateContext = new TemplateContext(); var defaultShapeTable = new ShapeTable ( - [], - [] + new Dictionary(), + new Dictionary() ); var shapeFactory = new DefaultShapeFactory( diff --git a/test/OrchardCore.Benchmarks/Support/MultiSelectShapeDescriptorIndex.cs b/test/OrchardCore.Benchmarks/Support/MultiSelectShapeDescriptorIndex.cs index dd394c0e0f4..9fdc01f8bb0 100644 --- a/test/OrchardCore.Benchmarks/Support/MultiSelectShapeDescriptorIndex.cs +++ b/test/OrchardCore.Benchmarks/Support/MultiSelectShapeDescriptorIndex.cs @@ -84,15 +84,15 @@ public MultiSelectShapeDescriptorIndex( public override IDictionary Bindings => _bindings; - public override IEnumerable> CreatingAsync => _creatingAsync; + public override IReadOnlyList> CreatingAsync => _creatingAsync; - public override IEnumerable> CreatedAsync => _createdAsync; + public override IReadOnlyList> CreatedAsync => _createdAsync; - public override IEnumerable> DisplayingAsync => _displayingAsync; + public override IReadOnlyList> DisplayingAsync => _displayingAsync; - public override IEnumerable> ProcessingAsync => _processingAsync; + public override IReadOnlyList> ProcessingAsync => _processingAsync; - public override IEnumerable> DisplayedAsync => _displayedAsync; + public override IReadOnlyList> DisplayedAsync => _displayedAsync; public override Func Placement => CalculatePlacement; @@ -112,9 +112,9 @@ private PlacementInfo CalculatePlacement(ShapePlacementContext ctx) return info ?? DefaultPlacementAction(ctx); } - public override IList Wrappers => _wrappers; + public override IReadOnlyList Wrappers => _wrappers; - public override IList BindingSources => _bindingSources; + public override IReadOnlyList BindingSources => _bindingSources; } public class MultiSelectShapeDescriptorIndexArray : ShapeDescriptor @@ -192,15 +192,15 @@ public MultiSelectShapeDescriptorIndexArray( public override IDictionary Bindings => _bindings; - public override IEnumerable> CreatingAsync => _creatingAsync; + public override IReadOnlyList> CreatingAsync => _creatingAsync; - public override IEnumerable> CreatedAsync => _createdAsync; + public override IReadOnlyList> CreatedAsync => _createdAsync; - public override IEnumerable> DisplayingAsync => _displayingAsync; + public override IReadOnlyList> DisplayingAsync => _displayingAsync; - public override IEnumerable> ProcessingAsync => _processingAsync; + public override IReadOnlyList> ProcessingAsync => _processingAsync; - public override IEnumerable> DisplayedAsync => _displayedAsync; + public override IReadOnlyList> DisplayedAsync => _displayedAsync; public override Func Placement => CalculatePlacement; @@ -220,7 +220,7 @@ private PlacementInfo CalculatePlacement(ShapePlacementContext ctx) return info ?? DefaultPlacementAction(ctx); } - public override IList Wrappers => _wrappers; + public override IReadOnlyList Wrappers => _wrappers; - public override IList BindingSources => _bindingSources; + public override IReadOnlyList BindingSources => _bindingSources; } diff --git a/test/OrchardCore.Tests/DisplayManagement/Decriptors/DefaultShapeTableManagerTests.cs b/test/OrchardCore.Tests/DisplayManagement/Decriptors/DefaultShapeTableManagerTests.cs index 38571ec5ef6..16b1ab33d30 100644 --- a/test/OrchardCore.Tests/DisplayManagement/Decriptors/DefaultShapeTableManagerTests.cs +++ b/test/OrchardCore.Tests/DisplayManagement/Decriptors/DefaultShapeTableManagerTests.cs @@ -9,7 +9,7 @@ using OrchardCore.Modules.Manifest; using OrchardCore.Tests.Stubs; -namespace OrchardCore.Tests.DisplayManagement.Decriptors +namespace OrchardCore.Tests.DisplayManagement.Descriptors { public class DefaultShapeTableManagerTests : IDisposable { @@ -118,7 +118,7 @@ public DefaultShapeTableManagerTests() serviceCollection.AddMemoryCache(); serviceCollection.AddScoped(); serviceCollection.AddScoped(); - serviceCollection.AddKeyedSingleton>(nameof(DefaultShapeTableManager)); + serviceCollection.AddKeyedSingleton>(nameof(DefaultShapeTableManager), new ConcurrentDictionary()); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(new StubHostingEnvironment());