Skip to content

OpenAPI: Set the describedby top-level link #1497

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions docs/usage/openapi-client.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# OpenAPI clients

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

The following generators are supported, though you may try others as well:
> [!NOTE]
> If you prefer a generic JSON:API client instead of a typed one, choose from the existing
> [client libraries](https://jsonapi.org/implementations/#client-libraries).

The following code generators are supported, though you may try others as well:
- [NSwag](https://github.com/RicoSuter/NSwag): Produces clients for C# and TypeScript
- [Kiota](https://learn.microsoft.com/en-us/openapi/kiota/overview): Produces clients for C#, Go, Java, PHP, Python, Ruby, Swift and TypeScript

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

> [!WARNING]
> NSwag v14 is currently *incompatible* with JsonApiDotNetCore (tracked [here](https://github.com/RicoSuter/NSwag/issues/4662)). Stick with v13.x for the moment.
> NSwag v14 is currently *incompatible* with JsonApiDotNetCore (tracked [here](https://github.com/RicoSuter/NSwag/issues/4662)).
> Stick with v13.x for the moment.

4. Add our client package to your project:

Expand Down Expand Up @@ -141,8 +146,11 @@ The following steps describe how to generate and use a JSON:API client in C#, us
```

> [!TIP]
> 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.
> 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.
> 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.
> 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.

### Other IDEs

Expand Down Expand Up @@ -215,7 +223,8 @@ Likewise, you can enable nullable reference types by adding `/GenerateNullableRe
The available command-line switches for Kiota are described [here](https://learn.microsoft.com/en-us/openapi/kiota/using#client-generation).

At the time of writing, Kiota provides [no official integration](https://github.com/microsoft/kiota/issues/3005) with MSBuild.
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!
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!

```xml
<Target Name="KiotaRunTool" BeforeTargets="BeforeCompile;CoreCompile" Condition="$(DesignTimeBuild) != true And $(BuildingProject) == true">
Expand Down
20 changes: 20 additions & 0 deletions docs/usage/openapi-documentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# OpenAPI documentation

After [enabling OpenAPI](~/usage/openapi.md), you can expose a documentation website with SwaggerUI or Redoc.

### SwaggerUI

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.
This can be enabled by installing the `Swashbuckle.AspNetCore.SwaggerUI` NuGet package and adding the following to your `Program.cs` file:

```c#
app.UseSwaggerUI();
```

By default, SwaggerUI will be available at `http://localhost:<port>/swagger`.

### Redoc

[Redoc](https://github.com/Redocly/redoc) is another popular tool that generates a documentation website from an OpenAPI document.
It lists the endpoints and their schemas, but doesn't provide the ability to execute requests.
The `Swashbuckle.AspNetCore.ReDoc` NuGet package provides integration with Swashbuckle.
48 changes: 35 additions & 13 deletions docs/usage/openapi.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# OpenAPI

JsonApiDotNetCore provides an extension package that enables you to produce an [OpenAPI specification](https://swagger.io/specification/) for your JSON:API endpoints.
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.
The package provides an integration with [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore).
Exposing an [OpenAPI document](https://swagger.io/specification/) for your JSON:API endpoints enables to provide a
[documentation website](https://swagger.io/tools/swagger-ui/) and to generate typed
[client libraries](https://openapi-generator.tech/docs/generators/) in various languages.

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

## Getting started

Expand All @@ -13,7 +15,11 @@ The package provides an integration with [Swashbuckle](https://github.com/domain
dotnet add package JsonApiDotNetCore.OpenApi
```

2. Add the integration in your `Program.cs` file.
> [!NOTE]
> Because this package is still experimental, it's not yet available on NuGet.
> Use the steps [here](https://github.com/json-api-dotnet/JsonApiDotNetCore?tab=readme-ov-file#trying-out-the-latest-build) to install.

2. Add the JSON:API support to your `Program.cs` file.

```c#
builder.Services.AddJsonApi<AppDbContext>();
Expand All @@ -30,22 +36,37 @@ The package provides an integration with [Swashbuckle](https://github.com/domain
app.UseSwagger();
```

By default, the OpenAPI specification will be available at `http://localhost:<port>/swagger/v1/swagger.json`.
By default, the OpenAPI document will be available at `http://localhost:<port>/swagger/v1/swagger.json`.

### Customizing the Route Template

## Documentation
Because Swashbuckle doesn't properly implement the ASP.NET Options pattern, you must *not* use its
[documented way](https://github.com/domaindrivendev/Swashbuckle.AspNetCore?tab=readme-ov-file#change-the-path-for-swagger-json-endpoints)
to change the route template:

### SwaggerUI
```c#
// DO NOT USE THIS! INCOMPATIBLE WITH JSON:API!
app.UseSwagger(options => options.RouteTemplate = "api-docs/{documentName}/swagger.yaml");
```

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.
This can be enabled by installing the `Swashbuckle.AspNetCore.SwaggerUI` NuGet package and adding the following to your `Program.cs` file:
Instead, always call `UseSwagger()` *without parameters*. To change the route template, use the code below:

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

By default, SwaggerUI will be available at `http://localhost:<port>/swagger`.
If you want to inject dependencies to set the route template, use:

```c#
builder.Services.AddOptions<SwaggerOptions>().Configure<IServiceProvider>((options, serviceProvider) =>
{
var webHostEnvironment = serviceProvider.GetRequiredService<IWebHostEnvironment>();
string appName = webHostEnvironment.ApplicationName;
options.RouteTemplate = $"api-docs/{{documentName}}/{appName}-swagger.yaml";
});
```

### Triple-slash comments
## Triple-slash comments

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

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).
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).
This adds documentation for additional types, such as triple-slash comments on enums used in your resource models.
1 change: 1 addition & 0 deletions docs/usage/toc.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
# [Common Pitfalls](common-pitfalls.md)

# [OpenAPI](openapi.md)
## [Documentation](openapi-documentation.md)
## [Clients](openapi-client.md)

# Extensibility
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4781,15 +4781,30 @@
},
"errorResponseDocument": {
"required": [
"errors"
"errors",
"links"
],
"type": "object",
"properties": {
"links": {
"allOf": [
{
"$ref": "#/components/schemas/linksInErrorDocument"
}
]
},
"errors": {
"type": "array",
"items": {
"$ref": "#/components/schemas/errorObject"
}
},
"meta": {
"type": "object",
"additionalProperties": {
"type": "object",
"nullable": true
}
}
},
"additionalProperties": false
Expand All @@ -4812,6 +4827,22 @@
},
"additionalProperties": false
},
"linksInErrorDocument": {
"required": [
"self"
],
"type": "object",
"properties": {
"self": {
"minLength": 1,
"type": "string"
},
"describedby": {
"type": "string"
}
},
"additionalProperties": false
},
"linksInRelationship": {
"required": [
"related",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,37 @@ public List<ErrorObject> Errors {
get { return BackingStore?.Get<List<ErrorObject>>("errors"); }
set { BackingStore?.Set("errors", value); }
}
#endif
/// <summary>The links property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public LinksInErrorDocument? Links {
get { return BackingStore?.Get<LinksInErrorDocument?>("links"); }
set { BackingStore?.Set("links", value); }
}
#nullable restore
#else
public LinksInErrorDocument Links {
get { return BackingStore?.Get<LinksInErrorDocument>("links"); }
set { BackingStore?.Set("links", value); }
}
#endif
/// <summary>The primary error message.</summary>
public override string Message { get => base.Message; }
/// <summary>The meta property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public ErrorResponseDocument_meta? Meta {
get { return BackingStore?.Get<ErrorResponseDocument_meta?>("meta"); }
set { BackingStore?.Set("meta", value); }
}
#nullable restore
#else
public ErrorResponseDocument_meta Meta {
get { return BackingStore?.Get<ErrorResponseDocument_meta>("meta"); }
set { BackingStore?.Set("meta", value); }
}
#endif
/// <summary>
/// Instantiates a new errorResponseDocument and sets the default values.
/// </summary>
Expand All @@ -46,6 +74,8 @@ public static ErrorResponseDocument CreateFromDiscriminatorValue(IParseNode pars
public virtual IDictionary<string, Action<IParseNode>> GetFieldDeserializers() {
return new Dictionary<string, Action<IParseNode>> {
{"errors", n => { Errors = n.GetCollectionOfObjectValues<ErrorObject>(ErrorObject.CreateFromDiscriminatorValue)?.ToList(); } },
{"links", n => { Links = n.GetObjectValue<LinksInErrorDocument>(LinksInErrorDocument.CreateFromDiscriminatorValue); } },
{"meta", n => { Meta = n.GetObjectValue<ErrorResponseDocument_meta>(ErrorResponseDocument_meta.CreateFromDiscriminatorValue); } },
};
}
/// <summary>
Expand All @@ -55,6 +85,8 @@ public virtual IDictionary<string, Action<IParseNode>> GetFieldDeserializers() {
public virtual void Serialize(ISerializationWriter writer) {
_ = writer ?? throw new ArgumentNullException(nameof(writer));
writer.WriteCollectionOfObjectValues<ErrorObject>("errors", Errors);
writer.WriteObjectValue<LinksInErrorDocument>("links", Links);
writer.WriteObjectValue<ErrorResponseDocument_meta>("meta", Meta);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// <auto-generated/>
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions.Store;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System;
namespace OpenApiKiotaClientExample.GeneratedCode.Models {
public class ErrorResponseDocument_meta : IAdditionalDataHolder, IBackedModel, IParsable {
/// <summary>Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well.</summary>
public IDictionary<string, object> AdditionalData {
get { return BackingStore?.Get<IDictionary<string, object>>("AdditionalData"); }
set { BackingStore?.Set("AdditionalData", value); }
}
/// <summary>Stores model information.</summary>
public IBackingStore BackingStore { get; private set; }
/// <summary>
/// Instantiates a new errorResponseDocument_meta and sets the default values.
/// </summary>
public ErrorResponseDocument_meta() {
BackingStore = BackingStoreFactorySingleton.Instance.CreateBackingStore();
AdditionalData = new Dictionary<string, object>();
}
/// <summary>
/// Creates a new instance of the appropriate class based on discriminator value
/// </summary>
/// <param name="parseNode">The parse node to use to read the discriminator value and create the object</param>
public static ErrorResponseDocument_meta CreateFromDiscriminatorValue(IParseNode parseNode) {
_ = parseNode ?? throw new ArgumentNullException(nameof(parseNode));
return new ErrorResponseDocument_meta();
}
/// <summary>
/// The deserialization information for the current model
/// </summary>
public virtual IDictionary<string, Action<IParseNode>> GetFieldDeserializers() {
return new Dictionary<string, Action<IParseNode>> {
};
}
/// <summary>
/// Serializes information the current object
/// </summary>
/// <param name="writer">Serialization writer to use to serialize this model</param>
public virtual void Serialize(ISerializationWriter writer) {
_ = writer ?? throw new ArgumentNullException(nameof(writer));
writer.WriteAdditionalData(AdditionalData);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// <auto-generated/>
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions.Store;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System;
namespace OpenApiKiotaClientExample.GeneratedCode.Models {
public class LinksInErrorDocument : IBackedModel, IParsable {
/// <summary>Stores model information.</summary>
public IBackingStore BackingStore { get; private set; }
/// <summary>The describedby property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public string? Describedby {
get { return BackingStore?.Get<string?>("describedby"); }
set { BackingStore?.Set("describedby", value); }
}
#nullable restore
#else
public string Describedby {
get { return BackingStore?.Get<string>("describedby"); }
set { BackingStore?.Set("describedby", value); }
}
#endif
/// <summary>The self property</summary>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public string? Self {
get { return BackingStore?.Get<string?>("self"); }
set { BackingStore?.Set("self", value); }
}
#nullable restore
#else
public string Self {
get { return BackingStore?.Get<string>("self"); }
set { BackingStore?.Set("self", value); }
}
#endif
/// <summary>
/// Instantiates a new linksInErrorDocument and sets the default values.
/// </summary>
public LinksInErrorDocument() {
BackingStore = BackingStoreFactorySingleton.Instance.CreateBackingStore();
}
/// <summary>
/// Creates a new instance of the appropriate class based on discriminator value
/// </summary>
/// <param name="parseNode">The parse node to use to read the discriminator value and create the object</param>
public static LinksInErrorDocument CreateFromDiscriminatorValue(IParseNode parseNode) {
_ = parseNode ?? throw new ArgumentNullException(nameof(parseNode));
return new LinksInErrorDocument();
}
/// <summary>
/// The deserialization information for the current model
/// </summary>
public virtual IDictionary<string, Action<IParseNode>> GetFieldDeserializers() {
return new Dictionary<string, Action<IParseNode>> {
{"describedby", n => { Describedby = n.GetStringValue(); } },
{"self", n => { Self = n.GetStringValue(); } },
};
}
/// <summary>
/// Serializes information the current object
/// </summary>
/// <param name="writer">Serialization writer to use to serialize this model</param>
public virtual void Serialize(ISerializationWriter writer) {
_ = writer ?? throw new ArgumentNullException(nameof(writer));
writer.WriteStringValue("describedby", Describedby);
writer.WriteStringValue("self", Self);
}
}
}
Loading