diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj index 4e32da2e..2b969895 100644 --- a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/Asp.Versioning.Http.csproj @@ -15,6 +15,21 @@ + + + + + + + + diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/docs/agent-guide.md b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/docs/agent-guide.md new file mode 100644 index 00000000..2847cb5a --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/docs/agent-guide.md @@ -0,0 +1,50 @@ +# Asp.Versioning.Http + +`Asp.Versioning.Http` adds API versioning to ASP.NET Core **Minimal APIs**. Use +`Asp.Versioning.Mvc` as well when the service has controllers. + +## Set up + +```csharp +using Asp.Versioning; + +builder.Services.AddProblemDetails(); +builder.Services.AddApiVersioning( options => options.ReportApiVersions = true ); +``` + +`ReportApiVersions = true` adds the `api-supported-versions` and `api-deprecated-versions` +response headers. Without it, clients get no version information in responses. + +## Version the endpoints + +Create one versioned API from the app, then declare the versions each endpoint implements: + +```csharp +var api = app.NewVersionedApi(); + +api.MapGet( "/items", () => "version 1" ).HasApiVersion( 1.0 ); +api.MapGet( "/items", () => "version 2" ).HasApiVersion( 2.0 ); + +// no version required +api.MapGet( "/ping", () => "pong" ).IsApiVersionNeutral(); +``` + +`MapGroup( ... ).HasApiVersion( ... )` on the object returned by `NewVersionedApi()` versions every +endpoint in the group. A request is routed by the `api-version` query string parameter, for example +`GET /items?api-version=2.0`. + +## Behavior you can rely on + +- A request with **no** version to a versioned endpoint fails with **400** (`unspecified`). +- A version no endpoint supports fails with **400** (`unsupported`); a value that is not a version + (`api-version=banana`) fails with **400** (`invalid`). Both are `application/problem+json`. +- `api-version=1` is read as `1.0`. +- Call `NewVersionedApi()` on the app itself. Calling it on a `MapGroup` throws + `InvalidOperationException` ("A versioned API group cannot be mapped as a nested group."). + +## More + +- Change where clients send the version, or assume a default version: + [version sources and defaults](version-sources-and-defaults.md). +- Deprecate a version or change error responses: + [deprecation and errors](deprecation-and-errors.md). diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/docs/deprecation-and-errors.md b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/docs/deprecation-and-errors.md new file mode 100644 index 00000000..6ede10d1 --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/docs/deprecation-and-errors.md @@ -0,0 +1,36 @@ +# Deprecation and error responses + +## Deprecate a version + +Mark a version deprecated on the endpoints that still implement it: + +```csharp +var api = app.NewVersionedApi(); + +api.MapGet( "/old", () => "old" ) + .HasDeprecatedApiVersion( 0.9 ) + .HasApiVersion( 1.0 ); +``` + +With `ReportApiVersions = true`, `GET /old?api-version=0.9` still succeeds and the response carries +`api-supported-versions: 1.0, 2.0` and `api-deprecated-versions: 0.9`. The supported list covers all +versions in the API, not only the ones on that endpoint. + +## Error responses + +All of these are `application/problem+json` with `status` 400 by default: + +| Request | `type` ends with | +|---|---| +| No version, no default assumed | `#unspecified` | +| A version nothing implements, such as `api-version=3.0` | `#unsupported` | +| A value that is not a version, such as `api-version=banana` | `#invalid` | + +Change the status for unsupported versions with `options.UnsupportedApiVersionStatusCode = 404;`; +the response is still a `#unsupported` problem. + +## Version-neutral endpoints + +`IsApiVersionNeutral()` means no version is required: `GET /ping` and `GET /ping?api-version=1.0` +both succeed. A neutral endpoint still rejects a version nothing in the API supports: +`GET /ping?api-version=9.0` is a 400 `unsupported`. diff --git a/src/AspNetCore/WebApi/src/Asp.Versioning.Http/docs/version-sources-and-defaults.md b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/docs/version-sources-and-defaults.md new file mode 100644 index 00000000..31833602 --- /dev/null +++ b/src/AspNetCore/WebApi/src/Asp.Versioning.Http/docs/version-sources-and-defaults.md @@ -0,0 +1,52 @@ +# Version sources and defaults + +## Where the version is read from + +The default reader accepts the version from the `api-version` **query string** parameter and from a +**URL segment**. Setting `ApiVersionReader` **replaces** that default; it does not add to it. + +```csharp +// header only: "?api-version=2.0" is now ignored and the request fails as unspecified +builder.Services.AddApiVersioning( options => + options.ApiVersionReader = new HeaderApiVersionReader( "x-api-version" ) ); +``` + +To accept several sources, combine them explicitly: + +```csharp +builder.Services.AddApiVersioning( options => + options.ApiVersionReader = ApiVersionReader.Combine( + new QueryStringApiVersionReader( "v" ), + new HeaderApiVersionReader( "x-api-version" ) ) ); +``` + +With the combined reader above, `?v=1.0` and `x-api-version: 2.0` both work, and the default +`api-version` parameter no longer does. + +## URL segment versioning + +Put the `apiVersion` route constraint in the route template: + +```csharp +var api = app.NewVersionedApi(); +api.MapGet( "/v{version:apiVersion}/seg", () => "seg" ).HasApiVersion( 1.0 ); +``` + +`GET /v1/seg` and `GET /v1.0/seg` succeed. A version no endpoint supports (`/v2/seg`) does **not** +produce the 400 `unsupported` problem: the route no longer matches and the response is **404**. A +request to `/seg?api-version=1.0` is also 404, because the template requires the segment. + +## When the client sends no version + +By default an unspecified version fails with 400. To serve a default version instead: + +```csharp +builder.Services.AddApiVersioning( options => +{ + options.AssumeDefaultVersionWhenUnspecified = true; + options.DefaultApiVersion = new ApiVersion( 2, 0 ); // optional; the default is 1.0 +} ); +``` + +Assuming a default hides a missing version from clients, so only enable it for services that already had +unversioned clients.