Skip to content

Commit c29a2e6

Browse files
committed
Convert OpenAPI client example into worker service
1 parent 2e6e9a0 commit c29a2e6

File tree

7 files changed

+179
-98
lines changed

7 files changed

+179
-98
lines changed

src/Examples/OpenApiNSwagClientExample/ColoredConsoleLogDelegatingHandler.cs renamed to src/Examples/OpenApiNSwagClientExample/ColoredConsoleLogHttpMessageHandler.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,24 @@ namespace OpenApiNSwagClientExample;
55
/// <summary>
66
/// Writes incoming and outgoing HTTP messages to the console.
77
/// </summary>
8-
[UsedImplicitly]
9-
internal sealed class ColoredConsoleLogDelegatingHandler : DelegatingHandler
8+
internal sealed class ColoredConsoleLogHttpMessageHandler : DelegatingHandler
109
{
1110
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
1211
{
12+
#if DEBUG
1313
await LogRequestAsync(request, cancellationToken);
14+
#endif
1415

1516
HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
1617

18+
#if DEBUG
1719
await LogResponseAsync(response, cancellationToken);
20+
#endif
1821

1922
return response;
2023
}
2124

25+
[UsedImplicitly]
2226
private static async Task LogRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
2327
{
2428
using var _ = new ConsoleColorScope(ConsoleColor.Green);
@@ -33,6 +37,7 @@ private static async Task LogRequestAsync(HttpRequestMessage request, Cancellati
3337
}
3438
}
3539

40+
[UsedImplicitly]
3641
private static async Task LogResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken)
3742
{
3843
using var _ = new ConsoleColorScope(ConsoleColor.Cyan);

src/Examples/OpenApiNSwagClientExample/OpenApiNSwagClientExample.csproj

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk.Worker">
22
<PropertyGroup>
33
<!-- TargetFrameworks does not work, see https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2706 -->
44
<TargetFramework>net8.0</TargetFramework>
5-
<OutputType>Exe</OutputType>
65
</PropertyGroup>
76

87
<Import Project="..\..\..\package-versions.props" />
@@ -12,19 +11,16 @@
1211
</ItemGroup>
1312

1413
<ItemGroup>
15-
<PackageReference Include="Microsoft.Extensions.ApiDescription.Client" Version="$(MicrosoftApiClientVersion)">
16-
<PrivateAssets>all</PrivateAssets>
17-
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
18-
</PackageReference>
14+
<PackageReference Include="Microsoft.Extensions.ApiDescription.Client" Version="$(MicrosoftApiClientVersion)" PrivateAssets="all" />
15+
<PackageReference Include="Microsoft.Extensions.Hosting" Version="$(AspNetCoreVersion)" />
16+
<PackageReference Include="Microsoft.Extensions.Http" Version="$(AspNetCoreVersion)" />
1917
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
20-
<PackageReference Include="NSwag.ApiDescription.Client" Version="$(NSwagApiClientVersion)">
21-
<PrivateAssets>all</PrivateAssets>
22-
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
23-
</PackageReference>
18+
<PackageReference Include="NSwag.ApiDescription.Client" Version="$(NSwagApiClientVersion)" PrivateAssets="all" />
2419
</ItemGroup>
2520

2621
<ItemGroup>
2722
<OpenApiReference Include="..\JsonApiDotNetCoreExample\GeneratedSwagger\JsonApiDotNetCoreExample.json">
23+
<Namespace>OpenApiNSwagClientExample</Namespace>
2824
<ClassName>ExampleApiClient</ClassName>
2925
<OutputPath>ExampleApiClient.cs</OutputPath>
3026
<CodeGenerator>NSwagCSharp</CodeGenerator>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System.Net;
2+
using System.Text;
3+
using JetBrains.Annotations;
4+
using JsonApiDotNetCore.OpenApi.Client.NSwag;
5+
6+
namespace OpenApiNSwagClientExample;
7+
8+
/// <summary>
9+
/// Prints the specified people, their assigned todo-items, and its tags.
10+
/// </summary>
11+
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)]
12+
internal sealed class PeopleMessageFormatter
13+
{
14+
public static void PrintPeople(ApiResponse<PersonCollectionResponseDocument?> peopleResponse)
15+
{
16+
string message = WritePeople(peopleResponse);
17+
Console.WriteLine(message);
18+
}
19+
20+
private static string WritePeople(ApiResponse<PersonCollectionResponseDocument?> peopleResponse)
21+
{
22+
if (peopleResponse is { StatusCode: (int)HttpStatusCode.NotModified, Result: null })
23+
{
24+
return "The HTTP response hasn't changed, so no response body was returned.";
25+
}
26+
27+
var builder = new StringBuilder();
28+
builder.AppendLine($"Found {peopleResponse.Result!.Data.Count} people:");
29+
30+
foreach (PersonDataInResponse person in peopleResponse.Result!.Data)
31+
{
32+
WritePerson(person, peopleResponse.Result!.Included, builder);
33+
}
34+
35+
return builder.ToString();
36+
}
37+
38+
private static void WritePerson(PersonDataInResponse person, ICollection<DataInResponse> includes, StringBuilder builder)
39+
{
40+
ToManyTodoItemInResponse assignedTodoItems = person.Relationships.AssignedTodoItems;
41+
42+
builder.AppendLine($" Person {person.Id}: {person.Attributes.DisplayName} with {assignedTodoItems.Data.Count} assigned todo-items:");
43+
WriteRelatedTodoItems(assignedTodoItems.Data, includes, builder);
44+
}
45+
46+
private static void WriteRelatedTodoItems(IEnumerable<TodoItemIdentifier> todoItemIdentifiers, ICollection<DataInResponse> includes, StringBuilder builder)
47+
{
48+
foreach (TodoItemIdentifier todoItemIdentifier in todoItemIdentifiers)
49+
{
50+
TodoItemDataInResponse includedTodoItem = includes.OfType<TodoItemDataInResponse>().Single(include => include.Id == todoItemIdentifier.Id);
51+
ToManyTagInResponse tags = includedTodoItem.Relationships.Tags;
52+
53+
builder.AppendLine($" TodoItem {includedTodoItem.Id}: {includedTodoItem.Attributes.Description} with {tags.Data.Count} tags:");
54+
WriteRelatedTags(tags.Data, includes, builder);
55+
}
56+
}
57+
58+
private static void WriteRelatedTags(IEnumerable<TagIdentifier> tagIdentifiers, ICollection<DataInResponse> includes, StringBuilder builder)
59+
{
60+
foreach (TagIdentifier tagIdentifier in tagIdentifiers)
61+
{
62+
TagDataInResponse includedTag = includes.OfType<TagDataInResponse>().Single(include => include.Id == tagIdentifier.Id);
63+
builder.AppendLine($" Tag {includedTag.Id}: {includedTag.Attributes.Name}");
64+
}
65+
}
66+
}
Lines changed: 7 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,10 @@
1-
using System.Net;
2-
using JsonApiDotNetCore.OpenApi.Client.NSwag;
31
using OpenApiNSwagClientExample;
42

