In past projects, documenting my APIs was a “nice-to-have” (so it never happened). Unfortunately, once you’ve created a sizeable code footprint, going back and adding missing documentation after the fact is a Herculean task. Fortunately, with ASP.NET Core, supporting API documentation is as simple as adding some code and comments to API controllers.
First, be sure your web project generates XML documentation by going into the project Properties and clicking the Build tab.
Out of the box, this will cause Visual Studio to start warning you about every missing XML comment in the project. You can suppress the warning by adding 1591 above.
Swagger is just a specification, not an implementation. Its official name is OpenAPI, but most people still refer to it as Swagger. The two main implementations for .NET are Swashbuckle and NSwag. I’ll be using Swashbuckle in my examples.
Open the NuGet package manager, search for Swashbuckle.AspNetCore, and add it to your project.
Once installed, go to your Startup.cs file and add a few lines of code. In ConfigureServices, provide the following code:
services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Info() { Title = "Web App", Version = "v1" }); // Set the comments path for the Swagger JSON and UI. var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); c.IncludeXmlComments(xmlPath); });
Then, in Configure, add the following lines of code:
app.UseSwagger(); if (env.IsDevelopment()) { app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "Web App V1"); }); }
At this point, you can debug your application. By default, swashbuckle adds a route to your application, so if you navigate to /swagger, SwaggerUI will display.
using Microsoft.AspNetCore.Mvc; namespace WebApplication.ApiControllers { /// <summary> /// Verifies that the swagger documentation generator works as expected. /// </summary> [Route("api/[controller]")] [ApiController] public class TestController : ControllerBase { /// <summary> /// Retrieves test data. /// </summary> /// <returns>The test data.</returns> [HttpGet] public IActionResult GetTestData() { var model = new { Message = "Hello, world!" }; return Ok(model); } } }
Bringing up SwaggerUI again, we can see our new API end-point is listed.
Notice the <summary> comment appears next to the route. We can provide additional information about different responses using the [SwaggerResponse] attribute.
/// <summary> /// Retrieves test data. /// </summary> /// <returns>The test data.</returns> [HttpGet] [SwaggerResponse((int)HttpStatusCode.OK, Description = "The data was returned successfully.")] [SwaggerResponse((int)HttpStatusCode.Unauthorized, Description = "You are not authorized to access this resource.")] public IActionResult GetTestData() { var model = new { Message = "Hello, world!" }; return Ok(model); } This will update the swagger documentation:
I especially like that you can associate a different return-type to each response, so you can specify that on success, return the normal model but on error, return an error model.
Note: At the time of this writing, you must specify the method (e.g., [HttpGet]), or SwaggerUI will return an error.
When I started using MVC (version 3 back in 2012), I was using Razor heavily. Razor incorporated a lot of functionality surrounding validation, centering around the ModelState and data annotations. With the introduction of SPAs and WebAPI, I stopped using ModelState entirely and went back to writing explicit if-statements at the top of my API calls for validation.
ModelState worked by defining data annotations on API models, from the System.ComponentModel.DataAnnotations namespace. These annotations covered the most common forms of validation: Required? Has Min Length? Has Max Length? Valid Email? Matches Regex? In some applications that’s all you really need, but in most instances, validation can be as complex as the business logic itself.
Attributes are a compile-time artifact, which means you cannot perform complex logic easily. For example, you would have to implement your own validation attributes (or inherit from IValidatableObject) to read from a configuration file or database. However, at that point, you’re mixing concerns and may have to jump through loops to get access to the IoC (though this is much easier with ASP.NET Core). Personally, I like my API models to be POCOs.
For now, let’s consider a model that uses data annotations and the IValidatableObject interface:
public class AddressModel : IValidatableObject { [Required] [MaxLength(100)] public string Line1 { get; set; } [MaxLength(100)] public string Line2 { get; set; } [Required] [MaxLength(100)] public string City { get; set; } [Required] [MaxLength(2)] public string State { get; set; } [Required] [RegularExpression(@"^\d{5}(-?\d{4})?$")] public string Zip { get; set; } public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { var stateRepository = (IStateRepository)validationContext.GetService(typeof(IStateRepository)); var state = stateRepository.GetState(State); if (state == null) { yield return new ValidationResult("Unknown state", new[] { nameof(State) }); } } }
Here’s what the swagger documentation looks like for that model:
Our AddressModel is certainly not a simple POCO anymore, and grabbing the IStateRepository from the underlying DI container is ugly. Furthermore, with APIs tending toward asynchronous operations, this synchronous validation becomes unacceptable. So, do we go back to mile-long if-statements?
If you’re like me, you were greatly relieved when Entity Framework 5 introduced fluent configurations. Instead of using a nasty EDMX file or more attributes developers could define the configuration using chained method calls. FluentValidation is a library that achieves the same fluent configuration chaining, but for validation.
Here is an equivalent validation for the AddressModel using FluentValidation:
public class AddressModelValidator : AbstractValidator<AddressModel> { private readonly IServiceProvider serviceProvider; public AddressModelValidator(IServiceProvider serviceProvider) { this.serviceProvider = serviceProvider; RuleFor(x => x.Line1).NotEmpty(); RuleFor(x => x.Line1).MaximumLength(100); RuleFor(x => x.Line2).MaximumLength(100); RuleFor(x => x.City).NotEmpty(); RuleFor(x => x.City).MaximumLength(100); RuleFor(x => x.State).NotEmpty(); RuleFor(x => x.State).MaximumLength(2); RuleFor(x => x.Zip).NotEmpty(); RuleFor(x => x.Zip).Matches(@"^\d{5}(-?\d{4})?$"); RuleFor(x => x.State).MustAsync(IsKnownState).When(x => x.State != null); } private async Task<bool> IsKnownState(string abbreviation, CancellationToken token) { var stateRepository = serviceProvider.GetRequiredService<IStateRepository>(); var state = await stateRepository.GetStateAsync(abbreviation, token); return state != null; } }
Notice I was able to easily inject an IServiceProvider into the constructor, which allows me to easily retrieve dependencies on-demand. I’m also able provide an asynchronous implementation, using MustAsync, to check the validity of the provided state.
Also notice I did not directly inject the IStateRepository interface into the constructor. This is an artifact of the way lifetime scopes are managed by the dependency injection framework. You’ll also notice I used GetRequiredServices, which is extension method that makes working with the IServiceProvider interface easier.
To let ASP.NET Core know to use FluentValidation, we must update the Startup.cs file again. First, open the NuGet package manager and add FluentValidation.AspNetCore to your project. In the ConfigureServices method, tag a call to AddFluentValidation onto the AddMvc method.
services.AddMvc() .SetCompatibilityVersion(CompatibilityVersion.Version_2_1) .AddFluentValidation(o => { o.RegisterValidatorsFromAssemblyContaining<AddressModelValidator>(); });
The good news is FluentValidation will reuse the dependency injection configuration provided by ASP.NET Core. The call to RegisterValidatorsFromAssemblyContaining automatically registers all validator classes in an assembly with the service collection. That’s what allows me to pass things like IServiceProvider or IValidatorFactory to the constructor. The AddFluentValidation method provides several methods for configuring FluentValidation to work with ASP.NET Core.
If you open SwaggerUI again, you’ll notice we’ve lost all our model validation info. By default, Swashbuckle only knows how to generate documentation for data annotations.
Fortunately, some other smart people already went through the pain of integrating Swashbuckle and FluentValidation. Simply add the MicroElements.Swashbuckle.FluentValidation NuGet package, and now you can make a simple modification to the AddSwaggerGen call:
services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Info() { Title = "Web App", Version = "v1" }); // Set the comments path for the Swagger JSON and UI. var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); c.IncludeXmlComments(xmlPath); c.AddFluentValidationRules(); });
This single line of code lets swagger inspect your validator classes to build the equivalent documentation that came with data annotations. So now, when you open SwaggerUI, you can view the same level of detail you had originally:
As I’ve shown, adding API documentation to ASP.NET Core is extremely simple. So far, I love how flexible ASP.NET Core is. I don’t feel like I’m jumping through hoops to access dependency injection or swap in my own libraries. It just works!
Lastly, if you’d like your organization to boast more swashbuckling swagger than Mick Jagger, when it comes to Customer Engagement, Analytics and Insights, Cloud and Hybrid IT, or Enterprise Mobility, please don’t hesitate to reach out. Our expert consultants would love to get you started.
FluentValidation Docs – https://github.com/JeremySkinner/FluentValidation/wiki/a.-Index
Swagger Configuration – https://docs.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-swashbuckle?view=aspnetcore-2.1&tabs=visual-studio%2Cvisual-studio-xml
Swagger/FluentValidation Integration – https://github.com/micro-elements/MicroElements.Swashbuckle.FluentValidation
Customizing Swagger Docs – https://www.schaeflein.net/adding-implementation-notes-to-swagger-ui-via-swashbuckle-attributes/
ASP.NET Model Validation – https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-2.1
Necessary cookies are absolutely essential for the website to function properly. These cookies ensure basic functionalities and security features of the website, anonymously.
Cookie | Duration | Description |
---|---|---|
cookielawinfo-checbox-analytics | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Analytics". |
cookielawinfo-checbox-functional | 11 months | The cookie is set by GDPR cookie consent to record the user consent for the cookies in the category "Functional". |
cookielawinfo-checbox-others | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Other. |
cookielawinfo-checkbox-necessary | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookies is used to store the user consent for the cookies in the category "Necessary". |
cookielawinfo-checkbox-performance | 11 months | This cookie is set by GDPR Cookie Consent plugin. The cookie is used to store the user consent for the cookies in the category "Performance". |
viewed_cookie_policy | 11 months | The cookie is set by the GDPR Cookie Consent plugin and is used to store whether or not user has consented to the use of cookies. It does not store any personal data. |
Functional cookies help to perform certain functionalities like sharing the content of the website on social media platforms, collect feedbacks, and other third-party features.
Performance cookies are used to understand and analyze the key performance indexes of the website which helps in delivering a better user experience for the visitors.
Analytical cookies are used to understand how visitors interact with the website. These cookies help provide information on metrics the number of visitors, bounce rate, traffic source, etc.
Advertisement cookies are used to provide visitors with relevant ads and marketing campaigns. These cookies track visitors across websites and collect information to provide customized ads.
Other uncategorized cookies are those that are being analyzed and have not been classified into a category as yet.