Skip to content

Commit ff2f420

Browse files
authored
Merge pull request #1497 from json-api-dotnet/links-described-by
OpenAPI: Set the describedby top-level link
2 parents 22b00b3 + 6b5bb6a commit ff2f420

File tree

42 files changed

+1091
-42
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1091
-42
lines changed

Diff for: docs/usage/openapi-client.md

+15-6
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">

Diff for: docs/usage/openapi-documentation.md

+20
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.

Diff for: docs/usage/openapi.md

+35-13
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.

Diff for: docs/usage/toc.md

+1
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

Diff for: src/Examples/JsonApiDotNetCoreExample/GeneratedSwagger/JsonApiDotNetCoreExample.json

+32-1
Original file line numberDiff line numberDiff line change
@@ -4781,15 +4781,30 @@
47814781
},
47824782
"errorResponseDocument": {
47834783
"required": [
4784-
"errors"
4784+
"errors",
4785+
"links"
47854786
],
47864787
"type": "object",
47874788
"properties": {
4789+
"links": {
4790+
"allOf": [
4791+
{
4792+
"$ref": "#/components/schemas/linksInErrorDocument"
4793+
}
4794+
]
4795+
},
47884796
"errors": {
47894797
"type": "array",
47904798
"items": {
47914799
"$ref": "#/components/schemas/errorObject"
47924800
}
4801+
},
4802+
"meta": {
4803+
"type": "object",
4804+
"additionalProperties": {
4805+
"type": "object",
4806+
"nullable": true
4807+
}
47934808
}
47944809
},
47954810
"additionalProperties": false
@@ -4812,6 +4827,22 @@
48124827
},
48134828
"additionalProperties": false
48144829
},
4830+
"linksInErrorDocument": {
4831+
"required": [
4832+
"self"
4833+
],
4834+
"type": "object",
4835+
"properties": {
4836+
"self": {
4837+
"minLength": 1,
4838+
"type": "string"
4839+
},
4840+
"describedby": {
4841+
"type": "string"
4842+
}
4843+
},
4844+
"additionalProperties": false
4845+
},
48154846
"linksInRelationship": {
48164847
"required": [
48174848
"related",

Diff for: src/Examples/OpenApiKiotaClientExample/GeneratedCode/Models/ErrorResponseDocument.cs

+32
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,37 @@ public List<ErrorObject> Errors {
2323
get { return BackingStore?.Get<List<ErrorObject>>("errors"); }
2424
set { BackingStore?.Set("errors", value); }
2525
}
26+
#endif
27+
/// <summary>The links property</summary>
28+
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
29+
#nullable enable
30+
public LinksInErrorDocument? Links {
31+
get { return BackingStore?.Get<LinksInErrorDocument?>("links"); }
32+
set { BackingStore?.Set("links", value); }
33+
}
34+
#nullable restore
35+
#else
36+
public LinksInErrorDocument Links {
37+
get { return BackingStore?.Get<LinksInErrorDocument>("links"); }
38+
set { BackingStore?.Set("links", value); }
39+
}
2640
#endif
2741
/// <summary>The primary error message.</summary>
2842
public override string Message { get => base.Message; }
43+
/// <summary>The meta property</summary>
44+
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
45+
#nullable enable
46+
public ErrorResponseDocument_meta? Meta {
47+
get { return BackingStore?.Get<ErrorResponseDocument_meta?>("meta"); }
48+
set { BackingStore?.Set("meta", value); }
49+
}
50+
#nullable restore
51+
#else
52+
public ErrorResponseDocument_meta Meta {
53+
get { return BackingStore?.Get<ErrorResponseDocument_meta>("meta"); }
54+
set { BackingStore?.Set("meta", value); }
55+
}
56+
#endif
2957
/// <summary>
3058
/// Instantiates a new errorResponseDocument and sets the default values.
3159
/// </summary>
@@ -46,6 +74,8 @@ public static ErrorResponseDocument CreateFromDiscriminatorValue(IParseNode pars
4674
public virtual IDictionary<string, Action<IParseNode>> GetFieldDeserializers() {
4775
return new Dictionary<string, Action<IParseNode>> {
4876
{"errors", n => { Errors = n.GetCollectionOfObjectValues<ErrorObject>(ErrorObject.CreateFromDiscriminatorValue)?.ToList(); } },
77+
{"links", n => { Links = n.GetObjectValue<LinksInErrorDocument>(LinksInErrorDocument.CreateFromDiscriminatorValue); } },
78+
{"meta", n => { Meta = n.GetObjectValue<ErrorResponseDocument_meta>(ErrorResponseDocument_meta.CreateFromDiscriminatorValue); } },
4979
};
5080
}
5181
/// <summary>
@@ -55,6 +85,8 @@ public virtual IDictionary<string, Action<IParseNode>> GetFieldDeserializers() {
5585
public virtual void Serialize(ISerializationWriter writer) {
5686
_ = writer ?? throw new ArgumentNullException(nameof(writer));
5787
writer.WriteCollectionOfObjectValues<ErrorObject>("errors", Errors);
88+
writer.WriteObjectValue<LinksInErrorDocument>("links", Links);
89+
writer.WriteObjectValue<ErrorResponseDocument_meta>("meta", Meta);
5890
}
5991
}
6092
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// <auto-generated/>
2+
using Microsoft.Kiota.Abstractions.Serialization;
3+
using Microsoft.Kiota.Abstractions.Store;
4+
using System.Collections.Generic;
5+
using System.IO;
6+
using System.Linq;
7+
using System;
8+
namespace OpenApiKiotaClientExample.GeneratedCode.Models {
9+
public class ErrorResponseDocument_meta : IAdditionalDataHolder, IBackedModel, IParsable {
10+
/// <summary>Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well.</summary>
11+
public IDictionary<string, object> AdditionalData {
12+
get { return BackingStore?.Get<IDictionary<string, object>>("AdditionalData"); }
13+
set { BackingStore?.Set("AdditionalData", value); }
14+
}
15+
/// <summary>Stores model information.</summary>
16+
public IBackingStore BackingStore { get; private set; }
17+
/// <summary>
18+
/// Instantiates a new errorResponseDocument_meta and sets the default values.
19+
/// </summary>
20+
public ErrorResponseDocument_meta() {
21+
BackingStore = BackingStoreFactorySingleton.Instance.CreateBackingStore();
22+
AdditionalData = new Dictionary<string, object>();
23+
}
24+
/// <summary>
25+
/// Creates a new instance of the appropriate class based on discriminator value
26+
/// </summary>
27+
/// <param name="parseNode">The parse node to use to read the discriminator value and create the object</param>
28+
public static ErrorResponseDocument_meta CreateFromDiscriminatorValue(IParseNode parseNode) {
29+
_ = parseNode ?? throw new ArgumentNullException(nameof(parseNode));
30+
return new ErrorResponseDocument_meta();
31+
}
32+
/// <summary>
33+
/// The deserialization information for the current model
34+
/// </summary>
35+
public virtual IDictionary<string, Action<IParseNode>> GetFieldDeserializers() {
36+
return new Dictionary<string, Action<IParseNode>> {
37+
};
38+
}
39+
/// <summary>
40+
/// Serializes information the current object
41+
/// </summary>
42+
/// <param name="writer">Serialization writer to use to serialize this model</param>
43+
public virtual void Serialize(ISerializationWriter writer) {
44+
_ = writer ?? throw new ArgumentNullException(nameof(writer));
45+
writer.WriteAdditionalData(AdditionalData);
46+
}
47+
}
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// <auto-generated/>
2+
using Microsoft.Kiota.Abstractions.Serialization;
3+
using Microsoft.Kiota.Abstractions.Store;
4+
using System.Collections.Generic;
5+
using System.IO;
6+
using System.Linq;
7+
using System;
8+
namespace OpenApiKiotaClientExample.GeneratedCode.Models {
9+
public class LinksInErrorDocument : IBackedModel, IParsable {
10+
/// <summary>Stores model information.</summary>
11+
public IBackingStore BackingStore { get; private set; }
12+
/// <summary>The describedby property</summary>
13+
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
14+
#nullable enable
15+
public string? Describedby {
16+
get { return BackingStore?.Get<string?>("describedby"); }
17+
set { BackingStore?.Set("describedby", value); }
18+
}
19+
#nullable restore
20+
#else
21+
public string Describedby {
22+
get { return BackingStore?.Get<string>("describedby"); }
23+
set { BackingStore?.Set("describedby", value); }
24+
}
25+
#endif
26+
/// <summary>The self property</summary>
27+
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
28+
#nullable enable
29+
public string? Self {
30+
get { return BackingStore?.Get<string?>("self"); }
31+
set { BackingStore?.Set("self", value); }
32+
}
33+
#nullable restore
34+
#else
35+
public string Self {
36+
get { return BackingStore?.Get<string>("self"); }
37+
set { BackingStore?.Set("self", value); }
38+
}
39+
#endif
40+
/// <summary>
41+
/// Instantiates a new linksInErrorDocument and sets the default values.
42+
/// </summary>
43+
public LinksInErrorDocument() {
44+
BackingStore = BackingStoreFactorySingleton.Instance.CreateBackingStore();
45+
}
46+
/// <summary>
47+
/// Creates a new instance of the appropriate class based on discriminator value
48+
/// </summary>
49+
/// <param name="parseNode">The parse node to use to read the discriminator value and create the object</param>
50+
public static LinksInErrorDocument CreateFromDiscriminatorValue(IParseNode parseNode) {
51+
_ = parseNode ?? throw new ArgumentNullException(nameof(parseNode));
52+
return new LinksInErrorDocument();
53+
}
54+
/// <summary>
55+
/// The deserialization information for the current model
56+
/// </summary>
57+
public virtual IDictionary<string, Action<IParseNode>> GetFieldDeserializers() {
58+
return new Dictionary<string, Action<IParseNode>> {
59+
{"describedby", n => { Describedby = n.GetStringValue(); } },
60+
{"self", n => { Self = n.GetStringValue(); } },
61+
};
62+
}
63+
/// <summary>
64+
/// Serializes information the current object
65+
/// </summary>
66+
/// <param name="writer">Serialization writer to use to serialize this model</param>
67+
public virtual void Serialize(ISerializationWriter writer) {
68+
_ = writer ?? throw new ArgumentNullException(nameof(writer));
69+
writer.WriteStringValue("describedby", Describedby);
70+
writer.WriteStringValue("self", Self);
71+
}
72+
}
73+
}

0 commit comments

Comments
 (0)