Flux Text-to-Speech Brings Voice Agents to Life (Sponsored)
Deepgram just dropped Flux TTS, text-to-speech built for live conversation. It responds in as low as 80ms, carries context across turns, and handles interruptions natively. Spin up a stream and test it on your stack.
The Agent That Wrote Your Code Shouldn't Be the One Reviewing It (Sponsored)
Ask a coding agent to review the change it just wrote, and it will tell you the change looks fine. Same model, same assumptions, same blind spot β the tests pass, the pipeline goes green, and the logic error ships anyway.
That's the gap Gitar, Sonar's AI code reviewer, was built to close. It automatically reviews every pull request with full context of your codebase and your team's conventions β not just the diff β and catches the functional and behavioral bugs that rule-based analysis can't describe.
Gitar runs next to SonarQube instead of replacing it. SonarQube checks every change the same algorithmic way across 40+ languages, against quality gates you define.
It works with GitHub, GitLab, Bitbucket, and Azure DevOps, never retains your source code, and is free for 14 days.
Your app starts fine.
The first few requests work.
Then, hours later, one feature reaches for a setting that was never configured - and throws, deep in a request, far from the real cause.
I've been through this multiple times.
Configuration is just JSON or strings until you validate it.
ASP .NET Core can check your configuration the moment the app starts, so a typo in appsettings.json stops the app right away instead of returning a 500 Internal Error later.
There are two ways to do this: Data Annotations and FluentValidation.
In this post, we will explore:
Why configuration validation matters
Validate with Data Annotations
Fail fast at startup with ValidateOnStart
Validate with FluentValidation
Cross-field rules and named options
Data Annotations vs FluentValidation: when to use each
This binds the section, but it does not check anything.
If Token is missing or RetryCount is 0, binding still succeeds - you get an empty string and a zero.
The problem only shows up later, when some code actually uses those values.
An empty Token sails through binding and only fails when you make your first GitHub call - as a 401, or a NullReferenceException three layers deep, with nothing pointing back to the real cause: a missing config value.
Validation closes that gap. It turns a silent, half-configured object into an immediate failure.
By default, the Options system validates the first time the configuration is resolved - which might be on the first request that needs it, or much later.
You want the opposite: a bad configuration should stop the app from starting at all.
ValidateOnStart does that. Chain it onto the builder:
Now validation runs during application startup.
If the configuration is invalid, the host throws an OptionsValidationException and the app refuses to start.
This is exactly what you want: a broken deployment fails immediately and visibly during application startup.
Note: ValidateOnStart is the single most valuable line here. Always add it. Catching a configuration mistake at startup makes a real difference between a failed deploy and a runtime incident.
Data Annotations are fine for simple, per-property checks. But they have limits.
They scatter validation rules across your settings class as attributes, they cannot express rules that span several properties, and complex conditions get awkward fast.
And if you already use FluentValidation in your project, you will want your configuration validated the same way.
Write a validator for the settings class - the same AbstractValidator<T> you use everywhere else:
csharp
1usingFluentValidation;23publicsealedclassGitHubSettingsValidator:AbstractValidator<GitHubSettings>4{5publicGitHubSettingsValidator()6{7RuleFor(x => x.Token).NotEmpty();89RuleFor(x => x.BaseUrl)10.NotEmpty()11.Must(url => Uri.TryCreate(url, UriKind.Absolute,out _))12.WithMessage("BaseUrl must be a valid absolute URL.");1314RuleFor(x => x.RetryCount).InclusiveBetween(1,10);15}16}
Here is the gap: FluentValidation has no built-in support for the Options pattern.
The Options system validates through the IValidateOptions<TOptions> interface, and FluentValidation does not implement it.
So we provide a small, reusable bridge that runs any FluentValidation validator as an Options validator:
csharp
1usingFluentValidation;2usingMicrosoft.Extensions.Options;34namespaceConfiguration.Validation;56publicclassFluentValidateOptions<TOptions>:IValidateOptions<TOptions>7whereTOptions:class8{9privatereadonlyIServiceProvider _serviceProvider;10privatereadonlystring? _name;1112publicFluentValidateOptions(IServiceProvider serviceProvider,string? name)13{14 _serviceProvider = serviceProvider;15 _name = name;16}1718publicValidateOptionsResultValidate(string? name,TOptions options)19{20if(_name isnotnull&& _name != name)21{22return ValidateOptionsResult.Skip;23}2425 ArgumentNullException.ThrowIfNull(options);2627usingvar scope = _serviceProvider.CreateScope();2829var validator = scope.ServiceProvider.GetRequiredService<IValidator<TOptions>>();3031var result = validator.Validate(options);32if(result.IsValid)33{34return ValidateOptionsResult.Success;35}3637var type = options.GetType().Name;38var errors =newList<string>();39foreach(var failure in result.Errors)40{41 errors.Add($"Validation failed for {type}.{failure.PropertyName} "+42$"with the error: {failure.ErrorMessage}");43}4445return ValidateOptionsResult.Fail(errors);46}47}
A few things make this work:
It implements IValidateOptions<TOptions>, the hook the Options system calls.
IValidateOptions is registered as a singleton, but FluentValidation validators are usually scoped - so we open a scope with _serviceProvider.CreateScope() and resolve IValidator<TOptions> from it. Resolving a scoped service straight into a singleton would throw.
The _name check returns ValidateOptionsResult.Skip for named options that do not match, which we will use shortly.
Now wrap it in an extension method, so registering it reads cleanly:
Finally, register the validator itself and enable validation.
This is the step that is easy to forget - without it, the IValidator<GitHubSettings> resolve inside the bridge fails:
AddValidatorsFromAssemblyContaining scans the assembly and registers every validator, including GitHubSettingsValidator.
ValidateFluentValidation plugs the bridge in, and ValidateOnStart runs it at startup - so the same fail-fast behavior you got from Data Annotations now applies to your FluentValidation rules.
Now, an invalid configuration fails at boot with the message your bridge is formatted:
1OptionsValidationException: Validation failed for GitHubSettings.Token
2with the error: 'Token' must not be empty.
Note: validation runs against the final bound object, so it works the same no matter where the values come from - appsettings.json, environment variables, or a secret store like Azure Key Vault.
A rule that depends on more than one property is hard to express with attributes but trivial in a validator.
Say RetryDelaySeconds only matters when retries are enabled - add this rule to the validator:
Because FluentValidateOptions captures the builder's name and compares it in Validate, returning ValidateOptionsResult.Skip when the names do not match, each named instance is validated independently.
One reusable bridge covers every setting type and every name.
Data Annotations vs FluentValidation: When to Use Each
Both approaches fail fast at startup. The difference lies in how the rules are written and how far they stretch.
Data Annotations
FluentValidation
Setup
None - built in
A validator plus a small bridge
Where rules live
Attributes on the settings class
A separate validator class
Simple rules
Great ([Required], [Range])
Great
Cross-field / conditional rules
Awkward or impossible
Easy (When, Must)
Reuse and testing
Limited
Validators are plain classes you can unit-test
Use Data Annotations when the rules are simple per-property checks and you want zero extra setup.
Use FluentValidation when rules span multiple properties, depend on conditions, or when your project already standardizes on it - so configuration is validated the same way as everything else.
One more advantage: a validator is a plain class, so you can unit-test your configuration rules directly, with no host or DI container:
csharp
1var validator =newGitHubSettingsValidator();23var result = validator.TestValidate(newGitHubSettings{ Token =""});45result.ShouldHaveValidationErrorFor(x => x.Token);
Note: you do not have to choose globally. Validate simple settings with attributes and complex ones with FluentValidation, in the same app.
Validate your configuration. Binding succeeds even when values are missing or out of range - validation is what turns a half-configured object into an immediate, clear error.
Fail fast with ValidateOnStart. It moves validation to application startup, so a bad deploy stops at startup instead of failing later in production. Always add it.
Use Data Annotations for simple rules.[Required], [Url], and [Range] plus ValidateDataAnnotations cover most per-property checks with no setup.
Use FluentValidation for complex rules. The reusable FluentValidateOptions<T> bridge runs any validator as an options validator, and handles cross-field rules, conditions, and named options.
Bad configuration is one of the most common ways a deployment breaks.
Validating it at startup takes a few lines of code and pays for itself the first time it prevents a broken config from reaching production.
Hope you find this newsletter useful. See you next time.
You can download source code for this newsletter for free
Covers everything: C#, ASP.NET Core, EF Core, system design β answer each question first, reveal the solution, and a test after every chapter proves it stuck. Finish, and you earn a verifiable certificate for your LinkedIn.
Not sure where you stand? Take the free .NET Interview Run:
Find out your real level β Junior to Senior+
A realistic mock .NET interview β across 13 areas of C#, .NET, ASP.NET Core and System Design
No credit card required. When you finish, you get a personalized report: your level, your strongest and weakest areas, and where to focus next β the perfect way to benchmark yourself before diving into the Playbook.
Comments