Skip to content

Commit 59b5ce2

Browse files
committed
Added StronglyTypedConfiguration validation in FluentValidation
1 parent 69cda83 commit 59b5ce2

File tree

5 files changed

+181
-0
lines changed

5 files changed

+181
-0
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="FluentValidation" Version="11.2.2" />
11+
</ItemGroup>
12+
13+
</Project>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.0.31903.59
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConfigFluentValidation", "ConfigFluentValidation.csproj", "{045C85A1-4518-4A65-99ED-D56E29B023AF}"
7+
EndProject
8+
Global
9+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+
Debug|Any CPU = Debug|Any CPU
11+
Release|Any CPU = Release|Any CPU
12+
EndGlobalSection
13+
GlobalSection(SolutionProperties) = preSolution
14+
HideSolutionNode = FALSE
15+
EndGlobalSection
16+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
17+
{045C85A1-4518-4A65-99ED-D56E29B023AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
18+
{045C85A1-4518-4A65-99ED-D56E29B023AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
19+
{045C85A1-4518-4A65-99ED-D56E29B023AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
20+
{045C85A1-4518-4A65-99ED-D56E29B023AF}.Release|Any CPU.Build.0 = Release|Any CPU
21+
EndGlobalSection
22+
EndGlobal

ConfigFluentValidation/Program.cs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
using FluentValidation;
2+
using Microsoft.Extensions.Options;
3+
4+
var builder = WebApplication.CreateBuilder(args);
5+
6+
// Register the validator
7+
builder.Services.AddScoped<IValidator<SlackApiSettings>, SlackApiSettingsValidator>();
8+
9+
builder.Services.AddOptions<SlackApiSettings>()
10+
.BindConfiguration("SlackApi")
11+
.ValidateFluentValidation() // <- Enable validation
12+
.ValidateOnStart(); // <- Validate on app start
13+
14+
// Explicitly register the settings object by delegating to the IOptions object
15+
builder.Services.AddSingleton(resolver =>
16+
resolver.GetRequiredService<IOptions<SlackApiSettings>>().Value);
17+
18+
var app = builder.Build();
19+
20+
// app.MapGet("/", (IOptions<SlackApiSettings> options) => options.Value);
21+
app.MapGet("/", (SlackApiSettings options) => options);
22+
23+
app.Run();
24+
25+
public class SlackApiSettings
26+
{
27+
public string? WebhookUrl { get; set; }
28+
public string? DisplayName { get; set; }
29+
public bool ShouldNotify { get; set; }
30+
}
31+
32+
public class SlackApiSettingsValidator : AbstractValidator<SlackApiSettings>
33+
{
34+
public SlackApiSettingsValidator()
35+
{
36+
RuleFor(x => x.DisplayName).NotEmpty();
37+
RuleFor(x => x.WebhookUrl)
38+
.NotEmpty()
39+
// .MustAsync((_, _) => Task.FromResult(true)) 👈 can't use async validators
40+
.Must(uri => Uri.TryCreate(uri, UriKind.Absolute, out _))
41+
.When(x => !string.IsNullOrEmpty(x.WebhookUrl));
42+
}
43+
}
44+
45+
public static class OptionsBuilderFluentValidationExtensions
46+
{
47+
/// <summary>
48+
/// Register this options instance for validation using FluentValidation
49+
/// Note that you _can't_ use async validators, or you will get an exception at runtime.
50+
/// </summary>
51+
/// <typeparam name="TOptions">The options type to be configured.</typeparam>
52+
/// <param name="optionsBuilder">The options builder to add the services to.</param>
53+
/// <returns>The <see cref="OptionsBuilder{TOptions}"/> so that additional calls can be chained.</returns>
54+
public static OptionsBuilder<TOptions> ValidateFluentValidation<TOptions>(this OptionsBuilder<TOptions> optionsBuilder) where TOptions : class
55+
{
56+
optionsBuilder.Services.AddSingleton<IValidateOptions<TOptions>>(
57+
provider => new FluentValidationOptions<TOptions>(optionsBuilder.Name, provider));
58+
return optionsBuilder;
59+
}
60+
}
61+
62+
public class FluentValidationOptions<TOptions> : IValidateOptions<TOptions> where TOptions : class
63+
{
64+
private readonly IServiceProvider _serviceProvider;
65+
private readonly string? _name;
66+
67+
public FluentValidationOptions(string? name, IServiceProvider serviceProvider)
68+
{
69+
_serviceProvider = serviceProvider;
70+
_name = name;
71+
}
72+
73+
public ValidateOptionsResult Validate(string? name, TOptions options)
74+
{
75+
// Null name is used to configure all named options.
76+
if (_name != null && _name != name)
77+
{
78+
// Ignored if not validating this instance.
79+
return ValidateOptionsResult.Skip;
80+
}
81+
82+
// Ensure options are provided to validate against
83+
ArgumentNullException.ThrowIfNull(options);
84+
85+
// Validators are registered as scoped, so need to create a scope,
86+
// as we will be called from the root scope
87+
using var scope = _serviceProvider.CreateScope();
88+
var validator = scope.ServiceProvider.GetRequiredService<IValidator<TOptions>>();
89+
var results = validator.Validate(options);
90+
if (results.IsValid)
91+
{
92+
return ValidateOptionsResult.Success;
93+
}
94+
95+
string typeName = options.GetType().Name;
96+
var errors = new List<string>();
97+
foreach (var result in results.Errors)
98+
{
99+
errors.Add($"Fluent validation failed for '{typeName}.{result.PropertyName}' with the error: '{result.ErrorMessage}'.");
100+
}
101+
102+
return ValidateOptionsResult.Fail(errors);
103+
}
104+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"iisSettings": {
3+
"windowsAuthentication": false,
4+
"anonymousAuthentication": true,
5+
"iisExpress": {
6+
"applicationUrl": "http://localhost:1215",
7+
"sslPort": 44362
8+
}
9+
},
10+
"profiles": {
11+
"ConfigFluentValidation": {
12+
"commandName": "Project",
13+
"dotnetRunMessages": true,
14+
"launchBrowser": true,
15+
"applicationUrl": "https://localhost:7092;http://localhost:5120",
16+
"environmentVariables": {
17+
"ASPNETCORE_ENVIRONMENT": "Development"
18+
}
19+
},
20+
"IIS Express": {
21+
"commandName": "IISExpress",
22+
"launchBrowser": true,
23+
"environmentVariables": {
24+
"ASPNETCORE_ENVIRONMENT": "Development"
25+
}
26+
}
27+
}
28+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
},
8+
"AllowedHosts": "*",
9+
"SlackApi": {
10+
"WebhookUrl": "http://example.com/test/url",
11+
"DisplayName": null,
12+
"ShouldNotify": true
13+
}
14+
}

0 commit comments

Comments
 (0)