This is Part 3 of a series on validating application settings.

In our last post, we looked at how to use FluentValidation to carry out our settings validation.

In this post, we will look at how to use data annotations to validate our settings.

To recap, our settings class looks like this:

public class ApplicationOptions
{
    public string APIKey { get; set; }
    public int RetryCount { get; set; }
    public int RequestsPerMinute { get; set; }
    public int RequestsPerDay { get; set; }
}

Our constraints are these:

  • The APIKey must be composed of uppercase characters with a maximum length of 10
  • The RetryCount must be between 1 and 5
  • The RequestsPerMinute cannot be more than 1000
  • The RequestsPerDay cannot be more than the RequetsPerMinute
  • All of these settings are mandatory

DataAnnotations use attributes against each of the properties we want to validate.

So, we update our class as follows:

public class ApplicationOptions
{
  [Required]
  [StringLength(10)]
  [RegularExpression("^[A-Z]{10}$")]
  public string APIKey { get; set; } = null!;
  [Required]
  [Range(1, 5)]
  public int RetryCount { get; set; }
  [Range(0, 1_000)]
  [Required] 
  public int RequestsPerMinute { get; set; }
  [Required]
  public int RequestsPerDay { get; set; }
}

Next, we update our Program.cs startup as follows:

builder.Services.AddOptions<ApplicationOptions>()
    .Bind(builder.Configuration.GetSection(nameof(ApplicationOptions)))
    .ValidateDataAnnotations()
    .ValidateOnStart();

The ValidateOnStart method call is important because if you omit it, the settings will not be validated until they are first requested. You want the application to fail immediately on start. Otherwise, users will have an unfriendly experience.

If we supply invalid data, for instance, an invalid RetryCount, we will get an error as follows:

Settings3Annotatoions

The validation that RequestsPerMinute cannot exceed the RequestsPerDay cannot be enforced through annotations - you must validate that separately as follows:

builder.Services.AddOptions<ApplicationOptions>()
    .Bind(builder.Configuration.GetSection(nameof(ApplicationOptions)))
    .ValidateDataAnnotations()
    .Validate(config =>
    {
        // Assert the requests per day are greater
        return config.RequestsPerDay > config.RequestsPerMinute;
    }, "Requests per day must be greater than or equal to requests per minute.")
    .ValidateOnStart();

You can also use the direct method AddOptionsWithValidateOnStart as follows:

builder.Services.AddOptionsWithValidateOnStart<ApplicationOptions>()
    .Bind(builder.Configuration.GetSection(nameof(ApplicationOptions)))
    .ValidateDataAnnotations()
    .Validate(config =>
    {
        // Assert the requests per day are greater
        return config.RequestsPerDay > config.RequestsPerMinute;
    }, "Requests per day must be greater than or equal to requests per minute.");

Check the documentation for a list of attributes you can use to validate various properties by type.

Our next post will examine how to write more complex validation without annotation.

The code is in my GitHub.

Happy hacking!