Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to validate configuration settings using IValidateOptions in ASP.NET Core 2.2?

Microsoft's ASP.NET Core documentation briefly mentions that you can implement IValidateOptions<TOptions> to validate configuration settings from appsettings.json, but a complete example is not provided. How is IValidateOptions intended to be used? More specifically:

  • Where do you wire up your validator class?
  • How can you log a useful message explaining what the problem is if validation fails?

I have actually found a solution already. I'm posting my code since I can't find any mention of IValidateOptions on Stack Overflow at this time.

like image 561
David Tarulli Avatar asked Jul 21 '19 18:07

David Tarulli


4 Answers

I eventually found an example of how this is done in the commit where the options validation feature was added. As with so many things in asp.net core, the answer is to add your validator to the DI container and it will automatically be used.

With this approach the PolygonConfiguration goes into the DI container after validation and can be injected into the controllers that need it. I prefer this to injecting IOptions<PolygonConfiguration> into my controllers.

It appears that the validation code runs the first time an instance of PolygonConfiguration is requested from the container (i.e. when the controller is instantiated). It might be nice to validate earlier during startup, but I'm satisfied with this for now.

Here's what I ended up doing:

public class Startup
{
    public Startup(IConfiguration configuration, ILoggerFactory loggerFactory)
    {
        Configuration = configuration;
        Logger = loggerFactory.CreateLogger<Startup>();
    }

    public IConfiguration Configuration { get; }
    private ILogger<Startup> Logger { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

        //Bind configuration settings
        services.Configure<PolygonConfiguration>(Configuration.GetSection(nameof(PolygonConfiguration)));

        //Add validator
        services.AddSingleton<IValidateOptions<PolygonConfiguration>, PolygonConfigurationValidator>();

        //Validate configuration and add to DI container
        services.AddSingleton<PolygonConfiguration>(container =>
        {
            try
            {
                return container.GetService<IOptions<PolygonConfiguration>>().Value;
            }
            catch (OptionsValidationException ex)
            {
                foreach (var validationFailure in ex.Failures)
                    Logger.LogError($"appSettings section '{nameof(PolygonConfiguration)}' failed validation. Reason: {validationFailure}");

                throw;
            }
        });
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
       ...
    }
}

appSettings.json with some valid and invalid values

{
  "PolygonConfiguration": {
    "SupportedPolygons": [
      {
        "Description": "Triangle",
        "NumberOfSides": 3
      },
      {
        "Description": "Invalid",
        "NumberOfSides": -1
      },
      {
        "Description": "",
        "NumberOfSides": 6
      }
    ]
  }
}

The validator class itself

    public class PolygonConfigurationValidator : IValidateOptions<PolygonConfiguration>
    {
        public ValidateOptionsResult Validate(string name, PolygonConfiguration options)
        {
            if (options is null)
                return ValidateOptionsResult.Fail("Configuration object is null.");

            if (options.SupportedPolygons is null || options.SupportedPolygons.Count == 0)
                return ValidateOptionsResult.Fail($"{nameof(PolygonConfiguration.SupportedPolygons)} collection must contain at least one element.");

            foreach (var polygon in options.SupportedPolygons)
            {
                if (string.IsNullOrWhiteSpace(polygon.Description))
                    return ValidateOptionsResult.Fail($"Property '{nameof(Polygon.Description)}' cannot be blank.");

                if (polygon.NumberOfSides < 3)
                    return ValidateOptionsResult.Fail($"Property '{nameof(Polygon.NumberOfSides)}' must be at least 3.");
            }

            return ValidateOptionsResult.Success;
        }
    }

And the configuration models

    public class Polygon
    {
        public string Description { get; set; }
        public int NumberOfSides { get; set; }
    }

    public class PolygonConfiguration
    {
        public List<Polygon> SupportedPolygons { get; set; }
    }
like image 164
David Tarulli Avatar answered Oct 19 '22 19:10

David Tarulli


Probably too late now, but for the benefit of anyone else that stumbles across this...

Near the bottom of the documentation section (linked to in the question), this line appears

Eager validation (fail fast at startup) is under consideration for a future release.

On searching a little more for information on this, I came across this github issue, which provides an IStartupFilter, and an extension method for IOptions (which I've repeated below just incase the issue disappears)...

This solution ensures that the options are validated ahead of the application "running".

public static class EagerValidationExtensions {
    public static OptionsBuilder<TOptions> ValidateEagerly<TOptions>(this OptionsBuilder<TOptions> optionsBuilder)
        where TOptions : class, new()
    {
        optionsBuilder.Services.AddTransient<IStartupFilter, StartupOptionsValidation<TOptions>>();
        return optionsBuilder;
    }
}

public class StartupOptionsValidation<T>: IStartupFilter
{
    public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
    {
        return builder =>
        {
            var options = builder.ApplicationServices.GetRequiredService(typeof(IOptions<>).MakeGenericType(typeof(T)));
            if (options != null)
            {
                var optionsValue = ((IOptions<object>)options).Value;
            }

            next(builder);
        };
    }
}

I then have, an extension method called from within ConfigureServices that looks like this

services
  .AddOptions<SomeOptions>()
  .Configure(options=>{ options.SomeProperty = "abcd" })
  .Validate(x=>
  {
      // do FluentValidation here
  })
  .ValidateEagerly();
like image 38
0909EM Avatar answered Oct 19 '22 19:10

0909EM


Eager validation (fail fast at startup) is under consideration for a future release.

Since .NET 6 this is possible with ValidateOnStart()

Usage:

services.AddOptions<ComplexOptions>()
  .Configure(o => o.Boolean = false)
  .Validate(o => o.Boolean, "Boolean must be true.")
  .ValidateOnStart();

Background info: Pull Request: Add Eager Options Validation: ValidateOnStart API

like image 12
Julian Avatar answered Oct 19 '22 20:10

Julian


Just build a lib for integrate FluentValidation with Microsoft.Extensions.Options.

https://github.com/iron9light/FluentValidation.Extensions

The nuget is here: https://www.nuget.org/packages/IL.FluentValidation.Extensions.Options/

Sample:

public class MyOptionsValidator : AbstractValidator<MyOptions> {
    // ...
}

using IL.FluentValidation.Extensions.Options;

// Registration
services.AddOptions<MyOptions>("optionalOptionsName")
    .Configure(o => { })
    .Validate<MyOptions, MyOptionsValidator>(); // ❗ Register validator type

// Consumption
var monitor = services.BuildServiceProvider()
    .GetService<IOptionsMonitor<MyOptions>>();

try
{
    var options = monitor.Get("optionalOptionsName");
}
catch (OptionsValidationException ex)
{
}
like image 6
iron9light Avatar answered Oct 19 '22 20:10

iron9light