Skip to content

Update to NSwag v14.1 #1600

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 28 additions & 37 deletions docs/usage/openapi-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ After [enabling OpenAPI](~/usage/openapi.md), you can generate a typed JSON:API
> [client libraries](https://jsonapi.org/implementations/#client-libraries).

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

# [NSwag](#tab/nswag)
Expand Down Expand Up @@ -39,11 +39,11 @@ To generate your C# client, follow the steps below.
### Visual Studio

The easiest way to get started is by using the built-in capabilities of Visual Studio.
The following steps describe how to generate and use a JSON:API client in C#, using our package.
The following steps describe how to generate and use a JSON:API client in C#, combined with our NuGet package.

1. In **Solution Explorer**, right-click your client project, select **Add** > **Service Reference** and choose **OpenAPI**.

2. On the next page, specify the OpenAPI URL to your JSON:API server, for example: `http://localhost:14140/swagger/v1/swagger.json`.
1. On the next page, specify the OpenAPI URL to your JSON:API server, for example: `http://localhost:14140/swagger/v1/swagger.json`.
Specify `ExampleApiClient` as the class name, optionally provide a namespace and click **Finish**.
Visual Studio now downloads your swagger.json and updates your project file.
This adds a pre-build step that generates the client code.
Expand All @@ -52,25 +52,15 @@ The following steps describe how to generate and use a JSON:API client in C#, us
> To later re-download swagger.json and regenerate the client code,
> right-click **Dependencies** > **Manage Connected Services** and click the **Refresh** icon.

3. Although not strictly required, we recommend running package update now, which fixes some issues.
1. Run package update now, which fixes incompatibilities and bugs from older versions.

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

4. Add our client package to your project:
1. Add our client package to your project:

```
dotnet add package JsonApiDotNetCore.OpenApi.Client.NSwag
```

5. Add the following line inside the **OpenApiReference** section in your project file:

```xml
<Options>/GenerateExceptionClasses:false /GenerateNullableReferenceTypes:true /GenerateOptionalPropertiesAsNullable:true /GenerateOptionalParameters:true /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.NSwag</Options>
```

6. Add the following glue code to connect our package with your generated code.
1. Add the following glue code to connect our package with your generated code.

> [!NOTE]
> The class name must be the same as specified in step 2.
Expand All @@ -83,14 +73,15 @@ The following steps describe how to generate and use a JSON:API client in C#, us

partial class ExampleApiClient : JsonApiClient
{
partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings)
partial void Initialize()
{
SetSerializerSettingsForJsonApi(settings);
_instanceSettings = new JsonSerializerSettings(_settings.Value);
SetSerializerSettingsForJsonApi(_instanceSettings);
}
}
```

7. Add code that calls one of your JSON:API endpoints.
1. Add code that calls one of your JSON:API endpoints.

```c#
using var httpClient = new HttpClient();
Expand All @@ -101,15 +92,15 @@ The following steps describe how to generate and use a JSON:API client in C#, us
["filter"] = "has(assignedTodoItems)",
["sort"] = "-lastName",
["page[size]"] = "5"
}, null);
});

foreach (var person in getResponse.Data)
{
Console.WriteLine($"Found person {person.Id}: {person.Attributes!.DisplayName}");
}
```

8. Extend your demo code to send a partial PATCH request with the help of our package:
1. Extend the demo code to send a partial PATCH request with the help of our package:

```c#
var updatePersonRequest = new UpdatePersonRequestDocument
Expand Down Expand Up @@ -163,16 +154,15 @@ Alternatively, the following section shows what to add to your client project fi
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.ApiDescription.Client" Version="8.0.*" PrivateAssets="all" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.*" />
<PackageReference Include="NSwag.ApiDescription.Client" Version="13.20.*" PrivateAssets="all" />
<PackageReference Include="NSwag.ApiDescription.Client" Version="14.1.*" PrivateAssets="all" />
</ItemGroup>

