ASP.NET WEB API documentation using Swagger – Extend schema generation using SchemaFilters and FluentValidation rules

In this post we will see how to extend schema generation using FluentValidation rules.
We will see how to display min/max constraints for Integer type in Swagger UI and how to extend default examples to show valid email for email attribute.To get overall view about Schema filter, follow the official documentation at: Swashbuckle – Schema filters

Add FluentValidation to project + new API endpoint for demo

Install latest FluentValidation.WebAPI package into SwaggerDemo project.
Now let’s create another API endpoint, add new UsersController, model and validation:

public class UsersController : ApiController
    {
        public IHttpActionResult Post(UserModel model)
        {
            return Ok();//stub
        }
    }

    [Validator(typeof(UserModelValidator))]
    public class UserModel
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public int YearOfBirth { get; set; }
    }

    public class UserModelValidator : AbstractValidator
    {
        public UserModelValidator()
        {
            RuleFor(m => m.Email)
                .NotEmpty()
                .SetValidator(new EmailValidator());

            RuleFor(m => m.FirstName)
                .NotEmpty()
                .Length(3, 25);

            RuleFor(m => m.LastName)
                .Length(3, 25);

            RuleFor(m => m.YearOfBirth)
                .NotEmpty()
                .InclusiveBetween(1991, 2017);
        }
    }

Implement Schema updates based on validation rules

We will create and use IFluentValidatorSchemaUpdater interface and implement it for every suitable validator. With this design, we automatically conform to Single responsibility and Interface segregation principle for each implementation

    public interface IFluentValidatorSchemaUpdater
    {
        void Update(Schema schema, PropertyRule rule);
    }

    public abstract class FluentValidatorSchemaUpdater
        : IFluentValidatorSchemaUpdater where TValidator : IPropertyValidator
    {
        public void Update(Schema schema, PropertyRule rule)
        {
            var validator = rule.Validators.FirstOrDefault(v => v.GetType() == typeof(TValidator));

            if (validator != null)
            {
                Update(schema, rule, (TValidator)validator);
            }
        }

        protected abstract void Update(Schema schema, PropertyRule rule, TValidator validator);
    }

    public class RequriedFieldFluentValidatorSchemaUpdater
        : FluentValidatorSchemaUpdater
    {
        protected override void Update(
            Schema schema,
            PropertyRule rule,
            NotEmptyValidator validator)
        {
            if (schema.required == null)
            {
                schema.required = new List();
            }

            schema.required.Add(rule.PropertyName);
        }
    }

    public class EmailFluentValidatorSchemaUpdater
        : FluentValidatorSchemaUpdater
    {
        protected override void Update(
            Schema schema,
            PropertyRule rule,
            EmailValidator validator)
        {
            schema.properties[rule.PropertyName].example = "string.string@example.com";
        }
    }

    public class StringMinMaxFluentValidatorSchemaUpdater : FluentValidatorSchemaUpdater
    {
        protected override void Update(
            Schema schema,
            PropertyRule rule,
            LengthValidator validator)
        {
            schema.properties[rule.PropertyName].minLength = validator.Min;
            schema.properties[rule.PropertyName].maxLength = validator.Max;
        }
    }

    public class IntMinMaxFluentValidatorSchemaUpdater : FluentValidatorSchemaUpdater
    {
        protected override void Update(
            Schema schema,
            PropertyRule rule,
            InclusiveBetweenValidator validator)
        {
            if (rule.Expression.ReturnType == typeof(int))
            {
                schema.properties[rule.PropertyName].example = (int)validator.From;
                schema.properties[rule.PropertyName].minimum = (int)validator.From;
                schema.properties[rule.PropertyName].maximum = (int)validator.To;
            }
        }
    }

    public class FormatFluentValidatorSchemaUpdater
        : FluentValidatorSchemaUpdater
    {
        protected override void Update(
            Schema schema,
            PropertyRule rule,
            RegularExpressionValidator validator)
        {
            schema.properties[rule.PropertyName].format = validator.Expression;
        }
    }

Register Schema filter to Swagger configuration

Now add new schema filter, here we manually inject all the IFluentValidatorSchemaUpdater implementations, but it could also be done using dependency injection library, such as Ninject.

The implementation is pretty straightforward:

  • For each class decorated with ValidatorAttribute (from FluentValidator namespace), such as UserModel,
  • Take properties with validation rules,
  • Update Swagger schema according to registered schema updaters
 public class FluentValidationSchemaFilter : ISchemaFilter
    {
        public void Apply(Schema schema, SchemaRegistry schemaRegistry, Type type)
        {
            var updaters = new List{
                new StringMinMaxFluentValidatorSchemaUpdater(),
                new FormatFluentValidatorSchemaUpdater(),
                new RequriedFieldFluentValidatorSchemaUpdater(),
                new EmailFluentValidatorSchemaUpdater(),
                new IntMinMaxFluentValidatorSchemaUpdater()
            };

            var customAttribute = type.GetCustomAttribute();

            if (customAttribute == null)
            {
                return;
            }

            var validator = (IValidator)Activator.CreateInstance(customAttribute.ValidatorType);
            var descriptor = validator.CreateDescriptor();
            var membersWithValidations = descriptor.GetMembersWithValidators();

            foreach (var member in membersWithValidations)
            {
                var rules = descriptor.GetRulesForMember(member.Key);

                foreach (var r in rules)
                {
                    updaters.ForEach(u => u.Update(schema, (PropertyRule)r));
                }
            }
        }
    }

The last step is to register SchemaFilter in SwaggerConfig

 c.SchemaFilter();

Now after running project we will see below improvements:

  • Email is displayed not as autogenerated string, but as valid email address
  • YearOfBirth is generated as valid integer based on declared validation rules
  • When hover over the Model field, such as YearOfBirth, in pop-up it is possible to see minimum and maximum values. The same works for strings as well (min/max length)

Summary:

We have seen how to extend Swagger generated schema using FluentValidation rules.
It is a quite common technique to mark particular property/method with custom attribute, that could be used to auto-generate documentation.

This entry was posted in ASP.NET, Documentation, WEB API and tagged , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.