diff --git a/Build/build.cmd b/Build/build.cmd index 798519f9..ba32e3fc 100644 --- a/Build/build.cmd +++ b/Build/build.cmd @@ -11,7 +11,7 @@ echo Restoring dependicies was successful. @set project=..\src\Flurl.Http.CodeGen\Flurl.Http.CodeGen.csproj -@call dotnet run -c Release -p %project% ..\src\Flurl.Http\HttpExtensions.cs +@call dotnet run -c Release -p %project% ..\src\Flurl.Http\GeneratedExtensions.cs @if ERRORLEVEL 1 ( echo Error! Generation cs file failed. exit /b 1 diff --git a/Build/test.cmd b/Build/test.cmd index 0cd90821..566e9caf 100644 --- a/Build/test.cmd +++ b/Build/test.cmd @@ -1,3 +1,3 @@ @cd ..\test\Flurl.Test\ - -@call dotnet test -c Release \ No newline at end of file +@call dotnet test -c Release +@cd ..\..\Build\ \ No newline at end of file diff --git a/Flurl.sln b/Flurl.sln index 6f225d06..63ee2597 100644 --- a/Flurl.sln +++ b/Flurl.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26430.6 +VisualStudioVersion = 15.0.26730.3 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flurl", "src\Flurl\Flurl.csproj", "{117B6C6E-53F9-45AE-9439-F4FB7E21B116}" EndProject @@ -18,11 +18,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{86A5ACB4-F EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flurl.Test", "Test\Flurl.Test\Flurl.Test.csproj", "{DF68EB0E-9566-4577-B709-291520383F8D}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9B549ED2-56BB-46AC-A430-E39C2AE74BF3}" - ProjectSection(SolutionItems) = preProject - appveyor.yml = appveyor.yml - EndProjectSection -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PackageTesters", "PackageTesters", "{9A136878-A43E-4154-9B5E-EDAF27E8628D}" EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "PackageTester.Shared", "PackageTesters\PackageTester.Shared\PackageTester.Shared.shproj", "{D4717AA7-5549-4BAD-81C5-406844A12990}" @@ -33,12 +28,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PackageTester.NETCore", "Pa EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PackageTester.NET45", "PackageTesters\PackageTester.NET45\PackageTester.NET45.csproj", "{AA8792B6-E0FA-46BA-BA03-C7971745F577}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PackageTester.PCL", "PackageTesters\PackageTester.PCL\PackageTester.PCL.csproj", "{7018B9E0-AD5B-42E4-AD35-59F324ED8E56}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{B6BF9238-4541-4E1F-955E-C95F1C2A1F46}" + ProjectSection(SolutionItems) = preProject + appveyor.yml = appveyor.yml + Build\build.cmd = Build\build.cmd + Build\test.cmd = Build\test.cmd + EndProjectSection EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution - PackageTesters\PackageTester.Shared\PackageTester.Shared.projitems*{7018b9e0-ad5b-42e4-ad35-59f324ed8e56}*SharedItemsImports = 4 PackageTesters\PackageTester.Shared\PackageTester.Shared.projitems*{84fb572a-8b77-4b09-b825-2a240bce1b7a}*SharedItemsImports = 4 + PackageTesters\PackageTester.Shared\PackageTester.Shared.projitems*{aa8792b6-e0fa-46ba-ba03-c7971745f577}*SharedItemsImports = 4 PackageTesters\PackageTester.Shared\PackageTester.Shared.projitems*{d4717aa7-5549-4bad-81c5-406844a12990}*SharedItemsImports = 13 EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -74,10 +74,6 @@ Global {AA8792B6-E0FA-46BA-BA03-C7971745F577}.Debug|Any CPU.Build.0 = Debug|Any CPU {AA8792B6-E0FA-46BA-BA03-C7971745F577}.Release|Any CPU.ActiveCfg = Release|Any CPU {AA8792B6-E0FA-46BA-BA03-C7971745F577}.Release|Any CPU.Build.0 = Release|Any CPU - {7018B9E0-AD5B-42E4-AD35-59F324ED8E56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7018B9E0-AD5B-42E4-AD35-59F324ED8E56}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7018B9E0-AD5B-42E4-AD35-59F324ED8E56}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7018B9E0-AD5B-42E4-AD35-59F324ED8E56}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -91,6 +87,8 @@ Global {84FB572A-8B77-4B09-B825-2A240BCE1B7A} = {9A136878-A43E-4154-9B5E-EDAF27E8628D} {0231607B-9CA3-4277-9F19-9925694D22E0} = {9A136878-A43E-4154-9B5E-EDAF27E8628D} {AA8792B6-E0FA-46BA-BA03-C7971745F577} = {9A136878-A43E-4154-9B5E-EDAF27E8628D} - {7018B9E0-AD5B-42E4-AD35-59F324ED8E56} = {9A136878-A43E-4154-9B5E-EDAF27E8628D} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {61289482-AC5A-44E1-AEA1-76A3F3CCB6A4} EndGlobalSection EndGlobal diff --git a/PackageTesters/PackageTester.NET45/PackageTester.NET45.csproj b/PackageTesters/PackageTester.NET45/PackageTester.NET45.csproj index 3a0ffa0d..7f8c90ff 100644 --- a/PackageTesters/PackageTester.NET45/PackageTester.NET45.csproj +++ b/PackageTesters/PackageTester.NET45/PackageTester.NET45.csproj @@ -31,11 +31,11 @@ 4 - - ..\..\packages\Flurl.2.4.0-pre\lib\net40\Flurl.dll + + ..\..\packages\Flurl.2.5.0\lib\net40\Flurl.dll - - ..\..\packages\Flurl.Http.1.2.0-pre\lib\net45\Flurl.Http.dll + + ..\..\packages\Flurl.Http.2.0.0-pre1\lib\net45\Flurl.Http.dll ..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll @@ -59,11 +59,6 @@ - - - {7018b9e0-ad5b-42e4-ad35-59f324ed8e56} - PackageTester.PCL - - + \ No newline at end of file diff --git a/PackageTesters/PackageTester.NET45/Program.cs b/PackageTesters/PackageTester.NET45/Program.cs index 8b7344f2..abd09a22 100644 --- a/PackageTesters/PackageTester.NET45/Program.cs +++ b/PackageTesters/PackageTester.NET45/Program.cs @@ -1,5 +1,4 @@ using System; -using PackageTester.PCL; namespace PackageTester.NET45 { @@ -7,9 +6,6 @@ public class Program { public static void Main(string[] args) { new Tester().DoTestsAsync().Wait(); - Console.WriteLine(); - Console.WriteLine("Testing against PCL..."); - new PclTester().DoTestsAsync().Wait(); Console.ReadLine(); } } diff --git a/PackageTesters/PackageTester.NET45/packages.config b/PackageTesters/PackageTester.NET45/packages.config index 39ae0a02..cebbe1f6 100644 --- a/PackageTesters/PackageTester.NET45/packages.config +++ b/PackageTesters/PackageTester.NET45/packages.config @@ -1,6 +1,6 @@  - - + + \ No newline at end of file diff --git a/PackageTesters/PackageTester.NET461/PackageTester.NET461.csproj b/PackageTesters/PackageTester.NET461/PackageTester.NET461.csproj index 5be0bfa8..e7a57446 100644 --- a/PackageTesters/PackageTester.NET461/PackageTester.NET461.csproj +++ b/PackageTesters/PackageTester.NET461/PackageTester.NET461.csproj @@ -33,11 +33,11 @@ 4 - - ..\..\packages\Flurl.2.4.0-pre\lib\net40\Flurl.dll + + ..\..\packages\Flurl.2.5.0\lib\net40\Flurl.dll - - ..\..\packages\Flurl.Http.1.2.0-pre\lib\net45\Flurl.Http.dll + + ..\..\packages\Flurl.Http.2.0.0-pre1\lib\net45\Flurl.Http.dll ..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll diff --git a/PackageTesters/PackageTester.NET461/packages.config b/PackageTesters/PackageTester.NET461/packages.config index feb04626..6d7e87b9 100644 --- a/PackageTesters/PackageTester.NET461/packages.config +++ b/PackageTesters/PackageTester.NET461/packages.config @@ -1,6 +1,6 @@  - - + + \ No newline at end of file diff --git a/PackageTesters/PackageTester.NETCore/PackageTester.NETCore.csproj b/PackageTesters/PackageTester.NETCore/PackageTester.NETCore.csproj index fdeb0265..ca76ca1e 100644 --- a/PackageTesters/PackageTester.NETCore/PackageTester.NETCore.csproj +++ b/PackageTesters/PackageTester.NETCore/PackageTester.NETCore.csproj @@ -8,7 +8,7 @@ - + \ No newline at end of file diff --git a/PackageTesters/PackageTester.PCL/PackageTester.PCL.csproj b/PackageTesters/PackageTester.PCL/PackageTester.PCL.csproj deleted file mode 100644 index 2117f55e..00000000 --- a/PackageTesters/PackageTester.PCL/PackageTester.PCL.csproj +++ /dev/null @@ -1,65 +0,0 @@ - - - - - Debug - AnyCPU - {7018B9E0-AD5B-42E4-AD35-59F324ED8E56} - Library - Properties - PackageTester.PCL - PackageTester.PCL - v4.5 - 512 - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - false - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - false - - - - ..\..\packages\Flurl.2.4.0-pre\lib\net40\Flurl.dll - - - ..\..\packages\Flurl.Http.1.2.0-pre\lib\net45\Flurl.Http.dll - - - ..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/PackageTesters/PackageTester.PCL/PclTester.cs b/PackageTesters/PackageTester.PCL/PclTester.cs deleted file mode 100644 index 8d792efc..00000000 --- a/PackageTesters/PackageTester.PCL/PclTester.cs +++ /dev/null @@ -1,4 +0,0 @@ -namespace PackageTester.PCL -{ - public class PclTester : Tester { } -} \ No newline at end of file diff --git a/PackageTesters/PackageTester.PCL/packages.config b/PackageTesters/PackageTester.PCL/packages.config deleted file mode 100644 index 39ae0a02..00000000 --- a/PackageTesters/PackageTester.PCL/packages.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Test/Flurl.Test/CommonExtensionsTests.cs b/Test/Flurl.Test/CommonExtensionsTests.cs index 0f54d3a3..9621761b 100644 --- a/Test/Flurl.Test/CommonExtensionsTests.cs +++ b/Test/Flurl.Test/CommonExtensionsTests.cs @@ -6,7 +6,7 @@ namespace Flurl.Test { - [TestFixture] + [TestFixture, Parallelizable] public class CommonExtensionsTests { [Test] diff --git a/Test/Flurl.Test/Flurl.Test.csproj b/Test/Flurl.Test/Flurl.Test.csproj index fbcfaced..b35ff87a 100644 --- a/Test/Flurl.Test/Flurl.Test.csproj +++ b/Test/Flurl.Test/Flurl.Test.csproj @@ -7,7 +7,7 @@ - + diff --git a/Test/Flurl.Test/Http/ClientConfigTests.cs b/Test/Flurl.Test/Http/ClientConfigTests.cs deleted file mode 100644 index 8fdb3906..00000000 --- a/Test/Flurl.Test/Http/ClientConfigTests.cs +++ /dev/null @@ -1,222 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Flurl.Http; -using Flurl.Http.Configuration; -using Flurl.Http.Testing; -using NUnit.Framework; - -namespace Flurl.Test.Http -{ - [TestFixture] - public class ClientConfigTestsBase : ConfigTestsBase - { - protected override FlurlHttpSettings GetSettings() - { - return GetClient().Settings; - } - - [Test] - public void can_set_timeout() - { - var client = "http://www.api.com".WithTimeout(TimeSpan.FromSeconds(15)); - Assert.AreEqual(client.HttpClient.Timeout, TimeSpan.FromSeconds(15)); - } - - [Test] - public void can_set_timeout_in_seconds() - { - var client = "http://www.api.com".WithTimeout(15); - Assert.AreEqual(client.HttpClient.Timeout, TimeSpan.FromSeconds(15)); - } - - [Test] - public void can_set_header() - { - var client = "http://www.api.com".WithHeader("a", 1); - - var headers = client.HttpClient.DefaultRequestHeaders.ToList(); - Assert.AreEqual(1, headers.Count); - Assert.AreEqual("a", headers[0].Key); - CollectionAssert.AreEqual(headers[0].Value, new[] { "1" }); - } - - [Test] - public void can_set_headers_from_anon_object() - { - var client = "http://www.api.com".WithHeaders(new { a = "b", one = 2 }); - - var headers = client.HttpClient.DefaultRequestHeaders.ToList(); - Assert.AreEqual(2, headers.Count); - Assert.AreEqual("a", headers[0].Key); - CollectionAssert.AreEqual(headers[0].Value, new[] { "b" }); - Assert.AreEqual("one", headers[1].Key); - CollectionAssert.AreEqual(headers[1].Value, new[] { "2" }); - } - - [Test] - public void can_set_headers_from_dictionary() - { - var client = "http://www.api.com".WithHeaders(new Dictionary { { "a", "b" }, { "one", 2 } }); - - var headers = client.HttpClient.DefaultRequestHeaders.ToList(); - Assert.AreEqual(2, headers.Count); - Assert.AreEqual("a", headers[0].Key); - CollectionAssert.AreEqual(headers[0].Value, new[] { "b" }); - Assert.AreEqual("one", headers[1].Key); - CollectionAssert.AreEqual(headers[1].Value, new[] { "2" }); - } - - [Test] - public void can_setup_basic_auth() - { - var client = "http://www.api.com".WithBasicAuth("user", "pass"); - - var header = client.HttpClient.DefaultRequestHeaders.First(); - Assert.AreEqual("Authorization", header.Key); - Assert.AreEqual("Basic dXNlcjpwYXNz", header.Value.First()); - } - - [Test] - public void can_setup_oauth_bearer_token() - { - var client = "http://www.api.com".WithOAuthBearerToken("mytoken"); - - var header = client.HttpClient.DefaultRequestHeaders.First(); - Assert.AreEqual("Authorization", header.Key); - Assert.AreEqual("Bearer mytoken", header.Value.First()); - } - - [Test] - public async Task can_allow_specific_http_status() - { - using (var test = new HttpTest()) - { - test.RespondWith("Nothing to see here", 404); - // no exception = pass - await "http://www.api.com" - .AllowHttpStatus(HttpStatusCode.Conflict, HttpStatusCode.NotFound) - .DeleteAsync(); - } - } - - [Test] - public void can_clear_non_success_status() - { - using (var test = new HttpTest()) - { - test.RespondWith("I'm a teapot", 418); - // allow 4xx - var client = "http://www.api.com".AllowHttpStatus("4xx"); - // but then disallow it - client.Settings.AllowedHttpStatusRange = null; - Assert.ThrowsAsync(async () => await client.GetAsync()); - } - } - - [Test] - public async Task can_allow_any_http_status() - { - using (var test = new HttpTest()) - { - test.RespondWith("epic fail", 500); - try - { - var result = await "http://www.api.com".AllowAnyHttpStatus().GetAsync(); - Assert.IsFalse(result.IsSuccessStatusCode); - } - catch (Exception) - { - Assert.Fail("Exception should not have been thrown."); - } - } - } - - [Test] - public void can_override_settings_fluently() - { - using (var test = new HttpTest()) - { - FlurlHttp.GlobalSettings.AllowedHttpStatusRange = "*"; - test.RespondWith("epic fail", 500); - Assert.ThrowsAsync(async () => await "http://www.api.com".ConfigureClient(c => c.AllowedHttpStatusRange = "2xx").GetAsync()); - } - } - - [Test] - public void WithUrl_shares_HttpClient_but_not_Url() - { - var client1 = new FlurlClient("http://www.api.com/for-client1").WithCookie("mycookie", "123"); - var client2 = client1.WithUrl("http://www.api.com/for-client2"); - var client3 = client1.WithUrl("http://www.api.com/for-client3"); - var client4 = client2.WithUrl("http://www.api.com/for-client4"); - - CollectionAssert.AreEquivalent(client1.Cookies, client2.Cookies); - CollectionAssert.AreEquivalent(client1.Cookies, client3.Cookies); - CollectionAssert.AreEquivalent(client1.Cookies, client4.Cookies); - var urls = new[] { client1, client2, client3, client4 }.Select(c => c.Url.ToString()); - CollectionAssert.AllItemsAreUnique(urls); - } - - [Test] - public void WithUrl_doesnt_propagate_HttpClient_disposal() - { - var client1 = new FlurlClient("http://www.api.com/for-client1").WithCookie("mycookie", "123"); - var client2 = client1.WithUrl("http://www.api.com/for-client2"); - var client3 = client1.WithUrl("http://www.api.com/for-client3"); - var client4 = client2.WithUrl("http://www.api.com/for-client4"); - - client2.Dispose(); - client3.Dispose(); - - CollectionAssert.IsEmpty(client2.Cookies); - CollectionAssert.IsEmpty(client3.Cookies); - - CollectionAssert.IsNotEmpty(client1.Cookies); - CollectionAssert.IsNotEmpty(client4.Cookies); - } - - [Test] - public void WithClient_shares_HttpClient_but_not_Url() - { - var client1 = new FlurlClient("http://www.api.com/for-client1").WithCookie("mycookie", "123"); - var client2 = "http://www.api.com/for-client2".WithClient(client1); - var client3 = "http://www.api.com/for-client3".WithClient(client1); - var client4 = "http://www.api.com/for-client4".WithClient(client1); - - CollectionAssert.AreEquivalent(client1.Cookies, client2.Cookies); - CollectionAssert.AreEquivalent(client1.Cookies, client3.Cookies); - CollectionAssert.AreEquivalent(client1.Cookies, client4.Cookies); - var urls = new[] { client1, client2, client3, client4 }.Select(c => c.Url.ToString()); - CollectionAssert.AllItemsAreUnique(urls); - } - - [Test] - public void WithClient_doesnt_propagate_HttpClient_disposal() - { - var client1 = new FlurlClient("http://www.api.com/for-client1").WithCookie("mycookie", "123"); - var client2 = "http://www.api.com/for-client2".WithClient(client1); - var client3 = "http://www.api.com/for-client3".WithClient(client1); - var client4 = "http://www.api.com/for-client4".WithClient(client1); - - client2.Dispose(); - client3.Dispose(); - - CollectionAssert.IsEmpty(client2.Cookies); - CollectionAssert.IsEmpty(client3.Cookies); - - CollectionAssert.IsNotEmpty(client1.Cookies); - CollectionAssert.IsNotEmpty(client4.Cookies); - } - - [Test] - public void can_use_uri_with_WithUrl() - { - var uri = new System.Uri("http://www.mysite.com/foo?x=1"); - var fc = new FlurlClient().WithUrl(uri); - Assert.AreEqual(uri.ToString(), fc.Url.ToString()); - } - } -} \ No newline at end of file diff --git a/Test/Flurl.Test/Http/ClientLifetimeTests.cs b/Test/Flurl.Test/Http/ClientLifetimeTests.cs deleted file mode 100644 index cf9ff78b..00000000 --- a/Test/Flurl.Test/Http/ClientLifetimeTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Threading.Tasks; -using Flurl.Http; -using Flurl.Http.Testing; -using NUnit.Framework; - -namespace Flurl.Test.Http -{ - [TestFixture, Parallelizable] - public class ClientLifetimeTests - { - [Test] - public async Task autodispose_true_creates_new_httpclients() { - var fac = new TestHttpClientFactoryWithCounter(); - var fc = new FlurlClient("http://www.mysite.com") { - Settings = { HttpClientFactory = fac, AutoDispose = true } - }; - var x = await fc.GetAsync(); - var y = await fc.GetAsync(); - var z = await fc.GetAsync(); - Assert.AreEqual(3, fac.NewClientCount); - } - - [Test] - public async Task autodispose_false_reuses_httpclient() { - var fac = new TestHttpClientFactoryWithCounter(); - var fc = new FlurlClient("http://www.mysite.com") { - Settings = { HttpClientFactory = fac, AutoDispose = false } - }; - var x = await fc.GetAsync(); - var y = await fc.GetAsync(); - var z = await fc.GetAsync(); - Assert.AreEqual(1, fac.NewClientCount); - } - - private class TestHttpClientFactoryWithCounter : TestHttpClientFactory - { - public int NewClientCount { get; set; } - - public override HttpClient CreateClient(Url url, HttpMessageHandler handler) { - NewClientCount++; - return base.CreateClient(url, handler); - } - } - } -} diff --git a/Test/Flurl.Test/Http/FlurlClientTests.cs b/Test/Flurl.Test/Http/FlurlClientTests.cs index 5c342882..85f8142f 100644 --- a/Test/Flurl.Test/Http/FlurlClientTests.cs +++ b/Test/Flurl.Test/Http/FlurlClientTests.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Reflection; using Flurl.Http; using NUnit.Framework; @@ -11,24 +12,84 @@ public class FlurlClientTests [Test] // check that for every FlurlClient extension method, we have an equivalent Url and string extension public void extension_methods_consistently_supported() { - var fcExts = ReflectionHelper.GetAllExtensionMethods(typeof(FlurlClient).GetTypeInfo().Assembly); - var urlExts = ReflectionHelper.GetAllExtensionMethods(typeof(FlurlClient).GetTypeInfo().Assembly); - var stringExts = ReflectionHelper.GetAllExtensionMethods(typeof(FlurlClient).GetTypeInfo().Assembly); - var whitelist = new[] { "WithUrl" }; // cases where Url method of the same name was excluded intentionally + var frExts = ReflectionHelper.GetAllExtensionMethods(typeof(FlurlClient).GetTypeInfo().Assembly) + // URL builder methods on IFlurlClient get a free pass. We're looking for things like HTTP calling methods. + .Where(mi => mi.DeclaringType != typeof(UrlBuilderExtensions)) + .ToList(); + var urlExts = ReflectionHelper.GetAllExtensionMethods(typeof(FlurlClient).GetTypeInfo().Assembly).ToList(); + var stringExts = ReflectionHelper.GetAllExtensionMethods(typeof(FlurlClient).GetTypeInfo().Assembly).ToList(); - foreach (var method in fcExts) { - if (whitelist.Contains(method.Name)) - continue; + Assert.That(frExts.Count > 20, $"IFlurlRequest only has {frExts.Count} extension methods? Something's wrong here."); + // Url and string should contain all extension methods that IFlurlRequest has + foreach (var method in frExts) { if (!urlExts.Any(m => ReflectionHelper.AreSameMethodSignatures(method, m))) { var args = string.Join(", ", method.GetParameters().Select(p => p.ParameterType.Name)); - Assert.Fail($"No equivalent Url extension method found for FlurlClient.{method.Name}({args})"); + Assert.Fail($"No equivalent Url extension method found for IFlurlRequest.{method.Name}({args})"); } if (!stringExts.Any(m => ReflectionHelper.AreSameMethodSignatures(method, m))) { var args = string.Join(", ", method.GetParameters().Select(p => p.ParameterType.Name)); - Assert.Fail($"No equivalent string extension method found for FlurlClient.{method.Name}({args})"); + Assert.Fail($"No equivalent string extension method found for IFlurlRequest.{method.Name}({args})"); } } } + + [Test] + public void can_create_request_without_base_url() { + var cli = new FlurlClient(); + var req = cli.Request("http://myapi.com/foo?x=1&y=2#foo"); + Assert.AreEqual("http://myapi.com/foo?x=1&y=2#foo", req.Url.ToString()); + } + + [Test] + public void can_create_request_with_base_url() { + var cli = new FlurlClient("http://myapi.com"); + var req = cli.Request("foo", "bar"); + Assert.AreEqual("http://myapi.com/foo/bar", req.Url.ToString()); + } + + [Test] + public void request_with_full_url_overrides_base_url() { + var cli = new FlurlClient("http://myapi.com"); + var req = cli.Request("http://otherapi.com", "foo"); + Assert.AreEqual("http://otherapi.com/foo", req.Url.ToString()); + } + + [Test] + public void can_create_request_with_base_url_and_no_segments() { + var cli = new FlurlClient("http://myapi.com"); + var req = cli.Request(); + Assert.AreEqual("http://myapi.com", req.Url.ToString()); + } + + [Test] + public void cannot_create_request_without_base_url_or_segments() { + var cli = new FlurlClient(); + Assert.Throws(() => { + var req = cli.Request(); + }); + } + + [Test] + public void cannot_create_request_without_base_url_or_segments_comprising_full_url() { + var cli = new FlurlClient(); + Assert.Throws(() => { + var req = cli.Request("foo", "bar"); + }); + } + + [Test] + public void default_factory_doesnt_reuse_disposed_clients() { + var cli1 = "http://api.com".WithHeader("foo", "1").Client; + var cli2 = "http://api.com".WithHeader("foo", "2").Client; + cli1.Dispose(); + var cli3 = "http://api.com".WithHeader("foo", "3").Client; + + Assert.AreEqual(cli1, cli2); + Assert.IsTrue(cli1.IsDisposed); + Assert.IsTrue(cli2.IsDisposed); + Assert.AreNotEqual(cli1, cli3); + Assert.IsFalse(cli3.IsDisposed); + } } } \ No newline at end of file diff --git a/Test/Flurl.Test/Http/FlurlHttpExceptionTests.cs b/Test/Flurl.Test/Http/FlurlHttpExceptionTests.cs new file mode 100644 index 00000000..efab246b --- /dev/null +++ b/Test/Flurl.Test/Http/FlurlHttpExceptionTests.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Flurl.Http; +using NUnit.Framework; + +namespace Flurl.Test.Http +{ + [TestFixture, Parallelizable] + class FlurlHttpExceptionTests : HttpTestFixtureBase + { + [Test] + public async Task exception_message_is_nice() { + HttpTest.RespondWithJson(new { message = "bad data!" }, 400); + + try { + await "http://myapi.com".PostJsonAsync(new { data = "bad" }); + Assert.Fail("should have thrown 400."); + } + catch (FlurlHttpException ex) { + Assert.AreEqual("POST http://myapi.com failed with status code 400 (Bad Request).\r\nRequest body:\r\n{\"data\":\"bad\"}\r\nResponse body:\r\n{\"message\":\"bad data!\"}", ex.Message); + } + } + + [Test] + public async Task exception_message_excludes_request_response_labels_when_body_empty() { + HttpTest.RespondWith("", 400); + + try { + await "http://myapi.com".GetAsync(); + Assert.Fail("should have thrown 400."); + } + catch (FlurlHttpException ex) { + // no "Request body:", "Response body:", or line breaks + Assert.AreEqual("GET http://myapi.com failed with status code 400 (Bad Request).", ex.Message); + } + } + } +} diff --git a/Test/Flurl.Test/Http/GetTests.cs b/Test/Flurl.Test/Http/GetTests.cs index 1ae6e6f3..6d6fd23e 100644 --- a/Test/Flurl.Test/Http/GetTests.cs +++ b/Test/Flurl.Test/Http/GetTests.cs @@ -9,7 +9,7 @@ namespace Flurl.Test.Http { - [TestFixture] + [TestFixture, Parallelizable] public class GetTests : HttpTestFixtureBase { [Test] @@ -132,7 +132,7 @@ public async Task can_get_error_json_untyped() { public async Task can_get_null_json_when_timeout_and_exception_handled() { HttpTest.SimulateTimeout(); var data = await "http://api.com" - .ConfigureClient(c => c.OnError = call => call.ExceptionHandled = true) + .ConfigureRequest(c => c.OnError = call => call.ExceptionHandled = true) .GetJsonAsync(); Assert.IsNull(data); } diff --git a/Test/Flurl.Test/Http/GlobalConfigTests.cs b/Test/Flurl.Test/Http/GlobalConfigTests.cs deleted file mode 100644 index 182009f1..00000000 --- a/Test/Flurl.Test/Http/GlobalConfigTests.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.Net.Http; -using System.Threading.Tasks; -using Flurl.Http; -using Flurl.Http.Configuration; -using Flurl.Http.Testing; -using NUnit.Framework; - -namespace Flurl.Test.Http -{ - /// - /// All global settings can also be set at the client level, so this base class allows ClientConfigTests to - /// inherit all the same tests. - /// - [Parallelizable] - public abstract class ConfigTestsBase - { - protected abstract FlurlHttpSettings GetSettings(); - - private FlurlClient _client; - protected FlurlClient GetClient() { - if (_client == null) - _client = new FlurlClient("http://www.api.com"); - return _client; - } - - [TearDown] - public void ResetDefaults() { - GetSettings().ResetDefaults(); - _client = null; - } - - [Test] - public void can_provide_custom_httpclient_factory() { - GetSettings().HttpClientFactory = new SomeCustomHttpClientFactory(); - Assert.IsInstanceOf(GetClient().HttpClient); - Assert.IsInstanceOf(GetClient().HttpMessageHandler); - } - - [Test] - public async Task can_allow_non_success_status() { - using (var test = new HttpTest()) { - GetSettings().AllowedHttpStatusRange = "4xx"; - test.RespondWith("I'm a teapot", 418); - try { - var result = await GetClient().GetAsync(); - Assert.IsFalse(result.IsSuccessStatusCode); - } - catch (Exception) { - Assert.Fail("Exception should not have been thrown."); - } - } - } - - [Test] - public async Task can_set_pre_callback() { - var callbackCalled = false; - using (var test = new HttpTest()) { - test.RespondWith("ok"); - GetSettings().BeforeCall = req => { - CollectionAssert.IsNotEmpty(test.ResponseQueue); // verifies that callback is running before HTTP call is made - callbackCalled = true; - }; - Assert.IsFalse(callbackCalled); - await GetClient().GetAsync(); - Assert.IsTrue(callbackCalled); - } - } - - [Test] - public async Task can_set_post_callback() { - var callbackCalled = false; - using (var test = new HttpTest()) { - test.RespondWith("ok"); - GetSettings().AfterCall = call => { - CollectionAssert.IsEmpty(test.ResponseQueue); // verifies that callback is running after HTTP call is made - callbackCalled = true; - }; - Assert.IsFalse(callbackCalled); - await GetClient().GetAsync(); - Assert.IsTrue(callbackCalled); - } - } - - [TestCase(true)] - [TestCase(false)] - public async Task can_set_error_callback(bool markExceptionHandled) { - var callbackCalled = false; - using (var test = new HttpTest()) { - test.RespondWith("server error", 500); - GetSettings().OnError = call => { - CollectionAssert.IsEmpty(test.ResponseQueue); // verifies that callback is running after HTTP call is made - callbackCalled = true; - call.ExceptionHandled = markExceptionHandled; - }; - Assert.IsFalse(callbackCalled); - try { - await GetClient().GetAsync(); - Assert.IsTrue(callbackCalled, "OnError was never called"); - Assert.IsTrue(markExceptionHandled, "ExceptionHandled was marked false in callback, but exception was not propagated."); - } - catch (FlurlHttpException) { - Assert.IsTrue(callbackCalled, "OnError was never called"); - Assert.IsFalse(markExceptionHandled, "ExceptionHandled was marked true in callback, but exception was propagated."); - } - } - } - - [Test] - public async Task can_disable_exception_behavior() { - using (var test = new HttpTest()) { - GetSettings().OnError = call => { - call.ExceptionHandled = true; - }; - test.RespondWith("server error", 500); - try { - var result = await GetClient().GetAsync(); - Assert.IsFalse(result.IsSuccessStatusCode); - } - catch (FlurlHttpException) { - Assert.Fail("Flurl should not have thrown exception."); - } - } - } - - private class SomeCustomHttpClientFactory : IHttpClientFactory - { - public HttpClient CreateClient(Url url, HttpMessageHandler handler) { - return new SomeCustomHttpClient(); - } - - public HttpMessageHandler CreateMessageHandler() { - return new SomeCustomMessageHandler(); - } - } - - private class SomeCustomHttpClient : HttpClient { } - private class SomeCustomMessageHandler : HttpClientHandler { } - } - - [TestFixture] - public class GlobalConfigTestsBase : ConfigTestsBase - { - protected override FlurlHttpSettings GetSettings() { - return FlurlHttp.GlobalSettings; - } - } -} diff --git a/Test/Flurl.Test/Http/HeadTests.cs b/Test/Flurl.Test/Http/HeadTests.cs index 851ac1a5..3189875f 100644 --- a/Test/Flurl.Test/Http/HeadTests.cs +++ b/Test/Flurl.Test/Http/HeadTests.cs @@ -6,7 +6,7 @@ namespace Flurl.Test.Http { - [TestFixture] + [TestFixture, Parallelizable] public class HeadTests : HttpTestFixtureBase { [Test] diff --git a/Test/Flurl.Test/Http/HttpTestFixtureBase.cs b/Test/Flurl.Test/Http/HttpTestFixtureBase.cs index ce03aa65..5cadba8a 100644 --- a/Test/Flurl.Test/Http/HttpTestFixtureBase.cs +++ b/Test/Flurl.Test/Http/HttpTestFixtureBase.cs @@ -6,7 +6,6 @@ namespace Flurl.Test.Http { - [Parallelizable] public abstract class HttpTestFixtureBase { protected HttpTest HttpTest { get; private set; } diff --git a/Test/Flurl.Test/Http/PostTests.cs b/Test/Flurl.Test/Http/PostTests.cs index 60c9c4a6..cba272f6 100644 --- a/Test/Flurl.Test/Http/PostTests.cs +++ b/Test/Flurl.Test/Http/PostTests.cs @@ -5,7 +5,7 @@ namespace Flurl.Test.Http { - [TestFixture] + [TestFixture, Parallelizable] public class PostTests : HttpTestFixtureBase { [Test] @@ -33,11 +33,11 @@ public async Task can_post_object_as_json() { [Test] public async Task can_post_url_encoded() { - await "http://some-api.com".PostUrlEncodedAsync(new { a = 1, b = 2, c = "hi there" }); + await "http://some-api.com".PostUrlEncodedAsync(new { a = 1, b = 2, c = "hi there", d = new[] { 1, 2, 3 } }); HttpTest.ShouldHaveCalled("http://some-api.com") .WithVerb(HttpMethod.Post) .WithContentType("application/x-www-form-urlencoded") - .WithRequestBody("a=1&b=2&c=hi+there") + .WithRequestBody("a=1&b=2&c=hi+there&d=1&d=2&d=3") .Times(1); } diff --git a/Test/Flurl.Test/Http/RealHttpTests.cs b/Test/Flurl.Test/Http/RealHttpTests.cs index 762af15d..fbb1401a 100644 --- a/Test/Flurl.Test/Http/RealHttpTests.cs +++ b/Test/Flurl.Test/Http/RealHttpTests.cs @@ -1,18 +1,18 @@ using System; using System.Collections.Generic; using System.IO; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Flurl.Http; +using Flurl.Http.Configuration; using Flurl.Http.Testing; using NUnit.Framework; namespace Flurl.Test.Http { /// - /// Most HTTP tests in this project are with Flurl in fake mode. These are some real ones, mostly using the handy site - /// http://httpbin.org. One important aspect these verify is that AutoDispose behavior is not preventing us from getting - /// stuff out of the response (i.e. that we're not disposing too early). + /// Most HTTP tests in this project are with Flurl in fake mode. These are some real ones, mostly using http://httpbin.org. /// [TestFixture, Parallelizable] public class RealHttpTests @@ -20,7 +20,7 @@ public class RealHttpTests [Test] public async Task can_download_file() { var folder = "c:\\flurl-test-" + Guid.NewGuid(); // random so parallel tests don't trip over each other - var path = await "http://www.google.com".DownloadFileAsync(folder, "google.txt"); + var path = await "https://www.google.com".DownloadFileAsync(folder, "google.txt"); Assert.AreEqual($@"{folder}\google.txt", path); Assert.That(File.Exists(path)); File.Delete(path); @@ -29,7 +29,8 @@ public async Task can_download_file() { [Test] public async Task can_set_request_cookies() { - var resp = await "http://httpbin.org/cookies".WithCookies(new { x = 1, y = 2 }).GetJsonAsync(); + var cli = new FlurlClient(); + var resp = await cli.Request("https://httpbin.org/cookies").WithCookies(new { x = 1, y = 2 }).GetJsonAsync(); // httpbin returns json representation of cookies that were set on the server. Assert.AreEqual("1", resp.cookies.x); @@ -38,62 +39,46 @@ public async Task can_set_request_cookies() { [Test] public async Task can_set_cookies_before_setting_url() { - var fc = new FlurlClient().WithCookie("z", "999"); - var resp = await fc.WithUrl("http://httpbin.org/cookies").GetJsonAsync(); + var cli = new FlurlClient().WithCookie("z", "999"); + var resp = await cli.Request("https://httpbin.org/cookies").GetJsonAsync(); Assert.AreEqual("999", resp.cookies.z); } [Test] public async Task can_get_response_cookies() { - var fc = new FlurlClient().EnableCookies(); - await fc.WithUrl("https://httpbin.org/cookies/set?z=999").HeadAsync(); - Assert.AreEqual("999", fc.Cookies["z"].Value); + var cli = new FlurlClient().EnableCookies(); + await cli.Request("https://httpbin.org/cookies/set?z=999").HeadAsync(); + Assert.AreEqual("999", cli.Cookies["z"].Value); } [Test] - public async Task cant_persist_cookies_without_resuing_client() { - var fc = "http://httpbin.org/cookies".WithCookie("z", 999); + public async Task can_persist_cookies() { + var cli = new FlurlClient("https://httpbin.org/cookies"); + var req = cli.Request().WithCookie("z", 999); // cookie should be set - Assert.AreEqual("999", fc.Cookies["z"].Value); + Assert.AreEqual("999", cli.Cookies["z"].Value); + Assert.AreEqual("999", req.Cookies["z"].Value); - await fc.HeadAsync(); - // FlurlClient was auto-disposed, so cookie should be gone - CollectionAssert.IsEmpty(fc.Cookies); + await req.HeadAsync(); + // FlurlClient should be re-used, so cookie should stick + Assert.AreEqual("999", cli.Cookies["z"].Value); + Assert.AreEqual("999", req.Cookies["z"].Value); // httpbin returns json representation of cookies that were set on the server. - var resp = await "http://httpbin.org/cookies".GetJsonAsync(); - Assert.IsFalse((resp.cookies as IDictionary).ContainsKey("z")); - } - - [Test] - public async Task can_persist_cookies() { - using (var fc = new FlurlClient()) { - var fc2 = "http://httpbin.org/cookies".WithClient(fc).WithCookie("z", 999); - // cookie should be set - Assert.AreEqual("999", fc.Cookies["z"].Value); - Assert.AreEqual("999", fc2.Cookies["z"].Value); - - await fc2.HeadAsync(); - // FlurlClient should be re-used, so cookie should stick - Assert.AreEqual("999", fc.Cookies["z"].Value); - Assert.AreEqual("999", fc2.Cookies["z"].Value); - - // httpbin returns json representation of cookies that were set on the server. - var resp = await "http://httpbin.org/cookies".WithClient(fc).GetJsonAsync(); - Assert.AreEqual("999", resp.cookies.z); - } + var resp = await cli.Request().GetJsonAsync(); + Assert.AreEqual("999", resp.cookies.z); } [Test] public async Task can_post_and_receive_json() { - var result = await "http://httpbin.org/post".PostJsonAsync(new { a = 1, b = 2 }).ReceiveJson(); + var result = await "https://httpbin.org/post".PostJsonAsync(new { a = 1, b = 2 }).ReceiveJson(); Assert.AreEqual(result.json.a, 1); Assert.AreEqual(result.json.b, 2); } [Test] public async Task can_get_stream() { - using (var stream = await "http://www.google.com".GetStreamAsync()) + using (var stream = await "https://www.google.com".GetStreamAsync()) using (var ms = new MemoryStream()) { stream.CopyTo(ms); Assert.Greater(ms.Length, 0); @@ -102,37 +87,24 @@ public async Task can_get_stream() { [Test] public async Task can_get_string() { - var s = await "http://www.google.com".GetStringAsync(); + var s = await "https://www.google.com".GetStringAsync(); Assert.Greater(s.Length, 0); } [Test] public async Task can_get_byte_array() { - var bytes = await "http://www.google.com".GetBytesAsync(); + var bytes = await "https://www.google.com".GetBytesAsync(); Assert.Greater(bytes.Length, 0); } [Test] public void fails_on_non_success_status() { - Assert.ThrowsAsync(async () => await "http://httpbin.org/status/418".GetAsync()); + Assert.ThrowsAsync(async () => await "https://httpbin.org/status/418".GetAsync()); } [Test] public async Task can_allow_non_success_status() { - await "http://httpbin.org/status/418".AllowHttpStatus("4xx").GetAsync(); - } - - [Test] - public void can_cancel_request() { - var ex = Assert.ThrowsAsync(async () => - { - var cts = new CancellationTokenSource(); - var task = "http://www.google.com".GetStringAsync(cts.Token); - cts.Cancel(); - await task; - }); - - Assert.IsNotNull(ex.InnerException as TaskCanceledException); + await "https://httpbin.org/status/418".AllowHttpStatus("4xx").GetAsync(); } [Test] @@ -148,7 +120,7 @@ public async Task can_post_multipart() { try { using (var stream = File.OpenRead(path2)) { - var resp = await "http://httpbin.org/post" + var resp = await "https://httpbin.org/post" .PostMultipartAsync(content => { content .AddStringParts(new { a = 1, b = 2 }) @@ -177,34 +149,159 @@ public async Task can_post_multipart() { public async Task can_handle_error() { var handlerCalled = false; - FlurlHttp.Configure(c => { - c.OnError = call => { - call.ExceptionHandled = true; - handlerCalled = true; - }; - }); - try { - await "https://httpbin.org/status/500".GetAsync(); + await "https://httpbin.org/status/500".ConfigureRequest(c => { + c.OnError = call => { + call.ExceptionHandled = true; + handlerCalled = true; + }; + }).GetAsync(); Assert.IsTrue(handlerCalled, "error handler shoule have been called."); } catch (FlurlHttpException) { Assert.Fail("exception should have been supressed."); } - finally { - FlurlHttp.Configure(c => c.ResetDefaults()); - } } [Test] public async Task can_comingle_real_and_fake_tests() { // do a fake call while a real call is running var realTask = "https://www.google.com/".GetStringAsync(); - using (new HttpTest()) { + using (var test = new HttpTest()) { + test.RespondWith("fake!"); var fake = await "https://www.google.com/".GetStringAsync(); - Assert.AreEqual("", fake); + Assert.AreEqual("fake!", fake); + } + Assert.AreNotEqual("fake!", await realTask); + } + + [Test] + public void can_set_timeout() { + var ex = Assert.ThrowsAsync(async () => { + await "https://httpbin.org/delay/5" + .WithTimeout(TimeSpan.FromMilliseconds(50)) + .HeadAsync(); + }); + Assert.That(ex.InnerException is TaskCanceledException); + } + + [Test] + public void can_cancel_request() { + var cts = new CancellationTokenSource(); + var ex = Assert.ThrowsAsync(async () => { + var task = "https://httpbin.org/delay/5".GetAsync(cts.Token); + cts.Cancel(); + await task; + }); + Assert.That(ex.InnerException is TaskCanceledException); + } + + [Test] // make sure the 2 tokens in play are playing nicely together + public void can_set_timeout_and_cancellation_token() { + // cancellation with timeout value set + var cts = new CancellationTokenSource(); + var ex = Assert.ThrowsAsync(async () => { + var task = "https://httpbin.org/delay/5" + .WithTimeout(TimeSpan.FromMilliseconds(50)) + .GetAsync(cts.Token); + cts.Cancel(); + await task; + }); + Assert.That(ex.InnerException is TaskCanceledException); + Assert.IsTrue(cts.Token.IsCancellationRequested); + + // timeout with cancellation token set + cts = new CancellationTokenSource(); + ex = Assert.ThrowsAsync(async () => { + await "https://httpbin.org/delay/5" + .WithTimeout(TimeSpan.FromMilliseconds(50)) + .GetAsync(cts.Token); + }); + Assert.That(ex.InnerException is TaskCanceledException); + Assert.IsFalse(cts.Token.IsCancellationRequested); + } + + [Test] + public async Task can_set_request_cookies_with_a_delegating_handler() { + var resp = await new FlurlClient("http://httpbin.org") + .Configure(settings => settings.HttpClientFactory = new DelegatingHandlerHttpClientFactory()) + .Request("cookies") + .WithCookies(new { x = 1, y = 2 }) + .GetJsonAsync(); + + // httpbin returns json representation of cookies that were set on the server. + Assert.AreEqual("1", resp.cookies.x); + Assert.AreEqual("2", resp.cookies.y); + } + + [Test] + public async Task can_get_response_cookies_with_a_delegating_handler() { + var cli = new FlurlClient("https://httpbin.org") + .Configure(settings => settings.HttpClientFactory = new DelegatingHandlerHttpClientFactory()) + .EnableCookies(); + + await cli.Request("cookies/set?z=999").HeadAsync(); + Assert.AreEqual("999", cli.Cookies["z"].Value); + } + + [Test] + public async Task connection_lease_timeout_doesnt_disrupt_calls() { + // Specific behavior associated with ConnectionLeaseTimeout is coverd in SettingsTests. + // Here let's just make sure it isn't disruptive in any way in real calls. + + var cli = new FlurlClient("http://www.google.com"); + cli.Settings.ConnectionLeaseTimeout = TimeSpan.FromMilliseconds(20); + + // initiate a call to google every 10ms for 100ms. + var tasks = new List(); + for (var i = 0; i < 10; i++) { + tasks.Add(cli.Request().GetAsync()); + await Task.Delay(10); + } + await Task.WhenAll(tasks); // failed HTTP status, etc, would throw here and fail the test. + } + + [Test] + public async Task test_settings_override_client_settings() { + var cli1 = new FlurlClient(); + cli1.Settings.HttpClientFactory = new DefaultHttpClientFactory(); + var h = cli1.HttpClient; // force (lazy) instantiation + + using (var test = new HttpTest()) { + test.Settings.CookiesEnabled = false; + + test.RespondWith("foo!"); + var s = await cli1.Request("http://www.google.com") + .EnableCookies() // test says cookies are off, and test should always win + .GetStringAsync(); + Assert.AreEqual("foo!", s); + Assert.IsFalse(cli1.Settings.CookiesEnabled); + + var cli2 = new FlurlClient(); + cli2.Settings.HttpClientFactory = new DefaultHttpClientFactory(); + h = cli2.HttpClient; + + test.RespondWith("foo 2!"); + s = await cli2.Request("http://www.google.com") + .EnableCookies() // test says cookies are off, and test should always win + .GetStringAsync(); + Assert.AreEqual("foo 2!", s); + Assert.IsFalse(cli2.Settings.CookiesEnabled); + } + } + + public class DelegatingHandlerHttpClientFactory : DefaultHttpClientFactory + { + public override HttpMessageHandler CreateMessageHandler() { + var handler = base.CreateMessageHandler(); + + return new PassThroughDelegatingHandler(new PassThroughDelegatingHandler(handler)); + } + + public class PassThroughDelegatingHandler : DelegatingHandler + { + public PassThroughDelegatingHandler(HttpMessageHandler innerHandler) : base(innerHandler) { } } - Assert.AreNotEqual("", await realTask); } } } \ No newline at end of file diff --git a/Test/Flurl.Test/Http/SettingsExtensionsTests.cs b/Test/Flurl.Test/Http/SettingsExtensionsTests.cs new file mode 100644 index 00000000..3b3b70c9 --- /dev/null +++ b/Test/Flurl.Test/Http/SettingsExtensionsTests.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Flurl.Http; +using Flurl.Http.Testing; +using NUnit.Framework; + +namespace Flurl.Test.Http +{ + // IFlurlClient and IFlurlRequest both implement IHttpSettingsContainer, which defines a number + // of settings-related extension methods. This abstract test class allows those methods to be + // tested against both both client-level and request-level implementations. + public abstract class SettingsExtensionsTests where T : IHttpSettingsContainer + { + protected abstract T GetSettingsContainer(); + protected abstract IFlurlRequest GetRequest(T sc); + + [Test] + public void can_set_timeout() { + var sc = GetSettingsContainer().WithTimeout(TimeSpan.FromSeconds(15)); + Assert.AreEqual(TimeSpan.FromSeconds(15), sc.Settings.Timeout); + } + + [Test] + public void can_set_timeout_in_seconds() { + var sc = GetSettingsContainer().WithTimeout(15); + Assert.AreEqual(sc.Settings.Timeout, TimeSpan.FromSeconds(15)); + } + + [Test] + public void can_set_header() { + var sc = GetSettingsContainer().WithHeader("a", 1); + Assert.AreEqual(1, sc.Headers.Count); + Assert.AreEqual(1, sc.Headers["a"]); + } + + [Test] + public void can_set_headers_from_anon_object() { + var sc = GetSettingsContainer().WithHeaders(new { a = "b", one = 2 }); + Assert.AreEqual(2, sc.Headers.Count); + Assert.AreEqual("b", sc.Headers["a"]); + Assert.AreEqual(2, sc.Headers["one"]); + } + + [Test] + public void can_set_headers_from_dictionary() { + var sc = GetSettingsContainer().WithHeaders(new Dictionary { { "a", "b" }, { "one", 2 } }); + Assert.AreEqual(2, sc.Headers.Count); + Assert.AreEqual("b", sc.Headers["a"]); + Assert.AreEqual(2, sc.Headers["one"]); + } + + [Test] + public void underscores_in_properties_convert_to_hyphens_in_header_names() { + var sc = GetSettingsContainer().WithHeaders(new { User_Agent = "Flurl", Cache_Control = "no-cache" }); + Assert.IsTrue(sc.Headers.ContainsKey("User-Agent")); + Assert.IsTrue(sc.Headers.ContainsKey("Cache-Control")); + + // make sure we can disable the behavior + sc.WithHeaders(new { no_i_really_want_underscores = "foo" }, false); + Assert.IsTrue(sc.Headers.ContainsKey("no_i_really_want_underscores")); + + // dictionaries don't get this behavior since you can use hyphens explicitly + sc.WithHeaders(new Dictionary { { "exclude_dictionaries", "bar" } }); + Assert.IsTrue(sc.Headers.ContainsKey("exclude_dictionaries")); + + // same with strings + sc.WithHeaders("exclude_strings=123"); + Assert.IsTrue(sc.Headers.ContainsKey("exclude_strings")); + } + + [Test] + public void can_setup_oauth_bearer_token() { + var sc = GetSettingsContainer().WithOAuthBearerToken("mytoken"); + Assert.AreEqual(1, sc.Headers.Count); + Assert.AreEqual("Bearer mytoken", sc.Headers["Authorization"]); + } + + [Test] + public void can_setup_basic_auth() { + var sc = GetSettingsContainer().WithBasicAuth("user", "pass"); + Assert.AreEqual(1, sc.Headers.Count); + Assert.AreEqual("Basic dXNlcjpwYXNz", sc.Headers["Authorization"]); + } + + [Test] + public async Task can_allow_specific_http_status() { + using (var test = new HttpTest()) { + test.RespondWith("Nothing to see here", 404); + var sc = GetSettingsContainer().AllowHttpStatus(HttpStatusCode.Conflict, HttpStatusCode.NotFound); + await GetRequest(sc).DeleteAsync(); // no exception = pass + } + } + + [Test] + public void can_clear_non_success_status() { + using (var test = new HttpTest()) { + test.RespondWith("I'm a teapot", 418); + // allow 4xx + var sc = GetSettingsContainer().AllowHttpStatus("4xx"); + // but then disallow it + sc.Settings.AllowedHttpStatusRange = null; + Assert.ThrowsAsync(async () => await GetRequest(sc).GetAsync()); + } + } + + [Test] + public async Task can_allow_any_http_status() { + using (var test = new HttpTest()) { + test.RespondWith("epic fail", 500); + try { + var sc = GetSettingsContainer().AllowAnyHttpStatus(); + var result = await GetRequest(sc).GetAsync(); + Assert.IsFalse(result.IsSuccessStatusCode); + } + catch (Exception) { + Assert.Fail("Exception should not have been thrown."); + } + } + } + } + + [TestFixture, Parallelizable] + public class ClientSettingsExtensionsTests : SettingsExtensionsTests + { + protected override IFlurlClient GetSettingsContainer() => new FlurlClient(); + protected override IFlurlRequest GetRequest(IFlurlClient client) => client.Request("http://api.com"); + + [Test] + public void WithUrl_shares_client_but_not_Url() { + var cli = new FlurlClient().WithCookie("mycookie", "123"); + var req1 = cli.Request("http://www.api.com/for-req1"); + var req2 = cli.Request("http://www.api.com/for-req2"); + var req3 = cli.Request("http://www.api.com/for-req3"); + + CollectionAssert.AreEquivalent(req1.Cookies, req2.Cookies); + CollectionAssert.AreEquivalent(req1.Cookies, req3.Cookies); + var urls = new[] { req1, req2, req3 }.Select(c => c.Url.ToString()); + CollectionAssert.AllItemsAreUnique(urls); + } + + [Test] + public void WithClient_shares_client_but_not_Url() { + var cli = new FlurlClient().WithCookie("mycookie", "123"); + var req1 = "http://www.api.com/for-req1".WithClient(cli); + var req2 = "http://www.api.com/for-req2".WithClient(cli); + var req3 = "http://www.api.com/for-req3".WithClient(cli); + + CollectionAssert.AreEquivalent(req1.Cookies, req2.Cookies); + CollectionAssert.AreEquivalent(req1.Cookies, req3.Cookies); + var urls = new[] { req1, req2, req3 }.Select(c => c.Url.ToString()); + CollectionAssert.AllItemsAreUnique(urls); + } + + [Test] + public void can_use_uri_with_WithUrl() { + var uri = new System.Uri("http://www.mysite.com/foo?x=1"); + var req = new FlurlClient().Request(uri); + Assert.AreEqual(uri.ToString(), req.Url.ToString()); + } + + [Test] + public void can_override_settings_fluently() { + using (var test = new HttpTest()) { + var cli = new FlurlClient().Configure(s => s.AllowedHttpStatusRange = "*"); + test.RespondWith("epic fail", 500); + Assert.ThrowsAsync(async () => await "http://www.api.com" + .ConfigureRequest(c => c.AllowedHttpStatusRange = "2xx") + .WithClient(cli) // client-level settings shouldn't win + .GetAsync()); + } + } + } + + [TestFixture, Parallelizable] + public class RequestSettingsExtensionsTests : SettingsExtensionsTests + { + protected override IFlurlRequest GetSettingsContainer() => new FlurlRequest("http://api.com"); + protected override IFlurlRequest GetRequest(IFlurlRequest req) => req; + } +} \ No newline at end of file diff --git a/Test/Flurl.Test/Http/SettingsTests.cs b/Test/Flurl.Test/Http/SettingsTests.cs new file mode 100644 index 00000000..1d283b93 --- /dev/null +++ b/Test/Flurl.Test/Http/SettingsTests.cs @@ -0,0 +1,278 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Flurl.Http; +using Flurl.Http.Configuration; +using Flurl.Http.Testing; +using NUnit.Framework; + +namespace Flurl.Test.Http +{ + /// + /// FlurlHttpSettings are available at the global, test, client, and request level. This abstract class + /// allows the same tests to be run against settings at all 4 levels. + /// + public abstract class SettingsTestsBase + { + protected abstract FlurlHttpSettings GetSettings(); + protected abstract IFlurlRequest GetRequest(); + + [Test] + public async Task can_allow_non_success_status() { + using (var test = new HttpTest()) { + GetSettings().AllowedHttpStatusRange = "4xx"; + test.RespondWith("I'm a teapot", 418); + try { + var result = await GetRequest().GetAsync(); + Assert.IsFalse(result.IsSuccessStatusCode); + } + catch (Exception) { + Assert.Fail("Exception should not have been thrown."); + } + } + } + + [Test] + public async Task can_set_pre_callback() { + var callbackCalled = false; + using (var test = new HttpTest()) { + test.RespondWith("ok"); + GetSettings().BeforeCall = call => { + CollectionAssert.IsNotEmpty(test.ResponseQueue); // verifies that callback is running before HTTP call is made + callbackCalled = true; + }; + Assert.IsFalse(callbackCalled); + await GetRequest().GetAsync(); + Assert.IsTrue(callbackCalled); + } + } + + [Test] + public async Task can_set_post_callback() { + var callbackCalled = false; + using (var test = new HttpTest()) { + test.RespondWith("ok"); + GetSettings().AfterCall = call => { + CollectionAssert.IsEmpty(test.ResponseQueue); // verifies that callback is running after HTTP call is made + callbackCalled = true; + }; + Assert.IsFalse(callbackCalled); + await GetRequest().GetAsync(); + Assert.IsTrue(callbackCalled); + } + } + + [TestCase(true)] + [TestCase(false)] + public async Task can_set_error_callback(bool markExceptionHandled) { + var callbackCalled = false; + using (var test = new HttpTest()) { + test.RespondWith("server error", 500); + GetSettings().OnError = call => { + CollectionAssert.IsEmpty(test.ResponseQueue); // verifies that callback is running after HTTP call is made + callbackCalled = true; + call.ExceptionHandled = markExceptionHandled; + }; + Assert.IsFalse(callbackCalled); + try { + await GetRequest().GetAsync(); + Assert.IsTrue(callbackCalled, "OnError was never called"); + Assert.IsTrue(markExceptionHandled, "ExceptionHandled was marked false in callback, but exception was not propagated."); + } + catch (FlurlHttpException) { + Assert.IsTrue(callbackCalled, "OnError was never called"); + Assert.IsFalse(markExceptionHandled, "ExceptionHandled was marked true in callback, but exception was propagated."); + } + } + } + + [Test] + public async Task can_disable_exception_behavior() { + using (var test = new HttpTest()) { + GetSettings().OnError = call => { + call.ExceptionHandled = true; + }; + test.RespondWith("server error", 500); + try { + var result = await GetRequest().GetAsync(); + Assert.IsFalse(result.IsSuccessStatusCode); + } + catch (FlurlHttpException) { + Assert.Fail("Flurl should not have thrown exception."); + } + } + } + + [Test] + public void can_reset_defaults() { + GetSettings().JsonSerializer = null; + GetSettings().CookiesEnabled = true; + GetSettings().BeforeCall = (call) => Console.WriteLine("Before!"); + + Assert.IsNull(GetSettings().JsonSerializer); + Assert.IsTrue(GetSettings().CookiesEnabled); + Assert.IsNotNull(GetSettings().BeforeCall); + + GetSettings().ResetDefaults(); + + Assert.That(GetSettings().JsonSerializer is NewtonsoftJsonSerializer); + Assert.IsFalse(GetSettings().CookiesEnabled); + Assert.IsNull(GetSettings().BeforeCall); + } + } + + [TestFixture, NonParallelizable] // touches global settings, so can't run in parallel + public class GlobalSettingsTests : SettingsTestsBase + { + protected override FlurlHttpSettings GetSettings() => FlurlHttp.GlobalSettings; + protected override IFlurlRequest GetRequest() => new FlurlRequest("http://api.com"); + + [TearDown] + public void ResetDefaults() => FlurlHttp.GlobalSettings.ResetDefaults(); + + [Test] + public void settings_propagate_correctly() { + FlurlHttp.GlobalSettings.CookiesEnabled = false; + FlurlHttp.GlobalSettings.AllowedHttpStatusRange = "4xx"; + + var client1 = new FlurlClient(); + client1.Settings.CookiesEnabled = true; + Assert.AreEqual("4xx", client1.Settings.AllowedHttpStatusRange); + client1.Settings.AllowedHttpStatusRange = "5xx"; + + var req = client1.Request("http://myapi.com"); + Assert.IsTrue(req.Settings.CookiesEnabled, "request should inherit client settings when not set at request level"); + Assert.AreEqual("5xx", req.Settings.AllowedHttpStatusRange, "request should inherit client settings when not set at request level"); + + var client2 = new FlurlClient(); + client2.Settings.CookiesEnabled = false; + + req.WithClient(client2); + Assert.IsFalse(req.Settings.CookiesEnabled, "request should inherit client settings when not set at request level"); + Assert.AreEqual("4xx", req.Settings.AllowedHttpStatusRange, "request should inherit global settings when not set at request or client level"); + + client2.Settings.CookiesEnabled = true; + client2.Settings.AllowedHttpStatusRange = "3xx"; + Assert.IsTrue(req.Settings.CookiesEnabled, "request should inherit client sttings when not set at request level"); + Assert.AreEqual("3xx", req.Settings.AllowedHttpStatusRange, "request should inherit client sttings when not set at request level"); + + req.Settings.CookiesEnabled = false; + req.Settings.AllowedHttpStatusRange = "6xx"; + Assert.IsFalse(req.Settings.CookiesEnabled, "request-level settings should override any defaults"); + Assert.AreEqual("6xx", req.Settings.AllowedHttpStatusRange, "request-level settings should override any defaults"); + + req.Settings.ResetDefaults(); + Assert.IsTrue(req.Settings.CookiesEnabled, "request should inherit client sttings when cleared at request level"); + Assert.AreEqual("3xx", req.Settings.AllowedHttpStatusRange, "request should inherit client sttings when cleared request level"); + + client2.Settings.ResetDefaults(); + Assert.IsFalse(req.Settings.CookiesEnabled, "request should inherit global settings when cleared at request and client level"); + Assert.AreEqual("4xx", req.Settings.AllowedHttpStatusRange, "request should inherit global settings when cleared at request and client level"); + } + + [Test] + public void can_provide_custom_client_factory() { + FlurlHttp.GlobalSettings.HttpClientFactory = new SomeCustomHttpClientFactory(); + Assert.IsInstanceOf(GetRequest().Client.HttpClient); + Assert.IsInstanceOf(GetRequest().Client.HttpMessageHandler); + } + + [Test] + public void can_configure_global_from_FlurlHttp_object() { + FlurlHttp.Configure(settings => settings.CookiesEnabled = true); + Assert.IsTrue(FlurlHttp.GlobalSettings.CookiesEnabled); + } + + [Test] + public void can_configure_client_from_FlurlHttp_object() { + FlurlHttp.ConfigureClient("http://host1.com/foo", settings => settings.CookiesEnabled = true); + Assert.IsTrue(new FlurlRequest("https://host1.com/bar").Client.Settings.CookiesEnabled); // different URL but same host, so should use same client + Assert.IsFalse(new FlurlRequest("http://host2.com").Client.Settings.CookiesEnabled); + } + } + + [TestFixture, Parallelizable] + public class HttpTestSettingsTests : SettingsTestsBase + { + private HttpTest _test; + + [SetUp] + public void CreateTest() => _test = new HttpTest(); + + [TearDown] + public void DisposeTest() => _test.Dispose(); + + protected override FlurlHttpSettings GetSettings() => HttpTest.Current.Settings; + protected override IFlurlRequest GetRequest() => new FlurlRequest("http://api.com"); + } + + [TestFixture, Parallelizable] + public class ClientSettingsTests : SettingsTestsBase + { + private readonly Lazy _client = new Lazy(() => new FlurlClient()); + + protected override FlurlHttpSettings GetSettings() => _client.Value.Settings; + protected override IFlurlRequest GetRequest() => _client.Value.Request("http://api.com"); + + [Test] + public void can_provide_custom_client_factory() { + var cli = new FlurlClient(); + cli.Settings.HttpClientFactory = new SomeCustomHttpClientFactory(); + Assert.IsInstanceOf(cli.HttpClient); + Assert.IsInstanceOf(cli.HttpMessageHandler); + } + + [Test] + public async Task connection_lease_timeout_sets_connection_close_header() { + using (var test = new HttpTest()) { + var cli = new FlurlClient("http://api.com"); + cli.Settings.ConnectionLeaseTimeout = TimeSpan.FromMilliseconds(50); + + await cli.Request("1").GetAsync(); + test.ShouldHaveCalled("http://api.com/1").WithoutHeader("Connection"); + + // exceed the timeout + await Task.Delay(51); + + // slam it many times concurrently + await Task.WhenAll( + cli.Request("2").GetAsync(), + cli.Request("2").GetAsync(), + cli.Request("2").GetAsync(), + cli.Request("2").GetAsync(), + cli.Request("2").GetAsync(), + cli.Request("2").GetAsync(), + cli.Request("2").GetAsync(), + cli.Request("2").GetAsync()); + + // connection:close header should get sent exactly once + test.ShouldHaveCalled("http://api.com/2").WithHeader("Connection", "close").Times(1); + + await Task.Delay(10); + + await cli.Request("3").GetAsync(); + test.ShouldHaveCalled("http://api.com/3").WithoutHeader("Connection"); + } + } + } + + [TestFixture, Parallelizable] + public class RequestSettingsTests : SettingsTestsBase + { + private readonly Lazy _req = new Lazy(() => new FlurlRequest("http://api.com")); + + protected override FlurlHttpSettings GetSettings() => _req.Value.Settings; + protected override IFlurlRequest GetRequest() => _req.Value; + } + + public class SomeCustomHttpClientFactory : IHttpClientFactory + { + public HttpClient CreateHttpClient(HttpMessageHandler handler) => new SomeCustomHttpClient(); + public HttpMessageHandler CreateMessageHandler() => new SomeCustomMessageHandler(); + } + + public class SomeCustomHttpClient : HttpClient { } + public class SomeCustomMessageHandler : HttpClientHandler { } +} diff --git a/Test/Flurl.Test/Http/TestingTests.cs b/Test/Flurl.Test/Http/TestingTests.cs index e4b1b9e1..48e25d90 100644 --- a/Test/Flurl.Test/Http/TestingTests.cs +++ b/Test/Flurl.Test/Http/TestingTests.cs @@ -9,7 +9,7 @@ namespace Flurl.Test.Http { - [TestFixture] + [TestFixture, Parallelizable] public class TestingTests : HttpTestFixtureBase { [Test] @@ -123,6 +123,24 @@ public async Task can_assert_multiple_occurances_of_query_param() { HttpTest.ShouldHaveMadeACall().WithQueryParamValues(new { x = new[] { 1, 2, 4 } })); } + [Test] + public async Task can_assert_headers() { + await "http://api.com".WithHeaders(new { h1 = "val1", h2 = "val2" }).GetAsync(); + + HttpTest.ShouldHaveMadeACall().WithHeader("h1"); + HttpTest.ShouldHaveMadeACall().WithHeader("h2", "val2"); + HttpTest.ShouldHaveMadeACall().WithHeader("h1", "val*"); + + HttpTest.ShouldHaveMadeACall().WithoutHeader("h3"); + HttpTest.ShouldHaveMadeACall().WithoutHeader("h2", "val1"); + HttpTest.ShouldHaveMadeACall().WithoutHeader("h1", "foo*"); + + Assert.Throws(() => + HttpTest.ShouldHaveMadeACall().WithHeader("h3")); + Assert.Throws(() => + HttpTest.ShouldHaveMadeACall().WithoutHeader("h1")); + } + [Test] public async Task can_simulate_timeout() { HttpTest.SimulateTimeout(); @@ -140,7 +158,7 @@ public async Task can_simulate_timeout() { public async Task can_simulate_timeout_with_exception_handled() { HttpTest.SimulateTimeout(); var result = await "http://www.api.com" - .ConfigureClient(c => c.OnError = call => call.ExceptionHandled = true) + .ConfigureRequest(c => c.OnError = call => call.ExceptionHandled = true) .GetAsync(); Assert.IsNull(result); } @@ -159,28 +177,10 @@ public async Task can_fake_headers() { public async Task can_fake_cookies() { HttpTest.RespondWith(cookies: new { c1 = "foo" }); - var fc = "http://www.api.com".EnableCookies(); - await fc.GetAsync(); - Assert.AreEqual(1, fc.Cookies.Count()); - Assert.AreEqual("foo", fc.Cookies["c1"].Value); - } - - // https://github.com/tmenier/Flurl/issues/169 - [Test] - public async Task cannot_inspect_RequestBody_with_uncaptured_content() { - using (var httpTest = new HttpTest()) { - // use StringContent instead of CapturedStringContent - await "http://api.com".SendAsync(HttpMethod.Post, new StringContent("foo", null, "text/plain")); - try { - httpTest.ShouldHaveMadeACall().WithRequestBody("foo"); - Assert.Fail("Asserting RequestBody with uncaptured content should have thrown FlurlHttpException."); - } - catch (FlurlHttpException ex) { - // message should mention RequestBody and CapturedStringContent - StringAssert.Contains("RequestBody", ex.Message); - StringAssert.Contains("CapturedStringContent", ex.Message); - } - } + var rec = "http://www.api.com".EnableCookies(); + await rec.GetAsync(); + Assert.AreEqual(1, rec.Cookies.Count); + Assert.AreEqual("foo", rec.Cookies["c1"].Value); } // https://github.com/tmenier/Flurl/issues/175 @@ -195,8 +195,16 @@ public async Task can_deserialize_default_response_more_than_once() { Assert.IsNull(resp); } - // parallel testing not supported in PCL -#if !PORTABLE + [Test] + public void can_configure_settings_for_test() { + Assert.IsFalse(new FlurlRequest().Settings.CookiesEnabled); + using (new HttpTest().Configure(settings => settings.CookiesEnabled = true)) { + Assert.IsTrue(new FlurlRequest().Settings.CookiesEnabled); + } + // test disposed, should revert back to global settings + Assert.IsFalse(new FlurlRequest().Settings.CookiesEnabled); + } + [Test] public async Task can_test_in_parallel() { await Task.WhenAll( @@ -206,7 +214,6 @@ await Task.WhenAll( CallAndAssertCountAsync(4), CallAndAssertCountAsync(6)); } -#endif private async Task CallAndAssertCountAsync(int calls) { using (var test = new HttpTest()) { diff --git a/Test/Flurl.Test/UrlBuilderTests.cs b/Test/Flurl.Test/UrlBuilderTests.cs index 66db08e5..66b136db 100644 --- a/Test/Flurl.Test/UrlBuilderTests.cs +++ b/Test/Flurl.Test/UrlBuilderTests.cs @@ -8,7 +8,7 @@ namespace Flurl.Test { - [TestFixture] + [TestFixture, Parallelizable] public class UrlBuilderTests { [Test] diff --git a/src/Flurl.Http.CodeGen/ExtensionMethodModel.cs b/src/Flurl.Http.CodeGen/HttpExtensionMethod.cs similarity index 90% rename from src/Flurl.Http.CodeGen/ExtensionMethodModel.cs rename to src/Flurl.Http.CodeGen/HttpExtensionMethod.cs index 7a822602..23272bc5 100644 --- a/src/Flurl.Http.CodeGen/ExtensionMethodModel.cs +++ b/src/Flurl.Http.CodeGen/HttpExtensionMethod.cs @@ -3,19 +3,19 @@ namespace Flurl.Http.CodeGen { - public class ExtensionMethodModel + public class HttpExtensionMethod { - public static IEnumerable GetAll() { + public static IEnumerable GetAll() { return from httpVerb in new[] { null, "Get", "Post", "Head", "Put", "Delete", "Patch" } from bodyType in new[] { null, "Json", /*"Xml",*/ "String", "UrlEncoded" } - from extensionType in new[] { "IFlurlClient", "Url", "string" } + from extensionType in new[] { "IFlurlRequest", "Url", "string" } where SupportedCombo(httpVerb, bodyType, extensionType) from deserializeType in new[] { null, "Json", "JsonList", /*"Xml",*/ "String", "Stream", "Bytes" } where httpVerb == "Get" || deserializeType == null from isGeneric in new[] { true, false } where AllowDeserializeToGeneric(deserializeType) || !isGeneric - select new ExtensionMethodModel { + select new HttpExtensionMethod { HttpVerb = httpVerb, BodyType = bodyType, ExtentionOfType = extensionType, @@ -27,7 +27,7 @@ where AllowDeserializeToGeneric(deserializeType) || !isGeneric private static bool SupportedCombo(string verb, string bodyType, string extensionType) { switch (verb) { case null: // Send - return bodyType != null || extensionType != "IFlurlClient"; + return bodyType != null || extensionType != "IFlurlRequest"; case "Post": return true; case "Put": diff --git a/src/Flurl.Http.CodeGen/Program.cs b/src/Flurl.Http.CodeGen/Program.cs index 62234b13..01d8934d 100644 --- a/src/Flurl.Http.CodeGen/Program.cs +++ b/src/Flurl.Http.CodeGen/Program.cs @@ -8,11 +8,12 @@ namespace Flurl.Http.CodeGen class Program { static int Main(string[] args) { - var codePath = (args.Length > 0) ? args[0] : @"..\Flurl.Http.Shared\HttpExtensions.cs"; + var codePath = (args.Length > 0) ? args[0] : @"..\Flurl.Http\GeneratedExtensions.cs"; if (!File.Exists(codePath)) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("Code file not found: " + Path.GetFullPath(codePath)); + Console.ReadLine(); return 2; } @@ -22,20 +23,22 @@ static int Main(string[] args) { { writer .WriteLine("// This file was auto-generated by Flurl.Http.CodeGen. Do not edit directly.") - .WriteLine() - .WriteLine("using System.Collections.Generic;") + .WriteLine("using System;") + .WriteLine("using System.Collections.Generic;") .WriteLine("using System.IO;") - .WriteLine("using System.Net.Http;") + .WriteLine("using System.Net;") + .WriteLine("using System.Net.Http;") .WriteLine("using System.Threading;") .WriteLine("using System.Threading.Tasks;") - .WriteLine("using Flurl.Http.Content;") + .WriteLine("using Flurl.Http.Configuration;") + .WriteLine("using Flurl.Http.Content;") .WriteLine("") .WriteLine("namespace Flurl.Http") .WriteLine("{") - .WriteLine(" /// ") - .WriteLine("/// Http extensions for Flurl Client.") + .WriteLine("/// ") + .WriteLine("/// Auto-generated fluent extension methods on String, Url, and IFlurlRequest.") .WriteLine("/// ") - .WriteLine("public static class HttpExtensions") + .WriteLine("public static class GeneratedExtensions") .WriteLine("{"); WriteExtensionMethods(writer); @@ -51,6 +54,7 @@ static int Main(string[] args) { catch (Exception ex) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine(ex); + Console.ReadLine(); return 2; } } @@ -58,7 +62,7 @@ static int Main(string[] args) { private static void WriteExtensionMethods(CodeWriter writer) { string name = null; - foreach (var xm in ExtensionMethodModel.GetAll()) { + foreach (var xm in HttpExtensionMethod.GetAll()) { var hasRequestBody = (xm.HttpVerb == "Post" || xm.HttpVerb == "Put" || xm.HttpVerb == "Patch" || xm.HttpVerb == null); if (xm.Name != name) { @@ -66,14 +70,14 @@ private static void WriteExtensionMethods(CodeWriter writer) name = xm.Name; } writer.WriteLine("/// "); - var summaryStart = (xm.ExtentionOfType == "IFlurlClient") ? "Sends" : "Creates a FlurlClient from the URL and sends"; + var summaryStart = (xm.ExtentionOfType == "IFlurlRequest") ? "Sends" : "Creates a FlurlRequest from the URL and sends"; if (xm.HttpVerb == null) writer.WriteLine("/// @0 an asynchronous request.", summaryStart); else writer.WriteLine("/// @0 an asynchronous @1 request.", summaryStart, xm.HttpVerb.ToUpperInvariant()); writer.WriteLine("/// "); - if (xm.ExtentionOfType == "IFlurlClient") - writer.WriteLine("/// The IFlurlClient instance."); + if (xm.ExtentionOfType == "IFlurlRequest") + writer.WriteLine("/// The IFlurlRequest instance."); if (xm.ExtentionOfType == "Url" || xm.ExtentionOfType == "string") writer.WriteLine("/// The URL."); if (xm.HttpVerb == null) @@ -87,7 +91,7 @@ private static void WriteExtensionMethods(CodeWriter writer) writer.WriteLine("/// A Task whose result is @0.", xm.ReturnTypeDescription); var args = new List(); - args.Add("this " + xm.ExtentionOfType + (xm.ExtentionOfType == "IFlurlClient" ? " client" : " url")); + args.Add("this " + xm.ExtentionOfType + (xm.ExtentionOfType == "IFlurlRequest" ? " request" : " url")); if (xm.HttpVerb == null) args.Add("HttpMethod verb"); if (xm.BodyType != null) @@ -101,7 +105,7 @@ private static void WriteExtensionMethods(CodeWriter writer) writer.WriteLine("public static Task<@0> @1@2(@3) {", xm.TaskArg, xm.Name, xm.IsGeneric ? "" : "", string.Join(", ", args)); - if (xm.ExtentionOfType == "IFlurlClient") + if (xm.ExtentionOfType == "IFlurlRequest") { args.Clear(); args.Add( @@ -118,22 +122,45 @@ private static void WriteExtensionMethods(CodeWriter writer) if (xm.BodyType != null) { writer.WriteLine("var content = new Captured@0Content(@1);", xm.BodyType, - xm.BodyType == "String" ? "data" : $"client.Settings.{xm.BodyType}Serializer.Serialize(data)"); + xm.BodyType == "String" ? "data" : $"request.Settings.{xm.BodyType}Serializer.Serialize(data)"); } - var client = (xm.ExtentionOfType == "IFlurlClient") ? "client" : "new FlurlClient(url, false)"; + var request = (xm.ExtentionOfType == "IFlurlRequest") ? "request" : "new FlurlRequest(url)"; var receive = (xm.DeserializeToType == null) ? "" : string.Format(".Receive{0}{1}()", xm.DeserializeToType, xm.IsGeneric ? "" : ""); - writer.WriteLine("return @0.SendAsync(@1)@2;", client, string.Join(", ", args), receive); + writer.WriteLine("return @0.SendAsync(@1)@2;", request, string.Join(", ", args), receive); } else { - writer.WriteLine("return new FlurlClient(url, false).@0(@1);", + writer.WriteLine("return new FlurlRequest(url).@0(@1);", xm.Name + (xm.IsGeneric ? "" : ""), string.Join(", ", args.Skip(1).Select(a => a.Split(' ')[1]))); } writer.WriteLine("}").WriteLine(); } + + foreach (var xtype in new[] { "Url", "string" }) { + foreach (var xm in UrlExtensionMethod.GetAll()) { + if (xm.Name != name) { + Console.WriteLine($"writing {xm.Name}..."); + name = xm.Name; + } + + writer.WriteLine("/// "); + writer.WriteLine($"/// {xm.Description}"); + writer.WriteLine("/// "); + writer.WriteLine("/// The URL."); + foreach (var p in xm.Params) + writer.WriteLine($"/// {p.Description}"); + writer.WriteLine("/// The IFlurlRequest."); + + var argList = new List { $"this {xtype} url" }; + argList.AddRange(xm.Params.Select(p => $"{p.Type} {p.Name}" + (p.Default == null ? "" : $" = {p.Default}"))); + writer.WriteLine($"public static IFlurlRequest {xm.Name}({string.Join(", ", argList)}) {{"); + writer.WriteLine($"return new FlurlRequest(url).{xm.Name}({string.Join(", ", xm.Params.Select(p => p.Name))});"); + writer.WriteLine("}"); + } + } } } } \ No newline at end of file diff --git a/src/Flurl.Http.CodeGen/UrlExtensionMethod.cs b/src/Flurl.Http.CodeGen/UrlExtensionMethod.cs new file mode 100644 index 00000000..b523de9e --- /dev/null +++ b/src/Flurl.Http.CodeGen/UrlExtensionMethod.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Flurl.Http.CodeGen +{ + /// + /// Extension methods manually defined on IFlurlRequest and IFlurlClient. We'll auto-gen overloads for Url and string. + /// + public class UrlExtensionMethod + { + public static IEnumerable GetAll() { + // header extensions + yield return new UrlExtensionMethod("WithHeader", "Creates a new FlurlRequest with the URL and sets a request header.") + .AddParam("name", "string", "The header name.") + .AddParam("value", "object", "The header value."); + yield return new UrlExtensionMethod("WithHeaders", "Creates a new FlurlRequest with the URL and sets request headers based on property names/values of the provided object, or keys/values if object is a dictionary, to be sent") + .AddParam("headers", "object", "Names/values of HTTP headers to set. Typically an anonymous object or IDictionary.") + .AddParam("replaceUnderscoreWithHyphen", "bool", "If true, underscores in property names will be replaced by hyphens. Default is true.", "true"); + yield return new UrlExtensionMethod("WithBasicAuth", "Creates a new FlurlRequest with the URL and sets the Authorization header according to Basic Authentication protocol.") + .AddParam("username", "string", "Username of authenticating user.") + .AddParam("password", "string", "Password of authenticating user."); + yield return new UrlExtensionMethod("WithOAuthBearerToken", "Creates a new FlurlRequest with the URL and sets the Authorization header with a bearer token according to OAuth 2.0 specification.") + .AddParam("token", "string", "The acquired oAuth bearer token."); + + // cookie extensions + yield return new UrlExtensionMethod("EnableCookies", "Creates a new FlurlRequest with the URL and allows cookies to be sent and received. Not necessary to call when setting cookies via WithCookie/WithCookies."); + yield return new UrlExtensionMethod("WithCookie", "Creates a new FlurlRequest with the URL and sets an HTTP cookie to be sent") + .AddParam("cookie", "Cookie", ""); + yield return new UrlExtensionMethod("WithCookie", "Creates a new FlurlRequest with the URL and sets an HTTP cookie to be sent.") + .AddParam("name", "string", "The cookie name.") + .AddParam("value", "object", "The cookie value.") + .AddParam("expires", "DateTime?", "The cookie expiration (optional). If excluded, cookie only lives for duration of session.", "null"); + yield return new UrlExtensionMethod("WithCookies", "Creates a new FlurlRequest with the URL and sets HTTP cookies to be sent, based on property names / values of the provided object, or keys / values if object is a dictionary.") + .AddParam("cookies", "object", "Names/values of HTTP cookies to set. Typically an anonymous object or IDictionary.") + .AddParam("expires", "DateTime?", "Expiration for all cookies (optional). If excluded, cookies only live for duration of session.", "null"); + + // settings extensions + yield return new UrlExtensionMethod("ConfigureRequest", "Creates a new FlurlRequest with the URL and allows changing its Settings inline.") + .AddParam("action", "Action", "A delegate defining the Settings changes."); + yield return new UrlExtensionMethod("WithTimeout", "Creates a new FlurlRequest with the URL and sets the request timeout.") + .AddParam("timespan", "TimeSpan", "Time to wait before the request times out."); + yield return new UrlExtensionMethod("WithTimeout", "Creates a new FlurlRequest with the URL and sets the request timeout.") + .AddParam("seconds", "int", "Seconds to wait before the request times out."); + yield return new UrlExtensionMethod("AllowHttpStatus", "Creates a new FlurlRequest with the URL and adds a pattern representing an HTTP status code or range of codes which (in addition to 2xx) will NOT result in a FlurlHttpException being thrown.") + .AddParam("pattern", "string", "Examples: \"3xx\", \"100,300,600\", \"100-299,6xx\""); + yield return new UrlExtensionMethod("AllowHttpStatus", "Creates a new FlurlRequest with the URL and adds an HttpStatusCode which (in addtion to 2xx) will NOT result in a FlurlHttpException being thrown.") + .AddParam("statusCodes", "params HttpStatusCode[]", "The HttpStatusCode(s) to allow."); + yield return new UrlExtensionMethod("AllowAnyHttpStatus", "Creates a new FlurlRequest with the URL and configures it to allow any returned HTTP status without throwing a FlurlHttpException."); + } + + public string Name { get; } + public string Description { get; } + public IList Params { get; } = new List(); + + public UrlExtensionMethod(string name, string description) { + Name = name; + Description = description; + } + + public UrlExtensionMethod AddParam(string name, string type, string description, string defaultVal = null) { + Params.Add(new Param { Name = name, Type = type, Description = description, Default = defaultVal }); + return this; + } + + public class Param + { + public string Name { get; set; } + public string Type { get; set; } + public string Description { get; set; } + public string Default { get; set; } + } + } +} diff --git a/src/Flurl.Http/ClientConfigExtensions.cs b/src/Flurl.Http/ClientConfigExtensions.cs deleted file mode 100644 index ede3fdd1..00000000 --- a/src/Flurl.Http/ClientConfigExtensions.cs +++ /dev/null @@ -1,413 +0,0 @@ -using System; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using Flurl.Http.Configuration; -using Flurl.Util; - -namespace Flurl.Http -{ - /// - /// Extensions for configure the Flurl Client. - /// - public static class ClientConfigExtensions - { - /// - /// Returns a new IFlurlClient where all state (HttpClient, etc) is shared but with a different URL. - /// Allows you to re-use the underlying HttpClient instance (such as to share cookies, etc) with - /// different URLs in a thread-safe way. - /// - /// The client. - /// The Url to call. - public static IFlurlClient WithUrl(this IFlurlClient client, Url url) { - var fc = client.Clone(); - fc.Url = url; - // prevent the new client from automatically disposing the parent's HttpClient - fc.Settings.AutoDispose = false; - return fc; - } - - /// - /// Fluently specify that an existing IFlurlClient should be used to call the Url, rather than creating a new one. - /// Enables re-using the underlying HttpClient. - /// - /// The URL. - /// The IFlurlClient to use in calling the Url - public static IFlurlClient WithClient(this Url url, IFlurlClient client) { - return client.WithUrl(url); - } - - /// - /// Fluently specify that an existing IFlurlClient should be used to call the Url, rather than creating a new one. - /// Enables re-using the underlying HttpClient. - /// - /// The URL. - /// The IFlurlClient to use in calling the Url - public static IFlurlClient WithClient(this string url, IFlurlClient client) { - return client.WithUrl(url); - } - - /// - /// Change FlurlHttpSettings for this client instance. - /// - /// The client. - /// Action defining the settings changes. - /// The IFlurlClient with the modified HttpClient - public static IFlurlClient ConfigureClient(this IFlurlClient client, Action action) { - action(client.Settings); - return client; - } - - /// - /// Creates a FlurlClient from the URL and allows changing the FlurlHttpSettings associated with the instance. - /// - /// The URL. - /// Action defining the settings changes. - /// The IFlurlClient with the modified HttpClient - public static IFlurlClient ConfigureClient(this Url url, Action action) { - return new FlurlClient(url, true).ConfigureClient(action); - } - - /// - /// Creates a FlurlClient from the URL and allows changing the FlurlHttpSettings associated with the instance. - /// - /// The URL. - /// Action defining the settings changes. - /// The FlurlClient with the modified HttpClient - public static IFlurlClient ConfigureClient(this string url, Action action) { - return new FlurlClient(url, true).ConfigureClient(action); - } - - /// - /// Provides access to modifying the underlying HttpClient. - /// - /// The client. - /// Action to perform on the HttpClient. - /// The FlurlClient with the modified HttpClient - public static IFlurlClient ConfigureHttpClient(this IFlurlClient client, Action action) { - action(client.HttpClient); - return client; - } - - /// - /// Creates a FlurlClient from the URL and provides access to modifying the underlying HttpClient. - /// - /// The URL. - /// Action to perform on the HttpClient. - /// The FlurlClient with the modified HttpClient - public static IFlurlClient ConfigureHttpClient(this Url url, Action action) { - return new FlurlClient(url, true).ConfigureHttpClient(action); - } - - /// - /// Creates a FlurlClient from the URL and provides access to modifying the underlying HttpClient. - /// - /// The URL. - /// Action to perform on the HttpClient. - /// The FlurlClient with the modified HttpClient - public static IFlurlClient ConfigureHttpClient(this string url, Action action) { - return new FlurlClient(url, true).ConfigureHttpClient(action); - } - - /// - /// Sets the client timout to the specified timespan. - /// - /// The client. - /// Time to wait before the request times out. - /// The modified FlurlClient. - public static IFlurlClient WithTimeout(this IFlurlClient client, TimeSpan timespan) { - client.HttpClient.Timeout = timespan; - return client; - } - - /// - /// Creates a FlurlClient from the URL and sets the client timout to the specified timespan. - /// - /// The URL. - /// Time to wait before the request times out. - /// The created FlurlClient. - public static IFlurlClient WithTimeout(this Url url, TimeSpan timespan) { - return new FlurlClient(url, true).WithTimeout(timespan); - } - - /// - /// Creates a FlurlClient from the URL and sets the client timout to the specified timespan. - /// - /// The URL. - /// Time to wait before the request times out. - /// The created FlurlClient. - public static IFlurlClient WithTimeout(this string url, TimeSpan timespan) { - return new FlurlClient(url, true).WithTimeout(timespan); - } - - /// - /// Sets the client timout to the specified number of seconds. - /// - /// The client. - /// Number of seconds to wait before the request times out. - /// The modified FlurlClient. - /// is less than or greater than .-or- is .-or- is . - public static IFlurlClient WithTimeout(this IFlurlClient client, int seconds) { - return client.WithTimeout(TimeSpan.FromSeconds(seconds)); - } - - /// - /// Creates a FlurlClient from the URL and sets the client timout to the specified number of seconds. - /// - /// The URL. - /// Number of seconds to wait before the request times out. - /// The created FlurlClient. - /// is less than or greater than .-or- is .-or- is . - public static IFlurlClient WithTimeout(this Url url, int seconds) { - return new FlurlClient(url, true).WithTimeout(seconds); - } - - /// - /// Creates a FlurlClient from the URL and sets the client timout to the specified number of seconds. - /// - /// The URL. - /// Number of seconds to wait before the request times out. - /// The created FlurlClient. - /// is less than or greater than .-or- is .-or- is . - public static IFlurlClient WithTimeout(this string url, int seconds) { - return new FlurlClient(url, true).WithTimeout(seconds); - } - - /// - /// Sets an HTTP header to be sent with all requests made with this FlurlClient. - /// - /// The client. - /// HTTP header name. - /// HTTP header value. - /// The modified FlurlClient. - public static IFlurlClient WithHeader(this IFlurlClient client, string name, object value) { - var values = new[] { value?.ToString() }; - client.HttpClient.DefaultRequestHeaders.Add(name, values); - return client; - } - - /// - /// Creates a FlurlClient from the URL and sets an HTTP header to be sent with all requests made with this FlurlClient. - /// - /// The URL. - /// HTTP header name. - /// HTTP header value. - /// The modified FlurlClient. - public static IFlurlClient WithHeader(this Url url, string name, object value) { - return new FlurlClient(url, true).WithHeader(name, value); - } - - /// - /// Creates a FlurlClient from the URL and sets an HTTP header to be sent with all requests made with this FlurlClient. - /// - /// The URL. - /// HTTP header name. - /// HTTP header value. - /// The modified FlurlClient. - public static IFlurlClient WithHeader(this string url, string name, object value) { - return new FlurlClient(url, true).WithHeader(name, value); - } - - /// - /// Sets HTTP headers based on property names/values of the provided object, or keys/values if object is a dictionary, to be sent with all requests made with this FlurlClient. - /// - /// The client. - /// Names/values of HTTP headers to set. Typically an anonymous object or IDictionary. - /// The modified FlurlClient. - public static IFlurlClient WithHeaders(this IFlurlClient client, object headers) { - if (headers == null) - return client; - - foreach (var kv in headers.ToKeyValuePairs()) { - if (kv.Value == null) - continue; - - client.HttpClient.DefaultRequestHeaders.Add(kv.Key, new[] { kv.Value.ToString() }); - } - - return client; - } - - /// - /// Creates a FlurlClient from the URL and sets HTTP headers based on property names/values of the provided object, or keys/values if object is a dictionary, to be sent with all requests made with this FlurlClient. - /// - /// The URL. - /// Names/values of HTTP headers to set. Typically an anonymous object or IDictionary. - /// The modified FlurlClient. - public static IFlurlClient WithHeaders(this Url url, object headers) { - return new FlurlClient(url, true).WithHeaders(headers); - } - - /// - /// Creates a FlurlClient from the URL and sets HTTP headers based on property names/values of the provided object, or keys/values if object is a dictionary, to be sent with all requests made with this FlurlClient. - /// - /// The URL. - /// Names/values of HTTP headers to set. Typically an anonymous object or IDictionary. - /// The modified FlurlClient. - public static IFlurlClient WithHeaders(this string url, object headers) { - return new FlurlClient(url, true).WithHeaders(headers); - } - - /// - /// Sets HTTP authorization header according to Basic Authentication protocol to be sent with all requests made with this FlurlClient. - /// - /// The client. - /// Username of authenticating user. - /// Password of authenticating user. - /// The modified FlurlClient. - public static IFlurlClient WithBasicAuth(this IFlurlClient client, string username, string password) { - // http://stackoverflow.com/questions/14627399/setting-authorization-header-of-httpclient - var value = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}")); - client.HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", value); - return client; - } - - /// - /// Creates a FlurlClient from the URL and sets HTTP authorization header according to Basic Authentication protocol to be sent with all requests made with this FlurlClient. - /// - /// The URL. - /// Username of authenticating user. - /// Password of authenticating user. - /// The new IFlurlClient instance. - public static IFlurlClient WithBasicAuth(this Url url, string username, string password) { - return new FlurlClient(url, true).WithBasicAuth(username, password); - } - - /// - /// Creates a FlurlClient from the URL and sets HTTP authorization header according to Basic Authentication protocol to be sent with all requests made with this FlurlClient. - /// - /// The URL. - /// Username of authenticating user. - /// Password of authenticating user. - /// The new IFlurlClient instance. - public static IFlurlClient WithBasicAuth(this string url, string username, string password) { - return new FlurlClient(url, true).WithBasicAuth(username, password); - } - - /// - /// Sets HTTP authorization header with acquired bearer token according to OAuth 2.0 specification to be sent with all requests made with this FlurlClient. - /// - /// The client. - /// The acquired bearer token to pass. - /// The modified FlurlClient. - public static IFlurlClient WithOAuthBearerToken(this IFlurlClient client, string token) { - client.HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); - return client; - } - - /// - /// Creates a FlurlClient from the URL and sets HTTP authorization header with acquired bearer token according to OAuth 2.0 specification to be sent with all requests made with this FlurlClient. - /// - /// The URL. - /// The acquired bearer token to pass. - /// The new IFlurlClient instance. - public static IFlurlClient WithOAuthBearerToken(this Url url, string token) { - return new FlurlClient(url, true).WithOAuthBearerToken(token); - } - - /// - /// Creates a FlurlClient from the URL and sets HTTP authorization header with acquired bearer token according to OAuth 2.0 specification to be sent with all requests made with this FlurlClient. - /// - /// The URL. - /// The acquired bearer token to pass. - /// The new IFlurlClient instance. - public static IFlurlClient WithOAuthBearerToken(this string url, string token) { - return new FlurlClient(url, true).WithOAuthBearerToken(token); - } - - /// - /// Adds a pattern representing an HTTP status code or range of codes which (in addition to 2xx) will NOT result in a FlurlHttpException being thrown. - /// - /// The client. - /// Examples: "3xx", "100,300,600", "100-299,6xx" - /// The modified FlurlClient. - public static IFlurlClient AllowHttpStatus(this IFlurlClient client, string pattern) { - if (!string.IsNullOrWhiteSpace(pattern)) { - var current = client.Settings.AllowedHttpStatusRange; - if (string.IsNullOrWhiteSpace(current)) - client.Settings.AllowedHttpStatusRange = pattern; - else - client.Settings.AllowedHttpStatusRange += "," + pattern; - } - return client; - } - - /// - /// Creates a FlurlClient from the URL and adds a pattern representing an HTTP status code or range of codes which (in addition to 2xx) will NOT result in a FlurlHttpException being thrown. - /// - /// The URL. - /// Examples: "3xx", "100,300,600", "100-299,6xx" - /// The new IFlurlClient instance. - public static IFlurlClient AllowHttpStatus(this Url url, string pattern) { - return new FlurlClient(url, true).AllowHttpStatus(pattern); - } - - /// - /// Creates a FlurlClient from the URL and adds a pattern representing an HTTP status code or range of codes which (in addition to 2xx) will NOT result in a FlurlHttpException being thrown. - /// - /// The URL. - /// Examples: "3xx", "100,300,600", "100-299,6xx" - /// The new IFlurlClient instance. - public static IFlurlClient AllowHttpStatus(this string url, string pattern) { - return new FlurlClient(url, true).AllowHttpStatus(pattern); - } - - /// - /// Adds an which (in addition to 2xx) will NOT result in a FlurlHttpException being thrown. - /// - /// The client. - /// Examples: HttpStatusCode.NotFound - /// The modified FlurlClient. - public static IFlurlClient AllowHttpStatus(this IFlurlClient client, params HttpStatusCode[] statusCodes) { - var pattern = string.Join(",", statusCodes.Select(c => (int)c)); - return AllowHttpStatus(client, pattern); - } - - /// - /// Adds an which (in addition to 2xx) will NOT result in a FlurlHttpException being thrown. - /// - /// The URL. - /// Examples: HttpStatusCode.NotFound - /// The new IFlurlClient instance. - public static IFlurlClient AllowHttpStatus(this Url url, params HttpStatusCode[] statusCodes) { - return new FlurlClient(url, true).AllowHttpStatus(statusCodes); - } - - /// - /// Adds an which (in addition to 2xx) will NOT result in a FlurlHttpException being thrown. - /// - /// The URL. - /// Examples: HttpStatusCode.NotFound - /// The new IFlurlClient instance. - public static IFlurlClient AllowHttpStatus(this string url, params HttpStatusCode[] statusCodes) { - return new FlurlClient(url, true).AllowHttpStatus(statusCodes); - } - - /// - /// Prevents a FlurlHttpException from being thrown on any completed response, regardless of the HTTP status code. - /// - /// The modified IFlurlClient. - public static IFlurlClient AllowAnyHttpStatus(this IFlurlClient client) { - client.Settings.AllowedHttpStatusRange = "*"; - return client; - } - - /// - /// Creates a FlurlClient from the URL and prevents a FlurlHttpException from being thrown on any completed response, regardless of the HTTP status code. - /// - /// The new IFlurlClient instance. - public static IFlurlClient AllowAnyHttpStatus(this Url url) { - return new FlurlClient(url, true).AllowAnyHttpStatus(); - } - - /// - /// Creates a FlurlClient from the URL and prevents a FlurlHttpException from being thrown on any completed response, regardless of the HTTP status code. - /// - /// The new IFlurlClient instance. - public static IFlurlClient AllowAnyHttpStatus(this string url) { - return new FlurlClient(url, true).AllowAnyHttpStatus(); - } - } -} \ No newline at end of file diff --git a/src/Flurl.Http/Configuration/DefaultHttpClientFactory.cs b/src/Flurl.Http/Configuration/DefaultHttpClientFactory.cs index 996fcdc6..44396763 100644 --- a/src/Flurl.Http/Configuration/DefaultHttpClientFactory.cs +++ b/src/Flurl.Http/Configuration/DefaultHttpClientFactory.cs @@ -1,4 +1,5 @@ -using System.Net.Http; +using System; +using System.Net.Http; namespace Flurl.Http.Configuration { @@ -14,8 +15,11 @@ public class DefaultHttpClientFactory : IHttpClientFactory /// In order not to lose Flurl.Http functionality, it is recommended to call base.CreateClient and /// customize the result. /// - public virtual HttpClient CreateClient(Url url, HttpMessageHandler handler) { - return new HttpClient(new FlurlMessageHandler(handler)); + public virtual HttpClient CreateHttpClient(HttpMessageHandler handler) { + return new HttpClient(handler) { + // Timeouts handled per request via FlurlHttpSettings.Timeout + Timeout = System.Threading.Timeout.InfiniteTimeSpan + }; } /// @@ -27,4 +31,4 @@ public virtual HttpMessageHandler CreateMessageHandler() { return new HttpClientHandler(); } } -} +} \ No newline at end of file diff --git a/src/Flurl.Http/Configuration/DefaultUrlEncodedSerializer.cs b/src/Flurl.Http/Configuration/DefaultUrlEncodedSerializer.cs index d0068987..4a6387e3 100644 --- a/src/Flurl.Http/Configuration/DefaultUrlEncodedSerializer.cs +++ b/src/Flurl.Http/Configuration/DefaultUrlEncodedSerializer.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Text; using Flurl.Util; namespace Flurl.Http.Configuration @@ -16,17 +15,13 @@ public class DefaultUrlEncodedSerializer : ISerializer /// /// The object. public string Serialize(object obj) { - var sb = new StringBuilder(); - foreach (var kv in obj.ToKeyValuePairs()) { - if (kv.Value == null) - continue; - if (sb.Length > 0) - sb.Append('&'); - sb.Append(Url.EncodeQueryParamValue(kv.Key, true)); - sb.Append('='); - sb.Append(Url.EncodeQueryParamValue(kv.Value, true)); - } - return sb.ToString(); + if (obj == null) + return null; + + var qp = new QueryParamCollection(); + foreach (var kv in obj.ToKeyValuePairs()) + qp[kv.Key] = new QueryParameter(kv.Key, kv.Value); + return qp.ToString(true); } /// @@ -36,7 +31,7 @@ public string Serialize(object obj) { /// The s. /// Deserializing to UrlEncoded not supported. public T Deserialize(string s) { - throw new NotImplementedException("Deserializing to UrlEncoded not supported."); + throw new NotImplementedException("Deserializing to UrlEncoded is not supported."); } /// @@ -46,7 +41,7 @@ public T Deserialize(string s) { /// The stream. /// Deserializing to UrlEncoded not supported. public T Deserialize(Stream stream) { - throw new NotImplementedException("Deserializing to UrlEncoded not supported."); + throw new NotImplementedException("Deserializing to UrlEncoded is not supported."); } } } \ No newline at end of file diff --git a/src/Flurl.Http/Configuration/FlurlClientFactoryBase.cs b/src/Flurl.Http/Configuration/FlurlClientFactoryBase.cs new file mode 100644 index 00000000..b0e89291 --- /dev/null +++ b/src/Flurl.Http/Configuration/FlurlClientFactoryBase.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Concurrent; + +namespace Flurl.Http.Configuration +{ + /// + /// Encapsulates a creation/caching strategy for IFlurlClient instances. Custom factories looking to extend + /// Flurl's behavior should inherit from this class, rather than implementing IFlurlClientFactory directly. + /// + public abstract class FlurlClientFactoryBase : IFlurlClientFactory + { + private readonly ConcurrentDictionary _clients = new ConcurrentDictionary(); + + /// + /// By defaykt, uses a caching strategy of one FlurlClient per host. This maximizes reuse of + /// underlying HttpClient/Handler while allowing things like cookies to be host-specific. + /// + /// The URL. + /// The FlurlClient instance. + public virtual IFlurlClient Get(Url url) { + return _clients.AddOrUpdate( + GetCacheKey(url), + u => Create(u), + (u, client) => client.IsDisposed ? Create(u) : client); + } + + /// + /// Defines a strategy for getting a cache key based on a Url. Default implementation + /// returns the host part (i.e www.api.com) so that all calls to the same host use the + /// same FlurlClient (and HttpClient/HttpMessageHandler) instance. + /// + /// The URL. + /// The cache key + protected abstract string GetCacheKey(Url url); + + /// + /// Creates a new FlurlClient + /// + /// The URL (not used) + /// + protected virtual IFlurlClient Create(Url url) => new FlurlClient(); + } +} diff --git a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs index 68910ca7..ea4381b6 100644 --- a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs +++ b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs @@ -1,117 +1,244 @@ using System; -using System.Net.Http; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; using System.Threading.Tasks; +using Flurl.Http.Testing; namespace Flurl.Http.Configuration { /// - /// A set of properties that affect Flurl.Http behavior. Generally set via static FlurlHttp.Configure method. + /// A set of properties that affect Flurl.Http behavior /// public class FlurlHttpSettings { + // We need to maintain order of precedence (request > client > global) in some tricky scenarios. + // e.g. if we explicitly set some FlurlRequest.Settings, then set the FlurlClient, we want the + // client-level settings to override the global settings but not the request-level settings. + private FlurlHttpSettings _defaults; + + // Values are dictionary-backed so we can check for key existence. Can't do null-coalescing + // because if a setting is set to null at the request level, that should stick. + private readonly IDictionary _vals = new Dictionary(); + /// - /// Initializes a new instance of the class. + /// Creates a new FlurlHttpSettings object using another FlurlHttpSettings object as its default values. /// - public FlurlHttpSettings() { - ResetDefaults(); + public FlurlHttpSettings(FlurlHttpSettings defaults) { + _defaults = defaults; } /// - /// Gets or sets value indicating whether to automatically dispose underlying HttpClient immediately after each call. + /// Creates a new FlurlHttpSettings object. /// - public bool AutoDispose { get; set; } + public FlurlHttpSettings() : this(FlurlHttp.GlobalSettings) { } /// - /// Gets or sets the default timeout for every HTTP request. + /// Gets or sets the HTTP request timeout. /// - public TimeSpan DefaultTimeout { get; set; } + public TimeSpan? Timeout { + get => Get(() => Timeout); + set => Set(() => Timeout, value); + } /// /// Gets or sets a pattern representing a range of HTTP status codes which (in addtion to 2xx) will NOT result in Flurl.Http throwing an Exception. /// Examples: "3xx", "100,300,600", "100-299,6xx", "*" (allow everything) /// 2xx will never throw regardless of this setting. /// - public string AllowedHttpStatusRange { get; set; } + public string AllowedHttpStatusRange { + get => Get(() => AllowedHttpStatusRange); + set => Set(() => AllowedHttpStatusRange, value); + } /// /// Gets or sets a value indicating whether cookies should be sent/received with each HTTP request. /// - public bool CookiesEnabled { get; set; } - - /// - /// Gets or sets a factory used to create HttpClient object used in Flurl HTTP calls. Default value - /// is an instance of DefaultHttpClientFactory. Custom factory implementations should generally - /// inherit from DefaultHttpClientFactory, call base.CreateClient, and manipulate the returned HttpClient, - /// otherwise functionality such as callbacks and most testing features will be lost. - /// - public IHttpClientFactory HttpClientFactory { get; set; } + public bool CookiesEnabled { + get => Get(() => CookiesEnabled); + set => Set(() => CookiesEnabled, value); + } /// /// Gets or sets object used to serialize and deserialize JSON. Default implementation uses Newtonsoft Json.NET. /// - public ISerializer JsonSerializer { get; set; } + public ISerializer JsonSerializer { + get => Get(() => JsonSerializer); + set => Set(() => JsonSerializer, value); + } /// /// Gets or sets object used to serialize URL-encoded data. (Deserialization not supported in default implementation.) /// - public ISerializer UrlEncodedSerializer { get; set; } + public ISerializer UrlEncodedSerializer { + get => Get(() => UrlEncodedSerializer); + set => Set(() => UrlEncodedSerializer, value); + } /// /// Gets or sets a callback that is called immediately before every HTTP request is sent. /// - public Action BeforeCall { get; set; } + public Action BeforeCall { + get => Get(() => BeforeCall); + set => Set(() => BeforeCall, value); + } /// /// Gets or sets a callback that is asynchronously called immediately before every HTTP request is sent. /// - public Func BeforeCallAsync { get; set; } + public Func BeforeCallAsync { + get => Get(() => BeforeCallAsync); + set => Set(() => BeforeCallAsync, value); + } /// /// Gets or sets a callback that is called immediately after every HTTP response is received. /// - public Action AfterCall { get; set; } + public Action AfterCall { + get => Get(() => AfterCall); + set => Set(() => AfterCall, value); + } /// /// Gets or sets a callback that is asynchronously called immediately after every HTTP response is received. /// - public Func AfterCallAsync { get; set; } + public Func AfterCallAsync { + get => Get(() => AfterCallAsync); + set => Set(() => AfterCallAsync, value); + } /// /// Gets or sets a callback that is called when an error occurs during any HTTP call, including when any non-success /// HTTP status code is returned in the response. Response should be null-checked if used in the event handler. /// - public Action OnError { get; set; } + public Action OnError { + get => Get(() => OnError); + set => Set(() => OnError, value); + } /// /// Gets or sets a callback that is asynchronously called when an error occurs during any HTTP call, including when any non-success /// HTTP status code is returned in the response. Response should be null-checked if used in the event handler. /// - public Func OnErrorAsync { get; set; } + public Func OnErrorAsync { + get => Get(() => OnErrorAsync); + set => Set(() => OnErrorAsync, value); + } /// - /// Clear all custom global options and set default values. + /// Resets all overridden settings to their default values. For example, on a FlurlRequest, + /// all settings are reset to FlurlClient-level settings. /// - public void ResetDefaults() { - AutoDispose = false; - DefaultTimeout = new HttpClient().Timeout; - AllowedHttpStatusRange = null; - CookiesEnabled = false; - HttpClientFactory = new DefaultHttpClientFactory(); + public virtual void ResetDefaults() { + _vals.Clear(); + } + + /// + /// Gets a settings value from this instance if explicitly set, otherwise from the default settings that back this instance. + /// + protected T Get(Expression> property) { + var p = (property.Body as MemberExpression).Member as PropertyInfo; + var testVals = HttpTest.Current?.Settings._vals; + return + testVals?.ContainsKey(p.Name) == true ? (T)testVals[p.Name] : + _vals.ContainsKey(p.Name) ? (T)_vals[p.Name] : + _defaults != null ? (T)p.GetValue(_defaults) : + default(T); + } + + /// + /// Sets a settings value for this instance. + /// + protected void Set(Expression> property, T value) { + var p = (property.Body as MemberExpression).Member as PropertyInfo; + _vals[p.Name] = value; + } + + /// + /// Merges other settings with this one. Overrides defaults, but does NOT override + /// this settings' explicitly set values. + /// + /// The settings to merge. + public FlurlHttpSettings Merge(FlurlHttpSettings other) { + _defaults = other; + return this; + } + } + + /// + /// Client-level settings for Flurl.Http + /// + public class ClientFlurlHttpSettings : FlurlHttpSettings + { + /// + /// Creates a new FlurlHttpSettings object using another FlurlHttpSettings object as its default values. + /// + public ClientFlurlHttpSettings(FlurlHttpSettings defaults) : base(defaults) { } + + /// + /// Specifies the time to keep the underlying HTTP/TCP conneciton open. When expired, a Connection: close header + /// is sent with the next request, which should force a new connection and DSN lookup to occur on the next call. + /// Default is null, effectively disabling the behavior. + /// + public TimeSpan? ConnectionLeaseTimeout { + get => Get(() => ConnectionLeaseTimeout); + set => Set(() => ConnectionLeaseTimeout, value); + } + + /// + /// Gets or sets a factory used to create the HttpClient and HttpMessageHandler used for HTTP calls. + /// Whenever possible, custom factory implementations should inherit from DefaultHttpClientFactory, + /// only override the method(s) needed, call the base method, and modify the result. + /// + public IHttpClientFactory HttpClientFactory { + get => Get(() => HttpClientFactory); + set => Set(() => HttpClientFactory, value); + } + } + + /// + /// Global default settings for Flurl.Http + /// + public class GlobalFlurlHttpSettings : ClientFlurlHttpSettings + { + internal GlobalFlurlHttpSettings() : base(null) { + ResetDefaults(); + } + + /// + /// Gets or sets the factory that defines creating, caching, and reusing FlurlClient instances and, + /// by proxy, HttpClient instances. + /// + public IFlurlClientFactory FlurlClientFactory { + get => Get(() => FlurlClientFactory); + set => Set(() => FlurlClientFactory, value); + } + + /// + /// Resets all global settings to their Flurl.Http-defined default values. + /// + public override void ResetDefaults() { + base.ResetDefaults(); + Timeout = TimeSpan.FromSeconds(100); // same as HttpClient JsonSerializer = new NewtonsoftJsonSerializer(null); UrlEncodedSerializer = new DefaultUrlEncodedSerializer(); - BeforeCall = null; - BeforeCallAsync = null; - AfterCall = null; - AfterCallAsync = null; - OnError = null; - OnErrorAsync = null; + FlurlClientFactory = new PerHostFlurlClientFactory(); + HttpClientFactory = new DefaultHttpClientFactory(); } + } + /// + /// Settings overrides within the context of an HttpTest + /// + public class TestFlurlHttpSettings : GlobalFlurlHttpSettings + { /// - /// Clones this instance. + /// Resets all test settings to their Flurl.Http-defined default values. /// - public FlurlHttpSettings Clone() { - return (FlurlHttpSettings)MemberwiseClone(); + public override void ResetDefaults() { + base.ResetDefaults(); + FlurlClientFactory = new TestFlurlClientFactory(); + HttpClientFactory = new TestHttpClientFactory(); } } -} \ No newline at end of file +} diff --git a/src/Flurl.Http/Configuration/FlurlMessageHandler.cs b/src/Flurl.Http/Configuration/FlurlMessageHandler.cs deleted file mode 100644 index 4aacf7dc..00000000 --- a/src/Flurl.Http/Configuration/FlurlMessageHandler.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Flurl.Http.Content; - -namespace Flurl.Http.Configuration -{ - /// - /// HTTP message handler used by default in all Flurl-created HttpClients. - /// - public class FlurlMessageHandler : DelegatingHandler - { - /// - /// Initializes a new instance of the class. - /// - public FlurlMessageHandler() { } - - /// - /// Initializes a new instance of the class with a specific inner handler. - /// - /// The inner handler. - public FlurlMessageHandler(HttpMessageHandler innerHandler) : base(innerHandler) { } - - /// - /// Send request asynchronous. - /// - /// The request. - /// The cancellation token. - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - var call = HttpCall.Get(request); - - await FlurlHttp.RaiseEventAsync(request, FlurlEventType.BeforeCall).ConfigureAwait(false); - call.StartedUtc = DateTime.UtcNow; - try { - call.Response = await InnerSendAsync(call, request, cancellationToken).ConfigureAwait(false); - call.Response.RequestMessage = request; - if (call.Succeeded) - return call.Response; - - if (call.Response.Content != null) - call.ErrorResponseBody = await call.Response.Content.StripCharsetQuotes().ReadAsStringAsync().ConfigureAwait(false); - throw new FlurlHttpException(call, null); - } - catch (Exception ex) { - call.Exception = ex; - await FlurlHttp.RaiseEventAsync(request, FlurlEventType.OnError).ConfigureAwait(false); - throw; - } - finally { - call.EndedUtc = DateTime.UtcNow; - await FlurlHttp.RaiseEventAsync(request, FlurlEventType.AfterCall).ConfigureAwait(false); - } - } - - private async Task InnerSendAsync(HttpCall call, HttpRequestMessage request, CancellationToken cancellationToken) { - try { - return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested) { - throw new FlurlHttpTimeoutException(call, ex); - } - catch (Exception ex) { - throw new FlurlHttpException(call, ex); - } - } - } -} \ No newline at end of file diff --git a/src/Flurl.Http/Configuration/IFlurlClientFactory.cs b/src/Flurl.Http/Configuration/IFlurlClientFactory.cs new file mode 100644 index 00000000..b702edb2 --- /dev/null +++ b/src/Flurl.Http/Configuration/IFlurlClientFactory.cs @@ -0,0 +1,16 @@ +namespace Flurl.Http.Configuration +{ + /// + /// Interface for defining a strategy for creating, caching, and reusing IFlurlClient instances and, + /// by proxy, their underlying HttpClient instances. + /// + public interface IFlurlClientFactory + { + /// + /// Strategy to create a FlurlClient or reuse an exisitng one, based on URL being called. + /// + /// The URL being called. + /// + IFlurlClient Get(Url url); + } +} \ No newline at end of file diff --git a/src/Flurl.Http/Configuration/IHttpClientFactory.cs b/src/Flurl.Http/Configuration/IHttpClientFactory.cs index 1a85eeec..fcb68970 100644 --- a/src/Flurl.Http/Configuration/IHttpClientFactory.cs +++ b/src/Flurl.Http/Configuration/IHttpClientFactory.cs @@ -11,15 +11,16 @@ namespace Flurl.Http.Configuration public interface IHttpClientFactory { /// - /// Creates the client. + /// Defines how HttpClient should be instantiated and configured by default. Do NOT attempt + /// to cache/reuse HttpClient instances here - that should be done at the FlurlClient level + /// via a custom FlurlClientFactory that gets registered globally. /// - /// The URL. - /// The handler. + /// The HttpMessageHandler used to construct the HttpClient. /// - HttpClient CreateClient(Url url, HttpMessageHandler handler); - + HttpClient CreateHttpClient(HttpMessageHandler handler); + /// - /// Creates the message handler. + /// Defines how the /// /// HttpMessageHandler CreateMessageHandler(); diff --git a/src/Flurl.Http/Configuration/PerBaseUrlFlurlClientFactory.cs b/src/Flurl.Http/Configuration/PerBaseUrlFlurlClientFactory.cs new file mode 100644 index 00000000..b599b940 --- /dev/null +++ b/src/Flurl.Http/Configuration/PerBaseUrlFlurlClientFactory.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Flurl.Http.Configuration +{ + /// + /// An IFlurlClientFactory implementation that caches and reuses the same IFlurlClient instance + /// per URL requested, which it assumes is a "base" URL, and sets the IFlurlClient.BaseUrl property + /// to that value. Ideal for use with IoC containers - register as a singleton, inject into a service + /// that wraps some web service, and use to set a private IFlurlClient field in the constructor. + /// + public class PerBaseUrlFlurlClientFactory : FlurlClientFactoryBase + { + /// + /// Returns the entire URL, which is assumed to be some "base" URL for a service. + /// + /// The URL. + /// The cache key + protected override string GetCacheKey(Url url) => url.ToString(); + + /// + /// Returns a new new FlurlClient with BaseUrl set to the URL passed. + /// + /// The URL + /// + protected override IFlurlClient Create(Url url) => new FlurlClient(url); + } +} diff --git a/src/Flurl.Http/Configuration/PerHostFlurlClientFactory.cs b/src/Flurl.Http/Configuration/PerHostFlurlClientFactory.cs new file mode 100644 index 00000000..34191fdc --- /dev/null +++ b/src/Flurl.Http/Configuration/PerHostFlurlClientFactory.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Flurl.Http.Configuration +{ + /// + /// An IFlurlClientFactory implementation that caches and reuses the same one instance of + /// FlurlClient per host being called. Maximizes reuse of underlying HttpClient/Handler + /// while allowing things like cookies to be host-specific. This is the default + /// implementation used when calls are made fluently off Urls/strings. + /// + public class PerHostFlurlClientFactory : FlurlClientFactoryBase + { + /// + /// Returns the host part of the URL (i.e. www.api.com) so that all calls to the same + /// host use the same FlurlClient (and HttpClient/HttpMessageHandler) instance. + /// + /// The URL. + /// The cache key + protected override string GetCacheKey(Url url) => new Uri(url).Host; + } +} diff --git a/src/Flurl.Http/Content/CapturedMultipartContent.cs b/src/Flurl.Http/Content/CapturedMultipartContent.cs index a10c758b..cbdb55ca 100644 --- a/src/Flurl.Http/Content/CapturedMultipartContent.cs +++ b/src/Flurl.Http/Content/CapturedMultipartContent.cs @@ -24,17 +24,11 @@ public class CapturedMultipartContent : MultipartContent /// /// Initializes a new instance of the class. /// - /// The FlurlHttpSettings used to serialize each content part. - public CapturedMultipartContent(FlurlHttpSettings settings) : base("form-data") { - _settings = settings; + /// The FlurlHttpSettings used to serialize each content part. (Defaults to FlurlHttp.GlobalSettings.) + public CapturedMultipartContent(FlurlHttpSettings settings = null) : base("form-data") { + _settings = settings ?? FlurlHttp.GlobalSettings; } - /// - /// Initializes a new instance of the class, using FlurlHttp.GlobalSettings - /// to determine how to serialize each content part. - /// - public CapturedMultipartContent() : this(FlurlHttp.GlobalSettings) { } - /// /// Add a content part to the multipart request. /// diff --git a/src/Flurl.Http/CookieExtensions.cs b/src/Flurl.Http/CookieExtensions.cs index 88ec4554..ff99e24f 100644 --- a/src/Flurl.Http/CookieExtensions.cs +++ b/src/Flurl.Http/CookieExtensions.cs @@ -1,151 +1,68 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Net; +using System.Text; +using System.Threading.Tasks; using Flurl.Util; namespace Flurl.Http { /// - /// Cookie extension for Flurl Client. + /// Fluent extension methods for working with HTTP cookies. /// public static class CookieExtensions { - /// - /// Allows cookies to be sent and received in calls made with this client. Not necessary to call when setting cookies via WithCookie/WithCookies. - /// - public static IFlurlClient EnableCookies(this IFlurlClient client) { - client.Settings.CookiesEnabled = true; - return client; - } - - /// - /// Allows cookies to be sent and received in calls made to this Url. Not necessary to call when setting cookies via WithCookie/WithCookies. - /// - public static IFlurlClient EnableCookies(this Url url) { - return new FlurlClient(url).EnableCookies(); - } - /// - /// Allows cookies to be sent and received in calls made to this Url. Not necessary to call when setting cookies via WithCookie/WithCookies. + /// Allows cookies to be sent and received. Not necessary to call when setting cookies via WithCookie/WithCookies. /// - public static IFlurlClient EnableCookies(this string url) { - return new FlurlClient(url).EnableCookies(); + /// The IFlurlClient or IFlurlRequest. + /// This IFlurlClient. + public static T EnableCookies(this T clientOrRequest) where T : IHttpSettingsContainer { + clientOrRequest.Settings.CookiesEnabled = true; + return clientOrRequest; } /// - /// Sets an HTTP cookie to be sent with all requests made with this FlurlClient. + /// Sets an HTTP cookie to be sent with this IFlurlRequest or all requests made with this IFlurlClient. /// - /// The client. + /// The IFlurlClient or IFlurlRequest. /// The cookie to set. - /// The modified FlurlClient. - /// is null. - public static IFlurlClient WithCookie(this IFlurlClient client, Cookie cookie) { - client.Settings.CookiesEnabled = true; - client.Cookies[cookie.Name] = cookie; - return client; - } - - /// - /// Creates a FlurlClient from the URL and sets an HTTP cookie to be sent with all requests made with this FlurlClient. - /// - /// The URL. - /// the cookie to set. - /// The modified FlurlClient. - /// is null. - public static IFlurlClient WithCookie(this string url, Cookie cookie) { - return new FlurlClient(url, true).WithCookie(cookie); + /// This IFlurlClient. + public static T WithCookie(this T clientOrRequest, Cookie cookie) where T : IHttpSettingsContainer { + clientOrRequest.Settings.CookiesEnabled = true; + clientOrRequest.Cookies[cookie.Name] = cookie; + return clientOrRequest; } /// - /// Creates a FlurlClient from the URL and sets an HTTP cookie to be sent with all requests made with this FlurlClient. + /// Sets an HTTP cookie to be sent with this IFlurlRequest or all requests made with this IFlurlClient. /// - /// The URL. - /// the cookie to set. - /// The modified FlurlClient. - /// is null. - public static IFlurlClient WithCookie(this Url url, Cookie cookie) { - return new FlurlClient(url, true).WithCookie(cookie); + /// The IFlurlClient or IFlurlRequest. + /// The cookie name. + /// The cookie value. + /// The cookie expiration (optional). If excluded, cookie only lives for duration of session. + /// This IFlurlClient. + public static T WithCookie(this T clientOrRequest, string name, object value, DateTime? expires = null) where T : IHttpSettingsContainer { + var cookie = new Cookie(name, value?.ToInvariantString()) { Expires = expires ?? DateTime.MinValue }; + return clientOrRequest.WithCookie(cookie); } /// - /// Sets an HTTP cookie to be sent with all requests made with this FlurlClient. + /// Sets HTTP cookies to be sent with this IFlurlRequest or all requests made with this IFlurlClient, based on property names/values of the provided object, or keys/values if object is a dictionary. /// - /// The client. - /// cookie name. - /// cookie value. - /// cookie expiration (optional). If excluded, cookie only lives for duration of session. - /// The modified FlurlClient. - /// is null. - public static IFlurlClient WithCookie(this IFlurlClient client, string name, object value, DateTime? expires = null) { - var cookie = new Cookie(name, value?.ToInvariantString()) { Expires = expires ?? DateTime.MinValue }; - return client.WithCookie(cookie); - } - - /// - /// Creates a FlurlClient from the URL and sets an HTTP cookie to be sent with all requests made with this FlurlClient. - /// - /// The URL. - /// cookie name. - /// cookie value. - /// cookie expiration (optional). If excluded, cookie only lives for duration of session. - /// The modified FlurlClient. - /// is null. - public static IFlurlClient WithCookie(this string url, string name, object value, DateTime? expires = null) { - return new FlurlClient(url, true).WithCookie(name, value, expires); - } - - /// - /// Creates a FlurlClient from the URL and sets an HTTP cookie to be sent with all requests made with this FlurlClient. - /// - /// The URL. - /// cookie name. - /// cookie value. - /// cookie expiration (optional). If excluded, cookie only lives for duration of session. - /// The modified FlurlClient. - /// is null. - public static IFlurlClient WithCookie(this Url url, string name, object value, DateTime? expires = null) { - return new FlurlClient(url, true).WithCookie(name, value, expires); - } - - /// - /// Sets HTTP cookies based on property names/values of the provided object, or keys/values if object is a dictionary, to be sent with all requests made with this FlurlClient. - /// - /// The client. - /// Names/values of HTTP cookies to set. Typically an anonymous object or IDictionary. - /// Expiration for all cookies (optional). If excluded, cookies only live for duration of session. - /// The modified FlurlClient. - /// is null. - public static IFlurlClient WithCookies(this IFlurlClient client, object cookies, DateTime? expires = null) { + /// The IFlurlClient or IFlurlRequest. + /// Names/values of HTTP cookies to set. Typically an anonymous object or IDictionary. + /// Expiration for all cookies (optional). If excluded, cookies only live for duration of session. + /// This IFlurlClient. + public static T WithCookies(this T clientOrRequest, object cookies, DateTime? expires = null) where T : IHttpSettingsContainer { if (cookies == null) - return client; + return clientOrRequest; foreach (var kv in cookies.ToKeyValuePairs()) - client.WithCookie(kv.Key, kv.Value, expires); - - return client; - } - - /// - /// Creates a FlurlClient from the URL and sets HTTP cookies based on property names/values of the provided object, or keys/values if object is a dictionary, to be sent with all requests made with this FlurlClient. - /// - /// The URL. - /// Names/values of HTTP cookies to set. Typically an anonymous object or IDictionary. - /// Expiration for all cookies (optional). If excluded, cookies only live for duration of session. - /// The modified FlurlClient. - /// is null. - public static IFlurlClient WithCookies(this Url url, object cookies, DateTime? expires = null) { - return new FlurlClient(url, true).WithCookies(cookies); - } + clientOrRequest.WithCookie(kv.Key, kv.Value, expires); - /// - /// Creates a FlurlClient from the URL and sets HTTP cookies based on property names/values of the provided object, or keys/values if object is a dictionary, to be sent with all requests made with this FlurlClient. - /// - /// The URL. - /// Names/values of HTTP cookies to set. Typically an anonymous object or IDictionary. - /// Expiration for all cookies (optional). If excluded, cookies only live for duration of session. - /// The modified FlurlClient. - /// is null. - public static IFlurlClient WithCookies(this string url, object cookies, DateTime? expires = null) { - return new FlurlClient(url, true).WithCookies(cookies); + return clientOrRequest; } } -} \ No newline at end of file +} diff --git a/src/Flurl.Http/DownloadExtensions.cs b/src/Flurl.Http/DownloadExtensions.cs index 6be61bb2..1b9259ff 100644 --- a/src/Flurl.Http/DownloadExtensions.cs +++ b/src/Flurl.Http/DownloadExtensions.cs @@ -5,42 +5,31 @@ namespace Flurl.Http { /// - /// Download extensions for the Flurl Client. + /// Fluent extension methods for downloading a file. /// public static class DownloadExtensions { /// /// Asynchronously downloads a file at the specified URL. /// - /// The flurl client. + /// The flurl request. /// Path of local folder where file is to be downloaded. /// Name of local file. If not specified, the source filename (last segment of the URL) is used. /// Buffer size in bytes. Default is 4096. /// A Task whose result is the local path of the downloaded file. - public static async Task DownloadFileAsync(this IFlurlClient client, string localFolderPath, string localFileName = null, int bufferSize = 4096) { + public static async Task DownloadFileAsync(this IFlurlRequest request, string localFolderPath, string localFileName = null, int bufferSize = 4096) { if (localFileName == null) - localFileName = client.Url.Path.Split('/').Last(); + localFileName = request.Url.Path.Split('/').Last(); - // need to temporarily disable autodispose if set, otherwise reading from stream will fail - var autoDispose = client.Settings.AutoDispose; - client.Settings.AutoDispose = false; + var response = await request.SendAsync(HttpMethod.Get, completionOption: HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); - try { - var response = await client.SendAsync(HttpMethod.Get, completionOption: HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); - - // http://codereview.stackexchange.com/a/18679 - using (var httpStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) - using (var filestream = await FileUtil.OpenWriteAsync(localFolderPath, localFileName, bufferSize).ConfigureAwait(false)) { - await httpStream.CopyToAsync(filestream, bufferSize).ConfigureAwait(false); - } - - return FileUtil.CombinePath(localFolderPath, localFileName); - } - finally { - client.Settings.AutoDispose = autoDispose; - if (client.Settings.AutoDispose) - client.Dispose(); + // http://codereview.stackexchange.com/a/18679 + using (var httpStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) + using (var filestream = await FileUtil.OpenWriteAsync(localFolderPath, localFileName, bufferSize).ConfigureAwait(false)) { + await httpStream.CopyToAsync(filestream, bufferSize).ConfigureAwait(false); } + + return FileUtil.CombinePath(localFolderPath, localFileName); } /// @@ -52,7 +41,7 @@ public static async Task DownloadFileAsync(this IFlurlClient client, str /// Buffer size in bytes. Default is 4096. /// A Task whose result is the local path of the downloaded file. public static Task DownloadFileAsync(this string url, string localFolderPath, string localFileName = null, int bufferSize = 4096) { - return new FlurlClient(url, true).DownloadFileAsync(localFolderPath, localFileName, bufferSize); + return new FlurlRequest(url).DownloadFileAsync(localFolderPath, localFileName, bufferSize); } /// @@ -64,7 +53,7 @@ public static Task DownloadFileAsync(this string url, string localFolder /// Buffer size in bytes. Default is 4096. /// A Task whose result is the local path of the downloaded file. public static Task DownloadFileAsync(this Url url, string localFolderPath, string localFileName = null, int bufferSize = 4096) { - return new FlurlClient(url, true).DownloadFileAsync(localFolderPath, localFileName, bufferSize); + return new FlurlRequest(url).DownloadFileAsync(localFolderPath, localFileName, bufferSize); } } } \ No newline at end of file diff --git a/src/Flurl.Http/FileUtil.cs b/src/Flurl.Http/FileUtil.cs index 064af7b1..c565b0b9 100644 --- a/src/Flurl.Http/FileUtil.cs +++ b/src/Flurl.Http/FileUtil.cs @@ -1,12 +1,14 @@ using System.IO; +#if NETSTANDARD1_1 using System.Linq; +#endif using System.Threading.Tasks; namespace Flurl.Http { internal static class FileUtil { -#if PORTABLE +#if NETSTANDARD1_1 internal static string GetFileName(string path) { return path?.Split(PCLStorage.PortablePath.DirectorySeparatorChar).Last(); } diff --git a/src/Flurl.Http/Flurl.Http.csproj b/src/Flurl.Http/Flurl.Http.csproj index 804d572d..35d4b625 100644 --- a/src/Flurl.Http/Flurl.Http.csproj +++ b/src/Flurl.Http/Flurl.Http.csproj @@ -1,54 +1,20 @@  - net45;netstandard1.3;portable-net45+win8+wpa81+wp8 + net45;netstandard1.3;netstandard1.1; True - 1.2.0 + Flurl.Http + 2.0.0 Todd Menier - A fluent, portable, testable HTTP client library that extends Flurl's URL builder chain. + WARNING: 2.0 CONTAINS BREAKING CHANGES - REVIEW RELEASE NOTES CAREFULLY! Flurl.Http is a fluent, portable, testable HTTP client library that extends Flurl's URL builder chain. http://tmenier.github.io/Flurl https://pbs.twimg.com/profile_images/534024476296376320/IuPGZ_bX_400x400.png https://raw.githubusercontent.com/tmenier/Flurl/master/LICENSE https://github.com/tmenier/Flurl.git git - httpclient rest json http fluent portable url uri tdd assert async - - 1.2.0 - Up'd solution to VS2017, up'd Flurl dependency to 2.4.0 - 1.1.3 - Minor fixes (github #175, #182, #186) - 1.1.2 - Minor bug fixes (github #159 & #169), up'd Flurl dependency to 2.3.0 - 1.1.1 - More query param assert variations (github #102), HttpCall.Url now a Flurl.Url instance - 1.1.0 - Parallel testing (github #67), better DI/IoC/mocking support (github #146), assert query params (github #102) - 1.0.3 - .NET Core 1.0.1, fixed assembly references (github #131) - 1.0.2 - Updated Flurl dependency to 2.2.1 https://www.nuget.org/packages/Flurl/2.2.1 - 1.0.1 - Updated Flurl dependency to 2.2 https://www.nuget.org/packages/Flurl/2.2.0 - 1.0.0 - Many updates and new features: https://github.com/tmenier/Flurl/releases/tag/Flurl.Http.1.0.0 - 0.10.1 - DLL version fix (github #90) - 0.10.0 - Lib updates, including Flurl 2.0 which contains breaking changes: https://github.com/tmenier/Flurl/wiki/Release-Notes - 0.9.0 - BREAKING CHANGES: https://github.com/tmenier/Flurl/wiki/Release-Notes - 0.8.0 - .NET Core support (github #61, thx @kroniak) - 0.7.0 - BREAKING CHANGES: https://github.com/tmenier/Flurl/wiki/Release-Notes - 0.6.4 - nuspec fix for Xamarin/non-PCL, AllowHttpStatus overloads with HttpStatusCode enum. - 0.6.3 - Updated Flurl dependency to 1.0.9. - 0.6.2 - Respect Json.NET's JsonConvert.DefaultSettings - 0.6.1 - Fixed possibly dictionary lookup bug (github #34). - 0.6.0 - Added support for cancellation tokens, PATCH, string request bodies. - 0.5.3 - Updated Flurl dependency to 1.0.7. - 0.5.2 - Allowed HTTP status at the individual client/call level, i.e. url.AllowHttpStatus("3xx"), url.AllowAnyHttpStatus() - 0.5.1 - Configure which HTTP statuses won't throw exceptions, i.e. FlurlHttp.Configure(c => c.AllowedHttpStatusRange = "100-299,4xx"); - 0.5.0 - Added deserialization helpers for error responses (FlurlHttpException.FlurlHttpException.GetResponseJson, etc). - 0.4.2 - Updated Flurl dependency to 1.0.6. - 0.4.1 - GitHub #25 - some exceptions not triggering global OnError. - 0.4.0 - Client lifetime management - see http://bit.ly/1zqmuLA. - 0.3.0 - Added support for sending cookies (WithCookie/WithCookies), including breaking change to IHttpClientFactory. - 0.2.5 - Added hook to create HttpClientHandler from custom factory, updated Flurl dependency. - 0.2.4 - Updated Flurl dependency for PCL to 1.0.2. - 0.2.3 - New properties added to HttpCall: Url, Completed, Succeeded, HttpStatus. - 0.2.2 - Updated Flurl dependency for PCL to 1.0.1. - 0.2.1 - Added support for getting streams and byte arrays. - 0.2.0 - Added .NET 4.5 specific version with fewer dependencies. - 0.1.3 - Added support for HEAD requests via HeadAsync (thanks to @benb1n). - - false + httpclient rest json http fluent url uri tdd assert async + See https://github.com/tmenier/Flurl/releases + false @@ -61,40 +27,28 @@ - + - - NETSTANDARD - - - - - + + - - PORTABLE - .NETPortable - v4.5 - Profile259 - .NETPortable,Version=v0.0,Profile=Profile259 - C:\Program Files (x86)\MSBuild\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets + + portable-net45+win8+wp8 - - - - - - - - + + bin\Debug\net45\Flurl.Http.xml + + + + diff --git a/src/Flurl.Http/FlurlClient.cs b/src/Flurl.Http/FlurlClient.cs index fffc2c07..94565bfd 100644 --- a/src/Flurl.Http/FlurlClient.cs +++ b/src/Flurl.Http/FlurlClient.cs @@ -1,267 +1,152 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Net; using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; +using System.Linq; using Flurl.Http.Configuration; using Flurl.Http.Testing; +using Flurl.Util; namespace Flurl.Http { /// /// Interface defining FlurlClient's contract (useful for mocking and DI) /// - public interface IFlurlClient : IDisposable { + public interface IFlurlClient : IHttpSettingsContainer, IDisposable { /// - /// Creates a copy of this FlurlClient with a shared instance of HttpClient and HttpMessageHandler + /// Gets or sets the FlurlHttpSettings object used by this client. /// - IFlurlClient Clone(); + new ClientFlurlHttpSettings Settings { get; set; } /// - /// Gets or sets the FlurlHttpSettings object used by this client. + /// Gets the HttpClient to be used in subsequent HTTP calls. Creation (when necessary) is delegated + /// to FlurlHttp.FlurlClientFactory. Reused for the life of the FlurlClient. /// - FlurlHttpSettings Settings { get; set; } + HttpClient HttpClient { get; } /// - /// Gets or sets the URL to be called. + /// Gets the HttpMessageHandler to be used in subsequent HTTP calls. Creation (when necessary) is delegated + /// to FlurlHttp.FlurlClientFactory. /// - Url Url { get; set; } + HttpMessageHandler HttpMessageHandler { get; } /// - /// Collection of HttpCookies sent and received. + /// Gets or sets base URL associated with this client. /// - IDictionary Cookies { get; } + string BaseUrl { get; set; } /// - /// Gets the HttpClient to be used in subsequent HTTP calls. Creation (when necessary) is delegated - /// to FlurlHttp.HttpClientFactory. Reused for the life of the FlurlClient. + /// Instantiates a new IFlurClient, optionally appending path segments to the BaseUrl. /// - HttpClient HttpClient { get; } + /// The URL or URL segments for the request. If BaseUrl is defined, it is assumed that these are path segments off that base. + /// A new IFlurlRequest + IFlurlRequest Request(params object[] urlSegments); /// - /// Gets the HttpMessageHandler to be used in subsequent HTTP calls. Creation (when necessary) is delegated - /// to FlurlHttp.HttpClientFactory. + /// Checks whether the connection lease timeout (as specified in Settings.ConnectionLeaseTimeout) has passed since + /// connection was opened. If it has, resets the interval and returns true. /// - HttpMessageHandler HttpMessageHandler { get; } + bool CheckAndRenewConnectionLease(); /// - /// Creates and asynchronously sends an HttpRequestMethod, disposing HttpClient if AutoDispose it true. - /// Mainly used to implement higher-level extension methods (GetJsonAsync, etc). + /// Gets a value indicating whether this instance (and its underlying HttpClient) has been disposed. /// - /// The HTTP method used to make the request. - /// Contents of the request body. - /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. - /// The HttpCompletionOption used in the request. Optional. - /// A Task whose result is the received HttpResponseMessage. - Task SendAsync(HttpMethod verb, HttpContent content = null, CancellationToken? cancellationToken = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead); + bool IsDisposed { get; } } /// - /// A chainable wrapper around HttpClient and Flurl.Url. + /// A reusable object for making HTTP calls. /// public class FlurlClient : IFlurlClient { - /// - /// Initializes a new instance of the class. - /// - /// The FlurlHttpSettings associated with this instance. - public FlurlClient(FlurlHttpSettings settings = null) { - Settings = settings ?? FlurlHttp.GlobalSettings.Clone(); - } + private readonly Lazy _httpClient; + private readonly Lazy _httpMessageHandler; /// /// Initializes a new instance of the class. /// - /// Action allowing you to overide default settings inline. - public FlurlClient(Action configure) : this() { - configure(Settings); + /// The base URL associated with this client. + public FlurlClient(string baseUrl = null) { + BaseUrl = baseUrl; + Settings = new ClientFlurlHttpSettings(FlurlHttp.GlobalSettings); + _httpClient = new Lazy(() => Settings.HttpClientFactory.CreateHttpClient(HttpMessageHandler)); + _httpMessageHandler = new Lazy(() => Settings.HttpClientFactory.CreateMessageHandler()); } - /// - /// Initializes a new instance of the class. - /// - /// The URL to call with this FlurlClient instance. - public FlurlClient(Url url) : this() { - Url = url; - } + /// + public string BaseUrl { get; set; } - /// - /// Initializes a new instance of the class. - /// - /// The URL to call with this FlurlClient instance. - public FlurlClient(string url) : this() { - Url = new Url(url); - } + /// + public ClientFlurlHttpSettings Settings { get; set; } - /// - /// Initializes a new instance of the class. - /// - /// The URL to call with this FlurlClient instance. - /// Indicates whether to automatically dispose underlying HttpClient immediately after each call. - public FlurlClient(Url url, bool autoDispose) : this(url) { - Settings.AutoDispose = autoDispose; - } + /// + public IDictionary Headers { get; } = new Dictionary(); - /// - /// Initializes a new instance of the class. - /// - /// The URL to call with this FlurlClient instance. - /// Indicates whether to automatically dispose underlying HttpClient immediately after each call. - public FlurlClient(string url, bool autoDispose) : this(url) { - Settings.AutoDispose = autoDispose; - } + /// + public IDictionary Cookies { get; } = new Dictionary(); - /// - /// Creates a copy of this FlurlClient with a shared instance of HttpClient and HttpMessageHandler - /// - public IFlurlClient Clone() { - return new FlurlClient { - _httpClient = _httpClient, - _httpMessageHandler = _httpMessageHandler, - _parent = this, - Settings = Settings, - Url = Url, - Cookies = Cookies - }; - } + /// + public HttpClient HttpClient => HttpTest.Current?.HttpClient ?? _httpClient.Value; - private HttpClient _httpClient; - private HttpMessageHandler _httpMessageHandler; - private FlurlClient _parent; + /// + public HttpMessageHandler HttpMessageHandler => HttpTest.Current?.HttpMessageHandler ?? _httpMessageHandler.Value; - /// - /// Gets or sets the FlurlHttpSettings object used by this client. - /// - public FlurlHttpSettings Settings { get; set; } - - /// - /// Gets or sets the URL to be called. - /// - public Url Url { get; set; } + /// + public IFlurlRequest Request(params object[] urlSegments) { + var parts = new List(urlSegments.Select(s => s.ToInvariantString())); + if (!Url.IsValid(parts.FirstOrDefault()) && !string.IsNullOrEmpty(BaseUrl)) + parts.Insert(0, BaseUrl); - /// - /// Collection of HttpCookies sent and received. - /// - public IDictionary Cookies { get; private set; } = new Dictionary(); - - /// - /// Gets the HttpClient to be used in subsequent HTTP calls. Creation (when necessary) is delegated - /// to FlurlHttp.HttpClientFactory. Reused for the life of the FlurlClient. - /// - public HttpClient HttpClient => EnsureHttpClient(); + if (!parts.Any()) + throw new ArgumentException("Cannot create a Request. BaseUrl is not defined and no segments were passed."); + if (!Url.IsValid(parts[0])) + throw new ArgumentException("Cannot create a Request. Neither BaseUrl nor the first segment passed is a valid URL."); - /// - /// Gets the HttpMessageHandler to be used in subsequent HTTP calls. Creation (when necessary) is delegated - /// to FlurlHttp.HttpClientFactory. - /// - public HttpMessageHandler HttpMessageHandler => EnsureHttpMessageHandler(); + return new FlurlRequest(Url.Combine(parts.ToArray())).WithClient(this); + } - private HttpClient EnsureHttpClient(HttpClient hc = null) { - if (_httpClient == null) { - if (hc == null) { - hc = Settings.HttpClientFactory.CreateClient(Url, HttpMessageHandler); - hc.Timeout = Settings.DefaultTimeout; - } - _httpClient = hc; - _parent?.EnsureHttpClient(hc); - } - return _httpClient; + FlurlHttpSettings IHttpSettingsContainer.Settings { + get => Settings; + set => Settings = value as ClientFlurlHttpSettings; } - private HttpMessageHandler EnsureHttpMessageHandler(HttpMessageHandler handler = null) { - if (_httpMessageHandler == null) { - if (handler == null) { - handler = (HttpTest.Current == null) ? - Settings.HttpClientFactory.CreateMessageHandler() : - new FakeHttpMessageHandler(); + private Lazy _connectionLeaseStart = new Lazy(() => DateTime.UtcNow); + private readonly object _connectionLeaseLock = new object(); + + private bool IsConnectionLeaseExpired => + Settings.ConnectionLeaseTimeout.HasValue && + DateTime.UtcNow - _connectionLeaseStart.Value > Settings.ConnectionLeaseTimeout; + + /// + public bool CheckAndRenewConnectionLease() { + // do double-check locking to avoid lock overhead most of the time + if (IsConnectionLeaseExpired) { + lock (_connectionLeaseLock) { + if (IsConnectionLeaseExpired) { + _connectionLeaseStart = new Lazy(() => DateTime.UtcNow); + return true; + } } - _httpMessageHandler = handler; - _parent?.EnsureHttpMessageHandler(handler); } - return _httpMessageHandler; + return false; } + /// + public bool IsDisposed { get; private set; } + /// - /// Creates and asynchronously sends an HttpRequestMethod, disposing HttpClient if AutoDispose it true. - /// Mainly used to implement higher-level extension methods (GetJsonAsync, etc). + /// Disposes the underlying HttpClient and HttpMessageHandler. /// - /// The HTTP method used to make the request. - /// Contents of the request body. - /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. - /// The HttpCompletionOption used in the request. Optional. - /// A Task whose result is the received HttpResponseMessage. - public async Task SendAsync(HttpMethod verb, HttpContent content = null, CancellationToken? cancellationToken = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - var request = new HttpRequestMessage(verb, Url) { Content = content }; - var call = new HttpCall(request, Settings); + public virtual void Dispose() { + if (IsDisposed) + return; - try { - if (Settings.CookiesEnabled) - WriteRequestCookies(request); - return await HttpClient.SendAsync(request, completionOption, cancellationToken ?? CancellationToken.None).ConfigureAwait(false); - } - catch (Exception) when (call.ExceptionHandled) { - return call.Response; - } - finally { - if (Settings.CookiesEnabled) - ReadResponseCookies(call.Response); - if (Settings.AutoDispose) - Dispose(); - } - } - - private void WriteRequestCookies(HttpRequestMessage request) { - if (!Cookies.Any()) return; - var uri = request.RequestUri; - var cookieHandler = HttpMessageHandler as HttpClientHandler; - - // if the inner handler is an HttpClientHandler (which it usually is), put the cookies in the CookieContainer. - if (cookieHandler != null && cookieHandler.UseCookies) { - if (cookieHandler.CookieContainer == null) - cookieHandler.CookieContainer = new CookieContainer(); - foreach (var cookie in Cookies.Values) - cookieHandler.CookieContainer.Add(uri, cookie); - } - else { - // http://stackoverflow.com/a/15588878/62600 - request.Headers.TryAddWithoutValidation("Cookie", string.Join("; ", Cookies.Values)); - } - } - - private void ReadResponseCookies(HttpResponseMessage response) { - var uri = response.RequestMessage.RequestUri; + if (_httpMessageHandler.IsValueCreated) + _httpMessageHandler.Value.Dispose(); + if (_httpClient.IsValueCreated) + _httpClient.Value.Dispose(); - // if the inner handler is an HttpClientHandler (which it usually is), it's already plucked the - // cookies out of the headers and put them in the CookieContainer. - var jar = (HttpMessageHandler as HttpClientHandler)?.CookieContainer; - if (jar == null) { - // http://stackoverflow.com/a/15588878/62600 - IEnumerable cookieHeaders; - if (!response.Headers.TryGetValues("Set-Cookie", out cookieHeaders)) - return; - - jar = new CookieContainer(); - foreach (string header in cookieHeaders) { - jar.SetCookies(uri, header); - } - } - - foreach (var cookie in jar.GetCookies(uri).Cast()) - Cookies[cookie.Name] = cookie; - } - - /// - /// Disposes the underlying HttpClient and HttpMessageHandler, setting both properties to null. - /// This FlurlClient can still be reused, but those underlying objects will be re-created as needed. Previously set headers, etc, will be lost. - /// - public void Dispose() { - _httpMessageHandler?.Dispose(); - _httpClient?.Dispose(); - _httpMessageHandler = null; - _httpClient = null; - Cookies = new Dictionary(); + IsDisposed = true; } } } \ No newline at end of file diff --git a/src/Flurl.Http/FlurlHttp.cs b/src/Flurl.Http/FlurlHttp.cs index 506c2591..9386fba5 100644 --- a/src/Flurl.Http/FlurlHttp.cs +++ b/src/Flurl.Http/FlurlHttp.cs @@ -1,6 +1,4 @@ using System; -using System.Net.Http; -using System.Threading.Tasks; using Flurl.Http.Configuration; namespace Flurl.Http @@ -12,73 +10,35 @@ public static class FlurlHttp { private static readonly object _configLock = new object(); - private static Lazy _settings = - new Lazy(() => new FlurlHttpSettings()); + private static Lazy _settings = + new Lazy(() => new GlobalFlurlHttpSettings()); /// /// Globally configured Flurl.Http settings. Should normally be written to by calling FlurlHttp.Configure once application at startup. /// - public static FlurlHttpSettings GlobalSettings { - get { return _settings.Value; } - } + public static GlobalFlurlHttpSettings GlobalSettings => _settings.Value; /// /// Provides thread-safe access to Flurl.Http's global configuration settings. Should only be called once at application startup. /// - /// - /// A delegate callback throws an exception. - public static void Configure(Action configAction) { + /// the action to perform against the GlobalSettings + public static void Configure(Action configAction) { lock (_configLock) { configAction(GlobalSettings); } } /// - /// Triggers the specified sync and async event handlers, usually defined on + /// Provides thread-safe access to the Settings associated with a specific IFlurlClient. The URL is used to find the client, + /// but keep in mind that the same client will be used in all calls to the same host by default. /// - public static Task RaiseEventAsync(HttpRequestMessage request, FlurlEventType eventType) { - var call = HttpCall.Get(request); - var settings = call?.Settings; - - if (settings == null) - return NoOpTask.Instance; - - switch (eventType) { - case FlurlEventType.BeforeCall: - return HandleEventAsync(settings.BeforeCall, settings.BeforeCallAsync, call); - case FlurlEventType.AfterCall: - return HandleEventAsync(settings.AfterCall, settings.AfterCallAsync, call); - case FlurlEventType.OnError: - return HandleEventAsync(settings.OnError, settings.OnErrorAsync, call); - default: - return NoOpTask.Instance; + /// the URL used to find the IFlurlClient + /// the action to perform against the IFlurlClient's Settings + public static void ConfigureClient(string url, Action configAction) { + var client = GlobalSettings.FlurlClientFactory.Get(url); + lock (_configLock) { + configAction(client.Settings); } } - - private static Task HandleEventAsync(Action syncHandler, Func asyncHandler, HttpCall call) { - if (syncHandler != null) - syncHandler(call); - if (asyncHandler != null) - return asyncHandler(call); - return NoOpTask.Instance; - } - } - - /// - /// Flurl event types/ - /// - public enum FlurlEventType { - /// - /// The before call - /// - BeforeCall, - /// - /// The after call - /// - AfterCall, - /// - /// The on error - /// - OnError } } \ No newline at end of file diff --git a/src/Flurl.Http/FlurlHttpException.cs b/src/Flurl.Http/FlurlHttpException.cs index ccc67a9a..6b19c251 100644 --- a/src/Flurl.Http/FlurlHttpException.cs +++ b/src/Flurl.Http/FlurlHttpException.cs @@ -1,5 +1,6 @@ using System; using System.Dynamic; +using System.Text; namespace Flurl.Http { @@ -38,18 +39,22 @@ public FlurlHttpException(HttpCall call, Exception inner) : this(call, BuildMess public FlurlHttpException(HttpCall call) : this(call, BuildMessage(call, null), null) { } private static string BuildMessage(HttpCall call, Exception inner) { - if (call.Response != null && !call.Succeeded) { - return string.Format("Request to {0} failed with status code {1} ({2}).", - call.Request.RequestUri.AbsoluteUri, - (int)call.Response.StatusCode, - call.Response.ReasonPhrase); - } - if (inner != null) { - return $"Request to {call.Request.RequestUri.AbsoluteUri} failed. {inner.Message}"; - } + var sb = new StringBuilder(); - // in theory we should never get here. - return $"Request to {call.Request.RequestUri.AbsoluteUri} failed."; + if (call.Response != null && !call.Succeeded) + sb.AppendLine($"{call} failed with status code {(int)call.Response.StatusCode} ({call.Response.ReasonPhrase})."); + else if (inner != null) + sb.AppendLine($"{call} failed. {inner.Message}"); + else // in theory we should never get here. + sb.AppendLine($"{call} failed."); + + if (!string.IsNullOrWhiteSpace(call.RequestBody)) + sb.AppendLine("Request body:").AppendLine(call.RequestBody); + + if (!string.IsNullOrWhiteSpace(call.ErrorResponseBody)) + sb.AppendLine("Response body:").AppendLine(call.ErrorResponseBody); + + return sb.ToString().Trim(); } /// @@ -67,8 +72,8 @@ public string GetResponseString() { public T GetResponseJson() { return Call?.ErrorResponseBody == null ? default(T) : - Call.Settings?.JsonSerializer == null ? default(T) : - Call.Settings.JsonSerializer.Deserialize(Call.ErrorResponseBody); + Call?.FlurlRequest?.Settings?.JsonSerializer == null ? default(T) : + Call.FlurlRequest.Settings.JsonSerializer.Deserialize(Call.ErrorResponseBody); } /// @@ -93,7 +98,7 @@ public class FlurlHttpTimeoutException : FlurlHttpException public FlurlHttpTimeoutException(HttpCall call, Exception inner) : base(call, BuildMessage(call), inner) { } private static string BuildMessage(HttpCall call) { - return $"Request to {call} timed out."; + return $"{call} timed out."; } } } \ No newline at end of file diff --git a/src/Flurl.Http/FlurlRequest.cs b/src/Flurl.Http/FlurlRequest.cs new file mode 100644 index 00000000..5cbbd896 --- /dev/null +++ b/src/Flurl.Http/FlurlRequest.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Flurl.Http.Configuration; +using Flurl.Util; + +namespace Flurl.Http +{ + /// + /// Interface defining FlurlRequest's contract (useful for mocking and DI) + /// + public interface IFlurlRequest : IHttpSettingsContainer + { + /// + /// Gets or sets the IFlurlClient to use when sending the request. + /// + IFlurlClient Client { get; set; } + + /// + /// Gets or sets the URL to be called. + /// + Url Url { get; set; } + + /// + /// Creates and asynchronously sends an HttpRequestMethod. + /// Mainly used to implement higher-level extension methods (GetJsonAsync, etc). + /// + /// The HTTP method used to make the request. + /// Contents of the request body. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. + /// The HttpCompletionOption used in the request. Optional. + /// A Task whose result is the received HttpResponseMessage. + Task SendAsync(HttpMethod verb, HttpContent content = null, CancellationToken? cancellationToken = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead); + } + + /// + /// A chainable wrapper around HttpClient and Flurl.Url. + /// + public class FlurlRequest : IFlurlRequest + { + private IFlurlClient _client; + + /// + /// Initializes a new instance of the class. + /// + /// The URL to call with this FlurlRequest instance. + public FlurlRequest(Url url = null) { + Settings = new FlurlHttpSettings(); + Url = url; + } + + /// + /// Gets or sets the FlurlHttpSettings used by this request. + /// + public FlurlHttpSettings Settings { get; set; } + + /// + /// Gets or sets the IFlurlClient to use when sending the request. + /// + public IFlurlClient Client { + get { + if (_client == null) { + _client = FlurlHttp.GlobalSettings.FlurlClientFactory.Get(Url); + Settings.Merge(_client.Settings); + } + return _client; + } + set { + _client = value; + Settings.Merge(_client?.Settings ?? FlurlHttp.GlobalSettings); + } + } + + /// + /// Gets or sets the URL to be called. + /// + public Url Url { get; set; } + + /// + /// Collection of headers sent on this request. + /// + public IDictionary Headers { get; } = new Dictionary(); + + /// + /// Collection of HttpCookies sent and received by the IFlurlClient associated with this request. + /// + public IDictionary Cookies => Client.Cookies; + + /// + /// Creates and asynchronously sends an HttpRequestMessage. + /// Mainly used to implement higher-level extension methods (GetJsonAsync, etc). + /// + /// The HTTP method used to make the request. + /// Contents of the request body. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. + /// The HttpCompletionOption used in the request. Optional. + /// A Task whose result is the received HttpResponseMessage. + public async Task SendAsync(HttpMethod verb, HttpContent content = null, CancellationToken? cancellationToken = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { + var request = new HttpRequestMessage(verb, Url) { Content = content }; + var call = new HttpCall(this, request); + + await HandleEventAsync(Settings.BeforeCall, Settings.BeforeCallAsync, call).ConfigureAwait(false); + request.RequestUri = new Uri(Url); // in case it was modifed in the handler above + + var userToken = cancellationToken ?? CancellationToken.None; + var token = userToken; + + if (Settings.Timeout.HasValue) { + var cts = CancellationTokenSource.CreateLinkedTokenSource(userToken); + cts.CancelAfter(Settings.Timeout.Value); + token = cts.Token; + } + + call.StartedUtc = DateTime.UtcNow; + try { + WriteHeaders(request); + if (Settings.CookiesEnabled) + WriteRequestCookies(request); + + if (Client.CheckAndRenewConnectionLease()) + request.Headers.ConnectionClose = true; + + call.Response = await Client.HttpClient.SendAsync(request, completionOption, token).ConfigureAwait(false); + call.Response.RequestMessage = request; + + if (call.Succeeded) + return call.Response; + + // response content is only awaited here if the call failed. + if (call.Response.Content != null) + call.ErrorResponseBody = await call.Response.Content.StripCharsetQuotes().ReadAsStringAsync().ConfigureAwait(false); + + throw new FlurlHttpException(call, null); + } + catch (Exception ex) { + call.Exception = ex; + await HandleEventAsync(Settings.OnError, Settings.OnErrorAsync, call).ConfigureAwait(false); + + if (call.ExceptionHandled) + return call.Response; + + if (ex is OperationCanceledException && !userToken.IsCancellationRequested) + throw new FlurlHttpTimeoutException(call, ex); + + if (ex is FlurlHttpException) + throw; + + throw new FlurlHttpException(call, ex); + } + finally { + request.Dispose(); + if (Settings.CookiesEnabled) + ReadResponseCookies(call.Response); + + call.EndedUtc = DateTime.UtcNow; + await HandleEventAsync(Settings.AfterCall, Settings.AfterCallAsync, call).ConfigureAwait(false); + } + } + + private void WriteHeaders(HttpRequestMessage request) { + Headers.Merge(Client.Headers); + foreach (var header in Headers.Where(h => h.Value != null)) + request.Headers.TryAddWithoutValidation(header.Key, header.Value.ToInvariantString()); + } + + private void WriteRequestCookies(HttpRequestMessage request) { + if (!Cookies.Any()) return; + var uri = request.RequestUri; + var cookieHandler = FindHttpClientHandler(Client.HttpMessageHandler); + + // if the handler is an HttpClientHandler (which it usually is), put the cookies in the CookieContainer. + if (cookieHandler != null && cookieHandler.UseCookies) { + if (cookieHandler.CookieContainer == null) + cookieHandler.CookieContainer = new CookieContainer(); + + Cookies.Merge(Client.Cookies); + foreach (var cookie in Cookies.Values) + cookieHandler.CookieContainer.Add(uri, cookie); + } + else { + // http://stackoverflow.com/a/15588878/62600 + request.Headers.TryAddWithoutValidation("Cookie", string.Join("; ", Cookies.Values)); + } + } + + private void ReadResponseCookies(HttpResponseMessage response) { + var uri = response?.RequestMessage?.RequestUri; + if (uri == null) + return; + + // if the handler is an HttpClientHandler (which it usually is), it's already plucked the + // cookies out of the headers and put them in the CookieContainer. + var jar = FindHttpClientHandler(Client.HttpMessageHandler)?.CookieContainer; + if (jar == null) { + // http://stackoverflow.com/a/15588878/62600 + IEnumerable cookieHeaders; + if (!response.Headers.TryGetValues("Set-Cookie", out cookieHeaders)) + return; + + jar = new CookieContainer(); + foreach (string header in cookieHeaders) { + jar.SetCookies(uri, header); + } + } + + foreach (var cookie in jar.GetCookies(uri).Cast()) + Cookies[cookie.Name] = cookie; + } + + private HttpClientHandler FindHttpClientHandler(HttpMessageHandler handler) { + // if it's an HttpClientHandler, return it + var httpClientHandler = handler as HttpClientHandler; + if (httpClientHandler != null) + return httpClientHandler; + + // if it's a DelegatingHandler, check the InnerHandler recursively + var delegatingHandler = handler as DelegatingHandler; + if (delegatingHandler != null) + return FindHttpClientHandler(delegatingHandler.InnerHandler); + + // it's neither + return null; + } + + private static Task HandleEventAsync(Action syncHandler, Func asyncHandler, HttpCall call) { + syncHandler?.Invoke(call); + if (asyncHandler != null) + return asyncHandler(call); + return Task.FromResult(0); + } + } +} \ No newline at end of file diff --git a/src/Flurl.Http/HttpExtensions.cs b/src/Flurl.Http/GeneratedExtensions.cs similarity index 61% rename from src/Flurl.Http/HttpExtensions.cs rename to src/Flurl.Http/GeneratedExtensions.cs index 9ddbc857..3b535c9e 100644 --- a/src/Flurl.Http/HttpExtensions.cs +++ b/src/Flurl.Http/GeneratedExtensions.cs @@ -1,21 +1,23 @@ // This file was auto-generated by Flurl.Http.CodeGen. Do not edit directly. - +using System; using System.Collections.Generic; using System.IO; +using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Flurl.Http.Configuration; using Flurl.Http.Content; namespace Flurl.Http { /// - /// Http extensions for Flurl Client. + /// Auto-generated fluent extension methods on String, Url, and IFlurlRequest. /// - public static class HttpExtensions + public static class GeneratedExtensions { /// - /// Creates a FlurlClient from the URL and sends an asynchronous request. + /// Creates a FlurlRequest from the URL and sends an asynchronous request. /// /// The URL. /// The HTTP method used to make the request. @@ -24,11 +26,11 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task SendAsync(this Url url, HttpMethod verb, HttpContent content, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).SendAsync(verb, content, cancellationToken, completionOption); + return new FlurlRequest(url).SendAsync(verb, content, cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous request. + /// Creates a FlurlRequest from the URL and sends an asynchronous request. /// /// The URL. /// The HTTP method used to make the request. @@ -37,25 +39,25 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task SendAsync(this string url, HttpMethod verb, HttpContent content, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).SendAsync(verb, content, cancellationToken, completionOption); + return new FlurlRequest(url).SendAsync(verb, content, cancellationToken, completionOption); } /// /// Sends an asynchronous request. /// - /// The IFlurlClient instance. + /// The IFlurlRequest instance. /// The HTTP method used to make the request. /// Contents of the request body. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. - public static Task SendJsonAsync(this IFlurlClient client, HttpMethod verb, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - var content = new CapturedJsonContent(client.Settings.JsonSerializer.Serialize(data)); - return client.SendAsync(verb, content: content, cancellationToken: cancellationToken, completionOption: completionOption); + public static Task SendJsonAsync(this IFlurlRequest request, HttpMethod verb, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { + var content = new CapturedJsonContent(request.Settings.JsonSerializer.Serialize(data)); + return request.SendAsync(verb, content: content, cancellationToken: cancellationToken, completionOption: completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous request. + /// Creates a FlurlRequest from the URL and sends an asynchronous request. /// /// The URL. /// The HTTP method used to make the request. @@ -64,11 +66,11 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task SendJsonAsync(this Url url, HttpMethod verb, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).SendJsonAsync(verb, data, cancellationToken, completionOption); + return new FlurlRequest(url).SendJsonAsync(verb, data, cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous request. + /// Creates a FlurlRequest from the URL and sends an asynchronous request. /// /// The URL. /// The HTTP method used to make the request. @@ -77,25 +79,25 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task SendJsonAsync(this string url, HttpMethod verb, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).SendJsonAsync(verb, data, cancellationToken, completionOption); + return new FlurlRequest(url).SendJsonAsync(verb, data, cancellationToken, completionOption); } /// /// Sends an asynchronous request. /// - /// The IFlurlClient instance. + /// The IFlurlRequest instance. /// The HTTP method used to make the request. /// Contents of the request body. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. - public static Task SendStringAsync(this IFlurlClient client, HttpMethod verb, string data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { + public static Task SendStringAsync(this IFlurlRequest request, HttpMethod verb, string data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { var content = new CapturedStringContent(data); - return client.SendAsync(verb, content: content, cancellationToken: cancellationToken, completionOption: completionOption); + return request.SendAsync(verb, content: content, cancellationToken: cancellationToken, completionOption: completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous request. + /// Creates a FlurlRequest from the URL and sends an asynchronous request. /// /// The URL. /// The HTTP method used to make the request. @@ -104,11 +106,11 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task SendStringAsync(this Url url, HttpMethod verb, string data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).SendStringAsync(verb, data, cancellationToken, completionOption); + return new FlurlRequest(url).SendStringAsync(verb, data, cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous request. + /// Creates a FlurlRequest from the URL and sends an asynchronous request. /// /// The URL. /// The HTTP method used to make the request. @@ -117,25 +119,25 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task SendStringAsync(this string url, HttpMethod verb, string data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).SendStringAsync(verb, data, cancellationToken, completionOption); + return new FlurlRequest(url).SendStringAsync(verb, data, cancellationToken, completionOption); } /// /// Sends an asynchronous request. /// - /// The IFlurlClient instance. + /// The IFlurlRequest instance. /// The HTTP method used to make the request. /// Contents of the request body. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. - public static Task SendUrlEncodedAsync(this IFlurlClient client, HttpMethod verb, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - var content = new CapturedUrlEncodedContent(client.Settings.UrlEncodedSerializer.Serialize(data)); - return client.SendAsync(verb, content: content, cancellationToken: cancellationToken, completionOption: completionOption); + public static Task SendUrlEncodedAsync(this IFlurlRequest request, HttpMethod verb, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { + var content = new CapturedUrlEncodedContent(request.Settings.UrlEncodedSerializer.Serialize(data)); + return request.SendAsync(verb, content: content, cancellationToken: cancellationToken, completionOption: completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous request. + /// Creates a FlurlRequest from the URL and sends an asynchronous request. /// /// The URL. /// The HTTP method used to make the request. @@ -144,11 +146,11 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task SendUrlEncodedAsync(this Url url, HttpMethod verb, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).SendUrlEncodedAsync(verb, data, cancellationToken, completionOption); + return new FlurlRequest(url).SendUrlEncodedAsync(verb, data, cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous request. + /// Creates a FlurlRequest from the URL and sends an asynchronous request. /// /// The URL. /// The HTTP method used to make the request. @@ -157,254 +159,254 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task SendUrlEncodedAsync(this string url, HttpMethod verb, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).SendUrlEncodedAsync(verb, data, cancellationToken, completionOption); + return new FlurlRequest(url).SendUrlEncodedAsync(verb, data, cancellationToken, completionOption); } /// /// Sends an asynchronous GET request. /// - /// The IFlurlClient instance. + /// The IFlurlRequest instance. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. - public static Task GetAsync(this IFlurlClient client, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return client.SendAsync(HttpMethod.Get, cancellationToken: cancellationToken, completionOption: completionOption); + public static Task GetAsync(this IFlurlRequest request, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { + return request.SendAsync(HttpMethod.Get, cancellationToken: cancellationToken, completionOption: completionOption); } /// /// Sends an asynchronous GET request. /// - /// The IFlurlClient instance. + /// The IFlurlRequest instance. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the JSON response body deserialized to an object of type T. - public static Task GetJsonAsync(this IFlurlClient client, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return client.SendAsync(HttpMethod.Get, cancellationToken: cancellationToken, completionOption: completionOption).ReceiveJson(); + public static Task GetJsonAsync(this IFlurlRequest request, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { + return request.SendAsync(HttpMethod.Get, cancellationToken: cancellationToken, completionOption: completionOption).ReceiveJson(); } /// /// Sends an asynchronous GET request. /// - /// The IFlurlClient instance. + /// The IFlurlRequest instance. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the JSON response body deserialized to a dynamic. - public static Task GetJsonAsync(this IFlurlClient client, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return client.SendAsync(HttpMethod.Get, cancellationToken: cancellationToken, completionOption: completionOption).ReceiveJson(); + public static Task GetJsonAsync(this IFlurlRequest request, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { + return request.SendAsync(HttpMethod.Get, cancellationToken: cancellationToken, completionOption: completionOption).ReceiveJson(); } /// /// Sends an asynchronous GET request. /// - /// The IFlurlClient instance. + /// The IFlurlRequest instance. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the JSON response body deserialized to a list of dynamics. - public static Task> GetJsonListAsync(this IFlurlClient client, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return client.SendAsync(HttpMethod.Get, cancellationToken: cancellationToken, completionOption: completionOption).ReceiveJsonList(); + public static Task> GetJsonListAsync(this IFlurlRequest request, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { + return request.SendAsync(HttpMethod.Get, cancellationToken: cancellationToken, completionOption: completionOption).ReceiveJsonList(); } /// /// Sends an asynchronous GET request. /// - /// The IFlurlClient instance. + /// The IFlurlRequest instance. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the response body as a string. - public static Task GetStringAsync(this IFlurlClient client, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return client.SendAsync(HttpMethod.Get, cancellationToken: cancellationToken, completionOption: completionOption).ReceiveString(); + public static Task GetStringAsync(this IFlurlRequest request, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { + return request.SendAsync(HttpMethod.Get, cancellationToken: cancellationToken, completionOption: completionOption).ReceiveString(); } /// /// Sends an asynchronous GET request. /// - /// The IFlurlClient instance. + /// The IFlurlRequest instance. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the response body as a Stream. - public static Task GetStreamAsync(this IFlurlClient client, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return client.SendAsync(HttpMethod.Get, cancellationToken: cancellationToken, completionOption: completionOption).ReceiveStream(); + public static Task GetStreamAsync(this IFlurlRequest request, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { + return request.SendAsync(HttpMethod.Get, cancellationToken: cancellationToken, completionOption: completionOption).ReceiveStream(); } /// /// Sends an asynchronous GET request. /// - /// The IFlurlClient instance. + /// The IFlurlRequest instance. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the response body as a byte array. - public static Task GetBytesAsync(this IFlurlClient client, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return client.SendAsync(HttpMethod.Get, cancellationToken: cancellationToken, completionOption: completionOption).ReceiveBytes(); + public static Task GetBytesAsync(this IFlurlRequest request, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { + return request.SendAsync(HttpMethod.Get, cancellationToken: cancellationToken, completionOption: completionOption).ReceiveBytes(); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous GET request. + /// Creates a FlurlRequest from the URL and sends an asynchronous GET request. /// /// The URL. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task GetAsync(this Url url, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).GetAsync(cancellationToken, completionOption); + return new FlurlRequest(url).GetAsync(cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous GET request. + /// Creates a FlurlRequest from the URL and sends an asynchronous GET request. /// /// The URL. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the JSON response body deserialized to an object of type T. public static Task GetJsonAsync(this Url url, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).GetJsonAsync(cancellationToken, completionOption); + return new FlurlRequest(url).GetJsonAsync(cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous GET request. + /// Creates a FlurlRequest from the URL and sends an asynchronous GET request. /// /// The URL. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the JSON response body deserialized to a dynamic. public static Task GetJsonAsync(this Url url, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).GetJsonAsync(cancellationToken, completionOption); + return new FlurlRequest(url).GetJsonAsync(cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous GET request. + /// Creates a FlurlRequest from the URL and sends an asynchronous GET request. /// /// The URL. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the JSON response body deserialized to a list of dynamics. public static Task> GetJsonListAsync(this Url url, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).GetJsonListAsync(cancellationToken, completionOption); + return new FlurlRequest(url).GetJsonListAsync(cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous GET request. + /// Creates a FlurlRequest from the URL and sends an asynchronous GET request. /// /// The URL. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the response body as a string. public static Task GetStringAsync(this Url url, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).GetStringAsync(cancellationToken, completionOption); + return new FlurlRequest(url).GetStringAsync(cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous GET request. + /// Creates a FlurlRequest from the URL and sends an asynchronous GET request. /// /// The URL. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the response body as a Stream. public static Task GetStreamAsync(this Url url, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).GetStreamAsync(cancellationToken, completionOption); + return new FlurlRequest(url).GetStreamAsync(cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous GET request. + /// Creates a FlurlRequest from the URL and sends an asynchronous GET request. /// /// The URL. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the response body as a byte array. public static Task GetBytesAsync(this Url url, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).GetBytesAsync(cancellationToken, completionOption); + return new FlurlRequest(url).GetBytesAsync(cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous GET request. + /// Creates a FlurlRequest from the URL and sends an asynchronous GET request. /// /// The URL. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task GetAsync(this string url, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).GetAsync(cancellationToken, completionOption); + return new FlurlRequest(url).GetAsync(cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous GET request. + /// Creates a FlurlRequest from the URL and sends an asynchronous GET request. /// /// The URL. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the JSON response body deserialized to an object of type T. public static Task GetJsonAsync(this string url, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).GetJsonAsync(cancellationToken, completionOption); + return new FlurlRequest(url).GetJsonAsync(cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous GET request. + /// Creates a FlurlRequest from the URL and sends an asynchronous GET request. /// /// The URL. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the JSON response body deserialized to a dynamic. public static Task GetJsonAsync(this string url, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).GetJsonAsync(cancellationToken, completionOption); + return new FlurlRequest(url).GetJsonAsync(cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous GET request. + /// Creates a FlurlRequest from the URL and sends an asynchronous GET request. /// /// The URL. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the JSON response body deserialized to a list of dynamics. public static Task> GetJsonListAsync(this string url, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).GetJsonListAsync(cancellationToken, completionOption); + return new FlurlRequest(url).GetJsonListAsync(cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous GET request. + /// Creates a FlurlRequest from the URL and sends an asynchronous GET request. /// /// The URL. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the response body as a string. public static Task GetStringAsync(this string url, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).GetStringAsync(cancellationToken, completionOption); + return new FlurlRequest(url).GetStringAsync(cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous GET request. + /// Creates a FlurlRequest from the URL and sends an asynchronous GET request. /// /// The URL. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the response body as a Stream. public static Task GetStreamAsync(this string url, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).GetStreamAsync(cancellationToken, completionOption); + return new FlurlRequest(url).GetStreamAsync(cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous GET request. + /// Creates a FlurlRequest from the URL and sends an asynchronous GET request. /// /// The URL. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the response body as a byte array. public static Task GetBytesAsync(this string url, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).GetBytesAsync(cancellationToken, completionOption); + return new FlurlRequest(url).GetBytesAsync(cancellationToken, completionOption); } /// /// Sends an asynchronous POST request. /// - /// The IFlurlClient instance. + /// The IFlurlRequest instance. /// Contents of the request body. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. - public static Task PostAsync(this IFlurlClient client, HttpContent content, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return client.SendAsync(HttpMethod.Post, content: content, cancellationToken: cancellationToken, completionOption: completionOption); + public static Task PostAsync(this IFlurlRequest request, HttpContent content, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { + return request.SendAsync(HttpMethod.Post, content: content, cancellationToken: cancellationToken, completionOption: completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous POST request. + /// Creates a FlurlRequest from the URL and sends an asynchronous POST request. /// /// The URL. /// Contents of the request body. @@ -412,11 +414,11 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task PostAsync(this Url url, HttpContent content, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).PostAsync(content, cancellationToken, completionOption); + return new FlurlRequest(url).PostAsync(content, cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous POST request. + /// Creates a FlurlRequest from the URL and sends an asynchronous POST request. /// /// The URL. /// Contents of the request body. @@ -424,24 +426,24 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task PostAsync(this string url, HttpContent content, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).PostAsync(content, cancellationToken, completionOption); + return new FlurlRequest(url).PostAsync(content, cancellationToken, completionOption); } /// /// Sends an asynchronous POST request. /// - /// The IFlurlClient instance. + /// The IFlurlRequest instance. /// Contents of the request body. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. - public static Task PostJsonAsync(this IFlurlClient client, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - var content = new CapturedJsonContent(client.Settings.JsonSerializer.Serialize(data)); - return client.SendAsync(HttpMethod.Post, content: content, cancellationToken: cancellationToken, completionOption: completionOption); + public static Task PostJsonAsync(this IFlurlRequest request, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { + var content = new CapturedJsonContent(request.Settings.JsonSerializer.Serialize(data)); + return request.SendAsync(HttpMethod.Post, content: content, cancellationToken: cancellationToken, completionOption: completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous POST request. + /// Creates a FlurlRequest from the URL and sends an asynchronous POST request. /// /// The URL. /// Contents of the request body. @@ -449,11 +451,11 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task PostJsonAsync(this Url url, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).PostJsonAsync(data, cancellationToken, completionOption); + return new FlurlRequest(url).PostJsonAsync(data, cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous POST request. + /// Creates a FlurlRequest from the URL and sends an asynchronous POST request. /// /// The URL. /// Contents of the request body. @@ -461,24 +463,24 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task PostJsonAsync(this string url, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).PostJsonAsync(data, cancellationToken, completionOption); + return new FlurlRequest(url).PostJsonAsync(data, cancellationToken, completionOption); } /// /// Sends an asynchronous POST request. /// - /// The IFlurlClient instance. + /// The IFlurlRequest instance. /// Contents of the request body. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. - public static Task PostStringAsync(this IFlurlClient client, string data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { + public static Task PostStringAsync(this IFlurlRequest request, string data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { var content = new CapturedStringContent(data); - return client.SendAsync(HttpMethod.Post, content: content, cancellationToken: cancellationToken, completionOption: completionOption); + return request.SendAsync(HttpMethod.Post, content: content, cancellationToken: cancellationToken, completionOption: completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous POST request. + /// Creates a FlurlRequest from the URL and sends an asynchronous POST request. /// /// The URL. /// Contents of the request body. @@ -486,11 +488,11 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task PostStringAsync(this Url url, string data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).PostStringAsync(data, cancellationToken, completionOption); + return new FlurlRequest(url).PostStringAsync(data, cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous POST request. + /// Creates a FlurlRequest from the URL and sends an asynchronous POST request. /// /// The URL. /// Contents of the request body. @@ -498,24 +500,24 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task PostStringAsync(this string url, string data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).PostStringAsync(data, cancellationToken, completionOption); + return new FlurlRequest(url).PostStringAsync(data, cancellationToken, completionOption); } /// /// Sends an asynchronous POST request. /// - /// The IFlurlClient instance. + /// The IFlurlRequest instance. /// Contents of the request body. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. - public static Task PostUrlEncodedAsync(this IFlurlClient client, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - var content = new CapturedUrlEncodedContent(client.Settings.UrlEncodedSerializer.Serialize(data)); - return client.SendAsync(HttpMethod.Post, content: content, cancellationToken: cancellationToken, completionOption: completionOption); + public static Task PostUrlEncodedAsync(this IFlurlRequest request, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { + var content = new CapturedUrlEncodedContent(request.Settings.UrlEncodedSerializer.Serialize(data)); + return request.SendAsync(HttpMethod.Post, content: content, cancellationToken: cancellationToken, completionOption: completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous POST request. + /// Creates a FlurlRequest from the URL and sends an asynchronous POST request. /// /// The URL. /// Contents of the request body. @@ -523,11 +525,11 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task PostUrlEncodedAsync(this Url url, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).PostUrlEncodedAsync(data, cancellationToken, completionOption); + return new FlurlRequest(url).PostUrlEncodedAsync(data, cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous POST request. + /// Creates a FlurlRequest from the URL and sends an asynchronous POST request. /// /// The URL. /// Contents of the request body. @@ -535,56 +537,56 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task PostUrlEncodedAsync(this string url, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).PostUrlEncodedAsync(data, cancellationToken, completionOption); + return new FlurlRequest(url).PostUrlEncodedAsync(data, cancellationToken, completionOption); } /// /// Sends an asynchronous HEAD request. /// - /// The IFlurlClient instance. + /// The IFlurlRequest instance. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. - public static Task HeadAsync(this IFlurlClient client, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return client.SendAsync(HttpMethod.Head, cancellationToken: cancellationToken, completionOption: completionOption); + public static Task HeadAsync(this IFlurlRequest request, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { + return request.SendAsync(HttpMethod.Head, cancellationToken: cancellationToken, completionOption: completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous HEAD request. + /// Creates a FlurlRequest from the URL and sends an asynchronous HEAD request. /// /// The URL. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task HeadAsync(this Url url, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).HeadAsync(cancellationToken, completionOption); + return new FlurlRequest(url).HeadAsync(cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous HEAD request. + /// Creates a FlurlRequest from the URL and sends an asynchronous HEAD request. /// /// The URL. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task HeadAsync(this string url, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).HeadAsync(cancellationToken, completionOption); + return new FlurlRequest(url).HeadAsync(cancellationToken, completionOption); } /// /// Sends an asynchronous PUT request. /// - /// The IFlurlClient instance. + /// The IFlurlRequest instance. /// Contents of the request body. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. - public static Task PutAsync(this IFlurlClient client, HttpContent content, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return client.SendAsync(HttpMethod.Put, content: content, cancellationToken: cancellationToken, completionOption: completionOption); + public static Task PutAsync(this IFlurlRequest request, HttpContent content, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { + return request.SendAsync(HttpMethod.Put, content: content, cancellationToken: cancellationToken, completionOption: completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous PUT request. + /// Creates a FlurlRequest from the URL and sends an asynchronous PUT request. /// /// The URL. /// Contents of the request body. @@ -592,11 +594,11 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task PutAsync(this Url url, HttpContent content, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).PutAsync(content, cancellationToken, completionOption); + return new FlurlRequest(url).PutAsync(content, cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous PUT request. + /// Creates a FlurlRequest from the URL and sends an asynchronous PUT request. /// /// The URL. /// Contents of the request body. @@ -604,24 +606,24 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task PutAsync(this string url, HttpContent content, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).PutAsync(content, cancellationToken, completionOption); + return new FlurlRequest(url).PutAsync(content, cancellationToken, completionOption); } /// /// Sends an asynchronous PUT request. /// - /// The IFlurlClient instance. + /// The IFlurlRequest instance. /// Contents of the request body. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. - public static Task PutJsonAsync(this IFlurlClient client, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - var content = new CapturedJsonContent(client.Settings.JsonSerializer.Serialize(data)); - return client.SendAsync(HttpMethod.Put, content: content, cancellationToken: cancellationToken, completionOption: completionOption); + public static Task PutJsonAsync(this IFlurlRequest request, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { + var content = new CapturedJsonContent(request.Settings.JsonSerializer.Serialize(data)); + return request.SendAsync(HttpMethod.Put, content: content, cancellationToken: cancellationToken, completionOption: completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous PUT request. + /// Creates a FlurlRequest from the URL and sends an asynchronous PUT request. /// /// The URL. /// Contents of the request body. @@ -629,11 +631,11 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task PutJsonAsync(this Url url, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).PutJsonAsync(data, cancellationToken, completionOption); + return new FlurlRequest(url).PutJsonAsync(data, cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous PUT request. + /// Creates a FlurlRequest from the URL and sends an asynchronous PUT request. /// /// The URL. /// Contents of the request body. @@ -641,24 +643,24 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task PutJsonAsync(this string url, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).PutJsonAsync(data, cancellationToken, completionOption); + return new FlurlRequest(url).PutJsonAsync(data, cancellationToken, completionOption); } /// /// Sends an asynchronous PUT request. /// - /// The IFlurlClient instance. + /// The IFlurlRequest instance. /// Contents of the request body. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. - public static Task PutStringAsync(this IFlurlClient client, string data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { + public static Task PutStringAsync(this IFlurlRequest request, string data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { var content = new CapturedStringContent(data); - return client.SendAsync(HttpMethod.Put, content: content, cancellationToken: cancellationToken, completionOption: completionOption); + return request.SendAsync(HttpMethod.Put, content: content, cancellationToken: cancellationToken, completionOption: completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous PUT request. + /// Creates a FlurlRequest from the URL and sends an asynchronous PUT request. /// /// The URL. /// Contents of the request body. @@ -666,11 +668,11 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task PutStringAsync(this Url url, string data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).PutStringAsync(data, cancellationToken, completionOption); + return new FlurlRequest(url).PutStringAsync(data, cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous PUT request. + /// Creates a FlurlRequest from the URL and sends an asynchronous PUT request. /// /// The URL. /// Contents of the request body. @@ -678,56 +680,56 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task PutStringAsync(this string url, string data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).PutStringAsync(data, cancellationToken, completionOption); + return new FlurlRequest(url).PutStringAsync(data, cancellationToken, completionOption); } /// /// Sends an asynchronous DELETE request. /// - /// The IFlurlClient instance. + /// The IFlurlRequest instance. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. - public static Task DeleteAsync(this IFlurlClient client, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return client.SendAsync(HttpMethod.Delete, cancellationToken: cancellationToken, completionOption: completionOption); + public static Task DeleteAsync(this IFlurlRequest request, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { + return request.SendAsync(HttpMethod.Delete, cancellationToken: cancellationToken, completionOption: completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous DELETE request. + /// Creates a FlurlRequest from the URL and sends an asynchronous DELETE request. /// /// The URL. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task DeleteAsync(this Url url, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).DeleteAsync(cancellationToken, completionOption); + return new FlurlRequest(url).DeleteAsync(cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous DELETE request. + /// Creates a FlurlRequest from the URL and sends an asynchronous DELETE request. /// /// The URL. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task DeleteAsync(this string url, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).DeleteAsync(cancellationToken, completionOption); + return new FlurlRequest(url).DeleteAsync(cancellationToken, completionOption); } /// /// Sends an asynchronous PATCH request. /// - /// The IFlurlClient instance. + /// The IFlurlRequest instance. /// Contents of the request body. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. - public static Task PatchAsync(this IFlurlClient client, HttpContent content, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return client.SendAsync(new HttpMethod("PATCH"), content: content, cancellationToken: cancellationToken, completionOption: completionOption); + public static Task PatchAsync(this IFlurlRequest request, HttpContent content, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { + return request.SendAsync(new HttpMethod("PATCH"), content: content, cancellationToken: cancellationToken, completionOption: completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous PATCH request. + /// Creates a FlurlRequest from the URL and sends an asynchronous PATCH request. /// /// The URL. /// Contents of the request body. @@ -735,11 +737,11 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task PatchAsync(this Url url, HttpContent content, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).PatchAsync(content, cancellationToken, completionOption); + return new FlurlRequest(url).PatchAsync(content, cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous PATCH request. + /// Creates a FlurlRequest from the URL and sends an asynchronous PATCH request. /// /// The URL. /// Contents of the request body. @@ -747,24 +749,24 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task PatchAsync(this string url, HttpContent content, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).PatchAsync(content, cancellationToken, completionOption); + return new FlurlRequest(url).PatchAsync(content, cancellationToken, completionOption); } /// /// Sends an asynchronous PATCH request. /// - /// The IFlurlClient instance. + /// The IFlurlRequest instance. /// Contents of the request body. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. - public static Task PatchJsonAsync(this IFlurlClient client, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - var content = new CapturedJsonContent(client.Settings.JsonSerializer.Serialize(data)); - return client.SendAsync(new HttpMethod("PATCH"), content: content, cancellationToken: cancellationToken, completionOption: completionOption); + public static Task PatchJsonAsync(this IFlurlRequest request, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { + var content = new CapturedJsonContent(request.Settings.JsonSerializer.Serialize(data)); + return request.SendAsync(new HttpMethod("PATCH"), content: content, cancellationToken: cancellationToken, completionOption: completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous PATCH request. + /// Creates a FlurlRequest from the URL and sends an asynchronous PATCH request. /// /// The URL. /// Contents of the request body. @@ -772,11 +774,11 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task PatchJsonAsync(this Url url, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).PatchJsonAsync(data, cancellationToken, completionOption); + return new FlurlRequest(url).PatchJsonAsync(data, cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous PATCH request. + /// Creates a FlurlRequest from the URL and sends an asynchronous PATCH request. /// /// The URL. /// Contents of the request body. @@ -784,24 +786,24 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task PatchJsonAsync(this string url, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).PatchJsonAsync(data, cancellationToken, completionOption); + return new FlurlRequest(url).PatchJsonAsync(data, cancellationToken, completionOption); } /// /// Sends an asynchronous PATCH request. /// - /// The IFlurlClient instance. + /// The IFlurlRequest instance. /// Contents of the request body. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. - public static Task PatchStringAsync(this IFlurlClient client, string data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { + public static Task PatchStringAsync(this IFlurlRequest request, string data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { var content = new CapturedStringContent(data); - return client.SendAsync(new HttpMethod("PATCH"), content: content, cancellationToken: cancellationToken, completionOption: completionOption); + return request.SendAsync(new HttpMethod("PATCH"), content: content, cancellationToken: cancellationToken, completionOption: completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous PATCH request. + /// Creates a FlurlRequest from the URL and sends an asynchronous PATCH request. /// /// The URL. /// Contents of the request body. @@ -809,11 +811,11 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task PatchStringAsync(this Url url, string data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).PatchStringAsync(data, cancellationToken, completionOption); + return new FlurlRequest(url).PatchStringAsync(data, cancellationToken, completionOption); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous PATCH request. + /// Creates a FlurlRequest from the URL and sends an asynchronous PATCH request. /// /// The URL. /// Contents of the request body. @@ -821,8 +823,268 @@ public static class HttpExtensions /// The HttpCompletionOption used in the request. Optional. /// A Task whose result is the received HttpResponseMessage. public static Task PatchStringAsync(this string url, string data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { - return new FlurlClient(url, false).PatchStringAsync(data, cancellationToken, completionOption); + return new FlurlRequest(url).PatchStringAsync(data, cancellationToken, completionOption); } + /// + /// Creates a new FlurlRequest with the URL and sets a request header. + /// + /// The URL. + /// The header name. + /// The header value. + /// The IFlurlRequest. + public static IFlurlRequest WithHeader(this Url url, string name, object value) { + return new FlurlRequest(url).WithHeader(name, value); + } + /// + /// Creates a new FlurlRequest with the URL and sets request headers based on property names/values of the provided object, or keys/values if object is a dictionary, to be sent + /// + /// The URL. + /// Names/values of HTTP headers to set. Typically an anonymous object or IDictionary. + /// If true, underscores in property names will be replaced by hyphens. Default is true. + /// The IFlurlRequest. + public static IFlurlRequest WithHeaders(this Url url, object headers, bool replaceUnderscoreWithHyphen = true) { + return new FlurlRequest(url).WithHeaders(headers, replaceUnderscoreWithHyphen); + } + /// + /// Creates a new FlurlRequest with the URL and sets the Authorization header according to Basic Authentication protocol. + /// + /// The URL. + /// Username of authenticating user. + /// Password of authenticating user. + /// The IFlurlRequest. + public static IFlurlRequest WithBasicAuth(this Url url, string username, string password) { + return new FlurlRequest(url).WithBasicAuth(username, password); + } + /// + /// Creates a new FlurlRequest with the URL and sets the Authorization header with a bearer token according to OAuth 2.0 specification. + /// + /// The URL. + /// The acquired oAuth bearer token. + /// The IFlurlRequest. + public static IFlurlRequest WithOAuthBearerToken(this Url url, string token) { + return new FlurlRequest(url).WithOAuthBearerToken(token); + } + /// + /// Creates a new FlurlRequest with the URL and allows cookies to be sent and received. Not necessary to call when setting cookies via WithCookie/WithCookies. + /// + /// The URL. + /// The IFlurlRequest. + public static IFlurlRequest EnableCookies(this Url url) { + return new FlurlRequest(url).EnableCookies(); + } + /// + /// Creates a new FlurlRequest with the URL and sets an HTTP cookie to be sent + /// + /// The URL. + /// + /// The IFlurlRequest. + public static IFlurlRequest WithCookie(this Url url, Cookie cookie) { + return new FlurlRequest(url).WithCookie(cookie); + } + /// + /// Creates a new FlurlRequest with the URL and sets an HTTP cookie to be sent. + /// + /// The URL. + /// The cookie name. + /// The cookie value. + /// The cookie expiration (optional). If excluded, cookie only lives for duration of session. + /// The IFlurlRequest. + public static IFlurlRequest WithCookie(this Url url, string name, object value, DateTime? expires = null) { + return new FlurlRequest(url).WithCookie(name, value, expires); + } + /// + /// Creates a new FlurlRequest with the URL and sets HTTP cookies to be sent, based on property names / values of the provided object, or keys / values if object is a dictionary. + /// + /// The URL. + /// Names/values of HTTP cookies to set. Typically an anonymous object or IDictionary. + /// Expiration for all cookies (optional). If excluded, cookies only live for duration of session. + /// The IFlurlRequest. + public static IFlurlRequest WithCookies(this Url url, object cookies, DateTime? expires = null) { + return new FlurlRequest(url).WithCookies(cookies, expires); + } + /// + /// Creates a new FlurlRequest with the URL and allows changing its Settings inline. + /// + /// The URL. + /// A delegate defining the Settings changes. + /// The IFlurlRequest. + public static IFlurlRequest ConfigureRequest(this Url url, Action action) { + return new FlurlRequest(url).ConfigureRequest(action); + } + /// + /// Creates a new FlurlRequest with the URL and sets the request timeout. + /// + /// The URL. + /// Time to wait before the request times out. + /// The IFlurlRequest. + public static IFlurlRequest WithTimeout(this Url url, TimeSpan timespan) { + return new FlurlRequest(url).WithTimeout(timespan); + } + /// + /// Creates a new FlurlRequest with the URL and sets the request timeout. + /// + /// The URL. + /// Seconds to wait before the request times out. + /// The IFlurlRequest. + public static IFlurlRequest WithTimeout(this Url url, int seconds) { + return new FlurlRequest(url).WithTimeout(seconds); + } + /// + /// Creates a new FlurlRequest with the URL and adds a pattern representing an HTTP status code or range of codes which (in addition to 2xx) will NOT result in a FlurlHttpException being thrown. + /// + /// The URL. + /// Examples: "3xx", "100,300,600", "100-299,6xx" + /// The IFlurlRequest. + public static IFlurlRequest AllowHttpStatus(this Url url, string pattern) { + return new FlurlRequest(url).AllowHttpStatus(pattern); + } + /// + /// Creates a new FlurlRequest with the URL and adds an HttpStatusCode which (in addtion to 2xx) will NOT result in a FlurlHttpException being thrown. + /// + /// The URL. + /// The HttpStatusCode(s) to allow. + /// The IFlurlRequest. + public static IFlurlRequest AllowHttpStatus(this Url url, params HttpStatusCode[] statusCodes) { + return new FlurlRequest(url).AllowHttpStatus(statusCodes); + } + /// + /// Creates a new FlurlRequest with the URL and configures it to allow any returned HTTP status without throwing a FlurlHttpException. + /// + /// The URL. + /// The IFlurlRequest. + public static IFlurlRequest AllowAnyHttpStatus(this Url url) { + return new FlurlRequest(url).AllowAnyHttpStatus(); + } + /// + /// Creates a new FlurlRequest with the URL and sets a request header. + /// + /// The URL. + /// The header name. + /// The header value. + /// The IFlurlRequest. + public static IFlurlRequest WithHeader(this string url, string name, object value) { + return new FlurlRequest(url).WithHeader(name, value); + } + /// + /// Creates a new FlurlRequest with the URL and sets request headers based on property names/values of the provided object, or keys/values if object is a dictionary, to be sent + /// + /// The URL. + /// Names/values of HTTP headers to set. Typically an anonymous object or IDictionary. + /// If true, underscores in property names will be replaced by hyphens. Default is true. + /// The IFlurlRequest. + public static IFlurlRequest WithHeaders(this string url, object headers, bool replaceUnderscoreWithHyphen = true) { + return new FlurlRequest(url).WithHeaders(headers, replaceUnderscoreWithHyphen); + } + /// + /// Creates a new FlurlRequest with the URL and sets the Authorization header according to Basic Authentication protocol. + /// + /// The URL. + /// Username of authenticating user. + /// Password of authenticating user. + /// The IFlurlRequest. + public static IFlurlRequest WithBasicAuth(this string url, string username, string password) { + return new FlurlRequest(url).WithBasicAuth(username, password); + } + /// + /// Creates a new FlurlRequest with the URL and sets the Authorization header with a bearer token according to OAuth 2.0 specification. + /// + /// The URL. + /// The acquired oAuth bearer token. + /// The IFlurlRequest. + public static IFlurlRequest WithOAuthBearerToken(this string url, string token) { + return new FlurlRequest(url).WithOAuthBearerToken(token); + } + /// + /// Creates a new FlurlRequest with the URL and allows cookies to be sent and received. Not necessary to call when setting cookies via WithCookie/WithCookies. + /// + /// The URL. + /// The IFlurlRequest. + public static IFlurlRequest EnableCookies(this string url) { + return new FlurlRequest(url).EnableCookies(); + } + /// + /// Creates a new FlurlRequest with the URL and sets an HTTP cookie to be sent + /// + /// The URL. + /// + /// The IFlurlRequest. + public static IFlurlRequest WithCookie(this string url, Cookie cookie) { + return new FlurlRequest(url).WithCookie(cookie); + } + /// + /// Creates a new FlurlRequest with the URL and sets an HTTP cookie to be sent. + /// + /// The URL. + /// The cookie name. + /// The cookie value. + /// The cookie expiration (optional). If excluded, cookie only lives for duration of session. + /// The IFlurlRequest. + public static IFlurlRequest WithCookie(this string url, string name, object value, DateTime? expires = null) { + return new FlurlRequest(url).WithCookie(name, value, expires); + } + /// + /// Creates a new FlurlRequest with the URL and sets HTTP cookies to be sent, based on property names / values of the provided object, or keys / values if object is a dictionary. + /// + /// The URL. + /// Names/values of HTTP cookies to set. Typically an anonymous object or IDictionary. + /// Expiration for all cookies (optional). If excluded, cookies only live for duration of session. + /// The IFlurlRequest. + public static IFlurlRequest WithCookies(this string url, object cookies, DateTime? expires = null) { + return new FlurlRequest(url).WithCookies(cookies, expires); + } + /// + /// Creates a new FlurlRequest with the URL and allows changing its Settings inline. + /// + /// The URL. + /// A delegate defining the Settings changes. + /// The IFlurlRequest. + public static IFlurlRequest ConfigureRequest(this string url, Action action) { + return new FlurlRequest(url).ConfigureRequest(action); + } + /// + /// Creates a new FlurlRequest with the URL and sets the request timeout. + /// + /// The URL. + /// Time to wait before the request times out. + /// The IFlurlRequest. + public static IFlurlRequest WithTimeout(this string url, TimeSpan timespan) { + return new FlurlRequest(url).WithTimeout(timespan); + } + /// + /// Creates a new FlurlRequest with the URL and sets the request timeout. + /// + /// The URL. + /// Seconds to wait before the request times out. + /// The IFlurlRequest. + public static IFlurlRequest WithTimeout(this string url, int seconds) { + return new FlurlRequest(url).WithTimeout(seconds); + } + /// + /// Creates a new FlurlRequest with the URL and adds a pattern representing an HTTP status code or range of codes which (in addition to 2xx) will NOT result in a FlurlHttpException being thrown. + /// + /// The URL. + /// Examples: "3xx", "100,300,600", "100-299,6xx" + /// The IFlurlRequest. + public static IFlurlRequest AllowHttpStatus(this string url, string pattern) { + return new FlurlRequest(url).AllowHttpStatus(pattern); + } + /// + /// Creates a new FlurlRequest with the URL and adds an HttpStatusCode which (in addtion to 2xx) will NOT result in a FlurlHttpException being thrown. + /// + /// The URL. + /// The HttpStatusCode(s) to allow. + /// The IFlurlRequest. + public static IFlurlRequest AllowHttpStatus(this string url, params HttpStatusCode[] statusCodes) { + return new FlurlRequest(url).AllowHttpStatus(statusCodes); + } + /// + /// Creates a new FlurlRequest with the URL and configures it to allow any returned HTTP status without throwing a FlurlHttpException. + /// + /// The URL. + /// The IFlurlRequest. + public static IFlurlRequest AllowAnyHttpStatus(this string url) { + return new FlurlRequest(url).AllowAnyHttpStatus(); + } } } diff --git a/src/Flurl.Http/HeaderExtensions.cs b/src/Flurl.Http/HeaderExtensions.cs new file mode 100644 index 00000000..0d854d52 --- /dev/null +++ b/src/Flurl.Http/HeaderExtensions.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Flurl.Util; + +namespace Flurl.Http +{ + /// + /// Fluent extension methods for working with HTTP request headers. + /// + public static class HeaderExtensions + { + /// + /// Sets an HTTP header to be sent with this IFlurlRequest or all requests made with this IFlurlClient. + /// + /// The IFlurlClient or IFlurlRequest. + /// HTTP header name. + /// HTTP header value. + /// This IFlurlClient or IFlurlRequest. + public static T WithHeader(this T clientOrRequest, string name, object value) where T : IHttpSettingsContainer { + clientOrRequest.Headers[name] = value; + return clientOrRequest; + } + + /// + /// Sets HTTP headers based on property names/values of the provided object, or keys/values if object is a dictionary, to be sent with this IFlurlRequest or all requests made with this IFlurlClient. + /// + /// The IFlurlClient or IFlurlRequest. + /// Names/values of HTTP headers to set. Typically an anonymous object or IDictionary. + /// If true, underscores in property names will be replaced by hyphens. Default is true. + /// This IFlurlClient or IFlurlRequest. + public static T WithHeaders(this T clientOrRequest, object headers, bool replaceUnderscoreWithHyphen = true) where T : IHttpSettingsContainer { + if (headers == null) + return clientOrRequest; + + // underscore replacement only applies when object properties are parsed to kv pairs + replaceUnderscoreWithHyphen = replaceUnderscoreWithHyphen && !(headers is string) && !(headers is IEnumerable); + + foreach (var kv in headers.ToKeyValuePairs()) { + var key = replaceUnderscoreWithHyphen ? kv.Key.Replace("_", "-") : kv.Key; + clientOrRequest.WithHeader(key, kv.Value); + } + + return clientOrRequest; + } + + /// + /// Sets HTTP authorization header according to Basic Authentication protocol to be sent with this IFlurlRequest or all requests made with this IFlurlClient. + /// + /// The IFlurlClient or IFlurlRequest. + /// Username of authenticating user. + /// Password of authenticating user. + /// This IFlurlClient or IFlurlRequest. + public static T WithBasicAuth(this T clientOrRequest, string username, string password) where T : IHttpSettingsContainer { + // http://stackoverflow.com/questions/14627399/setting-authorization-header-of-httpclient + var encodedCreds = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}")); + return clientOrRequest.WithHeader("Authorization", $"Basic {encodedCreds}"); + } + + /// + /// Sets HTTP authorization header with acquired bearer token according to OAuth 2.0 specification to be sent with this IFlurlRequest or all requests made with this IFlurlClient. + /// + /// The IFlurlClient or IFlurlRequest. + /// The acquired bearer token to pass. + /// This IFlurlClient or IFlurlRequest. + public static T WithOAuthBearerToken(this T clientOrRequest, string token) where T : IHttpSettingsContainer { + return clientOrRequest.WithHeader("Authorization", $"Bearer {token}"); + } + } +} diff --git a/src/Flurl.Http/HttpCall.cs b/src/Flurl.Http/HttpCall.cs index 39c01418..3176a1f4 100644 --- a/src/Flurl.Http/HttpCall.cs +++ b/src/Flurl.Http/HttpCall.cs @@ -1,7 +1,6 @@ using System; using System.Net; using System.Net.Http; -using Flurl.Http.Configuration; using Flurl.Http.Content; namespace Flurl.Http @@ -12,15 +11,11 @@ namespace Flurl.Http /// public class HttpCall { - private readonly Lazy _url; - - internal HttpCall(HttpRequestMessage request, FlurlHttpSettings settings) { + internal HttpCall(IFlurlRequest flurlRequest, HttpRequestMessage request) { + FlurlRequest = flurlRequest; Request = request; if (request?.Properties != null) request.Properties["FlurlHttpCall"] = this; - - Settings = settings; - _url = new Lazy(() => new Url(Request.RequestUri.AbsoluteUri)); } internal static HttpCall Get(HttpRequestMessage request) { @@ -31,26 +26,19 @@ internal static HttpCall Get(HttpRequestMessage request) { } /// - /// FlurlHttpSettings used for this call. + /// The IFlurlRequest associated with this call. /// - public FlurlHttpSettings Settings { get; } + public IFlurlRequest FlurlRequest { get; } /// - /// HttpRequestMessage associated with this call. + /// The HttpRequestMessage associated with this call. /// public HttpRequestMessage Request { get; } /// - /// Captured request body. Available only if Request.Content is a Flurl.Http.Content.CapturedStringContent. + /// Captured request body. Available ONLY if Request.Content is a Flurl.Http.Content.CapturedStringContent. /// - public string RequestBody { - get { - var csc = Request.Content as CapturedStringContent; - if (csc == null) - throw new FlurlHttpException(this, "RequestBody is only available when Request.Content derives from Flurl.Http.Content.CapturedStringContent.", null); - return csc.Content; - } - } + public string RequestBody => (Request.Content as CapturedStringContent)?.Content; /// /// HttpResponseMessage associated with the call if the call completed, otherwise null. @@ -83,11 +71,6 @@ public string RequestBody { /// public TimeSpan? Duration => EndedUtc - StartedUtc; - /// - /// The URL being called. - /// - public Url Url => _url.Value; - /// /// True if a response was received, regardless of whether it is an error status. /// @@ -97,7 +80,7 @@ public string RequestBody { /// True if a response with a successful HTTP status was received. /// public bool Succeeded => Completed && - (Response.IsSuccessStatusCode || HttpStatusRangeParser.IsMatch(Settings.AllowedHttpStatusRange, Response.StatusCode)); + (Response.IsSuccessStatusCode || HttpStatusRangeParser.IsMatch(FlurlRequest.Settings.AllowedHttpStatusRange, Response.StatusCode)); /// /// HttpStatusCode of the response if the call completed, otherwise null. @@ -108,5 +91,13 @@ public string RequestBody { /// Body of the HTTP response if unsuccessful, otherwise null. (Successful responses are not captured as strings, mainly for performance reasons.) /// public string ErrorResponseBody { get; set; } + + /// + /// Returns the verb and absolute URI associated with this call. + /// + /// + public override string ToString() { + return $"{Request.Method:U} {FlurlRequest.Url}"; + } } } diff --git a/src/Flurl.Http/HttpResponseMessageExtensions.cs b/src/Flurl.Http/HttpResponseMessageExtensions.cs index a927de77..6c6eb305 100644 --- a/src/Flurl.Http/HttpResponseMessageExtensions.cs +++ b/src/Flurl.Http/HttpResponseMessageExtensions.cs @@ -3,7 +3,7 @@ using System.Dynamic; using System.IO; using System.Net.Http; -#if NETSTANDARD +#if NETSTANDARD1_3 using System.Text; #endif using System.Threading.Tasks; @@ -29,7 +29,7 @@ public static async Task ReceiveJson(this Task respon var call = HttpCall.Get(resp.RequestMessage); try { using (var stream = await resp.Content.ReadAsStreamAsync().ConfigureAwait(false)) - return call.Settings.JsonSerializer.Deserialize(stream); + return call.FlurlRequest.Settings.JsonSerializer.Deserialize(stream); } catch (Exception ex) { call.Exception = ex; @@ -64,7 +64,7 @@ public static async Task> ReceiveJsonList(this TaskA Task whose result is the response body as a string. /// s = await url.PostAsync(data).ReceiveString() public static async Task ReceiveString(this Task response) { -#if NETSTANDARD +#if NETSTANDARD1_3 Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); #endif var resp = await response.ConfigureAwait(false); diff --git a/src/Flurl.Http/IHttpSettingsContainer.cs b/src/Flurl.Http/IHttpSettingsContainer.cs new file mode 100644 index 00000000..7036f191 --- /dev/null +++ b/src/Flurl.Http/IHttpSettingsContainer.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using Flurl.Http.Configuration; +using Flurl.Util; + +namespace Flurl.Http +{ + /// + /// Defines stateful aspects (headers, cookies, etc) common to both IFlurlClient and IFlurlRequest + /// + public interface IHttpSettingsContainer + { + /// + /// Gets or sets the FlurlHttpSettings object used by this client. + /// + FlurlHttpSettings Settings { get; set; } + + /// + /// Collection of headers sent on all requests using this client. + /// + IDictionary Headers { get; } + + /// + /// Collection of HttpCookies sent and received with all requests using this client. + /// + IDictionary Cookies { get; } + } +} diff --git a/src/Flurl.Http/MultipartExtensions.cs b/src/Flurl.Http/MultipartExtensions.cs index ec05a6d0..20ad91cc 100644 --- a/src/Flurl.Http/MultipartExtensions.cs +++ b/src/Flurl.Http/MultipartExtensions.cs @@ -7,7 +7,7 @@ namespace Flurl.Http { /// - /// MultipartExtensions + /// Fluent extension menthods for sending multipart/form-data requests. /// public static class MultipartExtensions { @@ -15,35 +15,35 @@ public static class MultipartExtensions /// Sends an asynchronous multipart/form-data POST request. /// /// A delegate for building the content parts. - /// The Flurl client. + /// The IFlurlRequest. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// A Task whose result is the received HttpResponseMessage. - public static Task PostMultipartAsync(this IFlurlClient client, Action buildContent, CancellationToken cancellationToken = default(CancellationToken)) { - var cmc = new CapturedMultipartContent(client.Settings); + public static Task PostMultipartAsync(this IFlurlRequest request, Action buildContent, CancellationToken cancellationToken = default(CancellationToken)) { + var cmc = new CapturedMultipartContent(request.Settings); buildContent(cmc); - return client.SendAsync(HttpMethod.Post, cmc, cancellationToken); + return request.SendAsync(HttpMethod.Post, cmc, cancellationToken); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous multipart/form-data POST request. + /// Creates a FlurlRequest from the URL and sends an asynchronous multipart/form-data POST request. /// /// A delegate for building the content parts. /// The URL. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// A Task whose result is the received HttpResponseMessage. public static Task PostMultipartAsync(this Url url, Action buildContent, CancellationToken cancellationToken = default(CancellationToken)) { - return new FlurlClient(url, false).PostMultipartAsync(buildContent, cancellationToken); + return new FlurlRequest(url).PostMultipartAsync(buildContent, cancellationToken); } /// - /// Creates a FlurlClient from the URL and sends an asynchronous multipart/form-data POST request. + /// Creates a FlurlRequest from the URL and sends an asynchronous multipart/form-data POST request. /// /// A delegate for building the content parts. /// The URL. /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// A Task whose result is the received HttpResponseMessage. public static Task PostMultipartAsync(this string url, Action buildContent, CancellationToken cancellationToken = default(CancellationToken)) { - return new FlurlClient(url, false).PostMultipartAsync(buildContent, cancellationToken); + return new FlurlRequest(url).PostMultipartAsync(buildContent, cancellationToken); } } } diff --git a/src/Flurl.Http/NoOpTask.cs b/src/Flurl.Http/NoOpTask.cs deleted file mode 100644 index 599f72ab..00000000 --- a/src/Flurl.Http/NoOpTask.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Threading.Tasks; - -namespace Flurl.Http -{ - internal static class NoOpTask - { -#if !PORTABLE - public static readonly Task Instance = Task.FromResult(0); -#elif PORTABLE - public static readonly Task Instance = TaskEx.FromResult(0); -#endif - } -} \ No newline at end of file diff --git a/src/Flurl.Http/SettingsExtensions.cs b/src/Flurl.Http/SettingsExtensions.cs new file mode 100644 index 00000000..7fa4e38d --- /dev/null +++ b/src/Flurl.Http/SettingsExtensions.cs @@ -0,0 +1,125 @@ +using System; +using System.Linq; +using System.Net; +using Flurl.Http.Configuration; + +namespace Flurl.Http +{ + /// + /// Fluent extension methods for tweaking FlurlHttpSettings + /// + public static class SettingsExtensions + { + /// + /// Change FlurlHttpSettings for this IFlurlClient. + /// + /// The IFlurlClient. + /// Action defining the settings changes. + /// The IFlurlClient with the modified Settings + public static IFlurlClient Configure(this IFlurlClient client, Action action) { + action(client.Settings); + return client; + } + + /// + /// Change FlurlHttpSettings for this IFlurlRequest. + /// + /// The IFlurlRequest. + /// Action defining the settings changes. + /// The IFlurlRequest with the modified Settings + public static IFlurlRequest ConfigureRequest(this IFlurlRequest request, Action action) { + action(request.Settings); + return request; + } + + /// + /// Fluently specify the IFlurlClient to use with this IFlurlRequest. + /// + /// The IFlurlRequest. + /// The IFlurlClient to use when sending the request. + /// A new IFlurlRequest to use in calling the Url + public static IFlurlRequest WithClient(this IFlurlRequest request, IFlurlClient client) { + request.Client = client; + return request; + } + + /// + /// Fluently returns a new IFlurlRequest that can be used to call this Url with the given client. + /// + /// + /// The IFlurlClient to use to call the Url. + /// A new IFlurlRequest to use in calling the Url + public static IFlurlRequest WithClient(this Url url, IFlurlClient client) { + return client.Request(url); + } + + /// + /// Fluently returns a new IFlurlRequest that can be used to call this Url with the given client. + /// + /// + /// The IFlurlClient to use to call the Url. + /// A new IFlurlRequest to use in calling the Url + public static IFlurlRequest WithClient(this string url, IFlurlClient client) { + return client.Request(url); + } + + /// + /// Sets the timeout for this IFlurlRequest or all requests made with this IFlurlClient. + /// + /// The IFlurlClient or IFlurlRequest. + /// Time to wait before the request times out. + /// This IFlurlClient or IFlurlRequest. + public static T WithTimeout(this T obj, TimeSpan timespan) where T : IHttpSettingsContainer { + obj.Settings.Timeout = timespan; + return obj; + } + + /// + /// Sets the timeout for this IFlurlRequest or all requests made with this IFlurlClient. + /// + /// The IFlurlClient or IFlurlRequest. + /// Seconds to wait before the request times out. + /// This IFlurlClient or IFlurlRequest. + public static T WithTimeout(this T obj, int seconds) where T : IHttpSettingsContainer { + obj.Settings.Timeout = TimeSpan.FromSeconds(seconds); + return obj; + } + + /// + /// Adds a pattern representing an HTTP status code or range of codes which (in addition to 2xx) will NOT result in a FlurlHttpException being thrown. + /// + /// The IFlurlClient or IFlurlRequest. + /// Examples: "3xx", "100,300,600", "100-299,6xx" + /// This IFlurlClient or IFlurlRequest. + public static T AllowHttpStatus(this T obj, string pattern) where T : IHttpSettingsContainer { + if (!string.IsNullOrWhiteSpace(pattern)) { + var current = obj.Settings.AllowedHttpStatusRange; + if (string.IsNullOrWhiteSpace(current)) + obj.Settings.AllowedHttpStatusRange = pattern; + else + obj.Settings.AllowedHttpStatusRange += "," + pattern; + } + return obj; + } + + /// + /// Adds an which (in addition to 2xx) will NOT result in a FlurlHttpException being thrown. + /// + /// The IFlurlClient or IFlurlRequest. + /// Examples: HttpStatusCode.NotFound + /// This IFlurlClient or IFlurlRequest. + public static T AllowHttpStatus(this T obj, params HttpStatusCode[] statusCodes) where T : IHttpSettingsContainer { + var pattern = string.Join(",", statusCodes.Select(c => (int)c)); + return AllowHttpStatus(obj, pattern); + } + + /// + /// Prevents a FlurlHttpException from being thrown on any completed response, regardless of the HTTP status code. + /// + /// This IFlurlClient or IFlurlRequest. + public static T AllowAnyHttpStatus(this T obj) where T : IHttpSettingsContainer { + obj.Settings.AllowedHttpStatusRange = "*"; + return obj; + } + } +} \ No newline at end of file diff --git a/src/Flurl.Http/Testing/HttpCallAssertion.cs b/src/Flurl.Http/Testing/HttpCallAssertion.cs index 3ac6567a..46a23b7a 100644 --- a/src/Flurl.Http/Testing/HttpCallAssertion.cs +++ b/src/Flurl.Http/Testing/HttpCallAssertion.cs @@ -51,7 +51,7 @@ public HttpCallAssertion WithUrlPattern(string urlPattern) { return this; } _expectedConditions.Add($"URL pattern {urlPattern}"); - return With(c => MatchesPattern(c.Url, urlPattern)); + return With(c => MatchesPattern(c.FlurlRequest.Url, urlPattern)); } /// @@ -61,7 +61,7 @@ public HttpCallAssertion WithUrlPattern(string urlPattern) { /// public HttpCallAssertion WithQueryParam(string name) { _expectedConditions.Add($"query parameter {name}"); - return With(c => c.Url.QueryParams.Any(q => q.Name == name)); + return With(c => c.FlurlRequest.Url.QueryParams.Any(q => q.Name == name)); } /// @@ -71,7 +71,7 @@ public HttpCallAssertion WithQueryParam(string name) { /// public HttpCallAssertion WithoutQueryParam(string name) { _expectedConditions.Add($"no query parameter {name}"); - return Without(c => c.Url.QueryParams.Any(q => q.Name == name)); + return Without(c => c.FlurlRequest.Url.QueryParams.Any(q => q.Name == name)); } /// @@ -82,7 +82,7 @@ public HttpCallAssertion WithoutQueryParam(string name) { public HttpCallAssertion WithQueryParams(params string[] names) { if (!names.Any()) { _expectedConditions.Add("any query parameters"); - return With(c => c.Url.QueryParams.Any()); + return With(c => c.FlurlRequest.Url.QueryParams.Any()); } return names.Select(WithQueryParam).LastOrDefault() ?? this; } @@ -95,7 +95,7 @@ public HttpCallAssertion WithQueryParams(params string[] names) { public HttpCallAssertion WithoutQueryParams(params string[] names) { if (!names.Any()) { _expectedConditions.Add("no query parameters"); - return Without(c => c.Url.QueryParams.Any()); + return Without(c => c.FlurlRequest.Url.QueryParams.Any()); } return names.Select(WithoutQueryParam).LastOrDefault() ?? this; } @@ -113,7 +113,7 @@ public HttpCallAssertion WithQueryParamValue(string name, object value) { return this; } _expectedConditions.Add($"query parameter {name}={value}"); - return With(c => c.Url.QueryParams.Any(qp => QueryParamMatches(qp, name, value))); + return With(c => c.FlurlRequest.Url.QueryParams.Any(qp => QueryParamMatches(qp, name, value))); } /// @@ -129,7 +129,7 @@ public HttpCallAssertion WithoutQueryParamValue(string name, object value) { return this; } _expectedConditions.Add($"no query parameter {name}={value}"); - return Without(c => c.Url.QueryParams.Any(qp => QueryParamMatches(qp, name, value))); + return Without(c => c.FlurlRequest.Url.QueryParams.Any(qp => QueryParamMatches(qp, name, value))); } /// @@ -203,6 +203,32 @@ public HttpCallAssertion WithOAuthBearerToken(string token) { && c.Request.Headers.Authorization?.Parameter == token); } + /// + /// Asserts whther the calls were made containing the given request header. + /// + /// Expected header name + /// Expected header value pattern + /// + public HttpCallAssertion WithHeader(string name, string valuePattern = "*") { + _expectedConditions.Add($"header {name}: {valuePattern}"); + return With(c => + c.Request.Headers.TryGetValues(name, out var vals) && + vals.Any(v => MatchesPattern(v, valuePattern))); + } + + /// + /// Asserts whther the calls were made that do not contain the given request header. + /// + /// Expected header name + /// Expected header value pattern + /// + public HttpCallAssertion WithoutHeader(string name, string valuePattern = "*") { + _expectedConditions.Add($"no header {name}: {valuePattern}"); + return Without(c => + c.Request.Headers.TryGetValues(name, out var vals) && + vals.Any(v => MatchesPattern(v, valuePattern))); + } + /// /// Asserts whether the Authorization header was set with basic auth. /// diff --git a/src/Flurl.Http/Testing/HttpTest.cs b/src/Flurl.Http/Testing/HttpTest.cs index 28ffe8b0..da8adc95 100644 --- a/src/Flurl.Http/Testing/HttpTest.cs +++ b/src/Flurl.Http/Testing/HttpTest.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net; using System.Net.Http; +using Flurl.Http.Configuration; using Flurl.Http.Content; using Flurl.Util; @@ -14,21 +15,55 @@ namespace Flurl.Http.Testing /// public class HttpTest : IDisposable { + private readonly Lazy _httpClient; + private readonly Lazy _httpMessageHandler; + /// - /// Gets the current HttpTest from the logical (async) call context + /// Initializes a new instance of the class. /// - public static HttpTest Current => GetCurrentTest(); - - /// - /// Initializes a new instance of the class. - /// - /// A delegate callback throws an exception. - public HttpTest() { + /// A delegate callback throws an exception. + public HttpTest() { + Settings = new TestFlurlHttpSettings(); ResponseQueue = new Queue(); CallLog = new List(); + _httpClient = new Lazy(() => Settings.HttpClientFactory.CreateHttpClient(HttpMessageHandler)); + _httpMessageHandler = new Lazy(() => Settings.HttpClientFactory.CreateMessageHandler()); SetCurrentTest(this); } + internal HttpClient HttpClient => _httpClient.Value; + internal HttpMessageHandler HttpMessageHandler => _httpMessageHandler.Value; + + /// + /// Gets or sets the FlurlHttpSettings object used by this test. + /// + public GlobalFlurlHttpSettings Settings { get; set; } + + /// + /// Gets the current HttpTest from the logical (async) call context + /// + public static HttpTest Current => GetCurrentTest(); + + /// + /// Queue of HttpResponseMessages to be returned in place of real responses during testing. + /// + public Queue ResponseQueue { get; set; } + + /// + /// List of all (fake) HTTP calls made since this HttpTest was created. + /// + public List CallLog { get; } + + /// + /// Change FlurlHttpSettings for the scope of this HttpTest. + /// + /// Action defining the settings changes. + /// This HttpTest + public HttpTest Configure(Action action) { + action(Settings); + return this; + } + /// /// Adds an HttpResponseMessage to the response queue. /// @@ -50,7 +85,7 @@ public HttpTest RespondWith(string body, int status = 200, object headers = null /// The simulated response cookies (optional). /// The current HttpTest object (so more responses can be chained). public HttpTest RespondWithJson(object body, int status = 200, object headers = null, object cookies = null) { - var content = new CapturedJsonContent(FlurlHttp.GlobalSettings.JsonSerializer.Serialize(body)); + var content = new CapturedJsonContent(Settings.JsonSerializer.Serialize(body)); return RespondWith(content, status, headers, cookies); } @@ -89,11 +124,6 @@ public HttpTest SimulateTimeout() { return this; } - /// - /// Queue of HttpResponseMessages to be returned in place of real responses during testing. - /// - public Queue ResponseQueue { get; set; } - internal HttpResponseMessage GetNextResponse() { return ResponseQueue.Any() ? ResponseQueue.Dequeue() : new HttpResponseMessage { StatusCode = HttpStatusCode.OK, @@ -101,11 +131,6 @@ internal HttpResponseMessage GetNextResponse() { }; } - /// - /// List of all (fake) HTTP calls made since this HttpTest was created. - /// - public List CallLog { get; private set; } - /// /// Asserts whether matching URL was called, throwing HttpCallAssertException if it wasn't. /// @@ -141,20 +166,19 @@ public void ShouldNotHaveMadeACall() { /// public void Dispose() { SetCurrentTest(null); - FlurlHttp.GlobalSettings.ResetDefaults(); } -#if PORTABLE - private static HttpTest _test; - private static void SetCurrentTest(HttpTest test) => _test = test; - private static HttpTest GetCurrentTest() => _test; -#elif NET45 +#if NET45 private static void SetCurrentTest(HttpTest test) => System.Runtime.Remoting.Messaging.CallContext.LogicalSetData("FlurlHttpTest", test); private static HttpTest GetCurrentTest() => System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("FlurlHttpTest") as HttpTest; -#else +#elif NETSTANDARD1_3 private static System.Threading.AsyncLocal _test = new System.Threading.AsyncLocal(); private static void SetCurrentTest(HttpTest test) => _test.Value = test; private static HttpTest GetCurrentTest() => _test.Value; +#elif NETSTANDARD1_1 + private static HttpTest _test; + private static void SetCurrentTest(HttpTest test) => _test = test; + private static HttpTest GetCurrentTest() => _test; #endif } } \ No newline at end of file diff --git a/src/Flurl.Http/Testing/TestFactories.cs b/src/Flurl.Http/Testing/TestFactories.cs new file mode 100644 index 00000000..98e6b5ac --- /dev/null +++ b/src/Flurl.Http/Testing/TestFactories.cs @@ -0,0 +1,46 @@ +using System; +using System.Net.Http; +using Flurl.Http.Configuration; + +namespace Flurl.Http.Testing +{ + /// + /// IHttpClientFactory implementation used to fake and record calls in tests. + /// + public class TestHttpClientFactory : DefaultHttpClientFactory + { + /// + /// Creates an instance of FakeHttpMessageHander, which prevents actual HTTP calls from being made. + /// + /// + public override HttpMessageHandler CreateMessageHandler() { + return new FakeHttpMessageHandler(); + } + } + + /// + /// IFlurlClientFactory implementation used to fake and record calls in tests. + /// + public class TestFlurlClientFactory : FlurlClientFactoryBase + { + private readonly Lazy _client = new Lazy(() => new FlurlClient()); + + /// + /// Returns the FlurlClient sigleton used for testing + /// + /// The URL. + /// The FlurlClient instance. + public override IFlurlClient Get(Url url) { + return _client.Value; + } + + /// + /// Not used. Singleton FlurlClient used for lifetime of test. + /// + /// + /// + protected override string GetCacheKey(Url url) { + return null; + } + } +} \ No newline at end of file diff --git a/src/Flurl.Http/Testing/TestHttpClientFactory.cs b/src/Flurl.Http/Testing/TestHttpClientFactory.cs deleted file mode 100644 index a90b52c8..00000000 --- a/src/Flurl.Http/Testing/TestHttpClientFactory.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Net.Http; -using Flurl.Http.Configuration; - -namespace Flurl.Http.Testing -{ - /// - /// Fake http client factory. - /// - public class TestHttpClientFactory : DefaultHttpClientFactory - { - /// - /// Creates an instance of FakeHttpMessageHander, which prevents actual HTTP calls from being made. - /// - /// - public override HttpMessageHandler CreateMessageHandler() { - return new FakeHttpMessageHandler(); - } - } -} \ No newline at end of file diff --git a/src/Flurl.Http/UrlBuilderExtensions.cs b/src/Flurl.Http/UrlBuilderExtensions.cs new file mode 100644 index 00000000..64345208 --- /dev/null +++ b/src/Flurl.Http/UrlBuilderExtensions.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; + +namespace Flurl.Http +{ + /// + /// URL builder extension methods on FlurlRequest + /// + public static class UrlBuilderExtensions + { + /// + /// Appends a segment to the URL path, ensuring there is one and only one '/' character as a seperator. + /// + /// The IFlurlRequest associated with the URL + /// The segment to append + /// This IFlurlRequest + /// is . + public static IFlurlRequest AppendPathSegment(this IFlurlRequest request, object segment) { + request.Url.AppendPathSegment(segment); + return request; + } + + /// + /// Appends multiple segments to the URL path, ensuring there is one and only one '/' character as a seperator. + /// + /// The IFlurlRequest associated with the URL + /// The segments to append + /// This IFlurlRequest + public static IFlurlRequest AppendPathSegments(this IFlurlRequest request, params object[] segments) { + request.Url.AppendPathSegments(segments); + return request; + } + + /// + /// Appends multiple segments to the URL path, ensuring there is one and only one '/' character as a seperator. + /// + /// The IFlurlRequest associated with the URL + /// The segments to append + /// This IFlurlRequest + public static IFlurlRequest AppendPathSegments(this IFlurlRequest request, IEnumerable segments) { + request.Url.AppendPathSegments(segments); + return request; + } + + /// + /// Adds a parameter to the URL query, overwriting the value if name exists. + /// + /// The IFlurlRequest associated with the URL + /// Name of query parameter + /// Value of query parameter + /// Indicates how to handle null values. Defaults to Remove (any existing) + /// This IFlurlRequest + public static IFlurlRequest SetQueryParam(this IFlurlRequest request, string name, object value, NullValueHandling nullValueHandling = NullValueHandling.Remove) { + request.Url.SetQueryParam(name, value, nullValueHandling); + return request; + } + + /// + /// Adds a parameter to the URL query, overwriting the value if name exists. + /// + /// The IFlurlRequest associated with the URL + /// Name of query parameter + /// Value of query parameter + /// Set to true to indicate the value is already URL-encoded + /// Indicates how to handle null values. Defaults to Remove (any existing) + /// This IFlurlRequest + /// is . + public static IFlurlRequest SetQueryParam(this IFlurlRequest request, string name, string value, bool isEncoded = false, NullValueHandling nullValueHandling = NullValueHandling.Remove) { + request.Url.SetQueryParam(name, value, isEncoded, nullValueHandling); + return request; + } + + /// + /// Adds a parameter without a value to the URL query, removing any existing value. + /// + /// The IFlurlRequest associated with the URL + /// Name of query parameter + /// This IFlurlRequest + public static IFlurlRequest SetQueryParam(this IFlurlRequest request, string name) { + request.Url.SetQueryParam(name); + return request; + } + + /// + /// Parses values (usually an anonymous object or dictionary) into name/value pairs and adds them to the URL query, overwriting any that already exist. + /// + /// The IFlurlRequest associated with the URL + /// Typically an anonymous object, ie: new { x = 1, y = 2 } + /// Indicates how to handle null values. Defaults to Remove (any existing) + /// This IFlurlRequest + public static IFlurlRequest SetQueryParams(this IFlurlRequest request, object values, NullValueHandling nullValueHandling = NullValueHandling.Remove) { + request.Url.SetQueryParams(values, nullValueHandling); + return request; + } + + /// + /// Adds multiple parameters without values to the URL query. + /// + /// The IFlurlRequest associated with the URL + /// Names of query parameters. + /// This IFlurlRequest + public static IFlurlRequest SetQueryParams(this IFlurlRequest request, IEnumerable names) { + request.Url.SetQueryParams(names); + return request; + } + + /// + /// Adds multiple parameters without values to the URL query. + /// + /// The IFlurlRequest associated with the URL + /// Names of query parameters + /// This IFlurlRequest + public static IFlurlRequest SetQueryParams(this IFlurlRequest request, params string[] names) { + request.Url.SetQueryParams(names as IEnumerable); + return request; + } + + /// + /// Removes a name/value pair from the URL query by name. + /// + /// The IFlurlRequest associated with the URL + /// Query string parameter name to remove + /// This IFlurlRequest + public static IFlurlRequest RemoveQueryParam(this IFlurlRequest request, string name) { + request.Url.RemoveQueryParam(name); + return request; + } + + /// + /// Removes multiple name/value pairs from the URL query by name. + /// + /// The IFlurlRequest associated with the URL + /// Query string parameter names to remove + /// This IFlurlRequest + public static IFlurlRequest RemoveQueryParams(this IFlurlRequest request, params string[] names) { + request.Url.RemoveQueryParams(names); + return request; + } + + /// + /// Removes multiple name/value pairs from the URL query by name. + /// + /// The IFlurlRequest associated with the URL + /// Query string parameter names to remove + /// This IFlurlRequest + public static IFlurlRequest RemoveQueryParams(this IFlurlRequest request, IEnumerable names) { + request.Url.RemoveQueryParams(names); + return request; + } + + /// + /// Set the URL fragment fluently. + /// + /// The IFlurlRequest associated with the URL + /// The part of the URL afer # + /// This IFlurlRequest + public static IFlurlRequest SetFragment(this IFlurlRequest request, string fragment) { + request.Url.SetFragment(fragment); + return request; + } + + /// + /// Removes the URL fragment including the #. + /// + /// The IFlurlRequest associated with the URL + /// This IFlurlRequest + public static IFlurlRequest RemoveFragment(this IFlurlRequest request) { + request.Url.RemoveFragment(); + return request; + } + } +} diff --git a/src/Flurl/Flurl.csproj b/src/Flurl/Flurl.csproj index adc6ca82..783170d7 100644 --- a/src/Flurl/Flurl.csproj +++ b/src/Flurl/Flurl.csproj @@ -1,9 +1,10 @@  - net40;netstandard1.3;portable-net40+win8+wpa81+wp8+sl5 + net40;netstandard1.3;netstandard1.0; True - 2.4.0 + Flurl + 2.5.0 Todd Menier A fluent, portable URL builder. To make HTTP calls off the fluent chain, check out Flurl.Http. http://tmenier.github.io/Flurl @@ -11,32 +12,8 @@ https://raw.githubusercontent.com/tmenier/Flurl/master/LICENSE https://github.com/tmenier/Flurl.git git - fluent portable url uri querystring builder - - 2.4.0 - Up'd solution to VS2017, platform targeting fixes (github #176) - 2.3.0 - First-class support for name-only query parameters (github #164), NullValueHandling config enum - 2.2.1 - Fix net461 target (github #128) - 2.2.0 - Url.Combine enhancements, broader PCL support https://github.com/tmenier/Flurl/releases/tag/Flurl.2.2.0 - 2.1.0 - .NET Core 1.0.0 support. Target .NET Platform Standard 1.4 - 2.0.0 - BREAKING CHANGES: https://github.com/tmenier/Flurl/wiki/Release-Notes - 1.1.2 - Fix net461 target (github #74) - 1.1.1 - Packaging fix - 1.1.0 - .NET Core support (github #61, thx @kroniak) - 1.0.10 - More flexible kv parsing (github pr #16) - 1.0.9 - Decode + as space, optionally encode space as + (github #41) - 1.0.8 - Don't trim trailing slash (github #37) - 1.0.7 - Bugfix - parsing querystrings like: "?x=1&amp;x=2" (thanks @miiihi) - 1.0.6 - Use CultureInfo.InvariantCulture in a few string conversions - 1.0.5 - Url.GetRoot (static) and Url.ResetToRoot - 1.0.4 - Support for multi-value query params, i.e. SetQueryParam("x", new[] { 1, 2 }) => "x=1&amp;x=2") - 1.0.3 - Bugfix - exclude null values from querystring (thanks to @niemyjski) - 1.0.2 - Bugfix related to querystring with only key (thanks to @rafaelsteil) - 1.0.1 - Fixed DLL version, dropped support for xbox and wp7 due to VS2013 upgrade - 1.0.0 - Minor code cleanup, no API changes. - 0.2.2 - Nuspec updates, no changes to binaries. - 0.2.1 - Fixed a couple string extensions whose names were inconsistent with their Flurl.Url equivalent. - 0.2.0 - Added PCL support. - + fluent url uri querystring builder + See https://github.com/tmenier/Flurl/releases false @@ -48,26 +25,8 @@ bin\Release\Flurl.xml - - PORTABLE - .NETPortable - v4.0 - Profile328 - .NETPortable,Version=v0.0,Profile=Profile328 - C:\Program Files (x86)\MSBuild\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets - - - - NETSTANDARD - - - - - - - \ No newline at end of file diff --git a/src/Flurl/Util/CommonExtensions.cs b/src/Flurl/Util/CommonExtensions.cs index 5d6e123e..d9041854 100644 --- a/src/Flurl/Util/CommonExtensions.cs +++ b/src/Flurl/Util/CommonExtensions.cs @@ -3,7 +3,8 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -#if NETSTANDARD +using System.Text.RegularExpressions; +#if !NET40 using System.Reflection; #endif @@ -14,14 +15,14 @@ namespace Flurl.Util /// public static class CommonExtensions { - /// - /// Converts an object's public properties to a collection of string-based key-value pairs. If the object happens - /// to be an IDictionary, the IDictionary's keys and values converted to strings and returned. - /// - /// The object to parse into key-value pairs - /// - /// is . - public static IEnumerable> ToKeyValuePairs(this object obj) { + /// + /// Converts an object's public properties to a collection of string-based key-value pairs. If the object happens + /// to be an IDictionary, the IDictionary's keys and values converted to strings and returned. + /// + /// The object to parse into key-value pairs + /// + /// is . + public static IEnumerable> ToKeyValuePairs(this object obj) { if (obj == null) throw new ArgumentNullException(nameof(obj)); @@ -37,10 +38,11 @@ public static IEnumerable> ToKeyValuePairs(this obj public static string ToInvariantString(this object obj) { // inspired by: http://stackoverflow.com/a/19570016/62600 +#if !NETSTANDARD1_0 var c = obj as IConvertible; if (c != null) return c.ToString(CultureInfo.InvariantCulture); - +#endif var f = obj as IFormattable; if (f != null) return f.ToString(null, CultureInfo.InvariantCulture); @@ -48,13 +50,13 @@ public static string ToInvariantString(this object obj) { return obj.ToString(); } - /// - /// Splits at the first occurence of the given seperator. - /// - /// The string to split. - /// The separator to split on. - /// Array of at most 2 strings. (1 if separator is not found.) - public static string[] SplitOnFirstOccurence(this string s, char separator) { + /// + /// Splits at the first occurence of the given seperator. + /// + /// The string to split. + /// The separator to split on. + /// Array of at most 2 strings. (1 if separator is not found.) + public static string[] SplitOnFirstOccurence(this string s, char separator) { // Needed because full PCL profile doesn't support Split(char[], int) (#119) if (string.IsNullOrEmpty(s)) return new[] { s }; @@ -71,9 +73,15 @@ private static IEnumerable> StringToKV(string s) { } private static IEnumerable> ObjectToKV(object obj) { +#if NETSTANDARD1_0 + return from prop in obj.GetType().GetRuntimeProperties() + let val = prop.GetValue(obj, null) + select new KeyValuePair(prop.Name, val); +#else return from prop in obj.GetType().GetProperties() let val = prop.GetValue(obj, null) select new KeyValuePair(prop.Name, val); +#endif } private static IEnumerable> CollectionToKV(IEnumerable col) { @@ -86,8 +94,13 @@ private static IEnumerable> CollectionToKV(IEnumera object val; var type = item.GetType(); +#if NETSTANDARD1_0 + var keyProp = type.GetRuntimeProperty("Key") ?? type.GetRuntimeProperty("key") ?? type.GetRuntimeProperty("Name") ?? type.GetRuntimeProperty("name"); + var valProp = type.GetRuntimeProperty("Value") ?? type.GetRuntimeProperty("value"); +#else var keyProp = type.GetProperty("Key") ?? type.GetProperty("key") ?? type.GetProperty("Name") ?? type.GetProperty("name"); var valProp = type.GetProperty("Value") ?? type.GetProperty("value"); +#endif if (keyProp != null && valProp != null) { key = keyProp.GetValue(item, null)?.ToInvariantString(); @@ -102,5 +115,13 @@ private static IEnumerable> CollectionToKV(IEnumera yield return new KeyValuePair(key, val); } } + + /// + /// Merges the key/value pairs from d2 into d1, without overwriting those already set in d1. + /// + public static void Merge(this IDictionary d1, IDictionary d2) { + foreach (var kv in d2.Where(x => !d1.Keys.Contains(x.Key))) + d1.Add(kv); + } } } \ No newline at end of file