Implementing API Versioning in .NET 6.0

Implementing API Versioning in .NET 6.0

What is API Versioning?

We all know that changes to our APIs are unavoidable. Managing the impact of these changes is crucial in order not to break any existing clients that use our APIs, but also be able to support new clients. With API versioning, we can develop new versions of our service that new clients will be able to consume, while keeping our existing clients working.

Project Structure

No alt text provided for this image

Implementing API Versioning in .NET 6.0

Now, we’re going to take a look at how to implement API Versioning in a .NET 6.0 application. ?

Initially, we will assume that we have only our WeatherForecastController, and also a WeatherForecast model, both with the following code respectively:

using Microsoft.AspNetCore.Mvc;


namespace Api.Versioning.Controllers
{
? ? [ApiController]
? ? [Route("[controller]")]
? ? public class WeatherForecastController : ControllerBase
? ? {
? ? ? ? private static readonly string[] Summaries = new[]
? ? ? ? {
? ? ? ? "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
? ? };


? ? ? ? private readonly ILogger<WeatherForecastController> _logger;


? ? ? ? public WeatherForecastController(ILogger<WeatherForecastController> logger)
? ? ? ? {
? ? ? ? ? ? _logger = logger;
? ? ? ? }


? ? ? ? [HttpGet(Name = "GetWeatherForecast")]
? ? ? ? public IEnumerable<WeatherForecast> Get()
? ? ? ? {
? ? ? ? ? ? return Enumerable.Range(1, 5).Select(index => new WeatherForecast
? ? ? ? ? ? {
? ? ? ? ? ? ? ? Date = DateTime.Now.AddDays(index),
? ? ? ? ? ? ? ? TemperatureC = Random.Shared.Next(-20, 55),
? ? ? ? ? ? ? ? Summary = Summaries[Random.Shared.Next(Summaries.Length)]
? ? ? ? ? ? })
? ? ? ? ? ? .ToArray();
? ? ? ? }
? ? }
}
        


namespace Api.Versioning
{
? ? public class WeatherForecast
? ? {
? ? ? ? public DateTime Date { get; set; }
? ? ? ? public int TemperatureC { get; set; }
? ? ? ? public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
? ? ? ? public string? Summary { get; set; }
? ? }
}        

After some time, a new requirement comes that requires the API not to return a summary but a value that tells us if the weather is good or not.

If we change the existing WeatherForecast model, we risk of breaking the existing clients that use our Summary field.?

This is a sign that we need another version of our API.

To use API versioning, the first thing we need to do is install the following NuGet Packages: Microsoft.AspNetCore.Mvc.Versioning and Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer.

Next, add the following two files which can be found on aspnet-api-versioning/samples/aspnetcore/SwaggerSample at ms · dotnet/aspnet-api-versioning (github.com)?

This is a very helpful sample, and I recommend you go through it in order for things to be clearer.

First file is the ConfigureSwaggerOptions.cs:

namespace Microsoft.Examples
{
? ? using Microsoft.AspNetCore.Mvc.ApiExplorer;
? ? using Microsoft.Extensions.DependencyInjection;
? ? using Microsoft.Extensions.Options;
? ? using Microsoft.OpenApi.Models;
? ? using Swashbuckle.AspNetCore.SwaggerGen;
? ? using System;


? ? /// <summary>
? ? /// Configures the Swagger generation options.
? ? /// </summary>
? ? /// <remarks>This allows API versioning to define a Swagger document per API version after the
? ? /// <see cref="IApiVersionDescriptionProvider"/> service has been resolved from the service container.</remarks>
? ? public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
? ? {
? ? ? ? readonly IApiVersionDescriptionProvider provider;


? ? ? ? /// <summary>
? ? ? ? /// Initializes a new instance of the <see cref="ConfigureSwaggerOptions"/> class.
? ? ? ? /// </summary>
? ? ? ? /// <param name="provider">The <see cref="IApiVersionDescriptionProvider">provider</see> used to generate Swagger documents.</param>
? ? ? ? public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) => this.provider = provider;


? ? ? ? /// <inheritdoc />
? ? ? ? public void Configure(SwaggerGenOptions options)
? ? ? ? {
? ? ? ? ? ? // add a swagger document for each discovered API version
? ? ? ? ? ? // note: you might choose to skip or document deprecated API versions differently
? ? ? ? ? ? foreach (var description in provider.ApiVersionDescriptions)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
? ? ? ? ? ? }
? ? ? ? }


? ? ? ? static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
? ? ? ? {
? ? ? ? ? ? var info = new OpenApiInfo()
? ? ? ? ? ? {
? ? ? ? ? ? ? ? Title = "Weather Forecast API",
? ? ? ? ? ? ? ? Version = description.ApiVersion.ToString(),
? ? ? ? ? ? ? ? Description = "A sample application with Swagger, Swashbuckle, and API versioning.",
? ? ? ? ? ? ? ? Contact = new OpenApiContact() { Name = "Dimitar Iliev", Email = "[email protected]" },
? ? ? ? ? ? ? ? License = new OpenApiLicense() { Name = "MIT", Url = new Uri("https://opensource.org/licenses/MIT") }
? ? ? ? ? ? };


? ? ? ? ? ? if (description.IsDeprecated)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? info.Description += " This API version has been deprecated.";
? ? ? ? ? ? }


