diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 00000000..c7a8fdbc
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+github: tmenier
diff --git a/src/Flurl.CodeGen/Program.cs b/src/Flurl.CodeGen/Program.cs
index 0d3f1467..db0da26a 100644
--- a/src/Flurl.CodeGen/Program.cs
+++ b/src/Flurl.CodeGen/Program.cs
@@ -178,6 +178,7 @@ private static void WriteHttpExtensionMethod(CodeWriter writer, HttpExtensionMet
args.Add("cancellationToken");
if (xm.RequestBodyType != null) {
+ writer.WriteLine("request.EnsureClient();"); // might need to serialize body, and client might provide custom serializer
writer.WriteLine("var content = new Captured@0Content(@1);",
xm.RequestBodyType,
xm.RequestBodyType == "String" ? "body" : $"request.Settings.{xm.RequestBodyType}Serializer.Serialize(body)");
diff --git a/src/Flurl.Http.Newtonsoft/Flurl.Http.Newtonsoft.csproj b/src/Flurl.Http.Newtonsoft/Flurl.Http.Newtonsoft.csproj
index f6d3e546..8b7cf13b 100644
--- a/src/Flurl.Http.Newtonsoft/Flurl.Http.Newtonsoft.csproj
+++ b/src/Flurl.Http.Newtonsoft/Flurl.Http.Newtonsoft.csproj
@@ -4,10 +4,10 @@
9.0
True
Flurl.Http.Newtonsoft
- 0.9.0
+ 0.9.1
Todd Menier
A Newtonsoft-based JSON serializer for Flurl.Http 4.0 and above.
- Copyright (c) Todd Menier 2023.
+ Copyright (c) Todd Menier 2024.
https://flurl.dev
icon.png
https://github.com/tmenier/Flurl.git
diff --git a/src/Flurl.Http/Flurl.Http.csproj b/src/Flurl.Http/Flurl.Http.csproj
index 8e1dede3..773828c1 100644
--- a/src/Flurl.Http/Flurl.Http.csproj
+++ b/src/Flurl.Http/Flurl.Http.csproj
@@ -4,10 +4,10 @@
9.0
True
Flurl.Http
- 4.0.0
+ 4.0.1
Todd Menier
- 4.0 contains breaking changes! See flurl.dev/upgrade
- Copyright (c) Todd Menier 2023.
+ A fluent, testable HTTP client library.
+ Copyright (c) Todd Menier 2024.
https://flurl.dev
icon.png
README.md
diff --git a/src/Flurl.Http/FlurlRequest.cs b/src/Flurl.Http/FlurlRequest.cs
index 9a20926a..ca9762b4 100644
--- a/src/Flurl.Http/FlurlRequest.cs
+++ b/src/Flurl.Http/FlurlRequest.cs
@@ -53,6 +53,12 @@ public interface IFlurlRequest : ISettingsContainer, IHeadersContainer, IEventHa
///
FlurlCall RedirectedFrom { get; set; }
+ ///
+ /// If Client property is null, selects (or creates) a FlurlClient from the global FlurlHttp.Clients cache. Called
+ /// automatically just before a request is sent, so in most cases there is no need to call explicitly.
+ ///
+ IFlurlClient EnsureClient();
+
///
/// Asynchronously sends the HTTP request. Mainly used to implement higher-level extension methods (GetJsonAsync, etc).
///
@@ -138,12 +144,14 @@ public CookieJar CookieJar {
set => ApplyCookieJar(value);
}
+ ///
+ public IFlurlClient EnsureClient() => Client ??= FlurlHttp.GetClientForRequest(this);
+
///
public Task SendAsync(HttpMethod verb, HttpContent content = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) {
Verb = verb;
Content = content;
- Client ??= FlurlHttp.GetClientForRequest(this);
- return Client.SendAsync(this, completionOption, cancellationToken);
+ return EnsureClient().SendAsync(this, completionOption, cancellationToken);
}
internal static void SyncHeaders(IFlurlClient client, IFlurlRequest request) {
diff --git a/src/Flurl.Http/GeneratedExtensions.cs b/src/Flurl.Http/GeneratedExtensions.cs
index 397e2bd2..75b39ab3 100644
--- a/src/Flurl.Http/GeneratedExtensions.cs
+++ b/src/Flurl.Http/GeneratedExtensions.cs
@@ -25,6 +25,7 @@ public static class GeneratedExtensions
/// The token to monitor for cancellation requests.
/// A Task whose result is the received IFlurlResponse.
public static Task SendJsonAsync(this IFlurlRequest request, HttpMethod verb, object body, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) {
+ request.EnsureClient();
var content = new CapturedJsonContent(request.Settings.JsonSerializer.Serialize(body));
return request.SendAsync(verb, content, completionOption, cancellationToken);
}
@@ -39,6 +40,7 @@ public static Task SendJsonAsync(this IFlurlRequest request, Htt
/// The token to monitor for cancellation requests.
/// A Task whose result is the received IFlurlResponse.
public static Task SendStringAsync(this IFlurlRequest request, HttpMethod verb, string body, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) {
+ request.EnsureClient();
var content = new CapturedStringContent(body);
return request.SendAsync(verb, content, completionOption, cancellationToken);
}
@@ -53,6 +55,7 @@ public static Task SendStringAsync(this IFlurlRequest request, H
/// The token to monitor for cancellation requests.
/// A Task whose result is the received IFlurlResponse.
public static Task SendUrlEncodedAsync(this IFlurlRequest request, HttpMethod verb, object body, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) {
+ request.EnsureClient();
var content = new CapturedUrlEncodedContent(request.Settings.UrlEncodedSerializer.Serialize(body));
return request.SendAsync(verb, content, completionOption, cancellationToken);
}
@@ -133,6 +136,7 @@ public static Task PostAsync(this IFlurlRequest request, HttpCon
/// The token to monitor for cancellation requests.
/// A Task whose result is the received IFlurlResponse.
public static Task PostJsonAsync(this IFlurlRequest request, object body, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) {
+ request.EnsureClient();
var content = new CapturedJsonContent(request.Settings.JsonSerializer.Serialize(body));
return request.SendAsync(HttpMethod.Post, content, completionOption, cancellationToken);
}
@@ -146,6 +150,7 @@ public static Task PostJsonAsync(this IFlurlRequest request, obj
/// The token to monitor for cancellation requests.
/// A Task whose result is the received IFlurlResponse.
public static Task PostStringAsync(this IFlurlRequest request, string body, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) {
+ request.EnsureClient();
var content = new CapturedStringContent(body);
return request.SendAsync(HttpMethod.Post, content, completionOption, cancellationToken);
}
@@ -159,6 +164,7 @@ public static Task PostStringAsync(this IFlurlRequest request, s
/// The token to monitor for cancellation requests.
/// A Task whose result is the received IFlurlResponse.
public static Task PostUrlEncodedAsync(this IFlurlRequest request, object body, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) {
+ request.EnsureClient();
var content = new CapturedUrlEncodedContent(request.Settings.UrlEncodedSerializer.Serialize(body));
return request.SendAsync(HttpMethod.Post, content, completionOption, cancellationToken);
}
@@ -195,6 +201,7 @@ public static Task PutAsync(this IFlurlRequest request, HttpCont
/// The token to monitor for cancellation requests.
/// A Task whose result is the received IFlurlResponse.
public static Task PutJsonAsync(this IFlurlRequest request, object body, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) {
+ request.EnsureClient();
var content = new CapturedJsonContent(request.Settings.JsonSerializer.Serialize(body));
return request.SendAsync(HttpMethod.Put, content, completionOption, cancellationToken);
}
@@ -208,6 +215,7 @@ public static Task PutJsonAsync(this IFlurlRequest request, obje
/// The token to monitor for cancellation requests.
/// A Task whose result is the received IFlurlResponse.
public static Task PutStringAsync(this IFlurlRequest request, string body, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) {
+ request.EnsureClient();
var content = new CapturedStringContent(body);
return request.SendAsync(HttpMethod.Put, content, completionOption, cancellationToken);
}
@@ -244,6 +252,7 @@ public static Task PatchAsync(this IFlurlRequest request, HttpCo
/// The token to monitor for cancellation requests.
/// A Task whose result is the received IFlurlResponse.
public static Task PatchJsonAsync(this IFlurlRequest request, object body, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) {
+ request.EnsureClient();
var content = new CapturedJsonContent(request.Settings.JsonSerializer.Serialize(body));
return request.SendAsync(new HttpMethod("PATCH"), content, completionOption, cancellationToken);
}
@@ -257,6 +266,7 @@ public static Task PatchJsonAsync(this IFlurlRequest request, ob
/// The token to monitor for cancellation requests.
/// A Task whose result is the received IFlurlResponse.
public static Task PatchStringAsync(this IFlurlRequest request, string body, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead, CancellationToken cancellationToken = default) {
+ request.EnsureClient();
var content = new CapturedStringContent(body);
return request.SendAsync(new HttpMethod("PATCH"), content, completionOption, cancellationToken);
}
diff --git a/src/Flurl/Flurl.csproj b/src/Flurl/Flurl.csproj
index 91686e9f..dab02777 100644
--- a/src/Flurl/Flurl.csproj
+++ b/src/Flurl/Flurl.csproj
@@ -6,7 +6,7 @@
4.0.0
Todd Menier
A fluent, portable URL builder. To make HTTP calls off the fluent chain, check out Flurl.Http.
- Copyright (c) Todd Menier 2023.
+ Copyright (c) Todd Menier 2024.
https://flurl.dev
icon.png
https://github.com/tmenier/Flurl.git
diff --git a/test/Flurl.Test/Http/HttpTestFixtureBase.cs b/test/Flurl.Test/Http/HttpTestFixtureBase.cs
index 1050ea2d..179978b9 100644
--- a/test/Flurl.Test/Http/HttpTestFixtureBase.cs
+++ b/test/Flurl.Test/Http/HttpTestFixtureBase.cs
@@ -12,14 +12,12 @@ public abstract class HttpTestFixtureBase
[SetUp]
public void SetUp() {
- HttpTest = CreateHttpTest();
+ HttpTest = new HttpTest();
}
[TearDown]
public void TearTown() {
HttpTest.Dispose();
}
-
- protected virtual HttpTest CreateHttpTest() => new HttpTest();
}
}
diff --git a/test/Flurl.Test/Http/NewtonsoftTests.cs b/test/Flurl.Test/Http/NewtonsoftTests.cs
index 34aed71d..21e6028a 100644
--- a/test/Flurl.Test/Http/NewtonsoftTests.cs
+++ b/test/Flurl.Test/Http/NewtonsoftTests.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Net.Http;
using NUnit.Framework;
using Flurl.Http;
@@ -7,43 +8,143 @@
using System.Threading.Tasks;
using Flurl.Http.Configuration;
using Newtonsoft.Json;
-using NUnit.Framework.Constraints;
+using Newtonsoft.Json.Linq;
namespace Flurl.Test.Http
{
- // These inherit from GetTests and PostTests and swap out the JSON serializer
- // in play, which gets us a lot of free tests but also a lot of redundant ones.
- // Maybe worth refactoring someday, but they're fast so it's tolerable for now.
-
- [TestFixture, Parallelizable]
- public class NewtonsoftGetTests : GetTests
+ [TestFixture]
+ public class NewtonsoftTests
{
- protected override HttpTest CreateHttpTest() => base.CreateHttpTest()
- .WithSettings(settings => settings.JsonSerializer = new NewtonsoftJsonSerializer());
+ private readonly JsonSerializerSettings _jsonSettings = new JsonSerializerSettings {
+ DateFormatString = "M/d/yy"
+ };
+
+ // JObject is from Newtsonsoft; STJ doesn't know how to serialize correctly
+ private readonly object _requestObject = new JObject {
+ ["test"] = "test",
+ ["number"] = 1,
+ ["date"] = new DateTime(2024, 1, 2)
+ };
+ private readonly string _requestJson = "{\"test\":\"test\",\"number\":1,\"date\":\"1/2/24\"}";
+
+ private class ResponseModel
+ {
+ // JsonProperty from Newtonsoft; STJ will ignore
+ [JsonProperty("newtonsoft_prop")]
+ public string SomeProp { get; set; }
+ }
+ private readonly ResponseModel _responseObject = new ResponseModel { SomeProp = "xyz" };
+ private readonly string _responseJson = "{\"newtonsoft_prop\":\"xyz\"}";
+
+ [Test]
+ public void test_data_incompatible_with_default_serializer() {
+ // confirms our test data works only with Newtonsoft
+
+ var stj = new DefaultJsonSerializer();
+ Assert.AreNotEqual(_requestJson, stj.Serialize(_requestObject));
+ Assert.AreNotEqual(_responseObject.SomeProp, stj.Deserialize(_responseJson).SomeProp);
+
+ var njs = new NewtonsoftJsonSerializer(_jsonSettings);
+ Assert.AreEqual(_requestJson, njs.Serialize(_requestObject));
+ Assert.AreEqual(_responseObject.SomeProp, njs.Deserialize(_responseJson).SomeProp);
+ }
+
+ private async Task AssertCallAsync(Func> call, bool hasRequestBody) {
+ using var test = new HttpTest();
+ test.RespondWith(_responseJson);
+
+ var resp = await call();
+
+ Assert.AreEqual(_responseObject.SomeProp, resp.SomeProp);
+ if (hasRequestBody)
+ test.ShouldHaveMadeACall().WithRequestBody(_requestJson);
+ }
+
+ [Test, NonParallelizable]
+ public async Task works_with_clientless() {
+ try {
+ FlurlHttp.Clients.Clear();
+ FlurlHttp.Clients.UseNewtonsoft(_jsonSettings);
+
+ Assert.IsInstanceOf(FlurlHttp.GetClientForRequest(new FlurlRequest("http://api.com")).Settings.JsonSerializer);
+
+ await AssertCallAsync(() => "http://api.com".GetJsonAsync(), false);
+ await AssertCallAsync(() => "http://api.com".PostJsonAsync(_requestObject).ReceiveJson(), true);
+ await AssertCallAsync(() => "http://api.com".PutJsonAsync(_requestObject).ReceiveJson(), true);
+ await AssertCallAsync(() => "http://api.com".PatchJsonAsync(_requestObject).ReceiveJson(), true);
+ }
+ finally {
+ FlurlHttp.Clients.Clear();
+ FlurlHttp.Clients.WithDefaults(b => b.Settings.ResetDefaults());
+ }
+ }
+
+ [Test]
+ public async Task works_with_cached_clients() {
+ var cache = new FlurlClientCache().UseNewtonsoft(_jsonSettings);
+
+ await AssertCallAsync(() => cache.GetOrAdd("c1").Request("http://api1.com").GetJsonAsync(), false);
+ await AssertCallAsync(() => cache.GetOrAdd("c2").Request("http://api2.com").PostJsonAsync(_requestObject).ReceiveJson(), true);
+ await AssertCallAsync(() => cache.GetOrAdd("c3").Request("http://api3.com").PutJsonAsync(_requestObject).ReceiveJson(), true);
+ await AssertCallAsync(() => cache.GetOrAdd("c4").Request("http://api4.com").PatchJsonAsync(_requestObject).ReceiveJson(), true);
+ }
+
+ [Test]
+ public async Task works_with_individual_cached_client() {
+ var cache = new FlurlClientCache();
+ var cli = cache.GetOrAdd("foo", null, builder => builder.UseNewtonsoft(_jsonSettings));
+
+ await AssertCallAsync(() => cli.Request("http://api.com").GetJsonAsync(), false);
+ await AssertCallAsync(() => cli.Request("http://api.com").PostJsonAsync(_requestObject).ReceiveJson(), true);
+ await AssertCallAsync(() => cli.Request("http://api.com").PutJsonAsync(_requestObject).ReceiveJson(), true);
+ await AssertCallAsync(() => cli.Request("http://api.com").PatchJsonAsync(_requestObject).ReceiveJson(), true);
+ }
+
+ [Test]
+ public async Task works_with_individual_client() {
+ var cli = new FlurlClient().WithSettings(s => s.JsonSerializer = new NewtonsoftJsonSerializer(_jsonSettings));
+
+ await AssertCallAsync(() => cli.Request("http://api.com").GetJsonAsync(), false);
+ await AssertCallAsync(() => cli.Request("http://api.com").PostJsonAsync(_requestObject).ReceiveJson(), true);
+ await AssertCallAsync(() => cli.Request("http://api.com").PutJsonAsync(_requestObject).ReceiveJson(), true);
+ await AssertCallAsync(() => cli.Request("http://api.com").PatchJsonAsync(_requestObject).ReceiveJson(), true);
+ }
[Test]
public async Task can_get_dynamic() {
- HttpTest.RespondWithJson(new { id = 1, name = "Frank" });
+ using var test = new HttpTest();
+ test.RespondWithJson(new { id = 1, name = "Frank" });
- var data = await "http://some-api.com".GetJsonAsync();
+ var cli = new FlurlClient().WithSettings(s => s.JsonSerializer = new NewtonsoftJsonSerializer());
- Assert.AreEqual(1, data.id);
- Assert.AreEqual("Frank", data.name);
+ AssertResponse(await cli.Request("https://api.com").GetJsonAsync());
+ AssertResponse(await cli.Request("https://api.com").PostJsonAsync(new { a = 1, b = 2 }).ReceiveJson());
+
+ void AssertResponse(dynamic resp) {
+ Assert.AreEqual(1, resp.id);
+ Assert.AreEqual("Frank", resp.name);
+ }
}
[Test]
public async Task can_get_dynamic_list() {
- HttpTest.RespondWithJson(new[] {
+ using var test = new HttpTest();
+ test.RespondWithJson(new[] {
new { id = 1, name = "Frank" },
new { id = 2, name = "Claire" }
});
- var data = await "http://some-api.com".GetJsonListAsync();
+ var cli = new FlurlClient().WithSettings(s => s.JsonSerializer = new NewtonsoftJsonSerializer());
- Assert.AreEqual(1, data[0].id);
- Assert.AreEqual("Frank", data[0].name);
- Assert.AreEqual(2, data[1].id);
- Assert.AreEqual("Claire", data[1].name);
+ AssertResponse(await cli.Request("https://api.com").GetJsonListAsync());
+ AssertResponse(await cli.Request("https://api.com").PatchJsonAsync(new { a = 1, b = 2 }).ReceiveJsonList());
+
+ void AssertResponse(IList resp) {
+ Assert.AreEqual(1, resp[0].id);
+ Assert.AreEqual("Frank", resp[0].name);
+ Assert.AreEqual(2, resp[1].id);
+ Assert.AreEqual("Claire", resp[1].name);
+ }
}
[Test]
@@ -70,10 +171,13 @@ public async Task null_response_returns_null_dynamic() {
[TestCase(false)]
[TestCase(true)]
public async Task can_get_error_json_untyped(bool useShortcut) {
- HttpTest.RespondWithJson(new { code = 999, message = "our server crashed" }, 500);
+ using var test = new HttpTest();
+ test.RespondWithJson(new { code = 999, message = "our server crashed" }, 500);
+
+ var cli = new FlurlClient().WithSettings(s => s.JsonSerializer = new NewtonsoftJsonSerializer());
try {
- await "http://api.com".GetStringAsync();
+ await cli.Request("http://api.com").GetStringAsync();
}
catch (FlurlHttpException ex) {
var error = useShortcut ? // error is a dynamic this time
@@ -85,65 +189,4 @@ await ex.GetResponseJsonAsync() :
}
}
}
-
- [TestFixture, Parallelizable]
- public class NewtonsoftPostTests : PostTests
- {
- protected override HttpTest CreateHttpTest() => base.CreateHttpTest()
- .WithSettings(settings => settings.JsonSerializer = new NewtonsoftJsonSerializer());
-
- [Test]
- public async Task can_receive_json_dynamic() {
- HttpTest.RespondWithJson(new { id = 1, name = "Frank" });
-
- var data = await "http://some-api.com".PostJsonAsync(new { a = 1, b = 2 }).ReceiveJson();
-
- Assert.AreEqual(1, data.id);
- Assert.AreEqual("Frank", data.name);
- }
-
- [Test]
- public async Task can_receive_json_dynamic_list() {
- HttpTest.RespondWithJson(new[] {
- new { id = 1, name = "Frank" },
- new { id = 2, name = "Claire" }
- });
-
- var data = await "http://some-api.com".PostJsonAsync(new { a = 1, b = 2 }).ReceiveJsonList();
-
- Assert.AreEqual(1, data[0].id);
- Assert.AreEqual("Frank", data[0].name);
- Assert.AreEqual(2, data[1].id);
- Assert.AreEqual("Claire", data[1].name);
- }
- }
-
- [TestFixture, Parallelizable]
- public class NewtonsoftConfigTests
- {
- [Test]
- public void can_register_with_builder() {
- var cache = new FlurlClientCache();
- var cli = cache.GetOrAdd("foo", null, builder => builder.UseNewtonsoft(new JsonSerializerSettings { DateFormatString = "1234" }));
-
- Assert.IsInstanceOf(cli.Settings.JsonSerializer);
-
- var obj = new { Date = DateTime.Now };
- var json = cli.Settings.JsonSerializer.Serialize(obj);
- Assert.AreEqual("{\"Date\":\"1234\"}", json);
- }
-
- [Test]
- public void can_register_with_cache() {
- var cache = new FlurlClientCache().UseNewtonsoft(new JsonSerializerSettings { DateFormatString = "1234" });
- var cli = cache.GetOrAdd("foo");
-
- Assert.IsInstanceOf(cli.Settings.JsonSerializer);
-
- var obj = new { Date = DateTime.Now };
- var json = cli.Settings.JsonSerializer.Serialize(obj);
- Assert.AreEqual("{\"Date\":\"1234\"}", json);
- }
-
- }
}