Skip to content

Commit a935d56

Browse files
feat(media type formatter): add SystemTextJsonFormatter (#86)
* add SystemTextJsonMediaTypeFormatter * add ext method for creation * enable http2/3 on sample server * add loadtest * up readme + cl * use .ConfigureFormatters(x => x.Default = x.Formatters.SystemTextJsonFormatter()) for benchmark
1 parent ac8cfbe commit a935d56

File tree

10 files changed

+252
-32
lines changed

10 files changed

+252
-32
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
### Features
88
- **http client builder:** configurable http version/policy via `WithVersion`, `WithVersionPolicy`
99
- **http client builder:** defaults to http version http2.0
10+
- **media type formatter:** add `SystemTextJsonFormatter` `.ConfigureFormatters(x => x.Default = x.Formatters.SystemTextJsonFormatter())`
1011

1112
### Performance
1213
- **logging:** middleware loggings changed to compile-time logging

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,8 @@ httpClientBuilder.WithRequestBuilderDefaults(builder => builder.AsPut());
227227
// formatters - used for content negotiation, for "Accept" and body media formats. e.g. JSON, XML, etc...
228228
httpClientBuilder.ConfigureFormatters(opts =>
229229
{
230-
opts.Default = new MessagePackMediaTypeFormatter();
230+
opts.Default = new MessagePackMediaTypeFormatter(); // use messagepack
231+
opts.Default = opts.Formatters.SystemTextJsonFormatter(); // use system.text.json
231232
opts.Formatters.Add(new CustomFormatter());
232233
});
233234

benchmark/Benchmarking.cs

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using BenchmarkDotNet.Attributes;
22
using BenchmarkDotNet.Jobs;
3+
using FluentlyHttpClient.MediaFormatters;
34
using FluentlyHttpClient.Middleware;
45
using FluentlyHttpClient.Test;
56
using MessagePack.Resolvers;
@@ -16,8 +17,16 @@ namespace FluentlyHttpClient.Benchmarks;
1617
public class Benchmarking
1718
{
1819
private IFluentHttpClient? _jsonHttpClient;
20+
private IFluentHttpClient? _systemTextJsonHttpClient;
1921
private IFluentHttpClient? _messagePackHttpClient;
2022

23+
private readonly Hero _request = new()
24+
{
25+
Key = "valeera",
26+
Name = "Valeera",
27+
Title = "Shadow of the Uncrowned"
28+
};
29+
2130
private IServiceProvider BuildContainer()
2231
{
2332
Log.Logger = new LoggerConfiguration()
@@ -44,7 +53,7 @@ public void Setup()
4453
var fluentHttpClientFactory = BuildContainer()
4554
.GetRequiredService<IFluentHttpClientFactory>();
4655

47-
var clientBuilder = fluentHttpClientFactory.CreateBuilder("sketch7")
56+
var clientBuilder = fluentHttpClientFactory.CreateBuilder("newtonsoft")
4857
.WithBaseUrl("https://sketch7.com")
4958
.UseLogging(new LoggerHttpMiddlewareOptions
5059
{
@@ -55,33 +64,32 @@ public void Setup()
5564
.WithMessageHandler(mockHttp)
5665
;
5766

58-
_jsonHttpClient = fluentHttpClientFactory.Add(clientBuilder);
67+
_jsonHttpClient = clientBuilder
68+
.ConfigureFormatters(x => x.Default = x.Formatters.JsonFormatter)
69+
.Build();
5970

60-
clientBuilder = fluentHttpClientFactory.CreateBuilder("msgpacks")
61-
.WithBaseUrl("https://sketch7.com")
62-
.UseLogging(new LoggerHttpMiddlewareOptions
63-
{
64-
//ShouldLogDetailedRequest = true,
65-
//ShouldLogDetailedResponse = true
66-
})
67-
.UseTimer()
68-
.WithMessageHandler(mockHttp)
71+
_messagePackHttpClient = clientBuilder.WithIdentifier("msgpacks")
6972
.ConfigureFormatters(x => x.Default = new MessagePackMediaTypeFormatter(ContractlessStandardResolver.Options))
73+
.Build()
7074
;
71-
_messagePackHttpClient = fluentHttpClientFactory.Add(clientBuilder);
75+
76+
_systemTextJsonHttpClient = clientBuilder.WithIdentifier("system.text.json")
77+
.ConfigureFormatters(x => x.Default = x.Formatters.SystemTextJsonFormatter())
78+
.Build()
79+
;
80+
81+
Console.WriteLine($"Setup Complete");
82+
Console.WriteLine($" - _jsonHttpClient: {_jsonHttpClient.DefaultFormatter.GetType().Name}");
83+
Console.WriteLine($" - _messagePackHttpClient: {_messagePackHttpClient.DefaultFormatter.GetType().Name}");
84+
Console.WriteLine($" - _systemTextJsonHttpClient: {_systemTextJsonHttpClient.DefaultFormatter.GetType().Name}");
7285
}
7386

7487
[Benchmark]
7588
public Task<Hero> PostAsJson()
7689
{
7790
return _jsonHttpClient.CreateRequest("/api/json")
7891
.AsPost()
79-
.WithBody(new Hero
80-
{
81-
Key = "valeera",
82-
Name = "Valeera",
83-
Title = "Shadow of the Uncrowned"
84-
})
92+
.WithBody(_request)
8593
.Return<Hero>();
8694
}
8795

@@ -90,14 +98,16 @@ public Task<Hero> PostAsMessagePack()
9098
{
9199
return _messagePackHttpClient.CreateRequest("/api/msgpack")
92100
.AsPost()
93-
.WithBody(new Hero
94-
{
95-
Key = "valeera",
96-
Name = "Valeera",
97-
Title = "Shadow of the Uncrowned"
98-
})
101+
.WithBody(_request)
99102
.Return<Hero>();
100103
}
101104

102-
105+
[Benchmark]
106+
public Task<Hero> PostAsSystemTextJson()
107+
{
108+
return _systemTextJsonHttpClient.CreateRequest("/api/json")
109+
.AsPost()
110+
.WithBody(_request)
111+
.Return<Hero>();
112+
}
103113
}

samples/FluentlyHttpClient.Sample.Api/Program.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11

2+
using Microsoft.AspNetCore.Server.Kestrel.Core;
3+
24
namespace FluentlyHttpClient.Sample.Api;
35

46
public class Program
@@ -10,7 +12,10 @@ public static IHostBuilder CreateHostBuilder(string[] args)
1012
.ConfigureWebHostDefaults(webBuilder =>
1113
{
1214
webBuilder
13-
.UseUrls("http://localhost:5500/")
15+
.UseUrls("http://localhost:5500/", "https://localhost:5510")
16+
.ConfigureKestrel(opts => opts
17+
.ConfigureEndpointDefaults(lo => lo.Protocols = HttpProtocols.Http1AndHttp2AndHttp3)
18+
)
1419
.UseStartup<Startup>()
1520
;
1621
});

samples/FluentlyHttpClient.Sample.Api/Properties/launchSettings.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{
1+
{
22
"$schema": "http://json.schemastore.org/launchsettings.json",
33
"iisSettings": {
44
"windowsAuthentication": false,
@@ -12,7 +12,7 @@
1212
"FluentlyHttpClient.Sample.Api": {
1313
"commandName": "Project",
1414
"launchBrowser": true,
15-
"launchUrl": "api/values",
15+
"launchUrl": "api/heroes",
1616
"applicationUrl": "http://localhost:5500",
1717
"environmentVariables": {
1818
"ASPNETCORE_ENVIRONMENT": "Development"

src/FluentlyHttpClient/FluentlyHttpClient.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<!-- <TargetFramework>netstandard2.0</TargetFramework> -->
@@ -12,7 +12,7 @@
1212
<PackageReference Include="Microsoft.Extensions.Http" />
1313
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
1414
<PackageReference Include="Microsoft.AspNet.WebApi.Client" />
15-
<PackageReference Include="MimeTypesMap"/>
15+
<PackageReference Include="MimeTypesMap" />
1616
</ItemGroup>
1717

1818
</Project>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
using System.Net;
2+
using System.Text.Json;
3+
4+
namespace FluentlyHttpClient.MediaFormatters;
5+
6+
// todo: move to separate lib?
7+
public class SystemTextJsonMediaTypeFormatter : MediaTypeFormatter
8+
{
9+
private const string MediaType = "application/json";
10+
private readonly JsonSerializerOptions _options;
11+
12+
/// <summary>
13+
/// Initializes a new instance with default formatter resolver.
14+
/// </summary>
15+
public SystemTextJsonMediaTypeFormatter()
16+
: this(null)
17+
{
18+
}
19+
20+
/// <summary>
21+
/// Initializes a new instance with the provided formatter resolver.
22+
/// </summary>
23+
/// <param name="options"></param>
24+
public SystemTextJsonMediaTypeFormatter(JsonSerializerOptions? options)
25+
{
26+
SupportedMediaTypes.Add(new(MediaType));
27+
_options = options ?? new JsonSerializerOptions
28+
{
29+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
30+
};
31+
}
32+
33+
public override bool CanReadType(Type type)
34+
{
35+
ArgumentNullException.ThrowIfNull(type, nameof(type));
36+
return IsAllowedType(type);
37+
}
38+
39+
public override bool CanWriteType(Type type)
40+
{
41+
ArgumentNullException.ThrowIfNull(type, nameof(type));
42+
return IsAllowedType(type);
43+
}
44+
45+
private static bool IsAllowedType(Type t)
46+
{
47+
if (t is { IsAbstract: false, IsInterface: false, IsNotPublic: false })
48+
return true;
49+
50+
if (typeof(IEnumerable<>).IsAssignableFrom(t))
51+
return true;
52+
53+
return false;
54+
}
55+
56+
public override async Task<object?> ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
57+
=> await JsonSerializer.DeserializeAsync(readStream, type, _options);
58+
59+
public override async Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content,
60+
TransportContext transportContext)
61+
=> await JsonSerializer.SerializeAsync(writeStream, value, type, _options);
62+
}
63+
64+
public static partial class MediaTypeFormattingExtensions
65+
{
66+
/// <summary>
67+
/// Create new System.Text.Json media type formatter.
68+
/// </summary>
69+
/// <param name="formatters"></param>
70+
/// <param name="options">Json serialization options.</param>
71+
public static SystemTextJsonMediaTypeFormatter SystemTextJsonFormatter(
72+
this MediaTypeFormatterCollection formatters,
73+
JsonSerializerOptions? options = null
74+
) => new(options);
75+
}

test/FluentHttpClientFactoryTest.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,26 @@ public void ShouldSetDefaultFormatter()
138138
Assert.Equal(httpClient.Formatters.XmlFormatter, httpClient.DefaultFormatter);
139139
}
140140

141+
[Fact]
142+
public void SetDefaultFormatterMany_ShouldBeSetCorrectly()
143+
{
144+
var clientBuilder = GetNewClientFactory()
145+
.CreateBuilder("abc")
146+
.WithBaseUrl("http://abc.com")
147+
;
148+
var httpClient = clientBuilder
149+
.ConfigureFormatters(opts => opts.Default = opts.Formatters.XmlFormatter)
150+
.Build()
151+
;
152+
var httpClient2 = clientBuilder
153+
.ConfigureFormatters(opts => opts.Default = opts.Formatters.FormUrlEncodedFormatter)
154+
.Build()
155+
;
156+
157+
Assert.Equal(httpClient.Formatters.XmlFormatter, httpClient.DefaultFormatter);
158+
Assert.Equal(httpClient2.Formatters.FormUrlEncodedFormatter, httpClient2.DefaultFormatter);
159+
}
160+
141161
[Fact]
142162
public void ShouldAutoRegisterDefault()
143163
{

test/Integration/LoadTest.cs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Serilog;
3+
using System.Net;
4+
5+
namespace FluentlyHttpClient.Test.Integration;
6+
7+
public record MimirGqlSchema
8+
{
9+
public List<UniverseModel> UniversesIndex { get; set; }
10+
}
11+
12+
public record UniverseModel
13+
{
14+
public string Id { get; set; }
15+
public string Key { get; set; }
16+
public string Name { get; set; }
17+
}
18+
19+
public class LoadTest
20+
{
21+
private static IServiceProvider BuildContainer()
22+
{
23+
Log.Logger = new LoggerConfiguration()
24+
.WriteTo.Console()
25+
.WriteTo.Debug()
26+
.CreateLogger();
27+
var container = new ServiceCollection()
28+
.AddFluentlyHttpClient()
29+
.AddLogging(x => x.AddSerilog());
30+
return container.BuildServiceProvider();
31+
}
32+
33+
[Fact]
34+
[Trait("Category", "e2e")]
35+
public async void GqlHttp2Test()
36+
{
37+
var socketsHandler = new SocketsHttpHandler
38+
{
39+
PooledConnectionIdleTimeout = Timeout.InfiniteTimeSpan,
40+
KeepAlivePingDelay = TimeSpan.FromSeconds(60),
41+
KeepAlivePingTimeout = TimeSpan.FromSeconds(30),
42+
EnableMultipleHttp2Connections = true,
43+
};
44+
var httpClient = BuildContainer()
45+
.GetRequiredService<IFluentHttpClientFactory>()
46+
.CreateBuilder("mimir")
47+
.WithBaseUrl("XXX/v1/api/graphql")
48+
.WithBaseUrlTrailingSlash(false)
49+
.UseLogging()
50+
.UseTimer()
51+
.ConfigureFormatters(opts =>
52+
{
53+
//opts.Default = opts.Formatters.SystemTextJsonFormatter();
54+
})
55+
//.WithRequestBuilderDefaults(x => x.WithVersion(HttpVersion.Version11))
56+
.WithMessageHandler(socketsHandler)
57+
.Build();
58+
59+
for (int i = 0; i < 1; i++)
60+
{
61+
var tasks = Enumerable.Range(0, 1800)
62+
.Select(async (i) =>
63+
{
64+
var response = await httpClient.CreateGqlRequest(new()
65+
{
66+
//OperationName = "universe",
67+
//Variables =
68+
Query = @"
69+
query universes_getByIndex($input: UniverseIndexQuery) {
70+
universesIndex(input: $input) {
71+
...Universe
72+
}
73+
}
74+
75+
fragment Universe on Universe {
76+
id
77+
key
78+
name
79+
isArchived
80+
81+
heroes @include(if: true) {
82+
id
83+
name
84+
}
85+
86+
}
87+
"
88+
})//CreateRequest("/api/heroes/azmodan")
89+
.ReturnAsGqlResponse<MimirGqlSchema>();
90+
//response.Message.Dispose();
91+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
92+
return response;
93+
}
94+
);
95+
await Task.WhenAll(tasks);
96+
}
97+
98+
99+
//var response = await httpClient.CreateRequest("/api/heroes/azmodan")
100+
// .ReturnAsResponse<Hero>();
101+
////response.Message.Dispose();
102+
103+
//Assert.Equal(HttpStatusCode.OK, response.StatusCode);
104+
//Assert.Equal("azmodan", response.Data.Key);
105+
//Assert.Equal("Azmodan", response.Data.Name);
106+
//Assert.Equal("Lord of Sin", response.Data.Title);
107+
}
108+
}

test/Integration/MessagePackIntegrationTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public async void ShouldMakeRequest_Get()
1818
.UseTimer()
1919
.ConfigureFormatters(opts =>
2020
{
21-
opts.Default = _messagePackMediaTypeFormatter;
21+
//opts.Default = opts.Formatters.SystemTextJsonFormatter();
2222
})
2323
.Build();
2424

0 commit comments

Comments
 (0)