Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
460 views
in Technique[技术] by (71.8m points)

c# - 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)
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...