23

I am currently using Fluent Validation instead of Data Annotations for my Web api and using swagger for API documentation. Fluent validation rules are not reflected in swagger model as i am unable to configure fluent validation rules with swagger schema filter.

This Blog has a good explanation for using it with ASP.net MVC. but i am unable to configure it to use it in ASP.net Core.

So far i have tried the following code but i am unable to get validator type.

services.AddSwaggerGen(options => options.SchemaFilter<AddFluentValidationRules>());

public class AddFluentValidationRules : ISchemaFilter
{
    public void Apply(Schema model, SchemaFilterContext context)
    {
        model.Required = new List<string>();
        var validator = GetValidator(type); // How?
        var validatorDescriptor = validator.CreateDescriptor();

        foreach (var key in model.Properties.Keys)
        {
            foreach (var propertyValidator in validatorDescriptor.GetValidatorsForMember(key))
            {
                 // Add to model properties as in blog
            }
        }
    }
}
Mujahid Daud Khan
  • 1,983
  • 1
  • 14
  • 23

3 Answers3

27

I've created github project and nuget package based on Mujahid Daud Khan answer. I made redesign to support extensibility and supported other validators.

github: https://github.com/micro-elements/MicroElements.Swashbuckle.FluentValidation

nuget: https://www.nuget.org/packages/MicroElements.Swashbuckle.FluentValidation

Note: For WebApi see: https://github.com/micro-elements/MicroElements.Swashbuckle.FluentValidation.WebApi

Supported validators

  • INotNullValidator (NotNull)
  • INotEmptyValidator (NotEmpty)
  • ILengthValidator (Length, MinimumLength, MaximumLength, ExactLength)
  • IRegularExpressionValidator (Email, Matches)
  • IComparisonValidator (GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual)
  • IBetweenValidator (InclusiveBetween, ExclusiveBetween)

Usage

1. Reference packages in your web project:

<PackageReference Include="FluentValidation.AspNetCore" Version="7.5.2" />
<PackageReference Include="MicroElements.Swashbuckle.FluentValidation" Version="0.4.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="2.3.0" />

2. Change Startup.cs

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services
        .AddMvc()
        // Adds fluent validators to Asp.net
        .AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<CustomerValidator>());

    services.AddSwaggerGen(c =>
    {
        c.SwaggerDoc("v1", new Info { Title = "My API", Version = "v1" });
        // Adds fluent validation rules to swagger
        c.AddFluentValidationRules();
    });
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app
        .UseMvc()
        // Adds swagger
        .UseSwagger();

    // Adds swagger UI
    app.UseSwaggerUI(c =>
    {
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
    });
}

Swagger Sample model and validator

public class Sample
{
    public string PropertyWithNoRules { get; set; }

    public string NotNull { get; set; }
    public string NotEmpty { get; set; }
    public string EmailAddress { get; set; }
    public string RegexField { get; set; }

    public int ValueInRange { get; set; }
    public int ValueInRangeExclusive { get; set; }
}

public class SampleValidator : AbstractValidator<Sample>
{
    public SampleValidator()
    {
        RuleFor(sample => sample.NotNull).NotNull();
        RuleFor(sample => sample.NotEmpty).NotEmpty();
        RuleFor(sample => sample.EmailAddress).EmailAddress();
        RuleFor(sample => sample.RegexField).Matches(@"(\d{4})-(\d{2})-(\d{2})");

        RuleFor(sample => sample.ValueInRange).GreaterThanOrEqualTo(5).LessThanOrEqualTo(10);
        RuleFor(sample => sample.ValueInRangeExclusive).GreaterThan(5).LessThan(10);
    }
}

Feel free to add issues!

nwpie
  • 665
  • 11
  • 24
Alexey.Petriashev
  • 1,634
  • 1
  • 15
  • 19
11

After searching i have finally figured out that i needed to IValidationFactory for validator instance.

public class AddFluentValidationRules : ISchemaFilter
{
    private readonly IValidatorFactory _factory;

    /// <summary>
    ///     Default constructor with DI
    /// </summary>
    /// <param name="factory"></param>
    public AddFluentValidationRules(IValidatorFactory factory)
    {
        _factory = factory;
    }

    /// <summary>
    /// </summary>
    /// <param name="model"></param>
    /// <param name="context"></param>
    public void Apply(Schema model, SchemaFilterContext context)
    {

        // use IoC or FluentValidatorFactory to get AbstractValidator<T> instance
        var validator = _factory.GetValidator(context.SystemType);
        if (validator == null) return;
        if (model.Required == null)
            model.Required = new List<string>();

        var validatorDescriptor = validator.CreateDescriptor();
        foreach (var key in model.Properties.Keys)
        {
            foreach (var propertyValidator in validatorDescriptor
                .GetValidatorsForMember(ToPascalCase(key)))
            {
                if (propertyValidator is NotNullValidator 
                  || propertyValidator is NotEmptyValidator)
                    model.Required.Add(key);

                if (propertyValidator is LengthValidator lengthValidator)
                {
                    if (lengthValidator.Max > 0)
                        model.Properties[key].MaxLength = lengthValidator.Max;

                    model.Properties[key].MinLength = lengthValidator.Min;
                }

                if (propertyValidator is RegularExpressionValidator expressionValidator)
                    model.Properties[key].Pattern = expressionValidator.Expression;

                // Add more validation properties here;
            }
        }
    }

    /// <summary>
    ///     To convert case as swagger may be using lower camel case
    /// </summary>
    /// <param name="inputString"></param>
    /// <returns></returns>
    private static string ToPascalCase(string inputString)
    {
        // If there are 0 or 1 characters, just return the string.
        if (inputString == null) return null;
        if (inputString.Length < 2) return inputString.ToUpper();
        return inputString.Substring(0, 1).ToUpper() + inputString.Substring(1);
    }
}

and add this class to swaggerGen options

options.SchemaFilter<AddFluentValidationRules>();
nwpie
  • 665
  • 11
  • 24
Mujahid Daud Khan
  • 1,983
  • 1
  • 14
  • 23
  • I attempted to follow this solution; however, I'm running into an "InvalidOperationException: Cannot resolve 'FluentValidation.IValidator`1[MyClass]' from root provider because it requires scoped service 'MyConfig'." exception when the factory attempts to get the validator. My validators sometimes depend on scoped services (such as config). The default ASP.NET Core DI and validator works correctly when calling the API method directly, but for some reason it fails here. Did you ever run into this? – wdspider Jan 04 '18 at 15:09
  • @wdspider Is swagger working for you without fluent validation? – Mujahid Daud Khan Jan 04 '18 at 17:22
  • Yes. If I remove the `options.SchemaFilter();` line from the swaggerGen options (i.e. not attempt to integrate the fluent validation rules); I successfully get a generated swagger.json file along with the working Swashbuckle UI. The downside is that it doesn't show any of the validation information which is why I sought out this solution in the first place. – wdspider Jan 04 '18 at 19:44
  • Have you added fluent validation service? ie `services.AddMvc().AddFluentValidation(...)` – Mujahid Daud Khan Jan 04 '18 at 19:52
  • @wdspider, seems like your code is Ok. I have also used some other Filters and they also work fine. try using a filter described here https://stackoverflow.com/a/43074037/1735196 – Mujahid Daud Khan Jan 06 '18 at 18:45
9
  1. Install Nuget package: MicroElements.Swashbuckle.FluentValidation

  2. Add to ConfigureServices: services.AddFluentValidationRulesToSwagger();

Dmitry
  • 1,095
  • 14
  • 21