15 questions found
Why does an ASP.NET Core Web API need explicit versioning support, and what package provides it?
Beginner
Without explicit versioning, any change to an endpoint's contract risks breaking existing consumers who can't control when they upgrade -- the Asp.Versioning.Mvc (for controllers) and Asp.Versioning.Http (for minimal APIs) NuGet packages add structured support for defining, routing to, and documenting multiple concurrently-supported API versions, letting you evolve the API while giving consumers a migration window.
dotnet add package Asp.Versioning.Mvc
builder.Services.AddApiVersioning(options => {
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
});
Real-world example
A payments API team adds Asp.Versioning after their first breaking schema change forced an awkward, unversioned migration where every consumer had to update simultaneously overnight -- versioning going forward gives future changes a graceful rollout window.
Common follow-ups: What are the different strategies for exposing a version (URL, header, query string)?;How do you decide when a change is breaking enough to warrant a new version?
API Documentation with Swagger/OpenAPI;RESTful Web APIs & Controllers
How do you implement URL segment-based API versioning for controllers using [ApiVersion] and route templates?
Intermediate
Apply [ApiVersion("1.0")] to a controller and include {version:apiVersion} in its route template; the versioning middleware then matches incoming requests' URL version segment to the correct controller, letting you maintain separate controller classes (or actions within one, using MapToApiVersion) per supported version.
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/products")]
public class ProductsV1Controller : ControllerBase {
[HttpGet]
public IActionResult Get() => Ok(_v1Products);
}
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/products")]
public class ProductsV2Controller : ControllerBase {
[HttpGet]
public IActionResult Get() => Ok(_v2ProductsWithNewFields);
}
Real-world example
An inventory API introduces v2 with a restructured response shape while keeping v1 fully functional for existing integrators, both reachable via distinct, clearly versioned URLs (/api/v1/products and /api/v2/products).
Common follow-ups: How do you share common logic between V1 and V2 controllers without duplication?;What happens if a client requests a version that doesn't exist?
Controllers vs Minimal APIs;Routing
How do you implement API versioning for minimal API endpoints, since they don't use controller attributes?
Advanced
Asp.Versioning.Http provides a NewVersionedApi() builder extension and .HasApiVersion()/.MapToApiVersion() methods for grouping and versioning minimal API endpoint groups, mirroring the controller-based attribute approach but using a fluent, code-based API appropriate for the minimal API style.
var versionSet = app.NewApiVersionSet()
.HasApiVersion(new ApiVersion(1, 0))
.HasApiVersion(new ApiVersion(2, 0))
.ReportApiVersions()
.Build();
var group = app.MapGroup("api/v{version:apiVersion}/products").WithApiVersionSet(versionSet);
group.MapGet("/", () => Ok(v1Products)).HasApiVersion(1.0);
group.MapGet("/", () => Ok(v2Products)).HasApiVersion(2.0);
Real-world example
A minimal-API-based microservice adopts the same URL-segment versioning strategy as its controller-based sibling services, using NewApiVersionSet and MapGroup to keep the versioning approach consistent across the organization's differing API styles.
Common follow-ups: How does WithApiVersionSet differ from applying versioning per individual endpoint?;Can minimal APIs and controllers share the same versioning configuration in one app?
Controllers vs Minimal APIs;Routing
How does header-based API versioning work, and what's a scenario where it's preferred over URL segment versioning?
Intermediate
Header-based versioning reads the version from a custom request header (like X-Api-Version) rather than the URL path, configured via options.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version") -- preferred when you want completely stable, version-agnostic URLs (useful for caching proxies keyed purely on path, or when the URL is meant to represent a permanent resource identity independent of API evolution).
builder.Services.AddApiVersioning(options => {
options.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version");
});
// Client request:
// GET /api/products/5
// X-Api-Version: 2.0
Real-world example
An internal service-to-service API uses header-based versioning specifically to keep resource URLs (used as stable identifiers in logs and distributed traces) completely unaffected by API version changes over time.
Common follow-ups: How do you combine multiple version readers (URL AND header) simultaneously?;What are the caching implications of header-based versus URL-based versioning?
Caching;RESTful Web APIs & Controllers
How do you deprecate an API version gracefully, notifying consumers via response headers before eventually removing it?
Advanced
Marking a version with [ApiVersion("1.0", Deprecated = true)] combined with options.ReportApiVersions = true causes the middleware to automatically include api-deprecated-versions and api-supported-versions response headers on every response, giving automated tooling or attentive client developers a machine-readable signal of the impending removal well before the version is actually deleted from the codebase.
[ApiVersion("1.0", Deprecated = true)]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/orders")]
public class OrdersController : ControllerBase { }
// Every response now includes:
// api-deprecated-versions: 1.0
// api-supported-versions: 1.0, 2.0
Real-world example
A partner integration team's automated monitoring watches the api-deprecated-versions header on every API call, automatically opening an internal ticket when their integration is detected still using a version flagged as deprecated, well ahead of its actual removal date.
Common follow-ups: How long is a reasonable deprecation window before removing a version entirely?;How would you notify consumers through channels beyond just response headers?
RESTful Web APIs & Controllers;Logging
What is the difference between versioning at the API level (entire controller/group) versus versioning individual actions within a shared controller?
Intermediate
API-level versioning applies one or more [ApiVersion] attributes to the whole controller, meaning every action in it is available under all declared versions unless individually restricted. Action-level versioning uses [MapToApiVersion("x.0")] on specific actions within a multi-version controller to indicate that action only exists in that particular version, letting most actions be shared across versions while only the actually-changed ones are duplicated.
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/products")]
public class ProductsController : ControllerBase {
[HttpGet]
public IActionResult GetAll() => Ok(_products); // available in both 1.0 and 2.0
[HttpGet("{id}/reviews")]
[MapToApiVersion("2.0")]
public IActionResult GetReviews(int id) => Ok(_reviews); // only in 2.0
}
Real-world example
A products API adds a new reviews sub-resource only in v2 using MapToApiVersion, while the existing GetAll and GetById actions remain shared across both versions in the same controller, minimizing duplicated code.
Common follow-ups: What happens if MapToApiVersion references a version not declared on the controller?;When does splitting into fully separate controllers become clearer than this shared approach?
Controllers vs Minimal APIs;Endpoint Metadata
Route Constraints & Templates
How does ASP.NET Core API versioning handle a request that doesn't specify any version at all?
Advanced
Behavior depends on configuration: setting AssumeDefaultVersionWhenUnspecified = true combined with DefaultApiVersion routes unversioned requests to a designated default version transparently; without that setting, an unversioned request to a versioned endpoint typically results in a 400 Bad Request indicating an API version is required -- the choice reflects a trade-off between convenience for simple/internal clients versus explicitness that avoids silently locking clients into a version they didn't deliberately choose.
builder.Services.AddApiVersioning(options => {
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true; // unversioned requests silently get v1.0
});
// Without AssumeDefaultVersionWhenUnspecified:
// An unversioned request instead returns 400 Bad Request
Real-world example
A public API deliberately disables AssumeDefaultVersionWhenUnspecified, forcing every client to explicitly declare a version, preventing a scenario where an unversioned client silently continues receiving v1 responses forever without realizing newer versions exist.
Common follow-ups: What's the risk of silently defaulting unversioned requests to the latest version instead of a fixed default?;How do public versus internal APIs typically differ in this choice?
RESTful Web APIs & Controllers;Error Handling
How do you unit test that a specific API version's controller action behaves correctly, given the version-based routing?
Intermediate
Since versioned routing resolution happens at the ASP.NET Core routing/framework level (not inside your action method's logic), unit tests typically just instantiate and call the specific versioned controller class directly (e.g., ProductsV2Controller) without needing to simulate the actual HTTP routing/version-matching machinery -- reserving full routing verification (confirming a request with a given version header/URL actually reaches the intended controller) for integration tests using WebApplicationFactory.
[Fact]
public void GetAll_V2_ReturnsExpandedProductFields() {
var controller = new ProductsV2Controller(_mockService.Object);
var result = controller.GetAll() as OkObjectResult;
var products = result.Value as List<ProductV2Dto>;
Assert.Contains(products, p => p.Currency != null); // v2-specific field
}
Real-world example
A unit test suite directly tests ProductsV2Controller's business logic in isolation, while a separate integration test suite confirms that a request with 'api-version: 2.0' actually routes to that controller correctly -- keeping the two concerns cleanly separated.
Common follow-ups: What would an integration test verifying correct version routing look like?;How do you test the deprecation header behavior specifically?
Testing ASP.NET Core Applications;Controllers vs Minimal APIs
How would you design API versioning strategy differently for a public, third-party-consumed API versus an internal microservice API?
Advanced
Public APIs require conservative, long-lived version support (many months to years), extensive advance deprecation notice, and strict backward-compatibility guarantees since you can't coordinate directly with unknown external consumers. Internal microservice APIs, where you control or can coordinate with every consumer, often favor looser versioning (sometimes none at all, relying on coordinated deployments and consumer-driven contract tests) since breaking changes can be communicated and rolled out through direct team coordination rather than needing months of parallel version support.
// Public API: formal versioning, months of overlap
[ApiVersion("1.0", Deprecated = true)] // still supported 12+ months after v2 launch
[ApiVersion("2.0")]
// Internal API: looser, relies on coordinated deploys + contract tests instead
// often no formal API versioning attributes at all
Real-world example
A company's public partner-facing API maintains three concurrent versions with an 18-month deprecation policy, while their internal order-to-inventory service communication has no formal versioning at all, relying entirely on consumer-driven contract tests run in CI before any deploy.
Common follow-ups: What is consumer-driven contract testing and how does it substitute for formal versioning internally?;What risk does an internal API accept by skipping formal versioning?
Testing ASP.NET Core Applications;RESTful Web APIs & Controllers
How does query string-based API versioning work, and what are its trade-offs compared to URL segment versioning?
Intermediate
Query string versioning (?api-version=1.0) is configured via options.ApiVersionReader = new QueryStringApiVersionReader(), keeping the base URL path stable while the version is an optional parameter -- trade-offs include being less visible/discoverable than a URL segment, potentially interacting awkwardly with caching proxies that don't account for query strings in cache keys, but being simpler to add optionally without restructuring existing unversioned routes.
builder.Services.AddApiVersioning(options => {
options.ApiVersionReader = new QueryStringApiVersionReader("api-version");
});
// GET /api/products?api-version=2.0
Real-world example
A team retrofitting versioning onto an already-live, unversioned API chooses query string versioning specifically because it can be added without breaking any existing URLs that omit the parameter and fall back to the default version.
Common follow-ups: How does this interact with CDN or reverse-proxy caching that might ignore query strings?;Why is URL segment versioning generally considered more RESTful/discoverable?
Caching;RESTful Web APIs & Controllers