? ? ? ? ? ? return info;
? ? ? ? }
? ? }
}

        

Second file is the SwaggerDefaultValues.cs:


namespace Microsoft.Examples
{
? ? using Microsoft.AspNetCore.Mvc.ApiExplorer;
? ? using Microsoft.OpenApi.Models;
? ? using Swashbuckle.AspNetCore.SwaggerGen;
? ? using System.Linq;
? ? using System.Text.Json;


? ? /// <summary>
? ? /// Represents the Swagger/Swashbuckle operation filter used to document the implicit API version parameter.
? ? /// </summary>
? ? /// <remarks>This <see cref="IOperationFilter"/> is only required due to bugs in the <see cref="SwaggerGenerator"/>.
? ? /// Once they are fixed and published, this class can be removed.</remarks>
? ? public class SwaggerDefaultValues : IOperationFilter
? ? {
? ? ? ? /// <summary>
? ? ? ? /// Applies the filter to the specified operation using the given context.
? ? ? ? /// </summary>
? ? ? ? /// <param name="operation">The operation to apply the filter to.</param>
? ? ? ? /// <param name="context">The current operation filter context.</param>
? ? ? ? public void Apply(OpenApiOperation operation, OperationFilterContext context)
? ? ? ? {
? ? ? ? ? ? var apiDescription = context.ApiDescription;


? ? ? ? ? ? operation.Deprecated |= apiDescription.IsDeprecated();


? ? ? ? ? ? // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1752#issue-663991077
? ? ? ? ? ? foreach (var responseType in context.ApiDescription.SupportedResponseTypes)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/b7cf75e7905050305b115dd96640ddd6e74c7ac9/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SwaggerGenerator.cs#L383-L387
? ? ? ? ? ? ? ? var responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString();
? ? ? ? ? ? ? ? var response = operation.Responses[responseKey];


? ? ? ? ? ? ? ? foreach (var contentType in response.Content.Keys)
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? if (!responseType.ApiResponseFormats.Any(x => x.MediaType == contentType))
? ? ? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? ? ? response.Content.Remove(contentType);
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }


? ? ? ? ? ? if (operation.Parameters == null)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? return;
? ? ? ? ? ? }


? ? ? ? ? ? // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/412
? ? ? ? ? ? // REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/pull/413
? ? ? ? ? ? foreach (var parameter in operation.Parameters)
? ? ? ? ? ? {
? ? ? ? ? ? ? ? var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name);


? ? ? ? ? ? ? ? if (parameter.Description == null)
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? parameter.Description = description.ModelMetadata?.Description;
? ? ? ? ? ? ? ? }


? ? ? ? ? ? ? ? if (parameter.Schema.Default == null && description.DefaultValue != null)
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? // REF: https://github.com/Microsoft/aspnet-api-versioning/issues/429#issuecomment-605402330
? ? ? ? ? ? ? ? ? ? var json = JsonSerializer.Serialize(description.DefaultValue, description.ModelMetadata.ModelType);
? ? ? ? ? ? ? ? ? ? parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson(json);
? ? ? ? ? ? ? ? }


? ? ? ? ? ? ? ? parameter.Required |= description.IsRequired;
? ? ? ? ? ? }
? ? ? ? }
? ? }
}        

In short, SwaggerDefaultValues.cs is required because of bugs in the Swagger generator while ConfigureSwaggerOptions.cs allows us to specify metadata for each version of the API.?

We modify our Program.cs file with the code below:

using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Versioning;
using Microsoft.Examples;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.SwaggerGen;


var builder = WebApplication.CreateBuilder(args);


// Add services to the container.


builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();


builder.Services.AddApiVersioning(x =>
{
? ? x.AssumeDefaultVersionWhenUnspecified = true;
? ? x.DefaultApiVersion = new Microsoft.AspNetCore.Mvc.ApiVersion(1, 0);
? ? x.ReportApiVersions = true; //returns api-supported-versions header in the response
? ? x.ApiVersionReader = new QueryStringApiVersionReader("ver", "api-version");
});


builder.Services.AddVersionedApiExplorer(x =>
{
? ? x.GroupNameFormat = "'v'VVV";
? ? x.SubstituteApiVersionInUrl = true;
});


builder.Services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();


builder.Services.AddSwaggerGen(x =>
{
? ? x.OperationFilter<SwaggerDefaultValues>();
});


var app = builder.Build();


// Configure the HTTP request pipeline.


var provider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();


if (app.Environment.IsDevelopment())
{
? ? app.UseSwagger();
? ? app.UseSwaggerUI(x =>
? ? {
? ? ? ? foreach (var item in provider.ApiVersionDescriptions)
? ? ? ? {
? ? ? ? ? ? x.SwaggerEndpoint($"/swagger/{item.GroupName}/swagger.json", item.GroupName.ToUpperInvariant());
? ? ? ? }
? ? });
}


