Skip to content

Commit 6b5bb6a

Browse files
committed
Set the describedby top-level link to point to the OpenAPI document URL
1 parent b53ece1 commit 6b5bb6a

File tree

9 files changed

+127
-20
lines changed

9 files changed

+127
-20
lines changed

docs/usage/openapi-client.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
# OpenAPI clients
22

3-
After [enabling OpenAPI](~/usage/openapi.md), you can generate a JSON:API client for your API in various programming languages.
3+
After [enabling OpenAPI](~/usage/openapi.md), you can generate a typed JSON:API client for your API in various programming languages.
44

5-
The following generators are supported, though you may try others as well:
5+
> [!NOTE]
6+
> If you prefer a generic JSON:API client instead of a typed one, choose from the existing
7+
> [client libraries](https://jsonapi.org/implementations/#client-libraries).
8+
9+
The following code generators are supported, though you may try others as well:
610
- [NSwag](https://github.com/RicoSuter/NSwag): Produces clients for C# and TypeScript
711
- [Kiota](https://learn.microsoft.com/en-us/openapi/kiota/overview): Produces clients for C#, Go, Java, PHP, Python, Ruby, Swift and TypeScript
812

@@ -51,7 +55,8 @@ The following steps describe how to generate and use a JSON:API client in C#, us
5155
3. Although not strictly required, we recommend running package update now, which fixes some issues.
5256

5357
> [!WARNING]
54-
> NSwag v14 is currently *incompatible* with JsonApiDotNetCore (tracked [here](https://github.com/RicoSuter/NSwag/issues/4662)). Stick with v13.x for the moment.
58+
> NSwag v14 is currently *incompatible* with JsonApiDotNetCore (tracked [here](https://github.com/RicoSuter/NSwag/issues/4662)).
59+
> Stick with v13.x for the moment.
5560
5661
4. Add our client package to your project:
5762

@@ -141,8 +146,11 @@ The following steps describe how to generate and use a JSON:API client in C#, us
141146
```
142147
143148
> [!TIP]
144-
> The [example project](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/openapi/src/Examples/OpenApiNSwagClientExample) contains an enhanced version that uses `IHttpClientFactory` for [scalability](https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory) and [resiliency](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests#use-polly-based-handlers) and logs the HTTP requests and responses.
145-
> Additionally, the example shows how to write the swagger.json file to disk when building the server, which is imported from the client project. This keeps the server and client automatically in sync, which is handy when both are in the same solution.
149+
> The [example project](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/openapi/src/Examples/OpenApiNSwagClientExample) contains an enhanced version
150+
> that uses `IHttpClientFactory` for [scalability](https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory) and
151+
> [resiliency](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-requests#use-polly-based-handlers) and logs the HTTP requests and responses.
152+
> Additionally, the example shows how to write the swagger.json file to disk when building the server, which is imported from the client project.
153+
> This keeps the server and client automatically in sync, which is handy when both are in the same solution.
146154
147155
### Other IDEs
148156
@@ -215,7 +223,8 @@ Likewise, you can enable nullable reference types by adding `/GenerateNullableRe
215223
The available command-line switches for Kiota are described [here](https://learn.microsoft.com/en-us/openapi/kiota/using#client-generation).
216224

217225
At the time of writing, Kiota provides [no official integration](https://github.com/microsoft/kiota/issues/3005) with MSBuild.
218-
Our [example project](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/openapi/src/Examples/OpenApiKiotaClientExample) takes a stab at it, although it has glitches. If you're an MSBuild expert, please help out!
226+
Our [example project](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/openapi/src/Examples/OpenApiKiotaClientExample) takes a stab at it,
227+
although it has glitches. If you're an MSBuild expert, please help out!
219228

220229
```xml
221230
<Target Name="KiotaRunTool" BeforeTargets="BeforeCompile;CoreCompile" Condition="$(DesignTimeBuild) != true And $(BuildingProject) == true">

docs/usage/openapi-documentation.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# OpenAPI documentation
2+
3+
After [enabling OpenAPI](~/usage/openapi.md), you can expose a documentation website with SwaggerUI or Redoc.
4+
5+
### SwaggerUI
6+
7+
Swashbuckle ships with [SwaggerUI](https://swagger.io/tools/swagger-ui/), which enables to visualize and interact with the JSON:API endpoints through a web page.
8+
This can be enabled by installing the `Swashbuckle.AspNetCore.SwaggerUI` NuGet package and adding the following to your `Program.cs` file:
9+
10+
```c#
11+
app.UseSwaggerUI();
12+
```
13+
14+
By default, SwaggerUI will be available at `http://localhost:<port>/swagger`.
15+
16+
### Redoc
17+
18+
[Redoc](https://github.com/Redocly/redoc) is another popular tool that generates a documentation website from an OpenAPI document.
19+
It lists the endpoints and their schemas, but doesn't provide the ability to execute requests.
20+
The `Swashbuckle.AspNetCore.ReDoc` NuGet package provides integration with Swashbuckle.

docs/usage/openapi.md

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
# OpenAPI
22

3-
JsonApiDotNetCore provides an extension package that enables you to produce an [OpenAPI specification](https://swagger.io/specification/) for your JSON:API endpoints.
4-
This can be used to generate a [documentation website](https://swagger.io/tools/swagger-ui/) or to generate [client libraries](https://openapi-generator.tech/docs/generators/) in various languages.
5-
The package provides an integration with [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore).
3+
Exposing an [OpenAPI document](https://swagger.io/specification/) for your JSON:API endpoints enables to provide a
4+
[documentation website](https://swagger.io/tools/swagger-ui/) and to generate typed
5+
[client libraries](https://openapi-generator.tech/docs/generators/) in various languages.
66

7+
The [JsonApiDotNetCore.OpenApi](https://github.com/json-api-dotnet/JsonApiDotNetCore/pkgs/nuget/JsonApiDotNetCore.OpenApi) NuGet package
8+
provides OpenAPI support for JSON:API by integrating with [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore).
79

810
## Getting started
911

@@ -13,7 +15,11 @@ The package provides an integration with [Swashbuckle](https://github.com/domain
1315
dotnet add package JsonApiDotNetCore.OpenApi
1416
```
1517
16-
2. Add the integration in your `Program.cs` file.
18+
> [!NOTE]
19+
> Because this package is still experimental, it's not yet available on NuGet.
20+
> Use the steps [here](https://github.com/json-api-dotnet/JsonApiDotNetCore?tab=readme-ov-file#trying-out-the-latest-build) to install.
21+
22+
2. Add the JSON:API support to your `Program.cs` file.
1723
1824
```c#
1925
builder.Services.AddJsonApi<AppDbContext>();
@@ -30,22 +36,37 @@ The package provides an integration with [Swashbuckle](https://github.com/domain
3036
app.UseSwagger();
3137
```
3238
33-
By default, the OpenAPI specification will be available at `http://localhost:<port>/swagger/v1/swagger.json`.
39+
By default, the OpenAPI document will be available at `http://localhost:<port>/swagger/v1/swagger.json`.
40+
41+
### Customizing the Route Template
3442
35-
## Documentation
43+
Because Swashbuckle doesn't properly implement the ASP.NET Options pattern, you must *not* use its
44+
[documented way](https://github.com/domaindrivendev/Swashbuckle.AspNetCore?tab=readme-ov-file#change-the-path-for-swagger-json-endpoints)
45+
to change the route template:
3646
37-
### SwaggerUI
47+
```c#
48+
// DO NOT USE THIS! INCOMPATIBLE WITH JSON:API!
49+
app.UseSwagger(options => options.RouteTemplate = "api-docs/{documentName}/swagger.yaml");
50+
```
3851

39-
Swashbuckle also ships with [SwaggerUI](https://swagger.io/tools/swagger-ui/), which enables to visualize and interact with the API endpoints through a web page.
40-
This can be enabled by installing the `Swashbuckle.AspNetCore.SwaggerUI` NuGet package and adding the following to your `Program.cs` file:
52+
Instead, always call `UseSwagger()` *without parameters*. To change the route template, use the code below:
4153

4254
```c#
43-
app.UseSwaggerUI();
55+
builder.Services.Configure<SwaggerOptions>(options => options.RouteTemplate = "api-docs/{documentName}/swagger.yaml");
4456
```
4557

46-
By default, SwaggerUI will be available at `http://localhost:<port>/swagger`.
58+
If you want to inject dependencies to set the route template, use:
59+
60+
```c#
61+
builder.Services.AddOptions<SwaggerOptions>().Configure<IServiceProvider>((options, serviceProvider) =>
62+
{
63+
var webHostEnvironment = serviceProvider.GetRequiredService<IWebHostEnvironment>();
64+
string appName = webHostEnvironment.ApplicationName;
65+
options.RouteTemplate = $"api-docs/{{documentName}}/{appName}-swagger.yaml";
66+
});
67+
```
4768

48-
### Triple-slash comments
69+
## Triple-slash comments
4970

5071
Documentation for JSON:API endpoints is provided out of the box, which shows in SwaggerUI and through IDE IntelliSense in auto-generated clients.
5172
To also get documentation for your resource classes and their properties, add the following to your project file.
@@ -58,5 +79,6 @@ The `NoWarn` line is optional, which suppresses build warnings for undocumented
5879
</PropertyGroup>
5980
```
6081

61-
You can combine this with the documentation that Swagger itself supports, by enabling it as described [here](https://github.com/domaindrivendev/Swashbuckle.AspNetCore#include-descriptions-from-xml-comments).
82+
You can combine this with the documentation that Swagger itself supports, by enabling it as described
83+
[here](https://github.com/domaindrivendev/Swashbuckle.AspNetCore#include-descriptions-from-xml-comments).
6284
This adds documentation for additional types, such as triple-slash comments on enums used in your resource models.

docs/usage/toc.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
# [Common Pitfalls](common-pitfalls.md)
2727

2828
# [OpenAPI](openapi.md)
29+
## [Documentation](openapi-documentation.md)
2930
## [Clients](openapi-client.md)
3031

3132
# Extensibility
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using JsonApiDotNetCore.Serialization.Response;
2+
using Microsoft.Extensions.Options;
3+
using Swashbuckle.AspNetCore.Swagger;
4+
using Swashbuckle.AspNetCore.SwaggerGen;
5+
6+
namespace JsonApiDotNetCore.OpenApi;
7+
8+
/// <summary>
9+
/// Provides the OpenAPI URL for the "describedby" link in https://jsonapi.org/format/#document-top-level.
10+
/// </summary>
11+
internal sealed class OpenApiDescriptionLinkProvider : IDocumentDescriptionLinkProvider
12+
{
13+
private readonly IOptionsMonitor<SwaggerGeneratorOptions> _swaggerGeneratorOptionsMonitor;
14+
private readonly IOptionsMonitor<SwaggerOptions> _swaggerOptionsMonitor;
15+
16+
public OpenApiDescriptionLinkProvider(IOptionsMonitor<SwaggerGeneratorOptions> swaggerGeneratorOptionsMonitor,
17+
IOptionsMonitor<SwaggerOptions> swaggerOptionsMonitor)
18+
{
19+
ArgumentGuard.NotNull(swaggerGeneratorOptionsMonitor);
20+
ArgumentGuard.NotNull(swaggerOptionsMonitor);
21+
22+
_swaggerGeneratorOptionsMonitor = swaggerGeneratorOptionsMonitor;
23+
_swaggerOptionsMonitor = swaggerOptionsMonitor;
24+
}
25+
26+
/// <inheritdoc />
27+
public string? GetUrl()
28+
{
29+
SwaggerGeneratorOptions swaggerGeneratorOptions = _swaggerGeneratorOptionsMonitor.CurrentValue;
30+
31+
if (swaggerGeneratorOptions.SwaggerDocs.Count > 0)
32+
{
33+
string latestVersionDocumentName = swaggerGeneratorOptions.SwaggerDocs.Last().Key;
34+
35+
SwaggerOptions swaggerOptions = _swaggerOptionsMonitor.CurrentValue;
36+
return swaggerOptions.RouteTemplate.Replace("{documentName}", latestVersionDocumentName).Replace("{json|yaml}", "json");
37+
}
38+
39+
return null;
40+
}
41+
}

src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using JsonApiDotNetCore.OpenApi.JsonApiMetadata;
22
using JsonApiDotNetCore.OpenApi.SwaggerComponents;
3+
using JsonApiDotNetCore.Serialization.Response;
34
using Microsoft.AspNetCore.Mvc;
45
using Microsoft.AspNetCore.Mvc.ApiExplorer;
56
using Microsoft.Extensions.DependencyInjection;
@@ -75,6 +76,7 @@ private static void AddSwaggerGenerator(IServiceCollection services)
7576
AddSchemaGenerators(services);
7677

7778
services.TryAddSingleton<RelationshipTypeFactory>();
79+
services.AddSingleton<IDocumentDescriptionLinkProvider, OpenApiDescriptionLinkProvider>();
7880

7981
services.AddSwaggerGen();
8082
services.AddSingleton<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerGenOptions>();

test/OpenApiKiotaEndToEndTests/QueryStrings/FilterTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
139139
response.Data.ElementAt(0).Id.Should().Be(node.Children.ElementAt(1).StringId);
140140
response.Meta.ShouldNotBeNull();
141141
response.Meta.AdditionalData.ShouldContainKey("total").With(total => total.Should().Be(1));
142+
response.Links.ShouldNotBeNull();
143+
response.Links.Describedby.Should().Be("swagger/v1/swagger.json");
142144
}
143145
}
144146

@@ -162,6 +164,8 @@ public async Task Cannot_use_empty_filter()
162164
// Assert
163165
ErrorResponseDocument exception = (await action.Should().ThrowExactlyAsync<ErrorResponseDocument>()).Which;
164166
exception.ResponseStatusCode.Should().Be((int)HttpStatusCode.BadRequest);
167+
exception.Links.ShouldNotBeNull();
168+
exception.Links.Describedby.Should().Be("swagger/v1/swagger.json");
165169
exception.Errors.ShouldHaveCount(1);
166170

167171
ErrorObject error = exception.Errors[0];

test/OpenApiNSwagClientTests/LegacyClient/ResponseTests.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,8 @@ public async Task Getting_unknown_resource_translates_error_response()
203203
const string responseBody = $$"""
204204
{
205205
"links": {
206-
"self": "http://localhost/api/flights/ZvuH1"
206+
"self": "http://localhost/api/flights/ZvuH1",
207+
"describedby": "swagger/v1/swagger.json"
207208
},
208209
"errors": [
209210
{
@@ -225,6 +226,9 @@ public async Task Getting_unknown_resource_translates_error_response()
225226
// Assert
226227
ApiException<ErrorResponseDocument> exception = (await action.Should().ThrowExactlyAsync<ApiException<ErrorResponseDocument>>()).Which;
227228
exception.StatusCode.Should().Be((int)HttpStatusCode.NotFound);
229+
exception.Result.Links.ShouldNotBeNull();
230+
exception.Result.Links.Self.Should().Be("http://localhost/api/flights/ZvuH1");
231+
exception.Result.Links.Describedby.Should().Be("swagger/v1/swagger.json");
228232
exception.Result.Errors.ShouldHaveCount(1);
229233

230234
ErrorObject? error = exception.Result.Errors.ElementAt(0);

test/OpenApiNSwagEndToEndTests/QueryStrings/FilterTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
127127
response.Data.ElementAt(0).Id.Should().Be(node.Children.ElementAt(1).StringId);
128128
response.Meta.ShouldNotBeNull();
129129
response.Meta.ShouldContainKey("total").With(total => total.Should().Be(1));
130+
response.Links.ShouldNotBeNull();
131+
response.Links.Describedby.Should().Be("swagger/v1/swagger.json");
130132
}
131133

132134
[Fact]
@@ -148,6 +150,8 @@ public async Task Cannot_use_empty_filter()
148150
ApiException<ErrorResponseDocument> exception = (await action.Should().ThrowExactlyAsync<ApiException<ErrorResponseDocument>>()).Which;
149151
exception.StatusCode.Should().Be((int)HttpStatusCode.BadRequest);
150152
exception.Message.Should().Be("HTTP 400: The query string is invalid.");
153+
exception.Result.Links.ShouldNotBeNull();
154+
exception.Result.Links.Describedby.Should().Be("swagger/v1/swagger.json");
151155
exception.Result.Errors.ShouldHaveCount(1);
152156

153157
ErrorObject error = exception.Result.Errors.ElementAt(0);

0 commit comments

Comments
 (0)