Skip to content
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@
/// </summary>
public class IConfigurationExtensionsTests
{
/// <summary>
/// The same enum used for testing.
/// </summary>
public enum AppSettingKeys
{
/// <summary>
/// The test string key
/// </summary>
TestStringKey,

/// <summary>
/// The test int key
/// </summary>
TestIntKey,
}

/// <summary>
/// Test that given the null configuration when get options called then throws argument null exception.
/// </summary>
Expand Down Expand Up @@ -215,6 +231,103 @@ public void GivenMissingSection_WhenGetSectionCalled_ThenReturnsNonExistingSecti
section.Exists().ShouldBeFalse();
}

/// <summary>
/// Test that given the null configuration when get value called then throws argument null exception.
/// </summary>
[Fact]
public void GivenNullConfiguration_WhenGetValueCalled_ThenThrowsArgumentNullException()
{
// Arrange
IConfiguration? configuration = null;

// Act
var act = () => configuration!.GetValue<string, AppSettingKeys>(AppSettingKeys.TestStringKey);

// Assert
var ex = Should.Throw<ArgumentNullException>(act);
ex.ParamName.ShouldBe("config");
}

/// <summary>
/// Gets the value should return correct value when key exists.
/// </summary>
[Fact]
public void GivenConfigurationWithExistingKeys_WhenGetValueCalled_ThenReturnsValues()
{
// Arrange
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["TestStringKey"] = "TestValue",
["TestIntKey"] = "7",
})
.Build();

// Act
var value = configuration.GetValue<string, AppSettingKeys>(AppSettingKeys.TestStringKey);
var intValue = configuration.GetValue<int, AppSettingKeys>(AppSettingKeys.TestIntKey);

// Assert
value.ShouldBe("TestValue");
intValue.ShouldBe(7);
}
Comment thread
Copilot marked this conversation as resolved.

/// <summary>
/// Test that given the null configuration when get value with default called then throws argument null exception.
/// </summary>
[Fact]
public void GivenNullConfiguration_WhenGetValueWithDefaultCalled_ThenThrowsArgumentNullException()
{
// Arrange
IConfiguration? configuration = null;

// Act
var act = () => configuration!.GetValue<string, AppSettingKeys>(AppSettingKeys.TestStringKey, "fallback");

// Assert
var ex = Should.Throw<ArgumentNullException>(act);
ex.ParamName.ShouldBe("config");
}

/// <summary>
/// Test that given a missing key when get value with default called then returns the default value.
/// </summary>
[Fact]
public void GivenMissingKey_WhenGetValueWithDefaultCalled_ThenReturnsDefaultValue()
{
// Arrange
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection([])
.Build();

// Act
var value = configuration.GetValue<string, AppSettingKeys>(AppSettingKeys.TestStringKey, "fallback");

// Assert
value.ShouldBe("fallback");
}

/// <summary>
/// Test that given an existing key when get value with default called then returns configured value.
/// </summary>
[Fact]
public void GivenExistingKey_WhenGetValueWithDefaultCalled_ThenReturnsConfiguredValue()
{
// Arrange
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["TestIntKey"] = "7",
})
.Build();

// Act
var value = configuration.GetValue<int, AppSettingKeys>(AppSettingKeys.TestIntKey, 99);

// Assert
value.ShouldBe(7);
}

/// <summary>
/// The sample options class used for testing.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,72 @@ public void GivenMultipleRegistrations_WhenAddOptionsCalledTwice_ThenLastRegistr
value.Level.ShouldBe(2);
}

/// <summary>
/// Test that given the configuration with options section when add options with section name called then options are configured.
/// </summary>
[Fact]
public void GivenConfigurationWithOptionsSection_WhenAddOptionsWithSectionNameCalled_ThenOptionsAreConfigured()
{
// Arrange
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["OtherSampleOptions:Name"] = "Configured",
["OtherSampleOptions:Level"] = "7",
})
.Build();
var serviceCollection = new ServiceCollection();

// Act
serviceCollection.AddOptions<SampleOptions>(configuration, "OtherSampleOptions");
var provider = serviceCollection.BuildServiceProvider();

