How to validate a sequence of items from .NET Core configuration using IOptions etc

Viewed 140

I have trouble finding a way to validate items in a sequence for .NET Core configuration (tried with .NET 5 and 3.1).

Say that my settings are like so:

{
"MyOpt": {
    "IntOpt": 5,
    "StrOpt": "...",
    "Inner": {
        "InnerStr": "...",
        "InnerInt": 7
    },
    "Inners": [
        {
            "InnerStr": "...",
            "InnerInt": 1
        },
        {
            "InnerStr": "...",
            "InnerInt": 2
        }
    ]
}
}


public class Inner
{
    public string InnerStr { get; set; }

    [Range(0, 1000, ErrorMessage = "Value {0} must be between {1} and {2}")]
    public int InnerInt { get; set; }
}

public class MyOpt
{
    [Range(0, 1000, ErrorMessage = "Value {0} must be between {1} and {2}")]
    public int IntOpt { get; set; }
    public string StrOpt { get; set; }
    public IEnumerable<Inner> Inners { get; set; }
}

And that I configure this in my Startup.cs like so:

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

When I try to access this from, say in a controller in a WebAPI:

    public HomeController(ILogger<HomeController> logger, IOptions<OptOpt> options)
    {
        var whatever = options.Value.IntOpt;
        _logger = logger;
    }

Validations kick in correctly for MyOpt.IntOpt or MyOpt.Inner.InnerInt (both data annotations and conversion errors cause an exception to be thrown, e.g. for

{
"MyOpt": {
    "IntOpt": "abc",
    ...
}

I get a nice

ArgumentException: abc is not a valid value for Int32. (Parameter 'value').

However, if one of MyOpt.Inners items is misconfigured, it is silently dropped, e.g. for

{
"MyOpt": {
    // ...
    "Inners": [
        {
            "InnerStr": "...",
            "InnerInt": 1
        },
        {
            "InnerStr": "...",
            "InnerInt": "abc"
        }
    ]
}
}

I only get one item in MyOpt.Inners (the first one) and I can't seem to find a way for this to somehow "participate" in the validation - or that the binding failure of "abc" is reported through an exception.

Any idea?

1 Answers

Validation attributes won't by default go inside collections and validate their content, that's why attributes on the elements in your collection are not called.

You could implement IValidatableObject and trigger the validation of items in your collection:

 public class MyOpt : IValidatableObject
    {
        [Range(0, 1000, ErrorMessage = "Value {0} must be between {1} and {2}")]
        public int IntOpt { get; set; }
        public string StrOpt { get; set; }
        public IEnumerable<Inner> Inners { get; set; }
    
        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext){
          var results = new List<ValidationResult>();
          foreach (var item in this.Inners){
          var validationResults = new List<ValidationResult>();
               if (!Validator.TryValidateObject(item, new ValidationContext(item, validationContextItems), validationResults, true)))
    {
results.AddRange(validationResults);
}

          }
        }

return result;
    }

Note: code is untested, just a quick suggestion, look for docu onIValidatableObject and Validator.

Related