<ItemGroup>
<OpenApiReference Include="OpenAPIs\swagger.json">
<SourceUri>http://localhost:14140/swagger/v1/swagger.json</SourceUri>
<CodeGenerator>NSwagCSharp</CodeGenerator>
<ClassName>ExampleApiClient</ClassName>
<OutputPath>ExampleApiClient.cs</OutputPath>
<Options>/GenerateExceptionClasses:false /GenerateNullableReferenceTypes:true /GenerateOptionalPropertiesAsNullable:true /GenerateOptionalParameters:true /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.NSwag</Options>
<CodeGenerator>NSwagCSharp</CodeGenerator>
<ClassName>ExampleApiClient</ClassName>
<OutputPath>ExampleApiClient.cs</OutputPath>
</OpenApiReference>
</ItemGroup>
```
Expand Down Expand Up @@ -203,23 +193,24 @@ Various switches enable you to tweak the client generation to your needs. See th

# [NSwag](#tab/nswag)

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).
A guide with common best practices is available [here](https://stevetalkscode.co.uk/openapireference-commands).
The `OpenApiReference` can be customized using various [NSwag-specific MSBuild properties](https://github.com/RicoSuter/NSwag/blob/7d6df3af95081f3f0ed6dee04be8d27faa86f91a/src/NSwag.ApiDescription.Client/NSwag.ApiDescription.Client.props).
See [the source code](https://github.com/RicoSuter/NSwag/blob/master/src/NSwag.Commands/Commands/CodeGeneration/OpenApiToCSharpClientCommand.cs) for their meaning.

For example, the following section puts the generated code in a namespace and generates an interface (which is handy for dependency injection):
> [!NOTE]
> Earlier versions of NSwag required the use of `<Options>` to specify command-line switches directly.
> This is no longer recommended and may conflict with the new MSBuild properties.

For example, the following section puts the generated code in a namespace and generates an interface (handy when writing tests):

```xml
<OpenApiReference Include="swagger.json">
<Namespace>ExampleProject.GeneratedCode</Namespace>
<ClassName>SalesApiClient</ClassName>
<CodeGenerator>NSwagCSharp</CodeGenerator>
<Options>/GenerateClientInterfaces:true</Options>
<NSwagGenerateClientInterfaces>true</NSwagGenerateClientInterfaces>
</OpenApiReference>
```

Likewise, you can enable nullable reference types by adding `/GenerateNullableReferenceTypes:true /GenerateOptionalPropertiesAsNullable:true /GenerateOptionalParameters:true`.

# [Kiota](#tab/kiota)

The available command-line switches for Kiota are described [here](https://learn.microsoft.com/en-us/openapi/kiota/using#client-generation).
Expand Down Expand Up @@ -257,10 +248,10 @@ The use of HTTP headers varies per client generator. To use [ETags for caching](

# [NSwag](#tab/nswag)

NSwag needs extra settings to make response headers accessible. Specify the following in the `<Options>` element of your project file:
To gain access to HTTP response headers, add the following in a `PropertyGroup` or directly in the `OpenApiReference`:

```
/GenerateExceptionClasses:false /WrapResponses:true /GenerateResponseClasses:false /ResponseClass:ApiResponse /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.NSwag
<NSwagWrapResponses>true</NSwagWrapResponses>
```

This enables the following code, which is explained below:
Expand All @@ -272,7 +263,7 @@ Console.WriteLine($"Retrieved {getResponse.Result?.Data.Count ?? 0} people.");

// wait some time...

getResponse = await ApiResponse.TranslateAsync(() => apiClient.GetPersonCollectionAsync(null, eTag));
getResponse = await ApiResponse.TranslateAsync(() => apiClient.GetPersonCollectionAsync(if_None_Match: eTag));

if (getResponse is { StatusCode: (int)HttpStatusCode.NotModified, Result: null })
{
Expand All @@ -287,7 +278,7 @@ If you only want to ask whether data has changed without fetching it, use a HEAD

# [Kiota](#tab/kiota)

Use `HeadersInspectionHandlerOption` to gain access to response headers. For example:
Use `HeadersInspectionHandlerOption` to gain access to HTTP response headers. For example:

```c#
var headerInspector = new HeadersInspectionHandlerOption
Expand Down
2 changes: 1 addition & 1 deletion package-versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<InheritDocVersion>2.0.*</InheritDocVersion>
<KiotaVersion>1.*</KiotaVersion>
<MicrosoftApiClientVersion>8.0.*</MicrosoftApiClientVersion>
<NSwagApiClientVersion>13.20.*</NSwagApiClientVersion>
<NSwagApiClientVersion>14.1.*</NSwagApiClientVersion>
<NewtonsoftJsonVersion>13.0.*</NewtonsoftJsonVersion>
<SourceLinkVersion>8.0.*</SourceLinkVersion>
<SwashbuckleVersion>6.*-*</SwashbuckleVersion>
Expand Down
8 changes: 5 additions & 3 deletions src/Examples/OpenApiNSwagClientExample/ExampleApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ namespace OpenApiNSwagClientExample;
[UsedImplicitly(ImplicitUseTargetFlags.Itself)]
public partial class ExampleApiClient : JsonApiClient
{
partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings)
partial void Initialize()
{
SetSerializerSettingsForJsonApi(settings);
_instanceSettings = new JsonSerializerSettings(_settings.Value);

#if DEBUG
settings.Formatting = Formatting.Indented;
_instanceSettings.Formatting = Formatting.Indented;
#endif

SetSerializerSettingsForJsonApi(_instanceSettings);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
</PropertyGroup>

<Import Project="..\..\..\package-versions.props" />
<Import Project="..\..\JsonApiDotNetCore.OpenApi.Client.NSwag\Build\JsonApiDotNetCore.OpenApi.Client.NSwag.props" />

<ItemGroup>
<ProjectReference Include="..\..\JsonApiDotNetCore.OpenApi.Client.NSwag\JsonApiDotNetCore.OpenApi.Client.NSwag.csproj" />
Expand All @@ -20,11 +21,11 @@

<ItemGroup>
<OpenApiReference Include="..\JsonApiDotNetCoreExample\GeneratedSwagger\JsonApiDotNetCoreExample.json">
<Namespace>OpenApiNSwagClientExample</Namespace>
<ClassName>ExampleApiClient</ClassName>
<OutputPath>ExampleApiClient.cs</OutputPath>
<CodeGenerator>NSwagCSharp</CodeGenerator>
<Options>/GenerateExceptionClasses:false /WrapResponses:true /GenerateResponseClasses:false /ResponseClass:ApiResponse /GenerateNullableReferenceTypes:true /GenerateOptionalPropertiesAsNullable:true /GenerateOptionalParameters:true /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.NSwag</Options>
<Name>ExampleApi</Name>
<Namespace>$(MSBuildProjectName)</Namespace>
<ClassName>%(Name)Client</ClassName>
<OutputPath>%(ClassName).cs</OutputPath>
<NSwagWrapResponses>true</NSwagWrapResponses>
</OpenApiReference>
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project>
<PropertyGroup>
<CodeGenerator>NSwagCSharp</CodeGenerator>
<NSwagGenerateExceptionClasses>false</NSwagGenerateExceptionClasses>
<NSwagGenerateResponseClasses>false</NSwagGenerateResponseClasses>
<NSwagResponseClass>ApiResponse</NSwagResponseClass>
<NSwagAdditionalNamespaceUsages>JsonApiDotNetCore.OpenApi.Client.NSwag</NSwagAdditionalNamespaceUsages>
<NSwagGenerateNullableReferenceTypes>true</NSwagGenerateNullableReferenceTypes>
<NSwagGenerateOptionalPropertiesAsNullable>true</NSwagGenerateOptionalPropertiesAsNullable>
<NSwagGenerateOptionalParameters>true</NSwagGenerateOptionalParameters>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<ItemGroup>
<None Include="..\..\package-icon.png" Visible="false" Pack="True" PackagePath="" />
<None Include="..\..\PackageReadme.md" Visible="false" Pack="True" PackagePath="" />
<None Include="Build\*.props" Pack="True" PackagePath="build" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using JetBrains.Annotations;
using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiObjects.Links;
Expand All @@ -15,7 +14,7 @@ internal sealed class NullableToOneRelationshipInResponse<TResource>
[JsonPropertyName("links")]
public RelationshipLinks Links { get; set; } = null!;

[Required]
// Non-required because related data may not be included in the response.
[JsonPropertyName("data")]
public ResourceIdentifierInResponse<TResource>? Data { get; set; }

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using JetBrains.Annotations;
using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiObjects.Links;
Expand All @@ -15,7 +14,7 @@ internal sealed class ToManyRelationshipInResponse<TResource>
[JsonPropertyName("links")]
public RelationshipLinks Links { get; set; } = null!;

[Required]
// Non-required because related data may not be included in the response.
[JsonPropertyName("data")]
public ICollection<ResourceIdentifierInResponse<TResource>> Data { get; set; } = null!;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using JetBrains.Annotations;
using JsonApiDotNetCore.OpenApi.Swashbuckle.JsonApiObjects.Links;
Expand All @@ -15,7 +14,7 @@ internal sealed class ToOneRelationshipInResponse<TResource>
[JsonPropertyName("links")]
public RelationshipLinks Links { get; set; } = null!;

[Required]
// Non-required because related data may not be included in the response.
[JsonPropertyName("data")]
public ResourceIdentifierInResponse<TResource> Data { get; set; } = null!;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ private static void AddQueryStringParameters(OpenApiOperation operation, bool is
// The next best thing is to expose the query string parameters as unstructured and optional.
// - This makes SwaggerUI ask for JSON, which is a bit odd, but it works. For example: {"sort":"-id"} produces: ?sort=-id
// - This makes NSwag produce a C# client with method signature: GetAsync(IDictionary<string, string?>? query)
// when combined with <Options>/GenerateNullableReferenceTypes:true</Options> in the project file.
// when combined with <NSwagGenerateNullableReferenceTypes>true</NSwagGenerateNullableReferenceTypes> in the project file.

operation.Parameters.Add(new OpenApiParameter
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,8 +207,6 @@ private OpenApiSchema CreateReferenceSchemaForRelationship(Type relationshipSche
if (JsonApiSchemaFacts.IsRelationshipInResponseType(relationshipSchemaType))
{
_linksVisibilitySchemaGenerator.UpdateSchemaForRelationship(relationshipSchemaType, fullSchema, schemaRepository);

fullSchema.Required.Remove(JsonApiPropertyName.Data);
}

return referenceSchema;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ public async Task Can_create_resource_with_attributes()
response.AtomicResults.ShouldHaveCount(1);
TeacherDataInResponse teacherDataInResponse = response.AtomicResults.ElementAt(0).Data.Should().BeOfType<TeacherDataInResponse>().Which;

teacherDataInResponse.Attributes!.Name.Should().Be(newTeacher.Name);
teacherDataInResponse.Attributes.ShouldNotBeNull();
teacherDataInResponse.Attributes.Name.Should().Be(newTeacher.Name);
teacherDataInResponse.Attributes.EmailAddress.Should().Be(newTeacher.EmailAddress);

long newTeacherId = long.Parse(teacherDataInResponse.Id!);
Expand Down Expand Up @@ -147,7 +148,8 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
response.AtomicResults.ShouldHaveCount(1);
EnrollmentDataInResponse enrollmentDataInResponse = response.AtomicResults.ElementAt(0).Data.Should().BeOfType<EnrollmentDataInResponse>().Which;

enrollmentDataInResponse.Attributes!.EnrolledAt.Should().Be((Date)newEnrollment.EnrolledAt);
enrollmentDataInResponse.Attributes.ShouldNotBeNull();
enrollmentDataInResponse.Attributes.EnrolledAt.Should().Be((Date)newEnrollment.EnrolledAt);
enrollmentDataInResponse.Attributes.GraduatedAt.Should().BeNull();
enrollmentDataInResponse.Attributes.HasGraduated.Should().BeFalse();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,20 +180,23 @@ public async Task Can_use_local_IDs()
response.AtomicResults.ShouldHaveCount(7);

TeacherDataInResponse teacherInResponse = response.AtomicResults.ElementAt(0).Data.Should().BeOfType<TeacherDataInResponse>().Which;
teacherInResponse.Attributes!.Name.Should().Be(newTeacher.Name);
teacherInResponse.Attributes.ShouldNotBeNull();
teacherInResponse.Attributes.Name.Should().Be(newTeacher.Name);
teacherInResponse.Attributes.EmailAddress.Should().Be(newTeacher.EmailAddress);
long newTeacherId = long.Parse(teacherInResponse.Id!);

response.AtomicResults.ElementAt(1).Data.Should().BeNull();
response.AtomicResults.ElementAt(2).Data.Should().BeNull();

StudentDataInResponse studentInResponse = response.AtomicResults.ElementAt(3).Data.Should().BeOfType<StudentDataInResponse>().Which;
studentInResponse.Attributes!.Name.Should().Be(newStudent.Name);
studentInResponse.Attributes.ShouldNotBeNull();
studentInResponse.Attributes.Name.Should().Be(newStudent.Name);
studentInResponse.Attributes.EmailAddress.Should().Be(newStudent.EmailAddress);
long newStudentId = long.Parse(studentInResponse.Id!);

EnrollmentDataInResponse enrollmentInResponse = response.AtomicResults.ElementAt(4).Data.Should().BeOfType<EnrollmentDataInResponse>().Which;
enrollmentInResponse.Attributes!.EnrolledAt.Should().Be((Date)newEnrolledAt);
enrollmentInResponse.Attributes.ShouldNotBeNull();
enrollmentInResponse.Attributes.EnrolledAt.Should().Be((Date)newEnrolledAt);
long newEnrollmentId = long.Parse(enrollmentInResponse.Id!);

response.AtomicResults.ElementAt(5).Data.Should().BeNull();
Expand Down
Loading
Loading