// Assert
var options = provider.GetRequiredService<IOptions<SampleOptions>>().Value;
options.Name.ShouldBe("Configured");
options.Level.ShouldBe(7);
}
Comment thread
Copilot marked this conversation as resolved.

/// <summary>
/// Test that given the null section name when add options with section name called then throws argument null exception.
/// </summary>
[Fact]
public void GivenNullSectionName_WhenAddOptionsWithSectionNameCalled_ThenThrowsArgumentNullException()
{
// Arrange
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection([])
.Build();
var serviceCollection = new ServiceCollection();

// Act
var act = () => serviceCollection.AddOptions<SampleOptions>(configuration, null!);

// Assert
var ex = Should.Throw<ArgumentNullException>(act);
ex.ParamName.ShouldBe("sectionName");
}

/// <summary>
/// Test that given the whitespace section name when add options with section name called then throws argument exception.
/// </summary>
[Fact]
public void GivenWhitespaceSectionName_WhenAddOptionsWithSectionNameCalled_ThenThrowsArgumentException()
{
// Arrange
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection([])
.Build();
var serviceCollection = new ServiceCollection();

// Act
var act = () => serviceCollection.AddOptions<SampleOptions>(configuration, " ");

// Assert
var ex = Should.Throw<ArgumentException>(act);
ex.ParamName.ShouldBe("sectionName");
}

/// <summary>
/// The sample options class used for testing.
/// </summary>
Expand Down
31 changes: 31 additions & 0 deletions Neolution.Utilities/Extensions/IConfigurationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,35 @@ public static IConfigurationSection GetSection<T>(this IConfiguration config)
ArgumentNullException.ThrowIfNull(config);
return config.GetSection(typeof(T).Name);
}

/// <summary>
/// Gets the value with the specified key.
/// </summary>
/// <typeparam name="T">The type of the value.</typeparam>
/// <typeparam name="TEnum">The type of the enum.</typeparam>
/// <param name="config">The configuration.</param>
/// <param name="key">The key.</param>
/// <returns>The value.</returns>
public static T? GetValue<T, TEnum>(this IConfiguration config, TEnum key)
where TEnum : struct, Enum
{
ArgumentNullException.ThrowIfNull(config);
return config.GetValue<T>(key.ToString());
}

/// <summary>
/// Gets the value with the specified key.
/// </summary>
/// <typeparam name="T">The type of the value.</typeparam>
/// <typeparam name="TEnum">The type of the enum.</typeparam>
/// <param name="config">The configuration.</param>
/// <param name="key">The key.</param>
/// <param name="defaultValue">The default value to use if no value is found.</param>
/// <returns>The value.</returns>
public static T GetValue<T, TEnum>(this IConfiguration config, TEnum key, T defaultValue)
where TEnum : struct, Enum
{
ArgumentNullException.ThrowIfNull(config);
return config.GetValue<T>(key.ToString(), defaultValue)!;
}
}
18 changes: 18 additions & 0 deletions Neolution.Utilities/Extensions/IServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,22 @@ public static IServiceCollection AddOptions<TOptions>(this IServiceCollection se
serviceCollection.Configure<TOptions>(configuration.GetSection<TOptions>());
return serviceCollection;
}

/// <summary>
/// Adds the strongly typed options.
/// </summary>
/// <typeparam name="TOptions">The type of the options.</typeparam>
/// <param name="serviceCollection">The service collection.</param>
/// <param name="configuration">The configuration.</param>
/// <param name="sectionName">The section name.</param>
/// <returns>The service collection</returns>
public static IServiceCollection AddOptions<TOptions>(this IServiceCollection serviceCollection, IConfiguration configuration, string sectionName)
where TOptions : class
{
ArgumentNullException.ThrowIfNull(serviceCollection);
ArgumentNullException.ThrowIfNull(configuration);
ArgumentException.ThrowIfNullOrWhiteSpace(sectionName);
serviceCollection.Configure<TOptions>(configuration.GetSection(sectionName));
Comment thread
drebrez marked this conversation as resolved.
return serviceCollection;
Comment thread
drebrez marked this conversation as resolved.
}
}
Loading