10
votes

Comment valider les paramètres de configuration à l'aide de IValidateOptions dans ASP.NET Core 2.2?

La documentation ASP.NET Core de Microsoft mentionne brièvement que vous pouvez implémenter IValidateOptions<TOptions> pour valider les paramètres de configuration à partir de appsettings.json, mais un exemple complet n'est pas fourni. Comment IValidateOptions doit-il être utilisé? Plus précisement:

  • Où connectez-vous votre classe de validateurs?
  • Comment pouvez-vous enregistrer un message utile expliquant quel est le problème si la validation échoue?

J'ai déjà trouvé une solution. Je poste mon code car je ne trouve aucune mention d' IValidateOptions sur Stack Overflow pour le moment.


0 commentaires

4 Réponses :


13
votes

J'ai finalement trouvé un exemple de la façon dont cela est fait dans le commit où la fonction de validation des options a été ajoutée . Comme pour tant de choses dans asp.net core, la réponse est d'ajouter votre validateur au conteneur DI et il sera automatiquement utilisé.

Avec cette approche, PolygonConfiguration entre dans le conteneur DI après validation et peut être injecté dans les contrôleurs qui en ont besoin. Je préfère cela à l'injection d' IOptions<PolygonConfiguration> dans mes contrôleurs.

Il semble que le code de validation s'exécute la première fois qu'une instance de PolygonConfiguration est demandée au conteneur (c'est-à-dire lorsque le contrôleur est instancié). Cela peut être bien de valider plus tôt au démarrage, mais j'en suis satisfait pour le moment.

Voici ce que j'ai fini par faire:

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

    public class PolygonConfiguration
    {
        public List<Polygon> SupportedPolygons { get; set; }
    }

appSettings.json avec des valeurs valides et non valides

    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;
        }
    }

La classe de validateur elle-même

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

Et les modèles de configuration

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)
    {
       ...
    }
}


0 commentaires

1
votes

Une approche pourrait être d'ajouter un trait IValidatable<T> à vos classes de configuration. Ensuite, vous pouvez utiliser des anootations de données pour définir ce qui doit être validé et ce qui ne l'est pas. Je vais vous donner un exemple sur la façon d'ajouter un projet secondaire à votre solution qui prendrait soin dans le cas général.

Ici, nous avons la classe que nous voulons valider: Configs / JwtConfig.cs

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

Il s'agit de l '"interface de trait" qui ajoute la capacité de validation (en c # 8, cela pourrait être changé en une interface avec des méthodes par défaut) SettingValidation / Traits / IValidatable.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<SettingValidationStartupFilter>();
            using (var scope = services.BuildServiceProvider().CreateScope())
            {
                // Do not remove this call.
                // ReSharper disable once UnusedVariable
                var validatorFilter = scope.ServiceProvider.GetRequiredService<SettingValidationStartupFilter>();
            }
            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<T>(this IServiceCollection services, IConfiguration config)
            where T : class, IValidatable<T>, new()
        {
            services.Configure<T>(config);
            services.AddSingleton<IValidatable>(r => r.GetRequiredService<IOptions<T>>().Value);
            return services;
        }
    }
}

Une fois que vous avez cela, vous devez ajouter un filtre de démarrage: SettingValidation / Filters / SettingValidationStartupFilter.cs

using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using SettingValidation.Traits;

namespace SettingValidation.Filters
{
    public class SettingValidationStartupFilter
    {
        public SettingValidationStartupFilter(IEnumerable<IValidatable> validatables, ILogger<SettingValidationStartupFilter> logger)
        {
            foreach (var validatable in validatables)
            {
                validatable.Validate(logger);
            }
        }
    }
}

C'est la convention d'ajouter une méthode d'extension:

SettingValidation / Extensions / IServiceCollectionExtensions.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<T> : IValidatable
    {

    }

    public static class IValidatableTrait
    {
        public static void Validate(this IValidatable @this, ILogger logger)
        {
            var validation = new List<ValidationResult>();
            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();
            }
        }
    }
}

Enfin, activez l'utilisation du filtre de démarrage Startup.cs

using System.ComponentModel.DataAnnotations;
using SettingValidation.Traits;

namespace Configs
{
    public class JwtConfig : IValidatable<JwtConfig>
    {
        [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;
    }
}

Je me souviens d'avoir basé ce code à partir d'un article de blog sur Internet que je ne pouvais pas trouver pour le moment, c'est peut-être le même que vous avez trouvé, même si vous n'utilisez pas cette solution, essayez de refactoriser ce que vous avez fait dans un projet différent, afin qu'il puisse être réutilisé dans d'autres solutions ASP.NET Core dont vous disposez.

Bonne journée.


0 commentaires

3
votes

Probablement trop tard maintenant, mais pour le bénéfice de toute autre personne qui tombe sur ça ...

Près du bas de la section de documentation (lié à dans la question), cette ligne apparaît

Une validation rapide (échec rapide au démarrage) est à l'étude pour une version future.

En cherchant un peu plus d'informations à ce sujet, je suis tombé sur ce problème github , qui fournit un IStartupFilter et une méthode d'extension pour IOptions (que j'ai répétée ci-dessous juste au cas où le problème disparaîtrait) ...

Cette solution garantit que les options sont validées avant l'application «en cours d'exécution».

services
  .AddOptions<SomeOptions>()
  .Configure(options=>{ options.SomeProperty = "abcd" })
  .Validate(x=>
  {
      // do FluentValidation here
  })
  .ValidateEagerly();

J'ai alors, une méthode d'extension appelée depuis ConfigureServices qui ressemble à ceci

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);
        };
    }
}


0 commentaires

4
votes

Créez simplement une bibliothèque pour intégrer FluentValidation avec Microsoft.Extensions.Options.

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

Le nuget est ici: https://www.nuget.org/packages/IL.FluentValidation.Extensions.Options/

Échantillon:

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)
{
}


0 commentaires