app.UseHttpsRedirection();


app.UseAuthorization();


app.MapControllers();


app.Run();        

Now let's modify our existing WeatherForecastController to be the default version of our API (version 1.0).

using Microsoft.AspNetCore.Mvc;


namespace Api.Versioning.Controllers
{
? ? [ApiVersion("1.0")]
? ? [ApiController]
? ? [Route("[controller]")]
? ? public class WeatherForecastController : ControllerBase
? ? {
? ? ? ? private static readonly string[] Summaries = new[]
? ? ? ? {
? ? ? ? "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
? ? };


? ? ? ? private readonly ILogger<WeatherForecastController> _logger;


? ? ? ? public WeatherForecastController(ILogger<WeatherForecastController> logger)
? ? ? ? {
? ? ? ? ? ? _logger = logger;
? ? ? ? }


? ? ? ? [HttpGet(Name = "GetWeatherForecast")]
? ? ? ? public IEnumerable<WeatherForecast> Get()
? ? ? ? {
? ? ? ? ? ? return Enumerable.Range(1, 5).Select(index => new WeatherForecast
? ? ? ? ? ? {
? ? ? ? ? ? ? ? Date = DateTime.Now.AddDays(index),
? ? ? ? ? ? ? ? TemperatureC = Random.Shared.Next(-20, 55),
? ? ? ? ? ? ? ? Summary = Summaries[Random.Shared.Next(Summaries.Length)]
? ? ? ? ? ? })
? ? ? ? ? ? .ToArray();
? ? ? ? }
? ? }
}        

Next, we create a new Controller called WeatherForecastControllerV2 and add the following code:

using Microsoft.AspNetCore.Mvc;


namespace Api.Versioning.Controllers.V2
{
? ? [ApiVersion("2.0")]
? ? [ApiController]
? ? [Route("[controller]")]
? ? public class WeatherForecastController : ControllerBase
? ? {
? ? ? ? private static readonly string[] Summaries = new[]
? ? ? ? {
? ? ? ? "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
? ? };


? ? ? ? private readonly ILogger<WeatherForecastController> _logger;


? ? ? ? public WeatherForecastController(ILogger<WeatherForecastController> logger)
? ? ? ? {
? ? ? ? ? ? _logger = logger;
? ? ? ? }


? ? ? ? [HttpGet(Name = "GetWeatherForecast")]
? ? ? ? public IEnumerable<WeatherForecastV2> Get()
? ? ? ? {
? ? ? ? ? ? return Enumerable.Range(1, 5).Select(index => new WeatherForecastV2
? ? ? ? ? ? {
? ? ? ? ? ? ? ? Date = DateTime.Now.AddDays(index),
? ? ? ? ? ? ? ? TemperatureC = Random.Shared.Next(-20, 55),
? ? ? ? ? ? ? ? IsGoodWeather = Random.Shared.NextDouble() > 0.5 //generate random bool value
? ? ? ? ? ? })
? ? ? ? ? ? .ToArray();
? ? ? ? }
? ? }
}        

Note that we specified this Controller to be version 2.0 of our API.

Let's not forget to add the new WeatherForecastV2 model which will fulfill the new requirement we had.

namespace Api.Versioning
{
? ? public class WeatherForecastV2
? ? {
? ? ? ? public DateTime Date { get; set; }
? ? ? ? public int TemperatureC { get; set; }
? ? ? ? public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
? ? ? ? public bool IsGoodWeather { get; set; }
? ? }
}        

Now let us test the changes we did by calling both versions of our API through Postman.

We make HTTP GET requests to the following URLs:

/WeatherForecast

/WeatherForecast?ver=1.0

/WeatherForecast?api-version=2.0

Image 1
Image 2
Image 3

We can see that we successfully called both versions of our API, and they both are working as expected.

In the first two pictures, we called the version 1 of our API, both by not specifying a version and by using the ver=1.0.

In the third picture, we requested version 2 of our API by using the third option which was api-version=2.0.

Analyzing the responses, we see that version 1 of our API returns the old model with the Summary field, while the version 2 of our API returns the new model without the Summary but with the IsGoodWeather field.

Last thing is to explore the Swagger of our application.

No alt text provided for this image

We can note that in the "Select a definition part" we have both V1 and V2, corresponding to version 1 and version 2 of our API accordingly.

Perfect. Now we have implemented a fully functional versioning for our API. With this, we can support both old and new clients without worrying about breaking any of our functionalities!

Thanks for sticking to the end of another article from?"Iliev Talks Tech". #ilievtalkstech

The full, more detailed implementation of this example can be found on my GitHub repository on the following link:

DimitarIliev/Api-Versioning: Example of how to implement API versioning in .NET 6.0 (github.com)

要查看或添加评论,请登录

Dimitar Iliev ??的更多文章

社区洞察

其他会员也浏览了