From 4fb14782be14f9fb59228a6b054395f5c007d7d7 Mon Sep 17 00:00:00 2001 From: Daniele Debernardi Date: Fri, 3 Oct 2025 12:31:42 +0200 Subject: [PATCH 1/8] Add IConfiguration and IServiceCollection extensions --- CHANGELOG.md | 2 + .../IConfigurationExtensionsTests.cs | 142 ++++++++++++++++++ .../IServiceCollectionExtensionsTests.cs | 93 ++++++++++++ ...tion.Utilities.AspNetCore.UnitTests.csproj | 6 +- .../Extensions/IConfigurationExtensions.cs | 34 +++++ .../IServiceCollectionExtensions.cs | 23 +++ 6 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 Neolution.Utilities.AspNetCore.UnitTests/Extensions/IConfigurationExtensionsTests.cs create mode 100644 Neolution.Utilities.AspNetCore.UnitTests/Extensions/IServiceCollectionExtensionsTests.cs create mode 100644 Neolution.Utilities.AspNetCore/Extensions/IConfigurationExtensions.cs create mode 100644 Neolution.Utilities.AspNetCore/Extensions/IServiceCollectionExtensions.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 906959d..43241ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,4 +13,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `CultureInfo` extension method to get the language code of a culture - `IEnumerable` extension methods - `IFormFile` extension method (AspNetCore package only) +- `IConfiguration` extension methods (AspNetCore package only) +- `IServiceCollection` extension methods (AspNetCore package only) - `DbSet` extension methods for ISortableEntity interface (EntityFrameworkCore package only) diff --git a/Neolution.Utilities.AspNetCore.UnitTests/Extensions/IConfigurationExtensionsTests.cs b/Neolution.Utilities.AspNetCore.UnitTests/Extensions/IConfigurationExtensionsTests.cs new file mode 100644 index 0000000..8a97595 --- /dev/null +++ b/Neolution.Utilities.AspNetCore.UnitTests/Extensions/IConfigurationExtensionsTests.cs @@ -0,0 +1,142 @@ +namespace Neolution.Utilities.AspNetCore.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 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.AspNetCore.UnitTests/Extensions/IServiceCollectionExtensionsTests.cs b/Neolution.Utilities.AspNetCore.UnitTests/Extensions/IServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000..353d748 --- /dev/null +++ b/Neolution.Utilities.AspNetCore.UnitTests/Extensions/IServiceCollectionExtensionsTests.cs @@ -0,0 +1,93 @@ +namespace Neolution.Utilities.AspNetCore.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 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); + } + + /// + /// 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.AspNetCore.UnitTests/Neolution.Utilities.AspNetCore.UnitTests.csproj b/Neolution.Utilities.AspNetCore.UnitTests/Neolution.Utilities.AspNetCore.UnitTests.csproj index e2ddbfa..2bead9b 100644 --- a/Neolution.Utilities.AspNetCore.UnitTests/Neolution.Utilities.AspNetCore.UnitTests.csproj +++ b/Neolution.Utilities.AspNetCore.UnitTests/Neolution.Utilities.AspNetCore.UnitTests.csproj @@ -1,4 +1,4 @@ - + false @@ -29,7 +29,11 @@ + + + + diff --git a/Neolution.Utilities.AspNetCore/Extensions/IConfigurationExtensions.cs b/Neolution.Utilities.AspNetCore/Extensions/IConfigurationExtensions.cs new file mode 100644 index 0000000..5a2c4c8 --- /dev/null +++ b/Neolution.Utilities.AspNetCore/Extensions/IConfigurationExtensions.cs @@ -0,0 +1,34 @@ +namespace Neolution.Utilities.AspNetCore.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); + return config.GetSection(typeof(TOptions).Name).Get() + ?? throw new InvalidOperationException($"Could not find configuration section '{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.AspNetCore/Extensions/IServiceCollectionExtensions.cs b/Neolution.Utilities.AspNetCore/Extensions/IServiceCollectionExtensions.cs new file mode 100644 index 0000000..6728b27 --- /dev/null +++ b/Neolution.Utilities.AspNetCore/Extensions/IServiceCollectionExtensions.cs @@ -0,0 +1,23 @@ +namespace Neolution.Utilities.AspNetCore.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 services. + /// The configuration. + public static void AddOptions(this IServiceCollection services, IConfiguration configuration) + where TOptions : class + { + ArgumentNullException.ThrowIfNull(configuration); + services.Configure(configuration.GetSection()); + } +} From 3427276f925f82c5bc0ef31ee95d63993d58af36 Mon Sep 17 00:00:00 2001 From: Daniele Debernardi Date: Fri, 3 Oct 2025 12:35:00 +0200 Subject: [PATCH 2/8] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Extensions/IServiceCollectionExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Neolution.Utilities.AspNetCore/Extensions/IServiceCollectionExtensions.cs b/Neolution.Utilities.AspNetCore/Extensions/IServiceCollectionExtensions.cs index 6728b27..99409ea 100644 --- a/Neolution.Utilities.AspNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/Neolution.Utilities.AspNetCore/Extensions/IServiceCollectionExtensions.cs @@ -17,6 +17,7 @@ public static class IServiceCollectionExtensions public static void AddOptions(this IServiceCollection services, IConfiguration configuration) where TOptions : class { + ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(configuration); services.Configure(configuration.GetSection()); } From cec1f221afaf7cbc54197f1776358d7018ba313b Mon Sep 17 00:00:00 2001 From: Daniele Debernardi Date: Fri, 3 Oct 2025 12:36:37 +0200 Subject: [PATCH 3/8] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Extensions/IServiceCollectionExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Neolution.Utilities.AspNetCore/Extensions/IServiceCollectionExtensions.cs b/Neolution.Utilities.AspNetCore/Extensions/IServiceCollectionExtensions.cs index 99409ea..508c6ce 100644 --- a/Neolution.Utilities.AspNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/Neolution.Utilities.AspNetCore/Extensions/IServiceCollectionExtensions.cs @@ -15,10 +15,11 @@ public static class IServiceCollectionExtensions /// The services. /// The configuration. public static void AddOptions(this IServiceCollection services, IConfiguration configuration) + public static IServiceCollection AddOptions(this IServiceCollection services, IConfiguration configuration) where TOptions : class { - ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(configuration); services.Configure(configuration.GetSection()); + return services; } } From a329f5b48d67ac105d1b0b01361ca1b518ee368e Mon Sep 17 00:00:00 2001 From: Daniele Debernardi Date: Fri, 3 Oct 2025 12:41:03 +0200 Subject: [PATCH 4/8] fix copilot suggestion --- .../Extensions/IServiceCollectionExtensions.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Neolution.Utilities.AspNetCore/Extensions/IServiceCollectionExtensions.cs b/Neolution.Utilities.AspNetCore/Extensions/IServiceCollectionExtensions.cs index 508c6ce..d1609b4 100644 --- a/Neolution.Utilities.AspNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/Neolution.Utilities.AspNetCore/Extensions/IServiceCollectionExtensions.cs @@ -12,14 +12,15 @@ public static class IServiceCollectionExtensions /// Adds the strongly typed options. /// /// The type of the options. - /// The services. + /// The service collection. /// The configuration. - public static void AddOptions(this IServiceCollection services, IConfiguration configuration) - public static IServiceCollection AddOptions(this IServiceCollection services, IConfiguration configuration) + /// The service collection + public static IServiceCollection AddOptions(this IServiceCollection serviceCollection, IConfiguration configuration) where TOptions : class { + ArgumentNullException.ThrowIfNull(serviceCollection); ArgumentNullException.ThrowIfNull(configuration); - services.Configure(configuration.GetSection()); - return services; + serviceCollection.Configure(configuration.GetSection()); + return serviceCollection; } } From e84e619f454135ea82a6d6aa24a2d09bae58d1e1 Mon Sep 17 00:00:00 2001 From: Daniele Debernardi Date: Fri, 3 Oct 2025 13:33:23 +0200 Subject: [PATCH 5/8] move to core --- CHANGELOG.md | 4 +- ...tion.Utilities.AspNetCore.UnitTests.csproj | 6 +- .../IConfigurationExtensionsTests.cs | 2 +- .../IServiceCollectionExtensionsTests.cs | 2 +- .../Neolution.Utilities.UnitTests.csproj | 9 +++ .../packages.lock.json | 71 ++++++++++++++++++- .../Extensions/IConfigurationExtensions.cs | 2 +- .../IServiceCollectionExtensions.cs | 2 +- .../Neolution.Utilities.csproj | 3 + Neolution.Utilities/packages.lock.json | 67 +++++++++++++++++ 10 files changed, 156 insertions(+), 12 deletions(-) rename {Neolution.Utilities.AspNetCore.UnitTests => Neolution.Utilities.UnitTests}/Extensions/IConfigurationExtensionsTests.cs (98%) rename {Neolution.Utilities.AspNetCore.UnitTests => Neolution.Utilities.UnitTests}/Extensions/IServiceCollectionExtensionsTests.cs (97%) rename {Neolution.Utilities.AspNetCore => Neolution.Utilities}/Extensions/IConfigurationExtensions.cs (95%) rename {Neolution.Utilities.AspNetCore => Neolution.Utilities}/Extensions/IServiceCollectionExtensions.cs (94%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43241ef..6851ca2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +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) -- `IConfiguration` extension methods (AspNetCore package only) -- `IServiceCollection` extension methods (AspNetCore package only) - `DbSet` extension methods for ISortableEntity interface (EntityFrameworkCore package only) diff --git a/Neolution.Utilities.AspNetCore.UnitTests/Neolution.Utilities.AspNetCore.UnitTests.csproj b/Neolution.Utilities.AspNetCore.UnitTests/Neolution.Utilities.AspNetCore.UnitTests.csproj index 2bead9b..e2ddbfa 100644 --- a/Neolution.Utilities.AspNetCore.UnitTests/Neolution.Utilities.AspNetCore.UnitTests.csproj +++ b/Neolution.Utilities.AspNetCore.UnitTests/Neolution.Utilities.AspNetCore.UnitTests.csproj @@ -1,4 +1,4 @@ - + false @@ -29,11 +29,7 @@ - - - - diff --git a/Neolution.Utilities.AspNetCore.UnitTests/Extensions/IConfigurationExtensionsTests.cs b/Neolution.Utilities.UnitTests/Extensions/IConfigurationExtensionsTests.cs similarity index 98% rename from Neolution.Utilities.AspNetCore.UnitTests/Extensions/IConfigurationExtensionsTests.cs rename to Neolution.Utilities.UnitTests/Extensions/IConfigurationExtensionsTests.cs index 8a97595..375e722 100644 --- a/Neolution.Utilities.AspNetCore.UnitTests/Extensions/IConfigurationExtensionsTests.cs +++ b/Neolution.Utilities.UnitTests/Extensions/IConfigurationExtensionsTests.cs @@ -1,4 +1,4 @@ -namespace Neolution.Utilities.AspNetCore.UnitTests.Extensions; +namespace Neolution.Utilities.UnitTests.Extensions; using Microsoft.Extensions.Configuration; diff --git a/Neolution.Utilities.AspNetCore.UnitTests/Extensions/IServiceCollectionExtensionsTests.cs b/Neolution.Utilities.UnitTests/Extensions/IServiceCollectionExtensionsTests.cs similarity index 97% rename from Neolution.Utilities.AspNetCore.UnitTests/Extensions/IServiceCollectionExtensionsTests.cs rename to Neolution.Utilities.UnitTests/Extensions/IServiceCollectionExtensionsTests.cs index 353d748..c60335e 100644 --- a/Neolution.Utilities.AspNetCore.UnitTests/Extensions/IServiceCollectionExtensionsTests.cs +++ b/Neolution.Utilities.UnitTests/Extensions/IServiceCollectionExtensionsTests.cs @@ -1,4 +1,4 @@ -namespace Neolution.Utilities.AspNetCore.UnitTests.Extensions; +namespace Neolution.Utilities.UnitTests.Extensions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; diff --git a/Neolution.Utilities.UnitTests/Neolution.Utilities.UnitTests.csproj b/Neolution.Utilities.UnitTests/Neolution.Utilities.UnitTests.csproj index c483b02..9ec745b 100644 --- a/Neolution.Utilities.UnitTests/Neolution.Utilities.UnitTests.csproj +++ b/Neolution.Utilities.UnitTests/Neolution.Utilities.UnitTests.csproj @@ -30,4 +30,13 @@ + + + + + + + + + diff --git a/Neolution.Utilities.UnitTests/packages.lock.json b/Neolution.Utilities.UnitTests/packages.lock.json index fd478be..0dd7d35 100644 --- a/Neolution.Utilities.UnitTests/packages.lock.json +++ b/Neolution.Utilities.UnitTests/packages.lock.json @@ -140,6 +140,70 @@ "resolved": "17.13.0", "contentHash": "9LIUy0y+DvUmEPtbRDw6Bay3rzwqFV8P4efTrK4CZhQle3M/QwLPjISghfcolmEGAPWxuJi6m98ZEfk4VR4Lfg==" }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "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.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": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "BmANAnR5Xd4Oqw7yQ75xOAYODybZQRzdeNucg7kS5wWKd2PNnMdYtJ2Vciy0QLylRmv42DGl5+AFL9izA6F1Rw==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2" + } + }, + "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 +1232,12 @@ } }, "neolution.utilities": { - "type": "Project" + "type": "Project", + "dependencies": { + "Microsoft.Extensions.Configuration": "[8.0.0, )", + "Microsoft.Extensions.DependencyInjection": "[8.0.1, )", + "Microsoft.Extensions.Options.ConfigurationExtensions": "[8.0.0, )" + } } } } diff --git a/Neolution.Utilities.AspNetCore/Extensions/IConfigurationExtensions.cs b/Neolution.Utilities/Extensions/IConfigurationExtensions.cs similarity index 95% rename from Neolution.Utilities.AspNetCore/Extensions/IConfigurationExtensions.cs rename to Neolution.Utilities/Extensions/IConfigurationExtensions.cs index 5a2c4c8..cdd6bb6 100644 --- a/Neolution.Utilities.AspNetCore/Extensions/IConfigurationExtensions.cs +++ b/Neolution.Utilities/Extensions/IConfigurationExtensions.cs @@ -1,4 +1,4 @@ -namespace Neolution.Utilities.AspNetCore.Extensions; +namespace Neolution.Utilities.Extensions; using Microsoft.Extensions.Configuration; diff --git a/Neolution.Utilities.AspNetCore/Extensions/IServiceCollectionExtensions.cs b/Neolution.Utilities/Extensions/IServiceCollectionExtensions.cs similarity index 94% rename from Neolution.Utilities.AspNetCore/Extensions/IServiceCollectionExtensions.cs rename to Neolution.Utilities/Extensions/IServiceCollectionExtensions.cs index d1609b4..98c86fe 100644 --- a/Neolution.Utilities.AspNetCore/Extensions/IServiceCollectionExtensions.cs +++ b/Neolution.Utilities/Extensions/IServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -namespace Neolution.Utilities.AspNetCore.Extensions; +namespace Neolution.Utilities.Extensions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; diff --git a/Neolution.Utilities/Neolution.Utilities.csproj b/Neolution.Utilities/Neolution.Utilities.csproj index 250a70f..1286434 100644 --- a/Neolution.Utilities/Neolution.Utilities.csproj +++ b/Neolution.Utilities/Neolution.Utilities.csproj @@ -1,6 +1,9 @@  + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Neolution.Utilities/packages.lock.json b/Neolution.Utilities/packages.lock.json index 838eaca..7afad1b 100644 --- a/Neolution.Utilities/packages.lock.json +++ b/Neolution.Utilities/packages.lock.json @@ -2,6 +2,38 @@ "version": 1, "dependencies": { "net8.0": { + "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.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 +44,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.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.Primitives": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" + }, "SonarAnalyzer.CSharp": { "type": "Transitive", "resolved": "9.20.0.85982", From 3269323909910ffec6a6cd1c32540c3421fffdc9 Mon Sep 17 00:00:00 2001 From: Daniele Debernardi Date: Fri, 3 Oct 2025 14:32:28 +0200 Subject: [PATCH 6/8] reduce dependencies --- .../Neolution.Utilities.UnitTests.csproj | 2 + .../packages.lock.json | 38 +++++++++---------- .../Neolution.Utilities.csproj | 2 - Neolution.Utilities/packages.lock.json | 23 +---------- 4 files changed, 23 insertions(+), 42 deletions(-) diff --git a/Neolution.Utilities.UnitTests/Neolution.Utilities.UnitTests.csproj b/Neolution.Utilities.UnitTests/Neolution.Utilities.UnitTests.csproj index 9ec745b..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 diff --git a/Neolution.Utilities.UnitTests/packages.lock.json b/Neolution.Utilities.UnitTests/packages.lock.json index 0dd7d35..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,15 +159,6 @@ "resolved": "17.13.0", "contentHash": "9LIUy0y+DvUmEPtbRDw6Bay3rzwqFV8P4efTrK4CZhQle3M/QwLPjISghfcolmEGAPWxuJi6m98ZEfk4VR4Lfg==" }, - "Microsoft.Extensions.Configuration": { - "type": "Transitive", - "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.Configuration.Abstractions": { "type": "Transitive", "resolved": "8.0.0", @@ -165,14 +175,6 @@ "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" } }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "BmANAnR5Xd4Oqw7yQ75xOAYODybZQRzdeNucg7kS5wWKd2PNnMdYtJ2Vciy0QLylRmv42DGl5+AFL9izA6F1Rw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2" - } - }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "8.0.2", @@ -1234,8 +1236,6 @@ "neolution.utilities": { "type": "Project", "dependencies": { - "Microsoft.Extensions.Configuration": "[8.0.0, )", - "Microsoft.Extensions.DependencyInjection": "[8.0.1, )", "Microsoft.Extensions.Options.ConfigurationExtensions": "[8.0.0, )" } } diff --git a/Neolution.Utilities/Neolution.Utilities.csproj b/Neolution.Utilities/Neolution.Utilities.csproj index 1286434..c11f153 100644 --- a/Neolution.Utilities/Neolution.Utilities.csproj +++ b/Neolution.Utilities/Neolution.Utilities.csproj @@ -1,8 +1,6 @@  - - all diff --git a/Neolution.Utilities/packages.lock.json b/Neolution.Utilities/packages.lock.json index 7afad1b..14bab91 100644 --- a/Neolution.Utilities/packages.lock.json +++ b/Neolution.Utilities/packages.lock.json @@ -2,25 +2,6 @@ "version": 1, "dependencies": { "net8.0": { - "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.Extensions.Options.ConfigurationExtensions": { "type": "Direct", "requested": "[8.0.0, )", @@ -62,8 +43,8 @@ }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "8.0.2", - "contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==" + "resolved": "8.0.0", + "contentHash": "cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==" }, "Microsoft.Extensions.Options": { "type": "Transitive", From 6c0edbdded92e6ca6e233dd88f0df79892b4b8cc Mon Sep 17 00:00:00 2001 From: Daniele Debernardi Date: Fri, 3 Oct 2025 14:54:43 +0200 Subject: [PATCH 7/8] Update Neolution.Utilities/Extensions/IConfigurationExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Extensions/IConfigurationExtensions.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Neolution.Utilities/Extensions/IConfigurationExtensions.cs b/Neolution.Utilities/Extensions/IConfigurationExtensions.cs index cdd6bb6..d48053d 100644 --- a/Neolution.Utilities/Extensions/IConfigurationExtensions.cs +++ b/Neolution.Utilities/Extensions/IConfigurationExtensions.cs @@ -16,8 +16,12 @@ public static class IConfigurationExtensions public static TOptions GetOptions(this IConfiguration config) { ArgumentNullException.ThrowIfNull(config); - return config.GetSection(typeof(TOptions).Name).Get() - ?? throw new InvalidOperationException($"Could not find configuration section '{typeof(TOptions).Name}'"); + var section = config.GetSection(typeof(TOptions).Name); + if (!section.Exists()) + { + throw new InvalidOperationException($"Could not find configuration section '{typeof(TOptions).Name}'"); + } + return section.Get(); } /// From 6b7ff233493ce9f3b3c4314d5771066fa20cb9f4 Mon Sep 17 00:00:00 2001 From: Daniele Debernardi Date: Fri, 3 Oct 2025 15:14:25 +0200 Subject: [PATCH 8/8] Fix and add more tests --- .../IConfigurationExtensionsTests.cs | 69 +++++++++++++++++ .../IServiceCollectionExtensionsTests.cs | 74 +++++++++++++++++++ .../Extensions/IConfigurationExtensions.cs | 3 +- 3 files changed, 145 insertions(+), 1 deletion(-) diff --git a/Neolution.Utilities.UnitTests/Extensions/IConfigurationExtensionsTests.cs b/Neolution.Utilities.UnitTests/Extensions/IConfigurationExtensionsTests.cs index 375e722..c052258 100644 --- a/Neolution.Utilities.UnitTests/Extensions/IConfigurationExtensionsTests.cs +++ b/Neolution.Utilities.UnitTests/Extensions/IConfigurationExtensionsTests.cs @@ -66,6 +66,75 @@ public void GivenValidSection_WhenGetOptionsCalled_ThenBindsAndReturnsOptions() 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. /// diff --git a/Neolution.Utilities.UnitTests/Extensions/IServiceCollectionExtensionsTests.cs b/Neolution.Utilities.UnitTests/Extensions/IServiceCollectionExtensionsTests.cs index c60335e..a4f69e5 100644 --- a/Neolution.Utilities.UnitTests/Extensions/IServiceCollectionExtensionsTests.cs +++ b/Neolution.Utilities.UnitTests/Extensions/IServiceCollectionExtensionsTests.cs @@ -9,6 +9,26 @@ /// 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. /// @@ -75,6 +95,60 @@ public void GivenConfigurationWithoutOptionsSection_WhenAddOptionsCalled_ThenOpt 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. /// diff --git a/Neolution.Utilities/Extensions/IConfigurationExtensions.cs b/Neolution.Utilities/Extensions/IConfigurationExtensions.cs index d48053d..2312c1f 100644 --- a/Neolution.Utilities/Extensions/IConfigurationExtensions.cs +++ b/Neolution.Utilities/Extensions/IConfigurationExtensions.cs @@ -21,7 +21,8 @@ public static TOptions GetOptions(this IConfiguration config) { throw new InvalidOperationException($"Could not find configuration section '{typeof(TOptions).Name}'"); } - return section.Get(); + + return section.Get() ?? throw new InvalidOperationException($"Could not create '{typeof(TOptions).Name}'"); } ///