| | | 1 | | using FluentValidation; |
| | | 2 | | using FluentValidation.Results; |
| | | 3 | | |
| | | 4 | | using Microsoft.Extensions.Options; |
| | | 5 | | |
| | | 6 | | using NexusLabs.Needlr.Generators; |
| | | 7 | | |
| | | 8 | | namespace NexusLabs.Needlr.FluentValidation; |
| | | 9 | | |
| | | 10 | | /// <summary> |
| | | 11 | | /// Adapts a FluentValidation <see cref="IValidator{T}"/> to work as an |
| | | 12 | | /// <see cref="IValidateOptions{TOptions}"/> for Microsoft.Extensions.Options. |
| | | 13 | | /// </summary> |
| | | 14 | | /// <typeparam name="TOptions">The options type being validated.</typeparam> |
| | | 15 | | /// <remarks> |
| | | 16 | | /// <para> |
| | | 17 | | /// This adapter allows FluentValidation validators to be used seamlessly with |
| | | 18 | | /// Needlr's <c>[Options(ValidateOnStart = true)]</c> attribute. The adapter |
| | | 19 | | /// translates FluentValidation's <see cref="ValidationResult"/> into the |
| | | 20 | | /// <see cref="ValidateOptionsResult"/> expected by the options framework. |
| | | 21 | | /// </para> |
| | | 22 | | /// <para> |
| | | 23 | | /// Usage with Needlr source generation: |
| | | 24 | | /// <code> |
| | | 25 | | /// [Options("Database", ValidateOnStart = true, Validator = typeof(DatabaseOptionsValidator))] |
| | | 26 | | /// public class DatabaseOptions { ... } |
| | | 27 | | /// |
| | | 28 | | /// public class DatabaseOptionsValidator : AbstractValidator<DatabaseOptions> |
| | | 29 | | /// { |
| | | 30 | | /// public DatabaseOptionsValidator() |
| | | 31 | | /// { |
| | | 32 | | /// RuleFor(x => x.ConnectionString).NotEmpty(); |
| | | 33 | | /// } |
| | | 34 | | /// } |
| | | 35 | | /// </code> |
| | | 36 | | /// </para> |
| | | 37 | | /// <para> |
| | | 38 | | /// Register the adapter in your DI container: |
| | | 39 | | /// <code> |
| | | 40 | | /// services.AddFluentValidationOptionsAdapter<DatabaseOptions, DatabaseOptionsValidator>(); |
| | | 41 | | /// </code> |
| | | 42 | | /// </para> |
| | | 43 | | /// </remarks> |
| | | 44 | | public sealed class FluentValidationOptionsAdapter<TOptions> : IValidateOptions<TOptions> |
| | | 45 | | where TOptions : class |
| | | 46 | | { |
| | | 47 | | private readonly IValidator<TOptions> _validator; |
| | | 48 | | private readonly string? _name; |
| | | 49 | | |
| | | 50 | | /// <summary> |
| | | 51 | | /// Creates a new adapter for the specified FluentValidation validator. |
| | | 52 | | /// </summary> |
| | | 53 | | /// <param name="validator">The FluentValidation validator to adapt.</param> |
| | | 54 | | /// <param name="name">Optional name for named options validation.</param> |
| | 9 | 55 | | public FluentValidationOptionsAdapter(IValidator<TOptions> validator, string? name = null) |
| | | 56 | | { |
| | 9 | 57 | | _validator = validator ?? throw new ArgumentNullException(nameof(validator)); |
| | 9 | 58 | | _name = name; |
| | 9 | 59 | | } |
| | | 60 | | |
| | | 61 | | /// <summary> |
| | | 62 | | /// Validates the specified options instance. |
| | | 63 | | /// </summary> |
| | | 64 | | /// <param name="name">The name of the options instance being validated.</param> |
| | | 65 | | /// <param name="options">The options instance to validate.</param> |
| | | 66 | | /// <returns>A <see cref="ValidateOptionsResult"/> indicating success or failure.</returns> |
| | | 67 | | public ValidateOptionsResult Validate(string? name, TOptions options) |
| | | 68 | | { |
| | | 69 | | // If this adapter is for a specific named options, skip validation for other names |
| | 8 | 70 | | if (_name != null && _name != name) |
| | | 71 | | { |
| | 1 | 72 | | return ValidateOptionsResult.Skip; |
| | | 73 | | } |
| | | 74 | | |
| | 7 | 75 | | if (options == null) |
| | | 76 | | { |
| | 1 | 77 | | return ValidateOptionsResult.Fail($"Options of type {typeof(TOptions).Name} cannot be null."); |
| | | 78 | | } |
| | | 79 | | |
| | 6 | 80 | | var result = _validator.Validate(options); |
| | | 81 | | |
| | 6 | 82 | | if (result.IsValid) |
| | | 83 | | { |
| | 2 | 84 | | return ValidateOptionsResult.Success; |
| | | 85 | | } |
| | | 86 | | |
| | 4 | 87 | | var errors = result.Errors |
| | 4 | 88 | | .Where(f => f.Severity == Severity.Error) |
| | 4 | 89 | | .Select(FormatError) |
| | 4 | 90 | | .ToList(); |
| | | 91 | | |
| | 4 | 92 | | return errors.Count > 0 |
| | 4 | 93 | | ? ValidateOptionsResult.Fail(errors) |
| | 4 | 94 | | : ValidateOptionsResult.Success; |
| | | 95 | | } |
| | | 96 | | |
| | | 97 | | private static string FormatError(ValidationFailure failure) |
| | | 98 | | { |
| | 3 | 99 | | if (string.IsNullOrEmpty(failure.PropertyName)) |
| | | 100 | | { |
| | 0 | 101 | | return failure.ErrorMessage; |
| | | 102 | | } |
| | | 103 | | |
| | 3 | 104 | | return $"{failure.PropertyName}: {failure.ErrorMessage}"; |
| | | 105 | | } |
| | | 106 | | } |