Validate configurations with FluentValidation in ASP.NET Core
Image by Sven Mieke Unsplash

Validate configurations with FluentValidation in ASP.NET Core

Validating IOptions models with FluentValidaton

Configuration is an important part of the application. In many cases it determines how the application will behave, for example when you have different configurations per environment. Therefore it is important that configuration is valid not only in it's structural way but also in a logic way, meaning it needs to satisfy certain rule related specifically for the application where configuration is used.

.NET Core gives freedom of using multiple different configuration sorces combined, like using JSON configuration file together with environment variables or secret vault. This way you have more than one factor for things to go wrong.

One thing that is important is to detect faulty configuration and raise exception on application start rather than having to deal with issues while application is in usage by the users.

Great way for validating pretty much anything in C# is through FluentValidation NuGet package. You can find many examples how to achieve validation for different application modules. In this article I will explain how to apply FluentValidation in combinations with Options pattern to validate strongly typed configuration values.

Unlike validating ASP.NET Core MVC models, validation configuration models which are injected with IOptions interface is a bit tricky. It also has different timing for triggering validation as you would want to validate your configuration on a startup rather then on a specific action where model which is validated is used.

As an example I'll use a simple ASP.NET Core application that has configuration model which needs to have specific rules in order for application to work as expected. We will load list of service endpoint models which we for example want to check for the response.

The following is the configuration model that will be parsed from JSON configuration file using options pattern.

    public class ServiceEndpoint
    {
        public String Name { get; set; }
        public int Timeout { get; set; }
        public Uri Url { get; set; }
    }

    public class EndpointsConfiguration
    {
        public IEnumerable<ServiceEndpoint> ServiceEndpoints { get; set; }
    }
    

We will load EndpointsConfiguration class to IOptions<T> instance and the following rules need to be applied to configuration class instance:

  • service endpoints need to be named and names need to be unique
  • request timeout values need to be between 1 and 90
  • service urls need to be unique
  • all service urls need to be HTTPS

To have these rules in place I created a validator class which inherits abstract class AbstractValidator<EndpointsConfiguration> and implements all logic in its constructor method. Please not that unlike controller action model validation, instead of returning the validation result I used ValidationException to throw and exception instantly once first validation rule fails. This is because unlike controller action model validation I want application to crash instantly.

    public class EndpointsConfigurationValidator : AbstractValidator<EndpointsConfiguration>
    {
        public EndpointsConfigurationValidator()
        {
            RuleFor(m => m).NotNull()
                .OnFailure(_ => throw new ValidationException("No services configuration present"));
            RuleFor(m => m.ServiceEndpoints).NotNull().NotEmpty()
                .OnFailure(_ => throw new ValidationException("No service endpoints defined"));

            When(m => m.ServiceEndpoints != null, () =>
            {
                RuleFor(m => m.ServiceEndpoints)
                    .Must(s => s.GroupBy(s => s.Name).Count() == s.Count())
                    .OnFailure(_ => throw new ValidationException("Service endpoints configuration names must be unique"));

                RuleFor(m => m.ServiceEndpoints)
                    .Must(s => !s.Any(e => e.Timeout > 90 || e.Timeout < 1))
                    .OnFailure(_ => throw new ValidationException("Endpoint timeout must be tween 1 and 90"));

                RuleFor(m => m.ServiceEndpoints)
                    .Must(s => s.GroupBy(s => s.Url.ToString().ToLower()).Count() == s.Count())
                    .OnFailure(_ => throw new ValidationException("Service endpoints cannot repeat"));

                RuleFor(m => m.ServiceEndpoints)
                    .Must(s => s.All(e => e.Url.Scheme.Equals("https", StringComparison.CurrentCultureIgnoreCase)))
                    .OnFailure(_ => throw new ValidationException("All endpoints must be HTTPS"));
            });
        }
    }
    

Now we have all elements necessary for loading and validating configuration. All we need to to is to wire them up in a DI container setup in Startup.cs class file.

First thing we need to do is to register IOptions for configuration model class. After that we need to add validator instance to the services collection in order to be able to retrieve validator for specific model type.

        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<EndpointsConfiguration>(Configuration.GetSection(nameof(EndpointsConfiguration)));
            services.AddSingleton<AbstractValidator<EndpointsConfiguration>, EndpointsConfigurationValidator>();

            services.AddSingleton<EndpointsConfiguration>(container =>
            {
                var config = container.GetService<IOptions<EndpointsConfiguration>>().Value;
                var validator = container.GetService<AbstractValidator<EndpointsConfiguration>>();
                validator.Validate(config);
                return config;
            });

            services.AddControllers();
        }
    

Finally we declare singleton instance for the EndpointsConfiguration which is resolved by getting IOption<EndpointsConfiguration> from previously configured options, resolving validator instance for the model type and executing validation against EndpointsConfiguration instance.

Now as I mentioned we want to trigger validation on application start rather then on any specific controller constructor where configuration is injected. For that purpose, we need to inject configuration model instance to the pipeline method.

Note

Instead of injecting IOptions<EndpointsConfiguration> as we would usually do when using options pattern we will instead inject EndpointsConfiguration. This will trigger our third line in DI configuration method which bottom line relies on the first two (IOptions and FluentValidation validator registration). Singleton in stances are resolved only when they are required from the constructor or service locator. Until the are required, our singleton registration will not be executed

In the end, to test our validation I created a configuration section and added it to appsettings.json file.

On purpose I made one endpoint be HTTP instead of HTTPS to test our validation logic. 

{
  "EndpointsConfiguration": {
    "ServiceEndpoints": [
      {
        "Name": "Service 1",
        "Timeout": 10,
        "Url": "http://service1.myservices.com"
      },
      {
        "Name": "Service 2",
        "Timeout": 20,
        "Url": "https://service2.myservices.com"
      }
    ]
  }
}

    

Once application is started, validation is triggered on the pipeline initialization and ValidationException is thrown from the last validation rule configured in EndpointsConfigurationValidator class.

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article