docs/en/Community-Articles/2025-08-27-backcompat-rest-apis-ms-dotnet/article.md
With microservice architecture, each service develops and ships independently at its own pace, and clients infrequently update in lockstep. Backward compatibility means that when you release new versions, current consumers continue to function without changing code. This article provides a practical, 6–7 minute tutorial specific to .NET developers.
A change is breaking if a client that previously conformed can fail at compile time or runtime, or exhibit different business‑critical behavior, without changing that client in any way. In other words: if an old client needs to be altered in order to continue functioning as it did, your change is breaking.
address).price: string → price: number, or date format changes).status="closed" formerly included archived items, but no longer does).Golden rule: Old clients should continue to work exactly as they did before—without any changes.
Versioning is your master control lever for managing change. Typical methods:
GET /api/v1/orders
GET /api/v2/orders
Pros: Cache/gateway‑friendly; explicit in docs. Cons: URL noise.
GET /api/orders
x-api-version: 2.0
Pros: Clean URLs; multiple reader support. Cons: Needs proxy/CDN rules.
Pros: Semantically accurate.
Cons: More complicated to test and implement.
Recommendation: For the majority of teams, favor URI segments, with an optional x-api-version header for flexibility.
// Program.cs
using Asp.Versioning;
builder.Services.AddControllers();
builder.Services.AddApiVersioning(o =>
{
o.DefaultApiVersion = new ApiVersion(1, 0);
o.AssumeDefaultVersionWhenUnspecified = true;
o.ReportApiVersions = true; // response header: api-supported-versions
o.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(),
new HeaderApiVersionReader("x-api-version")
);
});
builder.Services.AddVersionedApiExplorer(o =>
{
o.GroupNameFormat = "'v'VVV"; // v1, v2
o.SubstituteApiVersionInUrl = true;
});
// Controller
using Asp.Versioning;
[ApiController]
[Route("api/v{version:apiVersion}/orders")]
public class OrdersController : ControllerBase
{
[HttpGet]
[ApiVersion("1.0", Deprecated = true)]
public IActionResult GetV1() => Ok(new { message = "v1" });
[HttpGet]
[MapToApiVersion("2.0")]
public IActionResult GetV2() => Ok(new { message = "v2", includes = new []{"items"} });
}
Obey the following rules for compatibility‑safe evolution:
public record OrderDto(
Guid Id,
decimal Total,
string Currency,
string? SalesChannel // new, optional
);
ProblemDetails). Avoid ad‑hoc error shapes on a per-endpoint basis.api-supported-versions: 1.0, 2.0Deprecation: true (in deprecated endpoints)Sunset: Wed, 01 Oct 2025 00:00:00 GMT (planned deprecation date)Idempotency-Key header for retry-safe POSTs.ETag/If-Match to prevent lost updates.nextPageToken) to protect clients from sorting/index changes.A good deprecation policy is announce → coexist → remove:
Canary & Blue‑Green: Initialize v2 with a small traffic portion and compare error/latency budgets prior to scaling up.
Minimal example (xUnit + snapshot style):
[Fact]
public async Task Orders_v1_contract_should_match_snapshot()
{
var resp = await _client.GetStringAsync("/api/v1/orders");
Approvals.VerifyJson(resp); // snapshot comparison
}
/swagger/v1/swagger.json, /swagger/v2/swagger.json). Display both in Swagger UI.Swagger UI configuration by group name:
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1");
c.SwaggerEndpoint("/swagger/v2/swagger.json", "API v2");
});
Backward compatibility is not a version number—it is disciplined change management. When you use add‑only schema evolution, a well‑defined versioning strategy, strict contract testing, and rolling rollout, you maintain microservice independence and safeguard consumer experience.