diff --git a/CHANGELOG.md b/CHANGELOG.md index dbfb25c..32ed738 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `IServiceCollectionExtensions.AddOptions` with custom section name +- `IConfigurationExtensions.GetValue` with strongly typed key + ## [2.0.0] - 2026-05-05 ### Changed diff --git a/Neolution.Utilities.UnitTests/Extensions/IConfigurationExtensionsTests.cs b/Neolution.Utilities.UnitTests/Extensions/IConfigurationExtensionsTests.cs index b109adb..2fdfcee 100644 --- a/Neolution.Utilities.UnitTests/Extensions/IConfigurationExtensionsTests.cs +++ b/Neolution.Utilities.UnitTests/Extensions/IConfigurationExtensionsTests.cs @@ -7,6 +7,22 @@ /// public class IConfigurationExtensionsTests { + /// + /// The same enum used for testing. + /// + public enum AppSettingKeys + { + /// + /// The test string key + /// + TestStringKey, + + /// + /// The test int key + /// + TestIntKey, + } + /// /// Test that given the null configuration when get options called then throws argument null exception. /// @@ -215,6 +231,103 @@ public void GivenMissingSection_WhenGetSectionCalled_ThenReturnsNonExistingSecti section.Exists().ShouldBeFalse(); } + /// + /// Test that given the null configuration when get value called then throws argument null exception. + /// + [Fact] + public void GivenNullConfiguration_WhenGetValueCalled_ThenThrowsArgumentNullException() + { + // Arrange + IConfiguration? configuration = null; + + // Act + var act = () => configuration!.GetValue(AppSettingKeys.TestStringKey); + + // Assert + var ex = Should.Throw(act); + ex.ParamName.ShouldBe("config"); + } + + /// + /// Gets the value should return correct value when key exists. + /// + [Fact] + public void GivenConfigurationWithExistingKeys_WhenGetValueCalled_ThenReturnsValues() + { + // Arrange + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["TestStringKey"] = "TestValue", + ["TestIntKey"] = "7", + }) + .Build(); + + // Act + var value = configuration.GetValue(AppSettingKeys.TestStringKey); + var intValue = configuration.GetValue(AppSettingKeys.TestIntKey); + + // Assert + value.ShouldBe("TestValue"); + intValue.ShouldBe(7); + } + + /// + /// Test that given the null configuration when get value with default called then throws argument null exception. + /// + [Fact] + public void GivenNullConfiguration_WhenGetValueWithDefaultCalled_ThenThrowsArgumentNullException() + { + // Arrange + IConfiguration? configuration = null; + + // Act + var act = () => configuration!.GetValue(AppSettingKeys.TestStringKey, "fallback"); + + // Assert + var ex = Should.Throw(act); + ex.ParamName.ShouldBe("config"); + } + + /// + /// Test that given a missing key when get value with default called then returns the default value. + /// + [Fact] + public void GivenMissingKey_WhenGetValueWithDefaultCalled_ThenReturnsDefaultValue() + { + // Arrange + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection([]) + .Build(); + + // Act + var value = configuration.GetValue(AppSettingKeys.TestStringKey, "fallback"); + + // Assert + value.ShouldBe("fallback"); + } + + /// + /// Test that given an existing key when get value with default called then returns configured value. + /// + [Fact] + public void GivenExistingKey_WhenGetValueWithDefaultCalled_ThenReturnsConfiguredValue() + { + // Arrange + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["TestIntKey"] = "7", + }) + .Build(); + + // Act + var value = configuration.GetValue(AppSettingKeys.TestIntKey, 99); + + // Assert + value.ShouldBe(7); + } + /// /// The sample options class used for testing. /// diff --git a/Neolution.Utilities.UnitTests/Extensions/IServiceCollectionExtensionsTests.cs b/Neolution.Utilities.UnitTests/Extensions/IServiceCollectionExtensionsTests.cs index a4f69e5..5f7fd0c 100644 --- a/Neolution.Utilities.UnitTests/Extensions/IServiceCollectionExtensionsTests.cs +++ b/Neolution.Utilities.UnitTests/Extensions/IServiceCollectionExtensionsTests.cs @@ -149,6 +149,72 @@ public void GivenMultipleRegistrations_WhenAddOptionsCalledTwice_ThenLastRegistr value.Level.ShouldBe(2); } + /// + /// Test that given the configuration with options section when add options with section name called then options are configured. + /// + [Fact] + public void GivenConfigurationWithOptionsSection_WhenAddOptionsWithSectionNameCalled_ThenOptionsAreConfigured() + { + // Arrange + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["OtherSampleOptions:Name"] = "Configured", + ["OtherSampleOptions:Level"] = "7", + }) + .Build(); + var serviceCollection = new ServiceCollection(); + + // Act + serviceCollection.AddOptions(configuration, "OtherSampleOptions"); + var provider = serviceCollection.BuildServiceProvider(); + + // Assert + var options = provider.GetRequiredService>().Value; + options.Name.ShouldBe("Configured"); + options.Level.ShouldBe(7); + } + + /// + /// Test that given the null section name when add options with section name called then throws argument null exception. + /// + [Fact] + public void GivenNullSectionName_WhenAddOptionsWithSectionNameCalled_ThenThrowsArgumentNullException() + { + // Arrange + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection([]) + .Build(); + var serviceCollection = new ServiceCollection(); + + // Act + var act = () => serviceCollection.AddOptions(configuration, null!); + + // Assert + var ex = Should.Throw(act); + ex.ParamName.ShouldBe("sectionName"); + } + + /// + /// Test that given the whitespace section name when add options with section name called then throws argument exception. + /// + [Fact] + public void GivenWhitespaceSectionName_WhenAddOptionsWithSectionNameCalled_ThenThrowsArgumentException() + { + // Arrange + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection([]) + .Build(); + var serviceCollection = new ServiceCollection(); + + // Act + var act = () => serviceCollection.AddOptions(configuration, " "); + + // Assert + var ex = Should.Throw(act); + ex.ParamName.ShouldBe("sectionName"); + } + /// /// The sample options class used for testing. /// diff --git a/Neolution.Utilities/Extensions/IConfigurationExtensions.cs b/Neolution.Utilities/Extensions/IConfigurationExtensions.cs index 2312c1f..d8997f4 100644 --- a/Neolution.Utilities/Extensions/IConfigurationExtensions.cs +++ b/Neolution.Utilities/Extensions/IConfigurationExtensions.cs @@ -36,4 +36,35 @@ public static IConfigurationSection GetSection(this IConfiguration config) ArgumentNullException.ThrowIfNull(config); return config.GetSection(typeof(T).Name); } + + /// + /// Gets the value with the specified key. + /// + /// The type of the value. + /// The type of the enum. + /// The configuration. + /// The key. + /// The value. + public static T? GetValue(this IConfiguration config, TEnum key) + where TEnum : struct, Enum + { + ArgumentNullException.ThrowIfNull(config); + return config.GetValue(key.ToString()); + } + + /// + /// Gets the value with the specified key. + /// + /// The type of the value. + /// The type of the enum. + /// The configuration. + /// The key. + /// The default value to use if no value is found. + /// The value. + public static T GetValue(this IConfiguration config, TEnum key, T defaultValue) + where TEnum : struct, Enum + { + ArgumentNullException.ThrowIfNull(config); + return config.GetValue(key.ToString(), defaultValue)!; + } } diff --git a/Neolution.Utilities/Extensions/IServiceCollectionExtensions.cs b/Neolution.Utilities/Extensions/IServiceCollectionExtensions.cs index 98c86fe..8da63e3 100644 --- a/Neolution.Utilities/Extensions/IServiceCollectionExtensions.cs +++ b/Neolution.Utilities/Extensions/IServiceCollectionExtensions.cs @@ -23,4 +23,22 @@ public static IServiceCollection AddOptions(this IServiceCollection se serviceCollection.Configure(configuration.GetSection()); return serviceCollection; } + + /// + /// Adds the strongly typed options. + /// + /// The type of the options. + /// The service collection. + /// The configuration. + /// The section name. + /// The service collection + public static IServiceCollection AddOptions(this IServiceCollection serviceCollection, IConfiguration configuration, string sectionName) + where TOptions : class + { + ArgumentNullException.ThrowIfNull(serviceCollection); + ArgumentNullException.ThrowIfNull(configuration); + ArgumentException.ThrowIfNullOrWhiteSpace(sectionName); + serviceCollection.Configure(configuration.GetSection(sectionName)); + return serviceCollection; + } }