ASP.NET WEB API documentation using Swagger – Update schema using SchemaFilters and FluenValidator rules

At this post we will look how to extend schema getting information from popular FluentValidation library attributes.
We will see how to display min-max constraints for different types and how to extend examples with proper email example.
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<UserModel>
    {
        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<TValidator>
        : 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<NotEmptyValidator>
    {
        protected override void Update(
            Schema schema,
            PropertyRule rule,
            NotEmptyValidator validator)
        {
            if (schema.required == null)
            {
                schema.required = new List<string>();
            }

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

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

    public class StringMinMaxFluentValidatorSchemaUpdater : FluentValidatorSchemaUpdater<LengthValidator>
    {
        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<InclusiveBetweenValidator>
    {
        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<RegularExpressionValidator>
    {
        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<IFluentValidatorSchemaUpdater>{
                new StringMinMaxFluentValidatorSchemaUpdater(),
                new FormatFluentValidatorSchemaUpdater(),
                new RequriedFieldFluentValidatorSchemaUpdater(),
                new EmailFluentValidatorSchemaUpdater(),
                new IntMinMaxFluentValidatorSchemaUpdater()
            };

            var customAttribute = type.GetCustomAttribute<ValidatorAttribute>();

            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<FluentValidationSchemaFilter>();

Now after running project we will see below improvements:

  • Email is displayed not as autogenerated string, but as valid one
  • YearOfBirth is generated as valid integer for applied 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.

Summary:

We have looked how it is possible to tweak generated scheme using FluentValidation rules as of example.
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 )

w

Connecting to %s

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