API Versioning
Once mobile apps and partners depend on your API, you cannot change response shapes freely. Versioning lets v2 introduce breaking changes while v1 keeps working for existing clients.
On this page · 5 min · Advanced
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 5 min
- Difficulty
- Advanced
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Decide when a change needs a new version
- Configure Asp.Versioning.WebApi with a URL segment
- Run v1 and v2 controllers side by side
- Mark old versions deprecated and report versions in headers
Explanation#
Additive changes (a new optional field, a new endpoint) do not need a new version. Breaking changes do: renaming or removing fields, changing types or meanings, new required inputs.
Install Asp.Versioning.WebApi (the successor of Microsoft.AspNet.WebApi.Versioning). Call config.AddApiVersioning(...) and register the apiVersion route constraint.
URL segments (/api/v1/products) are the easiest to see, cache and test. Query strings (?api-version=2.0) and headers are alternatives.
Put each version in its own namespace (Controllers.V1, Controllers.V2) with [ApiVersion("1.0")] / [ApiVersion("2.0")].
ReportApiVersions = true adds api-supported-versions and api-deprecated-versions headers, so clients learn about deprecations automatically.
Syntax#
[ApiVersion("2.0")]
[RoutePrefix("api/v{version:apiVersion}/products")]
public class ProductsController : ApiController { ... }
Code example: v1 and v2 of the products endpoint#
using System.Web.Http;
using System.Web.Http.Routing;
using Asp.Versioning;
using Asp.Versioning.Routing;
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
var constraintResolver = new DefaultInlineConstraintResolver
{
ConstraintMap = { ["apiVersion"] = typeof(ApiVersionRouteConstraint) },
};
config.MapHttpAttributeRoutes(constraintResolver);
config.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
});
}
}
namespace ShopApi.Controllers.V1
{
[ApiVersion("1.0", Deprecated = true)]
[RoutePrefix("api/v{version:apiVersion}/products")]
public class ProductsController : ApiController
{
[HttpGet, Route("{id:int}")]
public IHttpActionResult Get(int id)
{
return Ok(new { Id = id, Name = "Pen", Price = 20m });
}
}
}
namespace ShopApi.Controllers.V2
{
[ApiVersion("2.0")]
[RoutePrefix("api/v{version:apiVersion}/products")]
public class ProductsController : ApiController
{
[HttpGet, Route("{id:int}")]
public IHttpActionResult Get(int id)
{
// Breaking change: price is now an object with a currency
return Ok(new { Id = id, Name = "Pen", Price = new { Amount = 20m, Currency = "INR" } });
}
}
}
GET /api/v1/products/1
→ 200 {"Id":1,"Name":"Pen","Price":20.0}
api-supported-versions: 2.0
api-deprecated-versions: 1.0
GET /api/v2/products/1
→ 200 {"Id":1,"Name":"Pen","Price":{"Amount":20.0,"Currency":"INR"}}
GET /api/v3/products/1
→ 400 Bad Request — error code UnsupportedApiVersion: the resource does not support API version '3'.This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- The apiVersion constraint is registered so {version:apiVersion} works in route templates.
- AddApiVersioning sets 1.0 as default and turns on version reporting.
- Two ProductsController classes live in V1 and V2 namespaces with different [ApiVersion] attributes.
- v1 is marked Deprecated, so responses advertise that clients should move to v2.
- Requests for an unknown version get a clear 400 error.
Real-world use case#
An insurance company’s partners integrate slowly. v1 runs for 18 months after v2 is released; the deprecation header and monthly usage logs show which partners still need reminders before v1 is switched off.
Common mistakes#
Adding an optional field is not breaking. Too many versions multiply maintenance.
Duplicate business logic drifts apart. Share services; let versions differ only in DTOs and mapping.
Announce deprecation, measure usage, set a sunset date, then remove.
Best practices#
- Version only for breaking changes
- Keep versions thin over shared services
- Report and deprecate versions explicitly
- Document each version separately in Swagger
Remember this#
Additive = same version. Breaking = new version.
/api/v{version:apiVersion}/... + [ApiVersion("2.0")].
Deprecate → measure → sunset.
Practice exercise#
Your task: Add an OrdersController in a V2 namespace with [ApiVersion("2.0")] whose GET api/v2/orders/{id} returns { Id, Status, Items = new string[0] }.
Need a hint?
Show solution
using System.Web.Http;
using Asp.Versioning;
namespace ShopApi.Controllers.V2
{
[ApiVersion("2.0")]
[RoutePrefix("api/v{version:apiVersion}/orders")]
public class OrdersController : ApiController
{
[HttpGet, Route("{id:int}")]
public IHttpActionResult Get(int id)
{
return Ok(new { Id = id, Status = "Shipped", Items = new string[0] });
}
}
}
Mini quiz#
Which change requires a new API version?
Want more? Practise all ASP.NET Web API MCQs · ASP.NET Web API interview questions
My notes#
Saved in this browser.