I\'ve read the Microsoft documentation of fundamentals for Options and Configuration, but still can\'t find the right way to extract configuration into an object while validatin
You can try validating the class yourself in start up before adding it to service collection.
Startup
var settings = Configuration.GetSection("Email").Get<EmailConfig>();
//validate
var validationResults = new List<ValidationResult>();
var validationContext = new ValidationContext(settings, serviceProvider: null, items: null);
if (!Validator.TryValidateObject(settings, validationContext, validationResults,
validateAllProperties: true)) {
//...Fail early
//will have the validation results in the list
}
services.AddSingleton(settings);
That way you are not coupled to IOptions
and you also allow your code to fail early and you can explicitly inject the dependency where needed.
You could package the validation up into your own extension method like
public static T GetValid<T>(this IConfiguration configuration) {
var obj = configuration.Get<T>();
//validate
Validator.ValidateObject(obj, new ValidationContext(obj), true);
return obj;
}
for calls like
EmailConfig emailSection = Configuration.GetSection("Email").GetValid<EmailConfig>();
services.AddSingleton(emailSection);
Internally, ValidateDataAnnotations is basically doing the same thing.
/// <summary>
/// Validates a specific named options instance (or all when name is null).
/// </summary>
/// <param name="name">The name of the options instance being validated.</param>
/// <param name="options">The options instance.</param>
/// <returns>The <see cref="ValidateOptionsResult"/> result.</returns>
public ValidateOptionsResult Validate(string name, TOptions options)
{
// Null name is used to configure all named options.
if (Name == null || name == Name)
{
var validationResults = new List<ValidationResult>();
if (Validator.TryValidateObject(options,
new ValidationContext(options, serviceProvider: null, items: null),
validationResults,
validateAllProperties: true))
{
return ValidateOptionsResult.Success;
}
return ValidateOptionsResult.Fail(String.Join(Environment.NewLine,
validationResults.Select(r => "DataAnnotation validation failed for members " +
String.Join(", ", r.MemberNames) +
" with the error '" + r.ErrorMessage + "'.")));
}
// Ignored if not validating this instance.
return ValidateOptionsResult.Skip;
}
Source Code
There is still no answer as to how ValidateDataAnnotations work, but based on Nkosi's answer, I wrote this class extension to easily run the validation on-demand. Because it's an extension on Object, I put it into a sub-namespace to only enable it when needed.
namespace Websites.Business.Validation {
/// <summary>
/// Provides methods to validate objects based on DataAnnotations.
/// </summary>
public static class ValidationExtensions {
/// <summary>
/// Validates an object based on its DataAnnotations and throws an exception if the object is not valid.
/// </summary>
/// <param name="obj">The object to validate.</param>
public static T ValidateAndThrow<T>(this T obj) {
Validator.ValidateObject(obj, new ValidationContext(obj), true);
return obj;
}
/// <summary>
/// Validates an object based on its DataAnnotations and returns a list of validation errors.
/// </summary>
/// <param name="obj">The object to validate.</param>
/// <returns>A list of validation errors.</returns>
public static ICollection<ValidationResult> Validate<T>(this T obj) {
var Results = new List<ValidationResult>();
var Context = new ValidationContext(obj);
if (!Validator.TryValidateObject(obj, Context, Results, true))
return Results;
return null;
}
}
}
Then in Startup it's quite straightforward
EmailConfig EmailSection = Configuration.GetSection("Email").Get<EmailConfig>().ValidateAndThrow();
services.AddSingleton<EmailConfig>(EmailSection);
Works like a charm; actually works like I'd expect ValidateDataAnnotations to work.