Skip to content

Commit fd1f6c4

Browse files
committed
Replace API for change tracking with NSwag to support atomic operations
1 parent cdc6962 commit fd1f6c4

File tree

31 files changed

+1525
-1015
lines changed

31 files changed

+1525
-1015
lines changed

docs/usage/openapi-client.md

+32-55
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ After [enabling OpenAPI](~/usage/openapi.md), you can generate a typed JSON:API
77
> [client libraries](https://jsonapi.org/implementations/#client-libraries).
88
99
The following code generators are supported, though you may try others as well:
10-
- [NSwag](https://github.com/RicoSuter/NSwag) (v14.1 or higher): Produces clients for C# and TypeScript
10+
- [NSwag](https://github.com/RicoSuter/NSwag) (v14.1 or higher): Produces clients for C# (requires `Newtonsoft.Json`) and TypeScript
1111
- [Kiota](https://learn.microsoft.com/en-us/openapi/kiota/overview): Produces clients for C#, Go, Java, PHP, Python, Ruby, Swift and TypeScript
1212

1313
# [NSwag](#tab/nswag)
@@ -21,7 +21,7 @@ dotnet add package JsonApiDotNetCore.OpenApi.Client.NSwag
2121

2222
# [Kiota](#tab/kiota)
2323

24-
For C# clients, we provide an additional package that provides workarounds for bugs in Kiota.
24+
For C# clients, we provide an additional package that provides workarounds for bugs in Kiota, as well as MSBuild integration.
2525

2626
To add it to your project, run the following command:
2727
```
@@ -60,27 +60,6 @@ The following steps describe how to generate and use a JSON:API client in C#, co
6060
dotnet add package JsonApiDotNetCore.OpenApi.Client.NSwag
6161
```
6262
63-
1. Add the following glue code to connect our package with your generated code.
64-
65-
> [!NOTE]
66-
> The class name must be the same as specified in step 2.
67-
> If you also specified a namespace, put this class in the same namespace.
68-
> For example, add `namespace GeneratedCode;` below the `using` lines.
69-
70-
```c#
71-
using JsonApiDotNetCore.OpenApi.Client.NSwag;
72-
using Newtonsoft.Json;
73-
74-
partial class ExampleApiClient : JsonApiClient
75-
{
76-
partial void Initialize()
77-
{
78-
_instanceSettings = new JsonSerializerSettings(_settings.Value);
79-
SetSerializerSettingsForJsonApi(_instanceSettings);
80-
}
81-
}
82-
```
83-
8463
1. Add code that calls one of your JSON:API endpoints.
8564
8665
```c#
@@ -108,33 +87,32 @@ The following steps describe how to generate and use a JSON:API client in C#, co
10887
Data = new DataInUpdatePersonRequest
10988
{
11089
Id = "1",
111-
Attributes = new AttributesInUpdatePersonRequest
90+
// Using TrackChangesFor to send "firstName: null" instead of omitting it.
91+
Attributes = new TrackChangesFor<AttributesInUpdatePersonRequest>(_apiClient)
11292
{
113-
LastName = "Doe"
114-
}
93+
Initializer =
94+
{
95+
FirstName = null,
96+
LastName = "Doe"
97+
}
98+
}.Initializer
11599
}
116100
};
117101
118-
// This line results in sending "firstName: null" instead of omitting it.
119-
using (apiClient.WithPartialAttributeSerialization<UpdatePersonRequestDocument, AttributesInUpdatePersonRequest>(
120-
updatePersonRequest, person => person.FirstName))
121-
{
122-
// Workaround for https://github.com/RicoSuter/NSwag/issues/2499.
123-
await ApiResponse.TranslateAsync(() =>
124-
apiClient.PatchPersonAsync(updatePersonRequest.Data.Id, updatePersonRequest));
125-
126-
// The sent request looks like this:
127-
// {
128-
// "data": {
129-
// "type": "people",
130-
// "id": "1",
131-
// "attributes": {
132-
// "firstName": null,
133-
// "lastName": "Doe"
134-
// }
135-
// }
136-
// }
137-
}
102+
await ApiResponse.TranslateAsync(async () =>
103+
await _apiClient.PatchPersonAsync(updatePersonRequest.Data.Id, updatePersonRequest));
104+
105+
// The sent request looks like this:
106+
// {
107+
// "data": {
108+
// "type": "people",
109+
// "id": "1",
110+
// "attributes": {
111+
// "firstName": null,
112+
// "lastName": "Doe"
113+
// }
114+
// }
115+
// }
138116
```
139117
140118
> [!TIP]
@@ -146,9 +124,7 @@ The following steps describe how to generate and use a JSON:API client in C#, co
146124
147125
### Other IDEs
148126
149-
When using the command line, you can try the [Microsoft.dotnet-openapi Global Tool](https://docs.microsoft.com/en-us/aspnet/core/web-api/microsoft.dotnet-openapi?view=aspnetcore-5.0).
150-
151-
Alternatively, the following section shows what to add to your client project file directly:
127+
The following section shows what to add to your client project file directly:
152128
153129
```xml
154130
<ItemGroup>
@@ -160,9 +136,8 @@ Alternatively, the following section shows what to add to your client project fi
160136
<ItemGroup>
161137
<OpenApiReference Include="OpenAPIs\swagger.json">
162138
<SourceUri>http://localhost:14140/swagger/v1/swagger.json</SourceUri>
163-
<CodeGenerator>NSwagCSharp</CodeGenerator>
164139
<ClassName>ExampleApiClient</ClassName>
165-
<OutputPath>ExampleApiClient.cs</OutputPath>
140+
<OutputPath>%(ClassName).cs</OutputPath>
166141
</OpenApiReference>
167142
</ItemGroup>
168143
```
@@ -193,20 +168,20 @@ Various switches enable you to tweak the client generation to your needs. See th
193168

194169
# [NSwag](#tab/nswag)
195170

196-
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).
171+
The `OpenApiReference` element can be customized using various [NSwag-specific MSBuild properties](https://github.com/RicoSuter/NSwag/blob/7d6df3af95081f3f0ed6dee04be8d27faa86f91a/src/NSwag.ApiDescription.Client/NSwag.ApiDescription.Client.props).
197172
See [the source code](https://github.com/RicoSuter/NSwag/blob/master/src/NSwag.Commands/Commands/CodeGeneration/OpenApiToCSharpClientCommand.cs) for their meaning.
173+
The `JsonApiDotNetCore.OpenApi.Client.NSwag` package sets various of these for optimal JSON:API support.
198174

199175
> [!NOTE]
200176
> Earlier versions of NSwag required the use of `<Options>` to specify command-line switches directly.
201177
> This is no longer recommended and may conflict with the new MSBuild properties.
202178
203-
For example, the following section puts the generated code in a namespace and generates an interface (handy when writing tests):
179+
For example, the following section puts the generated code in a namespace, makes the client class internal and generates an interface (handy when writing tests):
204180

205181
```xml
206182
<OpenApiReference Include="swagger.json">
207183
<Namespace>ExampleProject.GeneratedCode</Namespace>
208-
<ClassName>SalesApiClient</ClassName>
209-
<CodeGenerator>NSwagCSharp</CodeGenerator>
184+
<NSwagClientClassAccessModifier>internal</NSwagClientClassAccessModifier>
210185
<NSwagGenerateClientInterfaces>true</NSwagGenerateClientInterfaces>
211186
</OpenApiReference>
212187
```
@@ -306,6 +281,7 @@ The [example project](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/
306281
demonstrates how to use them. It uses local IDs to:
307282
- Create a new tag
308283
- Create a new person
284+
- Update the person to clear an attribute (using `TrackChangesFor`)
309285
- Create a new todo-item, tagged with the new tag, and owned by the new person
310286
- Assign the todo-item to the created person
311287

@@ -316,6 +292,7 @@ See the [example project](https://github.com/json-api-dotnet/JsonApiDotNetCore/t
316292
demonstrates how to use them. It uses local IDs to:
317293
- Create a new tag
318294
- Create a new person
295+
- Update the person to clear an attribute (using built-in backing-store)
319296
- Create a new todo-item, tagged with the new tag, and owned by the new person
320297
- Assign the todo-item to the created person
321298

src/Examples/OpenApiKiotaClientExample/Worker.cs

+19-4
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
3838

3939
await SendOperationsRequestAsync(stoppingToken);
4040

41-
_ = await _apiClient.Api.People["999999"].GetAsync(cancellationToken: stoppingToken);
41+
await _apiClient.Api.People["999999"].GetAsync(cancellationToken: stoppingToken);
4242
}
4343
catch (ErrorResponseDocument exception)
4444
{
@@ -100,7 +100,7 @@ private async Task UpdatePersonAsync(CancellationToken cancellationToken)
100100
}
101101
};
102102

103-
_ = await _apiClient.Api.People[updatePersonRequest.Data.Id].PatchAsync(updatePersonRequest, cancellationToken: cancellationToken);
103+
await _apiClient.Api.People[updatePersonRequest.Data.Id].PatchAsync(updatePersonRequest, cancellationToken: cancellationToken);
104104
}
105105

106106
private async Task SendOperationsRequestAsync(CancellationToken cancellationToken)
@@ -131,7 +131,22 @@ private async Task SendOperationsRequestAsync(CancellationToken cancellationToke
131131
Lid = "new-person",
132132
Attributes = new AttributesInCreatePersonRequest
133133
{
134-
LastName = "Cinderella"
134+
FirstName = "Cinderella",
135+
LastName = "Tremaine"
136+
}
137+
}
138+
},
139+
new UpdatePersonOperation
140+
{
141+
Op = UpdateOperationCode.Update,
142+
Data = new DataInUpdatePersonRequest
143+
{
144+
Type = PersonResourceType.People,
145+
Lid = "new-person",
146+
Attributes = new AttributesInUpdatePersonRequest
147+
{
148+
// The --backing-store switch enables to send null and default values.
149+
FirstName = null
135150
}
136151
}
137152
},
@@ -191,7 +206,7 @@ private async Task SendOperationsRequestAsync(CancellationToken cancellationToke
191206

192207
OperationsResponseDocument? operationsResponse = await _apiClient.Api.Operations.PostAsync(operationsRequest, cancellationToken: cancellationToken);
193208

194-
var newTodoItem = (TodoItemDataInResponse)operationsResponse!.AtomicResults!.ElementAt(2).Data!;
209+
var newTodoItem = (TodoItemDataInResponse)operationsResponse!.AtomicResults!.ElementAt(3).Data!;
195210
Console.WriteLine($"Created todo-item with ID {newTodoItem.Id}: {newTodoItem.Attributes!.Description}.");
196211
}
197212
}

src/Examples/OpenApiNSwagClientExample/ExampleApiClient.cs

-20
This file was deleted.

src/Examples/OpenApiNSwagClientExample/Worker.cs

+29-13
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
3030

3131
await SendOperationsRequestAsync(stoppingToken);
3232

33-
_ = await _apiClient.GetPersonAsync("999999", null, null, stoppingToken);
33+
await _apiClient.GetPersonAsync("999999", null, null, stoppingToken);
3434
}
3535
catch (ApiException<ErrorResponseDocument> exception)
3636
{
@@ -57,20 +57,20 @@ private async Task UpdatePersonAsync(CancellationToken cancellationToken)
5757
Data = new DataInUpdatePersonRequest
5858
{
5959
Id = "1",
60-
Attributes = new AttributesInUpdatePersonRequest
60+
// Using TrackChangesFor to send "firstName: null" instead of omitting it.
61+
Attributes = new TrackChangesFor<AttributesInUpdatePersonRequest>(_apiClient)
6162
{
62-
LastName = "Doe"
63-
}
63+
Initializer =
64+
{
65+
FirstName = null,
66+
LastName = "Doe"
67+
}
68+
}.Initializer
6469
}
6570
};
6671

67-
// This line results in sending "firstName: null" instead of omitting it.
68-
using (_apiClient.WithPartialAttributeSerialization<UpdatePersonRequestDocument, AttributesInUpdatePersonRequest>(updatePersonRequest,
69-
person => person.FirstName))
70-
{
71-
_ = await ApiResponse.TranslateAsync(async () =>
72-
await _apiClient.PatchPersonAsync(updatePersonRequest.Data.Id, updatePersonRequest, cancellationToken: cancellationToken));
73-
}
72+
await ApiResponse.TranslateAsync(async () =>
73+
await _apiClient.PatchPersonAsync(updatePersonRequest.Data.Id, updatePersonRequest, cancellationToken: cancellationToken));
7474
}
7575

