Let’s create a versioned and documented ASP.NET Core Web API

Update: Swashbuckle.AspNetCore is no longer a pre-release and now reached 1.0.0 status.

Let’s create a versioned and semi-automatically documented Web API, this could be done both for public API, and quite useful for, internal-use API’s as well.

The ASP.NET Core Web API Project

Create a new project Visual C# > .NET Core > ASP.NET Core Web Application and give a descriptive name to your API.

We’re going to add three NuGet packages:

Install-Package Microsoft.AspNetCore.Mvc.Versioning
Install-Package Swashbuckle.AspNetCore
Install-Package SwashbuckleAspNetVersioningShim -Pre

Two of these packages are prerelease packages but they are fully functional

You can now remove the automatically created ValuesController.cs and let’s add a new HelloController.cs

using Microsoft.AspNetCore.Mvc;

namespace VersionedWebApi.Controllers
{
    /// <summary>
    /// HelloController, just saying Hello World!
    /// </summary>
    [ApiVersion("1.0", Deprecated = true)]
    [Route("api/v{version:apiVersion}/[controller]")]
    public class HelloController : Controller
    {
        /// <summary>
        /// Default Get call returning Hello World!
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        public string Get()
        {
            return "Hello World!";
        }
    }
}

I’ve decorated the class with an ApiVersion attribute stating that this is our version 1.0 HelloController and that it is already deprecated, and also note the modified Route attribute with a v{version:apiVersion} part.

Let’s create a new folder in the Controllers folder called v2 and create a second HelloController.cs in there.

using Microsoft.AspNetCore.Mvc;
using System;

namespace VersionedWebApi.Controllers.v2
{
    /// <summary>
    /// The modern HelloController, all up to date responses
    /// </summary>
    [ApiVersion("2.0")]
    [Route("api/v{version:apiVersion}/[controller]")]
    public class HelloController : Controller
    {
        /// <summary>
        /// Default Get call returning Hello current-year!
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        public HelloWorldModel Get()
        {
            return new HelloWorldModel
            {
                Message = $"Hello {DateTime.Now.Year}!"
            };
        }
    }
 
    /// <summary>
    /// HelloWorldModel class for HelloController
    /// </summary>
    public class HelloWorldModel
    {
        /// <summary>
        /// Message string
        /// </summary>
        public string Message { get; set; }
    }
}

This class is decorated with a 2.0 ApiVersion attribute and features a HelloWorldModel return type with a DateTime.Now part.

Now open up the Startup.cs file and let’s add Versioning support to our Web API.

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services
    var mvcBuilder = services.AddMvc();

    // Adds versioning capabilities, defaulting to version 1.0 calls if available
    services.AddApiVersioning(o =>
    {
        o.AssumeDefaultVersionWhenUnspecified = true;
        o.DefaultApiVersion = new ApiVersion(1, 0);
    });

    // Add generated documentation
    services.AddSwaggerGen(c =>
    {
        SwaggerVersioner.ConfigureSwaggerGen(c, mvcBuilder.PartManager);
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, ApplicationPartManager partManager)
{
    loggerFactory.AddConsole(Configuration.GetSection("Logging"));
    loggerFactory.AddDebug();

    // Generate swagger.json
    app.UseSwagger();

    // Let's enable SwaggerUI
    app.UseSwaggerUI(c =>
    {
        SwaggerVersioner.ConfigureSwaggerUI(c, partManager);
    });

    app.UseMvc();
}

Open the project’s Properties and go to Debug and change the Launch Url to api/v1/hello and start the application, see the result and change the url v1 to v2.

Now head over to /swagger and see your interactive documentation based on the /swagger/v1.0/swagger.json and /swagger/v2.0/swagger.json generated files.

Check out the finished project on github.com/PaddoSwam/AspNetCoreVersionedWebApi.

6 thoughts on “Let’s create a versioned and documented ASP.NET Core Web API”

  1. Thanks for the write up. Customising the shim to output better formatted docs has proven pretty simple (may raise any issue for allowing better configuration when I get a second).

    Have you managed to get the above to work with Areas? For example, my api route template is "api/v{version:apiVersion}/{area:exists}/{controller=Home}/{action=Get}"; I’m going to check that ApiExplorer is detecting them, but Swagger is not it seems.

    1. I ran into the same issue and currently working on a solution for that, it’s mostly the recognition of variable routing parts and handling them.

      I’ll keep you posted!

    2. I found the answer in case you’re interested. The ApiExplorer doesn’t detect controllers using conventional routing (they must use attribute routing).

      So, remove the route template from your UseMvc call. In my case I’m using both versioning as you’ve defined, and also Areas. Therefore, typical attributes on my controllers will look as follows:

      [Area("Diagnostics"), ApiVersion("1.0"), Route("api/v{version:apiVersion}/[Area]/[Controller]/[Action]")]
      

      You’re not quite there yet though. You’ll notice I haven’t supplied the
      [ApiExplorerSettings] attribute, which means the ApiExplorer will not populate the details required by Swagger. However, this can be done by convention. In startup under your AddMvc call, add a new convention:

      options.Conventions.Add(new ApiExplorerVisibilityEnabledConvention());

      Now create the class:

      internal sealed class ApiExplorerVisibilityEnabledConvention : IApplicationModelConvention
      {
      	public void Apply(ApplicationModel application)
      	{
      		foreach (var controller in application.Controllers.Where(c => !c.ApiExplorer.IsVisible.HasValue))
      		{
      			controller.ApiExplorer.IsVisible = true;
      			controller.ApiExplorer.GroupName = controller.ControllerName;
      		}
      	}
      }
      

      That should be the barebones required for this to display on swagger, though I did go one step further. I wanted Swagger to group my api calls by area, so I switched the above code to assign the GroupName as follows:

      internal sealed class ApiExplorerVisibilityEnabledConvention : IApplicationModelConvention
      {
      	public void Apply(ApplicationModel application)
      	{
      		foreach (var controller in application.Controllers.Where(c => !c.ApiExplorer.IsVisible.HasValue))
      		{
      			controller.ApiExplorer.IsVisible = true;
      			controller.ApiExplorer.GroupName = TryGetControllerArea(controller, out var area) ? 
      				area : controller.ControllerName;
      		}
      	}
      }
      
      private static bool TryGetControllerArea(ControllerModel controller, out string area)
      {
      	area = string.Empty;
      	var @namespace = controller.ControllerType.Namespace;
      	var areaIndex = @namespace.IndexOf(".Areas.", StringComparison.CurrentCulture);
      	if (areaIndex < 0)
      		return false;
      	area = @namespace.Substring(areaIndex + 7);
      	areaIndex = area.IndexOf('.');
      	if (areaIndex < 0)
      		return true;
      	area = area.Substring(0, areaIndex);
      	return true;
      }
      

      Hopefully that helps?

Leave a Reply

Your email address will not be published. Required fields are marked *

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