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

前端 未结 4 2178
[愿得一人]
[愿得一人] 2021-02-14 15:57

Microsoft\'s ASP.NET Core documentation briefly mentions that you can implement IValidateOptions to validate configuration settings from appsettings

4条回答
  •  长情又很酷
    2021-02-14 16:48

    One approach could be to add a trait IValidatable to your configuration classes. Then you could use Data anootations to define what should be validated and what not. I'll provide an example on how to add a side project to your solution that would take care in the general case.

    Here we have the class that we want to validate: Configs/JwtConfig.cs

    using System.ComponentModel.DataAnnotations;
    using SettingValidation.Traits;
    
    namespace Configs
    {
        public class JwtConfig : IValidatable
        {
            [Required, StringLength(256, MinimumLength = 32)]
            public string Key { get; set; }
            [Required]
            public string Issuer { get; set; } = string.Empty;
            [Required]
            public string Audience { get; set; } = "*";
            [Range(1, 30)]
            public int ExpireDays { get; set; } = 30;
        }
    }
    
    

    This is the "trait interface" that adds the validation capability (in c# 8 this could be changed to an interface with default methods) SettingValidation/Traits/IValidatable.cs

    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.Linq;
    using Microsoft.Extensions.Logging;
    
    namespace SettingValidation.Traits
    {
        public interface IValidatable
        {
        }
    
        public interface IValidatable : IValidatable
        {
    
        }
    
        public static class IValidatableTrait
        {
            public static void Validate(this IValidatable @this, ILogger logger)
            {
                var validation = new List();
                if (Validator.TryValidateObject(@this, new ValidationContext(@this), validation, validateAllProperties: true))
                {
                    logger.LogInformation($"{@this} Correctly validated.");
                }
                else
                {
                    logger.LogError($"{@this} Failed validation.{Environment.NewLine}{validation.Aggregate(new System.Text.StringBuilder(), (sb, vr) => sb.AppendLine(vr.ErrorMessage))}");
                    throw new ValidationException();
                }
            }
        }
    }
    
    

    Once you have this, you need to add a startup filter: SettingValidation/Filters/SettingValidationStartupFilter.cs

    using System.Collections.Generic;
    using Microsoft.Extensions.Logging;
    using SettingValidation.Traits;
    
    namespace SettingValidation.Filters
    {
        public class SettingValidationStartupFilter
        {
            public SettingValidationStartupFilter(IEnumerable validatables, ILogger logger)
            {
                foreach (var validatable in validatables)
                {
                    validatable.Validate(logger);
                }
            }
        }
    }
    
    

    It's convention to add an extension method:

    SettingValidation/Extensions/IServiceCollectionExtensions.cs

    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Options;
    using SettingValidation.Filters;
    using SettingValidation.Traits;
    
    namespace SettingValidation.Extensions
    {
        public static class IServiceCollectionExtensions
        {
    
            public static IServiceCollection UseConfigurationValidation(this IServiceCollection services)
            {
                services.AddSingleton();
                using (var scope = services.BuildServiceProvider().CreateScope())
                {
                    // Do not remove this call.
                    // ReSharper disable once UnusedVariable
                    var validatorFilter = scope.ServiceProvider.GetRequiredService();
                }
                return services;
            }
    
            //
            // Summary:
            //     Registers a configuration instance which TOptions will bind against.
            //
            // Parameters:
            //   services:
            //     The Microsoft.Extensions.DependencyInjection.IServiceCollection to add the services
            //     to.
            //
            //   config:
            //     The configuration being bound.
            //
            // Type parameters:
            //   TOptions:
            //     The type of options being configured.
            //
            // Returns:
            //     The Microsoft.Extensions.DependencyInjection.IServiceCollection so that additional
            //     calls can be chained.
            public static IServiceCollection ConfigureAndValidate(this IServiceCollection services, IConfiguration config)
                where T : class, IValidatable, new()
            {
                services.Configure(config);
                services.AddSingleton(r => r.GetRequiredService>().Value);
                return services;
            }
        }
    }
    
    

    Finally enable the usage of the startup filter Startup.cs

    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            ...
            services.ConfigureAndValidate(Configuration.GetSection("Jwt"));
            services.UseConfigurationValidation();
            ...
        }
    }
    

    I remember basing this code from some blog post in the internet I couldn't find right now, maybe it's the same you found, even if you dont use this solution, try refactoring what you did into a different project, so it can be reused in other ASP.NET Core solutions that you have.

    Have a nice day.

提交回复
热议问题