7676
private async Task SendOperationsRequestAsync(CancellationToken cancellationToken)
@@ -97,10 +97,26 @@ private async Task SendOperationsRequestAsync(CancellationToken cancellationToke
9797
Lid = "new-person",
9898
Attributes = new AttributesInCreatePersonRequest
9999
{
100-
LastName = "Cinderella"
100+
FirstName = "Cinderella",
101+
LastName = "Tremaine"
101102
}
102103
}
103104
},
105+
new UpdatePersonOperation
106+
{
107+
Data = new DataInUpdatePersonRequest
108+
{
109+
Lid = "new-person",
110+
// Using TrackChangesFor to send "firstName: null" instead of omitting it.
111+
Attributes = new TrackChangesFor<AttributesInUpdatePersonRequest>(_apiClient)
112+
{
113+
Initializer =
114+
{
115+
FirstName = null
116+
}
117+
}.Initializer
118+
}
119+
},
104120
new CreateTodoItemOperation
105121
{
106122
Data = new DataInCreateTodoItemRequest
@@ -149,7 +165,7 @@ private async Task SendOperationsRequestAsync(CancellationToken cancellationToke
149165

150166
ApiResponse<OperationsResponseDocument> operationsResponse = await _apiClient.PostOperationsAsync(operationsRequest, cancellationToken);
151167

152-
var newTodoItem = (TodoItemDataInResponse)operationsResponse.Result.Atomic_results.ElementAt(2).Data!;
168+
var newTodoItem = (TodoItemDataInResponse)operationsResponse.Result.Atomic_results.ElementAt(3).Data!;
153169
Console.WriteLine($"Created todo-item with ID {newTodoItem.Id}: {newTodoItem.Attributes!.Description}.");
154170
}
155171
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using JetBrains.Annotations;
2+
using Newtonsoft.Json;
3+
4+
namespace JsonApiDotNetCore.OpenApi.Client.NSwag;
5+
6+
// Referenced from liquid template, to ensure the built-in JsonInheritanceConverter from NSwag is never used.
7+
[PublicAPI]
8+
public abstract class BlockedJsonInheritanceConverter : JsonConverter
9+
{
10+
private const string DefaultDiscriminatorName = "discriminator";
11+
12+
public string DiscriminatorName { get; }
13+
14+
public override bool CanWrite => true;
15+
public override bool CanRead => true;
16+
17+
protected BlockedJsonInheritanceConverter()
18+
: this(DefaultDiscriminatorName)
19+
{
20+
}
21+
22+
protected BlockedJsonInheritanceConverter(string discriminatorName)
23+
{
24+
ArgumentException.ThrowIfNullOrEmpty(discriminatorName);
25+
26+
DiscriminatorName = discriminatorName;
27+
}
28+
29+
public override bool CanConvert(Type objectType)
30+
{
31+
return true;
32+
}
33+
34+
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
35+
{
36+
throw new InvalidOperationException("JsonInheritanceConverter is incompatible with JSON:API and must not be used.");
37+
}
38+
39+
public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
40+
{
41+
throw new InvalidOperationException("JsonInheritanceConverter is incompatible with JSON:API and must not be used.");
42+
}
43+
}

src/JsonApiDotNetCore.OpenApi.Client.NSwag/Build/JsonApiDotNetCore.OpenApi.Client.NSwag.props

+3
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,8 @@
88
<NSwagGenerateNullableReferenceTypes>true</NSwagGenerateNullableReferenceTypes>
99
<NSwagGenerateOptionalPropertiesAsNullable>true</NSwagGenerateOptionalPropertiesAsNullable>
1010
<NSwagGenerateOptionalParameters>true</NSwagGenerateOptionalParameters>
11+
<NSwagClientBaseClass>JsonApiDotNetCore.OpenApi.Client.NSwag.JsonApiClient</NSwagClientBaseClass>
12+
<NSwagClassStyle>Prism</NSwagClassStyle>
13+
<NSwagTemplateDirectory>$(MSBuildThisFileDirectory)../Templates</NSwagTemplateDirectory>
1114
</PropertyGroup>
1215
</Project>

0 commit comments

Comments
 (0)