ASP.NET Web API (.NET Framework) · Cross-Cutting

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.

Advanced 5 min Lesson 26 of 36Covers ASP.NET Web API 2.2 on .NET Framework 4.8.
Before you start:RoutingDTOs
0/36 in ASP.NET Web API
On this page · 5 min · Advanced

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#

C# Illustrative
[ApiVersion("2.0")]
[RoutePrefix("api/v{version:apiVersion}/products")]
public class ProductsController : ApiController { ... }

Code example: v1 and v2 of the products endpoint#

C#WebApiConfig.cs Illustrative
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" } });
        }
    }
}
Expected output
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#

  1. The apiVersion constraint is registered so {version:apiVersion} works in route templates.
  2. AddApiVersioning sets 1.0 as default and turns on version reporting.
  3. Two ProductsController classes live in V1 and V2 namespaces with different [ApiVersion] attributes.
  4. v1 is marked Deprecated, so responses advertise that clients should move to v2.
  5. 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#

Versioning every change

Adding an optional field is not breaking. Too many versions multiply maintenance.

Copy-pasting all logic into v2

Duplicate business logic drifts apart. Share services; let versions differ only in DTOs and mapping.

Removing v1 without notice

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?
Copy the V2 ProductsController pattern: namespace, [ApiVersion], [RoutePrefix] with {version:apiVersion}.
Show solution
C# Illustrative
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#

API Versioning quizMultiple choice Question 1 of 3

Which change requires a new API version?

Score so far: 0/0

Want more? Practise all ASP.NET Web API MCQs · ASP.NET Web API interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.