Skip to content

Commit 362bf5f

Browse files
maureiBart Koelman
and
Bart Koelman
authored
Move existing integration to JADNC (#1076)
* Copied integration and fixed compilation errors * Add models and adjusted models in OpenAPI document tests such that i reflect features that were covered in the private repository * Add FlightAttendantController to used controllers * Added client library with tests to project; made adjustmets to the models to make the client test pass without introducing any structural adjustments to the test codes * Adjusted build script to create OpenApiClient artifact, added documentation, minor model adjustments * Adjusted docs and minor refactors * Replace resource name formatter with internal one * Removed JsonApiInputFormatter workaround * Moved full document test to ExistingOpenApiIntegration test folder, which is to be removed at the very end once have dedicated test suites for all components * Disabled OpenAPI for nonjson and operations controller, as these are not yet supported and will cause the integration to crash * review feedback on docs * Add a very minimal example of a generated OpenAPI client for JsonApiDotNetCoreExample project. The only call that currently doesn't crash is the one associated to deleting a primary resource * Fixed inspect and cleanup code issues that do not appear when executed through Rider * rename existing to legacy in ExistingOpenApiIntegration folder and namespace * moved OpenApiClient to OpenApi.Client * Configure NSwag to generate a client with access modifier set to internal. Simplified ExampleApiClient to not use a generated interface * Move OpenAPI client tests to dedicated test project, cleared up some unrequired internalsVisibleTo usage * Some adjustments to stay closer to 1-on-1 model mapping from the private repository of the legacy integration. Also processed some of the review feedback * review feedback * Process inspectcode issues that do not appear when run locally * Update package reference in build script * Changed models to stay closer 1-to-1 mapping. Changed 'OpenApiClient' to 'OpenApi.Client'. Removed unneeded config from legacy integration startup. * process review feedback * rename ClassName in client test project * Updated example client project using Visual Studio defaults and updated documentation * Layout tweaks and fixes * fixed failed automatic rename 'openApiOpenApi' * reduced changed wrt private version of this test * Changed ID type from to for Flight * ReserveCabinPersonnel -> Purser, CabinPersonnel -> CabinCrewMembers * fixed failed automatic rename 'openApiOpenApi' * Flight: replaced OperatingAirplane with Purser, added to many relationship to new model Passenger * Review feedback * Fix client lifetime test * Feedback Co-authored-by: Bart Koelman <[email protected]>
1 parent 9209642 commit 362bf5f

File tree

109 files changed

+11189
-1078
lines changed

Some content is hidden

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

109 files changed

+11189
-1078
lines changed

Build.ps1

+2
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,12 @@ function CreateNuGetPackage {
8686
if ([string]::IsNullOrWhitespace($versionSuffix)) {
8787
dotnet pack .\src\JsonApiDotNetCore -c Release -o .\artifacts
8888
dotnet pack .\src\JsonApiDotNetCore.OpenApi -c Release -o .\artifacts
89+
dotnet pack .\src\JsonApiDotNetCore.OpenApi.Client -c Release -o .\artifacts
8990
}
9091
else {
9192
dotnet pack .\src\JsonApiDotNetCore -c Release -o .\artifacts --version-suffix=$versionSuffix
9293
dotnet pack .\src\JsonApiDotNetCore.OpenApi -c Release -o .\artifacts --version-suffix=$versionSuffix
94+
dotnet pack .\src\JsonApiDotNetCore.OpenApi.Client -c Release -o .\artifacts --version-suffix=$versionSuffix
9395
}
9496

9597
CheckLastExitCode

JsonApiDotNetCore.sln

+45
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.OpenApi",
4848
EndProject
4949
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApiTests", "test\OpenApiTests\OpenApiTests.csproj", "{B693DE14-BB28-496F-AB39-B4E674ABCA80}"
5050
EndProject
51+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCore.OpenApi.Client", "src\JsonApiDotNetCore.OpenApi.Client\JsonApiDotNetCore.OpenApi.Client.csproj", "{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}"
52+
EndProject
53+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonApiDotNetCoreExampleClient", "src\Examples\JsonApiDotNetCoreExampleClient\JsonApiDotNetCoreExampleClient.csproj", "{7FC5DFA3-6F66-4FD8-820D-81E93856F252}"
54+
EndProject
55+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenApiClientTests", "test\OpenApiClientTests\OpenApiClientTests.csproj", "{77F98215-3085-422E-B99D-4C404C2114CF}"
56+
EndProject
5157
Global
5258
GlobalSection(SolutionConfigurationPlatforms) = preSolution
5359
Debug|Any CPU = Debug|Any CPU
@@ -238,6 +244,42 @@ Global
238244
{B693DE14-BB28-496F-AB39-B4E674ABCA80}.Release|x64.Build.0 = Release|Any CPU
239245
{B693DE14-BB28-496F-AB39-B4E674ABCA80}.Release|x86.ActiveCfg = Release|Any CPU
240246
{B693DE14-BB28-496F-AB39-B4E674ABCA80}.Release|x86.Build.0 = Release|Any CPU
247+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
248+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
249+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Debug|x64.ActiveCfg = Debug|Any CPU
250+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Debug|x64.Build.0 = Debug|Any CPU
251+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Debug|x86.ActiveCfg = Debug|Any CPU
252+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Debug|x86.Build.0 = Debug|Any CPU
253+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
254+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Release|Any CPU.Build.0 = Release|Any CPU
255+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Release|x64.ActiveCfg = Release|Any CPU
256+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Release|x64.Build.0 = Release|Any CPU
257+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Release|x86.ActiveCfg = Release|Any CPU
258+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C}.Release|x86.Build.0 = Release|Any CPU
259+
{7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
260+
{7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Debug|Any CPU.Build.0 = Debug|Any CPU
261+
{7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Debug|x64.ActiveCfg = Debug|Any CPU
262+
{7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Debug|x64.Build.0 = Debug|Any CPU
263+
{7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Debug|x86.ActiveCfg = Debug|Any CPU
264+
{7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Debug|x86.Build.0 = Debug|Any CPU
265+
{7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Release|Any CPU.ActiveCfg = Release|Any CPU
266+
{7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Release|Any CPU.Build.0 = Release|Any CPU
267+
{7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Release|x64.ActiveCfg = Release|Any CPU
268+
{7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Release|x64.Build.0 = Release|Any CPU
269+
{7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Release|x86.ActiveCfg = Release|Any CPU
270+
{7FC5DFA3-6F66-4FD8-820D-81E93856F252}.Release|x86.Build.0 = Release|Any CPU
271+
{77F98215-3085-422E-B99D-4C404C2114CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
272+
{77F98215-3085-422E-B99D-4C404C2114CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
273+
{77F98215-3085-422E-B99D-4C404C2114CF}.Debug|x64.ActiveCfg = Debug|Any CPU
274+
{77F98215-3085-422E-B99D-4C404C2114CF}.Debug|x64.Build.0 = Debug|Any CPU
275+
{77F98215-3085-422E-B99D-4C404C2114CF}.Debug|x86.ActiveCfg = Debug|Any CPU
276+
{77F98215-3085-422E-B99D-4C404C2114CF}.Debug|x86.Build.0 = Debug|Any CPU
277+
{77F98215-3085-422E-B99D-4C404C2114CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
278+
{77F98215-3085-422E-B99D-4C404C2114CF}.Release|Any CPU.Build.0 = Release|Any CPU
279+
{77F98215-3085-422E-B99D-4C404C2114CF}.Release|x64.ActiveCfg = Release|Any CPU
280+
{77F98215-3085-422E-B99D-4C404C2114CF}.Release|x64.Build.0 = Release|Any CPU
281+
{77F98215-3085-422E-B99D-4C404C2114CF}.Release|x86.ActiveCfg = Release|Any CPU
282+
{77F98215-3085-422E-B99D-4C404C2114CF}.Release|x86.Build.0 = Release|Any CPU
241283
EndGlobalSection
242284
GlobalSection(SolutionProperties) = preSolution
243285
HideSolutionNode = FALSE
@@ -258,6 +300,9 @@ Global
258300
{210FD61E-FF5D-4CEE-8E0D-C739ECCCBA21} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
259301
{71287D6F-6C3B-44B4-9FCA-E78FE3F02289} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF}
260302
{B693DE14-BB28-496F-AB39-B4E674ABCA80} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
303+
{5ADAA902-5A75-4ECB-B4B4-03291D63CE9C} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF}
304+
{7FC5DFA3-6F66-4FD8-820D-81E93856F252} = {026FBC6C-AF76-4568-9B87-EC73457899FD}
305+
{77F98215-3085-422E-B99D-4C404C2114CF} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
261306
EndGlobalSection
262307
GlobalSection(ExtensibilityGlobals) = postSolution
263308
SolutionGuid = {A2421882-8F0A-4905-928F-B550B192F9A4}

docs/usage/openapi-client.md

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# OpenAPI Client
2+
3+
You can generate a JSON:API client in various programming languages from the [OpenAPI specification](https://swagger.io/specification/) file that JsonApiDotNetCore APIs provide.
4+
5+
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 issue 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".
6+
7+
## Getting started
8+
9+
### Visual Studio
10+
11+
The easiest way to get started is by using the built-in capabilities of Visual Studio. The next steps describe how to generate a JSON:API client library and use our package.
12+
13+
1. In **Solution Explorer**, right-click your client project, select **Add** > **Service Reference** and choose **OpenAPI**.
14+
15+
2. On the next page, specify the OpenAPI URL to your JSON:API server, for example: `http://localhost:14140/swagger/v1/swagger.json`.
16+
Optionally provide a class name and namespace and click **Finish**.
17+
Visual Studio now downloads your swagger.json and updates your project file. This results in a pre-build step that generates the client code.
18+
19+
Tip: To later re-download swagger.json and regenerate the client code, right-click **Dependencies** > **Manage Connected Services** and click the **Refresh** icon.
20+
3. Although not strictly required, we recommend to run package update now, which fixes some issues and removes the `Stream` parameter from generated calls.
21+
22+
4. Add some demo code that calls one of your JSON:API endpoints. For example:
23+
24+
```c#
25+
using var httpClient = new HttpClient();
26+
var apiClient = new ExampleApiClient("http://localhost:14140", httpClient);
27+
28+
PersonCollectionResponseDocument getResponse =
29+
await apiClient.GetPersonCollectionAsync();
30+
31+
foreach (PersonDataInResponse person in getResponse.Data)
32+
{
33+
Console.WriteLine($"Found user {person.Id} named " +
34+
$"'{person.Attributes.FirstName} {person.Attributes.LastName}'.");
35+
}
36+
```
37+
38+
5. Add our client package to your project:
39+
40+
```
41+
dotnet add package JsonApiDotNetCore.OpenApi.Client
42+
```
43+
44+
6. Add the following glue code to connect our package with your generated code. The code below assumes you specified `ExampleApiClient` as class name in step 2.
45+
46+
```c#
47+
using JsonApiDotNetCore.OpenApi.Client;
48+
using Newtonsoft.Json;
49+
50+
partial class ExampleApiClient : JsonApiClient
51+
{
52+
partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings)
53+
{
54+
SetSerializerSettingsForJsonApi(settings);
55+
}
56+
}
57+
```
58+
59+
7. Extend your demo code to send a partial PATCH request with the help of our package:
60+
61+
```c#
62+
var patchRequest = new PersonPatchRequestDocument
63+
{
64+
Data = new PersonDataInPatchRequest
65+
{
66+
Id = "1",
67+
Attributes = new PersonAttributesInPatchRequest
68+
{
69+
FirstName = "Jack"
70+
}
71+
}
72+
};
73+
74+
// This line results in sending "lastName: null" instead of omitting it.
75+
using (apiClient.RegisterAttributesForRequestDocument<PersonPatchRequestDocument,
76+
PersonAttributesInPatchRequest>(patchRequest, person => person.LastName))
77+
{
78+
PersonPrimaryResponseDocument patchResponse =
79+
await apiClient.PatchPersonAsync("1", patchRequest);
80+
81+
// The sent request looks like this:
82+
// {
83+
// "data": {
84+
// "type": "people",
85+
// "id": "1",
86+
// "attributes": {
87+
// "firstName": "Jack",
88+
// "lastName": null
89+
// }
90+
// }
91+
// }
92+
}
93+
```
94+
95+
### Other IDEs
96+
97+
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).
98+
99+
Alternatively, the next section shows what to add to your client project file directly:
100+
101+
```xml
102+
<ItemGroup>
103+
<PackageReference Include="Microsoft.Extensions.ApiDescription.Client" Version="3.0.0">
104+
<PrivateAssets>all</PrivateAssets>
105+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
106+
</PackageReference>
107+
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
108+
<PackageReference Include="NSwag.ApiDescription.Client" Version="13.0.5">
109+
<PrivateAssets>all</PrivateAssets>
110+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
111+
</PackageReference>
112+
</ItemGroup>
113+
114+
<ItemGroup>
115+
<OpenApiReference Include="OpenAPIs\swagger.json" CodeGenerator="NSwagCSharp" ClassName="ExampleApiClient">
116+
<SourceUri>http://localhost:14140/swagger/v1/swagger.json</SourceUri>
117+
</OpenApiReference>
118+
</ItemGroup>
119+
```
120+
121+
From here, continue from step 3 in the list of steps for Visual Studio.
122+
123+
## Configuration
124+
125+
### NSwag
126+
127+
The `OpenApiReference` element in the project file accepts an `Options` element to pass additional settings to the client generator,
128+
which are listed [here](https://github.com/RicoSuter/NSwag/blob/master/src/NSwag.Commands/Commands/CodeGeneration/OpenApiToCSharpClientCommand.cs).
129+
130+
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):
131+
132+
```xml
133+
<OpenApiReference Include="swagger.json">
134+
<Namespace>ExampleProject.GeneratedCode</Namespace>
135+
<ClassName>SalesApiClient</ClassName>
136+
<CodeGenerator>NSwagCSharp</CodeGenerator>
137+
<Options>/UseBaseUrl:false /GenerateClientInterfaces:true</Options>
138+
</OpenApiReference>
139+
```

docs/usage/openapi.md

+29-44
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,49 @@
11
# OpenAPI
22

3-
You can describe your API with an OpenAPI specification using the [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) integration for JsonApiDotNetCore.
3+
JsonApiDotNetCore provides an extension package that enables you to produce an [OpenAPI specification](https://swagger.io/specification/) for your JSON:API endpoints. This can be used to generate a [documentation website](https://swagger.io/tools/swagger-ui/) or to generate [client libraries](https://openapi-generator.tech/docs/generators/) in various languages. The package provides an integration with [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore).
44

5-
## Installation
65

7-
Install the `JsonApiDotNetCore.OpenApi` NuGet package.
6+
## Getting started
87

9-
### CLI
8+
1. Install the `JsonApiDotNetCore.OpenApi` NuGet package:
109

11-
```
12-
dotnet add package JsonApiDotNetCore.OpenApi
13-
```
14-
15-
### Visual Studio
16-
17-
```powershell
18-
Install-Package JsonApiDotNetCore.OpenApi
19-
```
20-
21-
### *.csproj
22-
23-
```xml
24-
<ItemGroup>
25-
<!-- Be sure to check NuGet for the latest version # -->
26-
<PackageReference Include="JsonApiDotNetCore.OpenApi" Version="4.0.0" />
27-
</ItemGroup>
28-
```
29-
30-
## Usage
10+
```
11+
dotnet add package JsonApiDotNetCore.OpenApi
12+
```
3113
32-
Add the integration in your `Startup` class.
14+
2. Add the integration in your `Startup` class.
3315
34-
```c#
35-
public class Startup
36-
{
37-
public void ConfigureServices(IServiceCollection services)
16+
```c#
17+
public class Startup
3818
{
39-
IMvcCoreBuilder mvcBuilder = services.AddMvcCore();
40-
services.AddJsonApi<AppDbContext>(mvcBuilder: mvcBuilder);
19+
public void ConfigureServices(IServiceCollection services)
20+
{
21+
IMvcCoreBuilder mvcBuilder = services.AddMvcCore();
4122
42-
// Adds the Swashbuckle integration.
43-
services.AddOpenApi(mvcBuilder);
44-
}
23+
services.AddJsonApi<AppDbContext>(mvcBuilder: mvcBuilder);
4524
46-
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
47-
{
48-
app.UseRouting();
49-
app.UseJsonApi();
25+
// Adds the Swashbuckle integration.
26+
services.AddOpenApi(mvcBuilder);
27+
}
28+
29+
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
30+
{
31+
app.UseRouting();
32+
app.UseJsonApi();
5033
51-
// Adds the Swashbuckle middleware.
52-
app.UseSwagger();
34+
// Adds the Swashbuckle middleware.
35+
app.UseSwagger();
5336
54-
app.UseEndpoints(endpoints => endpoints.MapControllers());
37+
app.UseEndpoints(endpoints => endpoints.MapControllers());
38+
}
5539
}
56-
}
57-
```
40+
```
5841
5942
By default, the OpenAPI specification will be available at `http://localhost:<port>/swagger/v1/swagger.json`.
6043
61-
Swashbuckle also ships with [SwaggerUI](https://swagger.io/tools/swagger-ui/), tooling for a generated documentation page. This can be enabled by installing the `Swashbuckle.AspNetCore.SwaggerUI` NuGet package and adding the following to your `Startup` class.
44+
## Documentation
45+
46+
Swashbuckle also ships with [SwaggerUI](https://swagger.io/tools/swagger-ui/), tooling for a generated documentation page. This can be enabled by installing the `Swashbuckle.AspNetCore.SwaggerUI` NuGet package and adding the following to your `Startup` class:
6247
6348
```c#
6449
// Startup.cs

docs/usage/toc.md

+2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
# [Errors](errors.md)
2222
# [Metadata](meta.md)
2323
# [Caching](caching.md)
24+
2425
# [OpenAPI](openapi.md)
26+
## [Client](openapi-client.md)
2527

2628
# Extensibility
2729
## [Layer Overview](extensibility/layer-overview.md)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using JsonApiDotNetCore.OpenApi.Client;
2+
using Newtonsoft.Json;
3+
4+
namespace JsonApiDotNetCoreExampleClient
5+
{
6+
public partial class ExampleApiClient : JsonApiClient
7+
{
8+
partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings)
9+
{
10+
SetSerializerSettingsForJsonApi(settings);
11+
12+
settings.Formatting = Formatting.Indented;
13+
}
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<TargetFramework>$(NetCoreAppVersion)</TargetFramework>
5+
</PropertyGroup>
6+
7+
<ItemGroup>
8+
<ProjectReference Include="..\..\JsonApiDotNetCore.OpenApi.Client\JsonApiDotNetCore.OpenApi.Client.csproj" />
9+
</ItemGroup>
10+
11+
<ItemGroup>
12+
<PackageReference Include="Microsoft.Extensions.ApiDescription.Client" Version="5.0.10">
13+
<PrivateAssets>all</PrivateAssets>
14+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
15+
</PackageReference>
16+
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
17+
<PackageReference Include="NSwag.ApiDescription.Client" Version="13.13.2">
18+
<PrivateAssets>all</PrivateAssets>
19+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
20+
</PackageReference>
21+
</ItemGroup>
22+
23+
<ItemGroup>
24+
<OpenApiReference Include="OpenAPIs\swagger.json" CodeGenerator="NSwagCSharp" ClassName="ExampleApiClient">
25+
<SourceUri>http://localhost:14140/swagger/v1/swagger.json</SourceUri>
26+
</OpenApiReference>
27+
</ItemGroup>
28+
</Project>

0 commit comments

Comments
 (0)