diff --git a/CHANGELOG.md b/CHANGELOG.md
index 906959d..6851ca2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,5 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `FileSize` utility with method to display bytes in a human-readable way
- `CultureInfo` extension method to get the language code of a culture
- `IEnumerable` extension methods
+- `IConfiguration` extension methods
+- `IServiceCollection` extension methods
- `IFormFile` extension method (AspNetCore package only)
- `DbSet` extension methods for ISortableEntity interface (EntityFrameworkCore package only)
diff --git a/Neolution.Utilities.UnitTests/Extensions/IConfigurationExtensionsTests.cs b/Neolution.Utilities.UnitTests/Extensions/IConfigurationExtensionsTests.cs
new file mode 100644
index 0000000..c052258
--- /dev/null
+++ b/Neolution.Utilities.UnitTests/Extensions/IConfigurationExtensionsTests.cs
@@ -0,0 +1,211 @@
+namespace Neolution.Utilities.UnitTests.Extensions;
+
+using Microsoft.Extensions.Configuration;
+
+///
+/// Unit tests for the class.
+///
+public class IConfigurationExtensionsTests
+{
+ ///
+ /// Test that given the null configuration when get options called then throws argument null exception.
+ ///
+ [Fact]
+ public void GivenNullConfiguration_WhenGetOptionsCalled_ThenThrowsArgumentNullException()
+ {
+ // Arrange
+ IConfiguration? configuration = null;
+
+ // Act
+ var act = () => configuration!.GetOptions();
+
+ // Assert
+ var ex = Should.Throw(act);
+ ex.ParamName.ShouldBe("config");
+ }
+
+ ///
+ /// Test that given the missing section when get options called then throws invalid operation exception.
+ ///
+ [Fact]
+ public void GivenMissingSection_WhenGetOptionsCalled_ThenThrowsInvalidOperationException()
+ {
+ // Arrange
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection([]) // No section for SampleOptions
+ .Build();
+
+ // Act
+ var act = () => configuration.GetOptions();
+
+ // Assert
+ var ex = Should.Throw(act);
+ ex.Message.ShouldBe("Could not find configuration section 'SampleOptions'");
+ }
+
+ ///
+ /// Test that given the valid section when get options called then binds and returns options.
+ ///
+ [Fact]
+ public void GivenValidSection_WhenGetOptionsCalled_ThenBindsAndReturnsOptions()
+ {
+ // Arrange
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["SampleOptions:Name"] = "TestName",
+ ["SampleOptions:Level"] = "42",
+ })
+ .Build();
+
+ // Act
+ var options = configuration.GetOptions();
+
+ // Assert
+ options.Name.ShouldBe("TestName");
+ options.Level.ShouldBe(42);
+ }
+
+ ///
+ /// Test that given the section with no matching properties when get options called then returns instance with defaults.
+ ///
+ [Fact]
+ public void GivenSectionWithNoMatchingProperties_WhenGetOptionsCalled_ThenReturnsInstanceWithDefaults()
+ {
+ // Arrange
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["SampleOptions:Irrelevant"] = "ignored",
+ })
+ .Build();
+
+ // Act
+ var options = configuration.GetOptions();
+
+ // Assert
+ options.Name.ShouldBeNull();
+ options.Level.ShouldBe(0);
+ }
+
+ ///
+ /// Test that given the section with invalid numeric value when get options called then throws invalid operation exception.
+ ///
+ [Fact]
+ public void GivenSectionWithInvalidNumericValue_WhenGetOptionsCalled_ThenThrowsInvalidOperationException()
+ {
+ // Arrange
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["SampleOptions:Name"] = "Something",
+ ["SampleOptions:Level"] = "not-an-int",
+ })
+ .Build();
+
+ // Act
+ var act = () => configuration.GetOptions();
+
+ // Assert
+ var ex = Should.Throw(act);
+ ex.Message.ShouldBe("Failed to convert configuration value at 'SampleOptions:Level' to type 'System.Int32'.");
+ }
+
+ ///
+ /// Test that given the valid section with different casing when get options called then binds successfully.
+ ///
+ [Fact]
+ public void GivenValidSectionWithDifferentCasing_WhenGetOptionsCalled_ThenBindsSuccessfully()
+ {
+ // Arrange
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ // lower-case section name to assert case-insensitive lookup
+ ["sampleoptions:Name"] = "CaseTest",
+ ["sampleoptions:Level"] = "7",
+ })
+ .Build();
+
+ // Act
+ var options = configuration.GetOptions();
+
+ // Assert
+ options.Name.ShouldBe("CaseTest");
+ options.Level.ShouldBe(7);
+ }
+
+ ///
+ /// Test that given the null configuration when get section called then throws argument null exception.
+ ///
+ [Fact]
+ public void GivenNullConfiguration_WhenGetSectionCalled_ThenThrowsArgumentNullException()
+ {
+ // Arrange
+ IConfiguration? configuration = null;
+
+ // Act
+ var act = () => configuration!.GetSection();
+
+ // Assert
+ var ex = Should.Throw(act);
+ ex.ParamName.ShouldBe("config");
+ }
+
+ ///
+ /// Test that given the existing section when get section called then returns section.
+ ///
+ [Fact]
+ public void GivenExistingSection_WhenGetSectionCalled_ThenReturnsSection()
+ {
+ // Arrange
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["SampleOptions:Name"] = "X",
+ })
+ .Build();
+
+ // Act
+ var section = configuration.GetSection();
+
+ // Assert
+ section.Key.ShouldBe("SampleOptions");
+ section.Exists().ShouldBeTrue();
+ }
+
+ ///
+ /// Test that given the missing section when get section called then returns non existing section.
+ ///
+ [Fact]
+ public void GivenMissingSection_WhenGetSectionCalled_ThenReturnsNonExistingSection()
+ {
+ // Arrange
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection([])
+ .Build();
+
+ // Act
+ var section = configuration.GetSection();
+
+ // Assert
+ section.Key.ShouldBe("SampleOptions");
+ section.Exists().ShouldBeFalse();
+ }
+
+ ///
+ /// The sample options class used for testing.
+ ///
+ public class SampleOptions
+ {
+ ///
+ /// Gets or sets the name.
+ ///
+ public string? Name { get; set; }
+
+ ///
+ /// Gets or sets the level.
+ ///
+ public int Level { get; set; }
+ }
+}
diff --git a/Neolution.Utilities.UnitTests/Extensions/IServiceCollectionExtensionsTests.cs b/Neolution.Utilities.UnitTests/Extensions/IServiceCollectionExtensionsTests.cs
new file mode 100644
index 0000000..a4f69e5
--- /dev/null
+++ b/Neolution.Utilities.UnitTests/Extensions/IServiceCollectionExtensionsTests.cs
@@ -0,0 +1,167 @@
+namespace Neolution.Utilities.UnitTests.Extensions;
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+
+///
+/// Unit tests for the class.
+///
+public class IServiceCollectionExtensionsTests
+{
+ ///
+ /// Test that given the null service collection when add options called then throws argument null exception.
+ ///
+ [Fact]
+ public void GivenNullServiceCollection_WhenAddOptionsCalled_ThenThrowsArgumentNullException()
+ {
+ // Arrange
+ ServiceCollection? serviceCollection = null;
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary { ["SampleOptions:Name"] = "X" })
+ .Build();
+
+ // Act
+ var act = () => serviceCollection!.AddOptions(configuration);
+
+ // Assert
+ var ex = Should.Throw(act);
+ ex.ParamName.ShouldBe("serviceCollection");
+ }
+
+ ///
+ /// Test that given the null configuration when add options called then throws argument null exception.
+ ///
+ [Fact]
+ public void GivenNullConfiguration_WhenAddOptionsCalled_ThenThrowsArgumentNullException()
+ {
+ // Arrange
+ var serviceCollection = new ServiceCollection();
+ IConfiguration? configuration = null;
+
+ // Act
+ var act = () => serviceCollection.AddOptions(configuration!);
+
+ // Assert
+ var ex = Should.Throw(act);
+ ex.ParamName.ShouldBe("configuration");
+ }
+
+ ///
+ /// Test that given the configuration with options section when add options called then options are configured.
+ ///
+ [Fact]
+ public void GivenConfigurationWithOptionsSection_WhenAddOptionsCalled_ThenOptionsAreConfigured()
+ {
+ // Arrange
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["SampleOptions:Name"] = "Configured",
+ ["SampleOptions:Level"] = "7",
+ })
+ .Build();
+ var serviceCollection = new ServiceCollection();
+
+ // Act
+ serviceCollection.AddOptions(configuration);
+ var provider = serviceCollection.BuildServiceProvider();
+
+ // Assert
+ var options = provider.GetRequiredService>().Value;
+ options.Name.ShouldBe("Configured");
+ options.Level.ShouldBe(7);
+ }
+
+ ///
+ /// Test that given the configuration without options section when add options called then options use defaults.
+ ///
+ [Fact]
+ public void GivenConfigurationWithoutOptionsSection_WhenAddOptionsCalled_ThenOptionsUseDefaults()
+ {
+ // Arrange
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection([])
+ .Build();
+ var serviceCollection = new ServiceCollection();
+
+ // Act
+ serviceCollection.AddOptions(configuration);
+ var provider = serviceCollection.BuildServiceProvider();
+
+ // Assert
+ var options = provider.GetRequiredService>().Value;
+ options.Name.ShouldBeNull();
+ options.Level.ShouldBe(0);
+ }
+
+ ///
+ /// Test that given the service collection when add options called then same instance is returned for fluent chaining.
+ ///
+ [Fact]
+ public void GivenServiceCollection_WhenAddOptionsCalled_ThenReturnsSameInstance()
+ {
+ // Arrange
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection([])
+ .Build();
+ var services = new ServiceCollection();
+
+ // Act
+ var returned = services.AddOptions(configuration);
+
+ // Assert
+ returned.ShouldBeSameAs(services);
+ }
+
+ ///
+ /// Test that given multiple registrations with different configurations when add options called twice then last registration wins.
+ ///
+ [Fact]
+ public void GivenMultipleRegistrations_WhenAddOptionsCalledTwice_ThenLastRegistrationWins()
+ {
+ // Arrange
+ var configuration1 = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["SampleOptions:Name"] = "First",
+ ["SampleOptions:Level"] = "1",
+ })
+ .Build();
+ var configuration2 = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["SampleOptions:Name"] = "Second",
+ ["SampleOptions:Level"] = "2",
+ })
+ .Build();
+
+ var services = new ServiceCollection();
+
+ // Act
+ services.AddOptions(configuration1);
+ services.AddOptions(configuration2);
+ var provider = services.BuildServiceProvider();
+
+ // Assert
+ var value = provider.GetRequiredService>().Value;
+ value.Name.ShouldBe("Second");
+ value.Level.ShouldBe(2);
+ }
+
+ ///
+ /// The sample options class used for testing.
+ ///
+ public class SampleOptions
+ {
+ ///
+ /// Gets or sets the name.
+ ///
+ public string? Name { get; set; }
+
+ ///
+ /// Gets or sets the level.
+ ///
+ public int Level { get; set; }
+ }
+}
diff --git a/Neolution.Utilities.UnitTests/Neolution.Utilities.UnitTests.csproj b/Neolution.Utilities.UnitTests/Neolution.Utilities.UnitTests.csproj
index c483b02..229f30d 100644
--- a/Neolution.Utilities.UnitTests/Neolution.Utilities.UnitTests.csproj
+++ b/Neolution.Utilities.UnitTests/Neolution.Utilities.UnitTests.csproj
@@ -12,6 +12,8 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
all
@@ -30,4 +32,13 @@
+
+
+
+
+
+
+
+
+
diff --git a/Neolution.Utilities.UnitTests/packages.lock.json b/Neolution.Utilities.UnitTests/packages.lock.json
index fd478be..22d68f6 100644
--- a/Neolution.Utilities.UnitTests/packages.lock.json
+++ b/Neolution.Utilities.UnitTests/packages.lock.json
@@ -8,6 +8,25 @@
"resolved": "6.0.4",
"contentHash": "lkhqpF8Pu2Y7IiN7OntbsTtdbpR1syMsm2F3IgX6ootA4ffRqWL5jF7XipHuZQTdVuWG/gVAAcf8mjk8Tz0xPg=="
},
+ "Microsoft.Extensions.Configuration": {
+ "type": "Direct",
+ "requested": "[8.0.0, )",
+ "resolved": "8.0.0",
+ "contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
+ "Microsoft.Extensions.Primitives": "8.0.0"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection": {
+ "type": "Direct",
+ "requested": "[8.0.1, )",
+ "resolved": "8.0.1",
+ "contentHash": "BmANAnR5Xd4Oqw7yQ75xOAYODybZQRzdeNucg7kS5wWKd2PNnMdYtJ2Vciy0QLylRmv42DGl5+AFL9izA6F1Rw==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2"
+ }
+ },
"Microsoft.NET.Test.Sdk": {
"type": "Direct",
"requested": "[17.13.0, )",
@@ -140,6 +159,53 @@
"resolved": "17.13.0",
"contentHash": "9LIUy0y+DvUmEPtbRDw6Bay3rzwqFV8P4efTrK4CZhQle3M/QwLPjISghfcolmEGAPWxuJi6m98ZEfk4VR4Lfg=="
},
+ "Microsoft.Extensions.Configuration.Abstractions": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "8.0.0"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Binder": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "mBMoXLsr5s1y2zOHWmKsE9veDcx8h1x/c3rz4baEdQKTeDcmQAPNbB54Pi/lhFO3K431eEq6PFbMgLaa6PHFfA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "8.0.0"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
+ "type": "Transitive",
+ "resolved": "8.0.2",
+ "contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg=="
+ },
+ "Microsoft.Extensions.Options": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
+ "Microsoft.Extensions.Primitives": "8.0.0"
+ }
+ },
+ "Microsoft.Extensions.Options.ConfigurationExtensions": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
+ "Microsoft.Extensions.Configuration.Binder": "8.0.0",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
+ "Microsoft.Extensions.Options": "8.0.0",
+ "Microsoft.Extensions.Primitives": "8.0.0"
+ }
+ },
+ "Microsoft.Extensions.Primitives": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
+ },
"Microsoft.NETCore.Platforms": {
"type": "Transitive",
"resolved": "1.1.0",
@@ -1168,7 +1234,10 @@
}
},
"neolution.utilities": {
- "type": "Project"
+ "type": "Project",
+ "dependencies": {
+ "Microsoft.Extensions.Options.ConfigurationExtensions": "[8.0.0, )"
+ }
}
}
}
diff --git a/Neolution.Utilities/Extensions/IConfigurationExtensions.cs b/Neolution.Utilities/Extensions/IConfigurationExtensions.cs
new file mode 100644
index 0000000..2312c1f
--- /dev/null
+++ b/Neolution.Utilities/Extensions/IConfigurationExtensions.cs
@@ -0,0 +1,39 @@
+namespace Neolution.Utilities.Extensions;
+
+using Microsoft.Extensions.Configuration;
+
+///
+/// The IConfiguration extension methods.
+///
+public static class IConfigurationExtensions
+{
+ ///
+ /// Gets the strongly typed options.
+ ///
+ /// The options type.
+ /// The configuration.
+ /// The options.
+ public static TOptions GetOptions(this IConfiguration config)
+ {
+ ArgumentNullException.ThrowIfNull(config);
+ var section = config.GetSection(typeof(TOptions).Name);
+ if (!section.Exists())
+ {
+ throw new InvalidOperationException($"Could not find configuration section '{typeof(TOptions).Name}'");
+ }
+
+ return section.Get() ?? throw new InvalidOperationException($"Could not create '{typeof(TOptions).Name}'");
+ }
+
+ ///
+ /// Gets the section by the specified options type.
+ ///
+ /// The options type.
+ /// The configuration.
+ /// The configuration section.
+ public static IConfigurationSection GetSection(this IConfiguration config)
+ {
+ ArgumentNullException.ThrowIfNull(config);
+ return config.GetSection(typeof(T).Name);
+ }
+}
diff --git a/Neolution.Utilities/Extensions/IServiceCollectionExtensions.cs b/Neolution.Utilities/Extensions/IServiceCollectionExtensions.cs
new file mode 100644
index 0000000..98c86fe
--- /dev/null
+++ b/Neolution.Utilities/Extensions/IServiceCollectionExtensions.cs
@@ -0,0 +1,26 @@
+namespace Neolution.Utilities.Extensions;
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+///
+/// The IServiceCollection extension methods.
+///
+public static class IServiceCollectionExtensions
+{
+ ///
+ /// Adds the strongly typed options.
+ ///
+ /// The type of the options.
+ /// The service collection.
+ /// The configuration.
+ /// The service collection
+ public static IServiceCollection AddOptions(this IServiceCollection serviceCollection, IConfiguration configuration)
+ where TOptions : class
+ {
+ ArgumentNullException.ThrowIfNull(serviceCollection);
+ ArgumentNullException.ThrowIfNull(configuration);
+ serviceCollection.Configure(configuration.GetSection());
+ return serviceCollection;
+ }
+}
diff --git a/Neolution.Utilities/Neolution.Utilities.csproj b/Neolution.Utilities/Neolution.Utilities.csproj
index 250a70f..c11f153 100644
--- a/Neolution.Utilities/Neolution.Utilities.csproj
+++ b/Neolution.Utilities/Neolution.Utilities.csproj
@@ -1,6 +1,7 @@
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Neolution.Utilities/packages.lock.json b/Neolution.Utilities/packages.lock.json
index 838eaca..14bab91 100644
--- a/Neolution.Utilities/packages.lock.json
+++ b/Neolution.Utilities/packages.lock.json
@@ -2,6 +2,19 @@
"version": 1,
"dependencies": {
"net8.0": {
+ "Microsoft.Extensions.Options.ConfigurationExtensions": {
+ "type": "Direct",
+ "requested": "[8.0.0, )",
+ "resolved": "8.0.0",
+ "contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "8.0.0",
+ "Microsoft.Extensions.Configuration.Binder": "8.0.0",
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
+ "Microsoft.Extensions.Options": "8.0.0",
+ "Microsoft.Extensions.Primitives": "8.0.0"
+ }
+ },
"Neolution.CodeAnalysis": {
"type": "Direct",
"requested": "[3.2.1, )",
@@ -12,6 +25,41 @@
"StyleCop.Analyzers.Unstable": "1.2.0.556"
}
},
+ "Microsoft.Extensions.Configuration.Abstractions": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==",
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "8.0.0"
+ }
+ },
+ "Microsoft.Extensions.Configuration.Binder": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "mBMoXLsr5s1y2zOHWmKsE9veDcx8h1x/c3rz4baEdQKTeDcmQAPNbB54Pi/lhFO3K431eEq6PFbMgLaa6PHFfA==",
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.Abstractions": "8.0.0"
+ }
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg=="
+ },
+ "Microsoft.Extensions.Options": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==",
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0",
+ "Microsoft.Extensions.Primitives": "8.0.0"
+ }
+ },
+ "Microsoft.Extensions.Primitives": {
+ "type": "Transitive",
+ "resolved": "8.0.0",
+ "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g=="
+ },
"SonarAnalyzer.CSharp": {
"type": "Transitive",
"resolved": "9.20.0.85982",