diff --git a/.gitattributes b/.gitattributes
index b508c0d946..1f6c60ff78 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,5 +1,6 @@
# When running OpenAPI tests, these committed files are downloaded and written to disk (so we'll know when something changes).
-# On Windows, these text files are auto-converted to crlf on fetch, while the written downloaded files use lf line endings.
+# On Windows, these text files are auto-converted to crlf on git fetch, while the written downloaded files use lf line endings.
# Therefore, running the tests on Windows creates local changes. Staging them auto-converts back to crlf, which undoes the changes.
-# To avoid this annoyance, the next line opts out of the auto-conversion and forces to lf.
+# To avoid this annoyance, the next lines opt out of the auto-conversion and force to lf.
swagger.g.json text eol=lf
+**/GeneratedSwagger/*.json text eol=lf
diff --git a/JsonApiDotNetCore.sln.DotSettings b/JsonApiDotNetCore.sln.DotSettings
index a5b6860b2d..077dd5e21f 100644
--- a/JsonApiDotNetCore.sln.DotSettings
+++ b/JsonApiDotNetCore.sln.DotSettings
@@ -15,6 +15,7 @@ JsonApiDotNetCore.ArgumentGuard.NotNull($EXPR$);
50
False
True
+ 71287D6F-6C3B-44B4-9FCA-E78FE3F02289/f:SchemaGenerator.cs
swagger.g.json
swagger.json
SOLUTION
diff --git a/docs/usage/openapi-client.md b/docs/usage/openapi-client.md
index 55d5bcb178..3f0b93f52e 100644
--- a/docs/usage/openapi-client.md
+++ b/docs/usage/openapi-client.md
@@ -3,8 +3,9 @@
You can generate a JSON:API client in various programming languages from the [OpenAPI specification](https://swagger.io/specification/) file that JsonApiDotNetCore APIs provide.
For C# .NET clients generated using [NSwag](https://github.com/RicoSuter/NSwag), we provide an additional package
-that introduces support for partial PATCH/POST requests. The concern here is that a property on a generated C# class
-being `null` could mean "set the value to `null` in the request" or "this is `null` because I never touched it".
+that provides workarounds for NSwag bugs and introduces support for partial PATCH/POST requests.
+The concern here is that a property on a generated C# class being `null` could either mean: "set the value to `null`
+in the request" or: "this is `null` because I never touched it".
## Getting started
@@ -26,29 +27,19 @@ The next steps describe how to generate a JSON:API client library and use our pa
3. Although not strictly required, we recommend to run package update now, which fixes some issues.
-4. Add code that calls one of your JSON:API endpoints.
+ > [!WARNING]
+ > NSwag v14 is currently *incompatible* with JsonApiDotNetCore (tracked [here](https://github.com/RicoSuter/NSwag/issues/4662)). Stick with v13.x for the moment.
- ```c#
- using var httpClient = new HttpClient();
- var apiClient = new ExampleApiClient("http://localhost:14140", httpClient);
-
- PersonCollectionResponseDocument getResponse = await apiClient.GetPersonCollectionAsync(new Dictionary
- {
- ["filter"] = "has(assignedTodoItems)",
- ["sort"] = "-lastName",
- ["page[size]"] = "5"
- });
+4. Add our client package to your project:
- foreach (PersonDataInResponse person in getResponse.Data)
- {
- Console.WriteLine($"Found person {person.Id}: {person.Attributes.DisplayName}");
- }
+ ```
+ dotnet add package JsonApiDotNetCore.OpenApi.Client
```
-5. Add our client package to your project:
+5. Add the next line inside the **OpenApiReference** section in your project file:
- ```
- dotnet add package JsonApiDotNetCore.OpenApi.Client
+ ```xml
+ /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions
```
6. Add the following glue code to connect our package with your generated code.
@@ -73,8 +64,28 @@ The next steps describe how to generate a JSON:API client library and use our pa
> [!TIP]
> The project at src/Examples/JsonApiDotNetCoreExampleClient contains an enhanced version that 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.
+
+7. Add code that calls one of your JSON:API endpoints.
+
+ ```c#
+ using var httpClient = new HttpClient();
+ var apiClient = new ExampleApiClient(httpClient);
+
+ var getResponse = await apiClient.GetPersonCollectionAsync(new Dictionary
+ {
+ ["filter"] = "has(assignedTodoItems)",
+ ["sort"] = "-lastName",
+ ["page[size]"] = "5"
+ });
+
+ foreach (var person in getResponse.Data)
+ {
+ Console.WriteLine($"Found person {person.Id}: {person.Attributes.DisplayName}");
+ }
+ ```
-7. Extend your demo code to send a partial PATCH request with the help of our package:
+8. Extend your demo code to send a partial PATCH request with the help of our package:
```c#
var patchRequest = new PersonPatchRequestDocument
@@ -94,7 +105,7 @@ The next steps describe how to generate a JSON:API client library and use our pa
person => person.FirstName))
{
// Workaround for https://github.com/RicoSuter/NSwag/issues/2499.
- await TranslateAsync(async () => await apiClient.PatchPersonAsync(patchRequest.Data.Id, null, patchRequest));
+ await ApiResponse.TranslateAsync(() => apiClient.PatchPersonAsync(patchRequest.Data.Id, null, patchRequest));
// The sent request looks like this:
// {
@@ -145,13 +156,13 @@ From here, continue from step 3 in the list of steps for Visual Studio.
The `OpenApiReference` element in the project file accepts an `Options` element to pass additional settings to the client generator,
which are listed [here](https://github.com/RicoSuter/NSwag/blob/master/src/NSwag.Commands/Commands/CodeGeneration/OpenApiToCSharpClientCommand.cs).
-For example, the next section puts the generated code in a namespace, removes the `baseUrl` parameter and generates an interface (which is handy for dependency injection):
+For example, the next section puts the generated code in a namespace and generates an interface (which is handy for dependency injection):
```xml
ExampleProject.GeneratedCode
SalesApiClient
NSwagCSharp
- /UseBaseUrl:false /GenerateClientInterfaces:true
+ /GenerateClientInterfaces:true
```
diff --git a/package-versions.props b/package-versions.props
index a836b2cc01..0129d2aee2 100644
--- a/package-versions.props
+++ b/package-versions.props
@@ -7,6 +7,7 @@
6.5.0
+ 8.0.*
0.13.*
1.0.*
35.2.*
diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/OpenAPIs/swagger.json b/src/Examples/JsonApiDotNetCoreExample/GeneratedSwagger/JsonApiDotNetCoreExample.json
similarity index 92%
rename from src/Examples/JsonApiDotNetCoreExampleClient/OpenAPIs/swagger.json
rename to src/Examples/JsonApiDotNetCoreExample/GeneratedSwagger/JsonApiDotNetCoreExample.json
index e1a8fa39b0..d5c1ef9db1 100644
--- a/src/Examples/JsonApiDotNetCoreExampleClient/OpenAPIs/swagger.json
+++ b/src/Examples/JsonApiDotNetCoreExample/GeneratedSwagger/JsonApiDotNetCoreExample.json
@@ -4,6 +4,11 @@
"title": "JsonApiDotNetCoreExample",
"version": "1.0"
},
+ "servers": [
+ {
+ "url": "https://localhost:44340"
+ }
+ ],
"paths": {
"/api/people": {
"get": {
@@ -23,7 +28,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -61,7 +66,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -91,7 +96,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -165,7 +170,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -215,7 +220,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -257,7 +262,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -357,7 +362,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -407,7 +412,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -444,14 +449,14 @@
{
"name": "query",
"in": "query",
- "description": "For syntax, see the documentation for the [`include`](https://www.jsonapi.net/usage/reading/including-relationships.html)/[`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
+ "description": "For syntax, see the documentation for the [`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -494,14 +499,14 @@
{
"name": "query",
"in": "query",
- "description": "For syntax, see the documentation for the [`include`](https://www.jsonapi.net/usage/reading/including-relationships.html)/[`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
+ "description": "For syntax, see the documentation for the [`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -683,7 +688,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -733,7 +738,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -770,14 +775,14 @@
{
"name": "query",
"in": "query",
- "description": "For syntax, see the documentation for the [`include`](https://www.jsonapi.net/usage/reading/including-relationships.html)/[`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
+ "description": "For syntax, see the documentation for the [`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -820,14 +825,14 @@
{
"name": "query",
"in": "query",
- "description": "For syntax, see the documentation for the [`include`](https://www.jsonapi.net/usage/reading/including-relationships.html)/[`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
+ "description": "For syntax, see the documentation for the [`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -1000,7 +1005,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -1038,7 +1043,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -1068,7 +1073,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -1142,7 +1147,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -1192,7 +1197,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -1234,7 +1239,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -1334,7 +1339,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -1384,7 +1389,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -1421,14 +1426,14 @@
{
"name": "query",
"in": "query",
- "description": "For syntax, see the documentation for the [`include`](https://www.jsonapi.net/usage/reading/including-relationships.html)/[`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
+ "description": "For syntax, see the documentation for the [`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -1471,14 +1476,14 @@
{
"name": "query",
"in": "query",
- "description": "For syntax, see the documentation for the [`include`](https://www.jsonapi.net/usage/reading/including-relationships.html)/[`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
+ "description": "For syntax, see the documentation for the [`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -1651,7 +1656,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -1689,7 +1694,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -1719,7 +1724,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -1793,7 +1798,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -1843,7 +1848,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -1885,7 +1890,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -1985,7 +1990,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -2035,7 +2040,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -2072,14 +2077,14 @@
{
"name": "query",
"in": "query",
- "description": "For syntax, see the documentation for the [`include`](https://www.jsonapi.net/usage/reading/including-relationships.html)/[`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
+ "description": "For syntax, see the documentation for the [`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -2122,14 +2127,14 @@
{
"name": "query",
"in": "query",
- "description": "For syntax, see the documentation for the [`include`](https://www.jsonapi.net/usage/reading/including-relationships.html)/[`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
+ "description": "For syntax, see the documentation for the [`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -2219,7 +2224,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -2269,7 +2274,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -2306,14 +2311,14 @@
{
"name": "query",
"in": "query",
- "description": "For syntax, see the documentation for the [`include`](https://www.jsonapi.net/usage/reading/including-relationships.html)/[`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
+ "description": "For syntax, see the documentation for the [`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -2356,14 +2361,14 @@
{
"name": "query",
"in": "query",
- "description": "For syntax, see the documentation for the [`include`](https://www.jsonapi.net/usage/reading/including-relationships.html)/[`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
+ "description": "For syntax, see the documentation for the [`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -2453,7 +2458,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -2503,7 +2508,7 @@
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -2540,14 +2545,14 @@
{
"name": "query",
"in": "query",
- "description": "For syntax, see the documentation for the [`include`](https://www.jsonapi.net/usage/reading/including-relationships.html)/[`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
+ "description": "For syntax, see the documentation for the [`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -2590,14 +2595,14 @@
{
"name": "query",
"in": "query",
- "description": "For syntax, see the documentation for the [`include`](https://www.jsonapi.net/usage/reading/including-relationships.html)/[`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
+ "description": "For syntax, see the documentation for the [`filter`](https://www.jsonapi.net/usage/reading/filtering.html)/[`sort`](https://www.jsonapi.net/usage/reading/sorting.html)/[`page`](https://www.jsonapi.net/usage/reading/pagination.html)/[`fields`](https://www.jsonapi.net/usage/reading/sparse-fieldset-selection.html) query string parameters.",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string",
"nullable": true
},
- "example": null
+ "example": ""
}
}
],
@@ -2755,7 +2760,34 @@
},
"components": {
"schemas": {
- "linksInRelationshipObject": {
+ "dataInResponse": {
+ "required": [
+ "id",
+ "type"
+ ],
+ "type": "object",
+ "properties": {
+ "type": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "id": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "additionalProperties": false,
+ "discriminator": {
+ "propertyName": "type",
+ "mapping": {
+ "people": "#/components/schemas/personDataInResponse",
+ "tags": "#/components/schemas/tagDataInResponse",
+ "todoItems": "#/components/schemas/todoItemDataInResponse"
+ }
+ },
+ "x-abstract": true
+ },
+ "linksInRelationship": {
"required": [
"related",
"self"
@@ -2801,6 +2833,19 @@
},
"additionalProperties": false
},
+ "linksInResourceData": {
+ "required": [
+ "self"
+ ],
+ "type": "object",
+ "properties": {
+ "self": {
+ "minLength": 1,
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
"linksInResourceDocument": {
"required": [
"self"
@@ -2871,19 +2916,6 @@
},
"additionalProperties": false
},
- "linksInResourceObject": {
- "required": [
- "self"
- ],
- "type": "object",
- "properties": {
- "self": {
- "minLength": 1,
- "type": "string"
- }
- },
- "additionalProperties": false
- },
"nullablePersonIdentifierResponseDocument": {
"required": [
"data",
@@ -2938,6 +2970,12 @@
],
"nullable": true
},
+ "included": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/dataInResponse"
+ }
+ },
"meta": {
"type": "object",
"additionalProperties": {
@@ -2974,7 +3012,7 @@
"links": {
"allOf": [
{
- "$ref": "#/components/schemas/linksInRelationshipObject"
+ "$ref": "#/components/schemas/linksInRelationship"
}
]
},
@@ -3062,6 +3100,12 @@
"$ref": "#/components/schemas/personDataInResponse"
}
},
+ "included": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/dataInResponse"
+ }
+ },
"meta": {
"type": "object",
"additionalProperties": {
@@ -3080,11 +3124,7 @@
"type": "object",
"properties": {
"type": {
- "allOf": [
- {
- "$ref": "#/components/schemas/personResourceType"
- }
- ]
+ "$ref": "#/components/schemas/personResourceType"
},
"id": {
"minLength": 1,
@@ -3114,11 +3154,7 @@
"type": "object",
"properties": {
"type": {
- "allOf": [
- {
- "$ref": "#/components/schemas/personResourceType"
- }
- ]
+ "$ref": "#/components/schemas/personResourceType"
},
"attributes": {
"allOf": [
@@ -3138,53 +3174,48 @@
"additionalProperties": false
},
"personDataInResponse": {
- "required": [
- "id",
- "links",
- "type"
- ],
- "type": "object",
- "properties": {
- "type": {
- "allOf": [
- {
- "$ref": "#/components/schemas/personResourceType"
- }
- ]
- },
- "id": {
- "minLength": 1,
- "type": "string"
- },
- "attributes": {
- "allOf": [
- {
- "$ref": "#/components/schemas/personAttributesInResponse"
- }
- ]
- },
- "relationships": {
- "allOf": [
- {
- "$ref": "#/components/schemas/personRelationshipsInResponse"
- }
- ]
- },
- "links": {
- "allOf": [
- {
- "$ref": "#/components/schemas/linksInResourceObject"
- }
- ]
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/dataInResponse"
},
- "meta": {
+ {
+ "required": [
+ "links"
+ ],
"type": "object",
- "additionalProperties": {
- "type": "object",
- "nullable": true
- }
+ "properties": {
+ "attributes": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/personAttributesInResponse"
+ }
+ ]
+ },
+ "relationships": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/personRelationshipsInResponse"
+ }
+ ]
+ },
+ "links": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/linksInResourceData"
+ }
+ ]
+ },
+ "meta": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "nullable": true
+ }
+ }
+ },
+ "additionalProperties": false
}
- },
+ ],
"additionalProperties": false
},
"personIdentifier": {
@@ -3195,11 +3226,7 @@
"type": "object",
"properties": {
"type": {
- "allOf": [
- {
- "$ref": "#/components/schemas/personResourceType"
- }
- ]
+ "$ref": "#/components/schemas/personResourceType"
},
"id": {
"minLength": 1,
@@ -3292,6 +3319,12 @@
}
]
},
+ "included": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/dataInResponse"
+ }
+ },
"meta": {
"type": "object",
"additionalProperties": {
@@ -3366,7 +3399,8 @@
"enum": [
"people"
],
- "type": "string"
+ "type": "string",
+ "additionalProperties": false
},
"personSecondaryResponseDocument": {
"required": [
@@ -3389,6 +3423,12 @@
}
]
},
+ "included": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/dataInResponse"
+ }
+ },
"meta": {
"type": "object",
"additionalProperties": {
@@ -3452,6 +3492,12 @@
"$ref": "#/components/schemas/tagDataInResponse"
}
},
+ "included": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/dataInResponse"
+ }
+ },
"meta": {
"type": "object",
"additionalProperties": {
@@ -3470,11 +3516,7 @@
"type": "object",
"properties": {
"type": {
- "allOf": [
- {
- "$ref": "#/components/schemas/tagResourceType"
- }
- ]
+ "$ref": "#/components/schemas/tagResourceType"
},
"id": {
"minLength": 1,
@@ -3504,11 +3546,7 @@
"type": "object",
"properties": {
"type": {
- "allOf": [
- {
- "$ref": "#/components/schemas/tagResourceType"
- }
- ]
+ "$ref": "#/components/schemas/tagResourceType"
},
"attributes": {
"allOf": [
@@ -3528,53 +3566,48 @@
"additionalProperties": false
},
"tagDataInResponse": {
- "required": [
- "id",
- "links",
- "type"
- ],
- "type": "object",
- "properties": {
- "type": {
- "allOf": [
- {
- "$ref": "#/components/schemas/tagResourceType"
- }
- ]
- },
- "id": {
- "minLength": 1,
- "type": "string"
- },
- "attributes": {
- "allOf": [
- {
- "$ref": "#/components/schemas/tagAttributesInResponse"
- }
- ]
- },
- "relationships": {
- "allOf": [
- {
- "$ref": "#/components/schemas/tagRelationshipsInResponse"
- }
- ]
- },
- "links": {
- "allOf": [
- {
- "$ref": "#/components/schemas/linksInResourceObject"
- }
- ]
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/dataInResponse"
},
- "meta": {
+ {
+ "required": [
+ "links"
+ ],
"type": "object",
- "additionalProperties": {
- "type": "object",
- "nullable": true
- }
+ "properties": {
+ "attributes": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/tagAttributesInResponse"
+ }
+ ]
+ },
+ "relationships": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/tagRelationshipsInResponse"
+ }
+ ]
+ },
+ "links": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/linksInResourceData"
+ }
+ ]
+ },
+ "meta": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "nullable": true
+ }
+ }
+ },
+ "additionalProperties": false
}
- },
+ ],
"additionalProperties": false
},
"tagIdentifier": {
@@ -3585,11 +3618,7 @@
"type": "object",
"properties": {
"type": {
- "allOf": [
- {
- "$ref": "#/components/schemas/tagResourceType"
- }
- ]
+ "$ref": "#/components/schemas/tagResourceType"
},
"id": {
"minLength": 1,
@@ -3681,6 +3710,12 @@
}
]
},
+ "included": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/dataInResponse"
+ }
+ },
"meta": {
"type": "object",
"additionalProperties": {
@@ -3734,7 +3769,8 @@
"enum": [
"tags"
],
- "type": "string"
+ "type": "string",
+ "additionalProperties": false
},
"toManyTagInRequest": {
"required": [
@@ -3760,7 +3796,7 @@
"links": {
"allOf": [
{
- "$ref": "#/components/schemas/linksInRelationshipObject"
+ "$ref": "#/components/schemas/linksInRelationship"
}
]
},
@@ -3804,7 +3840,7 @@
"links": {
"allOf": [
{
- "$ref": "#/components/schemas/linksInRelationshipObject"
+ "$ref": "#/components/schemas/linksInRelationship"
}
]
},
@@ -3849,7 +3885,7 @@
"links": {
"allOf": [
{
- "$ref": "#/components/schemas/linksInRelationshipObject"
+ "$ref": "#/components/schemas/linksInRelationship"
}
]
},
@@ -3966,6 +4002,12 @@
"$ref": "#/components/schemas/todoItemDataInResponse"
}
},
+ "included": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/dataInResponse"
+ }
+ },
"meta": {
"type": "object",
"additionalProperties": {
@@ -3984,11 +4026,7 @@
"type": "object",
"properties": {
"type": {
- "allOf": [
- {
- "$ref": "#/components/schemas/todoItemResourceType"
- }
- ]
+ "$ref": "#/components/schemas/todoItemResourceType"
},
"id": {
"minLength": 1,
@@ -4018,11 +4056,7 @@
"type": "object",
"properties": {
"type": {
- "allOf": [
- {
- "$ref": "#/components/schemas/todoItemResourceType"
- }
- ]
+ "$ref": "#/components/schemas/todoItemResourceType"
},
"attributes": {
"allOf": [
@@ -4042,53 +4076,48 @@
"additionalProperties": false
},
"todoItemDataInResponse": {
- "required": [
- "id",
- "links",
- "type"
- ],
- "type": "object",
- "properties": {
- "type": {
- "allOf": [
- {
- "$ref": "#/components/schemas/todoItemResourceType"
- }
- ]
- },
- "id": {
- "minLength": 1,
- "type": "string"
- },
- "attributes": {
- "allOf": [
- {
- "$ref": "#/components/schemas/todoItemAttributesInResponse"
- }
- ]
- },
- "relationships": {
- "allOf": [
- {
- "$ref": "#/components/schemas/todoItemRelationshipsInResponse"
- }
- ]
- },
- "links": {
- "allOf": [
- {
- "$ref": "#/components/schemas/linksInResourceObject"
- }
- ]
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/dataInResponse"
},
- "meta": {
+ {
+ "required": [
+ "links"
+ ],
"type": "object",
- "additionalProperties": {
- "type": "object",
- "nullable": true
- }
+ "properties": {
+ "attributes": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/todoItemAttributesInResponse"
+ }
+ ]
+ },
+ "relationships": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/todoItemRelationshipsInResponse"
+ }
+ ]
+ },
+ "links": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/linksInResourceData"
+ }
+ ]
+ },
+ "meta": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "object",
+ "nullable": true
+ }
+ }
+ },
+ "additionalProperties": false
}
- },
+ ],
"additionalProperties": false
},
"todoItemIdentifier": {
@@ -4099,11 +4128,7 @@
"type": "object",
"properties": {
"type": {
- "allOf": [
- {
- "$ref": "#/components/schemas/todoItemResourceType"
- }
- ]
+ "$ref": "#/components/schemas/todoItemResourceType"
},
"id": {
"minLength": 1,
@@ -4195,6 +4220,12 @@
}
]
},
+ "included": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/dataInResponse"
+ }
+ },
"meta": {
"type": "object",
"additionalProperties": {
@@ -4301,7 +4332,8 @@
"enum": [
"todoItems"
],
- "type": "string"
+ "type": "string",
+ "additionalProperties": false
}
}
}
diff --git a/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj b/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj
index 9de430b0f1..5335a96b42 100644
--- a/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj
+++ b/src/Examples/JsonApiDotNetCoreExample/JsonApiDotNetCoreExample.csproj
@@ -1,6 +1,8 @@
net8.0;net6.0
+ true
+ GeneratedSwagger
@@ -16,5 +18,6 @@
+
diff --git a/src/Examples/JsonApiDotNetCoreExample/Program.cs b/src/Examples/JsonApiDotNetCoreExample/Program.cs
index da02db1ba4..332235d491 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Program.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Program.cs
@@ -4,6 +4,7 @@
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Diagnostics;
using JsonApiDotNetCore.OpenApi;
+using JsonApiDotNetCoreExample;
using JsonApiDotNetCoreExample.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
@@ -16,7 +17,10 @@
WebApplication app = CreateWebApplication(args);
-await CreateDatabaseAsync(app.Services);
+if (!IsGeneratingOpenApiDocumentAtBuildTime())
+{
+ await CreateDatabaseAsync(app.Services);
+}
app.Run();
@@ -83,7 +87,7 @@ static void ConfigureServices(WebApplicationBuilder builder)
using (CodeTimingSessionManager.Current.Measure("AddOpenApi()"))
{
- builder.Services.AddOpenApi(mvcCoreBuilder);
+ builder.Services.AddOpenApi(mvcCoreBuilder, options => options.DocumentFilter());
}
}
@@ -112,6 +116,11 @@ static void ConfigurePipeline(WebApplication app)
app.MapControllers();
}
+static bool IsGeneratingOpenApiDocumentAtBuildTime()
+{
+ return Environment.GetCommandLineArgs().Any(argument => argument.Contains("GetDocument.Insider"));
+}
+
static async Task CreateDatabaseAsync(IServiceProvider serviceProvider)
{
await using AsyncServiceScope scope = serviceProvider.CreateAsyncScope();
diff --git a/src/Examples/JsonApiDotNetCoreExample/SetOpenApiServerAtBuildTimeFilter.cs b/src/Examples/JsonApiDotNetCoreExample/SetOpenApiServerAtBuildTimeFilter.cs
new file mode 100644
index 0000000000..894c0d0966
--- /dev/null
+++ b/src/Examples/JsonApiDotNetCoreExample/SetOpenApiServerAtBuildTimeFilter.cs
@@ -0,0 +1,25 @@
+using JetBrains.Annotations;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
+
+namespace JsonApiDotNetCoreExample;
+
+///
+/// This is normally not needed. It ensures the server URL is added to the OpenAPI file during build.
+///
+[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
+internal sealed class SetOpenApiServerAtBuildTimeFilter(IHttpContextAccessor httpContextAccessor) : IDocumentFilter
+{
+ private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor;
+
+ public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
+ {
+ if (_httpContextAccessor.HttpContext == null)
+ {
+ swaggerDoc.Servers.Add(new OpenApiServer
+ {
+ Url = "https://localhost:44340"
+ });
+ }
+ }
+}
diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/ColoredConsoleLogDelegatingHandler.cs b/src/Examples/JsonApiDotNetCoreExampleClient/ColoredConsoleLogDelegatingHandler.cs
new file mode 100644
index 0000000000..88679e112f
--- /dev/null
+++ b/src/Examples/JsonApiDotNetCoreExampleClient/ColoredConsoleLogDelegatingHandler.cs
@@ -0,0 +1,62 @@
+using JetBrains.Annotations;
+
+namespace JsonApiDotNetCoreExampleClient;
+
+///
+/// Writes incoming and outgoing HTTP messages to the console.
+///
+[UsedImplicitly]
+internal sealed class ColoredConsoleLogDelegatingHandler : DelegatingHandler
+{
+ protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ await LogRequestAsync(request, cancellationToken);
+
+ HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
+
+ await LogResponseAsync(response, cancellationToken);
+
+ return response;
+ }
+
+ private static async Task LogRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ {
+ using var _ = new ConsoleColorScope(ConsoleColor.Green);
+
+ Console.WriteLine($"--> {request}");
+ string? requestBody = request.Content != null ? await request.Content.ReadAsStringAsync(cancellationToken) : null;
+
+ if (!string.IsNullOrEmpty(requestBody))
+ {
+ Console.WriteLine();
+ Console.WriteLine(requestBody);
+ }
+ }
+
+ private static async Task LogResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken)
+ {
+ using var _ = new ConsoleColorScope(ConsoleColor.Cyan);
+
+ Console.WriteLine($"<-- {response}");
+ string responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
+
+ if (!string.IsNullOrEmpty(responseBody))
+ {
+ Console.WriteLine();
+ Console.WriteLine(responseBody);
+ }
+ }
+
+ private sealed class ConsoleColorScope : IDisposable
+ {
+ public ConsoleColorScope(ConsoleColor foregroundColor)
+ {
+ Console.ForegroundColor = foregroundColor;
+ }
+
+ public void Dispose()
+ {
+ Console.ResetColor();
+ }
+ }
+}
diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/ExampleApiClient.cs b/src/Examples/JsonApiDotNetCoreExampleClient/ExampleApiClient.cs
index 8a5b5d3c46..e55e79d97e 100644
--- a/src/Examples/JsonApiDotNetCoreExampleClient/ExampleApiClient.cs
+++ b/src/Examples/JsonApiDotNetCoreExampleClient/ExampleApiClient.cs
@@ -2,8 +2,6 @@
using JsonApiDotNetCore.OpenApi.Client;
using Newtonsoft.Json;
-// ReSharper disable UnusedParameterInPartialMethod
-
namespace JsonApiDotNetCoreExampleClient;
[UsedImplicitly(ImplicitUseTargetFlags.Itself)]
@@ -13,50 +11,8 @@ partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings)
{
SetSerializerSettingsForJsonApi(settings);
- // Optional: Makes the JSON easier to read when logged.
+#if DEBUG
settings.Formatting = Formatting.Indented;
- }
-
- // Optional: Log outgoing request to the console.
- partial void PrepareRequest(HttpClient client, HttpRequestMessage request, string url)
- {
- using var _ = new UsingConsoleColor(ConsoleColor.Green);
-
- Console.WriteLine($"--> {request}");
- string? requestBody = request.Content?.ReadAsStringAsync().GetAwaiter().GetResult();
-
- if (!string.IsNullOrEmpty(requestBody))
- {
- Console.WriteLine();
- Console.WriteLine(requestBody);
- }
- }
-
- // Optional: Log incoming response to the console.
- partial void ProcessResponse(HttpClient client, HttpResponseMessage response)
- {
- using var _ = new UsingConsoleColor(ConsoleColor.Cyan);
-
- Console.WriteLine($"<-- {response}");
- string responseBody = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
-
- if (!string.IsNullOrEmpty(responseBody))
- {
- Console.WriteLine();
- Console.WriteLine(responseBody);
- }
- }
-
- private sealed class UsingConsoleColor : IDisposable
- {
- public UsingConsoleColor(ConsoleColor foregroundColor)
- {
- Console.ForegroundColor = foregroundColor;
- }
-
- public void Dispose()
- {
- Console.ResetColor();
- }
+#endif
}
}
diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj b/src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj
index 1882a957a7..7d03f808b6 100644
--- a/src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj
+++ b/src/Examples/JsonApiDotNetCoreExampleClient/JsonApiDotNetCoreExampleClient.csproj
@@ -24,8 +24,8 @@
-
- http://localhost:14140/swagger/v1/swagger.json
+
+ /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions
diff --git a/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs b/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs
index b35f5723f2..a174a94747 100644
--- a/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs
+++ b/src/Examples/JsonApiDotNetCoreExampleClient/Program.cs
@@ -1,18 +1,28 @@
+using JsonApiDotNetCore.OpenApi.Client;
using JsonApiDotNetCoreExampleClient;
+#if DEBUG
+using var httpClient = new HttpClient(new ColoredConsoleLogDelegatingHandler
+{
+ InnerHandler = new HttpClientHandler()
+});
+#else
using var httpClient = new HttpClient();
-var apiClient = new ExampleApiClient("http://localhost:14140", httpClient);
+#endif
+
+var apiClient = new ExampleApiClient(httpClient);
PersonCollectionResponseDocument getResponse = await apiClient.GetPersonCollectionAsync(new Dictionary
{
["filter"] = "has(assignedTodoItems)",
["sort"] = "-lastName",
- ["page[size]"] = "5"
+ ["page[size]"] = "5",
+ ["include"] = "assignedTodoItems.tags"
});
foreach (PersonDataInResponse person in getResponse.Data)
{
- Console.WriteLine($"Found person {person.Id}: {person.Attributes.DisplayName}");
+ PrintPerson(person, getResponse.Included);
}
var patchRequest = new PersonPatchRequestDocument
@@ -31,23 +41,37 @@
using (apiClient.WithPartialAttributeSerialization(patchRequest, person => person.FirstName))
{
// Workaround for https://github.com/RicoSuter/NSwag/issues/2499.
- await TranslateAsync(() => apiClient.PatchPersonAsync(patchRequest.Data.Id, null, patchRequest));
+ await ApiResponse.TranslateAsync(() => apiClient.PatchPersonAsync(patchRequest.Data.Id, null, patchRequest));
}
Console.WriteLine("Press any key to close.");
Console.ReadKey();
-// ReSharper disable once UnusedLocalFunctionReturnValue
-static async Task TranslateAsync(Func> operation)
- where TResponse : class
+static void PrintPerson(PersonDataInResponse person, ICollection includes)
+{
+ ToManyTodoItemInResponse assignedTodoItems = person.Relationships.AssignedTodoItems;
+
+ Console.WriteLine($"Found person {person.Id}: {person.Attributes.DisplayName} with {assignedTodoItems.Data.Count} assigned todo-items:");
+
+ PrintRelatedTodoItems(assignedTodoItems.Data, includes);
+}
+
+static void PrintRelatedTodoItems(IEnumerable todoItemIdentifiers, ICollection includes)
{
- try
+ foreach (TodoItemIdentifier todoItemIdentifier in todoItemIdentifiers)
{
- return await operation();
+ TodoItemDataInResponse includedTodoItem = includes.OfType().Single(include => include.Id == todoItemIdentifier.Id);
+ Console.WriteLine($" TodoItem {includedTodoItem.Id}: {includedTodoItem.Attributes.Description}");
+
+ PrintRelatedTags(includedTodoItem.Relationships.Tags.Data, includes);
}
- catch (ApiException exception) when (exception.StatusCode == 204)
+}
+
+static void PrintRelatedTags(IEnumerable tagIdentifiers, ICollection includes)
+{
+ foreach (TagIdentifier tagIdentifier in tagIdentifiers)
{
- // Workaround for https://github.com/RicoSuter/NSwag/issues/2499
- return null;
+ TagDataInResponse includedTag = includes.OfType().Single(include => include.Id == tagIdentifier.Id);
+ Console.WriteLine($" Tag {includedTag.Id}: {includedTag.Attributes.Name}");
}
}
diff --git a/src/JsonApiDotNetCore.OpenApi.Client/ApiResponse.cs b/src/JsonApiDotNetCore.OpenApi.Client/ApiResponse.cs
index a94e5062dc..0c99f88209 100644
--- a/src/JsonApiDotNetCore.OpenApi.Client/ApiResponse.cs
+++ b/src/JsonApiDotNetCore.OpenApi.Client/ApiResponse.cs
@@ -23,4 +23,18 @@ public static class ApiResponse
return null;
}
}
+
+ public static async Task TranslateAsync(Func operation)
+ {
+ ArgumentGuard.NotNull(operation);
+
+ try
+ {
+ await operation();
+ }
+ catch (ApiException exception) when (exception.StatusCode == 204)
+ {
+ // Workaround for https://github.com/RicoSuter/NSwag/issues/2499
+ }
+ }
}
diff --git a/src/JsonApiDotNetCore.OpenApi/ConfigureMvcOptions.cs b/src/JsonApiDotNetCore.OpenApi/ConfigureMvcOptions.cs
new file mode 100644
index 0000000000..3bcc23587e
--- /dev/null
+++ b/src/JsonApiDotNetCore.OpenApi/ConfigureMvcOptions.cs
@@ -0,0 +1,42 @@
+using JsonApiDotNetCore.Middleware;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+
+namespace JsonApiDotNetCore.OpenApi;
+
+internal sealed class ConfigureMvcOptions : IConfigureOptions
+{
+ private readonly IControllerResourceMapping _controllerResourceMapping;
+ private readonly IJsonApiRoutingConvention _jsonApiRoutingConvention;
+
+ public ConfigureMvcOptions(IControllerResourceMapping controllerResourceMapping, IJsonApiRoutingConvention jsonApiRoutingConvention)
+ {
+ ArgumentGuard.NotNull(controllerResourceMapping);
+ ArgumentGuard.NotNull(jsonApiRoutingConvention);
+
+ _controllerResourceMapping = controllerResourceMapping;
+ _jsonApiRoutingConvention = jsonApiRoutingConvention;
+ }
+
+ public void Configure(MvcOptions options)
+ {
+ AddSwashbuckleCliCompatibility(options);
+ AddOpenApiEndpointConvention(options);
+ }
+
+ private void AddSwashbuckleCliCompatibility(MvcOptions options)
+ {
+ if (!options.Conventions.Any(convention => convention is IJsonApiRoutingConvention))
+ {
+ // See https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1957 for why this is needed.
+ options.Conventions.Insert(0, _jsonApiRoutingConvention);
+ }
+ }
+
+ private void AddOpenApiEndpointConvention(MvcOptions options)
+ {
+ var convention = new OpenApiEndpointConvention(_controllerResourceMapping);
+ options.Conventions.Add(convention);
+ }
+}
diff --git a/src/JsonApiDotNetCore.OpenApi/ConfigureSwaggerGenOptions.cs b/src/JsonApiDotNetCore.OpenApi/ConfigureSwaggerGenOptions.cs
new file mode 100644
index 0000000000..3a11157978
--- /dev/null
+++ b/src/JsonApiDotNetCore.OpenApi/ConfigureSwaggerGenOptions.cs
@@ -0,0 +1,93 @@
+using System.Reflection;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Middleware;
+using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects;
+using JsonApiDotNetCore.OpenApi.SwaggerComponents;
+using Microsoft.AspNetCore.Mvc.ApiExplorer;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Swashbuckle.AspNetCore.SwaggerGen;
+
+namespace JsonApiDotNetCore.OpenApi;
+
+internal sealed class ConfigureSwaggerGenOptions : IConfigureOptions
+{
+ private readonly IControllerResourceMapping _controllerResourceMapping;
+ private readonly JsonApiOperationIdSelector _operationIdSelector;
+ private readonly JsonApiSchemaIdSelector _schemaIdSelector;
+ private readonly IResourceGraph _resourceGraph;
+
+ public ConfigureSwaggerGenOptions(IControllerResourceMapping controllerResourceMapping, JsonApiOperationIdSelector operationIdSelector,
+ JsonApiSchemaIdSelector schemaIdSelector, IResourceGraph resourceGraph)
+ {
+ ArgumentGuard.NotNull(controllerResourceMapping);
+ ArgumentGuard.NotNull(operationIdSelector);
+ ArgumentGuard.NotNull(schemaIdSelector);
+ ArgumentGuard.NotNull(resourceGraph);
+
+ _controllerResourceMapping = controllerResourceMapping;
+ _operationIdSelector = operationIdSelector;
+ _schemaIdSelector = schemaIdSelector;
+ _resourceGraph = resourceGraph;
+ }
+
+ public void Configure(SwaggerGenOptions options)
+ {
+ options.SupportNonNullableReferenceTypes();
+ options.UseAllOfToExtendReferenceSchemas();
+
+ options.UseAllOfForInheritance();
+ options.SelectDiscriminatorNameUsing(_ => "type");
+ options.SelectDiscriminatorValueUsing(clrType => _resourceGraph.GetResourceType(clrType).PublicName);
+ options.SelectSubTypesUsing(GetConstructedTypesForResourceData);
+
+ SetOperationInfo(options, _controllerResourceMapping);
+ SetSchemaIdSelector(options);
+
+ options.DocumentFilter();
+ options.DocumentFilter();
+ options.OperationFilter();
+ }
+
+ private IEnumerable GetConstructedTypesForResourceData(Type baseType)
+ {
+ if (baseType != typeof(ResourceData))
+ {
+ return [];
+ }
+
+ List derivedTypes = [];
+
+ foreach (ResourceType resourceType in _resourceGraph.GetResourceTypes())
+ {
+ Type constructedType = typeof(ResourceDataInResponse<>).MakeGenericType(resourceType.ClrType);
+ derivedTypes.Add(constructedType);
+ }
+
+ return derivedTypes;
+ }
+
+ private void SetOperationInfo(SwaggerGenOptions swaggerGenOptions, IControllerResourceMapping controllerResourceMapping)
+ {
+ swaggerGenOptions.TagActionsBy(description => GetOperationTags(description, controllerResourceMapping));
+ swaggerGenOptions.CustomOperationIds(_operationIdSelector.GetOperationId);
+ }
+
+ private static IList GetOperationTags(ApiDescription description, IControllerResourceMapping controllerResourceMapping)
+ {
+ MethodInfo actionMethod = description.ActionDescriptor.GetActionMethod();
+ ResourceType? resourceType = controllerResourceMapping.GetResourceTypeForController(actionMethod.ReflectedType);
+
+ if (resourceType == null)
+ {
+ throw new NotSupportedException("Only JsonApiDotNetCore endpoints are supported.");
+ }
+
+ return [resourceType.PublicName];
+ }
+
+ private void SetSchemaIdSelector(SwaggerGenOptions swaggerGenOptions)
+ {
+ swaggerGenOptions.CustomSchemaIds(_schemaIdSelector.GetSchemaId);
+ }
+}
diff --git a/src/JsonApiDotNetCore.OpenApi/IncludeDependencyScanner.cs b/src/JsonApiDotNetCore.OpenApi/IncludeDependencyScanner.cs
new file mode 100644
index 0000000000..4507268793
--- /dev/null
+++ b/src/JsonApiDotNetCore.OpenApi/IncludeDependencyScanner.cs
@@ -0,0 +1,32 @@
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Resources.Annotations;
+
+namespace JsonApiDotNetCore.OpenApi;
+
+internal sealed class IncludeDependencyScanner
+{
+ ///
+ /// Returns all related resource types that are reachable from the specified resource type. May include itself.
+ ///
+ public IReadOnlySet GetReachableRelatedTypes(ResourceType resourceType)
+ {
+ ArgumentGuard.NotNull(resourceType);
+
+ var resourceTypesFound = new HashSet();
+ AddTypesFromRelationships(resourceType.Relationships, resourceTypesFound);
+ return resourceTypesFound;
+ }
+
+ private static void AddTypesFromRelationships(IEnumerable relationships, ISet resourceTypesFound)
+ {
+ foreach (RelationshipAttribute relationship in relationships)
+ {
+ ResourceType resourceType = relationship.RightType;
+
+ if (resourceTypesFound.Add(resourceType))
+ {
+ AddTypesFromRelationships(resourceType.Relationships, resourceTypesFound);
+ }
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/NullableResourceIdentifierResponseDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/NullableResourceIdentifierResponseDocument.cs
index 1d46bd26cd..f5b1bee34a 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/NullableResourceIdentifierResponseDocument.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/NullableResourceIdentifierResponseDocument.cs
@@ -10,11 +10,11 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents;
// Types in the current namespace are never touched by ASP.NET ModelState validation, therefore using a non-nullable reference type for a property does not
// imply this property is required. Instead, we use [Required] explicitly, because this is how Swashbuckle is instructed to mark properties as required.
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
-internal sealed class NullableResourceIdentifierResponseDocument : NullableSingleData>
+internal sealed class NullableResourceIdentifierResponseDocument : NullableSingleData>
where TResource : IIdentifiable
{
[JsonPropertyName("jsonapi")]
- public JsonapiObject Jsonapi { get; set; } = null!;
+ public Jsonapi Jsonapi { get; set; } = null!;
[Required]
[JsonPropertyName("links")]
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/NullableSecondaryResourceResponseDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/NullableSecondaryResourceResponseDocument.cs
index 4c141e55c5..846cb2abbe 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/NullableSecondaryResourceResponseDocument.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/NullableSecondaryResourceResponseDocument.cs
@@ -8,16 +8,19 @@
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
-internal sealed class NullableSecondaryResourceResponseDocument : NullableSingleData>
+internal sealed class NullableSecondaryResourceResponseDocument : NullableSingleData>
where TResource : IIdentifiable
{
[JsonPropertyName("jsonapi")]
- public JsonapiObject Jsonapi { get; set; } = null!;
+ public Jsonapi Jsonapi { get; set; } = null!;
[Required]
[JsonPropertyName("links")]
public LinksInResourceDocument Links { get; set; } = null!;
+ [JsonPropertyName("included")]
+ public IList Included { get; set; } = null!;
+
[JsonPropertyName("meta")]
public IDictionary Meta { get; set; } = null!;
}
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/PrimaryResourceResponseDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/PrimaryResourceResponseDocument.cs
index f66048a422..b4c5350a9a 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/PrimaryResourceResponseDocument.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/PrimaryResourceResponseDocument.cs
@@ -8,16 +8,19 @@
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
-internal sealed class PrimaryResourceResponseDocument : SingleData>
+internal sealed class PrimaryResourceResponseDocument : SingleData>
where TResource : IIdentifiable
{
[JsonPropertyName("jsonapi")]
- public JsonapiObject Jsonapi { get; set; } = null!;
+ public Jsonapi Jsonapi { get; set; } = null!;
[Required]
[JsonPropertyName("links")]
public LinksInResourceDocument Links { get; set; } = null!;
+ [JsonPropertyName("included")]
+ public IList Included { get; set; } = null!;
+
[JsonPropertyName("meta")]
public IDictionary Meta { get; set; } = null!;
}
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceCollectionResponseDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceCollectionResponseDocument.cs
index 109c2cde50..9eb76586a1 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceCollectionResponseDocument.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceCollectionResponseDocument.cs
@@ -8,16 +8,19 @@
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
-internal sealed class ResourceCollectionResponseDocument : ManyData>
+internal sealed class ResourceCollectionResponseDocument : ManyData>
where TResource : IIdentifiable
{
[JsonPropertyName("jsonapi")]
- public JsonapiObject Jsonapi { get; set; } = null!;
+ public Jsonapi Jsonapi { get; set; } = null!;
[Required]
[JsonPropertyName("links")]
public LinksInResourceCollectionDocument Links { get; set; } = null!;
+ [JsonPropertyName("included")]
+ public IList Included { get; set; } = null!;
+
[JsonPropertyName("meta")]
public IDictionary Meta { get; set; } = null!;
}
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierCollectionResponseDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierCollectionResponseDocument.cs
index d0b7ca4422..ed262ca324 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierCollectionResponseDocument.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierCollectionResponseDocument.cs
@@ -8,11 +8,11 @@
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
-internal sealed class ResourceIdentifierCollectionResponseDocument : ManyData>
+internal sealed class ResourceIdentifierCollectionResponseDocument : ManyData>
where TResource : IIdentifiable
{
[JsonPropertyName("jsonapi")]
- public JsonapiObject Jsonapi { get; set; } = null!;
+ public Jsonapi Jsonapi { get; set; } = null!;
[Required]
[JsonPropertyName("links")]
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierResponseDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierResponseDocument.cs
index dd479129fb..be16ed69ef 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierResponseDocument.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierResponseDocument.cs
@@ -8,11 +8,11 @@
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
-internal sealed class ResourceIdentifierResponseDocument : SingleData>
+internal sealed class ResourceIdentifierResponseDocument : SingleData>
where TResource : IIdentifiable
{
[JsonPropertyName("jsonapi")]
- public JsonapiObject Jsonapi { get; set; } = null!;
+ public Jsonapi Jsonapi { get; set; } = null!;
[Required]
[JsonPropertyName("links")]
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePatchRequestDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePatchRequestDocument.cs
index f76e871bc0..c06427b29c 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePatchRequestDocument.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePatchRequestDocument.cs
@@ -5,5 +5,5 @@
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
-internal sealed class ResourcePatchRequestDocument : SingleData>
+internal sealed class ResourcePatchRequestDocument : SingleData>
where TResource : IIdentifiable;
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePostRequestDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePostRequestDocument.cs
index 1d1a5e8651..1c9975b3e4 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePostRequestDocument.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePostRequestDocument.cs
@@ -5,5 +5,5 @@
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
-internal sealed class ResourcePostRequestDocument : SingleData>
+internal sealed class ResourcePostRequestDocument : SingleData>
where TResource : IIdentifiable;
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/SecondaryResourceResponseDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/SecondaryResourceResponseDocument.cs
index e895e2c22d..6d6db39bce 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/SecondaryResourceResponseDocument.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/SecondaryResourceResponseDocument.cs
@@ -8,16 +8,19 @@
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
-internal sealed class SecondaryResourceResponseDocument : SingleData>
+internal sealed class SecondaryResourceResponseDocument : SingleData>
where TResource : IIdentifiable
{
[JsonPropertyName("jsonapi")]
- public JsonapiObject Jsonapi { get; set; } = null!;
+ public Jsonapi Jsonapi { get; set; } = null!;
[Required]
[JsonPropertyName("links")]
public LinksInResourceDocument Links { get; set; } = null!;
+ [JsonPropertyName("included")]
+ public IList Included { get; set; } = null!;
+
[JsonPropertyName("meta")]
public IDictionary Meta { get; set; } = null!;
}
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/JsonapiObject.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Jsonapi.cs
similarity index 93%
rename from src/JsonApiDotNetCore.OpenApi/JsonApiObjects/JsonapiObject.cs
rename to src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Jsonapi.cs
index 8ad3074e4f..dcfc6ae2ca 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/JsonapiObject.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Jsonapi.cs
@@ -4,7 +4,7 @@
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
-internal sealed class JsonapiObject
+internal sealed class Jsonapi
{
[JsonPropertyName("version")]
public string Version { get; set; } = null!;
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInRelationshipObject.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInRelationship.cs
similarity index 89%
rename from src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInRelationshipObject.cs
rename to src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInRelationship.cs
index 16b9735dac..f67c02f79b 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInRelationshipObject.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInRelationship.cs
@@ -5,7 +5,7 @@
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Links;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
-internal sealed class LinksInRelationshipObject
+internal sealed class LinksInRelationship
{
[Required]
[JsonPropertyName("self")]
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceObject.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceData.cs
similarity index 87%
rename from src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceObject.cs
rename to src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceData.cs
index 552cc703e3..95d55fc545 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceObject.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Links/LinksInResourceData.cs
@@ -5,7 +5,7 @@
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Links;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
-internal sealed class LinksInResourceObject
+internal sealed class LinksInResourceData
{
[Required]
[JsonPropertyName("self")]
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ManyData.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ManyData.cs
index 5f80fd0f2b..34099658d9 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ManyData.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ManyData.cs
@@ -7,7 +7,7 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
internal abstract class ManyData
- where TData : ResourceIdentifierObject
+ where TData : class, IResourceIdentity
{
[Required]
[JsonPropertyName("data")]
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/NullableSingleData.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/NullableSingleData.cs
index 27063648cb..5bfad0cabe 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/NullableSingleData.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/NullableSingleData.cs
@@ -7,7 +7,7 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
internal abstract class NullableSingleData
- where TData : ResourceIdentifierObject
+ where TData : class, IResourceIdentity
{
[Required]
[JsonPropertyName("data")]
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/NullableToOneRelationshipInRequest.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/NullableToOneRelationshipInRequest.cs
index 2556f7d2cf..95b7821f48 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/NullableToOneRelationshipInRequest.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/NullableToOneRelationshipInRequest.cs
@@ -5,5 +5,5 @@
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
-internal sealed class NullableToOneRelationshipInRequest : NullableSingleData>
+internal sealed class NullableToOneRelationshipInRequest : NullableSingleData>
where TResource : IIdentifiable;
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/NullableToOneRelationshipInResponse.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/NullableToOneRelationshipInResponse.cs
index fdc45b811a..ae0a6ed539 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/NullableToOneRelationshipInResponse.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/NullableToOneRelationshipInResponse.cs
@@ -8,12 +8,12 @@
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
-internal sealed class NullableToOneRelationshipInResponse : NullableSingleData>
+internal sealed class NullableToOneRelationshipInResponse : NullableSingleData>
where TResource : IIdentifiable
{
[Required]
[JsonPropertyName("links")]
- public LinksInRelationshipObject Links { get; set; } = null!;
+ public LinksInRelationship Links { get; set; } = null!;
[JsonPropertyName("meta")]
public IDictionary Meta { get; set; } = null!;
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToManyRelationshipInRequest.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToManyRelationshipInRequest.cs
index 1161ca8d37..e2a3865ba8 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToManyRelationshipInRequest.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToManyRelationshipInRequest.cs
@@ -5,5 +5,5 @@
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
-internal sealed class ToManyRelationshipInRequest : ManyData>
+internal sealed class ToManyRelationshipInRequest : ManyData>
where TResource : IIdentifiable;
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToManyRelationshipInResponse.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToManyRelationshipInResponse.cs
index 14380f025e..51436b463c 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToManyRelationshipInResponse.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToManyRelationshipInResponse.cs
@@ -8,12 +8,12 @@
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
-internal sealed class ToManyRelationshipInResponse : ManyData>
+internal sealed class ToManyRelationshipInResponse : ManyData>
where TResource : IIdentifiable
{
[Required]
[JsonPropertyName("links")]
- public LinksInRelationshipObject Links { get; set; } = null!;
+ public LinksInRelationship Links { get; set; } = null!;
[JsonPropertyName("meta")]
public IDictionary Meta { get; set; } = null!;
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToOneRelationshipInRequest.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToOneRelationshipInRequest.cs
index 8d377cea65..7fdc2275d0 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToOneRelationshipInRequest.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToOneRelationshipInRequest.cs
@@ -5,5 +5,5 @@
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
-internal sealed class ToOneRelationshipInRequest : SingleData>
+internal sealed class ToOneRelationshipInRequest : SingleData>
where TResource : IIdentifiable;
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToOneRelationshipInResponse.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToOneRelationshipInResponse.cs
index a867dffdc0..94b11f630e 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToOneRelationshipInResponse.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToOneRelationshipInResponse.cs
@@ -8,12 +8,12 @@
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
-internal sealed class ToOneRelationshipInResponse : SingleData>
+internal sealed class ToOneRelationshipInResponse : SingleData>
where TResource : IIdentifiable
{
[Required]
[JsonPropertyName("links")]
- public LinksInRelationshipObject Links { get; set; } = null!;
+ public LinksInRelationship Links { get; set; } = null!;
[JsonPropertyName("meta")]
public IDictionary Meta { get; set; } = null!;
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/IResourceIdentity.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/IResourceIdentity.cs
new file mode 100644
index 0000000000..46ad6aae8c
--- /dev/null
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/IResourceIdentity.cs
@@ -0,0 +1,10 @@
+using JetBrains.Annotations;
+
+namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects;
+
+[UsedImplicitly(ImplicitUseTargetFlags.Members)]
+internal interface IResourceIdentity
+{
+ string Type { get; set; }
+ string Id { get; set; }
+}
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceData.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceData.cs
new file mode 100644
index 0000000000..21fcf96dff
--- /dev/null
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceData.cs
@@ -0,0 +1,17 @@
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json.Serialization;
+using JetBrains.Annotations;
+
+namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects;
+
+[UsedImplicitly(ImplicitUseTargetFlags.Members)]
+internal abstract class ResourceData : IResourceIdentity
+{
+ [Required]
+ [JsonPropertyName("type")]
+ public string Type { get; set; } = null!;
+
+ [Required]
+ [JsonPropertyName("id")]
+ public string Id { get; set; } = null!;
+}
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceObjectInPatchRequest.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceDataInPatchRequest.cs
similarity index 85%
rename from src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceObjectInPatchRequest.cs
rename to src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceDataInPatchRequest.cs
index 7263ededd4..248433afc4 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceObjectInPatchRequest.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceDataInPatchRequest.cs
@@ -5,7 +5,7 @@
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
-internal sealed class ResourceObjectInPatchRequest : ResourceIdentifierObject
+internal sealed class ResourceDataInPatchRequest : ResourceData
where TResource : IIdentifiable
{
[JsonPropertyName("attributes")]
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceObjectInPostRequest.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceDataInPostRequest.cs
similarity index 85%
rename from src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceObjectInPostRequest.cs
rename to src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceDataInPostRequest.cs
index f886883b4e..7310620ad0 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceObjectInPostRequest.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceDataInPostRequest.cs
@@ -5,7 +5,7 @@
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
-internal sealed class ResourceObjectInPostRequest : ResourceIdentifierObject
+internal sealed class ResourceDataInPostRequest : ResourceData
where TResource : IIdentifiable
{
[JsonPropertyName("attributes")]
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceObjectInResponse.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceDataInResponse.cs
similarity index 83%
rename from src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceObjectInResponse.cs
rename to src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceDataInResponse.cs
index ae61d9822c..c7ed1d02fd 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceObjectInResponse.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceDataInResponse.cs
@@ -7,7 +7,7 @@
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
-internal sealed class ResourceObjectInResponse : ResourceIdentifierObject
+internal sealed class ResourceDataInResponse : ResourceData
where TResource : IIdentifiable
{
[JsonPropertyName("attributes")]
@@ -18,7 +18,7 @@ internal sealed class ResourceObjectInResponse : ResourceIdentifierOb
[Required]
[JsonPropertyName("links")]
- public LinksInResourceObject Links { get; set; } = null!;
+ public LinksInResourceData Links { get; set; } = null!;
[JsonPropertyName("meta")]
public IDictionary Meta { get; set; } = null!;
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceIdentifierObject.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceIdentifier.cs
similarity index 63%
rename from src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceIdentifierObject.cs
rename to src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceIdentifier.cs
index 9a779627ea..3b8b8e5272 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceIdentifierObject.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ResourceObjects/ResourceIdentifier.cs
@@ -1,16 +1,12 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
-using JetBrains.Annotations;
using JsonApiDotNetCore.Resources;
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects;
// ReSharper disable once UnusedTypeParameter
-internal sealed class ResourceIdentifierObject : ResourceIdentifierObject
- where TResource : IIdentifiable;
-
-[UsedImplicitly(ImplicitUseTargetFlags.Members)]
-internal class ResourceIdentifierObject
+internal sealed class ResourceIdentifier : IResourceIdentity
+ where TResource : IIdentifiable
{
[Required]
[JsonPropertyName("type")]
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/SingleData.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/SingleData.cs
index 019fc58c2d..451b2a974f 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/SingleData.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/SingleData.cs
@@ -7,7 +7,7 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects;
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
internal abstract class SingleData
- where TData : ResourceIdentifierObject
+ where TData : class, IResourceIdentity
{
[Required]
[JsonPropertyName("data")]
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs
index 2da76a3931..17642015ff 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs
@@ -35,14 +35,15 @@ internal sealed class JsonApiOperationIdSelector
};
private readonly IControllerResourceMapping _controllerResourceMapping;
- private readonly JsonNamingPolicy? _namingPolicy;
+ private readonly IJsonApiOptions _options;
- public JsonApiOperationIdSelector(IControllerResourceMapping controllerResourceMapping, JsonNamingPolicy? namingPolicy)
+ public JsonApiOperationIdSelector(IControllerResourceMapping controllerResourceMapping, IJsonApiOptions options)
{
ArgumentGuard.NotNull(controllerResourceMapping);
+ ArgumentGuard.NotNull(options);
_controllerResourceMapping = controllerResourceMapping;
- _namingPolicy = namingPolicy;
+ _options = options;
}
public string GetOperationId(ApiDescription endpoint)
@@ -122,6 +123,7 @@ private string ApplyTemplate(string operationIdTemplate, ResourceType resourceTy
// @formatter:wrap_before_first_method_call true restore
// @formatter:wrap_chained_method_calls restore
- return _namingPolicy != null ? _namingPolicy.ConvertName(pascalCaseOperationId) : pascalCaseOperationId;
+ JsonNamingPolicy? namingPolicy = _options.SerializerOptions.PropertyNamingPolicy;
+ return namingPolicy != null ? namingPolicy.ConvertName(pascalCaseOperationId) : pascalCaseOperationId;
}
}
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiRequestFormatMetadataProvider.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiRequestFormatMetadataProvider.cs
index 309500954b..97aed20050 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiRequestFormatMetadataProvider.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiRequestFormatMetadataProvider.cs
@@ -9,7 +9,7 @@ namespace JsonApiDotNetCore.OpenApi;
internal sealed class JsonApiRequestFormatMetadataProvider : IInputFormatter, IApiRequestFormatMetadataProvider
{
- private static readonly Type[] JsonApiRequestObjectOpenType =
+ private static readonly Type[] JsonApiRequestOpenTypes =
[
typeof(ToManyRelationshipInRequest<>),
typeof(ToOneRelationshipInRequest<>),
@@ -36,8 +36,7 @@ public IReadOnlyList GetSupportedContentTypes(string contentType, Type o
ArgumentGuard.NotNullNorEmpty(contentType);
ArgumentGuard.NotNull(objectType);
- if (contentType == HeaderConstants.MediaType && objectType.IsGenericType &&
- JsonApiRequestObjectOpenType.Contains(objectType.GetGenericTypeDefinition()))
+ if (contentType == HeaderConstants.MediaType && objectType.IsGenericType && JsonApiRequestOpenTypes.Contains(objectType.GetGenericTypeDefinition()))
{
return new MediaTypeCollection
{
diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs
index c62bf662ff..cd9816fbde 100644
--- a/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs
+++ b/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs
@@ -9,14 +9,16 @@ namespace JsonApiDotNetCore.OpenApi;
internal sealed class JsonApiSchemaIdSelector
{
- private static readonly IDictionary OpenTypeToSchemaTemplateMap = new Dictionary
+ private const string ResourceTypeSchemaIdTemplate = "[ResourceName] Resource Type";
+
+ private static readonly IDictionary TypeToSchemaTemplateMap = new Dictionary
{
[typeof(ResourcePostRequestDocument<>)] = "[ResourceName] Post Request Document",
[typeof(ResourcePatchRequestDocument<>)] = "[ResourceName] Patch Request Document",
- [typeof(ResourceObjectInPostRequest<>)] = "[ResourceName] Data In Post Request",
+ [typeof(ResourceDataInPostRequest<>)] = "[ResourceName] Data In Post Request",
[typeof(AttributesInPostRequest<>)] = "[ResourceName] Attributes In Post Request",
[typeof(RelationshipsInPostRequest<>)] = "[ResourceName] Relationships In Post Request",
- [typeof(ResourceObjectInPatchRequest<>)] = "[ResourceName] Data In Patch Request",
+ [typeof(ResourceDataInPatchRequest<>)] = "[ResourceName] Data In Patch Request",
[typeof(AttributesInPatchRequest<>)] = "[ResourceName] Attributes In Patch Request",
[typeof(RelationshipsInPatchRequest<>)] = "[ResourceName] Relationships In Patch Request",
[typeof(ToOneRelationshipInRequest<>)] = "To One [ResourceName] In Request",
@@ -32,21 +34,23 @@ internal sealed class JsonApiSchemaIdSelector
[typeof(ToOneRelationshipInResponse<>)] = "To One [ResourceName] In Response",
[typeof(NullableToOneRelationshipInResponse<>)] = "Nullable To One [ResourceName] In Response",
[typeof(ToManyRelationshipInResponse<>)] = "To Many [ResourceName] In Response",
- [typeof(ResourceObjectInResponse<>)] = "[ResourceName] Data In Response",
+ [typeof(ResourceData)] = "Data In Response",
+ [typeof(ResourceDataInResponse<>)] = "[ResourceName] Data In Response",
[typeof(AttributesInResponse<>)] = "[ResourceName] Attributes In Response",
[typeof(RelationshipsInResponse<>)] = "[ResourceName] Relationships In Response",
- [typeof(ResourceIdentifierObject<>)] = "[ResourceName] Identifier"
+ [typeof(ResourceIdentifier<>)] = "[ResourceName] Identifier"
};
- private readonly JsonNamingPolicy? _namingPolicy;
private readonly IResourceGraph _resourceGraph;
+ private readonly IJsonApiOptions _options;
- public JsonApiSchemaIdSelector(JsonNamingPolicy? namingPolicy, IResourceGraph resourceGraph)
+ public JsonApiSchemaIdSelector(IResourceGraph resourceGraph, IJsonApiOptions options)
{
ArgumentGuard.NotNull(resourceGraph);
+ ArgumentGuard.NotNull(options);
- _namingPolicy = namingPolicy;
_resourceGraph = resourceGraph;
+ _options = options;
}
public string GetSchemaId(Type type)
@@ -60,23 +64,44 @@ public string GetSchemaId(Type type)
return resourceType.PublicName.Singularize();
}
- if (type.IsConstructedGenericType && OpenTypeToSchemaTemplateMap.ContainsKey(type.GetGenericTypeDefinition()))
+ if (type.IsConstructedGenericType)
{
Type openType = type.GetGenericTypeDefinition();
- Type resourceClrType = type.GetGenericArguments().First();
- resourceType = _resourceGraph.FindResourceType(resourceClrType);
- if (resourceType == null)
+ if (TypeToSchemaTemplateMap.TryGetValue(openType, out string? schemaTemplate))
{
- throw new UnreachableCodeException();
+ Type resourceClrType = type.GetGenericArguments().First();
+ resourceType = _resourceGraph.GetResourceType(resourceClrType);
+
+ return ApplySchemaTemplate(schemaTemplate, resourceType);
}
+ }
+ else
+ {
+ if (TypeToSchemaTemplateMap.TryGetValue(type, out string? schemaTemplate))
+ {
+ return ApplySchemaTemplate(schemaTemplate, null);
+ }
+ }
- string pascalCaseSchemaId = OpenTypeToSchemaTemplateMap[openType].Replace("[ResourceName]", resourceType.PublicName.Singularize()).ToPascalCase();
+ // Used for a fixed set of non-generic types, such as Jsonapi, LinksInResourceCollectionDocument etc.
+ return ApplySchemaTemplate(type.Name, null);
+ }
- return _namingPolicy != null ? _namingPolicy.ConvertName(pascalCaseSchemaId) : pascalCaseSchemaId;
- }
+ private string ApplySchemaTemplate(string schemaTemplate, ResourceType? resourceType)
+ {
+ string pascalCaseSchemaId = resourceType != null
+ ? schemaTemplate.Replace("[ResourceName]", resourceType.PublicName.Singularize()).ToPascalCase()
+ : schemaTemplate.ToPascalCase();
+
+ JsonNamingPolicy? namingPolicy = _options.SerializerOptions.PropertyNamingPolicy;
+ return namingPolicy != null ? namingPolicy.ConvertName(pascalCaseSchemaId) : pascalCaseSchemaId;
+ }
+
+ public string GetSchemaId(ResourceType resourceType)
+ {
+ ArgumentGuard.NotNull(resourceType);
- // Used for a fixed set of types, such as JsonApiObject, LinksInResourceCollectionDocument etc.
- return _namingPolicy != null ? _namingPolicy.ConvertName(type.Name) : type.Name;
+ return ApplySchemaTemplate(ResourceTypeSchemaIdTemplate, resourceType);
}
}
diff --git a/src/JsonApiDotNetCore.OpenApi/ResourceFieldValidationMetadataProvider.cs b/src/JsonApiDotNetCore.OpenApi/ResourceFieldValidationMetadataProvider.cs
index e40ab57618..82d59ac40d 100644
--- a/src/JsonApiDotNetCore.OpenApi/ResourceFieldValidationMetadataProvider.cs
+++ b/src/JsonApiDotNetCore.OpenApi/ResourceFieldValidationMetadataProvider.cs
@@ -9,8 +9,7 @@ namespace JsonApiDotNetCore.OpenApi;
internal sealed class ResourceFieldValidationMetadataProvider
{
- private readonly bool _validateModelState;
- private readonly NullabilityInfoContext _nullabilityContext = new();
+ private readonly IJsonApiOptions _options;
private readonly IModelMetadataProvider _modelMetadataProvider;
public ResourceFieldValidationMetadataProvider(IJsonApiOptions options, IModelMetadataProvider modelMetadataProvider)
@@ -18,7 +17,7 @@ public ResourceFieldValidationMetadataProvider(IJsonApiOptions options, IModelMe
ArgumentGuard.NotNull(options);
ArgumentGuard.NotNull(modelMetadataProvider);
- _validateModelState = options.ValidateModelState;
+ _options = options;
_modelMetadataProvider = modelMetadataProvider;
}
@@ -33,12 +32,13 @@ public bool IsNullable(ResourceFieldAttribute field)
bool hasRequiredAttribute = field.Property.HasAttribute();
- if (_validateModelState && hasRequiredAttribute)
+ if (_options.ValidateModelState && hasRequiredAttribute)
{
return false;
}
- NullabilityInfo nullabilityInfo = _nullabilityContext.Create(field.Property);
+ NullabilityInfoContext nullabilityContext = new();
+ NullabilityInfo nullabilityInfo = nullabilityContext.Create(field.Property);
return nullabilityInfo.ReadState != NullabilityState.NotNull;
}
@@ -48,7 +48,7 @@ public bool IsRequired(ResourceFieldAttribute field)
bool hasRequiredAttribute = field.Property.HasAttribute();
- if (!_validateModelState)
+ if (!_options.ValidateModelState)
{
return hasRequiredAttribute;
}
@@ -58,7 +58,8 @@ public bool IsRequired(ResourceFieldAttribute field)
return false;
}
- NullabilityInfo nullabilityInfo = _nullabilityContext.Create(field.Property);
+ NullabilityInfoContext nullabilityContext = new();
+ NullabilityInfo nullabilityInfo = nullabilityContext.Create(field.Property);
bool isRequiredValueType = field.Property.PropertyType.IsValueType && hasRequiredAttribute && nullabilityInfo.ReadState == NullabilityState.NotNull;
if (isRequiredValueType)
diff --git a/src/JsonApiDotNetCore.OpenApi/SchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SchemaGenerator.cs
new file mode 100644
index 0000000000..7bc287fe95
--- /dev/null
+++ b/src/JsonApiDotNetCore.OpenApi/SchemaGenerator.cs
@@ -0,0 +1,518 @@
+// This file is a copy of https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs
+// It was patched to fix broken inheritance using allOf. Changed code is marked with PATCH-START/PATCH-END comments.
+
+// PATCH-START
+#nullable disable
+#pragma warning disable
+// PATCH-END
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.ComponentModel.DataAnnotations;
+using System.Globalization;
+using System.Linq;
+using System.Reflection;
+using System.Text.Json;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ApiExplorer;
+using Microsoft.OpenApi.Models;
+
+// PATCH-START
+namespace Swashbuckle.AspNetCore.SwaggerGen.Patched
+//namespace Swashbuckle.AspNetCore.SwaggerGen
+// PATCH-END
+{
+ public class SchemaGenerator : ISchemaGenerator
+ {
+ private readonly SchemaGeneratorOptions _generatorOptions;
+ private readonly ISerializerDataContractResolver _serializerDataContractResolver;
+
+ public SchemaGenerator(SchemaGeneratorOptions generatorOptions, ISerializerDataContractResolver serializerDataContractResolver)
+ {
+ _generatorOptions = generatorOptions;
+ _serializerDataContractResolver = serializerDataContractResolver;
+ }
+
+ public OpenApiSchema GenerateSchema(
+ Type modelType,
+ SchemaRepository schemaRepository,
+ MemberInfo memberInfo = null,
+ ParameterInfo parameterInfo = null,
+ ApiParameterRouteInfo routeInfo = null)
+ {
+ if (memberInfo != null)
+ return GenerateSchemaForMember(modelType, schemaRepository, memberInfo);
+
+ if (parameterInfo != null)
+ return GenerateSchemaForParameter(modelType, schemaRepository, parameterInfo, routeInfo);
+
+ return GenerateSchemaForType(modelType, schemaRepository);
+ }
+
+ private OpenApiSchema GenerateSchemaForMember(
+ Type modelType,
+ SchemaRepository schemaRepository,
+ MemberInfo memberInfo,
+ DataProperty dataProperty = null)
+ {
+ var dataContract = GetDataContractFor(modelType);
+
+ var schema = _generatorOptions.UseOneOfForPolymorphism && IsBaseTypeWithKnownTypesDefined(dataContract, out var knownTypesDataContracts)
+ ? GeneratePolymorphicSchema(dataContract, schemaRepository, knownTypesDataContracts)
+ : GenerateConcreteSchema(dataContract, schemaRepository);
+
+ if (_generatorOptions.UseAllOfToExtendReferenceSchemas && schema.Reference != null)
+ {
+ schema.AllOf = new[] { new OpenApiSchema { Reference = schema.Reference } };
+ schema.Reference = null;
+ }
+
+ if (schema.Reference == null)
+ {
+ var customAttributes = memberInfo.GetInlineAndMetadataAttributes();
+
+ // Nullable, ReadOnly & WriteOnly are only relevant for Schema "properties" (i.e. where dataProperty is non-null)
+ if (dataProperty != null)
+ {
+ var requiredAttribute = customAttributes.OfType().FirstOrDefault();
+ schema.Nullable = _generatorOptions.SupportNonNullableReferenceTypes
+ ? dataProperty.IsNullable && requiredAttribute==null && !memberInfo.IsNonNullableReferenceType()
+ : dataProperty.IsNullable && requiredAttribute==null;
+
+ schema.ReadOnly = dataProperty.IsReadOnly;
+ schema.WriteOnly = dataProperty.IsWriteOnly;
+ schema.MinLength = modelType == typeof(string) && requiredAttribute is { AllowEmptyStrings: false } ? 1 : null;
+ }
+
+ var defaultValueAttribute = customAttributes.OfType().FirstOrDefault();
+ if (defaultValueAttribute != null)
+ {
+ var defaultAsJson = dataContract.JsonConverter(defaultValueAttribute.Value);
+ schema.Default = OpenApiAnyFactory.CreateFromJson(defaultAsJson);
+ }
+
+ var obsoleteAttribute = customAttributes.OfType().FirstOrDefault();
+ if (obsoleteAttribute != null)
+ {
+ schema.Deprecated = true;
+ }
+
+ // NullableAttribute behaves diffrently for Dictionaries
+ if (schema.AdditionalPropertiesAllowed && modelType.IsGenericType && modelType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
+ {
+ schema.AdditionalProperties.Nullable = !memberInfo.IsDictionaryValueNonNullable();
+ }
+
+ schema.ApplyValidationAttributes(customAttributes);
+
+ ApplyFilters(schema, modelType, schemaRepository, memberInfo: memberInfo);
+ }
+
+ return schema;
+ }
+
+ private OpenApiSchema GenerateSchemaForParameter(
+ Type modelType,
+ SchemaRepository schemaRepository,
+ ParameterInfo parameterInfo,
+ ApiParameterRouteInfo routeInfo)
+ {
+ var dataContract = GetDataContractFor(modelType);
+
+ var schema = _generatorOptions.UseOneOfForPolymorphism && IsBaseTypeWithKnownTypesDefined(dataContract, out var knownTypesDataContracts)
+ ? GeneratePolymorphicSchema(dataContract, schemaRepository, knownTypesDataContracts)
+ : GenerateConcreteSchema(dataContract, schemaRepository);
+
+ if (_generatorOptions.UseAllOfToExtendReferenceSchemas && schema.Reference != null)
+ {
+ schema.AllOf = new[] { new OpenApiSchema { Reference = schema.Reference } };
+ schema.Reference = null;
+ }
+
+ if (schema.Reference == null)
+ {
+ var customAttributes = parameterInfo.GetCustomAttributes();
+
+ var defaultValue = parameterInfo.HasDefaultValue
+ ? parameterInfo.DefaultValue
+ : customAttributes.OfType().FirstOrDefault()?.Value;
+
+ if (defaultValue != null)
+ {
+ var defaultAsJson = dataContract.JsonConverter(defaultValue);
+ schema.Default = OpenApiAnyFactory.CreateFromJson(defaultAsJson);
+ }
+
+ schema.ApplyValidationAttributes(customAttributes);
+ if (routeInfo != null)
+ {
+ schema.ApplyRouteConstraints(routeInfo);
+ }
+
+ ApplyFilters(schema, modelType, schemaRepository, parameterInfo: parameterInfo);
+ }
+
+ return schema;
+ }
+
+ private OpenApiSchema GenerateSchemaForType(Type modelType, SchemaRepository schemaRepository)
+ {
+ var dataContract = GetDataContractFor(modelType);
+
+ var schema = _generatorOptions.UseOneOfForPolymorphism && IsBaseTypeWithKnownTypesDefined(dataContract, out var knownTypesDataContracts)
+ ? GeneratePolymorphicSchema(dataContract, schemaRepository, knownTypesDataContracts)
+ : GenerateConcreteSchema(dataContract, schemaRepository);
+
+ if (schema.Reference == null)
+ {
+ ApplyFilters(schema, modelType, schemaRepository);
+ }
+
+ return schema;
+ }
+
+ private DataContract GetDataContractFor(Type modelType)
+ {
+ var effectiveType = Nullable.GetUnderlyingType(modelType) ?? modelType;
+ return _serializerDataContractResolver.GetDataContractForType(effectiveType);
+ }
+
+ private bool IsBaseTypeWithKnownTypesDefined(DataContract dataContract, out IEnumerable knownTypesDataContracts)
+ {
+ knownTypesDataContracts = null;
+
+ if (dataContract.DataType != DataType.Object) return false;
+
+ var subTypes = _generatorOptions.SubTypesSelector(dataContract.UnderlyingType);
+
+ if (!subTypes.Any()) return false;
+
+ var knownTypes = !dataContract.UnderlyingType.IsAbstract
+ ? new[] { dataContract.UnderlyingType }.Union(subTypes)
+ : subTypes;
+
+ knownTypesDataContracts = knownTypes.Select(knownType => GetDataContractFor(knownType));
+ return true;
+ }
+
+ private OpenApiSchema GeneratePolymorphicSchema(
+ DataContract dataContract,
+ SchemaRepository schemaRepository,
+ IEnumerable knownTypesDataContracts)
+ {
+ return new OpenApiSchema
+ {
+ OneOf = knownTypesDataContracts
+ .Select(allowedTypeDataContract => GenerateConcreteSchema(allowedTypeDataContract, schemaRepository))
+ .ToList()
+ };
+ }
+
+ private OpenApiSchema GenerateConcreteSchema(DataContract dataContract, SchemaRepository schemaRepository)
+ {
+ if (TryGetCustomTypeMapping(dataContract.UnderlyingType, out Func customSchemaFactory))
+ {
+ return customSchemaFactory();
+ }
+
+ if (dataContract.UnderlyingType.IsAssignableToOneOf(typeof(IFormFile), typeof(FileResult)))
+ {
+ return new OpenApiSchema { Type = "string", Format = "binary" };
+ }
+
+ Func schemaFactory;
+ bool returnAsReference;
+
+ switch (dataContract.DataType)
+ {
+ case DataType.Boolean:
+ case DataType.Integer:
+ case DataType.Number:
+ case DataType.String:
+ {
+ schemaFactory = () => CreatePrimitiveSchema(dataContract);
+ returnAsReference = dataContract.UnderlyingType.IsEnum && !_generatorOptions.UseInlineDefinitionsForEnums;
+ break;
+ }
+
+ case DataType.Array:
+ {
+ schemaFactory = () => CreateArraySchema(dataContract, schemaRepository);
+ returnAsReference = dataContract.UnderlyingType == dataContract.ArrayItemType;
+ break;
+ }
+
+ case DataType.Dictionary:
+ {
+ schemaFactory = () => CreateDictionarySchema(dataContract, schemaRepository);
+ returnAsReference = dataContract.UnderlyingType == dataContract.DictionaryValueType;
+ break;
+ }
+
+ case DataType.Object:
+ {
+ schemaFactory = () => CreateObjectSchema(dataContract, schemaRepository);
+ returnAsReference = true;
+ break;
+ }
+
+ default:
+ {
+ schemaFactory = () => new OpenApiSchema();
+ returnAsReference = false;
+ break;
+ }
+ }
+
+ return returnAsReference
+ ? GenerateReferencedSchema(dataContract, schemaRepository, schemaFactory)
+ : schemaFactory();
+ }
+
+ private bool TryGetCustomTypeMapping(Type modelType, out Func schemaFactory)
+ {
+ return _generatorOptions.CustomTypeMappings.TryGetValue(modelType, out schemaFactory)
+ || (modelType.IsConstructedGenericType && _generatorOptions.CustomTypeMappings.TryGetValue(modelType.GetGenericTypeDefinition(), out schemaFactory));
+ }
+
+ private OpenApiSchema CreatePrimitiveSchema(DataContract dataContract)
+ {
+ var schema = new OpenApiSchema
+ {
+ Type = dataContract.DataType.ToString().ToLower(CultureInfo.InvariantCulture),
+ Format = dataContract.DataFormat
+ };
+
+ // For backcompat only - EnumValues is obsolete
+ if (dataContract.EnumValues != null)
+ {
+ schema.Enum = dataContract.EnumValues
+ .Select(value => JsonSerializer.Serialize(value))
+ .Distinct()
+ .Select(valueAsJson => OpenApiAnyFactory.CreateFromJson(valueAsJson))
+ .ToList();
+
+ return schema;
+ }
+
+ if (dataContract.UnderlyingType.IsEnum)
+ {
+ schema.Enum = dataContract.UnderlyingType.GetEnumValues()
+ .Cast