API Versioning in .Net 7— Organizing Versions In Code, Different Versioning Schemes

Dipendu Paul
8 min readJul 3, 2023

--

Photo by Caleb Woods on Unsplash

In this article, we are going to explore different ways to organize versions in code and understand the required Asp.Versioning.Mvc library configurations that support them, and also we are going to discuss different schemes of versioning supported by the library.

If you’re new to API versioning in .Net API applications, I highly recommend you read the prequel to this article here. If you’re already acquainted with it or have already read the prequel, keep reading this article. The accompanying code to follow the steps mentioned in this article can be found at https://github.com/FOSSKolkata/api-versioning-demo. Please clone the code repository on your computer before moving ahead.

Different Ways to Organize Versions In Code

Versions In Separate Controller Classes

In the prequel to this article, we have already looked into how two or more versions of API endpoints can be segregated into separate controller classes with the same route name, with each of these controller classes providing implementations for one of the versions. The completed code of the prequel can be found in the “Milestone 1/SeparateControllerClasses” folder of the code repository. Make a copy of the folder and add the following endpoint definition in both TestController and Test2Controller classes:

[HttpGet, Route("unchanged")]
public string Unchanged()
{
return "I remain unchanged across versions";
}

Run the application and test the “unchanged” API endpoint by typing in the browser address bar https://localhost:{port number}/api/test/unchanged?api-version=1.0. Also, test version 2 of the endpoint by passing 2.0 as the value of api-version query string parameter. Repeat the same for the “testmethod” API endpoint, and make sure both versions of it are also working as expected.

Note that this way of organizing versions may result in code duplication in cases where most API endpoints remain the same between versions. But in my opinion, this may not be a huge issue, given your team is following best practices of keeping the controller classes lean, and devoid of any application logic, and you’re maintaining only 2–3 versions of API in parallel. Nevertheless, we can always work around this problem by keeping different version implementations in the same controller class. Let’s see how.

Versions In The Same Controller Class

Make a copy of the folder “Milestone 0” of the Git repository.

Step 1 — Add NuGet package Asp.Versioning.Mvc to the API project

Step 2 — Add the following code snippet to the ConfigureServices method in the Startup class.

services.AddApiVersioning(setup =>
{
setup.ReportApiVersions = true;
}).AddMvc();

Step 3 — Replace the content of the TestController.cs file with the following code:

using Microsoft.AspNetCore.Mvc;
using Asp.Versioning;

namespace ApiVersioningDemoInNet7.Controllers
{
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
[MapToApiVersion("1.0")]
[HttpGet, Route("testmethod")]
public string TestMethod()
{
return "Test method call successful";
}

[MapToApiVersion("2.0")]
[HttpGet, Route("testmethod")]
public string TestMethod2()
{
return "Test method V2 call successful";
}

[HttpGet, Route("unchanged")]
public string Unchanged()
{
return "I remain unchanged across versions";
}
}
}

Note, ApiVersion attribute is applied on the controller class multiple times for specifying more than one version.

MapToApiVersion attribute is applied to the testmethod endpoint definitions with the same route, to make routing of the requests to the correct version possible. Note that the unchanged endpoint is unchanged between versions and so MapToApiVersion attribute is not applied to it.

Run and test both the versions of testmethod and unchanged API endpoints and make sure things are working properly like in the case of the Separate Controller implementation. If you face any challenge, you can compare your code with that of “Milestone 1\SameControllerClass” folder of the Git repository.

Versions In Separate Namespaces

Now let’s see a pretty nifty way of segregating different versions of API endpoints by namespace. Start by making a copy of the folder “Milestone 0” of the Git repository and execute Step 1 as earlier. Then in Step 2, add the following code in the ConfigureServices method in the Startup class.

services.AddApiVersioning(
options =>
{
// reporting api versions will return the headers
// "api-supported-versions" and "api-deprecated-versions"
options.ReportApiVersions = true;
})
.AddMvc(
options =>
{
// automatically applies an api version based on the name of
// the defining controller's namespace
options.Conventions.Add(new VersionByNamespaceConvention());
});

In Step 3, create two sub-folders V1 and V2 in the Controllers folder, move the file TestController.cs to the V1 folder, and add a replica of the file in the V2 folder as shown below.

Update the content of “V1\TestController.cs” to look like the following:

using Microsoft.AspNetCore.Mvc;

namespace ApiVersioningDemoInNet7.Controllers.V1
{
[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
[HttpGet, Route("testmethod")]
public string TestMethod()
{
return "Test method call successful";
}
}
}

Update the content of “V2\TestController.cs” to look like the following:

using Microsoft.AspNetCore.Mvc;

namespace ApiVersioningDemoInNet7.Controllers.V2
{
[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
[HttpGet, Route("testmethod")]
public string TestMethod()
{
return "Test method V2 call successful";
}
}
}

Note the absence of any versioning-related attributes in the controller classes. The API versioning library determines the version by the namespace of the controller classes, and this magic happens due to the configuration, options.Conventions.Add(new VersionByNamespaceConvention());, done in the Startup class in step 2.

