FluentValidation is not directly compatible with Swagger API to validate models. But they do provide an interface through which we can compose Swagger validation manually. That means we look under FluentValidation validators and compose Swagger validator properties to make it compatible. More of all mapping by reading information from FluentValidation and setting it to Swagger Model Schema.
These can be done on any custom validation from FluentValidation too just that proper schema property has to be available from Swagger.
This is simple, it just looks if passed single value exists within Enum or provided list based on ruleset.
An extension method to utilise FixedList validator while composing fluent validation.
Now with extension method created, it is much simpler to use while composing FluentValidation RuleSet.
Ex:
Some parts of below codes were taken from some other source but not remember the link.
Building Swagger schema based on FluentValdation rules
The schemaModel has some predefined structure, on which properties have to be set based on validation logic from FluentValidation. The same approach has been made on enum/fixed list support with earlier created custom validation from FluentValidation.
That is all, now SwaggerFluentValidation has to be registered on Swagger something like
These can be done on any custom validation from FluentValidation too just that proper schema property has to be available from Swagger.
Custom validation from Enum/List values on FluentValidation
using FluentValidation.Validators;
using System.Collections.Generic;
using System.Linq;
using static System.String;
/// <summary>
/// Validator as per list of items.
/// </summary>
/// <seealso cref="PropertyValidator" />
public class FixedListValidator
: PropertyValidator
{
/// <summary>
/// Gets the valid items
/// </summary>
public IEnumerable<string> ValidItems { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="FixedListValidator"/> class.
/// </summary>
/// <param name="validItems">The valid items.</param>
public FixedListValidator(IEnumerable<string> validItems)
: base("{PropertyName} can contain only these values {ElementValues}.")
{
ValidItems = validItems;
}
/// <summary>
/// Returns true if value contains any value of <see cref="ValidItems"/>.
/// </summary>
/// <param name="context">The context.</param>
/// <returns>
/// <c>true</c> if the specified context is valid; otherwise, <c>false</c>.
/// </returns>
protected override bool IsValid(PropertyValidatorContext context)
{
var value = context?.PropertyValue as string;
if (!IsNullOrEmpty(value) && !ValidItems.Contains(value))
{
context.MessageFormatter.AppendArgument("ElementValues", Join(", ", ValidItems));
return false;
}
return true;
}
}
This is simple, it just looks if passed single value exists within Enum or provided list based on ruleset.
An extension method to utilise FixedList validator while composing fluent validation.
using FluentValidation;
using System.Collections.Generic;
/// <summary>
/// Extension rules for FluentValidations.
/// </summary>
internal static class ExtensionCustomRule
{
/// <summary>
/// Must contain valid items based on passed values from <paramref name="validItems"/> parameter.
/// </summary>
/// <typeparam name="TModel">Model for validation.</typeparam>
/// <param name="ruleBuilder">The rule builder.</param>
/// <param name="validItems">The valid items.</param>
/// <returns>Rule builder chain.</returns>
public static IRuleBuilderOptions<TModel, string> MustContainValidItems<TModel>(
this IRuleBuilder<TModel, string> ruleBuilder, IEnumerable<string> validItems)
=> ruleBuilder.SetValidator(new FixedListValidator(validItems));
}
Now with extension method created, it is much simpler to use while composing FluentValidation RuleSet.
Ex:
RuleFor(emp => emp.CustomerType).NotEmpty()
.MustContainValidItems(<Enum values to list or put any list of valuse to check against>);
Some parts of below codes were taken from some other source but not remember the link.
Building Swagger schema based on FluentValdation rules
The schemaModel has some predefined structure, on which properties have to be set based on validation logic from FluentValidation. The same approach has been made on enum/fixed list support with earlier created custom validation from FluentValidation.
using FluentValidation;
using FluentValidation.Validators;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using System;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// Swagger validation documentation through Fluent Validations.
/// </summary>
/// <seealso cref="ISchemaFilter" />
public class SwaggerFluentValidation
: ISchemaFilter
{
/// <summary>
/// The service provider
/// </summary>
private readonly IServiceProvider ServiceProvider;
/// <summary>
/// Initializes a new instance of the <see cref="SwaggerFluentValidation"/> class.
/// </summary>
/// <param name="provider">The provider.</param>
public SwaggerFluentValidation(IServiceProvider provider)
{
ServiceProvider = provider;
}
/// <summary>
/// Applies the specified model.
/// </summary>
/// <param name="schemaModel">The model.</param>
/// <param name="context">The context.</param>
public void Apply(Schema schemaModel, SchemaFilterContext context)
{
// Trys to get validator from FluentValidation library.
// I am using ServiceProvider but approach can be changed to
// reference from FluentValidation registered validator.
var validator = ServiceProvider
.GetService(typeof(IValidator<>)
.MakeGenericType(context.SystemType)) as IValidator;
if (validator == null)
{
return;
}
if (schemaModel.Required == null)
{
schemaModel.Required = new List<string>();
}
var validatorDescriptor = validator.CreateDescriptor();
foreach (var key in schemaModel.Properties.Keys)
{
foreach (var propertyValidator in validatorDescriptor
.GetValidatorsForMember(ToPascalCase(key)))
{
if (propertyValidator is NotNullValidator
|| propertyValidator is NotEmptyValidator)
{
schemaModel.Required.Add(key);
}
if (propertyValidator is LengthValidator lengthValidator)
{
if (lengthValidator.Max > 0)
{
schemaModel.Properties[key].MaxLength = lengthValidator.Max;
}
schemaModel.Properties[key].MinLength = lengthValidator.Min;
}
if (propertyValidator is RegularExpressionValidator expressionValidator)
{
schemaModel.Properties[key].Pattern = expressionValidator.Expression;
}
if (propertyValidator is FixedListValidator itemListValidator)
{
schemaModel.Properties[key].Enum = itemListValidator.ValidItems.ToList<object>();
}
}
}
}
/// <summary>
/// To convert case as swagger may be using lower camel case
/// </summary>
/// <param name="inputString">The input string.</param>
/// <returns>Pascal case for string.</returns>
private static string ToPascalCase(string inputString)
{
if (string.IsNullOrEmpty(inputString) || inputString.Length < 2)
{
return null;
}
return inputString.Substring(0, 1).ToUpper() + inputString.Substring(1);
}
}
That is all, now SwaggerFluentValidation has to be registered on Swagger something like
services.AddApiVersioning(o =>
{
o.AssumeDefaultVersionWhenUnspecified = true;
o.DefaultApiVersion = new ApiVersion(1, 0);
}).AddSwaggerGen(options =>
{
var provider = services.BuildServiceProvider().GetRequiredService<IApiVersionDescriptionProvider>();
options.ConfigureSwaggerVersions(provider);
options.SchemaFilter<SwaggerFluentValidation>(services.BuildServiceProvider());
});
This doesn't work with latest code. Now all the validators are generic
ReplyDelete