5-
#if DEBUG
6-
using var httpClient = new HttpClient(new ColoredConsoleLogDelegatingHandler
7-
{
8-
InnerHandler = new HttpClientHandler()
9-
});
10-
#else
11-
using var httpClient = new HttpClient();
12-
#endif
3+
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
4+
builder.Services.AddLogging(options => options.ClearProviders());
5+
builder.Services.AddHostedService<Worker>();
6+
builder.Services.AddSingleton<ColoredConsoleLogHttpMessageHandler>();
7+
builder.Services.AddHttpClient<ExampleApiClient>().AddHttpMessageHandler<ColoredConsoleLogHttpMessageHandler>();
138

14-
var apiClient = new ExampleApiClient(httpClient);
15-
16-
ApiResponse<PersonCollectionResponseDocument?> getResponse1 = await GetPersonCollectionAsync(apiClient, null);
17-
ApiResponse<PersonCollectionResponseDocument?> getResponse2 = await GetPersonCollectionAsync(apiClient, getResponse1.Headers["ETag"].First());
18-
19-
if (getResponse2 is { StatusCode: (int)HttpStatusCode.NotModified, Result: null })
20-
{
21-
Console.WriteLine("The HTTP response hasn't changed, so no response body was returned.");
22-
}
23-
24-
foreach (PersonDataInResponse person in getResponse1.Result!.Data)
25-
{
26-
PrintPerson(person, getResponse1.Result.Included);
27-
}
28-
29-
var patchRequest = new PersonPatchRequestDocument
30-
{
31-
Data = new PersonDataInPatchRequest
32-
{
33-
Id = "1",
34-
Attributes = new PersonAttributesInPatchRequest
35-
{
36-
LastName = "Doe"
37-
}
38-
}
39-
};
40-
41-
// This line results in sending "firstName: null" instead of omitting it.
42-
using (apiClient.WithPartialAttributeSerialization<PersonPatchRequestDocument, PersonAttributesInPatchRequest>(patchRequest, person => person.FirstName))
43-
{
44-
// Workaround for https://github.com/RicoSuter/NSwag/issues/2499.
45-
await ApiResponse.TranslateAsync(() => apiClient.PatchPersonAsync(patchRequest.Data.Id, null, patchRequest));
46-
}
47-
48-
Console.WriteLine("Press any key to close.");
49-
Console.ReadKey();
50-
51-
static Task<ApiResponse<PersonCollectionResponseDocument?>> GetPersonCollectionAsync(ExampleApiClient apiClient, string? ifNoneMatch)
52-
{
53-
return ApiResponse.TranslateAsync(() => apiClient.GetPersonCollectionAsync(new Dictionary<string, string?>
54-
{
55-
["filter"] = "has(assignedTodoItems)",
56-
["sort"] = "-lastName",
57-
["page[size]"] = "5",
58-
["include"] = "assignedTodoItems.tags"
59-
}, ifNoneMatch));
60-
}
61-
62-
static void PrintPerson(PersonDataInResponse person, ICollection<DataInResponse> includes)
63-
{
64-
ToManyTodoItemInResponse assignedTodoItems = person.Relationships.AssignedTodoItems;
65-
66-
Console.WriteLine($"Found person {person.Id}: {person.Attributes.DisplayName} with {assignedTodoItems.Data.Count} assigned todo-items:");
67-
68-
PrintRelatedTodoItems(assignedTodoItems.Data, includes);
69-
}
70-
71-
static void PrintRelatedTodoItems(IEnumerable<TodoItemIdentifier> todoItemIdentifiers, ICollection<DataInResponse> includes)
72-
{
73-
foreach (TodoItemIdentifier todoItemIdentifier in todoItemIdentifiers)
74-
{
75-
TodoItemDataInResponse includedTodoItem = includes.OfType<TodoItemDataInResponse>().Single(include => include.Id == todoItemIdentifier.Id);
76-
Console.WriteLine($" TodoItem {includedTodoItem.Id}: {includedTodoItem.Attributes.Description}");
77-
78-
PrintRelatedTags(includedTodoItem.Relationships.Tags.Data, includes);
79-
}
80-
}
81-
82-
static void PrintRelatedTags(IEnumerable<TagIdentifier> tagIdentifiers, ICollection<DataInResponse> includes)
83-
{
84-
foreach (TagIdentifier tagIdentifier in tagIdentifiers)
85-
{
86-
TagDataInResponse includedTag = includes.OfType<TagDataInResponse>().Single(include => include.Id == tagIdentifier.Id);
87-
Console.WriteLine($" Tag {includedTag.Id}: {includedTag.Attributes.Name}");
88-
}
89-
}
9+
IHost host = builder.Build();
10+
await host.RunAsync();
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "http://json.schemastore.org/launchsettings.json",
3+
"profiles": {
4+
"Kestrel": {
5+
"commandName": "Project",
6+
"dotnetRunMessages": true,
7+
"environmentVariables": {
8+
"DOTNET_ENVIRONMENT": "Development"
9+
}
10+
}
11+
}
12+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
using JsonApiDotNetCore.OpenApi.Client.NSwag;
2+
3+
namespace OpenApiNSwagClientExample;
4+
5+
public sealed class Worker(ExampleApiClient apiClient, IHostApplicationLifetime hostApplicationLifetime) : BackgroundService
6+
{
7+
private readonly ExampleApiClient _apiClient = apiClient;
8+
private readonly IHostApplicationLifetime _hostApplicationLifetime = hostApplicationLifetime;
9+
10+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
11+
{
12+
try
13+
{
14+
var queryString = new Dictionary<string, string?>
15+
{
16+
["filter"] = "has(assignedTodoItems)",
17+
["sort"] = "-lastName",
18+
["page[size]"] = "5",
19+
["include"] = "assignedTodoItems.tags"
20+
};
21+
22+
ApiResponse<PersonCollectionResponseDocument?> getResponse = await GetPeopleAsync(_apiClient, queryString, null, stoppingToken);
23+
PeopleMessageFormatter.PrintPeople(getResponse);
24+
25+
string eTag = getResponse.Headers["ETag"].Single();
26+
ApiResponse<PersonCollectionResponseDocument?> getResponseAgain = await GetPeopleAsync(_apiClient, queryString, eTag, stoppingToken);
27+
PeopleMessageFormatter.PrintPeople(getResponseAgain);
28+
29+
await UpdatePersonAsync(stoppingToken);
30+
31+
_ = await _apiClient.GetPersonAsync("999999", null, null, stoppingToken);
32+
}
33+
catch (ApiException<ErrorResponseDocument> exception)
34+
{
35+
Console.WriteLine($"JSON:API ERROR: {exception.Result.Errors.First().Detail}");
36+
}
37+
catch (HttpRequestException exception)
38+
{
39+
Console.WriteLine($"ERROR: {exception.Message}");
40+
}
41+
42+
_hostApplicationLifetime.StopApplication();
43+
}
44+
45+
private static Task<ApiResponse<PersonCollectionResponseDocument?>> GetPeopleAsync(ExampleApiClient apiClient, IDictionary<string, string?> queryString,
46+
string? ifNoneMatch, CancellationToken cancellationToken)
47+
{
48+
return ApiResponse.TranslateAsync(() => apiClient.GetPersonCollectionAsync(queryString, ifNoneMatch, cancellationToken));
49+
}
50+
51+
private async Task UpdatePersonAsync(CancellationToken cancellationToken)
52+
{
53+
var patchRequest = new PersonPatchRequestDocument
54+
{
55+
Data = new PersonDataInPatchRequest
56+
{
57+
Id = "1",
58+
Attributes = new PersonAttributesInPatchRequest
59+
{
60+
LastName = "Doe"
61+
}
62+
}
63+
};
64+
65+
// This line results in sending "firstName: null" instead of omitting it.
66+
using (_apiClient.WithPartialAttributeSerialization<PersonPatchRequestDocument, PersonAttributesInPatchRequest>(patchRequest,
67+
person => person.FirstName))
68+
{
69+
_ = await ApiResponse.TranslateAsync(() => _apiClient.PatchPersonAsync(patchRequest.Data.Id, null, patchRequest, cancellationToken));
70+
}
71+
}
72+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Warning",
5+
"Microsoft.Hosting.Lifetime": "Information",
6+
"System.Net.Http.HttpClient": "Information"
7+
}
8+
}
9+
}

0 commit comments

Comments
 (0)