Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Optify.Tests/DummySettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ public class UnmarkedDummyClassSettings
public string? X { get; init; }
}

[OptifyOptions(SectionName = "")]
public class EmptySectionNameDummyClassSettings
{
public string? X { get; init; }
}

[OptifyOptions]
public class DummySettingsWithRequiredKeyword
{
Expand All @@ -63,3 +69,10 @@ public class AttrValidatedOnStartDummySettings
[Required]
public string? X { get; init; }
}

[OptifyOptions(Validation = ValidationFlag.OnStart)]
public class AttrOnStartOnlyDummySettings
{
[Required]
public string? X { get; init; }
}
12 changes: 12 additions & 0 deletions Optify.Tests/TestData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ public static IEnumerable<string> GenericStringTestData()
yield return "two";
}

/// <summary>
/// Test data for null or whitespace strings.
/// </summary>
public static IEnumerable<string?> NullOrWhitespaceTestData()
{
yield return null;
yield return "";
yield return " ";
}

/// <summary>
/// A valid entry for all properties on all dummy settings types in the testing project.
/// Tests should start with this and override what is needed within the test.
Expand All @@ -23,9 +33,11 @@ public static IEnumerable<string> GenericStringTestData()
new("OverrideNamedDummyClassSettings:X", "valid"),
new("OverrideNamedDummyRecordSettings:X", "valid"),
new($"{nameof(UnmarkedDummyClassSettings)}:X", "valid"),
new($"{nameof(EmptySectionNameDummyClassSettings)}:X", "valid"),
new($"{nameof(DummySettingsWithRequiredKeyword)}:X", "valid"),
new($"{nameof(ValidatedDummySettings)}:X", "valid"),
new($"{nameof(AttrValidatedDummySettings)}:X", "valid"),
new($"{nameof(AttrValidatedOnStartDummySettings)}:X", "valid"),
new($"{nameof(AttrOnStartOnlyDummySettings)}:X", "valid"),
];
}
70 changes: 70 additions & 0 deletions Optify.Tests/UseOptifyTSectionNameTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,74 @@ public async Task Registers_section_name_from_type_name_as_third_priority()

await Assert.That(options.Value.X).IsEqualTo("came-from-type-name");
}

[Test]
public async Task Registers_section_name_from_type_name_when_no_attribute_present()
{
var host = new HostBuilder()
.IncludeConfiguration([
.. TestData.AllTestSettings,
new("UnmarkedDummyClassSettings:X", "came-from-type-name"),
])
.UseOptify<UnmarkedDummyClassSettings>(new OptifyConfiguration())
.Build();

var options = host.Services.GetRequiredService<IOptions<UnmarkedDummyClassSettings>>();

await Assert.That(options.Value.X).IsEqualTo("came-from-type-name");
}

[Test]
[MethodDataSource<TestData>(nameof(TestData.NullOrWhitespaceTestData))]
public async Task Registers_section_name_from_type_name_when_configuration_section_name_is_null_or_whitespace(
string? sectionName
)
{
var host = new HostBuilder()
.IncludeConfiguration([
.. TestData.AllTestSettings,
new("UnmarkedDummyClassSettings:X", "came-from-type-name"),
])
.UseOptify<UnmarkedDummyClassSettings>(new OptifyConfiguration { SectionName = sectionName })
.Build();

var options = host.Services.GetRequiredService<IOptions<UnmarkedDummyClassSettings>>();

await Assert.That(options.Value.X).IsEqualTo("came-from-type-name");
}

[Test]
[MethodDataSource<TestData>(nameof(TestData.NullOrWhitespaceTestData))]
public async Task Registers_section_name_from_type_name_when_both_configuration_and_attribute_section_names_are_null_or_whitespace(
string? sectionName
)
{
var host = new HostBuilder()
.IncludeConfiguration([
.. TestData.AllTestSettings,
new("EmptySectionNameDummyClassSettings:X", "came-from-type-name"),
])
.UseOptify<EmptySectionNameDummyClassSettings>(new OptifyConfiguration { SectionName = sectionName })
.Build();

var options = host.Services.GetRequiredService<IOptions<EmptySectionNameDummyClassSettings>>();

await Assert.That(options.Value.X).IsEqualTo("came-from-type-name");
}

