Back to: ASP.NET Core Web API Tutorials
ASP.NET Core Web API Versioning with Examples
In this article, I will discuss ASP.NET Core Web API Versioning with Examples. ASP.NET Core Web API Versioning is a way to serve different versions of our web API, allowing clients to specify which version they want to use, thereby enabling smooth evolution and backward compatibility of our API.
What is ASP.NET Core Web API Versioning?
API Versioning in ASP.NET Core Web API is a technique for managing multiple versions of an API, allowing clients to continue using older versions while new versions are developed and deployed. When we create APIs, clients, such as mobile apps, web apps, or other systems, start using them. Over time, requirements change, bugs are fixed, or new features are added. Existing clients might break if we change or remove something in our API. API versioning allows us to introduce changes without breaking existing client applications that rely on older API versions. In ASP.NET Core, API versioning is a systematic approach that enables us to:
- Publish new versions of APIs (e.g., v1, v2) while keeping older versions operational.
- Deprecate or remove old versions when it’s safe.
- Maintain backward compatibility and reduce the risk of breaking existing clients.
Why Do We Need Web API Versioning?
When You Deploy a Web API, Multiple Clients Start Using It: Once you create and release a Web API service, many different client applications, like mobile apps, web apps, third-party integrations, and desktop applications, begin consuming that API. Each client is usually built with a specific understanding of the API’s structure, including endpoints, request formats, and response data.
Business Growth Brings Changing Requirements: As the business evolves, the needs for the API change too. You might want to add new features, enhance existing functionality, or fix bugs. Sometimes, these changes involve altering the data returned by the API, changing how requests are made, or modifying the behavior of endpoints.
The Challenge: How to Change APIs Without Breaking Existing Clients?
If you change the API in a way that’s incompatible with how existing clients expect it to behave, those clients will break. For example, if you rename a field in the response JSON or remove a parameter, clients relying on the old API version will fail to parse the responses or submit requests correctly. This is where API versioning becomes crucial.
The Ideal Scenario: Keep Old Versions as IT IS, Introduce New Versions:
- To handle new requirements without breaking existing clients, we need to keep our API’s old version(s) as it is. These continue to serve the clients that depend on them without breaking.
- At the same time, you create a new version of the API with all the new changes and improvements. New clients, or those who want to upgrade, can start using this new version.
So, Web API Versioning is required for several reasons. They are as follows:
Backward Compatibility
Clients built on older API versions continue to work without breaking, even as you introduce new versions with added features or changes. Imagine a mobile app built last year using version 1.0 of your API. If you suddenly change the API’s contract (like removing or renaming fields) without versioning, the app will crash or behave unpredictably. API versioning allows us to:
- Keep the older versions running unchanged.
- Let developers add new functionality or fix bugs in new versions without worrying about breaking old clients.
Avoid Breaking Changes
Any change to the API that would break existing clients is called a breaking change. Examples of breaking changes include:
- Renaming or removing fields in the response data.
- Changing required request parameters.
- Changing URL paths or HTTP methods.
Without versioning, these changes would immediately break all existing clients. With versioning, we isolate changes to the new API version, leaving older versions unchanged.
Smooth Migration
Clients can upgrade from an old version to a new version at their own pace rather than being forced to adapt to changes immediately. It is essential for the following reasons:
- Large client applications or many third-party consumers might need time to test and update their code.
- Immediate forced changes could cause downtime or bugs on the client side.
API versioning allows controlled migration, minimizing risk and providing a better developer experience.
Multiple Client Support
Different clients might have different version requirements simultaneously. For example:
- Your web app uses the latest API version with all new features.
- An older mobile app still relies on an older API version because it hasn’t been updated.
- Third-party partners integrate with a specific stable API version for compliance or testing reasons.
API versioning supports running multiple versions of the API side-by-side, targeting different client needs without conflict.
What are the Different Options Available in ASP.NET Core Web API to maintain the API Versioning?
ASP.NET Core provides multiple ways to implement API versioning. The following are the common options:
- Query String Versioning: The version number is specified as a query parameter in the URL. Example: api/products?api-version=1.0
- URL Path Versioning: The version number is included directly in the URL path. Example: /api/v1/products
- Header Versioning: The version number is specified in a custom header, often called api-version. Example: Header: api-version: 1.0
- Media Type Versioning: Also known as content negotiation or accept header versioning, where the version number is included in the Accept header of the HTTP request. Example: Accept: application/json;v=1.0
Implement API Versioning in ASP.NET Core Web API:
Let’s proceed with implementing ASP.NET Core Web API Versioning. First, create a new ASP.NET Core Web API Project named APIVersioning. Once the Project is created, we need to install Microsoft.AspNetCore.Mvc.Versioning package. This can be done via the NuGet Package Manager Solution in Visual Studio or the following command in the Package Manager Console. This Package is required to implement API Versioning in ASP.NET Core Web API.
- Install-Package Microsoft.AspNetCore.Mvc.Versioning
Example: Product API with Multiple Versions
Let’s create a simple example with two API versions:
Two GET Methods:
- Version 1 (v1) returns basic product information, i.e., only the product ID and Name.
- Version 2 (v2) returns extended product information, including Id, Name, and the new Price property.
Two POST Methods:
- Version 1 (v1) accepts minimal product data in the request, only the Name of the product.
- Version 2 (v2) accepts both Name and Price in the request, reflecting the enhanced product model.
Define the Product Model
First, create a folder named Models in the project root directory. Then, inside the Models folder, create a class file named Product.cs and copy and paste the following code.
namespace APIVersioning.Models { public class Product { public int Id { get; set; } public string Name { get; set; } // Added in version 2 public double? Price { get; set; } } }
Here,
- The Product class is the base model representing a product entity.
- The Price property is made nullable (double?) because it only applies to API version 2 and later.
- This design allows older API versions to work without Price while newer versions can use it.
Define DTOs for Requests and Responses
DTOs define the shape of data exchanged between client and server for requests and responses, enabling API versions to have different contracts.
Request DTOs:
- ProductCreateRequestV1: Only requires Name.
- ProductCreateRequestV2: Requires both Name and Price.
Response DTOs:
- ProductResponseV1: Contains ID and Name only.
- ProductResponseV2: Contains Id, Name, and Price.
First, create a folder named DTOs in the project root directory where we will create all our DTOs. You can create a separate class file for each data transfer object (DTO). However, we are creating a single class file for all data transfer objects (DTOs). So, create a class file named ProductDTO.cs within the DTOs folder and copy and paste the following code.
namespace APIVersioning.DTOs { // Request DTO for creating a product (v1) public class ProductCreateRequestV1 { public string Name { get; set; } = null!; } // Response DTO for product info (v1) public class ProductResponseV1 { public int Id { get; set; } public string Name { get; set; } = null!; } // Request DTO for creating a product (v2) public class ProductCreateRequestV2 { public string Name { get; set; } = null!; public double Price { get; set; } } // Response DTO for product info (v2) public class ProductResponseV2 { public int Id { get; set; } public string Name { get; set; } = null!; public double Price { get; set; } } }
Code Explanations:
- By having separate DTOs per version, the API can evolve safely without breaking existing clients.
- New fields added in newer versions are not included in older version DTOs.
Implementing Query String Versioning in ASP.NET Core Web API
Query String Versioning is a method of API versioning where the version information is passed as a parameter in the URL’s query string. Instead of changing the route or adding a header, clients specify the desired version by including something like ?api-version=1.0 in the URL. For example:
- GET /api/products?api-version=1.0
- GET /api/products?api-version=2.0
The API version is indicated explicitly via the query string parameter api-version. This approach makes it simple for clients to specify which version they want to call. It is easy to test with browsers, as you only need to modify the URL query string. However, it is considered less RESTful because the version is not part of the resource path.
How Does Query String Web API Versioning Work?
When the client makes a request, the ASP.NET Core Web API looks for the version parameter in the query string (e.g., api-version). Based on this parameter’s value, the API framework determines which controller or action to route the request to. The API internally maps the version requested by the client to the correct controller implementation or logic handling that particular version.
- If the client requests api-version=1.0, the API serves version 1 endpoints.
- If the client requests api-version=2.0, the API serves version 2 endpoints.
- If the version is not specified, the API can be configured to use a default version or return an error.
This approach allows multiple API versions to coexist on the same route, differentiated only by the version query string.
Configuring Query String Versioning in Program Class
Let us start with Query String Versioning. So, please modify the Program.cs class file as follows. The following code is self-explained, so please read the comment lines for a better understanding.
using Microsoft.AspNetCore.Mvc.Versioning; using Microsoft.AspNetCore.Mvc; namespace APIVersioning { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add controllers with JSON options (disable camelCase) builder.Services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = null; }); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // Add API versioning services to DI builder.Services.AddApiVersioning(options => { // This allows the API to use a default version if the client does not specify one. options.AssumeDefaultVersionWhenUnspecified = true; // This sets the default version (here, 1.0). options.DefaultApiVersion = new ApiVersion(1, 0); // This adds headers in the response informing clients about all the supported API versions. options.ReportApiVersions = true; // This tells the framework to use the query string parameter api-version for versioning options.ApiVersionReader = new QueryStringApiVersionReader("api-version"); }); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run(); } } }
Code Explanations:
The following code configures API Versioning:
The AddApiVersioning service registered the Versioning services to the dependency injection container and enabled support for API Versioning. Let’s break down the above code statement line by line.
options.AssumeDefaultVersionWhenUnspecified = true:
- This setting defines what happens if a client request does not specify any API version.
- Setting it to true means the server will automatically use the default API version defined by DefaultApiVersion when no version is provided in the request.
- If set to false, the API rejects requests without a version, typically returning an HTTP 400 Bad Request error that indicates the version is missing.
- This setting enhances backward compatibility and ensures that clients don’t always have to specify the version explicitly, thereby reducing accidental errors.
options.DefaultApiVersion = new ApiVersion(1, 0):
- This defines the default version your API will use when none is specified in the request (and AssumeDefaultVersionWhenUnspecified is true).
- new ApiVersion(1, 0) means your default API version is major version 1, minor version 0.
- ASP.NET Core API versioning supports specifying major, minor, and patch versions, providing flexibility to version APIs granularly.
- Example: new ApiVersion(2, 1) would represent version 2.1 of your API.
options.ReportApiVersions = true:
Enabling this option means the API will include special HTTP headers in responses, informing clients about the supported and deprecated API versions. Specifically, ASP.NET Core adds headers like:
- api-supported-versions: Lists the API versions your service currently supports.
- api-deprecated-versions: Lists versions marked as deprecated.
This is very helpful for client developers, letting them programmatically detect which versions are available or due for retirement without constantly checking external documentation. This header-driven approach enhances transparency and facilitates effective management of the API lifecycle.
options.ApiVersionReader = new QueryStringApiVersionReader(“api-version”);
- This configures how the API version is extracted from incoming HTTP requests.
- QueryStringApiVersionReader instructs the versioning system to look for the version value in the URL query string parameter.
- The parameter name is “api-version”, meaning clients specify the version like this: /api/products?api-version=2.0
- The versioning system reads this api-version query parameter and routes the request to the appropriate API version implementation.
- Query string versioning is explicit and easy to test, making it a popular choice for many APIs.
Define API Versions in Controllers
Now, we can define different versions for our API methods using the ApiVersion attribute. So, create an Empty API Controller named ProductsController within the Controllers folder and then copy and paste the following code. The [ApiVersion(“1.0”)] and [ApiVersion(“2.0”)] are applied to methods that map each action to a specific API version. The Static list _products simulates an in-memory database, storing products with full info (Id, Name, Price).
using APIVersioning.DTOs; using APIVersioning.Models; using Microsoft.AspNetCore.Mvc; namespace APIVersioning.Controllers { [ApiController] [Route("api/products")] public class ProductsController : ControllerBase { // Simulating a data store with static list private static readonly List<Product> _products = new List<Product>() { new Product { Id = 1, Name = "Laptop", Price = 1200 }, new Product { Id = 2, Name = "Smartphone", Price = 700 } }; // GET Version 1: Return Id and Name only [HttpGet] [ApiVersion("1.0")] public IActionResult GetV1() { var productsV1 = _products.Select(p => new ProductResponseV1 { Id = p.Id, Name = p.Name }); return Ok(productsV1); } // GET Version 2: Return Id, Name, and Price [HttpGet] [ApiVersion("2.0")] public IActionResult GetV2() { var productsV2 = _products.Select(p => new ProductResponseV2 { Id = p.Id, Name = p.Name, Price = p.Price ?? 0 }); return Ok(productsV2); } // POST Version 1: Accept Name only, create product without price [HttpPost] [ApiVersion("1.0")] public IActionResult PostV1([FromBody] ProductCreateRequestV1 request) { if (string.IsNullOrWhiteSpace(request.Name)) return BadRequest("Name is required."); int newId = _products.Max(p => p.Id) + 1; var newProduct = new Product { Id = newId, Name = request.Name, Price = 0 // Price not supported in v1, default 0 }; _products.Add(newProduct); // Return version 1 response DTO (without price) var response = new ProductResponseV1 { Id = newProduct.Id, Name = newProduct.Name }; return Ok(response); } // POST Version 2: Accept Name and Price, create full product [HttpPost] [ApiVersion("2.0")] public IActionResult PostV2([FromBody] ProductCreateRequestV2 request) { if (string.IsNullOrWhiteSpace(request.Name)) return BadRequest("Name is required."); if (request.Price <= 0) return BadRequest("Price must be greater than zero."); int newId = _products.Max(p => p.Id) + 1; var newProduct = new Product { Id = newId, Name = request.Name, Price = request.Price }; _products.Add(newProduct); return Ok(newProduct); } } }
Code Explanations:
- GET v1 (GetV1): Returns a list of products with basic information (no price). This simulates the initial version of the API.
- GET v2 (GetV2): Returns full product details, including the price.
- POST v1 (PostV1): This allows the creation of a product with only a name. Price is not supported here and is set to 0 by default internally.
- POST v2 (PostV2): Allows product creation with name and price. Validates that the price is positive.
Testing API Endpoint using Postman:
GET Version 1
URL: GET /api/products?api-version=1.0
Response: The server responds with a list of products containing only ID and Name.
[
{“Id”:1,”Name”:”Laptop”},
{“Id”:2,”Name”:”Smartphone”}
]
GET Version 2
URL: GET /api/products?api-version=2.0
Response: The server responds with full product info, including Price.
[
{“Id”:1,”Name”:”Laptop”,”Price”:1200},
{“Id”:2,”Name”:”Smartphone”,”Price”:700}
]
POST Version 1 (Create product with Name only)
Request:
URL: POST /api/products?api-version=1.0
Content-Type: application/json
{
“Name”: “Tablet”
}
Response: The server adds a product with Price defaulted to 0 and returns the response without Price.
{“Id”:3,”Name”:”Tablet”}
POST Version 2 (Create product with Name and Price)
Request:
URL: POST /api/products?api-version=2.0
Content-Type: application/json
{
“Name”: “Monitor”,
“Price”: 350.0
}
Response: The server validates and adds the product with the given price, returning full info.
{“Id”:4,”Name”:”Monitor”,”Price”:350}
Why Is Swagger Not Working with API Versioning?
When we enable API Versioning in ASP.NET Core Web API, we often end up with multiple controller actions sharing the same HTTP method (GET, POST, etc.) and the same route template (e.g., /api/products), but differing only by the [ApiVersion] attribute specifying the API version they belong to.
Swagger (via Swashbuckle) by default does not understand these versioning attributes and treats these endpoints as duplicates because:
- It looks at the HTTP method + route combination to identify endpoints.
- Since both v1 and v2 methods have identical HTTP methods and routes, Swagger sees this as a conflict or duplicate endpoint.
- This leads to errors. For example, GetV1() and GetV2() respond to GET api/products but differ by version. Swagger sees this as the same endpoint twice.
How to Fix It: Configure Swagger to Support API Versioning
You must explicitly tell Swagger how to separate and document your different API versions by:
- Creating separate Swagger documents for each API version, e.g., one for v1.0 and another for v2.0.
- Filtering endpoints included in each Swagger doc based on their API version attributes, so v1 endpoints appear only in the v1 doc, and v2 endpoints only in the v2 doc.
Let us see the Step-by-Step approach to resolve this issue in our Program.cs class file.
Step-1: Define Swagger Documents Per API Version:
The following code registers two Swagger JSON documents, one for each API version. Each document will only contain the endpoints for its respective version. In the Swagger UI, users can select the API version documentation they want to view.
Step-2: Resolve Conflicts Between Identical Routes
When Swagger finds multiple actions with the same HTTP method and route, it must decide which one to use. The following code instructs Swagger to pick the first action found and ignore the rest, preventing errors. This is a quick way to avoid route conflicts during Swagger doc generation.
Step-3: Filter Endpoints Based on API Version Attributes:
The DocInclusionPredicate is a filter function that determines which endpoints are included in which Swagger document. It receives:
- version: The version string of the current Swagger doc (e.g., “1.0” or “2.0”).
- apiDesc: Metadata describing a single API endpoint.
It attempts to find the .NET MethodInfo for the endpoint. It collects [ApiVersion] attributes from both:
- The action method itself.
- The controller class to which the method belongs.
Combines all versions specified on the method and controller. Returns true only if the endpoint supports the version currently being generated for Swagger. This ensures that:
- Endpoints for v1 are only listed in the Swagger document for version 1.0.
- Endpoints for v2 are only listed in the Swagger document for version 2.0.
Step-4: Serve Swagger JSON and UI
The UseSwagger() middleware component exposes the Swagger JSON files at /swagger/1.0/swagger.json and /swagger/2.0/swagger.json. The UseSwaggerUI() middleware component hosts the interactive Swagger UI webpage, which reads those JSON files. Each Swagger endpoint added here creates a dropdown or tab in the UI, allowing users to visually switch between API versions.
Modifying The Program Class:
So, please modify the Program class as follows:
using Microsoft.AspNetCore.Mvc.Versioning; using Microsoft.AspNetCore.Mvc; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; using System.Reflection; namespace APIVersioning { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add controllers with JSON options (disable camelCase) builder.Services.AddControllers() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = null; }); // Add API versioning configuration builder.Services.AddApiVersioning(options => { // This allows the API to use a default version if the client does not specify one. options.AssumeDefaultVersionWhenUnspecified = true; // This sets the default version (here, 1.0). options.DefaultApiVersion = new ApiVersion(1, 0); // This adds headers in the response informing clients about all the supported API versions. options.ReportApiVersions = true; // This tells the framework to use the query string parameter api-version for versioning options.ApiVersionReader = new QueryStringApiVersionReader("api-version"); }); // Add Swagger generation and configure multiple version docs builder.Services.AddSwaggerGen(options => { // Define a Swagger document for API version 1.0 // This generates a separate swagger.json with metadata for version 1.0 endpoints options.SwaggerDoc("1.0", new OpenApiInfo { Title = "API Version", // Display title of the API Version = "1.0" // The API version string shown in Swagger UI }); // Define a Swagger document for API version 2.0 // Similarly, this creates swagger.json for version 2.0 endpoints options.SwaggerDoc("2.0", new OpenApiInfo { Title = "API Version", Version = "2.0" }); // In case of route conflicts (when multiple actions match the same path and HTTP method), // this resolves the conflict by selecting the first matching action. // This prevents Swagger generation errors caused by duplicate routes. options.ResolveConflictingActions(apiDescriptions => apiDescriptions.First()); // DocInclusionPredicate controls which endpoints get included in each Swagger document. // It receives the document version (string) and the ApiDescription for an endpoint. // Return true to include the endpoint in the current document, false to exclude it. options.DocInclusionPredicate((version, apiDesc) => { // Try to get MethodInfo for the endpoint, if unavailable exclude it from docs. if (!apiDesc.TryGetMethodInfo(out MethodInfo method)) return false; // Extract ApiVersion attributes applied on the action method. var methodVersions = method.GetCustomAttributes(true) .OfType<ApiVersionAttribute>() // Only consider [ApiVersion] attributes .SelectMany(attr => attr.Versions); // Extract all versions declared // Extract ApiVersion attributes applied on the controller class. var controllerVersions = method.DeclaringType? .GetCustomAttributes(true) .OfType<ApiVersionAttribute>() .SelectMany(attr => attr.Versions) ?? Enumerable.Empty<ApiVersion>(); // Combine versions declared at both method and controller levels, // to get the full set of API versions this endpoint supports. var allVersions = methodVersions.Union(controllerVersions).Distinct(); // Only include this endpoint if any of its versions match the current Swagger doc version // This ensures that only endpoints with a matching [ApiVersion] appear in each doc return allVersions.Any(v => v.ToString() == version); }); }); var app = builder.Build(); // Configure middleware for development environment if (app.Environment.IsDevelopment()) { // Enable middleware to serve the generated Swagger JSON documents as endpoints. // This middleware makes the swagger.json files (for all versions) available under /swagger/{version}/swagger.json app.UseSwagger(); // Enable middleware to serve Swagger UI, the interactive web page where users can explore and test API endpoints. // Swagger UI needs to know about all versioned Swagger JSON endpoints to show them as separate selectable versions. app.UseSwaggerUI(options => { // Add a Swagger endpoint for version 1.0 // This links the UI to the generated swagger.json for API Version 1.0 options.SwaggerEndpoint("/swagger/1.0/swagger.json", "API Version 1.0"); // Add a Swagger endpoint for version 2.0 // This links the UI to the generated swagger.json for API Version 2.0 options.SwaggerEndpoint("/swagger/2.0/swagger.json", "API Version 2.0"); }); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run(); } } }
What is ApiDescription (apiDesc)?
The ApiDescription class represents metadata about an individual API endpoint (action method) in our ASP.NET Core application. Swagger uses ApiDescription objects to build its documentation. It describes things like:
- The HTTP method (GET, POST, etc.)
- The route template (URL pattern)
- The controller and action method info
- The supported API versions (if versioning is enabled)
- Parameters, request body, response types, etc.
When Should We Use ASP.NET Core Web API Query String Versioning?
We need to use Query String Versioning in ASP.NET Core Web API applications in the following scenarios:
When you want to keep URLs clean and avoid embedding version info directly into the path:
- Some developers prefer not to include version information in the URL path, such as /api/v1/products. Instead, the base URL stays the same, e.g., /api/products, and versioning is handled separately via query parameters.
- This approach can help maintain consistent endpoint paths, making URLs simpler and easier for users and clients to read and remember.
For quick testing or debugging, as query parameters can be easily changed in browsers or tools like Postman:
- Query strings are very convenient for manual testing and debugging because you can simply add or modify them in the browser’s address bar or in API testing tools like Postman without changing the route.
- For example, simply appending ?api-version=2.0 to the URL instantly switches to a different API version, eliminating the need to remember different endpoints or paths.
- This ease of changing versions makes it very developer-friendly during development and troubleshooting.
When backward compatibility is important, and you want to support multiple API versions on the same route base
- Query string versioning allows multiple versions of the API to coexist at the same route URL. Clients can specify which version they want without affecting the base route structure.
- This is helpful when supporting legacy clients who rely on older API versions, while newer clients use updated versions, all on the same endpoint. It promotes backward compatibility without the need to create and maintain multiple URL structures.
In development or internal APIs, where simplicity and flexibility in specifying versions are more important than strict RESTful URL design
- In internal projects or during rapid development, strict RESTful conventions (such as embedding versions in URLs or using media types) may be less critical.
- Query string versioning offers a flexible and straightforward approach, enabling developers to iterate quickly and switch versions without modifying routes or headers.
- It allows internal consumers to easily test or switch versions without complexity, speeding up development and integration cycles.
So, Query String Versioning in ASP.NET Core Web API is easy to set up and ideal for internal and straightforward scenarios.
Implementing URL Path Versioning in ASP.NET Core Web API
URL Path Versioning is an API versioning approach where the version information is included directly in the route path. Instead of specifying the version as a query string or header, the client includes the version in the URL:
- GET /api/v1/products (for version 1.0)
- GET /api/v2/products (for version 2.0)
This approach is widely used for public APIs because the version is expressed as part of the route path, making it very explicit and RESTful. It’s easy to understand and test, but it does mean your URL changes every time you release a new version. It is ideal for public-facing APIs where you want clients to be explicitly aware of which version they are calling.
How Does URL Path Versioning Work?
When the client sends a request, such as /api/v1/products, the ASP.NET Core routing system uses the path to determine which versioned controller or action should handle the request. For example, /api/v1/products, ASP.NET Core routes the request to the controller and actions that handle version 1. Similarly, /api/v2/products maps to version 2 endpoints.
This is achieved by configuring API versioning to utilize URL path segments for distinguishing versions, and by including the version placeholder in the controller route attributes.
Configure URL Path API Versioning in the Program.cs
We need to modify our Program.cs to configure API versioning using URL segment versioning. So, please modify the AddApiVersioning services as follows:
// Add API versioning configuration builder.Services.AddApiVersioning(options => { // This allows the API to use a default version if the client does not specify one. options.AssumeDefaultVersionWhenUnspecified = true; // This sets the default version (here, 1.0). options.DefaultApiVersion = new ApiVersion(1, 0); // This adds headers in the response informing clients about all the supported API versions. options.ReportApiVersions = true; // Use URL path segment for versioning (default: /api/v{version}/...) e.g., /api/v1/products options.ApiVersionReader = new UrlSegmentApiVersionReader(); });
Modify Controller Routes to Include Version in URL Path
We need to modify our controller to include the version in the route template. The only key difference from the query string approach is the route template. So, please modify the ProductsController as follows:
using APIVersioning.DTOs; using APIVersioning.Models; using Microsoft.AspNetCore.Mvc; namespace APIVersioning.Controllers { [ApiController] [Route("api/v{version:apiVersion}/products")] public class ProductsController : ControllerBase { // Simulating a data store with static list private static readonly List<Product> _products = new List<Product>() { new Product { Id = 1, Name = "Laptop", Price = 1200 }, new Product { Id = 2, Name = "Smartphone", Price = 700 } }; // GET Version 1: Return Id and Name only [HttpGet] [ApiVersion("1.0")] public IActionResult GetV1() { var productsV1 = _products.Select(p => new ProductResponseV1 { Id = p.Id, Name = p.Name }); return Ok(productsV1); } // GET Version 2: Return Id, Name, and Price [HttpGet] [ApiVersion("2.0")] public IActionResult GetV2() { var productsV2 = _products.Select(p => new ProductResponseV2 { Id = p.Id, Name = p.Name, Price = p.Price ?? 0 }); return Ok(productsV2); } // POST Version 1: Accept Name only, create product without price [HttpPost] [ApiVersion("1.0")] public IActionResult PostV1([FromBody] ProductCreateRequestV1 request) { if (string.IsNullOrWhiteSpace(request.Name)) return BadRequest("Name is required."); int newId = _products.Max(p => p.Id) + 1; var newProduct = new Product { Id = newId, Name = request.Name, Price = 0 // Price not supported in v1, default 0 }; _products.Add(newProduct); // Return version 1 response DTO (without price) var response = new ProductResponseV1 { Id = newProduct.Id, Name = newProduct.Name }; return Ok(response); } // POST Version 2: Accept Name and Price, create full product [HttpPost] [ApiVersion("2.0")] public IActionResult PostV2([FromBody] ProductCreateRequestV2 request) { if (string.IsNullOrWhiteSpace(request.Name)) return BadRequest("Name is required."); if (request.Price <= 0) return BadRequest("Price must be greater than zero."); int newId = _products.Max(p => p.Id) + 1; var newProduct = new Product { Id = newId, Name = request.Name, Price = request.Price }; _products.Add(newProduct); return Ok(newProduct); } } }
Note: The only change from query string versioning is the [Route] attribute on the controller: [Route(“api/v{version:apiVersion}/products”)]. This tells ASP.NET Core to expect URLs like /api/v1/products and /api/v2/products, and it matches the version segment to the API version.
Testing the API Endpoints
GET Version 1:
URL: GET /api/v1/products
Response:
[ { “Id”: 1, “Name”: “Laptop” }, { “Id”: 2, “Name”: “Smartphone” } ]
GET Version 2:
URL: GET /api/v2/products
Response:
[ { “Id”: 1, “Name”: “Laptop”, “Price”: 1200 }, { “Id”: 2, “Name”: “Smartphone”, “Price”: 700 } ]
POST Version 1:
URL: POST /api/v1/products
Body:
{ “Name”: “Tablet” }
Response:
{ “Id”: 3, “Name”: “Tablet” }
POST Version 2:
URL: POST /api/v2/products
Body:
{ “Name”: “Monitor”, “Price”: 350 }
Response:
{ “Id”: 4, “Name”: “Monitor”, “Price”: 350 }
When Should We Use ASP.NET Core Web API URL Path Versioning?
We need to use URL Path Versioning in ASP.NET Core Web API applications in the following scenarios:
When you want the API version to be explicit and visible in the endpoint URL:
- With URL path versioning, the version number is part of the API route, such as /api/v1/products or /api/v2/products.
- This makes it obvious to both API consumers and developers which version they are calling, just by looking at the URL.
- It improves clarity and avoids confusion, especially in documentation or debugging scenarios.
When building public APIs or APIs used by third parties:
- Public-facing APIs often prefer URL path versioning because it is easy to communicate in documentation, code samples, and onboarding guides.
- Third-party consumers expect versioning to be part of the URL, as this is a common industry standard for RESTful APIs.
- Many large platforms, including Google, Microsoft, Facebook, and Twitter, use URL path versioning for their APIs.
It is the most popular versioning style for public APIs, enterprise APIs, and scenarios where long-term stability, clarity, and contract separation are critical. If your API will be used by external clients, needs to be easily documented, or must support multiple versions for years to come, URL Path Versioning is usually the best choice.
Implementing ASP.NET Core Web API Versioning Using HTTP Header
Header Versioning is a method where the API version is specified in a custom HTTP header (commonly named api-version) instead of in the URL path or query string. Clients send the desired version information in the request header, and the API middleware reads that header and dispatches the request to the corresponding controller version.
For example, the client sets the header: api-version: 1.0 and calls the endpoint like: GET /api/products. The server reads the api-version header and routes the request accordingly.
This keeps the URLs clean and separate from versioning concerns, allowing version control without changing the URL. The downside is that it’s harder to test manually (for example, via a browser) because headers need to be explicitly added, and some clients may find it more complex to implement.
How Does Header Versioning Work?
When the client sends a request, ASP.NET Core API Versioning middleware inspects the HTTP headers for the version key you specify (e.g., api-version). Based on the header’s value, it dispatches the request to the controller and action for that version.
- If the header is api-version: 1.0, the API executes the version 1 controller/action.
- If the header is api-version: 2.0, the API executes the version 2 controller/action.
- If the header is missing, the API may use a default version or reject the request, based on configuration.
Configure API Versioning in Program.cs for Header Versioning
We need to modify the Program.cs file to use header versioning by configuring ApiVersionReader to read version from the request header. So, please modify the AddApiVersioning services as follows. You can change “api-version” to “x-api-version” or any custom header you want.
// Add API versioning configuration builder.Services.AddApiVersioning(options => { // This allows the API to use a default version if the client does not specify one. options.AssumeDefaultVersionWhenUnspecified = true; // This sets the default version (here, 1.0). options.DefaultApiVersion = new ApiVersion(1, 0); // This adds headers in the response informing clients about all the supported API versions. options.ReportApiVersions = true; // Use header versioning: 'api-version' is the header name options.ApiVersionReader = new HeaderApiVersionReader("api-version"); });
Modify Controller to Use Header Versioning
With header versioning, you need to use the same route for all versions, with no version information included in the URL, i.e., not part of the URL path or query string. So, please modify the Products controller as follows:
using APIVersioning.DTOs; using APIVersioning.Models; using Microsoft.AspNetCore.Mvc; namespace APIVersioning.Controllers { [ApiController] [Route("api/products")] public class ProductsController : ControllerBase { // Simulating a data store with static list private static readonly List<Product> _products = new List<Product>() { new Product { Id = 1, Name = "Laptop", Price = 1200 }, new Product { Id = 2, Name = "Smartphone", Price = 700 } }; // GET Version 1: Return Id and Name only [HttpGet] [ApiVersion("1.0")] public IActionResult GetV1() { var productsV1 = _products.Select(p => new ProductResponseV1 { Id = p.Id, Name = p.Name }); return Ok(productsV1); } // GET Version 2: Return Id, Name, and Price [HttpGet] [ApiVersion("2.0")] public IActionResult GetV2() { var productsV2 = _products.Select(p => new ProductResponseV2 { Id = p.Id, Name = p.Name, Price = p.Price ?? 0 }); return Ok(productsV2); } // POST Version 1: Accept Name only, create product without price [HttpPost] [ApiVersion("1.0")] public IActionResult PostV1([FromBody] ProductCreateRequestV1 request) { if (string.IsNullOrWhiteSpace(request.Name)) return BadRequest("Name is required."); int newId = _products.Max(p => p.Id) + 1; var newProduct = new Product { Id = newId, Name = request.Name, Price = 0 // Price not supported in v1, default 0 }; _products.Add(newProduct); // Return version 1 response DTO (without price) var response = new ProductResponseV1 { Id = newProduct.Id, Name = newProduct.Name }; return Ok(response); } // POST Version 2: Accept Name and Price, create full product [HttpPost] [ApiVersion("2.0")] public IActionResult PostV2([FromBody] ProductCreateRequestV2 request) { if (string.IsNullOrWhiteSpace(request.Name)) return BadRequest("Name is required."); if (request.Price <= 0) return BadRequest("Price must be greater than zero."); int newId = _products.Max(p => p.Id) + 1; var newProduct = new Product { Id = newId, Name = request.Name, Price = request.Price }; _products.Add(newProduct); return Ok(newProduct); } } }
Test Header Versioning with Postman
GET Version 1
URL: GET /api/products
Header: api-version: 1.0
GET Version 2
URL: GET /api/products
Header: api-version: 2.0
POST Version 1
URL: POST /api/products
Header: api-version: 1.0
Content-Type: application/json
{
“Name”: “Tablet”
}
POST Version 2
URL: POST /api/products
Header: api-version: 2.0
Content-Type: application/json
{
“Name”: “Monitor”,
“Price”: 350.0
}
Note: All requests use the same URL; only the header changes!
When Should We Use ASP.NET Core Web API Header Versioning?
We need to use Header Versioning in ASP.NET Core Web API applications in the following scenarios:
When you want to keep URLs clean and stable, without exposing version info in the route:
- With header versioning, all versions of an endpoint use the same path, such as /api/products, and the version is specified in a request header like api-version: 2.0.
- Clean URLs can make API endpoints easier to remember and reduce the need for client-side changes when updating versions.
When clients can easily send custom HTTP headers:
- Header versioning works best for scenarios where API consumers can add headers to their requests, such as mobile apps, backend services, or advanced HTTP clients.
- This method is particularly suitable for machine-to-machine, mobile, or internal service integrations, where setting headers is easy.
When you want to separate versioning from resource identification:
- Versioning via headers keeps the URL focused solely on identifying the resource, while the header communicates the contract version.
- It enables you to update or change the versioning mechanism in the future without breaking existing URLs.
Header Versioning in ASP.NET Core Web API gives you clean URLs and flexible version negotiation. Only the request header changes; the endpoint path stays the same for all versions. We need to use it when clients can easily set headers, and when you want to avoid version information in the URL or query string.
Implementing ASP.NET Core Web API Versioning Using Media Type Versioning
Media Type Versioning (also known as Content Negotiation Versioning or Accept Header Versioning) is an API versioning approach in which the requested API version is specified in the Accept HTTP header, typically as a custom media type. Instead of including the version in the URL or a custom header, clients specify it as follows: Accept: application/json;v=2.0. The API versioning middleware parses the Accept header to determine which API version logic to use.
This method is more RESTful and uses HTTP content negotiation, allowing clients to request a specific version of the API via media types. While this is elegant and fits well with REST principles, it is more complicated to implement and can be confusing for clients to correctly specify the version in the Accept header.
How Does Media Type Versioning Work?
When a client sends a request with the Accept header specifying a versioned media type, the ASP.NET Core API versioning middleware examines this header. It extracts the version information embedded in the media type.
- If the header is application/json;v=1.0, it routes the request to version 1 controllers and actions.
- If the header is application/json;v=2.0, it routes the request to version 2.
- If no version is specified or the header is missing, the API can be configured to fall back to a default version or return an error.
The URL remains unchanged (e.g., /api/products), but the content negotiation system determines which controller action to use based on the version parameter in the header.
Configure API Versioning in Program.cs for Media Type Versioning
In the Program.cs file, we need to configure the API versioning service and specify that the API version will be read from the request Media Type Header. So, please modify the AddApiVersioning services as follows: The options.ApiVersionReader = new MediaTypeApiVersionReader(“v”) instructs the middleware to look for a “v” parameter in the Accept header, such as “application/json;v=2.0”.
// Add API versioning configuration builder.Services.AddApiVersioning(options => { // This allows the API to use a default version if the client does not specify one. options.AssumeDefaultVersionWhenUnspecified = true; // This sets the default version (here, 1.0). options.DefaultApiVersion = new ApiVersion(1, 0); // This adds headers in the response informing clients about all the supported API versions. options.ReportApiVersions = true; // Enable Media Type versioning options.ApiVersionReader = new MediaTypeApiVersionReader("v"); // "v" is the name of the parameter in Accept header, e.g. application/json;v=2.0 });
Modify Controller to Use Media Type Versioning
No changes needed here! The controller remains the same:
Testing Media Type Versioning with Postman
GET Version 1:
URL: GET /api/products
Header: Accept: application/json;v=1.0
GET Version 2:
URL: GET /api/products
Header: Accept: application/json;v=2.0
POST Version 1:
URL: POST /api/products
Header: Accept: application/json;v=1.0
Content-Type: application/json
{
“Name”: “Tablet”
}
POST Version 2:
URL: POST /api/products
Header: Accept: application/json;v=2.0
Content-Type: application/json
{
“Name”: “Monitor”,
“Price”: 350.0
}
When Should We Use ASP.NET Core Web API Media Type Versioning?
We need to use Media Type Versioning in ASP.NET Core Web API applications in the following scenarios:
When you want to keep URLs clean and unchanging, with all versioning handled in the Accept header:
- With media type versioning, the API path remains the same for all versions, such as /api/products, while the version is specified in the Accept header (e.g., Accept: application/json; v=2.0).
- This approach ensures your resource URLs remain simple and stable, regardless of how many versions your API supports.
When you want to strictly follow RESTful principles and use HTTP content negotiation:
- Media type versioning uses the HTTP Accept header as intended by REST for negotiating representations, not just data format, but also version.
- This is especially appropriate if your API is designed for high REST compliance or if you want to support multiple resource representations in the future.
- It allows your API to support both versioning and content type negotiation in a unified way.
Media Type Versioning is best for advanced, well-documented, and REST-compliant APIs, especially if you expect clients to support content negotiation!
So, API versioning is a crucial strategy in modern API development. It ensures that as our business grows and our API evolves, we can:
- Deliver improvements without breaking existing clients.
- Maintain service for existing clients while innovating for new ones.
- Manage multiple API versions effectively, supporting a wide range of clients.
This approach protects our client applications, reduces support overhead, and gives our development team the flexibility to continuously innovate and improve the API.
In the next article, I will discuss implementing Versioning using Query String in ASP.NET Core Web API with Examples. In this article, I explain what ASP.NET Core Web API Versioning is. I hope you enjoy this article on ASP.NET Core Web API Versioning.