Back to: ASP.NET Core Web API Tutorials
ASP.NET Core Web API Versioning using Query String
In this article, I will discuss how to Implement Web API Versioning using Query String in ASP.NET Core Web API Applications. Please read our previous article discussing What ASP.NET Core Web API Versioning is.
What is Query String Web API Versioning?
Query String Web API Versioning is a method of managing different versions of an API by specifying the API’s version number in the query string of the URL request. This approach is commonly used when an API is developed, and multiple versions of that API need to be maintained simultaneously to support different clients or ensure backward compatibility.
How Query String Web API Versioning Works?
The version of the API the client wishes to use is specified as a parameter in the query string of the URL. For example, https://api.example.com/products?version=1.0 would access version 1.0 of the products API endpoint.
The server-side logic parses the query string to determine the API version requested by the client. Based on this version information, the server decides which version of the code to execute. This method allows clients to explicitly request the version they need, making it clear which version of the API is being used.
How Do We Implement Query String Versioning in ASP.NET Core Web API?
Implementing Query String API Versioning in ASP.NET Core Web API is straightforward compared to other methods like URL Path or Header Versioning. It doesn’t require changes to the URL structure (except the query parameter) and can be easily parsed and routed on the server.
Let us proceed with the step-by-step implementation of the Query String API Versioning in ASP.NET Core Web API. Create a new ASP.NET Core Web API Project named QueryStringAPIVersioning.
Install the Necessary NuGet Package
Once the ASP.NET Core Web API Project is created, To implement Query String API Versioning in ASP.NET Core Web API, we need to install the Microsoft.AspNetCore.Mvc.Versioning package. This can be done via the NuGet Package Manager in Visual Studio or using the following command in the Package Manager Console:
Install-Package Microsoft.AspNetCore.Mvc.Versioning
Configure API Versioning
Next, we need to configure the services to add API versioning with the query string parameter in our Program.cs file, So please add the following code to the Program.cs class file:
// Add API versioning builder.Services.AddApiVersioning(options => { // Specify the default API version options.DefaultApiVersion = new ApiVersion(1, 0); // If the client does not specify a version, use the default version options.AssumeDefaultVersionWhenUnspecified = true; // Advertise the API versions supported for the particular endpoint options.ReportApiVersions = true; // Read the version number from the query string parameter "version" options.ApiVersionReader = new QueryStringApiVersionReader("version"); });
options.DefaultApiVersion = new ApiVersion(1, 0):
This setting specifies the default API version that the server will use if the client does not specify a version in their request. In this case, the default version is set to 1.0, represented by the new ApiVersion(1, 0), where 1 is the major version, and 0 is the minor version. The API versioning system in ASP.NET Core can handle major, minor, and even more granular versions if needed.
options.AssumeDefaultVersionWhenUnspecified = true:
This option controls what happens when a client makes a request without specifying an API version. By setting this to true, it ensures that if a version is not specified in the request, the API will default to using the DefaultApiVersion specified earlier. If this is set to false, the API might return an error when no version is specified.
options.ReportApiVersions = true:
This setting determines whether the API responses should include headers that indicate the versions of the API that are supported. When enabled (set to true), ASP.NET Core will include headers in API responses that list the available versions of the API. This is useful for clients to understand version deprecations, upgrades, or other changes without referring to documentation.
options.ApiVersionReader = new QueryStringApiVersionReader(“version”);
This option specifies how the API version is read from the client’s request. Here, the version is expected to be provided in the query string of the request URL. The parameter name expected is “version”. For example, a client could specify the version by appending ?version=2.0 to the URL. The QueryStringApiVersionReader is a built-in reader that looks for the version in the query string.
Define the Product Model
To demonstrate how to handle different versions of an API, let’s define a product model and then use some hardcoded data to return from the action methods in different versions. So, create a class file named Product.cs within the Models folder and then copy and paste the following code.
namespace QueryStringAPIVersioning.Models { public class Product { public int Id { get; set; } public string Name { get; set; } public double Price { get; set; } public string Category { get; set; } } }
Define API Versions in Controllers
You can now define different versions for your API methods using the ApiVersion attribute. So, create the Products Controller within the Controllers folder and copy and paste the following code. As you can see in the below code, using the [ApiVersion] attribute, we specify the version number they support.
using Microsoft.AspNetCore.Mvc; using QueryStringAPIVersioning.Models; namespace QueryStringAPIVersioning.Controllers { [Route("api/[controller]")] [ApiController] public class ProductsController : ControllerBase { [HttpGet] [ApiVersion("1.0")] public IEnumerable<Product> GetV1() { // Hardcoded list of products for version 1.0 return new List<Product> { new Product { Id = 1, Name = "Apple", Price = 1.50, Category = "Fruit" }, new Product { Id = 2, Name = "Bread", Price = 2.25, Category = "Bakery" } }; } [HttpGet] [ApiVersion("1.0")] [Route("{Id}")] public Product GetByIdV1(int Id) { return new Product { Id = Id, Name = "Apple", Price = 1.50, Category = "Fruit" }; } [HttpGet] [ApiVersion("2.0")] public IEnumerable<Product> GetV2() { // Hardcoded list of products for version 2.0, including additional fields return new List<Product> { new Product { Id = 1, Name = "Apple", Price = 1.50, Category = "Fruit" }, new Product { Id = 2, Name = "Bread", Price = 2.25, Category = "Bakery" }, new Product { Id = 3, Name = "Carrot", Price = 0.75, Category = "Vegetable" } }; } } }
Now, run the application and access the two versions of the endpoint. It should work as expected.
Testing API Version 1.0:
Testing API Version 2.0:
Why is Swagger not Working with API Versioning?
When using API Versioning, Swagger/OpenAPI will not work by default for documenting APIs in ASP.NET Core Web API. This is because Swagger needs to identify each endpoint uniquely, and by default, it does not differentiate endpoints that have the same route but are distinguished only by different attributes like [ApiVersion].
In our example, both GetV1() and GetV2() methods share the same HTTP method (GET) and path (api/Products), differing only by their API version. Swagger, by default, sees them as conflicting because it doesn’t account for the version difference.
To resolve this issue, we need to configure Swagger to understand and differentiate between different API versions. In the Program.cs class file where we configure services, we need to set up Swagger to generate separate documents for each API version. So, modify the Program class as follows:
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Versioning; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; using System.Reflection; namespace QueryStringAPIVersioning { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); // Add API versioning builder.Services.AddApiVersioning(options => { // Specify the default API version options.DefaultApiVersion = new ApiVersion(1, 0); // If the client does not specify a version, use the default version options.AssumeDefaultVersionWhenUnspecified = true; // Advertise the API versions supported for the particular endpoint options.ReportApiVersions = true; // Read the version number from the query string parameter "version" options.ApiVersionReader = new QueryStringApiVersionReader("version"); }); // Add Swagger generation builder.Services.AddSwaggerGen(options => { // Defines two Swagger documents, one for API version 1.0 and another for version 2.0. // Each document includes metadata like the title and version. // These documents will be accessible in Swagger UI, // allowing users to view and interact with the different versions of the API // Define a Swagger document for API version 1.0 options.SwaggerDoc("1.0", new OpenApiInfo {Title = "My API", Version = "1.0" }); // Define a Swagger document for API version 2.0 options.SwaggerDoc("2.0", new OpenApiInfo { Title = "My API", Version = "2.0" }); // Resolving Conflicts // This resolves conflicts when multiple actions match the same route and HTTP method // by selecting the first action encountered options.ResolveConflictingActions(apiDescriptions => apiDescriptions.First()); // Doc Inclusion Predicate // Define a predicate function to determine which API Endpoints should be included // in the Swagger documentation based on the version options.DocInclusionPredicate((version, apiDesc) => { // Attempt to get the MethodInfo for the API Endpoint if (!apiDesc.TryGetMethodInfo(out MethodInfo method)) return false; //If the MethodInfo for the API description cannot be retrieved, the endpoint is excluded. // Extracts the versions specified by [ApiVersion] attributes at the method level. // Ensures that method-level versioning is considered when deciding which endpoints to include in the Swagger documentation var methodVersions = method.GetCustomAttributes(true) .OfType<ApiVersionAttribute>() .SelectMany(attr => attr.Versions); // Extracts the versions specified by [ApiVersion] attributes at the controller level. // Ensures that controller-level versioning is considered, allowing for endpoints that might be versioned at the controller level. var controllerVersions = method.DeclaringType? .GetCustomAttributes(true) .OfType<ApiVersionAttribute>() .SelectMany(attr => attr.Versions); // Combining Versions // Combines the versions extracted from both the method and controller levels to create a unified set of versions. // Ensures that all relevant versions are considered, avoiding duplication by using Distinct() var allVersions = methodVersions.Union(controllerVersions).Distinct(); // Checks if any of the combined versions match the version specified in the Swagger document // This determines if the API Endpoint should be included in the current Swagger document based on the version. return allVersions.Any(v => v.ToString() == version); }); }); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { // Enable middleware to serve generated Swagger as a JSON endpoint app.UseSwagger(); // Enable middleware to serve Swagger UI app.UseSwaggerUI(options => { // Define the Swagger endpoints for each version options.SwaggerEndpoint("/swagger/1.0/swagger.json", "My API V1"); options.SwaggerEndpoint("/swagger/2.0/swagger.json", "My API V2"); }); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run(); } } }
AddSwaggerGen Service
The AddSwaggerGen service is used to configure and generate the Swagger documentation for our API. This service registers Swagger generation within the ASP.NET Core dependency injection container, allowing the application to generate OpenAPI/Swagger documents automatically. The generated documents provide a comprehensive description of the API, including endpoints, input parameters, and output results. This setup is essential for tools like Swagger UI to render interactive API documentation.
UseSwaggerUI Middleware
The UseSwaggerUI middleware component is used to serve the Swagger UI, which provides an interactive web interface for exploring and testing your API endpoints. This middleware sets up the Swagger UI at a specified endpoint, allowing developers and consumers to view and interact with the API documentation generated by AddSwaggerGen. It makes it easy to understand and use the API by providing a visual representation of the API’s capabilities and allowing direct interaction with the API through the browser.
Applying ApiVersion Attribute at the Controller level:
Now, let us modify the ProductsController.cs class file as follows. Here, you can see we have two controllers, and each controller is decorated with ApiVersion Attribute.
using Microsoft.AspNetCore.Mvc; using QueryStringAPIVersioning.Models; namespace QueryStringAPIVersioning.Controllers { [Route("api/products")] [ApiController] [ApiVersion("1.0")] public class ProductsV1Controller : ControllerBase { [HttpGet] public IEnumerable<Product> GetProducts() { // Hardcoded list of products for version 1.0 return new List<Product> { new Product { Id = 1, Name = "Apple", Price = 1.50, Category = "Fruit" }, new Product { Id = 2, Name = "Bread", Price = 2.25, Category = "Bakery" } }; } [HttpGet] [ApiVersion("1.0")] [Route("{Id}")] public Product GetGetProductsById(int Id) { return new Product { Id = Id, Name = "Apple", Price = 1.50, Category = "Fruit" }; } } [Route("api/products")] [ApiController] [ApiVersion("2.0")] public class ProductsV2Controller : ControllerBase { [HttpGet] [ApiVersion("2.0")] public IEnumerable<Product> GetProducts() { // Hardcoded list of products for version 2.0, including additional fields return new List<Product> { new Product { Id = 1, Name = "Apple", Price = 1.50, Category = "Fruit" }, new Product { Id = 2, Name = "Bread", Price = 2.25, Category = "Bakery" }, new Product { Id = 3, Name = "Carrot", Price = 0.75, Category = "Vegetable" } }; } } }
Advantages of Separate Controllers for Each Version:
- Each version of the API is completely isolated from others. This means that changes in one version do not affect the others. For example, if you need to introduce changes in v2, it won’t impact v1 as they are in separate controllers.
- Having separate controllers clearly separates code for different versions, making it easier to understand. For example, developers can easily find the code relevant to a specific version by looking at the controller dedicated to that version.
- Each version can be tested independently, reducing the complexity of test cases and potential interdependencies. For example, unit tests for v1 are completely separate from unit tests for v2, making it easier to pinpoint issues.
Disadvantages of Separate Controllers for Each Version:
- Common logic might need to be duplicated across different versions, leading to code redundancy. For example, if both v1 and v2 need to validate user input in the same way, the validation logic needs to be duplicated in both controllers.
- More controllers to manage and maintain, especially as the number of versions increases. For example, if you have five versions of an API, you’ll have five controllers to update, document, and test.
- Having multiple controllers can lead to a larger project structure, which can be harder to manage in the long term. For example, a large number of controllers can make the project structure more complex and harder to navigate.
When to Use Separate Controllers for Each Version:
Best for Projects with significant changes between versions, clear separation of concerns, and where version isolation is a priority. It increases duplication and maintenance overhead but provides isolation and clarity.
Advantages of Single Controller with Versioned Methods:
- Shared logic can be easily reused across different versions, reducing duplication. For example, common validation logic can be implemented once and used in methods for different versions.
- Fewer controllers to manage, which can make the project structure simpler and easier to navigate. For example, A single controller handling multiple versions reduces the number of files and classes in the project.
- Refactoring shared logic is easier because it’s centralized in one place. For example, changing a piece of common logic only needs to be done in one place rather than multiple controllers.
- All related business logic is consolidated within a single controller, making it easier to understand the overall flow. For example, developers can see all the different versions of an endpoint in one place, which can help them understand how the API has evolved.
Disadvantages of Single Controller with Versioned Methods:
- The controller can become complex and harder to manage as more versions are added. For example, having multiple versions of methods in a single controller can make the controller difficult to maintain.
- If not handled carefully, changes to one version might affect other versions. For example, a change in the shared logic might have side effects on multiple versions if not properly isolated.
When to Use Single Controller with Versioned Methods:
Best for Projects with minor changes between versions, where code reuse and simplicity are prioritized. It increases complexity within the controller and the risk of unintended interactions between versions but reduces duplication and simplifies project structure.
In the next article, I will discuss how to implement Web API Versioning using URL Path in ASP.NET Core with Examples. In this article, I explain How to Implement ASP.NET Core Web API Versioning using Query String. I hope you enjoy this ASP.NET Core Web API Versioning using Query String article.