Now go ahead and test if both versions of “testmethod” endpoint are working fine. The completed code can be found in the “Milestone 1/NamespaceConvention” folder of the code repository.

Versions configured through fluent syntax

We would now be exploring the last method of configuring versions dealt with in this article, i.e., defining the versions through fluent syntax. Start by making a copy of the folder “Milestone 0” of the Git repository and executing Step 1 as earlier.

In Step 2, modify the content of TestController class to the following:

using Microsoft.AspNetCore.Mvc;

namespace ApiVersioningDemoInNet7.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
[HttpGet, Route("testmethod")]
public string TestMethod()
{
return "Test method call successful";
}

[HttpGet, Route("testmethod")]
public string TestMethodV2()
{
return "Test method V2 call successful";
}
}
}

Please note that TestController class does not have anything related to versioning configuration.

In Step 3, we will do all the configuration in the Startup class using fluent syntax. Add a using statement using Asp.Versioning.Conventions; to the top of the Startup class and then add the following configuration in the ConfigureServices method. If you are following the article from the start the following configuration should be pretty self-explanatory.

services.AddApiVersioning(
options =>
{
// reporting api versions will return the headers
// "api-supported-versions" and "api-deprecated-versions"
options.ReportApiVersions = true;

})
.AddMvc(
options =>
{
options.Conventions.Controller<TestController>()
.HasApiVersion(1.0)
.HasApiVersion(2.0)
.Action(c => c.TestMethod()).MapToApiVersion(1.0)
.Action(c => c.TestMethodV2()).MapToApiVersion(2.0);

});

Test the application as usual. The completed code can be found in the “Milestone 1\FluentSyntax” folder of the code repository.

Different Versioning Schemes Supported By API Versioning Library

You have already seen how the version parameter is passed with the API requests, which is by default accepted and interpreted correctly by the versioning library, but other schemes require some configuration to be done. The API versioning library supports 4 schemes of versioning:

  • Query string
  • Header
  • MIME-type
  • URL segment

One can configure their API to work with one or more versioning schemes. In milestone 2, let us configure our application to accept all the schemes. Apart from it let us apply the knowledge we acquired in previous milestones. Overall, the following are our goals for milestone 2:

  1. Configure the application to route versioned calls to controllers by namespace.
  2. Redirect all the calls bereft of any version parameter to version 1.
  3. Enable all four types of versioning scheme

Let us start by making a copy of the content of “Milestone 1\NamespaceConvention” and modifying the service configuration in the Startup > ConfigureServices method to the following:

services.AddApiVersioning(
options =>
{
options.DefaultApiVersion = new ApiVersion(1.0); options.DefaultApiVersion = new ApiVersion(1.0);
options.AssumeDefaultVersionWhenUnspecified = true;

// reporting api versions will return the headers
// "api-supported-versions" and "api-deprecated-versions"
options.ReportApiVersions = true;
options.ApiVersionReader = ApiVersionReader.Combine(
new QueryStringApiVersionReader("api-version"),
new HeaderApiVersionReader("X-Version"),
new MediaTypeApiVersionReader("x-version"));
})
.AddMvc(
options =>
{
// automatically applies an api version based on the name of
// the defining controller's namespace
options.Conventions.Add(new VersionByNamespaceConvention());
});

In the above code, we configured the application to accept Query string, Header, and MIME type schemes. Note that each of the Reader types takes a string parameter using which we can specify the name of the parameter to be passed with the requests.

To enable the URL segment scheme also, you would need to add an extra route to all the controller classes as shown below:

[Route("api/[controller]")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
[HttpGet, Route("testmethod")]
public string TestMethod()
{
return "Test method call successful";
}
}

With the above configuration, we have accomplished all the 3 goals mentioned above. It is time to test the application using Postman.

The screenshot below shows how to pass the version parameter with the request's header. Note that the parameter name “X-Version” is the same as the parameter passed to HeaderApiVersionReader in Startup > ConfigureServices method.

Similarly, the below screenshot shows how the version parameter can be passed as a MIME type as part of the Accept header.

Similarly, the below screenshot shows how the version parameter can be passed with the request URL. Please note, testing of different versioning schemes should be done mutually exclusively. For example, before testing the URL segment scheme, if we passed any version parameter with header or as MIME type within Accept header for earlier testing, they should be disabled. Note the existence of the version identifier within the request URL.

Similarly, the below screenshot shows how the version parameter can be passed as a query string parameter.

As per the configuration, options.DefaultApiVersion = new ApiVersion(1.0); options.DefaultApiVersion = new ApiVersion(1.0);
options.AssumeDefaultVersionWhenUnspecified = true;
if we do not pass any version parameter with the request by means of any of the four schemes discussed above, then by default version 1 of the requested endpoint should get invoked. The below screenshot shows how to test that.

Please test all possible scenarios to ensure that the test API meets all three goals we intended. The completed code can be found in the Milestone 2 folder of the code repository.

Wrapping Up

Awesome! In this article, you have learned different ways of organizing and configuring versions, and you also saw four different schemes of versioning supported by the versioning library.

Now it is time to document the API neatly using Open API libraries to help client app developers to work efficiently with it. That’s the topic I have covered in the next article. You can find it here.

--

--