[Test]
public async Task Registers_section_name_from_type_name_when_configuration_is_null_and_no_attribute()
{
var host = new HostBuilder()
.IncludeConfiguration([
.. TestData.AllTestSettings,
new("UnmarkedDummyClassSettings:X", "came-from-type-name"),
])
.UseOptify<UnmarkedDummyClassSettings>(null!)
.Build();

var options = host.Services.GetRequiredService<IOptions<UnmarkedDummyClassSettings>>();

await Assert.That(options.Value.X).IsEqualTo("came-from-type-name");
}
}
54 changes: 54 additions & 0 deletions Optify.Tests/UseOptifyTValidationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,58 @@ await Assert
})
.Throws<OptionsValidationException>();
}

[Test]
public async Task Combines_validation_flags_from_configuration_and_attribute()
{
// Attribute has OnStart, configuration has DataAnnotations
// Combined: DataAnnotations | OnStart = validates on startup
var host = new HostBuilder()
.IncludeConfiguration([.. TestData.AllTestSettings, new("AttrOnStartOnlyDummySettings:X", null)])
.UseOptify<AttrOnStartOnlyDummySettings>(
new OptifyConfiguration { Validation = ValidationFlag.DataAnnotations }
)
.Build();

await Assert.That(() => host.StartAsync()).Throws<OptionsValidationException>();
}

[Test]
public async Task Uses_attribute_validation_when_configuration_is_null()
{
var host = new HostBuilder()
.IncludeConfiguration([.. TestData.AllTestSettings, new("AttrValidatedDummySettings:X", null)])
.UseOptify<AttrValidatedDummySettings>(null!)
.Build();

var options = host.Services.GetRequiredService<IOptions<AttrValidatedDummySettings>>();

await Assert.That(() => options.Value).Throws<OptionsValidationException>();
}

[Test]
public async Task Uses_attribute_validation_when_configuration_has_no_validation()
{
var host = new HostBuilder()
.IncludeConfiguration([.. TestData.AllTestSettings, new("AttrValidatedDummySettings:X", null)])
.UseOptify<AttrValidatedDummySettings>(new OptifyConfiguration())
.Build();

var options = host.Services.GetRequiredService<IOptions<AttrValidatedDummySettings>>();

await Assert.That(() => options.Value).Throws<OptionsValidationException>();
}

[Test]
public async Task No_validation_when_neither_configuration_nor_attribute_specify_validation()
{
var host = new HostBuilder()
.IncludeConfiguration([.. TestData.AllTestSettings, new("DummyClassSettingsA:X", null)])
.UseOptify<DummyClassSettingsA>(new OptifyConfiguration())
.Build();

var options = host.Services.GetRequiredService<IOptions<DummyClassSettingsA>>();

await Assert.That(() => options.Value).ThrowsNothing();
}
}
26 changes: 12 additions & 14 deletions Optify/HostBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,22 @@ public static IHostBuilder UseOptify<T>(this IHostBuilder hostBuilder, OptifyCon
(ctx, services) =>
{
var type = typeof(T);
var attribute = type.GetCustomAttributes(typeof(OptifyOptionsAttribute), false)
.FirstOrDefault(a => a is OptifyOptionsAttribute);
var attribute =
type.GetCustomAttributes(typeof(OptifyOptionsAttribute), false)
.FirstOrDefault(a => a is OptifyOptionsAttribute) as OptifyOptionsAttribute;

// Can come from 3 sources. Order of precedence:
// 1. `configuration.SectionName`
// 2. Attribute `SectionName`
// 3. `typeof(T).Name`
var sectionName = configuration.SectionName;
var validation = configuration.Validation;
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
var sectionName =
NullIfWhitespace(configuration?.SectionName)
?? NullIfWhitespace(attribute?.SectionName)
?? type.Name;
var validation = (configuration?.Validation ?? 0) | (attribute?.Validation ?? 0);

if (attribute is OptifyOptionsAttribute attr)
{
sectionName ??= string.IsNullOrWhiteSpace(attr.SectionName) ? type.Name : attr.SectionName;
validation |= attr.Validation;
}

services
.AddOptions<T>()
.Bind(ctx.Configuration.GetSection(sectionName!))
.MaybeAddValidation(validation);
services.AddOptions<T>().Bind(ctx.Configuration.GetSection(sectionName)).MaybeAddValidation(validation);
}
);

Expand All @@ -56,4 +52,6 @@ public static IHostBuilder UseOptify<T>(this IHostBuilder hostBuilder, OptifyCon
/// <returns>The extended <see cref="IHostBuilder"/> instance.</returns>
public static IHostBuilder UseOptify<T>(this IHostBuilder hostBuilder)
where T : class => hostBuilder.UseOptify<T>(new OptifyConfiguration());

private static string? NullIfWhitespace(string? value) => string.IsNullOrWhiteSpace(value) ? null : value;
}