Skip to content

Commit

Permalink
Utilize System.Text.Json.Serialization.JsonSerializerContext (#69)
Browse files Browse the repository at this point in the history
* Introduce DaktelaJsonSerializerContext
* Add custom JsonContent in HttpRequestSerializer
* Add DaktelaJsonSerializerContext.SerializationDateTimeOffset
* Use JsonSourceGenerationMode.Metadata
  • Loading branch information
trejjam authored Jun 5, 2023
1 parent 9cd7882 commit 916e6e9
Show file tree
Hide file tree
Showing 28 changed files with 669 additions and 204 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
with:
dotnet-version: '7.0.x'
- uses: actions/checkout@v3
- name: Retore
- name: Restore
run: dotnet restore --nologo src
- name: Build
run: dotnet build --no-restore --nologo --configuration Release src
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
with:
dotnet-version: '7.0.x'
- uses: actions/checkout@v3
- name: Retore
- name: Restore
run: dotnet restore --nologo src
- name: Build
run: dotnet build --no-restore --nologo --configuration Release src
Expand Down
86 changes: 86 additions & 0 deletions src/Daktela.HttpClient/Api/DaktelaJsonSerializerContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using Daktela.HttpClient.Implementations.JsonConverters;
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Daktela.HttpClient.Api;

[JsonSourceGenerationOptions(
WriteIndented = true,
PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
GenerationMode = JsonSourceGenerationMode.Metadata
)]
[JsonSerializable(typeof(Contacts.CreateContact))]
[JsonSerializable(typeof(Contacts.UpdateContact))]
[JsonSerializable(typeof(Tickets.CreateActivity))]
[JsonSerializable(typeof(Tickets.CreateTicket))]
[JsonSerializable(typeof(Tickets.UpdateActivity))]
[JsonSerializable(typeof(Tickets.UpdateTicket))]
[JsonSerializable(typeof(Tickets.ReadActivity<Tickets.Activities.CallActivity>))]
[JsonSerializable(typeof(Tickets.ReadActivity<Tickets.Activities.CommentActivity>))]
[JsonSerializable(typeof(Tickets.ReadActivity<Tickets.Activities.CustomActivity>))]
[JsonSerializable(typeof(Tickets.ReadActivity<Tickets.Activities.EmailActivity>))]
[JsonSerializable(typeof(Tickets.ReadActivity<Tickets.Activities.MessengerActivity>))]
[JsonSerializable(typeof(Tickets.ReadActivity<Tickets.Activities.SmsActivity>))]
[JsonSerializable(typeof(Tickets.ReadActivity<Tickets.Activities.ViberActivity>))]
[JsonSerializable(typeof(Tickets.ReadActivity<Tickets.Activities.WebChatActivity>))]
[JsonSerializable(typeof(Tickets.ReadActivity<Tickets.Activities.WhatsAppActivity>))]
[JsonSerializable(typeof(Responses.Errors.ComplexErrorResponse))]
[JsonSerializable(typeof(Responses.Errors.ErrorFormMessages))]
[JsonSerializable(typeof(Responses.Errors.NestedErrorForm))]
[JsonSerializable(typeof(Responses.Errors.PlainErrorResponse))]
[JsonSerializable(typeof(Responses.ListResponse<Contacts.ReadContact>))]
[JsonSerializable(typeof(Responses.ListResponse<Tickets.Category>))]
[JsonSerializable(typeof(Responses.ListResponse<Tickets.ReadActivity>))]
[JsonSerializable(typeof(Responses.ListResponse<Tickets.ReadActivityAttachment>))]
[JsonSerializable(typeof(Responses.ListResponse<Tickets.ReadTicket>))]
[JsonSerializable(typeof(Responses.SingleResponse<Contacts.ReadContact>))]
[JsonSerializable(typeof(Responses.SingleResponse<Tickets.ReadTicket>))]
[JsonSerializable(typeof(Responses.SingleResponse<Tickets.ReadActivity>))]
[JsonSerializable(typeof(IDictionary<string, ICollection<string>>))]
public partial class DaktelaJsonSerializerContext : JsonSerializerContext
{
private static TimeSpan _serializationDateTimeOffset;

public static TimeSpan SerializationDateTimeOffset
{
get => _serializationDateTimeOffset;
set
{
_serializationDateTimeOffset = value;
_convertersContext = null;
}
}

private static JsonSerializerOptions ConvertersContextOptions => new()
{
DefaultIgnoreCondition = JsonIgnoreCondition.Never,
IgnoreReadOnlyFields = false,
IgnoreReadOnlyProperties = false,
IncludeFields = false,
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters =
{
new DateTimeOffsetConverter(SerializationDateTimeOffset),
new TimeSpanConverter(),
new ReadActivityConverter(),
new CustomFieldsConverter(),
new EnumsConverterFactory(),
new EmailActivityOptionsHeadersAddressConverter(),
new ErrorResponseConverter(),
new ErrorFormConverter(),
},
};

private static DaktelaJsonSerializerContext? _convertersContext;

/// <summary>
/// The default <see cref="global::System.Text.Json.Serialization.JsonSerializerContext"/> associated with a default <see cref="global::System.Text.Json.JsonSerializerOptions"/> instance.
/// </summary>
public static DaktelaJsonSerializerContext CustomConverters => _convertersContext
??= new DaktelaJsonSerializerContext(
new JsonSerializerOptions(ConvertersContextOptions)
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Daktela.HttpClient.Api;
using Microsoft.Extensions.Options;

namespace Daktela.HttpClient.Configuration;

public class DaktelaPostConfigureOptions : IPostConfigureOptions<DaktelaOptions>
{
public void PostConfigure(
string? name, DaktelaOptions options
) => DaktelaJsonSerializerContext.SerializationDateTimeOffset = options.DateTimeOffset!.Value;
}
11 changes: 7 additions & 4 deletions src/Daktela.HttpClient/DependencyInjection/DaktelaExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,15 @@ public static IServiceCollection AddDaktelaHttpClient(
Action<IHttpClientBuilder>? configureHttpClientBuilder = null
)
{
serviceCollection.AddSingleton<IValidateOptions<DaktelaOptions>>(
_ => new DataAnnotationValidateOptions<DaktelaOptions>(nameof(DaktelaOptions))
);
serviceCollection
.AddSingleton<IValidateOptions<DaktelaOptions>>(
new DataAnnotationValidateOptions<DaktelaOptions>(nameof(DaktelaOptions))
)
.AddSingleton<IPostConfigureOptions<DaktelaOptions>, DaktelaPostConfigureOptions>();

serviceCollection.TryAddSingleton<IHttpRequestFactory, HttpRequestFactory>();
var httpClientBuilder = serviceCollection.AddHttpClient<IDaktelaHttpClient, DaktelaHttpClient>((serviceProvider, httpClient) =>
var httpClientBuilder = serviceCollection
.AddHttpClient<IDaktelaHttpClient, DaktelaHttpClient>((serviceProvider, httpClient) =>
{
var daktelaOptions = serviceProvider.GetRequiredService<IOptions<DaktelaOptions>>().Value;

Expand Down
74 changes: 61 additions & 13 deletions src/Daktela.HttpClient/Implementations/DaktelaHttpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Net;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization.Metadata;
using System.Threading;
using System.Threading.Tasks;

Expand Down Expand Up @@ -52,20 +53,31 @@ CancellationToken cancellationToken
.ConfigureAwait(false);

public async Task<SingleResponse<T>> GetAsync<T>(
IHttpResponseParser httpResponseParser, string path, CancellationToken cancellationToken
IHttpResponseParser httpResponseParser,
string path,
JsonTypeInfo<SingleResponse<T>> jsonTypeInfoForResponseType,
CancellationToken cancellationToken
) where T : class
{
using var httpRequestMessage = _httpRequestFactory.CreateHttpRequestMessage(HttpMethod.Get, path);

using var httpResponse = await RawSendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
using var httpResponse = await RawSendAsync(
httpRequestMessage,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken
);

// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
switch (httpResponse.StatusCode)
{
case HttpStatusCode.OK:
try
{
var response = await httpResponseParser.ParseResponseAsync<SingleResponse<T>>(httpResponse.Content, cancellationToken);
var response = await httpResponseParser.ParseResponseAsync(
httpResponse.Content,
jsonTypeInfoForResponseType,
cancellationToken
);

return response;
}
Expand All @@ -86,20 +98,29 @@ public async Task<ListResponse<T>> GetListAsync<T>(
IHttpResponseParser httpResponseParser,
string path,
IRequest request,
JsonTypeInfo<ListResponse<T>> jsonTypeInfoForResponseType,
CancellationToken cancellationToken
) where T : class
{
using var httpRequestMessage = _httpRequestFactory.CreateHttpRequestMessage(HttpMethod.Get, path, request);

using var httpResponse = await RawSendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
using var httpResponse = await RawSendAsync(
httpRequestMessage,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken
);

// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
switch (httpResponse.StatusCode)
{
case HttpStatusCode.OK:
try
{
var response = await httpResponseParser.ParseResponseAsync<ListResponse<T>>(httpResponse.Content, cancellationToken);
var response = await httpResponseParser.ParseResponseAsync(
httpResponse.Content,
jsonTypeInfoForResponseType,
cancellationToken
);

return response;
}
Expand All @@ -121,22 +142,31 @@ public async Task<TResponseContract> PostAsync<TRequest, TResponseContract>(
IHttpResponseParser httpResponseParser,
string path,
TRequest request,
JsonTypeInfo<TRequest> jsonTypeInfoForRequestType,
JsonTypeInfo<SingleResponse<TResponseContract>> jsonTypeInfoForResponseType,
CancellationToken cancellationToken
)
where TRequest : class
where TResponseContract : class
{
using var httpRequestMessage = _httpRequestFactory.CreateHttpRequestMessage(httpRequestSerializer, HttpMethod.Post, path, request);
using var httpRequestMessage = _httpRequestFactory.CreateHttpRequestMessage(
httpRequestSerializer, HttpMethod.Post, path, request, jsonTypeInfoForRequestType
);

using var httpResponse = await RawSendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
using var httpResponse = await RawSendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead,
cancellationToken);

// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
switch (httpResponse.StatusCode)
{
case HttpStatusCode.Created:
try
{
var response = await httpResponseParser.ParseResponseAsync<SingleResponse<TResponseContract>>(httpResponse.Content, cancellationToken);
var response = await httpResponseParser.ParseResponseAsync(
httpResponse.Content,
jsonTypeInfoForResponseType,
cancellationToken
);

return response.Result;
}
Expand All @@ -147,7 +177,11 @@ CancellationToken cancellationToken
case HttpStatusCode.BadRequest:
try
{
var badRequest = await httpResponseParser.ParseResponseAsync<SingleResponse<TResponseContract>>(httpResponse.Content, cancellationToken);
var badRequest = await httpResponseParser.ParseResponseAsync(
httpResponse.Content,
jsonTypeInfoForResponseType,
cancellationToken
);

throw new BadRequestException<TResponseContract>(badRequest.Result, badRequest.Error);
}
Expand All @@ -169,22 +203,31 @@ public async Task<TResponseContract> PutAsync<TRequest, TResponseContract>(
IHttpResponseParser httpResponseParser,
string path,
TRequest request,
JsonTypeInfo<TRequest> jsonTypeInfoForRequestType,
JsonTypeInfo<SingleResponse<TResponseContract>> jsonTypeInfoForResponseType,
CancellationToken cancellationToken
)
where TRequest : class
where TResponseContract : class
{
using var httpRequestMessage = _httpRequestFactory.CreateHttpRequestMessage(httpRequestSerializer, HttpMethod.Put, path, request);
using var httpRequestMessage = _httpRequestFactory.CreateHttpRequestMessage(
httpRequestSerializer, HttpMethod.Put, path, request, jsonTypeInfoForRequestType
);

using var httpResponse = await RawSendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
using var httpResponse = await RawSendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead,
cancellationToken);

// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
switch (httpResponse.StatusCode)
{
case HttpStatusCode.OK:
try
{
var response = await httpResponseParser.ParseResponseAsync<SingleResponse<TResponseContract>>(httpResponse.Content, cancellationToken);
var response = await httpResponseParser.ParseResponseAsync(
httpResponse.Content,
jsonTypeInfoForResponseType,
cancellationToken
);

return response.Result;
}
Expand All @@ -195,7 +238,12 @@ CancellationToken cancellationToken
case HttpStatusCode.BadRequest:
try
{
var badRequest = await httpResponseParser.ParseResponseAsync<SingleResponse<TResponseContract>>(httpResponse.Content, cancellationToken);
var badRequest =
await httpResponseParser.ParseResponseAsync(
httpResponse.Content,
jsonTypeInfoForResponseType,
cancellationToken
);

throw new BadRequestException<TResponseContract>(badRequest.Result, badRequest.Error);
}
Expand Down
Loading

0 comments on commit 916e6e9

Please sign in to comment.