Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to Use Configuration with ValidateDataAnnotations

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 validating data annotations.

One approach I tried in Startup.ConfigureServices

services.AddOptions<EmailConfig>().Bind(Configuration.GetSection("Email")).ValidateDataAnnotations();

This "should" allow accessing the configuration by adding this in the class constructor: (IOptions<EmailConfig> emailConfig)

However it's not working.

Another approach is to add (IConfiguration configuration) to the constructor, but this doesn't allow me to call ValidateDataAnnotations.

configuration.GetSection("Email").Get<EmailConfig>();

First question: does the responsibility to bind and validate the configuration belong to the Startup class or to the class using it? If it's used by several classes I'd say it belongs to Startup; and the class could be used in another project with different configuration layout.

Second question: what is the correct syntax to bind and validate the configuration so it can be accessed from the class?

Third question: if I'm validating through data annotations in Startup, then the class using the configuration simply assumes the configuration is valid and I don't put any re-validation whatsoever?

UPDATE: After gaining more experience and reviewing the structure of all my code, I changed my approach to follow standard patterns.

The following code DOES work... but only validates it when used. This can be registered in a class library and won't throw any errors until the particular service is used.

services.AddOptions<EmailConfig>()
    .Bind(configuration.GetSection("Email"))
    .ValidateDataAnnotations();

Then, in Configure, I add this to force validation of needed configuration values at startup (CheckNotNull is a custom extension method, what matters is simply that you call IOptions.Value

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app?.ApplicationServices.GetService<IOptions<EmailConfig>>().Value.CheckNotNull("Config: Email");
    app?.ApplicationServices.GetService<IOptions<OntraportConfig>>().Value.CheckNotNull("Config: Ontraport");
    ...

Then in the class using it

public class EmailService(IOptions<EmailConfig> config)
like image 914
Etienne Charland Avatar asked Mar 06 '19 14:03

Etienne Charland


People also ask

How do I use IOptions in net core 6?

IOptions is singleton and hence can be used to read configuration data within any service lifetime. Being singleton, it cannot read changes to the configuration data after the app has started. Run the app and hit the controller action. You should be able to see the values being fetched from the configuration file.

What is Ioption C#?

IOptionsMonitor is a Singleton service that retrieves current option values at any time, which is especially useful in singleton dependencies. IOptionsSnapshot is a Scoped service and provides a snapshot of the options at the time the IOptionsSnapshot<T> object is constructed.

What is services AddOptions?

AddOptions(IServiceCollection) Adds services required for using options. AddOptions<TOptions>(IServiceCollection) Gets an options builder that forwards Configure calls for the same named TOptions to the underlying service collection.


1 Answers

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

like image 107
Nkosi Avatar answered Oct 02 '22 22:10

Nkosi