From 0fd994e05b9d5f973bd886ea98d69a0a0fa26ec6 Mon Sep 17 00:00:00 2001 From: Nikolay Molchanov Date: Fri, 21 Jul 2017 19:19:31 +0300 Subject: [PATCH 01/56] [drop] pcl targets --- Flurl.sln | 11 +--- .../PackageTester.NET45.csproj | 11 +--- PackageTesters/PackageTester.NET45/Program.cs | 4 -- .../PackageTester.PCL.csproj | 65 ------------------- PackageTesters/PackageTester.PCL/PclTester.cs | 4 -- .../PackageTester.PCL/packages.config | 6 -- Test/Flurl.Test/Http/TestingTests.cs | 3 +- src/Flurl.Http/FileUtil.cs | 22 ------- src/Flurl.Http/Flurl.Http.csproj | 28 ++------ src/Flurl.Http/NoOpTask.cs | 4 -- src/Flurl.Http/Testing/HttpTest.cs | 6 +- src/Flurl/Flurl.csproj | 21 ++---- 12 files changed, 16 insertions(+), 169 deletions(-) delete mode 100644 PackageTesters/PackageTester.PCL/PackageTester.PCL.csproj delete mode 100644 PackageTesters/PackageTester.PCL/PclTester.cs delete mode 100644 PackageTesters/PackageTester.PCL/packages.config diff --git a/Flurl.sln b/Flurl.sln index 6f225d06..7711c6fa 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.26430.16 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flurl", "src\Flurl\Flurl.csproj", "{117B6C6E-53F9-45AE-9439-F4FB7E21B116}" EndProject @@ -33,12 +33,10 @@ 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}" -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 +72,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 +85,5 @@ 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 EndGlobal diff --git a/PackageTesters/PackageTester.NET45/PackageTester.NET45.csproj b/PackageTesters/PackageTester.NET45/PackageTester.NET45.csproj index 3a0ffa0d..abf86a1e 100644 --- a/PackageTesters/PackageTester.NET45/PackageTester.NET45.csproj +++ b/PackageTesters/PackageTester.NET45/PackageTester.NET45.csproj @@ -32,10 +32,10 @@ - ..\..\packages\Flurl.2.4.0-pre\lib\net40\Flurl.dll + ..\..\packages\Flurl.2.4.0\lib\net40\Flurl.dll - ..\..\packages\Flurl.Http.1.2.0-pre\lib\net45\Flurl.Http.dll + ..\..\packages\Flurl.Http.1.2.0\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.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/Http/TestingTests.cs b/Test/Flurl.Test/Http/TestingTests.cs index e4b1b9e1..e44806a4 100644 --- a/Test/Flurl.Test/Http/TestingTests.cs +++ b/Test/Flurl.Test/Http/TestingTests.cs @@ -196,7 +196,7 @@ public async Task can_deserialize_default_response_more_than_once() { } // parallel testing not supported in PCL -#if !PORTABLE + [Test] public async Task can_test_in_parallel() { await Task.WhenAll( @@ -206,7 +206,6 @@ await Task.WhenAll( CallAndAssertCountAsync(4), CallAndAssertCountAsync(6)); } -#endif private async Task CallAndAssertCountAsync(int calls) { using (var test = new HttpTest()) { diff --git a/src/Flurl.Http/FileUtil.cs b/src/Flurl.Http/FileUtil.cs index 064af7b1..8e2f4658 100644 --- a/src/Flurl.Http/FileUtil.cs +++ b/src/Flurl.Http/FileUtil.cs @@ -1,31 +1,10 @@ using System.IO; -using System.Linq; using System.Threading.Tasks; namespace Flurl.Http { internal static class FileUtil { -#if PORTABLE - internal static string GetFileName(string path) { - return path?.Split(PCLStorage.PortablePath.DirectorySeparatorChar).Last(); - } - - internal static string CombinePath(params string[] paths) { - return PCLStorage.PortablePath.Combine(paths); - } - - internal static async Task OpenReadAsync(string path, int bufferSize) { - var file = await PCLStorage.FileSystem.Current.GetFileFromPathAsync(path).ConfigureAwait(false); - return await file.OpenAsync(PCLStorage.FileAccess.Read).ConfigureAwait(false); - } - - internal static async Task OpenWriteAsync(string folderPath, string fileName, int bufferSize) { - var folder = await PCLStorage.FileSystem.Current.LocalStorage.CreateFolderAsync(folderPath, PCLStorage.CreationCollisionOption.OpenIfExists).ConfigureAwait(false); - var file = await folder.CreateFileAsync(fileName, PCLStorage.CreationCollisionOption.ReplaceExisting).ConfigureAwait(false); - return await file.OpenAsync(PCLStorage.FileAccess.ReadAndWrite).ConfigureAwait(false); - } -#else internal static string GetFileName(string path) { return Path.GetFileName(path); } @@ -43,6 +22,5 @@ internal static Task OpenWriteAsync(string folderPath, string fileName, var filePath = Path.Combine(folderPath, fileName); return Task.FromResult(new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize, useAsync: true)); } -#endif } } \ No newline at end of file diff --git a/src/Flurl.Http/Flurl.Http.csproj b/src/Flurl.Http/Flurl.Http.csproj index 804d572d..801768cf 100644 --- a/src/Flurl.Http/Flurl.Http.csproj +++ b/src/Flurl.Http/Flurl.Http.csproj @@ -1,9 +1,9 @@  - net45;netstandard1.3;portable-net45+win8+wpa81+wp8 + net45;netstandard1.3; True - 1.2.0 + 2.0.0 Todd Menier A fluent, portable, testable HTTP client library that extends Flurl's URL builder chain. http://tmenier.github.io/Flurl @@ -11,8 +11,9 @@ 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 + httpclient rest json http fluent url uri tdd assert async + 3.0.0 - Drop PCL target 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 @@ -61,7 +62,7 @@ - + @@ -78,25 +79,6 @@ - - PORTABLE - .NETPortable - v4.5 - Profile259 - .NETPortable,Version=v0.0,Profile=Profile259 - C:\Program Files (x86)\MSBuild\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets - - - - - - - - - - - - diff --git a/src/Flurl.Http/NoOpTask.cs b/src/Flurl.Http/NoOpTask.cs index 599f72ab..9c9faacf 100644 --- a/src/Flurl.Http/NoOpTask.cs +++ b/src/Flurl.Http/NoOpTask.cs @@ -4,10 +4,6 @@ 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/Testing/HttpTest.cs b/src/Flurl.Http/Testing/HttpTest.cs index 28ffe8b0..f2b3ad33 100644 --- a/src/Flurl.Http/Testing/HttpTest.cs +++ b/src/Flurl.Http/Testing/HttpTest.cs @@ -144,11 +144,7 @@ public void Dispose() { 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 diff --git a/src/Flurl/Flurl.csproj b/src/Flurl/Flurl.csproj index adc6ca82..4d8c166f 100644 --- a/src/Flurl/Flurl.csproj +++ b/src/Flurl/Flurl.csproj @@ -1,9 +1,9 @@  - net40;netstandard1.3;portable-net40+win8+wpa81+wp8+sl5 + net40;netstandard1.3; True - 2.4.0 + 3.0.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,8 +11,9 @@ https://raw.githubusercontent.com/tmenier/Flurl/master/LICENSE https://github.com/tmenier/Flurl.git git - fluent portable url uri querystring builder + fluent url uri querystring builder + 3.0.0 - Drop PCL target 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) @@ -48,15 +49,6 @@ 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 @@ -65,9 +57,4 @@ - - - - - \ No newline at end of file From f98de654810da4fd2f691b276a3fc43528576952 Mon Sep 17 00:00:00 2001 From: Nikolay Molchanov Date: Fri, 21 Jul 2017 20:45:37 +0300 Subject: [PATCH 02/56] [add] netstd 1.0 and netstd 1.1 with api remove [edit] nunit test adapter 3.8 --- Test/Flurl.Test/Flurl.Test.csproj | 2 +- .../Content/CapturedMultipartContent.cs | 23 +-- src/Flurl.Http/Content/FileContent.cs | 38 ++-- src/Flurl.Http/DownloadExtensions.cs | 40 +++-- src/Flurl.Http/FileUtil.cs | 8 +- src/Flurl.Http/Flurl.Http.csproj | 10 +- .../HttpResponseMessageExtensions.cs | 6 +- src/Flurl.Http/Testing/HttpTest.cs | 8 +- src/Flurl/Flurl.csproj | 6 +- src/Flurl/Util/CommonExtensions.cs | 168 ++++++++++-------- 10 files changed, 164 insertions(+), 145 deletions(-) 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/src/Flurl.Http/Content/CapturedMultipartContent.cs b/src/Flurl.Http/Content/CapturedMultipartContent.cs index a10c758b..c515563c 100644 --- a/src/Flurl.Http/Content/CapturedMultipartContent.cs +++ b/src/Flurl.Http/Content/CapturedMultipartContent.cs @@ -109,23 +109,24 @@ public CapturedMultipartContent AddFile(string name, Stream stream, string fileN return AddInternal(name, content, fileName); } - /// - /// Adds a file to the multipart request from a local path. - /// - /// The control name of the part. - /// The local path to the file. - /// The media type of the file. - /// The buffer size of the stream upload in bytes. Defaults to 4096. - /// This CapturedMultipartContent instance (supports method chaining). - public CapturedMultipartContent AddFile(string name, string path, string mediaType = null, int bufferSize = 4096) { +#if !NETSTANDARD1_1 + /// + /// Adds a file to the multipart request from a local path. + /// + /// The control name of the part. + /// The local path to the file. + /// The media type of the file. + /// The buffer size of the stream upload in bytes. Defaults to 4096. + /// This CapturedMultipartContent instance (supports method chaining). + public CapturedMultipartContent AddFile(string name, string path, string mediaType = null, int bufferSize = 4096) { var fileName = FileUtil.GetFileName(path); var content = new FileContent(path, bufferSize); if (mediaType != null) content.Headers.ContentType = new MediaTypeHeaderValue(mediaType); return AddInternal(name, content, fileName); } - - private CapturedMultipartContent AddInternal(string name, HttpContent content, string fileName) { +#endif + private CapturedMultipartContent AddInternal(string name, HttpContent content, string fileName) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("name must not be empty", nameof(name)); diff --git a/src/Flurl.Http/Content/FileContent.cs b/src/Flurl.Http/Content/FileContent.cs index caa715d7..5833946d 100644 --- a/src/Flurl.Http/Content/FileContent.cs +++ b/src/Flurl.Http/Content/FileContent.cs @@ -3,12 +3,13 @@ using System.Net.Http; using System.Threading.Tasks; +#if !NETSTANDARD1_1 namespace Flurl.Http.Content { - /// - /// Represents HTTP content based on a local file. Typically used with PostMultipartAsync for uploading files. - /// - public class FileContent : HttpContent + /// + /// Represents HTTP content based on a local file. Typically used with PostMultipartAsync for uploading files. + /// + public class FileContent : HttpContent { /// /// The local file path. @@ -27,26 +28,27 @@ public FileContent(string path, int bufferSize = 4096) { _bufferSize = bufferSize; } - /// - /// Serializes to stream asynchronous. - /// - /// The stream. - /// The context. - /// - protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) { + /// + /// Serializes to stream asynchronous. + /// + /// The stream. + /// The context. + /// + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) { using (var source = await FileUtil.OpenReadAsync(Path, _bufferSize).ConfigureAwait(false)) { await source.CopyToAsync(stream, _bufferSize).ConfigureAwait(false); } } - /// - /// Tries the length of the compute. - /// - /// The length. - /// - protected override bool TryComputeLength(out long length) { + /// + /// Tries the length of the compute. + /// + /// The length. + /// + protected override bool TryComputeLength(out long length) { length = -1; return false; } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/src/Flurl.Http/DownloadExtensions.cs b/src/Flurl.Http/DownloadExtensions.cs index 6be61bb2..b5c9fad5 100644 --- a/src/Flurl.Http/DownloadExtensions.cs +++ b/src/Flurl.Http/DownloadExtensions.cs @@ -2,6 +2,7 @@ using System.Net.Http; using System.Threading.Tasks; +#if !NETSTANDARD1_1 namespace Flurl.Http { /// @@ -9,15 +10,15 @@ namespace Flurl.Http /// public static class DownloadExtensions { - /// - /// Asynchronously downloads a file at the specified URL. - /// - /// The flurl client. - /// 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) { + /// + /// Asynchronously downloads a file at the specified URL. + /// + /// The flurl client. + /// 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) { if (localFileName == null) localFileName = client.Url.Path.Split('/').Last(); @@ -43,15 +44,15 @@ public static async Task DownloadFileAsync(this IFlurlClient client, str } } - /// - /// Asynchronously downloads a file at the specified URL. - /// - /// The Url. - /// 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 Task DownloadFileAsync(this string url, string localFolderPath, string localFileName = null, int bufferSize = 4096) { + /// + /// Asynchronously downloads a file at the specified URL. + /// + /// The Url. + /// 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 Task DownloadFileAsync(this string url, string localFolderPath, string localFileName = null, int bufferSize = 4096) { return new FlurlClient(url, true).DownloadFileAsync(localFolderPath, localFileName, bufferSize); } @@ -67,4 +68,5 @@ public static Task DownloadFileAsync(this Url url, string localFolderPat return new FlurlClient(url, true).DownloadFileAsync(localFolderPath, localFileName, bufferSize); } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/src/Flurl.Http/FileUtil.cs b/src/Flurl.Http/FileUtil.cs index 8e2f4658..a536254a 100644 --- a/src/Flurl.Http/FileUtil.cs +++ b/src/Flurl.Http/FileUtil.cs @@ -1,6 +1,7 @@ using System.IO; using System.Threading.Tasks; +#if !NETSTANDARD1_1 namespace Flurl.Http { internal static class FileUtil @@ -13,7 +14,7 @@ internal static string CombinePath(params string[] paths) { return Path.Combine(paths); } - internal static Task OpenReadAsync(string path, int bufferSize) { + internal static Task OpenReadAsync(string path, int bufferSize) { return Task.FromResult(new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, useAsync: true)); } @@ -22,5 +23,6 @@ internal static Task OpenWriteAsync(string folderPath, string fileName, var filePath = Path.Combine(folderPath, fileName); return Task.FromResult(new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize, useAsync: true)); } - } -} \ No newline at end of file + } +} +#endif \ No newline at end of file diff --git a/src/Flurl.Http/Flurl.Http.csproj b/src/Flurl.Http/Flurl.Http.csproj index 801768cf..79b20536 100644 --- a/src/Flurl.Http/Flurl.Http.csproj +++ b/src/Flurl.Http/Flurl.Http.csproj @@ -1,7 +1,7 @@  - net45;netstandard1.3; + net45;netstandard1.3;netstandard1.1; True 2.0.0 Todd Menier @@ -65,16 +65,16 @@ - - NETSTANDARD - - + + + + diff --git a/src/Flurl.Http/HttpResponseMessageExtensions.cs b/src/Flurl.Http/HttpResponseMessageExtensions.cs index a927de77..212253b3 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; @@ -64,8 +64,8 @@ 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 - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); +#if NETSTANDARD1_3 + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); #endif var resp = await response.ConfigureAwait(false); if (resp == null) return null; diff --git a/src/Flurl.Http/Testing/HttpTest.cs b/src/Flurl.Http/Testing/HttpTest.cs index f2b3ad33..4bf9026f 100644 --- a/src/Flurl.Http/Testing/HttpTest.cs +++ b/src/Flurl.Http/Testing/HttpTest.cs @@ -147,10 +147,14 @@ public void Dispose() { #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/Flurl.csproj b/src/Flurl/Flurl.csproj index 4d8c166f..ff47de5d 100644 --- a/src/Flurl/Flurl.csproj +++ b/src/Flurl/Flurl.csproj @@ -1,7 +1,7 @@  - net40;netstandard1.3; + net40;netstandard1.3;netstandard1.0; True 3.0.0 Todd Menier @@ -49,10 +49,6 @@ bin\Release\Flurl.xml - - NETSTANDARD - - diff --git a/src/Flurl/Util/CommonExtensions.cs b/src/Flurl/Util/CommonExtensions.cs index 5d6e123e..3dd92c8d 100644 --- a/src/Flurl/Util/CommonExtensions.cs +++ b/src/Flurl/Util/CommonExtensions.cs @@ -3,50 +3,51 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -#if NETSTANDARD +#if !NET40 using System.Reflection; #endif namespace Flurl.Util { - /// - /// CommonExtensions for objects. - /// - 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) { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); - - return - (obj is string) ? StringToKV((string)obj) : - (obj is IEnumerable) ? CollectionToKV((IEnumerable)obj) : - ObjectToKV(obj); - } + /// + /// CommonExtensions for objects. + /// + 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) { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + + return + (obj is string) ? StringToKV((string)obj) : + (obj is IEnumerable) ? CollectionToKV((IEnumerable)obj) : + ObjectToKV(obj); + } - /// - /// Returns a string that represents the current object, using CultureInfo.InvariantCulture where possible. - /// - public static string ToInvariantString(this object obj) { - // inspired by: http://stackoverflow.com/a/19570016/62600 + /// + /// Returns a string that represents the current object, using CultureInfo.InvariantCulture where possible. + /// + public static string ToInvariantString(this object obj) { + // inspired by: http://stackoverflow.com/a/19570016/62600 - var c = obj as IConvertible; +#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); - var f = obj as IFormattable; - if (f != null) - return f.ToString(null, CultureInfo.InvariantCulture); - - return obj.ToString(); - } + return obj.ToString(); + } /// /// Splits at the first occurence of the given seperator. @@ -55,52 +56,63 @@ public static string ToInvariantString(this object obj) { /// 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 }; - - var i = s.IndexOf(separator); - if (i == -1) - return new[] { s }; - - return new[] { s.Substring(0, i), s.Substring(i + 1) }; - } - - private static IEnumerable> StringToKV(string s) { - return Url.ParseQueryParams(s).Select(p => new KeyValuePair(p.Name, p.Value)); - } - - private static IEnumerable> ObjectToKV(object obj) { - return from prop in obj.GetType().GetProperties() + // Needed because full PCL profile doesn't support Split(char[], int) (#119) + if (string.IsNullOrEmpty(s)) + return new[] { s }; + + var i = s.IndexOf(separator); + if (i == -1) + return new[] { s }; + + return new[] { s.Substring(0, i), s.Substring(i + 1) }; + } + + private static IEnumerable> StringToKV(string s) { + return Url.ParseQueryParams(s).Select(p => new KeyValuePair(p.Name, p.Value)); + } + + 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); - } - - private static IEnumerable> CollectionToKV(IEnumerable col) { - // Accepts KeyValuePairs or any arbitrary types that contain a property called "Key" or "Name" and a property called "Value". - foreach (var item in col) { - if (item == null) - continue; - - string key; - object val; - - var type = item.GetType(); - var keyProp = type.GetProperty("Key") ?? type.GetProperty("key") ?? type.GetProperty("Name") ?? type.GetProperty("name"); +#endif + } + + private static IEnumerable> CollectionToKV(IEnumerable col) { + // Accepts KeyValuePairs or any arbitrary types that contain a property called "Key" or "Name" and a property called "Value". + foreach (var item in col) { + if (item == null) + continue; + + string key; + 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(); - val = valProp.GetValue(item, null); - } - else { - key = item.ToInvariantString(); - val = null; - } - - if (key != null) - yield return new KeyValuePair(key, val); - } - } - } + if (keyProp != null && valProp != null) { + key = keyProp.GetValue(item, null)?.ToInvariantString(); + val = valProp.GetValue(item, null); + } + else { + key = item.ToInvariantString(); + val = null; + } + + if (key != null) + yield return new KeyValuePair(key, val); + } + } + } } \ No newline at end of file From 77484834b524c53b6c033f987e55593a654ffa11 Mon Sep 17 00:00:00 2001 From: Nikolay Molchanov Date: Fri, 21 Jul 2017 22:58:03 +0300 Subject: [PATCH 03/56] [edit] update package tester deps --- .../PackageTester.NET45/PackageTester.NET45.csproj | 8 ++++---- PackageTesters/PackageTester.NET45/packages.config | 4 ++-- .../PackageTester.NET461/PackageTester.NET461.csproj | 8 ++++---- PackageTesters/PackageTester.NET461/packages.config | 4 ++-- .../PackageTester.NETCore/PackageTester.NETCore.csproj | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/PackageTesters/PackageTester.NET45/PackageTester.NET45.csproj b/PackageTesters/PackageTester.NET45/PackageTester.NET45.csproj index abf86a1e..6a58e0d7 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\lib\net40\Flurl.dll + + ..\..\packages\Flurl.3.0.0\lib\net40\Flurl.dll - - ..\..\packages\Flurl.Http.1.2.0\lib\net45\Flurl.Http.dll + + ..\..\packages\Flurl.Http.2.0.0\lib\net45\Flurl.Http.dll ..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll diff --git a/PackageTesters/PackageTester.NET45/packages.config b/PackageTesters/PackageTester.NET45/packages.config index 39ae0a02..d9e0f962 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..9566a523 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.3.0.0\lib\net40\Flurl.dll - - ..\..\packages\Flurl.Http.1.2.0-pre\lib\net45\Flurl.Http.dll + + ..\..\packages\Flurl.Http.2.0.0\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..cab6b76f 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..33667649 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 From 6c6e9a5f432cfd08dd4b02b342c25d8d4e2b87b7 Mon Sep 17 00:00:00 2001 From: Nikolay Molchanov Date: Fri, 21 Jul 2017 23:53:38 +0300 Subject: [PATCH 04/56] [add] full Flurl API for netstd 1.1 --- .../Content/CapturedMultipartContent.cs | 3 +- src/Flurl.Http/Content/FileContent.cs | 4 +-- src/Flurl.Http/DownloadExtensions.cs | 4 +-- src/Flurl.Http/FileUtil.cs | 28 +++++++++++++++++-- src/Flurl.Http/Flurl.Http.csproj | 9 ++++-- 5 files changed, 34 insertions(+), 14 deletions(-) diff --git a/src/Flurl.Http/Content/CapturedMultipartContent.cs b/src/Flurl.Http/Content/CapturedMultipartContent.cs index c515563c..934d75b0 100644 --- a/src/Flurl.Http/Content/CapturedMultipartContent.cs +++ b/src/Flurl.Http/Content/CapturedMultipartContent.cs @@ -109,7 +109,6 @@ public CapturedMultipartContent AddFile(string name, Stream stream, string fileN return AddInternal(name, content, fileName); } -#if !NETSTANDARD1_1 /// /// Adds a file to the multipart request from a local path. /// @@ -125,7 +124,7 @@ public CapturedMultipartContent AddFile(string name, string path, string mediaTy content.Headers.ContentType = new MediaTypeHeaderValue(mediaType); return AddInternal(name, content, fileName); } -#endif + private CapturedMultipartContent AddInternal(string name, HttpContent content, string fileName) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("name must not be empty", nameof(name)); diff --git a/src/Flurl.Http/Content/FileContent.cs b/src/Flurl.Http/Content/FileContent.cs index 5833946d..914e4a12 100644 --- a/src/Flurl.Http/Content/FileContent.cs +++ b/src/Flurl.Http/Content/FileContent.cs @@ -3,7 +3,6 @@ using System.Net.Http; using System.Threading.Tasks; -#if !NETSTANDARD1_1 namespace Flurl.Http.Content { /// @@ -50,5 +49,4 @@ protected override bool TryComputeLength(out long length) { return false; } } -} -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/src/Flurl.Http/DownloadExtensions.cs b/src/Flurl.Http/DownloadExtensions.cs index b5c9fad5..c12901e2 100644 --- a/src/Flurl.Http/DownloadExtensions.cs +++ b/src/Flurl.Http/DownloadExtensions.cs @@ -2,7 +2,6 @@ using System.Net.Http; using System.Threading.Tasks; -#if !NETSTANDARD1_1 namespace Flurl.Http { /// @@ -68,5 +67,4 @@ public static Task DownloadFileAsync(this Url url, string localFolderPat return new FlurlClient(url, true).DownloadFileAsync(localFolderPath, localFileName, bufferSize); } } -} -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/src/Flurl.Http/FileUtil.cs b/src/Flurl.Http/FileUtil.cs index a536254a..b01ff538 100644 --- a/src/Flurl.Http/FileUtil.cs +++ b/src/Flurl.Http/FileUtil.cs @@ -1,11 +1,33 @@ using System.IO; +#if NETSTANDARD1_1 +using System.Linq; +#endif using System.Threading.Tasks; -#if !NETSTANDARD1_1 namespace Flurl.Http { internal static class FileUtil { +#if NETSTANDARD1_1 + internal static string GetFileName(string path) { + return path?.Split(PCLStorage.PortablePath.DirectorySeparatorChar).Last(); + } + + internal static string CombinePath(params string[] paths) { + return PCLStorage.PortablePath.Combine(paths); + } + + internal static async Task OpenReadAsync(string path, int bufferSize) { + var file = await PCLStorage.FileSystem.Current.GetFileFromPathAsync(path).ConfigureAwait(false); + return await file.OpenAsync(PCLStorage.FileAccess.Read).ConfigureAwait(false); + } + + internal static async Task OpenWriteAsync(string folderPath, string fileName, int bufferSize) { + var folder = await PCLStorage.FileSystem.Current.LocalStorage.CreateFolderAsync(folderPath, PCLStorage.CreationCollisionOption.OpenIfExists).ConfigureAwait(false); + var file = await folder.CreateFileAsync(fileName, PCLStorage.CreationCollisionOption.ReplaceExisting).ConfigureAwait(false); + return await file.OpenAsync(PCLStorage.FileAccess.ReadAndWrite).ConfigureAwait(false); + } +#else internal static string GetFileName(string path) { return Path.GetFileName(path); } @@ -23,6 +45,6 @@ internal static Task OpenWriteAsync(string folderPath, string fileName, var filePath = Path.Combine(folderPath, fileName); return Task.FromResult(new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize, useAsync: true)); } +#endif } -} -#endif \ No newline at end of file +} \ No newline at end of file diff --git a/src/Flurl.Http/Flurl.Http.csproj b/src/Flurl.Http/Flurl.Http.csproj index 79b20536..0e2a9876 100644 --- a/src/Flurl.Http/Flurl.Http.csproj +++ b/src/Flurl.Http/Flurl.Http.csproj @@ -49,7 +49,7 @@ 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 + false @@ -67,14 +67,17 @@ - - + + + portable-net45+win8+wp8 + + From 786a9edff4e3bb46a88c49509fd5a43c9733407e Mon Sep 17 00:00:00 2001 From: Nikolay Molchanov Date: Sat, 22 Jul 2017 00:05:14 +0300 Subject: [PATCH 05/56] [edit] tab\space conflict remove --- .../Content/CapturedMultipartContent.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Flurl.Http/Content/CapturedMultipartContent.cs b/src/Flurl.Http/Content/CapturedMultipartContent.cs index 934d75b0..a10c758b 100644 --- a/src/Flurl.Http/Content/CapturedMultipartContent.cs +++ b/src/Flurl.Http/Content/CapturedMultipartContent.cs @@ -109,15 +109,15 @@ public CapturedMultipartContent AddFile(string name, Stream stream, string fileN return AddInternal(name, content, fileName); } - /// - /// Adds a file to the multipart request from a local path. - /// - /// The control name of the part. - /// The local path to the file. - /// The media type of the file. - /// The buffer size of the stream upload in bytes. Defaults to 4096. - /// This CapturedMultipartContent instance (supports method chaining). - public CapturedMultipartContent AddFile(string name, string path, string mediaType = null, int bufferSize = 4096) { + /// + /// Adds a file to the multipart request from a local path. + /// + /// The control name of the part. + /// The local path to the file. + /// The media type of the file. + /// The buffer size of the stream upload in bytes. Defaults to 4096. + /// This CapturedMultipartContent instance (supports method chaining). + public CapturedMultipartContent AddFile(string name, string path, string mediaType = null, int bufferSize = 4096) { var fileName = FileUtil.GetFileName(path); var content = new FileContent(path, bufferSize); if (mediaType != null) @@ -125,7 +125,7 @@ public CapturedMultipartContent AddFile(string name, string path, string mediaTy return AddInternal(name, content, fileName); } - private CapturedMultipartContent AddInternal(string name, HttpContent content, string fileName) { + private CapturedMultipartContent AddInternal(string name, HttpContent content, string fileName) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("name must not be empty", nameof(name)); From e08d8ee5795087c7d12819da2e6659c4db3e150b Mon Sep 17 00:00:00 2001 From: Nikolay Molchanov Date: Sat, 22 Jul 2017 00:25:23 +0300 Subject: [PATCH 06/56] [edit] tab\space conflict remove 2 --- src/Flurl.Http/Content/FileContent.cs | 34 +++++++++--------- src/Flurl.Http/DownloadExtensions.cs | 36 +++++++++---------- src/Flurl.Http/FileUtil.cs | 4 +-- .../HttpResponseMessageExtensions.cs | 2 +- src/Flurl.Http/Testing/HttpTest.cs | 2 +- src/Flurl/Util/CommonExtensions.cs | 6 ++-- 6 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/Flurl.Http/Content/FileContent.cs b/src/Flurl.Http/Content/FileContent.cs index 914e4a12..caa715d7 100644 --- a/src/Flurl.Http/Content/FileContent.cs +++ b/src/Flurl.Http/Content/FileContent.cs @@ -5,10 +5,10 @@ namespace Flurl.Http.Content { - /// - /// Represents HTTP content based on a local file. Typically used with PostMultipartAsync for uploading files. - /// - public class FileContent : HttpContent + /// + /// Represents HTTP content based on a local file. Typically used with PostMultipartAsync for uploading files. + /// + public class FileContent : HttpContent { /// /// The local file path. @@ -27,24 +27,24 @@ public FileContent(string path, int bufferSize = 4096) { _bufferSize = bufferSize; } - /// - /// Serializes to stream asynchronous. - /// - /// The stream. - /// The context. - /// - protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) { + /// + /// Serializes to stream asynchronous. + /// + /// The stream. + /// The context. + /// + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) { using (var source = await FileUtil.OpenReadAsync(Path, _bufferSize).ConfigureAwait(false)) { await source.CopyToAsync(stream, _bufferSize).ConfigureAwait(false); } } - /// - /// Tries the length of the compute. - /// - /// The length. - /// - protected override bool TryComputeLength(out long length) { + /// + /// Tries the length of the compute. + /// + /// The length. + /// + protected override bool TryComputeLength(out long length) { length = -1; return false; } diff --git a/src/Flurl.Http/DownloadExtensions.cs b/src/Flurl.Http/DownloadExtensions.cs index c12901e2..6be61bb2 100644 --- a/src/Flurl.Http/DownloadExtensions.cs +++ b/src/Flurl.Http/DownloadExtensions.cs @@ -9,15 +9,15 @@ namespace Flurl.Http /// public static class DownloadExtensions { - /// - /// Asynchronously downloads a file at the specified URL. - /// - /// The flurl client. - /// 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) { + /// + /// Asynchronously downloads a file at the specified URL. + /// + /// The flurl client. + /// 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) { if (localFileName == null) localFileName = client.Url.Path.Split('/').Last(); @@ -43,15 +43,15 @@ public static async Task DownloadFileAsync(this IFlurlClient client, str } } - /// - /// Asynchronously downloads a file at the specified URL. - /// - /// The Url. - /// 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 Task DownloadFileAsync(this string url, string localFolderPath, string localFileName = null, int bufferSize = 4096) { + /// + /// Asynchronously downloads a file at the specified URL. + /// + /// The Url. + /// 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 Task DownloadFileAsync(this string url, string localFolderPath, string localFileName = null, int bufferSize = 4096) { return new FlurlClient(url, true).DownloadFileAsync(localFolderPath, localFileName, bufferSize); } diff --git a/src/Flurl.Http/FileUtil.cs b/src/Flurl.Http/FileUtil.cs index b01ff538..c565b0b9 100644 --- a/src/Flurl.Http/FileUtil.cs +++ b/src/Flurl.Http/FileUtil.cs @@ -36,7 +36,7 @@ internal static string CombinePath(params string[] paths) { return Path.Combine(paths); } - internal static Task OpenReadAsync(string path, int bufferSize) { + internal static Task OpenReadAsync(string path, int bufferSize) { return Task.FromResult(new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, useAsync: true)); } @@ -46,5 +46,5 @@ internal static Task OpenWriteAsync(string folderPath, string fileName, return Task.FromResult(new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize, useAsync: true)); } #endif - } + } } \ No newline at end of file diff --git a/src/Flurl.Http/HttpResponseMessageExtensions.cs b/src/Flurl.Http/HttpResponseMessageExtensions.cs index 212253b3..ffa4e087 100644 --- a/src/Flurl.Http/HttpResponseMessageExtensions.cs +++ b/src/Flurl.Http/HttpResponseMessageExtensions.cs @@ -65,7 +65,7 @@ public static async Task> ReceiveJsonList(this Tasks = await url.PostAsync(data).ReceiveString() public static async Task ReceiveString(this Task response) { #if NETSTANDARD1_3 - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); #endif var resp = await response.ConfigureAwait(false); if (resp == null) return null; diff --git a/src/Flurl.Http/Testing/HttpTest.cs b/src/Flurl.Http/Testing/HttpTest.cs index 4bf9026f..2f7cee33 100644 --- a/src/Flurl.Http/Testing/HttpTest.cs +++ b/src/Flurl.Http/Testing/HttpTest.cs @@ -156,5 +156,5 @@ public void Dispose() { 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/Util/CommonExtensions.cs b/src/Flurl/Util/CommonExtensions.cs index 3dd92c8d..2f044ba0 100644 --- a/src/Flurl/Util/CommonExtensions.cs +++ b/src/Flurl/Util/CommonExtensions.cs @@ -39,13 +39,13 @@ public static string ToInvariantString(this object obj) { #if !NETSTANDARD1_0 var c = obj as IConvertible; - if (c != null) - return c.ToString(CultureInfo.InvariantCulture); + if (c != null) + return c.ToString(CultureInfo.InvariantCulture); #endif var f = obj as IFormattable; if (f != null) return f.ToString(null, CultureInfo.InvariantCulture); - + return obj.ToString(); } From 1ba29828b5ad96d83ced10ec1107b1af44ed0b5e Mon Sep 17 00:00:00 2001 From: Nikolay Molchanov Date: Sat, 22 Jul 2017 00:34:59 +0300 Subject: [PATCH 07/56] [edit] tab\space conflict remove 3 --- src/Flurl/Util/CommonExtensions.cs | 164 ++++++++++++++--------------- 1 file changed, 82 insertions(+), 82 deletions(-) diff --git a/src/Flurl/Util/CommonExtensions.cs b/src/Flurl/Util/CommonExtensions.cs index 2f044ba0..abd1307f 100644 --- a/src/Flurl/Util/CommonExtensions.cs +++ b/src/Flurl/Util/CommonExtensions.cs @@ -9,110 +9,110 @@ namespace Flurl.Util { - /// - /// CommonExtensions for objects. - /// - 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) { - if (obj == null) - throw new ArgumentNullException(nameof(obj)); + /// + /// CommonExtensions for objects. + /// + 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) { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); - return - (obj is string) ? StringToKV((string)obj) : - (obj is IEnumerable) ? CollectionToKV((IEnumerable)obj) : - ObjectToKV(obj); - } + return + (obj is string) ? StringToKV((string)obj) : + (obj is IEnumerable) ? CollectionToKV((IEnumerable)obj) : + ObjectToKV(obj); + } - /// - /// Returns a string that represents the current object, using CultureInfo.InvariantCulture where possible. - /// - public static string ToInvariantString(this object obj) { - // inspired by: http://stackoverflow.com/a/19570016/62600 + /// + /// Returns a string that represents the current object, using CultureInfo.InvariantCulture where possible. + /// + 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); + 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); - - return obj.ToString(); - } + var f = obj as IFormattable; + if (f != null) + return f.ToString(null, CultureInfo.InvariantCulture); + + 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) { - // Needed because full PCL profile doesn't support Split(char[], int) (#119) - if (string.IsNullOrEmpty(s)) - return new[] { s }; + /// + /// 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 }; - var i = s.IndexOf(separator); - if (i == -1) - return new[] { s }; + var i = s.IndexOf(separator); + if (i == -1) + return new[] { s }; - return new[] { s.Substring(0, i), s.Substring(i + 1) }; - } + return new[] { s.Substring(0, i), s.Substring(i + 1) }; + } - private static IEnumerable> StringToKV(string s) { - return Url.ParseQueryParams(s).Select(p => new KeyValuePair(p.Name, p.Value)); - } + private static IEnumerable> StringToKV(string s) { + return Url.ParseQueryParams(s).Select(p => new KeyValuePair(p.Name, p.Value)); + } - private static IEnumerable> ObjectToKV(object obj) { + 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); + 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() + 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) { - // Accepts KeyValuePairs or any arbitrary types that contain a property called "Key" or "Name" and a property called "Value". - foreach (var item in col) { - if (item == null) - continue; + private static IEnumerable> CollectionToKV(IEnumerable col) { + // Accepts KeyValuePairs or any arbitrary types that contain a property called "Key" or "Name" and a property called "Value". + foreach (var item in col) { + if (item == null) + continue; - string key; - object val; + string key; + object val; - var type = item.GetType(); + 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"); + 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 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(); - val = valProp.GetValue(item, null); - } - else { - key = item.ToInvariantString(); - val = null; - } + if (keyProp != null && valProp != null) { + key = keyProp.GetValue(item, null)?.ToInvariantString(); + val = valProp.GetValue(item, null); + } + else { + key = item.ToInvariantString(); + val = null; + } - if (key != null) - yield return new KeyValuePair(key, val); - } - } - } + if (key != null) + yield return new KeyValuePair(key, val); + } + } + } } \ No newline at end of file From 12803b481f435cf78eab5025b3203f5defea78ee Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Fri, 28 Jul 2017 08:24:58 -0500 Subject: [PATCH 08/56] NoOpTask class is overkill now that platform sniffing is factored out --- src/Flurl.Http/FlurlHttp.cs | 7 ++++--- src/Flurl.Http/NoOpTask.cs | 9 --------- 2 files changed, 4 insertions(+), 12 deletions(-) delete mode 100644 src/Flurl.Http/NoOpTask.cs diff --git a/src/Flurl.Http/FlurlHttp.cs b/src/Flurl.Http/FlurlHttp.cs index 506c2591..5c1b86d1 100644 --- a/src/Flurl.Http/FlurlHttp.cs +++ b/src/Flurl.Http/FlurlHttp.cs @@ -11,6 +11,7 @@ namespace Flurl.Http public static class FlurlHttp { private static readonly object _configLock = new object(); + private static readonly Task _completedTask = Task.FromResult(0); private static Lazy _settings = new Lazy(() => new FlurlHttpSettings()); @@ -41,7 +42,7 @@ public static Task RaiseEventAsync(HttpRequestMessage request, FlurlEventType ev var settings = call?.Settings; if (settings == null) - return NoOpTask.Instance; + return _completedTask; switch (eventType) { case FlurlEventType.BeforeCall: @@ -51,7 +52,7 @@ public static Task RaiseEventAsync(HttpRequestMessage request, FlurlEventType ev case FlurlEventType.OnError: return HandleEventAsync(settings.OnError, settings.OnErrorAsync, call); default: - return NoOpTask.Instance; + return _completedTask; } } @@ -60,7 +61,7 @@ private static Task HandleEventAsync(Action syncHandler, Func Date: Fri, 4 Aug 2017 16:34:52 -0500 Subject: [PATCH 09/56] major changes for Flurl.Http 2.0. better HttpClient reuse, request-level headers and settings, refactoring & cleanup --- Build/build.cmd | 2 +- Build/test.cmd | 4 +- Test/Flurl.Test/Http/ClientConfigTests.cs | 182 ++---- Test/Flurl.Test/Http/ClientLifetimeTests.cs | 48 -- Test/Flurl.Test/Http/FlurlClientTests.cs | 17 +- Test/Flurl.Test/Http/GetTests.cs | 2 +- Test/Flurl.Test/Http/GlobalConfigTests.cs | 49 +- Test/Flurl.Test/Http/RealHttpTests.cs | 51 +- Test/Flurl.Test/Http/TestingTests.cs | 10 +- ...nMethodModel.cs => HttpExtensionMethod.cs} | 10 +- src/Flurl.Http.CodeGen/Program.cs | 63 +- src/Flurl.Http.CodeGen/UrlExtensionMethod.cs | 73 +++ ...HttpExtensions.cs => AutoGenExtensions.cs} | 594 +++++++++++++----- src/Flurl.Http/ClientConfigExtensions.cs | 413 ------------ .../DefaultFlurlClientFactory.cs | 44 ++ .../Configuration/DefaultHttpClientFactory.cs | 30 - .../Configuration/FlurlHttpSettings.cs | 139 ++-- ...lientFactory.cs => IFlurlClientFactory.cs} | 16 +- src/Flurl.Http/CookieExtensions.cs | 176 +++--- src/Flurl.Http/DownloadExtensions.cs | 37 +- src/Flurl.Http/FlurlClient.cs | 232 ++----- src/Flurl.Http/FlurlHttp.cs | 7 +- src/Flurl.Http/FlurlRequest.cs | 175 ++++++ src/Flurl.Http/HeaderExtensions.cs | 67 ++ src/Flurl.Http/IHttpSettingsContainer.cs | 31 + src/Flurl.Http/MultipartExtensions.cs | 18 +- src/Flurl.Http/SettingsExtensions.cs | 137 ++++ src/Flurl.Http/Testing/HttpTest.cs | 40 +- .../Testing/TestFlurlClientFactory.cs | 32 + .../Testing/TestHttpClientFactory.cs | 19 - src/Flurl/Util/CommonExtensions.cs | 15 +- 31 files changed, 1430 insertions(+), 1303 deletions(-) delete mode 100644 Test/Flurl.Test/Http/ClientLifetimeTests.cs rename src/Flurl.Http.CodeGen/{ExtensionMethodModel.cs => HttpExtensionMethod.cs} (90%) create mode 100644 src/Flurl.Http.CodeGen/UrlExtensionMethod.cs rename src/Flurl.Http/{HttpExtensions.cs => AutoGenExtensions.cs} (61%) delete mode 100644 src/Flurl.Http/ClientConfigExtensions.cs create mode 100644 src/Flurl.Http/Configuration/DefaultFlurlClientFactory.cs delete mode 100644 src/Flurl.Http/Configuration/DefaultHttpClientFactory.cs rename src/Flurl.Http/Configuration/{IHttpClientFactory.cs => IFlurlClientFactory.cs} (59%) create mode 100644 src/Flurl.Http/FlurlRequest.cs create mode 100644 src/Flurl.Http/HeaderExtensions.cs create mode 100644 src/Flurl.Http/IHttpSettingsContainer.cs create mode 100644 src/Flurl.Http/SettingsExtensions.cs create mode 100644 src/Flurl.Http/Testing/TestFlurlClientFactory.cs delete mode 100644 src/Flurl.Http/Testing/TestHttpClientFactory.cs diff --git a/Build/build.cmd b/Build/build.cmd index 798519f9..a5765a15 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\AutoGenExtensions.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/Test/Flurl.Test/Http/ClientConfigTests.cs b/Test/Flurl.Test/Http/ClientConfigTests.cs index 8fdb3906..beab60bd 100644 --- a/Test/Flurl.Test/Http/ClientConfigTests.cs +++ b/Test/Flurl.Test/Http/ClientConfigTests.cs @@ -11,85 +11,69 @@ namespace Flurl.Test.Http { [TestFixture] - public class ClientConfigTestsBase : ConfigTestsBase + public class ClientConfigTestsBase //: ConfigTestsBase { - protected override FlurlHttpSettings GetSettings() - { - return GetClient().Settings; - } + //private FlurlClient _client = new FlurlClient(); - [Test] + //protected override FlurlHttpSettings GetSettings(FlurlRequest req) { + // return _client.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)); + var fc = new FlurlClient().WithTimeout(TimeSpan.FromSeconds(15)); + Assert.AreEqual(TimeSpan.FromSeconds(15), fc.Settings.Timeout); } - [Test] + [Test] public void can_set_timeout_in_seconds() { - var client = "http://www.api.com".WithTimeout(15); - Assert.AreEqual(client.HttpClient.Timeout, TimeSpan.FromSeconds(15)); + var client = new FlurlClient().WithTimeout(15); + Assert.AreEqual(client.Settings.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" }); + var req = new FlurlClient().WithHeader("a", 1); + Assert.AreEqual(1, req.Headers.Count); + Assert.AreEqual(1, req.Headers["a"]); } - [Test] + [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" }); + var req = new FlurlClient().WithHeaders(new { a = "b", one = 2 }); + Assert.AreEqual(2, req.Headers.Count); + Assert.AreEqual("b", req.Headers["a"]); + Assert.AreEqual(2, req.Headers["one"]); } - [Test] + [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" }); + var req = new FlurlClient().WithHeaders(new Dictionary { { "a", "b" }, { "one", 2 } }); + Assert.AreEqual(2, req.Headers.Count); + Assert.AreEqual("b", req.Headers["a"]); + Assert.AreEqual(2, req.Headers["one"]); } - [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 req = new FlurlClient().WithOAuthBearerToken("mytoken"); + Assert.AreEqual(1, req.Headers.Count); + Assert.AreEqual("Bearer mytoken", req.Headers["Authorization"]); + } - [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 void can_setup_basic_auth() { + var req = new FlurlClient().WithBasicAuth("user", "pass"); + Assert.AreEqual(1, req.Headers.Count); + Assert.AreEqual("Basic dXNlcjpwYXNz", req.Headers["Authorization"]); + } - [Test] + [Test] public async Task can_allow_specific_http_status() { using (var test = new HttpTest()) @@ -109,10 +93,10 @@ public void can_clear_non_success_status() { test.RespondWith("I'm a teapot", 418); // allow 4xx - var client = "http://www.api.com".AllowHttpStatus("4xx"); + var client = new FlurlClient().AllowHttpStatus("4xx"); // but then disallow it client.Settings.AllowedHttpStatusRange = null; - Assert.ThrowsAsync(async () => await client.GetAsync()); + Assert.ThrowsAsync(async () => await client.WithUrl("http://api.com").GetAsync()); } } @@ -137,86 +121,50 @@ public async Task can_allow_any_http_status() [Test] public void can_override_settings_fluently() { - using (var test = new HttpTest()) - { - FlurlHttp.GlobalSettings.AllowedHttpStatusRange = "*"; + using (var test = new HttpTest()) { + var client = new FlurlClient().WithSettings(s => s.AllowedHttpStatusRange = "*"); test.RespondWith("epic fail", 500); - Assert.ThrowsAsync(async () => await "http://www.api.com".ConfigureClient(c => c.AllowedHttpStatusRange = "2xx").GetAsync()); + Assert.ThrowsAsync(async () => await "http://www.api.com" + .WithSettings(c => c.AllowedHttpStatusRange = "2xx") + .WithClient(client) // client-level settings shouldn't win + .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() + public void WithUrl_shares_client_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"); + var client = new FlurlClient().WithCookie("mycookie", "123"); + var req1 = client.WithUrl("http://www.api.com/for-req1"); + var req2 = client.WithUrl("http://www.api.com/for-req2"); + var req3 = client.WithUrl("http://www.api.com/for-req3"); - 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.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_doesnt_propagate_HttpClient_disposal() + public void WithClient_shares_client_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); - - client2.Dispose(); - client3.Dispose(); - - CollectionAssert.IsEmpty(client2.Cookies); - CollectionAssert.IsEmpty(client3.Cookies); + var client = new FlurlClient().WithCookie("mycookie", "123"); + var req1 = "http://www.api.com/for-req1".WithClient(client); + var req2 = "http://www.api.com/for-req2".WithClient(client); + var req3 = "http://www.api.com/for-req3".WithClient(client); - CollectionAssert.IsNotEmpty(client1.Cookies); - CollectionAssert.IsNotEmpty(client4.Cookies); + 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 fc = new FlurlClient().WithUrl(uri); - Assert.AreEqual(uri.ToString(), fc.Url.ToString()); + var req = new FlurlClient().WithUrl(uri); + Assert.AreEqual(uri.ToString(), req.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..f5b2fc55 100644 --- a/Test/Flurl.Test/Http/FlurlClientTests.cs +++ b/Test/Flurl.Test/Http/FlurlClientTests.cs @@ -11,22 +11,21 @@ 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).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})"); } } } diff --git a/Test/Flurl.Test/Http/GetTests.cs b/Test/Flurl.Test/Http/GetTests.cs index 1ae6e6f3..5b8e069c 100644 --- a/Test/Flurl.Test/Http/GetTests.cs +++ b/Test/Flurl.Test/Http/GetTests.cs @@ -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) + .WithSettings(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 index 182009f1..4190a8b9 100644 --- a/Test/Flurl.Test/Http/GlobalConfigTests.cs +++ b/Test/Flurl.Test/Http/GlobalConfigTests.cs @@ -17,24 +17,17 @@ 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); + public void can_provide_custom_client_factory() { + GetSettings().FlurlClientFactory = new SomeCustomFlurlClientFactory(); + var client = new FlurlClient(); + Assert.IsInstanceOf(client.HttpClient); + Assert.IsInstanceOf(client.HttpMessageHandler); } [Test] @@ -43,7 +36,7 @@ public async Task can_allow_non_success_status() { GetSettings().AllowedHttpStatusRange = "4xx"; test.RespondWith("I'm a teapot", 418); try { - var result = await GetClient().GetAsync(); + var result = await "http://www.api.com".GetAsync(); Assert.IsFalse(result.IsSuccessStatusCode); } catch (Exception) { @@ -62,7 +55,7 @@ public async Task can_set_pre_callback() { callbackCalled = true; }; Assert.IsFalse(callbackCalled); - await GetClient().GetAsync(); + await "http://www.api.com".GetAsync(); Assert.IsTrue(callbackCalled); } } @@ -77,7 +70,7 @@ public async Task can_set_post_callback() { callbackCalled = true; }; Assert.IsFalse(callbackCalled); - await GetClient().GetAsync(); + await "http://www.api.com".GetAsync(); Assert.IsTrue(callbackCalled); } } @@ -95,7 +88,7 @@ public async Task can_set_error_callback(bool markExceptionHandled) { }; Assert.IsFalse(callbackCalled); try { - await GetClient().GetAsync(); + await "http://www.api.com".GetAsync(); Assert.IsTrue(callbackCalled, "OnError was never called"); Assert.IsTrue(markExceptionHandled, "ExceptionHandled was marked false in callback, but exception was not propagated."); } @@ -114,7 +107,7 @@ public async Task can_disable_exception_behavior() { }; test.RespondWith("server error", 500); try { - var result = await GetClient().GetAsync(); + var result = await "http://www.api.com".GetAsync(); Assert.IsFalse(result.IsSuccessStatusCode); } catch (FlurlHttpException) { @@ -123,26 +116,30 @@ public async Task can_disable_exception_behavior() { } } - private class SomeCustomHttpClientFactory : IHttpClientFactory + private class SomeCustomFlurlClientFactory : IFlurlClientFactory { - public HttpClient CreateClient(Url url, HttpMessageHandler handler) { + public HttpClient CreateHttpClient(HttpMessageHandler handler) { return new SomeCustomHttpClient(); } public HttpMessageHandler CreateMessageHandler() { return new SomeCustomMessageHandler(); } + + public IFlurlClient GetClient(Url url) { + return new FlurlClient(); + } } private class SomeCustomHttpClient : HttpClient { } private class SomeCustomMessageHandler : HttpClientHandler { } } - [TestFixture] - public class GlobalConfigTestsBase : ConfigTestsBase - { - protected override FlurlHttpSettings GetSettings() { - return FlurlHttp.GlobalSettings; - } - } + //[TestFixture] + //public class GlobalConfigTestsBase : ConfigTestsBase + //{ + // protected override FlurlHttpSettings GetSettings() { + // return FlurlHttp.GlobalSettings; + // } + //} } diff --git a/Test/Flurl.Test/Http/RealHttpTests.cs b/Test/Flurl.Test/Http/RealHttpTests.cs index 762af15d..8e96a9a7 100644 --- a/Test/Flurl.Test/Http/RealHttpTests.cs +++ b/Test/Flurl.Test/Http/RealHttpTests.cs @@ -10,9 +10,7 @@ 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 @@ -50,33 +48,18 @@ public async Task can_get_response_cookies() { Assert.AreEqual("999", fc.Cookies["z"].Value); } - [Test] - public async Task cant_persist_cookies_without_resuing_client() { - var fc = "http://httpbin.org/cookies".WithCookie("z", 999); - // cookie should be set - Assert.AreEqual("999", fc.Cookies["z"].Value); - - await fc.HeadAsync(); - // FlurlClient was auto-disposed, so cookie should be gone - CollectionAssert.IsEmpty(fc.Cookies); - - // 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); + var req = "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); + Assert.AreEqual("999", req.Cookies["z"].Value); - await fc2.HeadAsync(); + await req.HeadAsync(); // FlurlClient should be re-used, so cookie should stick Assert.AreEqual("999", fc.Cookies["z"].Value); - Assert.AreEqual("999", fc2.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".WithClient(fc).GetJsonAsync(); @@ -177,34 +160,30 @@ 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".WithSettings(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("", await realTask); + Assert.AreNotEqual("fake!", await realTask); } } } \ No newline at end of file diff --git a/Test/Flurl.Test/Http/TestingTests.cs b/Test/Flurl.Test/Http/TestingTests.cs index e44806a4..2b253d07 100644 --- a/Test/Flurl.Test/Http/TestingTests.cs +++ b/Test/Flurl.Test/Http/TestingTests.cs @@ -140,7 +140,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) + .WithSettings(c => c.OnError = call => call.ExceptionHandled = true) .GetAsync(); Assert.IsNull(result); } @@ -159,10 +159,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); + 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/169 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..bd1ee20d 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\AutoGenExtensions.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 AutoGenExtensions") .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..ffc78161 --- /dev/null +++ b/src/Flurl.Http.CodeGen/UrlExtensionMethod.cs @@ -0,0 +1,73 @@ +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."); + 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("WithSettings", "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/HttpExtensions.cs b/src/Flurl.Http/AutoGenExtensions.cs similarity index 61% rename from src/Flurl.Http/HttpExtensions.cs rename to src/Flurl.Http/AutoGenExtensions.cs index 9ddbc857..724345b5 100644 --- a/src/Flurl.Http/HttpExtensions.cs +++ b/src/Flurl.Http/AutoGenExtensions.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 AutoGenExtensions { /// - /// 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,266 @@ 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. + /// The IFlurlRequest. + public static IFlurlRequest WithHeaders(this Url url, object headers) { + return new FlurlRequest(url).WithHeaders(headers); + } + /// + /// 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 WithSettings(this Url url, Action action) { + return new FlurlRequest(url).WithSettings(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. + /// The IFlurlRequest. + public static IFlurlRequest WithHeaders(this string url, object headers) { + return new FlurlRequest(url).WithHeaders(headers); + } + /// + /// 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 WithSettings(this string url, Action action) { + return new FlurlRequest(url).WithSettings(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/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/DefaultFlurlClientFactory.cs b/src/Flurl.Http/Configuration/DefaultFlurlClientFactory.cs new file mode 100644 index 00000000..9e4991fb --- /dev/null +++ b/src/Flurl.Http/Configuration/DefaultFlurlClientFactory.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Concurrent; +using System.Net.Http; + +namespace Flurl.Http.Configuration +{ + /// + /// Default implementation of IFlurlClientFactory used by Flurl.Http. Custom factories looking to extend + /// Flurl's behavior should inherit from this class, rather than implementing IFlurlClientFactory directly. + /// + public class DefaultFlurlClientFactory : IFlurlClientFactory + { + private static readonly ConcurrentDictionary _clients = new ConcurrentDictionary(); + + /// + /// Override in custom factory to customize the creation of HttpClient used in all Flurl HTTP calls. + /// In order not to lose Flurl.Http functionality, it is recommended to call base.CreateClient and + /// customize the result. + /// + public virtual HttpClient CreateHttpClient(HttpMessageHandler handler) { + return new HttpClient(new FlurlMessageHandler(handler)); + } + + /// + /// Override in custom factory to customize the creation of HttpClientHandler used in all Flurl HTTP calls. + /// In order not to lose Flurl.Http functionality, it is recommended to call base.CreateMessageHandler and + /// customize the result. + /// + public virtual HttpMessageHandler CreateMessageHandler() { + return new HttpClientHandler(); + } + + /// + /// 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 GetClient(Url url) { + var key = new Uri(url).Host; + return _clients.GetOrAdd(key, _ => new FlurlClient()); + } + } +} diff --git a/src/Flurl.Http/Configuration/DefaultHttpClientFactory.cs b/src/Flurl.Http/Configuration/DefaultHttpClientFactory.cs deleted file mode 100644 index 996fcdc6..00000000 --- a/src/Flurl.Http/Configuration/DefaultHttpClientFactory.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Net.Http; - -namespace Flurl.Http.Configuration -{ - /// - /// Default implementation of IHttpClientFactory used by FlurlHttp. The created HttpClient includes hooks - /// that enable FlurlHttp's testing features and respect its configuration settings. Therefore, custom factories - /// should inherit from this class, rather than implementing IHttpClientFactory directly. - /// - public class DefaultHttpClientFactory : IHttpClientFactory - { - /// - /// Override in custom factory to customize the creation of HttpClient used in all Flurl HTTP calls. - /// 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)); - } - - /// - /// Override in custom factory to customize the creation of HttpClientHandler used in all Flurl HTTP calls. - /// In order not to lose Flurl.Http functionality, it is recommended to call base.CreateMessageHandler and - /// customize the result. - /// - public virtual HttpMessageHandler CreateMessageHandler() { - return new HttpClientHandler(); - } - } -} diff --git a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs index 68910ca7..06b5bfd2 100644 --- a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs +++ b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs @@ -1,117 +1,160 @@ using System; -using System.Net.Http; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; using System.Threading.Tasks; +using Flurl.Util; 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 { - /// - /// Initializes a new instance of the class. - /// - public FlurlHttpSettings() { - ResetDefaults(); - } + // There are some tricky order of precedence rules (request > client > global) that are easier + // to keep track of via dictionary 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 _settings = new Dictionary(); + + private static readonly FlurlHttpSettings _defaults = new FlurlHttpSettings { + FlurlClientFactory = new DefaultFlurlClientFactory(), + CookiesEnabled = false, + JsonSerializer = new NewtonsoftJsonSerializer(null), + UrlEncodedSerializer = new DefaultUrlEncodedSerializer() + }; /// - /// Gets or sets value indicating whether to automatically dispose underlying HttpClient immediately after each call. + /// Gets or sets a factory used to create HttpClient object used in Flurl HTTP calls. Default value + /// is an instance of DefaultFlurlClientFactory. Custom factory implementations should generally + /// inherit from DefaultFlurlClientFactory, call base.CreateClient, and manipulate the returned HttpClient, + /// otherwise functionality such as callbacks and most testing features will be lost. /// - public bool AutoDispose { get; set; } + public IFlurlClientFactory FlurlClientFactory { + get => Get(() => FlurlClientFactory); + set => Set(() => FlurlClientFactory, value); + } /// - /// 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. + /// Clears all custom options and resets them to their default values. /// public void ResetDefaults() { - AutoDispose = false; - DefaultTimeout = new HttpClient().Timeout; - AllowedHttpStatusRange = null; - CookiesEnabled = false; - HttpClientFactory = new DefaultHttpClientFactory(); - JsonSerializer = new NewtonsoftJsonSerializer(null); - UrlEncodedSerializer = new DefaultUrlEncodedSerializer(); - BeforeCall = null; - BeforeCallAsync = null; - AfterCall = null; - AfterCallAsync = null; - OnError = null; - OnErrorAsync = null; + _settings.Clear(); + } + + private T Get(Expression> property) { + var p = (property.Body as MemberExpression).Member as PropertyInfo; + return + _settings.ContainsKey(p.Name) ? (T)_settings[p.Name] : + _defaults._settings.ContainsKey(p.Name) ? (T)_defaults._settings[p.Name] : + default(T); + } + + private void Set(Expression> property, T value) { + var p = (property.Body as MemberExpression).Member as PropertyInfo; + _settings[p.Name] = value; } /// - /// Clones this instance. + /// Merges other settings with this one. Overrides defaults, but does NOT override + /// this settings' explicitly set values. /// - public FlurlHttpSettings Clone() { - return (FlurlHttpSettings)MemberwiseClone(); + /// The settings to merge. + public FlurlHttpSettings Merge(FlurlHttpSettings other) { + _settings.Merge(other._settings); + return this; } } -} \ No newline at end of file +} diff --git a/src/Flurl.Http/Configuration/IHttpClientFactory.cs b/src/Flurl.Http/Configuration/IFlurlClientFactory.cs similarity index 59% rename from src/Flurl.Http/Configuration/IHttpClientFactory.cs rename to src/Flurl.Http/Configuration/IFlurlClientFactory.cs index 1a85eeec..d8ac937b 100644 --- a/src/Flurl.Http/Configuration/IHttpClientFactory.cs +++ b/src/Flurl.Http/Configuration/IFlurlClientFactory.cs @@ -5,23 +5,29 @@ namespace Flurl.Http.Configuration /// /// Interface defining creation of HttpClient and HttpMessageHandler used in all Flurl HTTP calls. /// Implementation can be added via FlurlHttp.Configure. However, in order not to lose much of - /// Flurl.Http's functionality, it's almost always best to inherit DefaultHttpClientFactory and + /// Flurl.Http's functionality, it's almost always best to inherit DefaultFlurlClientFactory and /// extend the base implementations, rather than implementing this interface directly. /// - public interface IHttpClientFactory + public interface IFlurlClientFactory { /// /// Creates the client. /// - /// The URL. - /// The handler. + /// The message handler being used in this call /// - HttpClient CreateClient(Url url, HttpMessageHandler handler); + HttpClient CreateHttpClient(HttpMessageHandler handler); /// /// Creates the message handler. /// /// HttpMessageHandler CreateMessageHandler(); + + /// + /// Strategy to create an HttpClient or reuse an exisitng one, based on URL being called. + /// + /// The URL being called. + /// + IFlurlClient GetClient(Url url); } } \ No newline at end of file diff --git a/src/Flurl.Http/CookieExtensions.cs b/src/Flurl.Http/CookieExtensions.cs index 88ec4554..0360524d 100644 --- a/src/Flurl.Http/CookieExtensions.cs +++ b/src/Flurl.Http/CookieExtensions.cs @@ -1,43 +1,34 @@ 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. + /// + /// Allows cookies to be sent and received. Not necessary to call when setting cookies via WithCookie/WithCookies. /// + /// The IFlurlClient. + /// This IFlurlClient. 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. - /// - public static IFlurlClient EnableCookies(this string url) { - return new FlurlClient(url).EnableCookies(); - } - - /// - /// Sets an HTTP cookie to be sent with all requests made with this FlurlClient. + /// Sets an HTTP cookie to be sent with all requests made with this IFlurlClient. /// - /// The client. + /// The IFlurlClient. /// The cookie to set. - /// The modified FlurlClient. - /// is null. + /// This IFlurlClient. public static IFlurlClient WithCookie(this IFlurlClient client, Cookie cookie) { client.Settings.CookiesEnabled = true; client.Cookies[cookie.Name] = cookie; @@ -45,76 +36,26 @@ public static IFlurlClient WithCookie(this IFlurlClient client, Cookie cookie) { } /// - /// 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 all requests made with this IFlurlClient. /// - /// 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); - } - - /// - /// 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 Url url, Cookie cookie) { - return new FlurlClient(url, true).WithCookie(cookie); - } - - /// - /// Sets an HTTP cookie to be sent with all requests made with this FlurlClient. - /// - /// The client. - /// cookie name. - /// cookie value. - /// cookie expiration (optional). If excluded, cookie only lives for duration of session. - /// The modified FlurlClient. - /// is null. + /// The IFlurlClient. + /// The cookie name. + /// The cookie value. + /// The cookie expiration (optional). If excluded, cookie only lives for duration of session. + /// This IFlurlClient. 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) { + /// + /// Sets HTTP cookies to be sent with all requests made with this IFlurlClient, based on property names/values of the provided object, or keys/values if object is a dictionary. + /// + /// The IFlurlClient. + /// 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 IFlurlClient WithCookies(this IFlurlClient client, object cookies, DateTime? expires = null) { if (cookies == null) return client; @@ -124,28 +65,51 @@ public static IFlurlClient WithCookies(this IFlurlClient client, object cookies, 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); + /// + /// Allows cookies to be sent and received with this request's IFlurlClient. Not necessary to call when setting cookies via WithCookie/WithCookies. + /// + /// The IFlurlRequest. + /// This IFlurlRequest. + public static IFlurlRequest EnableCookies(this IFlurlRequest request) { + request.Settings.CookiesEnabled = true; // a little awkward to have this at both the client and request. + request.Client.EnableCookies(); + return request; + } + + /// + /// Sets an HTTP cookie to be sent with this request's IFlurlClient. + /// + /// The IFlurlRequest. + /// The cookie to set. + /// This IFlurlRequest. + public static IFlurlRequest WithCookie(this IFlurlRequest request, Cookie cookie) { + request.Client.WithCookie(cookie); + return request; } - /// - /// 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); + /// + /// Sets an HTTP cookie to be sent with this request's IFlurlClient. + /// + /// The IFlurlRequest. + /// The cookie name. + /// The cookie value. + /// The cookie expiration (optional). If excluded, cookie only lives for duration of session. + /// This IFlurlRequest. + public static IFlurlRequest WithCookie(this IFlurlRequest request, string name, object value, DateTime? expires = null) { + request.Client.WithCookie(name, value, expires); + return request; + } + + /// + /// Sets HTTP cookies to be sent with this request's IFlurlClient, based on property names/values of the provided object, or keys/values if object is a dictionary. + /// + /// The 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 IFlurlRequest. + public static IFlurlRequest WithCookies(this IFlurlRequest request, object cookies, DateTime? expires = null) { + request.Client.WithCookies(cookies, expires); + return request; } } -} \ 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/FlurlClient.cs b/src/Flurl.Http/FlurlClient.cs index fffc2c07..b2736f96 100644 --- a/src/Flurl.Http/FlurlClient.cs +++ b/src/Flurl.Http/FlurlClient.cs @@ -13,49 +13,18 @@ namespace Flurl.Http /// /// Interface defining FlurlClient's contract (useful for mocking and DI) /// - public interface IFlurlClient : IDisposable { - /// - /// Creates a copy of this FlurlClient with a shared instance of HttpClient and HttpMessageHandler - /// - IFlurlClient Clone(); - - /// - /// Gets or sets the FlurlHttpSettings object used by this client. - /// - FlurlHttpSettings Settings { get; set; } - - /// - /// Gets or sets the URL to be called. - /// - Url Url { get; set; } - - /// - /// Collection of HttpCookies sent and received. - /// - IDictionary Cookies { get; } - + public interface IFlurlClient : IHttpSettingsContainer, IDisposable { /// /// 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. + /// to FlurlHttp.FlurlClientFactory. Reused for the life of the FlurlClient. /// HttpClient HttpClient { get; } /// /// Gets the HttpMessageHandler to be used in subsequent HTTP calls. Creation (when necessary) is delegated - /// to FlurlHttp.HttpClientFactory. + /// to FlurlHttp.FlurlClientFactory. /// HttpMessageHandler HttpMessageHandler { get; } - - /// - /// Creates and asynchronously sends an HttpRequestMethod, disposing HttpClient if AutoDispose it true. - /// 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); } /// @@ -63,12 +32,17 @@ public interface IFlurlClient : IDisposable { /// public class FlurlClient : IFlurlClient { + private HttpClient _httpClient; + private HttpMessageHandler _httpMessageHandler; + /// /// Initializes a new instance of the class. /// /// The FlurlHttpSettings associated with this instance. public FlurlClient(FlurlHttpSettings settings = null) { - Settings = settings ?? FlurlHttp.GlobalSettings.Clone(); + Settings = settings ?? new FlurlHttpSettings().Merge(HttpTest.Current?.Settings ?? FlurlHttp.GlobalSettings); + HttpMessageHandler = Settings.FlurlClientFactory.CreateMessageHandler(); + HttpClient = Settings.FlurlClientFactory.CreateHttpClient(HttpMessageHandler); } /// @@ -79,182 +53,60 @@ public FlurlClient(Action configure) : this() { configure(Settings); } - /// - /// Initializes a new instance of the class. - /// - /// The URL to call with this FlurlClient instance. - public FlurlClient(Url url) : this() { - Url = url; - } - - /// - /// Initializes a new instance of the class. - /// - /// The URL to call with this FlurlClient instance. - public FlurlClient(string url) : this() { - Url = new Url(url); - } - - /// - /// 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; - } - - /// - /// 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; - } - - /// - /// 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 - }; - } - - private HttpClient _httpClient; - private HttpMessageHandler _httpMessageHandler; - private FlurlClient _parent; - /// /// Gets or sets the FlurlHttpSettings object used by this client. /// public FlurlHttpSettings Settings { get; set; } /// - /// Gets or sets the URL to be called. + /// Collection of headers sent on all requests using this client. /// - public Url Url { get; set; } + public IDictionary Headers { get; } = new Dictionary(); /// - /// Collection of HttpCookies sent and received. + /// Collection of HttpCookies sent and received on all requests using this client. /// 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. + /// to FlurlHttp.FlurlClientFactory. Reused for the life of the FlurlClient. /// - public HttpClient HttpClient => EnsureHttpClient(); + public HttpClient HttpClient { get; } /// /// Gets the HttpMessageHandler to be used in subsequent HTTP calls. Creation (when necessary) is delegated - /// to FlurlHttp.HttpClientFactory. - /// - public HttpMessageHandler HttpMessageHandler => EnsureHttpMessageHandler(); - - 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; - } - - private HttpMessageHandler EnsureHttpMessageHandler(HttpMessageHandler handler = null) { - if (_httpMessageHandler == null) { - if (handler == null) { - handler = (HttpTest.Current == null) ? - Settings.HttpClientFactory.CreateMessageHandler() : - new FakeHttpMessageHandler(); - } - _httpMessageHandler = handler; - _parent?.EnsureHttpMessageHandler(handler); - } - return _httpMessageHandler; - } - - /// - /// Creates and asynchronously sends an HttpRequestMethod, disposing HttpClient if AutoDispose it true. - /// 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(request, Settings); - - 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 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; - } + /// to FlurlHttp.FlurlClientFactory. + /// + public HttpMessageHandler HttpMessageHandler { get; } + + //private HttpClient EnsureHttpClient(HttpClient hc = null) { + // if (_httpClient == null) { + // if (hc == null) { + // hc = Settings.FlurlClientFactory.CreateHttpClient(Url, HttpMessageHandler); + // hc.Timeout = Settings.DefaultTimeout; + // } + // _httpClient = hc; + // _parent?.EnsureHttpClient(hc); + // } + // return _httpClient; + //} + + //private HttpMessageHandler EnsureHttpMessageHandler(HttpMessageHandler handler = null) { + // if (_httpMessageHandler == null) { + // if (handler == null) { + // handler = (HttpTest.Current == null) ? + // Settings.FlurlClientFactory.CreateMessageHandler() : + // new FakeHttpMessageHandler(); + // } + // _httpMessageHandler = handler; + // _parent?.EnsureHttpMessageHandler(handler); + // } + // return _httpMessageHandler; + //} /// /// 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(); diff --git a/src/Flurl.Http/FlurlHttp.cs b/src/Flurl.Http/FlurlHttp.cs index 5c1b86d1..718aea54 100644 --- a/src/Flurl.Http/FlurlHttp.cs +++ b/src/Flurl.Http/FlurlHttp.cs @@ -19,9 +19,7 @@ public static class FlurlHttp /// /// 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 FlurlHttpSettings GlobalSettings => _settings.Value; /// /// Provides thread-safe access to Flurl.Http's global configuration settings. Should only be called once at application startup. @@ -57,8 +55,7 @@ public static Task RaiseEventAsync(HttpRequestMessage request, FlurlEventType ev } private static Task HandleEventAsync(Action syncHandler, Func asyncHandler, HttpCall call) { - if (syncHandler != null) - syncHandler(call); + syncHandler?.Invoke(call); if (asyncHandler != null) return asyncHandler(call); return _completedTask; diff --git a/src/Flurl.Http/FlurlRequest.cs b/src/Flurl.Http/FlurlRequest.cs new file mode 100644 index 00000000..2aee4e30 --- /dev/null +++ b/src/Flurl.Http/FlurlRequest.cs @@ -0,0 +1,175 @@ +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.Http.Testing; +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. + /// The FlurlHttpSettings object used by this request. + public FlurlRequest(Url url, FlurlHttpSettings settings = null) { + Settings = settings ?? new FlurlHttpSettings().Merge(HttpTest.Current?.Settings ?? FlurlHttp.GlobalSettings); + Url = url; + } + + /// + /// Initializes a new instance of the class. + /// + /// The URL to call with this FlurlRequest instance. + /// The FlurlHttpSettings object used by this request. + public FlurlRequest(string url, FlurlHttpSettings settings = null) : this(new Url(url), settings) { } + + /// + /// 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 => _client = _client ?? Settings.FlurlClientFactory.GetClient(Url); + set => _client = value; + } + + /// + /// 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) { + Settings.Merge(Client.Settings); + if (Settings.Timeout.HasValue) + Client.HttpClient.Timeout = Settings.Timeout.Value; + var request = new HttpRequestMessage(verb, Url) { Content = content }; + var call = new HttpCall(request, Settings); + + try { + WriteHeaders(request); + if (Settings.CookiesEnabled) + WriteRequestCookies(request); + return await Client.HttpClient.SendAsync(request, completionOption, cancellationToken ?? CancellationToken.None).ConfigureAwait(false); + } + catch (Exception) when (call.ExceptionHandled) { + return call.Response; + } + finally { + request.Dispose(); + if (Settings.CookiesEnabled) + ReadResponseCookies(call.Response); + } + } + + 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 = Client.HttpMessageHandler as HttpClientHandler; + + // 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) { + if (response?.RequestMessage == null) return; + + var uri = response.RequestMessage.RequestUri; + + // 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 = (Client.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; + } + } +} \ No newline at end of file diff --git a/src/Flurl.Http/HeaderExtensions.cs b/src/Flurl.Http/HeaderExtensions.cs new file mode 100644 index 00000000..07d1c142 --- /dev/null +++ b/src/Flurl.Http/HeaderExtensions.cs @@ -0,0 +1,67 @@ +using System; +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 obj, string name, object value) where T : IHttpSettingsContainer { + obj.Headers[name] = value; + return obj; + } + + /// + /// 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. + /// This IFlurlClient or IFlurlRequest. + public static T WithHeaders(this T obj, object headers) where T : IHttpSettingsContainer { + if (headers == null) + return obj; + + foreach (var kv in headers.ToKeyValuePairs()) { + obj.WithHeader(kv.Key, kv.Value); + } + + return obj; + } + + /// + /// 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 obj, 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 obj.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 obj, string token) where T : IHttpSettingsContainer { + return obj.WithHeader("Authorization", $"Bearer {token}"); + } + } +} 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/SettingsExtensions.cs b/src/Flurl.Http/SettingsExtensions.cs new file mode 100644 index 00000000..6b5ae784 --- /dev/null +++ b/src/Flurl.Http/SettingsExtensions.cs @@ -0,0 +1,137 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using Flurl.Http.Configuration; + +namespace Flurl.Http +{ + /// + /// Fluent extension methods for tweaking FlurlHttpSettings + /// + public static class SettingsExtensions + { + /// + /// 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; + } + + /// + /// Fluently returns a new IFlurlRequest that can be used to call the given Url with this client. + /// + /// + /// The Url to call. + /// A new IFlurlRequest to use in calling the Url + public static IFlurlRequest WithUrl(this IFlurlClient client, Url url) { + return new FlurlRequest(url) { Client = client }; + } + + /// + /// 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.WithUrl(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.WithUrl(url); + } + + /// + /// Change FlurlHttpSettings for this IFlurlClient or IFlurlRequest. + /// + /// The IFlurlClient or IFlurlRequest. + /// Action defining the settings changes. + /// The T with the modified HttpClient + public static T WithSettings(this T obj, Action action) where T : IHttpSettingsContainer { + action(obj.Settings); + return obj; + } + + /// + /// 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/HttpTest.cs b/src/Flurl.Http/Testing/HttpTest.cs index 2f7cee33..7e784150 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,39 @@ namespace Flurl.Http.Testing /// public class HttpTest : IDisposable { - /// - /// Gets the current HttpTest from the logical (async) call context - /// - public static HttpTest Current => GetCurrentTest(); - /// /// Initializes a new instance of the class. /// /// A delegate callback throws an exception. public HttpTest() { + Settings = new FlurlHttpSettings { + FlurlClientFactory = new TestFlurlClientFactory() + }.Merge(FlurlHttp.GlobalSettings); ResponseQueue = new Queue(); CallLog = new List(); SetCurrentTest(this); } + /// + /// Gets or sets the FlurlHttpSettings object used by this test. + /// + public FlurlHttpSettings 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; } + /// /// Adds an HttpResponseMessage to the response queue. /// @@ -89,11 +108,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 +115,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,7 +150,6 @@ public void ShouldNotHaveMadeACall() { /// public void Dispose() { SetCurrentTest(null); - FlurlHttp.GlobalSettings.ResetDefaults(); } #if NET45 diff --git a/src/Flurl.Http/Testing/TestFlurlClientFactory.cs b/src/Flurl.Http/Testing/TestFlurlClientFactory.cs new file mode 100644 index 00000000..76acb4b3 --- /dev/null +++ b/src/Flurl.Http/Testing/TestFlurlClientFactory.cs @@ -0,0 +1,32 @@ +using System; +using System.Diagnostics; +using System.Net.Http; +using Flurl.Http.Configuration; + +namespace Flurl.Http.Testing +{ + /// + /// Fake http client factory. + /// + public class TestFlurlClientFactory : DefaultFlurlClientFactory + { + private readonly Lazy _client = new Lazy(() => new FlurlClient()); + + /// + /// Creates an instance of FakeHttpMessageHander, which prevents actual HTTP calls from being made. + /// + /// + public override HttpMessageHandler CreateMessageHandler() { + return new FakeHttpMessageHandler(); + } + + /// + /// Returns the FlurlClient sigleton used for testing + /// + /// The URL. + /// The FlurlClient instance. + public override IFlurlClient GetClient(Url url) { + return _client.Value; + } + } +} \ 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/Util/CommonExtensions.cs b/src/Flurl/Util/CommonExtensions.cs index abd1307f..d9041854 100644 --- a/src/Flurl/Util/CommonExtensions.cs +++ b/src/Flurl/Util/CommonExtensions.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text.RegularExpressions; #if !NET40 using System.Reflection; #endif @@ -45,7 +46,7 @@ public static string ToInvariantString(this object obj) { var f = obj as IFormattable; if (f != null) return f.ToString(null, CultureInfo.InvariantCulture); - + return obj.ToString(); } @@ -74,8 +75,8 @@ 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); + 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) @@ -114,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 From 707c321545818adb4af3c22c55c704c8b6f9e7e4 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Sun, 13 Aug 2017 14:28:48 -0500 Subject: [PATCH 10/56] FlurlHttpSettings refactoring - keep reference to default values to deal with some tricky order of precedence rules --- Test/Flurl.Test/Http/SettingsTests.cs | 59 +++++++++++++++++++ .../Configuration/FlurlHttpSettings.cs | 41 +++++++++---- src/Flurl.Http/FlurlRequest.cs | 5 +- 3 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 Test/Flurl.Test/Http/SettingsTests.cs diff --git a/Test/Flurl.Test/Http/SettingsTests.cs b/Test/Flurl.Test/Http/SettingsTests.cs new file mode 100644 index 00000000..b462cf18 --- /dev/null +++ b/Test/Flurl.Test/Http/SettingsTests.cs @@ -0,0 +1,59 @@ +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] + public class SettingsTests + { + [Test, NonParallelizable] // tests that mess with global settings shouldn't be parallelized + public void settings_propagate_correctly() { + try { + FlurlHttp.GlobalSettings.CookiesEnabled = false; + FlurlHttp.GlobalSettings.AllowedHttpStatusRange = "4xx"; + + var fc1 = new FlurlClient(); + fc1.Settings.CookiesEnabled = true; + Assert.AreEqual("4xx", fc1.Settings.AllowedHttpStatusRange); + fc1.Settings.AllowedHttpStatusRange = "5xx"; + + var req = fc1.WithUrl("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 fc2 = new FlurlClient(); + fc2.Settings.CookiesEnabled = false; + + req.WithClient(fc2); + 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"); + + fc2.Settings.CookiesEnabled = true; + fc2.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"); + + fc2.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"); + } + finally { + FlurlHttp.GlobalSettings.ResetDefaults(); + } + } + } +} diff --git a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs index 06b5bfd2..6246e821 100644 --- a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs +++ b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs @@ -3,7 +3,6 @@ using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; -using Flurl.Util; namespace Flurl.Http.Configuration { @@ -12,18 +11,36 @@ namespace Flurl.Http.Configuration /// public class FlurlHttpSettings { - // There are some tricky order of precedence rules (request > client > global) that are easier - // to keep track of via dictionary 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 _settings = new Dictionary(); + // 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; - private static readonly FlurlHttpSettings _defaults = new FlurlHttpSettings { + // 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 IDictionary _vals = new Dictionary(); + + private static FlurlHttpSettings _baseDefaults = new FlurlHttpSettings(null) { FlurlClientFactory = new DefaultFlurlClientFactory(), CookiesEnabled = false, JsonSerializer = new NewtonsoftJsonSerializer(null), UrlEncodedSerializer = new DefaultUrlEncodedSerializer() }; + /// + /// Creates a new FlurlHttpSettings object using another FlurlHttpSettings object as its default values. + /// + public FlurlHttpSettings(FlurlHttpSettings defaults) { + _defaults = defaults; + } + + /// + /// Creates a new FlurlHttpSettings object. + /// + public FlurlHttpSettings() { + _defaults = _baseDefaults; + } + /// /// Gets or sets a factory used to create HttpClient object used in Flurl HTTP calls. Default value /// is an instance of DefaultFlurlClientFactory. Custom factory implementations should generally @@ -131,20 +148,20 @@ public Func OnErrorAsync { /// Clears all custom options and resets them to their default values. /// public void ResetDefaults() { - _settings.Clear(); + _vals.Clear(); } private T Get(Expression> property) { var p = (property.Body as MemberExpression).Member as PropertyInfo; - return - _settings.ContainsKey(p.Name) ? (T)_settings[p.Name] : - _defaults._settings.ContainsKey(p.Name) ? (T)_defaults._settings[p.Name] : + return + _vals.ContainsKey(p.Name) ? (T)_vals[p.Name] : + _defaults != null ? (T)p.GetValue(_defaults) : default(T); } private void Set(Expression> property, T value) { var p = (property.Body as MemberExpression).Member as PropertyInfo; - _settings[p.Name] = value; + _vals[p.Name] = value; } /// @@ -153,7 +170,7 @@ private void Set(Expression> property, T value) { /// /// The settings to merge. public FlurlHttpSettings Merge(FlurlHttpSettings other) { - _settings.Merge(other._settings); + _defaults = other; return this; } } diff --git a/src/Flurl.Http/FlurlRequest.cs b/src/Flurl.Http/FlurlRequest.cs index 2aee4e30..cca231d8 100644 --- a/src/Flurl.Http/FlurlRequest.cs +++ b/src/Flurl.Http/FlurlRequest.cs @@ -72,7 +72,10 @@ public FlurlRequest(string url, FlurlHttpSettings settings = null) : this(new Ur /// public IFlurlClient Client { get => _client = _client ?? Settings.FlurlClientFactory.GetClient(Url); - set => _client = value; + set { + _client = value; + Settings.Merge(_client.Settings); + } } /// From a2656d4056566d4f99bba94ea49ea8c9bf46566e Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Sun, 13 Aug 2017 14:29:19 -0500 Subject: [PATCH 11/56] parallelize most tests --- Test/Flurl.Test/CommonExtensionsTests.cs | 2 +- Test/Flurl.Test/Http/ClientConfigTests.cs | 2 +- Test/Flurl.Test/Http/GetTests.cs | 2 +- Test/Flurl.Test/Http/GlobalConfigTests.cs | 3 +-- Test/Flurl.Test/Http/HeadTests.cs | 2 +- Test/Flurl.Test/Http/HttpTestFixtureBase.cs | 1 - Test/Flurl.Test/Http/PostTests.cs | 2 +- Test/Flurl.Test/Http/TestingTests.cs | 2 +- Test/Flurl.Test/UrlBuilderTests.cs | 2 +- 9 files changed, 8 insertions(+), 10 deletions(-) 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/Http/ClientConfigTests.cs b/Test/Flurl.Test/Http/ClientConfigTests.cs index beab60bd..49d2e6d7 100644 --- a/Test/Flurl.Test/Http/ClientConfigTests.cs +++ b/Test/Flurl.Test/Http/ClientConfigTests.cs @@ -10,7 +10,7 @@ namespace Flurl.Test.Http { - [TestFixture] + [TestFixture, Parallelizable] public class ClientConfigTestsBase //: ConfigTestsBase { //private FlurlClient _client = new FlurlClient(); diff --git a/Test/Flurl.Test/Http/GetTests.cs b/Test/Flurl.Test/Http/GetTests.cs index 5b8e069c..e2496365 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] diff --git a/Test/Flurl.Test/Http/GlobalConfigTests.cs b/Test/Flurl.Test/Http/GlobalConfigTests.cs index 4190a8b9..6a321c9a 100644 --- a/Test/Flurl.Test/Http/GlobalConfigTests.cs +++ b/Test/Flurl.Test/Http/GlobalConfigTests.cs @@ -12,7 +12,6 @@ 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(); @@ -135,7 +134,7 @@ private class SomeCustomHttpClient : HttpClient { } private class SomeCustomMessageHandler : HttpClientHandler { } } - //[TestFixture] + //[TestFixture, Parallelizable] //public class GlobalConfigTestsBase : ConfigTestsBase //{ // protected override FlurlHttpSettings GetSettings() { 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..40207449 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] diff --git a/Test/Flurl.Test/Http/TestingTests.cs b/Test/Flurl.Test/Http/TestingTests.cs index 2b253d07..ab49c646 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] 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] From ad8c051543f56ffd3fefd8fe581c9b4cfab6ce49 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Sun, 13 Aug 2017 15:54:22 -0500 Subject: [PATCH 12/56] re-introduce HttpClientFactory, keep FlurlClientFactory.Get separate. (the former makes sense at FlurlClient settings level; latter does not) --- Test/Flurl.Test/Http/GlobalConfigTests.cs | 8 ++--- .../DefaultFlurlClientFactory.cs | 21 +------------ .../Configuration/DefaultHttpClientFactory.cs | 30 +++++++++++++++++++ .../Configuration/FlurlHttpSettings.cs | 25 +++++++++++----- .../Configuration/IFlurlClientFactory.cs | 27 ++++------------- .../Configuration/IHttpClientFactory.cs | 28 +++++++++++++++++ src/Flurl.Http/FlurlClient.cs | 6 ++-- src/Flurl.Http/FlurlHttp.cs | 7 +++-- src/Flurl.Http/FlurlRequest.cs | 11 +++++-- src/Flurl.Http/Testing/HttpTest.cs | 9 +++--- ...FlurlClientFactory.cs => TestFactories.cs} | 17 +++++++---- 11 files changed, 115 insertions(+), 74 deletions(-) create mode 100644 src/Flurl.Http/Configuration/DefaultHttpClientFactory.cs create mode 100644 src/Flurl.Http/Configuration/IHttpClientFactory.cs rename src/Flurl.Http/Testing/{TestFlurlClientFactory.cs => TestFactories.cs} (71%) diff --git a/Test/Flurl.Test/Http/GlobalConfigTests.cs b/Test/Flurl.Test/Http/GlobalConfigTests.cs index 6a321c9a..6d45cdf9 100644 --- a/Test/Flurl.Test/Http/GlobalConfigTests.cs +++ b/Test/Flurl.Test/Http/GlobalConfigTests.cs @@ -23,7 +23,7 @@ public void ResetDefaults() { [Test] public void can_provide_custom_client_factory() { - GetSettings().FlurlClientFactory = new SomeCustomFlurlClientFactory(); + GetSettings().HttpClientFactory = new SomeCustomHttpClientFactory(); var client = new FlurlClient(); Assert.IsInstanceOf(client.HttpClient); Assert.IsInstanceOf(client.HttpMessageHandler); @@ -115,7 +115,7 @@ public async Task can_disable_exception_behavior() { } } - private class SomeCustomFlurlClientFactory : IFlurlClientFactory + private class SomeCustomHttpClientFactory : IHttpClientFactory { public HttpClient CreateHttpClient(HttpMessageHandler handler) { return new SomeCustomHttpClient(); @@ -124,10 +124,6 @@ public HttpClient CreateHttpClient(HttpMessageHandler handler) { public HttpMessageHandler CreateMessageHandler() { return new SomeCustomMessageHandler(); } - - public IFlurlClient GetClient(Url url) { - return new FlurlClient(); - } } private class SomeCustomHttpClient : HttpClient { } diff --git a/src/Flurl.Http/Configuration/DefaultFlurlClientFactory.cs b/src/Flurl.Http/Configuration/DefaultFlurlClientFactory.cs index 9e4991fb..ce6cfc60 100644 --- a/src/Flurl.Http/Configuration/DefaultFlurlClientFactory.cs +++ b/src/Flurl.Http/Configuration/DefaultFlurlClientFactory.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Concurrent; -using System.Net.Http; namespace Flurl.Http.Configuration { @@ -12,31 +11,13 @@ public class DefaultFlurlClientFactory : IFlurlClientFactory { private static readonly ConcurrentDictionary _clients = new ConcurrentDictionary(); - /// - /// Override in custom factory to customize the creation of HttpClient used in all Flurl HTTP calls. - /// In order not to lose Flurl.Http functionality, it is recommended to call base.CreateClient and - /// customize the result. - /// - public virtual HttpClient CreateHttpClient(HttpMessageHandler handler) { - return new HttpClient(new FlurlMessageHandler(handler)); - } - - /// - /// Override in custom factory to customize the creation of HttpClientHandler used in all Flurl HTTP calls. - /// In order not to lose Flurl.Http functionality, it is recommended to call base.CreateMessageHandler and - /// customize the result. - /// - public virtual HttpMessageHandler CreateMessageHandler() { - return new HttpClientHandler(); - } - /// /// 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 GetClient(Url url) { + public virtual IFlurlClient Get(Url url) { var key = new Uri(url).Host; return _clients.GetOrAdd(key, _ => new FlurlClient()); } diff --git a/src/Flurl.Http/Configuration/DefaultHttpClientFactory.cs b/src/Flurl.Http/Configuration/DefaultHttpClientFactory.cs new file mode 100644 index 00000000..19bd4d2d --- /dev/null +++ b/src/Flurl.Http/Configuration/DefaultHttpClientFactory.cs @@ -0,0 +1,30 @@ +using System.Net.Http; + +namespace Flurl.Http.Configuration +{ + /// + /// Default implementation of IHttpClientFactory used by FlurlHttp. The created HttpClient includes hooks + /// that enable FlurlHttp's testing features and respect its configuration settings. Therefore, custom factories + /// should inherit from this class, rather than implementing IHttpClientFactory directly. + /// + public class DefaultHttpClientFactory : IHttpClientFactory + { + /// + /// Override in custom factory to customize the creation of HttpClient used in all Flurl HTTP calls. + /// In order not to lose Flurl.Http functionality, it is recommended to call base.CreateClient and + /// customize the result. + /// + public virtual HttpClient CreateHttpClient(HttpMessageHandler handler) { + return new HttpClient(new FlurlMessageHandler(handler)); + } + + /// + /// Override in custom factory to customize the creation of HttpClientHandler used in all Flurl HTTP calls. + /// In order not to lose Flurl.Http functionality, it is recommended to call base.CreateMessageHandler and + /// customize the result. + /// + public virtual HttpMessageHandler CreateMessageHandler() { + return new HttpClientHandler(); + } + } +} \ No newline at end of file diff --git a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs index 6246e821..2a55d919 100644 --- a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs +++ b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs @@ -21,7 +21,7 @@ public class FlurlHttpSettings private IDictionary _vals = new Dictionary(); private static FlurlHttpSettings _baseDefaults = new FlurlHttpSettings(null) { - FlurlClientFactory = new DefaultFlurlClientFactory(), + HttpClientFactory = new DefaultHttpClientFactory(), CookiesEnabled = false, JsonSerializer = new NewtonsoftJsonSerializer(null), UrlEncodedSerializer = new DefaultUrlEncodedSerializer() @@ -43,13 +43,12 @@ public FlurlHttpSettings() { /// /// Gets or sets a factory used to create HttpClient object used in Flurl HTTP calls. Default value - /// is an instance of DefaultFlurlClientFactory. Custom factory implementations should generally - /// inherit from DefaultFlurlClientFactory, call base.CreateClient, and manipulate the returned HttpClient, - /// otherwise functionality such as callbacks and most testing features will be lost. + /// is an instance of DefaultHttpClientFactory. Custom factory implementations should generally + /// inherit from DefaultHttpClientFactory, the base implementations, and only customize as needed. /// - public IFlurlClientFactory FlurlClientFactory { - get => Get(() => FlurlClientFactory); - set => Set(() => FlurlClientFactory, value); + public IHttpClientFactory HttpClientFactory { + get => Get(() => HttpClientFactory); + set => Set(() => HttpClientFactory, value); } /// @@ -174,4 +173,16 @@ public FlurlHttpSettings Merge(FlurlHttpSettings other) { return this; } } + + /// + /// Global default settings for Flurl.Http + /// + public class GlobalFlurlHttpSettings : FlurlHttpSettings + { + /// + /// Gets or sets the factory that defines creating, caching, and reusing FlurlClient instances and, + /// by proxy, HttpClient instances. + /// + public IFlurlClientFactory FlurlClientFactory { get; set; } = new DefaultFlurlClientFactory(); + } } diff --git a/src/Flurl.Http/Configuration/IFlurlClientFactory.cs b/src/Flurl.Http/Configuration/IFlurlClientFactory.cs index d8ac937b..b702edb2 100644 --- a/src/Flurl.Http/Configuration/IFlurlClientFactory.cs +++ b/src/Flurl.Http/Configuration/IFlurlClientFactory.cs @@ -1,33 +1,16 @@ -using System.Net.Http; - -namespace Flurl.Http.Configuration +namespace Flurl.Http.Configuration { /// - /// Interface defining creation of HttpClient and HttpMessageHandler used in all Flurl HTTP calls. - /// Implementation can be added via FlurlHttp.Configure. However, in order not to lose much of - /// Flurl.Http's functionality, it's almost always best to inherit DefaultFlurlClientFactory and - /// extend the base implementations, rather than implementing this interface directly. + /// Interface for defining a strategy for creating, caching, and reusing IFlurlClient instances and, + /// by proxy, their underlying HttpClient instances. /// public interface IFlurlClientFactory { /// - /// Creates the client. - /// - /// The message handler being used in this call - /// - HttpClient CreateHttpClient(HttpMessageHandler handler); - - /// - /// Creates the message handler. - /// - /// - HttpMessageHandler CreateMessageHandler(); - - /// - /// Strategy to create an HttpClient or reuse an exisitng one, based on URL being called. + /// Strategy to create a FlurlClient or reuse an exisitng one, based on URL being called. /// /// The URL being called. /// - IFlurlClient GetClient(Url url); + 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 new file mode 100644 index 00000000..fcb68970 --- /dev/null +++ b/src/Flurl.Http/Configuration/IHttpClientFactory.cs @@ -0,0 +1,28 @@ +using System.Net.Http; + +namespace Flurl.Http.Configuration +{ + /// + /// Interface defining creation of HttpClient and HttpMessageHandler used in all Flurl HTTP calls. + /// Implementation can be added via FlurlHttp.Configure. However, in order not to lose much of + /// Flurl.Http's functionality, it's almost always best to inherit DefaultHttpClientFactory and + /// extend the base implementations, rather than implementing this interface directly. + /// + public interface IHttpClientFactory + { + /// + /// 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 HttpMessageHandler used to construct the HttpClient. + /// + HttpClient CreateHttpClient(HttpMessageHandler handler); + + /// + /// Defines how the + /// + /// + HttpMessageHandler CreateMessageHandler(); + } +} \ No newline at end of file diff --git a/src/Flurl.Http/FlurlClient.cs b/src/Flurl.Http/FlurlClient.cs index b2736f96..5391d544 100644 --- a/src/Flurl.Http/FlurlClient.cs +++ b/src/Flurl.Http/FlurlClient.cs @@ -40,9 +40,9 @@ public class FlurlClient : IFlurlClient /// /// The FlurlHttpSettings associated with this instance. public FlurlClient(FlurlHttpSettings settings = null) { - Settings = settings ?? new FlurlHttpSettings().Merge(HttpTest.Current?.Settings ?? FlurlHttp.GlobalSettings); - HttpMessageHandler = Settings.FlurlClientFactory.CreateMessageHandler(); - HttpClient = Settings.FlurlClientFactory.CreateHttpClient(HttpMessageHandler); + Settings = settings ?? new FlurlHttpSettings(FlurlHttp.GlobalSettings); + HttpMessageHandler = Settings.HttpClientFactory.CreateMessageHandler(); + HttpClient = Settings.HttpClientFactory.CreateHttpClient(HttpMessageHandler); } /// diff --git a/src/Flurl.Http/FlurlHttp.cs b/src/Flurl.Http/FlurlHttp.cs index 718aea54..9afe1fb5 100644 --- a/src/Flurl.Http/FlurlHttp.cs +++ b/src/Flurl.Http/FlurlHttp.cs @@ -2,6 +2,7 @@ using System.Net.Http; using System.Threading.Tasks; using Flurl.Http.Configuration; +using Flurl.Http.Testing; namespace Flurl.Http { @@ -13,13 +14,13 @@ public static class FlurlHttp private static readonly object _configLock = new object(); private static readonly Task _completedTask = Task.FromResult(0); - 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 => _settings.Value; + public static GlobalFlurlHttpSettings GlobalSettings => HttpTest.Current?.Settings ?? _settings.Value; /// /// Provides thread-safe access to Flurl.Http's global configuration settings. Should only be called once at application startup. diff --git a/src/Flurl.Http/FlurlRequest.cs b/src/Flurl.Http/FlurlRequest.cs index cca231d8..a937a894 100644 --- a/src/Flurl.Http/FlurlRequest.cs +++ b/src/Flurl.Http/FlurlRequest.cs @@ -51,7 +51,7 @@ public class FlurlRequest : IFlurlRequest /// The URL to call with this FlurlRequest instance. /// The FlurlHttpSettings object used by this request. public FlurlRequest(Url url, FlurlHttpSettings settings = null) { - Settings = settings ?? new FlurlHttpSettings().Merge(HttpTest.Current?.Settings ?? FlurlHttp.GlobalSettings); + Settings = settings ?? new FlurlHttpSettings().Merge(FlurlHttp.GlobalSettings); Url = url; } @@ -71,7 +71,13 @@ public FlurlRequest(string url, FlurlHttpSettings settings = null) : this(new Ur /// Gets or sets the IFlurlClient to use when sending the request. /// public IFlurlClient Client { - get => _client = _client ?? Settings.FlurlClientFactory.GetClient(Url); + get { + if (_client == null) { + _client = FlurlHttp.GlobalSettings.FlurlClientFactory.Get(Url); + Settings.Merge(_client.Settings); + } + return _client; + } set { _client = value; Settings.Merge(_client.Settings); @@ -103,7 +109,6 @@ public IFlurlClient Client { /// 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) { - Settings.Merge(Client.Settings); if (Settings.Timeout.HasValue) Client.HttpClient.Timeout = Settings.Timeout.Value; var request = new HttpRequestMessage(verb, Url) { Content = content }; diff --git a/src/Flurl.Http/Testing/HttpTest.cs b/src/Flurl.Http/Testing/HttpTest.cs index 7e784150..cc6392ba 100644 --- a/src/Flurl.Http/Testing/HttpTest.cs +++ b/src/Flurl.Http/Testing/HttpTest.cs @@ -20,9 +20,10 @@ public class HttpTest : IDisposable /// /// A delegate callback throws an exception. public HttpTest() { - Settings = new FlurlHttpSettings { - FlurlClientFactory = new TestFlurlClientFactory() - }.Merge(FlurlHttp.GlobalSettings); + Settings = new GlobalFlurlHttpSettings { + HttpClientFactory = new TestHttpClientFactory(), + FlurlClientFactory = new TestFlurlClientFactory() + }; ResponseQueue = new Queue(); CallLog = new List(); SetCurrentTest(this); @@ -31,7 +32,7 @@ public HttpTest() { /// /// Gets or sets the FlurlHttpSettings object used by this test. /// - public FlurlHttpSettings Settings { get; set; } + public GlobalFlurlHttpSettings Settings { get; set; } /// /// Gets the current HttpTest from the logical (async) call context diff --git a/src/Flurl.Http/Testing/TestFlurlClientFactory.cs b/src/Flurl.Http/Testing/TestFactories.cs similarity index 71% rename from src/Flurl.Http/Testing/TestFlurlClientFactory.cs rename to src/Flurl.Http/Testing/TestFactories.cs index 76acb4b3..1d3960ba 100644 --- a/src/Flurl.Http/Testing/TestFlurlClientFactory.cs +++ b/src/Flurl.Http/Testing/TestFactories.cs @@ -1,17 +1,14 @@ using System; -using System.Diagnostics; using System.Net.Http; using Flurl.Http.Configuration; namespace Flurl.Http.Testing { /// - /// Fake http client factory. + /// IHttpClientFactory implementation used to fake and record calls in tests. /// - public class TestFlurlClientFactory : DefaultFlurlClientFactory + public class TestHttpClientFactory : DefaultHttpClientFactory { - private readonly Lazy _client = new Lazy(() => new FlurlClient()); - /// /// Creates an instance of FakeHttpMessageHander, which prevents actual HTTP calls from being made. /// @@ -19,13 +16,21 @@ public class TestFlurlClientFactory : DefaultFlurlClientFactory public override HttpMessageHandler CreateMessageHandler() { return new FakeHttpMessageHandler(); } + } + + /// + /// IFlurlClientFactory implementation used to fake and record calls in tests. + /// + public class TestFlurlClientFactory : DefaultFlurlClientFactory + { + private readonly Lazy _client = new Lazy(() => new FlurlClient()); /// /// Returns the FlurlClient sigleton used for testing /// /// The URL. /// The FlurlClient instance. - public override IFlurlClient GetClient(Url url) { + public override IFlurlClient Get(Url url) { return _client.Value; } } From 6d02ce35c747d77b01c9b40b4d0909aaca078bda Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Wed, 16 Aug 2017 10:57:46 -0500 Subject: [PATCH 13/56] rename WithSettings to Configure (consistent with global FlurlHttp.Configure) --- Test/Flurl.Test/Http/ClientConfigTests.cs | 4 ++-- Test/Flurl.Test/Http/GetTests.cs | 2 +- Test/Flurl.Test/Http/RealHttpTests.cs | 2 +- Test/Flurl.Test/Http/TestingTests.cs | 2 +- src/Flurl.Http.CodeGen/Program.cs | 4 ++-- src/Flurl.Http.CodeGen/UrlExtensionMethod.cs | 2 +- src/Flurl.Http/FlurlClient.cs | 1 - .../{AutoGenExtensions.cs => GeneratedExtensions.cs} | 10 +++++----- src/Flurl.Http/SettingsExtensions.cs | 2 +- 9 files changed, 14 insertions(+), 15 deletions(-) rename src/Flurl.Http/{AutoGenExtensions.cs => GeneratedExtensions.cs} (99%) diff --git a/Test/Flurl.Test/Http/ClientConfigTests.cs b/Test/Flurl.Test/Http/ClientConfigTests.cs index 49d2e6d7..2a02d41c 100644 --- a/Test/Flurl.Test/Http/ClientConfigTests.cs +++ b/Test/Flurl.Test/Http/ClientConfigTests.cs @@ -122,10 +122,10 @@ public async Task can_allow_any_http_status() public void can_override_settings_fluently() { using (var test = new HttpTest()) { - var client = new FlurlClient().WithSettings(s => s.AllowedHttpStatusRange = "*"); + var client = new FlurlClient().Configure(s => s.AllowedHttpStatusRange = "*"); test.RespondWith("epic fail", 500); Assert.ThrowsAsync(async () => await "http://www.api.com" - .WithSettings(c => c.AllowedHttpStatusRange = "2xx") + .Configure(c => c.AllowedHttpStatusRange = "2xx") .WithClient(client) // client-level settings shouldn't win .GetAsync()); } diff --git a/Test/Flurl.Test/Http/GetTests.cs b/Test/Flurl.Test/Http/GetTests.cs index e2496365..f213b1f4 100644 --- a/Test/Flurl.Test/Http/GetTests.cs +++ b/Test/Flurl.Test/Http/GetTests.cs @@ -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" - .WithSettings(c => c.OnError = call => call.ExceptionHandled = true) + .Configure(c => c.OnError = call => call.ExceptionHandled = true) .GetJsonAsync(); Assert.IsNull(data); } diff --git a/Test/Flurl.Test/Http/RealHttpTests.cs b/Test/Flurl.Test/Http/RealHttpTests.cs index 8e96a9a7..61b0ec6d 100644 --- a/Test/Flurl.Test/Http/RealHttpTests.cs +++ b/Test/Flurl.Test/Http/RealHttpTests.cs @@ -161,7 +161,7 @@ public async Task can_handle_error() { var handlerCalled = false; try { - await "https://httpbin.org/status/500".WithSettings(c => { + await "https://httpbin.org/status/500".Configure(c => { c.OnError = call => { call.ExceptionHandled = true; handlerCalled = true; diff --git a/Test/Flurl.Test/Http/TestingTests.cs b/Test/Flurl.Test/Http/TestingTests.cs index ab49c646..45c158f8 100644 --- a/Test/Flurl.Test/Http/TestingTests.cs +++ b/Test/Flurl.Test/Http/TestingTests.cs @@ -140,7 +140,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" - .WithSettings(c => c.OnError = call => call.ExceptionHandled = true) + .Configure(c => c.OnError = call => call.ExceptionHandled = true) .GetAsync(); Assert.IsNull(result); } diff --git a/src/Flurl.Http.CodeGen/Program.cs b/src/Flurl.Http.CodeGen/Program.cs index bd1ee20d..01d8934d 100644 --- a/src/Flurl.Http.CodeGen/Program.cs +++ b/src/Flurl.Http.CodeGen/Program.cs @@ -8,7 +8,7 @@ namespace Flurl.Http.CodeGen class Program { static int Main(string[] args) { - var codePath = (args.Length > 0) ? args[0] : @"..\Flurl.Http\AutoGenExtensions.cs"; + var codePath = (args.Length > 0) ? args[0] : @"..\Flurl.Http\GeneratedExtensions.cs"; if (!File.Exists(codePath)) { Console.ForegroundColor = ConsoleColor.Red; @@ -38,7 +38,7 @@ static int Main(string[] args) { .WriteLine("/// ") .WriteLine("/// Auto-generated fluent extension methods on String, Url, and IFlurlRequest.") .WriteLine("/// ") - .WriteLine("public static class AutoGenExtensions") + .WriteLine("public static class GeneratedExtensions") .WriteLine("{"); WriteExtensionMethods(writer); diff --git a/src/Flurl.Http.CodeGen/UrlExtensionMethod.cs b/src/Flurl.Http.CodeGen/UrlExtensionMethod.cs index ffc78161..f5e3b3c9 100644 --- a/src/Flurl.Http.CodeGen/UrlExtensionMethod.cs +++ b/src/Flurl.Http.CodeGen/UrlExtensionMethod.cs @@ -35,7 +35,7 @@ public static IEnumerable GetAll() { .AddParam("expires", "DateTime?", "Expiration for all cookies (optional). If excluded, cookies only live for duration of session.", "null"); // settings extensions - yield return new UrlExtensionMethod("WithSettings", "Creates a new FlurlRequest with the URL and allows changing its Settings inline.") + yield return new UrlExtensionMethod("Configure", "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."); diff --git a/src/Flurl.Http/FlurlClient.cs b/src/Flurl.Http/FlurlClient.cs index 5391d544..fad0ed94 100644 --- a/src/Flurl.Http/FlurlClient.cs +++ b/src/Flurl.Http/FlurlClient.cs @@ -6,7 +6,6 @@ using System.Threading; using System.Threading.Tasks; using Flurl.Http.Configuration; -using Flurl.Http.Testing; namespace Flurl.Http { diff --git a/src/Flurl.Http/AutoGenExtensions.cs b/src/Flurl.Http/GeneratedExtensions.cs similarity index 99% rename from src/Flurl.Http/AutoGenExtensions.cs rename to src/Flurl.Http/GeneratedExtensions.cs index 724345b5..707cfa43 100644 --- a/src/Flurl.Http/AutoGenExtensions.cs +++ b/src/Flurl.Http/GeneratedExtensions.cs @@ -14,7 +14,7 @@ namespace Flurl.Http /// /// Auto-generated fluent extension methods on String, Url, and IFlurlRequest. /// - public static class AutoGenExtensions + public static class GeneratedExtensions { /// /// Creates a FlurlRequest from the URL and sends an asynchronous request. @@ -908,8 +908,8 @@ public static IFlurlRequest WithCookies(this Url url, object cookies, DateTime? /// The URL. /// A delegate defining the Settings changes. /// The IFlurlRequest. - public static IFlurlRequest WithSettings(this Url url, Action action) { - return new FlurlRequest(url).WithSettings(action); + public static IFlurlRequest Configure(this Url url, Action action) { + return new FlurlRequest(url).Configure(action); } /// /// Creates a new FlurlRequest with the URL and sets the request timeout. @@ -1037,8 +1037,8 @@ public static IFlurlRequest WithCookies(this string url, object cookies, DateTim /// The URL. /// A delegate defining the Settings changes. /// The IFlurlRequest. - public static IFlurlRequest WithSettings(this string url, Action action) { - return new FlurlRequest(url).WithSettings(action); + public static IFlurlRequest Configure(this string url, Action action) { + return new FlurlRequest(url).Configure(action); } /// /// Creates a new FlurlRequest with the URL and sets the request timeout. diff --git a/src/Flurl.Http/SettingsExtensions.cs b/src/Flurl.Http/SettingsExtensions.cs index 6b5ae784..4aa789f3 100644 --- a/src/Flurl.Http/SettingsExtensions.cs +++ b/src/Flurl.Http/SettingsExtensions.cs @@ -70,7 +70,7 @@ public static IFlurlRequest WithClient(this string url, IFlurlClient client) { /// The IFlurlClient or IFlurlRequest. /// Action defining the settings changes. /// The T with the modified HttpClient - public static T WithSettings(this T obj, Action action) where T : IHttpSettingsContainer { + public static T Configure(this T obj, Action action) where T : IHttpSettingsContainer { action(obj.Settings); return obj; } From 6b4210599053d0c8b56c922af6e60e63164cfd25 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Wed, 16 Aug 2017 14:45:24 -0500 Subject: [PATCH 14/56] Refactored cookie extensions to use IHttpSettingsContainer to reduce code --- src/Flurl.Http/CookieExtensions.cs | 87 +++++++----------------------- 1 file changed, 20 insertions(+), 67 deletions(-) diff --git a/src/Flurl.Http/CookieExtensions.cs b/src/Flurl.Http/CookieExtensions.cs index 0360524d..ff99e24f 100644 --- a/src/Flurl.Http/CookieExtensions.cs +++ b/src/Flurl.Http/CookieExtensions.cs @@ -16,100 +16,53 @@ public static class CookieExtensions /// /// Allows cookies to be sent and received. Not necessary to call when setting cookies via WithCookie/WithCookies. /// - /// The IFlurlClient. + /// The IFlurlClient or IFlurlRequest. /// This IFlurlClient. - public static IFlurlClient EnableCookies(this IFlurlClient client) { - client.Settings.CookiesEnabled = true; - return client; + 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 IFlurlClient. + /// Sets an HTTP cookie to be sent with this IFlurlRequest or all requests made with this IFlurlClient. /// - /// The IFlurlClient. + /// The IFlurlClient or IFlurlRequest. /// The cookie to set. /// This IFlurlClient. - public static IFlurlClient WithCookie(this IFlurlClient client, Cookie cookie) { - client.Settings.CookiesEnabled = true; - client.Cookies[cookie.Name] = cookie; - return client; + public static T WithCookie(this T clientOrRequest, Cookie cookie) where T : IHttpSettingsContainer { + clientOrRequest.Settings.CookiesEnabled = true; + clientOrRequest.Cookies[cookie.Name] = cookie; + return clientOrRequest; } /// - /// Sets an HTTP cookie to be sent with all requests made with this IFlurlClient. + /// Sets an HTTP cookie to be sent with this IFlurlRequest or all requests made with this IFlurlClient. /// - /// The IFlurlClient. + /// 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 IFlurlClient WithCookie(this IFlurlClient client, string name, object value, DateTime? expires = null) { + 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 client.WithCookie(cookie); + return clientOrRequest.WithCookie(cookie); } /// - /// Sets HTTP cookies to be sent with all requests made with this IFlurlClient, based on property names/values of the provided object, or keys/values if object is a dictionary. + /// 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 IFlurlClient. + /// 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 IFlurlClient WithCookies(this IFlurlClient client, object cookies, DateTime? expires = null) { + 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); + clientOrRequest.WithCookie(kv.Key, kv.Value, expires); - return client; - } - - /// - /// Allows cookies to be sent and received with this request's IFlurlClient. Not necessary to call when setting cookies via WithCookie/WithCookies. - /// - /// The IFlurlRequest. - /// This IFlurlRequest. - public static IFlurlRequest EnableCookies(this IFlurlRequest request) { - request.Settings.CookiesEnabled = true; // a little awkward to have this at both the client and request. - request.Client.EnableCookies(); - return request; - } - - /// - /// Sets an HTTP cookie to be sent with this request's IFlurlClient. - /// - /// The IFlurlRequest. - /// The cookie to set. - /// This IFlurlRequest. - public static IFlurlRequest WithCookie(this IFlurlRequest request, Cookie cookie) { - request.Client.WithCookie(cookie); - return request; - } - - /// - /// Sets an HTTP cookie to be sent with this request's IFlurlClient. - /// - /// The IFlurlRequest. - /// The cookie name. - /// The cookie value. - /// The cookie expiration (optional). If excluded, cookie only lives for duration of session. - /// This IFlurlRequest. - public static IFlurlRequest WithCookie(this IFlurlRequest request, string name, object value, DateTime? expires = null) { - request.Client.WithCookie(name, value, expires); - return request; - } - - /// - /// Sets HTTP cookies to be sent with this request's IFlurlClient, based on property names/values of the provided object, or keys/values if object is a dictionary. - /// - /// The 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 IFlurlRequest. - public static IFlurlRequest WithCookies(this IFlurlRequest request, object cookies, DateTime? expires = null) { - request.Client.WithCookies(cookies, expires); - return request; + return clientOrRequest; } } } From 8bd90ff1029c058c57fb06545b3eca7d30320074 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Wed, 16 Aug 2017 14:47:13 -0500 Subject: [PATCH 15/56] a few naming changes for consistency --- Test/Flurl.Test/Http/RealHttpTests.cs | 20 +++++++++--------- src/Flurl.Http/HeaderExtensions.cs | 30 +++++++++++++-------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/Test/Flurl.Test/Http/RealHttpTests.cs b/Test/Flurl.Test/Http/RealHttpTests.cs index 61b0ec6d..b8a0ef53 100644 --- a/Test/Flurl.Test/Http/RealHttpTests.cs +++ b/Test/Flurl.Test/Http/RealHttpTests.cs @@ -36,33 +36,33 @@ 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 client = new FlurlClient().WithCookie("z", "999"); + var resp = await client.WithUrl("http://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 client = new FlurlClient().EnableCookies(); + await client.WithUrl("https://httpbin.org/cookies/set?z=999").HeadAsync(); + Assert.AreEqual("999", client.Cookies["z"].Value); } [Test] public async Task can_persist_cookies() { - using (var fc = new FlurlClient()) { - var req = "http://httpbin.org/cookies".WithClient(fc).WithCookie("z", 999); + using (var client = new FlurlClient()) { + var req = "http://httpbin.org/cookies".WithClient(client).WithCookie("z", 999); // cookie should be set - Assert.AreEqual("999", fc.Cookies["z"].Value); + Assert.AreEqual("999", client.Cookies["z"].Value); Assert.AreEqual("999", req.Cookies["z"].Value); await req.HeadAsync(); // FlurlClient should be re-used, so cookie should stick - Assert.AreEqual("999", fc.Cookies["z"].Value); + Assert.AreEqual("999", client.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".WithClient(fc).GetJsonAsync(); + var resp = await "http://httpbin.org/cookies".WithClient(client).GetJsonAsync(); Assert.AreEqual("999", resp.cookies.z); } } diff --git a/src/Flurl.Http/HeaderExtensions.cs b/src/Flurl.Http/HeaderExtensions.cs index 07d1c142..bc82ef61 100644 --- a/src/Flurl.Http/HeaderExtensions.cs +++ b/src/Flurl.Http/HeaderExtensions.cs @@ -15,53 +15,53 @@ 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. + /// The IFlurlClient or IFlurlRequest. /// HTTP header name. /// HTTP header value. /// This IFlurlClient or IFlurlRequest. - public static T WithHeader(this T obj, string name, object value) where T : IHttpSettingsContainer { - obj.Headers[name] = value; - return obj; + 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. + /// The IFlurlClient or IFlurlRequest. /// Names/values of HTTP headers to set. Typically an anonymous object or IDictionary. /// This IFlurlClient or IFlurlRequest. - public static T WithHeaders(this T obj, object headers) where T : IHttpSettingsContainer { + public static T WithHeaders(this T clientOrRequest, object headers) where T : IHttpSettingsContainer { if (headers == null) - return obj; + return clientOrRequest; foreach (var kv in headers.ToKeyValuePairs()) { - obj.WithHeader(kv.Key, kv.Value); + clientOrRequest.WithHeader(kv.Key, kv.Value); } - return obj; + 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. + /// The IFlurlClient or IFlurlRequest. /// Username of authenticating user. /// Password of authenticating user. /// This IFlurlClient or IFlurlRequest. - public static T WithBasicAuth(this T obj, string username, string password) where T : IHttpSettingsContainer { + 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 obj.WithHeader("Authorization", $"Basic {encodedCreds}"); + 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 IFlurlClient or IFlurlRequest. /// The acquired bearer token to pass. /// This IFlurlClient or IFlurlRequest. - public static T WithOAuthBearerToken(this T obj, string token) where T : IHttpSettingsContainer { - return obj.WithHeader("Authorization", $"Bearer {token}"); + public static T WithOAuthBearerToken(this T clientOrRequest, string token) where T : IHttpSettingsContainer { + return clientOrRequest.WithHeader("Authorization", $"Bearer {token}"); } } } From f19fc913121e21a82c5a1dff4bf5802454379978 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Wed, 16 Aug 2017 14:48:16 -0500 Subject: [PATCH 16/56] some tweaks to settings and big refactoring of settings tests --- Test/Flurl.Test/Http/ClientConfigTests.cs | 170 ---------------- Test/Flurl.Test/Http/GlobalConfigTests.cs | 140 ------------- .../Http/SettingsExtensionsTests.cs | 164 ++++++++++++++++ Test/Flurl.Test/Http/SettingsTests.cs | 184 ++++++++++++++++-- .../Configuration/FlurlHttpSettings.cs | 28 +-- src/Flurl.Http/FlurlClient.cs | 62 +++--- src/Flurl.Http/FlurlRequest.cs | 1 - 7 files changed, 371 insertions(+), 378 deletions(-) delete mode 100644 Test/Flurl.Test/Http/ClientConfigTests.cs delete mode 100644 Test/Flurl.Test/Http/GlobalConfigTests.cs create mode 100644 Test/Flurl.Test/Http/SettingsExtensionsTests.cs diff --git a/Test/Flurl.Test/Http/ClientConfigTests.cs b/Test/Flurl.Test/Http/ClientConfigTests.cs deleted file mode 100644 index 2a02d41c..00000000 --- a/Test/Flurl.Test/Http/ClientConfigTests.cs +++ /dev/null @@ -1,170 +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, Parallelizable] - public class ClientConfigTestsBase //: ConfigTestsBase - { - //private FlurlClient _client = new FlurlClient(); - - //protected override FlurlHttpSettings GetSettings(FlurlRequest req) { - // return _client.Settings; - //} - - [Test] - public void can_set_timeout() - { - var fc = new FlurlClient().WithTimeout(TimeSpan.FromSeconds(15)); - Assert.AreEqual(TimeSpan.FromSeconds(15), fc.Settings.Timeout); - } - - [Test] - public void can_set_timeout_in_seconds() - { - var client = new FlurlClient().WithTimeout(15); - Assert.AreEqual(client.Settings.Timeout, TimeSpan.FromSeconds(15)); - } - - [Test] - public void can_set_header() - { - var req = new FlurlClient().WithHeader("a", 1); - Assert.AreEqual(1, req.Headers.Count); - Assert.AreEqual(1, req.Headers["a"]); - } - - [Test] - public void can_set_headers_from_anon_object() - { - var req = new FlurlClient().WithHeaders(new { a = "b", one = 2 }); - Assert.AreEqual(2, req.Headers.Count); - Assert.AreEqual("b", req.Headers["a"]); - Assert.AreEqual(2, req.Headers["one"]); - } - - [Test] - public void can_set_headers_from_dictionary() - { - var req = new FlurlClient().WithHeaders(new Dictionary { { "a", "b" }, { "one", 2 } }); - Assert.AreEqual(2, req.Headers.Count); - Assert.AreEqual("b", req.Headers["a"]); - Assert.AreEqual(2, req.Headers["one"]); - } - - [Test] - public void can_setup_oauth_bearer_token() { - var req = new FlurlClient().WithOAuthBearerToken("mytoken"); - Assert.AreEqual(1, req.Headers.Count); - Assert.AreEqual("Bearer mytoken", req.Headers["Authorization"]); - } - - [Test] - public void can_setup_basic_auth() { - var req = new FlurlClient().WithBasicAuth("user", "pass"); - Assert.AreEqual(1, req.Headers.Count); - Assert.AreEqual("Basic dXNlcjpwYXNz", req.Headers["Authorization"]); - } - - [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 = new FlurlClient().AllowHttpStatus("4xx"); - // but then disallow it - client.Settings.AllowedHttpStatusRange = null; - Assert.ThrowsAsync(async () => await client.WithUrl("http://api.com").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()) { - var client = new FlurlClient().Configure(s => s.AllowedHttpStatusRange = "*"); - test.RespondWith("epic fail", 500); - Assert.ThrowsAsync(async () => await "http://www.api.com" - .Configure(c => c.AllowedHttpStatusRange = "2xx") - .WithClient(client) // client-level settings shouldn't win - .GetAsync()); - } - } - - [Test] - public void WithUrl_shares_client_but_not_Url() - { - var client = new FlurlClient().WithCookie("mycookie", "123"); - var req1 = client.WithUrl("http://www.api.com/for-req1"); - var req2 = client.WithUrl("http://www.api.com/for-req2"); - var req3 = client.WithUrl("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 client = new FlurlClient().WithCookie("mycookie", "123"); - var req1 = "http://www.api.com/for-req1".WithClient(client); - var req2 = "http://www.api.com/for-req2".WithClient(client); - var req3 = "http://www.api.com/for-req3".WithClient(client); - - 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().WithUrl(uri); - Assert.AreEqual(uri.ToString(), req.Url.ToString()); - } - } -} \ No newline at end of file diff --git a/Test/Flurl.Test/Http/GlobalConfigTests.cs b/Test/Flurl.Test/Http/GlobalConfigTests.cs deleted file mode 100644 index 6d45cdf9..00000000 --- a/Test/Flurl.Test/Http/GlobalConfigTests.cs +++ /dev/null @@ -1,140 +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. - /// - public abstract class ConfigTestsBase - { - protected abstract FlurlHttpSettings GetSettings(); - - [TearDown] - public void ResetDefaults() { - GetSettings().ResetDefaults(); - } - - [Test] - public void can_provide_custom_client_factory() { - GetSettings().HttpClientFactory = new SomeCustomHttpClientFactory(); - var client = new FlurlClient(); - Assert.IsInstanceOf(client.HttpClient); - Assert.IsInstanceOf(client.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 "http://www.api.com".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 "http://www.api.com".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 "http://www.api.com".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 "http://www.api.com".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 "http://www.api.com".GetAsync(); - Assert.IsFalse(result.IsSuccessStatusCode); - } - catch (FlurlHttpException) { - Assert.Fail("Flurl should not have thrown exception."); - } - } - } - - private class SomeCustomHttpClientFactory : IHttpClientFactory - { - public HttpClient CreateHttpClient(HttpMessageHandler handler) { - return new SomeCustomHttpClient(); - } - - public HttpMessageHandler CreateMessageHandler() { - return new SomeCustomMessageHandler(); - } - } - - private class SomeCustomHttpClient : HttpClient { } - private class SomeCustomMessageHandler : HttpClientHandler { } - } - - //[TestFixture, Parallelizable] - //public class GlobalConfigTestsBase : ConfigTestsBase - //{ - // protected override FlurlHttpSettings GetSettings() { - // return FlurlHttp.GlobalSettings; - // } - //} -} diff --git a/Test/Flurl.Test/Http/SettingsExtensionsTests.cs b/Test/Flurl.Test/Http/SettingsExtensionsTests.cs new file mode 100644 index 00000000..4daec488 --- /dev/null +++ b/Test/Flurl.Test/Http/SettingsExtensionsTests.cs @@ -0,0 +1,164 @@ +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 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.WithUrl("http://api.com"); + + [Test] + public void WithUrl_shares_client_but_not_Url() { + var client = new FlurlClient().WithCookie("mycookie", "123"); + var req1 = client.WithUrl("http://www.api.com/for-req1"); + var req2 = client.WithUrl("http://www.api.com/for-req2"); + var req3 = client.WithUrl("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 client = new FlurlClient().WithCookie("mycookie", "123"); + var req1 = "http://www.api.com/for-req1".WithClient(client); + var req2 = "http://www.api.com/for-req2".WithClient(client); + var req3 = "http://www.api.com/for-req3".WithClient(client); + + 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().WithUrl(uri); + Assert.AreEqual(uri.ToString(), req.Url.ToString()); + } + + [Test] + public void can_override_settings_fluently() { + using (var test = new HttpTest()) { + var client = new FlurlClient().Configure(s => s.AllowedHttpStatusRange = "*"); + test.RespondWith("epic fail", 500); + Assert.ThrowsAsync(async () => await "http://www.api.com" + .Configure(c => c.AllowedHttpStatusRange = "2xx") + .WithClient(client) // 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 index b462cf18..93be4503 100644 --- a/Test/Flurl.Test/Http/SettingsTests.cs +++ b/Test/Flurl.Test/Http/SettingsTests.cs @@ -1,40 +1,147 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Text; using System.Threading.Tasks; using Flurl.Http; +using Flurl.Http.Configuration; +using Flurl.Http.Testing; using NUnit.Framework; namespace Flurl.Test.Http { - [TestFixture, Parallelizable] - public class SettingsTests + /// + /// 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."); + } + } + } + } + + [TestFixture, NonParallelizable] // touches global settings, so can't run in parallel + public class GlobalSettingsTests : SettingsTestsBase { - [Test, NonParallelizable] // tests that mess with global settings shouldn't be parallelized + 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() { try { FlurlHttp.GlobalSettings.CookiesEnabled = false; FlurlHttp.GlobalSettings.AllowedHttpStatusRange = "4xx"; - var fc1 = new FlurlClient(); - fc1.Settings.CookiesEnabled = true; - Assert.AreEqual("4xx", fc1.Settings.AllowedHttpStatusRange); - fc1.Settings.AllowedHttpStatusRange = "5xx"; + var client1 = new FlurlClient(); + client1.Settings.CookiesEnabled = true; + Assert.AreEqual("4xx", client1.Settings.AllowedHttpStatusRange); + client1.Settings.AllowedHttpStatusRange = "5xx"; - var req = fc1.WithUrl("http://myapi.com"); + var req = client1.WithUrl("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 fc2 = new FlurlClient(); - fc2.Settings.CookiesEnabled = false; + var client2 = new FlurlClient(); + client2.Settings.CookiesEnabled = false; - req.WithClient(fc2); + 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"); - fc2.Settings.CookiesEnabled = true; - fc2.Settings.AllowedHttpStatusRange = "3xx"; + 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"); @@ -47,7 +154,7 @@ public void settings_propagate_correctly() { 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"); - fc2.Settings.ResetDefaults(); + 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"); } @@ -55,5 +162,54 @@ public void settings_propagate_correctly() { FlurlHttp.GlobalSettings.ResetDefaults(); } } + + [Test] + public void can_provide_custom_client_factory() { + FlurlHttp.GlobalSettings.HttpClientFactory = new SomeCustomHttpClientFactory(); + Assert.IsInstanceOf(GetRequest().Client.HttpClient); + Assert.IsInstanceOf(GetRequest().Client.HttpMessageHandler); + } + } + + [TestFixture, Parallelizable] + public class HttpTestSettingsTests : SettingsTestsBase + { + 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.WithUrl("http://api.com"); + + [Test] + public void can_provide_custom_client_factory() { + var client = new FlurlClient(); + client.HttpClientFactory = new SomeCustomHttpClientFactory(); + Assert.IsInstanceOf(client.HttpClient); + Assert.IsInstanceOf(client.HttpMessageHandler); + } } + + [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/src/Flurl.Http/Configuration/FlurlHttpSettings.cs b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs index 2a55d919..95cc3858 100644 --- a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs +++ b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs @@ -21,7 +21,6 @@ public class FlurlHttpSettings private IDictionary _vals = new Dictionary(); private static FlurlHttpSettings _baseDefaults = new FlurlHttpSettings(null) { - HttpClientFactory = new DefaultHttpClientFactory(), CookiesEnabled = false, JsonSerializer = new NewtonsoftJsonSerializer(null), UrlEncodedSerializer = new DefaultUrlEncodedSerializer() @@ -41,16 +40,6 @@ public FlurlHttpSettings() { _defaults = _baseDefaults; } - /// - /// 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, the base implementations, and only customize as needed. - /// - public IHttpClientFactory HttpClientFactory { - get => Get(() => HttpClientFactory); - set => Set(() => HttpClientFactory, value); - } - /// /// Gets or sets the HTTP request timeout. /// @@ -150,7 +139,10 @@ public void ResetDefaults() { _vals.Clear(); } - private T Get(Expression> property) { + /// + /// 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; return _vals.ContainsKey(p.Name) ? (T)_vals[p.Name] : @@ -158,7 +150,10 @@ private T Get(Expression> property) { default(T); } - private void Set(Expression> property, T value) { + /// + /// 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; } @@ -184,5 +179,12 @@ public class GlobalFlurlHttpSettings : FlurlHttpSettings /// by proxy, HttpClient instances. /// public IFlurlClientFactory FlurlClientFactory { get; set; } = new DefaultFlurlClientFactory(); + + /// + /// 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; set; } = new DefaultHttpClientFactory(); } } diff --git a/src/Flurl.Http/FlurlClient.cs b/src/Flurl.Http/FlurlClient.cs index fad0ed94..fb14b0a1 100644 --- a/src/Flurl.Http/FlurlClient.cs +++ b/src/Flurl.Http/FlurlClient.cs @@ -1,10 +1,7 @@ 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; namespace Flurl.Http @@ -31,8 +28,9 @@ public interface IFlurlClient : IHttpSettingsContainer, IDisposable { /// public class FlurlClient : IFlurlClient { - private HttpClient _httpClient; - private HttpMessageHandler _httpMessageHandler; + private IHttpClientFactory _httpClientFactory; + private readonly Lazy _httpClient; + private readonly Lazy _httpMessageHandler; /// /// Initializes a new instance of the class. @@ -40,8 +38,8 @@ public class FlurlClient : IFlurlClient /// The FlurlHttpSettings associated with this instance. public FlurlClient(FlurlHttpSettings settings = null) { Settings = settings ?? new FlurlHttpSettings(FlurlHttp.GlobalSettings); - HttpMessageHandler = Settings.HttpClientFactory.CreateMessageHandler(); - HttpClient = Settings.HttpClientFactory.CreateHttpClient(HttpMessageHandler); + _httpClient = new Lazy(() => HttpClientFactory.CreateHttpClient(HttpMessageHandler)); + _httpMessageHandler = new Lazy(() => HttpClientFactory.CreateMessageHandler()); } /// @@ -57,6 +55,16 @@ public FlurlClient(Action configure) : this() { /// public FlurlHttpSettings Settings { get; set; } + /// + /// 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 => _httpClientFactory ?? FlurlHttp.GlobalSettings.HttpClientFactory; + set => _httpClientFactory = value; + } + /// /// Collection of headers sent on all requests using this client. /// @@ -71,48 +79,22 @@ public FlurlClient(Action configure) : this() { /// 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. /// - public HttpClient HttpClient { get; } + public HttpClient HttpClient => _httpClient.Value; /// /// Gets the HttpMessageHandler to be used in subsequent HTTP calls. Creation (when necessary) is delegated /// to FlurlHttp.FlurlClientFactory. /// - public HttpMessageHandler HttpMessageHandler { get; } - - //private HttpClient EnsureHttpClient(HttpClient hc = null) { - // if (_httpClient == null) { - // if (hc == null) { - // hc = Settings.FlurlClientFactory.CreateHttpClient(Url, HttpMessageHandler); - // hc.Timeout = Settings.DefaultTimeout; - // } - // _httpClient = hc; - // _parent?.EnsureHttpClient(hc); - // } - // return _httpClient; - //} - - //private HttpMessageHandler EnsureHttpMessageHandler(HttpMessageHandler handler = null) { - // if (_httpMessageHandler == null) { - // if (handler == null) { - // handler = (HttpTest.Current == null) ? - // Settings.FlurlClientFactory.CreateMessageHandler() : - // new FakeHttpMessageHandler(); - // } - // _httpMessageHandler = handler; - // _parent?.EnsureHttpMessageHandler(handler); - // } - // return _httpMessageHandler; - //} + public HttpMessageHandler HttpMessageHandler => _httpMessageHandler.Value; /// - /// Disposes the underlying HttpClient and HttpMessageHandler, setting both properties to null. + /// Disposes the underlying HttpClient and HttpMessageHandler. /// public void Dispose() { - _httpMessageHandler?.Dispose(); - _httpClient?.Dispose(); - _httpMessageHandler = null; - _httpClient = null; - Cookies = new Dictionary(); + if (_httpMessageHandler.IsValueCreated) + _httpMessageHandler.Value.Dispose(); + if (_httpClient.IsValueCreated) + _httpClient.Value.Dispose(); } } } \ No newline at end of file diff --git a/src/Flurl.Http/FlurlRequest.cs b/src/Flurl.Http/FlurlRequest.cs index a937a894..4a7cd7f9 100644 --- a/src/Flurl.Http/FlurlRequest.cs +++ b/src/Flurl.Http/FlurlRequest.cs @@ -6,7 +6,6 @@ using System.Threading; using System.Threading.Tasks; using Flurl.Http.Configuration; -using Flurl.Http.Testing; using Flurl.Util; namespace Flurl.Http From de773c5b516d5807336c6772de43f6a188b7e0dd Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Wed, 16 Aug 2017 17:00:17 -0500 Subject: [PATCH 17/56] consolidated constructors --- src/Flurl.Http/Content/CapturedMultipartContent.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) 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. /// From c1114947a7fa2106411d4dc9b7f88572ea17a00f Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Wed, 16 Aug 2017 17:00:44 -0500 Subject: [PATCH 18/56] a couple test tweaks --- Test/Flurl.Test/Http/SettingsTests.cs | 77 +++++++++++++-------------- src/Flurl.Http/Testing/HttpTest.cs | 2 +- 2 files changed, 37 insertions(+), 42 deletions(-) diff --git a/Test/Flurl.Test/Http/SettingsTests.cs b/Test/Flurl.Test/Http/SettingsTests.cs index 93be4503..9ab322f4 100644 --- a/Test/Flurl.Test/Http/SettingsTests.cs +++ b/Test/Flurl.Test/Http/SettingsTests.cs @@ -120,47 +120,42 @@ public void ResetDefaults() { [Test] public void settings_propagate_correctly() { - try { - 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.WithUrl("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"); - } - finally { - FlurlHttp.GlobalSettings.ResetDefaults(); - } + 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.WithUrl("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] diff --git a/src/Flurl.Http/Testing/HttpTest.cs b/src/Flurl.Http/Testing/HttpTest.cs index cc6392ba..632358d6 100644 --- a/src/Flurl.Http/Testing/HttpTest.cs +++ b/src/Flurl.Http/Testing/HttpTest.cs @@ -70,7 +70,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); } From e1e5f9d3a050d2302b0f0d6668da61adf4d6ca26 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Thu, 17 Aug 2017 16:42:24 -0500 Subject: [PATCH 19/56] request-level timeouts --- .../PackageTester.NETCore/Program.cs | 8 +- Test/Flurl.Test/Http/RealHttpTests.cs | 110 +++++++++++------- .../Configuration/DefaultHttpClientFactory.cs | 8 +- .../Configuration/FlurlHttpSettings.cs | 1 + .../Configuration/FlurlMessageHandler.cs | 14 +-- src/Flurl.Http/FlurlHttp.cs | 2 +- src/Flurl.Http/FlurlRequest.cs | 19 ++- 7 files changed, 103 insertions(+), 59 deletions(-) diff --git a/PackageTesters/PackageTester.NETCore/Program.cs b/PackageTesters/PackageTester.NETCore/Program.cs index 8bf4da40..973308b4 100644 --- a/PackageTesters/PackageTester.NETCore/Program.cs +++ b/PackageTesters/PackageTester.NETCore/Program.cs @@ -1,12 +1,18 @@ using System; +using Flurl.Http; namespace PackageTester { public class Program { public static void Main(string[] args) { - new Tester().DoTestsAsync().Wait(); + var client = new FlurlClient().EnableCookies(); + client.WithUrl("https://httpbin.org/cookies/set?z=999").HeadAsync().Wait(); + Console.WriteLine("999" == client.Cookies["z"].Value); Console.ReadLine(); + + //new Tester().DoTestsAsync().Wait(); + //Console.ReadLine(); } } } \ No newline at end of file diff --git a/Test/Flurl.Test/Http/RealHttpTests.cs b/Test/Flurl.Test/Http/RealHttpTests.cs index b8a0ef53..b13d30dd 100644 --- a/Test/Flurl.Test/Http/RealHttpTests.cs +++ b/Test/Flurl.Test/Http/RealHttpTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -18,7 +17,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); @@ -27,7 +26,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 client = new FlurlClient(); + var resp = await client.WithUrl("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); @@ -37,7 +37,7 @@ public async Task can_set_request_cookies() { [Test] public async Task can_set_cookies_before_setting_url() { var client = new FlurlClient().WithCookie("z", "999"); - var resp = await client.WithUrl("http://httpbin.org/cookies").GetJsonAsync(); + var resp = await client.WithUrl("https://httpbin.org/cookies").GetJsonAsync(); Assert.AreEqual("999", resp.cookies.z); } @@ -50,33 +50,32 @@ public async Task can_get_response_cookies() { [Test] public async Task can_persist_cookies() { - using (var client = new FlurlClient()) { - var req = "http://httpbin.org/cookies".WithClient(client).WithCookie("z", 999); - // cookie should be set - Assert.AreEqual("999", client.Cookies["z"].Value); - Assert.AreEqual("999", req.Cookies["z"].Value); - - await req.HeadAsync(); - // FlurlClient should be re-used, so cookie should stick - Assert.AreEqual("999", client.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".WithClient(client).GetJsonAsync(); - Assert.AreEqual("999", resp.cookies.z); - } + var client = new FlurlClient(); + var req = "https://httpbin.org/cookies".WithClient(client).WithCookie("z", 999); + // cookie should be set + Assert.AreEqual("999", client.Cookies["z"].Value); + Assert.AreEqual("999", req.Cookies["z"].Value); + + await req.HeadAsync(); + // FlurlClient should be re-used, so cookie should stick + Assert.AreEqual("999", client.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 "https://httpbin.org/cookies".WithClient(client).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); @@ -85,37 +84,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] @@ -131,7 +117,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 }) @@ -185,5 +171,51 @@ public async Task can_comingle_real_and_fake_tests() { } 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); + } } } \ No newline at end of file diff --git a/src/Flurl.Http/Configuration/DefaultHttpClientFactory.cs b/src/Flurl.Http/Configuration/DefaultHttpClientFactory.cs index 19bd4d2d..e4c1197b 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 { @@ -15,7 +16,10 @@ public class DefaultHttpClientFactory : IHttpClientFactory /// customize the result. /// public virtual HttpClient CreateHttpClient(HttpMessageHandler handler) { - return new HttpClient(new FlurlMessageHandler(handler)); + return new HttpClient(new FlurlMessageHandler(handler)) { + // Timeouts handled per request via FlurlHttpSettings.Timeout + Timeout = System.Threading.Timeout.InfiniteTimeSpan + }; } /// diff --git a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs index 95cc3858..a915c4c2 100644 --- a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs +++ b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs @@ -21,6 +21,7 @@ public class FlurlHttpSettings private IDictionary _vals = new Dictionary(); private static FlurlHttpSettings _baseDefaults = new FlurlHttpSettings(null) { + Timeout = TimeSpan.FromSeconds(100), // same as HttpClient CookiesEnabled = false, JsonSerializer = new NewtonsoftJsonSerializer(null), UrlEncodedSerializer = new DefaultUrlEncodedSerializer() diff --git a/src/Flurl.Http/Configuration/FlurlMessageHandler.cs b/src/Flurl.Http/Configuration/FlurlMessageHandler.cs index 4aacf7dc..cdd95d6e 100644 --- a/src/Flurl.Http/Configuration/FlurlMessageHandler.cs +++ b/src/Flurl.Http/Configuration/FlurlMessageHandler.cs @@ -33,7 +33,7 @@ protected override async Task SendAsync(HttpRequestMessage await FlurlHttp.RaiseEventAsync(request, FlurlEventType.BeforeCall).ConfigureAwait(false); call.StartedUtc = DateTime.UtcNow; try { - call.Response = await InnerSendAsync(call, request, cancellationToken).ConfigureAwait(false); + call.Response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); call.Response.RequestMessage = request; if (call.Succeeded) return call.Response; @@ -52,17 +52,5 @@ protected override async Task SendAsync(HttpRequestMessage 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/FlurlHttp.cs b/src/Flurl.Http/FlurlHttp.cs index 9afe1fb5..3936e0f3 100644 --- a/src/Flurl.Http/FlurlHttp.cs +++ b/src/Flurl.Http/FlurlHttp.cs @@ -27,7 +27,7 @@ public static class FlurlHttp /// /// /// A delegate callback throws an exception. - public static void Configure(Action configAction) { + public static void Configure(Action configAction) { lock (_configLock) { configAction(GlobalSettings); } diff --git a/src/Flurl.Http/FlurlRequest.cs b/src/Flurl.Http/FlurlRequest.cs index 4a7cd7f9..5f33415b 100644 --- a/src/Flurl.Http/FlurlRequest.cs +++ b/src/Flurl.Http/FlurlRequest.cs @@ -108,8 +108,15 @@ public IFlurlClient Client { /// 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) { - if (Settings.Timeout.HasValue) - Client.HttpClient.Timeout = Settings.Timeout.Value; + 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; + } + var request = new HttpRequestMessage(verb, Url) { Content = content }; var call = new HttpCall(request, Settings); @@ -117,11 +124,17 @@ public async Task SendAsync(HttpMethod verb, HttpContent co WriteHeaders(request); if (Settings.CookiesEnabled) WriteRequestCookies(request); - return await Client.HttpClient.SendAsync(request, completionOption, cancellationToken ?? CancellationToken.None).ConfigureAwait(false); + return await Client.HttpClient.SendAsync(request, completionOption, token).ConfigureAwait(false); } catch (Exception) when (call.ExceptionHandled) { return call.Response; } + catch (OperationCanceledException ex) when (!userToken.IsCancellationRequested) { + throw new FlurlHttpTimeoutException(call, ex); + } + catch (Exception ex) { + throw new FlurlHttpException(call, ex); + } finally { request.Dispose(); if (Settings.CookiesEnabled) From 3e44c2ea443114b95fc3ce95191394d502b5d8a1 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Thu, 17 Aug 2017 16:43:04 -0500 Subject: [PATCH 20/56] build folder in sln --- Build/build.cmd | 2 +- Flurl.sln | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Build/build.cmd b/Build/build.cmd index a5765a15..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\AutoGenExtensions.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/Flurl.sln b/Flurl.sln index 7711c6fa..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.16 +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,6 +28,13 @@ 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("{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*{84fb572a-8b77-4b09-b825-2a240bce1b7a}*SharedItemsImports = 4 @@ -86,4 +88,7 @@ Global {0231607B-9CA3-4277-9F19-9925694D22E0} = {9A136878-A43E-4154-9B5E-EDAF27E8628D} {AA8792B6-E0FA-46BA-BA03-C7971745F577} = {9A136878-A43E-4154-9B5E-EDAF27E8628D} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {61289482-AC5A-44E1-AEA1-76A3F3CCB6A4} + EndGlobalSection EndGlobal From 16556571e193e4df1250856f71f0ed2ddcf87776 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Fri, 18 Aug 2017 09:02:16 -0500 Subject: [PATCH 21/56] fix & tests for resetting to default settings --- Test/Flurl.Test/Http/SettingsTests.cs | 29 +++++++++++++-- .../Configuration/FlurlHttpSettings.cs | 37 ++++++++++++------- 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/Test/Flurl.Test/Http/SettingsTests.cs b/Test/Flurl.Test/Http/SettingsTests.cs index 9ab322f4..4ca5dbfb 100644 --- a/Test/Flurl.Test/Http/SettingsTests.cs +++ b/Test/Flurl.Test/Http/SettingsTests.cs @@ -105,6 +105,23 @@ public async Task can_disable_exception_behavior() { } } } + + [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 @@ -114,9 +131,7 @@ public class GlobalSettingsTests : SettingsTestsBase protected override IFlurlRequest GetRequest() => new FlurlRequest("http://api.com"); [TearDown] - public void ResetDefaults() { - FlurlHttp.GlobalSettings.ResetDefaults(); - } + public void ResetDefaults() => FlurlHttp.GlobalSettings.ResetDefaults(); [Test] public void settings_propagate_correctly() { @@ -169,6 +184,14 @@ public void can_provide_custom_client_factory() { [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"); } diff --git a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs index a915c4c2..df68e58a 100644 --- a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs +++ b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs @@ -20,13 +20,6 @@ public class FlurlHttpSettings // because if a setting is set to null at the request level, that should stick. private IDictionary _vals = new Dictionary(); - private static FlurlHttpSettings _baseDefaults = new FlurlHttpSettings(null) { - Timeout = TimeSpan.FromSeconds(100), // same as HttpClient - CookiesEnabled = false, - JsonSerializer = new NewtonsoftJsonSerializer(null), - UrlEncodedSerializer = new DefaultUrlEncodedSerializer() - }; - /// /// Creates a new FlurlHttpSettings object using another FlurlHttpSettings object as its default values. /// @@ -37,9 +30,7 @@ public FlurlHttpSettings(FlurlHttpSettings defaults) { /// /// Creates a new FlurlHttpSettings object. /// - public FlurlHttpSettings() { - _defaults = _baseDefaults; - } + public FlurlHttpSettings() : this(FlurlHttp.GlobalSettings) { } /// /// Gets or sets the HTTP request timeout. @@ -134,9 +125,10 @@ public Func OnErrorAsync { } /// - /// Clears all custom options and resets them to their 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() { + public virtual void ResetDefaults() { _vals.Clear(); } @@ -175,17 +167,34 @@ public FlurlHttpSettings Merge(FlurlHttpSettings other) { /// public class GlobalFlurlHttpSettings : FlurlHttpSettings { + 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; set; } = new DefaultFlurlClientFactory(); + public IFlurlClientFactory FlurlClientFactory { get; set; } /// /// 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; set; } = new DefaultHttpClientFactory(); + public IHttpClientFactory HttpClientFactory { get; set; } + + /// + /// Resets all global settings to their Flurl.Http-defined default values. + /// + public override void ResetDefaults() { + base.ResetDefaults(); + Timeout = TimeSpan.FromSeconds(100); // same as HttpClient + CookiesEnabled = false; + JsonSerializer = new NewtonsoftJsonSerializer(null); + UrlEncodedSerializer = new DefaultUrlEncodedSerializer(); + FlurlClientFactory = new DefaultFlurlClientFactory(); + HttpClientFactory = new DefaultHttpClientFactory(); + } } } From 01ad1ce7761f21cfcfadfd36768165f5b06c437a Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Thu, 24 Aug 2017 20:38:39 -0500 Subject: [PATCH 22/56] added URL builder extensions on IFlurlRequest --- Test/Flurl.Test/Http/FlurlClientTests.cs | 8 +- src/Flurl.Http/UrlBuilderExtensions.cs | 158 +++++++++++++++++++++++ 2 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 src/Flurl.Http/UrlBuilderExtensions.cs diff --git a/Test/Flurl.Test/Http/FlurlClientTests.cs b/Test/Flurl.Test/Http/FlurlClientTests.cs index f5b2fc55..c2dd30bf 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,7 +12,10 @@ 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 frExts = ReflectionHelper.GetAllExtensionMethods(typeof(FlurlClient).GetTypeInfo().Assembly).ToList(); + 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(); diff --git a/src/Flurl.Http/UrlBuilderExtensions.cs b/src/Flurl.Http/UrlBuilderExtensions.cs new file mode 100644 index 00000000..2c45ee46 --- /dev/null +++ b/src/Flurl.Http/UrlBuilderExtensions.cs @@ -0,0 +1,158 @@ +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 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 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 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. + /// + /// 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. + /// + /// 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. + /// + /// 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. + /// + /// 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. + /// + /// 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. + /// + /// 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. + /// + /// 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. + /// + /// 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. + /// + /// 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 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 #. + /// + /// This IFlurlRequest + public static IFlurlRequest RemoveFragment(this IFlurlRequest request) { + request.Url.RemoveFragment(); + return request; + } + } +} From 823969ff316eddbc6b9145893796161c84167a35 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Thu, 24 Aug 2017 20:40:48 -0500 Subject: [PATCH 23/56] added IFlurlClient.Request() and BaseUrl, removed WithUrl --- .../PackageTester.NETCore/Program.cs | 2 +- Test/Flurl.Test/Http/FlurlClientTests.cs | 44 ++++++++++++++++ Test/Flurl.Test/Http/RealHttpTests.cs | 12 ++--- .../Http/SettingsExtensionsTests.cs | 10 ++-- Test/Flurl.Test/Http/SettingsTests.cs | 4 +- src/Flurl.Http/FlurlClient.cs | 51 +++++++++++++++---- src/Flurl.Http/FlurlRequest.cs | 15 +++--- src/Flurl.Http/SettingsExtensions.cs | 27 +--------- 8 files changed, 111 insertions(+), 54 deletions(-) diff --git a/PackageTesters/PackageTester.NETCore/Program.cs b/PackageTesters/PackageTester.NETCore/Program.cs index 973308b4..38f2f523 100644 --- a/PackageTesters/PackageTester.NETCore/Program.cs +++ b/PackageTesters/PackageTester.NETCore/Program.cs @@ -7,7 +7,7 @@ public class Program { public static void Main(string[] args) { var client = new FlurlClient().EnableCookies(); - client.WithUrl("https://httpbin.org/cookies/set?z=999").HeadAsync().Wait(); + //client.Request("https://httpbin.org/cookies/set?z=999").HeadAsync().Wait(); Console.WriteLine("999" == client.Cookies["z"].Value); Console.ReadLine(); diff --git a/Test/Flurl.Test/Http/FlurlClientTests.cs b/Test/Flurl.Test/Http/FlurlClientTests.cs index c2dd30bf..ec767e23 100644 --- a/Test/Flurl.Test/Http/FlurlClientTests.cs +++ b/Test/Flurl.Test/Http/FlurlClientTests.cs @@ -33,5 +33,49 @@ public void extension_methods_consistently_supported() { } } } + + [Test] + public void can_create_request_without_base_url() { + var cli = new FlurlClient(); + var req = cli.Request("http://myapi.com/foo"); + Assert.AreEqual("http://myapi.com/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"); + }); + } } } \ No newline at end of file diff --git a/Test/Flurl.Test/Http/RealHttpTests.cs b/Test/Flurl.Test/Http/RealHttpTests.cs index b13d30dd..9f1c6bcd 100644 --- a/Test/Flurl.Test/Http/RealHttpTests.cs +++ b/Test/Flurl.Test/Http/RealHttpTests.cs @@ -27,7 +27,7 @@ public async Task can_download_file() { [Test] public async Task can_set_request_cookies() { var client = new FlurlClient(); - var resp = await client.WithUrl("https://httpbin.org/cookies").WithCookies(new { x = 1, y = 2 }).GetJsonAsync(); + var resp = await client.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); @@ -37,21 +37,21 @@ public async Task can_set_request_cookies() { [Test] public async Task can_set_cookies_before_setting_url() { var client = new FlurlClient().WithCookie("z", "999"); - var resp = await client.WithUrl("https://httpbin.org/cookies").GetJsonAsync(); + var resp = await client.Request("https://httpbin.org/cookies").GetJsonAsync(); Assert.AreEqual("999", resp.cookies.z); } [Test] public async Task can_get_response_cookies() { var client = new FlurlClient().EnableCookies(); - await client.WithUrl("https://httpbin.org/cookies/set?z=999").HeadAsync(); + await client.Request("https://httpbin.org/cookies/set?z=999").HeadAsync(); Assert.AreEqual("999", client.Cookies["z"].Value); } [Test] public async Task can_persist_cookies() { - var client = new FlurlClient(); - var req = "https://httpbin.org/cookies".WithClient(client).WithCookie("z", 999); + var client = new FlurlClient("https://httpbin.org/cookies"); + var req = client.Request().WithCookie("z", 999); // cookie should be set Assert.AreEqual("999", client.Cookies["z"].Value); Assert.AreEqual("999", req.Cookies["z"].Value); @@ -62,7 +62,7 @@ public async Task can_persist_cookies() { Assert.AreEqual("999", req.Cookies["z"].Value); // httpbin returns json representation of cookies that were set on the server. - var resp = await "https://httpbin.org/cookies".WithClient(client).GetJsonAsync(); + var resp = await client.Request().GetJsonAsync(); Assert.AreEqual("999", resp.cookies.z); } diff --git a/Test/Flurl.Test/Http/SettingsExtensionsTests.cs b/Test/Flurl.Test/Http/SettingsExtensionsTests.cs index 4daec488..5b2dadf4 100644 --- a/Test/Flurl.Test/Http/SettingsExtensionsTests.cs +++ b/Test/Flurl.Test/Http/SettingsExtensionsTests.cs @@ -107,14 +107,14 @@ public async Task can_allow_any_http_status() { public class ClientSettingsExtensionsTests : SettingsExtensionsTests { protected override IFlurlClient GetSettingsContainer() => new FlurlClient(); - protected override IFlurlRequest GetRequest(IFlurlClient client) => client.WithUrl("http://api.com"); + protected override IFlurlRequest GetRequest(IFlurlClient client) => client.Request("http://api.com"); [Test] public void WithUrl_shares_client_but_not_Url() { var client = new FlurlClient().WithCookie("mycookie", "123"); - var req1 = client.WithUrl("http://www.api.com/for-req1"); - var req2 = client.WithUrl("http://www.api.com/for-req2"); - var req3 = client.WithUrl("http://www.api.com/for-req3"); + var req1 = client.Request("http://www.api.com/for-req1"); + var req2 = client.Request("http://www.api.com/for-req2"); + var req3 = client.Request("http://www.api.com/for-req3"); CollectionAssert.AreEquivalent(req1.Cookies, req2.Cookies); CollectionAssert.AreEquivalent(req1.Cookies, req3.Cookies); @@ -138,7 +138,7 @@ public void WithClient_shares_client_but_not_Url() { [Test] public void can_use_uri_with_WithUrl() { var uri = new System.Uri("http://www.mysite.com/foo?x=1"); - var req = new FlurlClient().WithUrl(uri); + var req = new FlurlClient().Request(uri); Assert.AreEqual(uri.ToString(), req.Url.ToString()); } diff --git a/Test/Flurl.Test/Http/SettingsTests.cs b/Test/Flurl.Test/Http/SettingsTests.cs index 4ca5dbfb..0cfff7d7 100644 --- a/Test/Flurl.Test/Http/SettingsTests.cs +++ b/Test/Flurl.Test/Http/SettingsTests.cs @@ -143,7 +143,7 @@ public void settings_propagate_correctly() { Assert.AreEqual("4xx", client1.Settings.AllowedHttpStatusRange); client1.Settings.AllowedHttpStatusRange = "5xx"; - var req = client1.WithUrl("http://myapi.com"); + 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"); @@ -202,7 +202,7 @@ 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.WithUrl("http://api.com"); + protected override IFlurlRequest GetRequest() => _client.Value.Request("http://api.com"); [Test] public void can_provide_custom_client_factory() { diff --git a/src/Flurl.Http/FlurlClient.cs b/src/Flurl.Http/FlurlClient.cs index fb14b0a1..822c5d0d 100644 --- a/src/Flurl.Http/FlurlClient.cs +++ b/src/Flurl.Http/FlurlClient.cs @@ -2,7 +2,9 @@ using System.Collections.Generic; using System.Net; using System.Net.Http; +using System.Linq; using Flurl.Http.Configuration; +using Flurl.Util; namespace Flurl.Http { @@ -21,10 +23,22 @@ public interface IFlurlClient : IHttpSettingsContainer, IDisposable { /// to FlurlHttp.FlurlClientFactory. /// HttpMessageHandler HttpMessageHandler { get; } + + /// + /// The base URL associated with this client. + /// + string BaseUrl { get; set; } + + /// + /// Instantiates a new IFlurClient, optionally appending path segments to the BaseUrl. + /// + /// 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); } /// - /// A chainable wrapper around HttpClient and Flurl.Url. + /// A reusable object for making HTTP calls. /// public class FlurlClient : IFlurlClient { @@ -35,20 +49,18 @@ public class FlurlClient : IFlurlClient /// /// Initializes a new instance of the class. /// - /// The FlurlHttpSettings associated with this instance. - public FlurlClient(FlurlHttpSettings settings = null) { - Settings = settings ?? new FlurlHttpSettings(FlurlHttp.GlobalSettings); + /// The base URL associated with this client. + public FlurlClient(string baseUrl = null) { + BaseUrl = baseUrl; + Settings = new FlurlHttpSettings(FlurlHttp.GlobalSettings); _httpClient = new Lazy(() => HttpClientFactory.CreateHttpClient(HttpMessageHandler)); _httpMessageHandler = new Lazy(() => HttpClientFactory.CreateMessageHandler()); } /// - /// Initializes a new instance of the class. + /// The base URL associated with this client. /// - /// Action allowing you to overide default settings inline. - public FlurlClient(Action configure) : this() { - configure(Settings); - } + public string BaseUrl { get; set; } /// /// Gets or sets the FlurlHttpSettings object used by this client. @@ -87,6 +99,27 @@ public IHttpClientFactory HttpClientFactory { /// public HttpMessageHandler HttpMessageHandler => _httpMessageHandler.Value; + /// + /// Instantiates a new IFlurClient, optionally appending path segments to the BaseUrl. + /// + /// 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 + public IFlurlRequest Request(params object[] urlSegments) { + if (!urlSegments.Any()) { + if (string.IsNullOrEmpty(BaseUrl)) + throw new ArgumentException("Cannot create a Request. No URL segments were passed, and this Client does not have a BaseUrl defined."); + return new FlurlRequest(this, BaseUrl); + } + + if (!Url.IsValid(urlSegments[0]?.ToString())) { + if (string.IsNullOrEmpty(BaseUrl)) + throw new ArgumentException("Cannot create a Request. This Client does not have a BaseUrl defined, and the first segment passed is not a valid URL."); + return new FlurlRequest(this, BaseUrl.AppendPathSegments(urlSegments)); + } + + return new FlurlRequest(this, Url.Combine(urlSegments.Select(s => s.ToInvariantString()).ToArray())); + } + /// /// Disposes the underlying HttpClient and HttpMessageHandler. /// diff --git a/src/Flurl.Http/FlurlRequest.cs b/src/Flurl.Http/FlurlRequest.cs index 5f33415b..bc5090d8 100644 --- a/src/Flurl.Http/FlurlRequest.cs +++ b/src/Flurl.Http/FlurlRequest.cs @@ -47,10 +47,11 @@ public class FlurlRequest : IFlurlRequest /// /// Initializes a new instance of the class. /// + /// The IFlurlClient used to send the request. /// The URL to call with this FlurlRequest instance. - /// The FlurlHttpSettings object used by this request. - public FlurlRequest(Url url, FlurlHttpSettings settings = null) { - Settings = settings ?? new FlurlHttpSettings().Merge(FlurlHttp.GlobalSettings); + public FlurlRequest(IFlurlClient client, Url url = null) { + Settings = new FlurlHttpSettings().Merge(FlurlHttp.GlobalSettings); + Client = client; Url = url; } @@ -58,8 +59,10 @@ public FlurlRequest(Url url, FlurlHttpSettings settings = null) { /// Initializes a new instance of the class. /// /// The URL to call with this FlurlRequest instance. - /// The FlurlHttpSettings object used by this request. - public FlurlRequest(string url, FlurlHttpSettings settings = null) : this(new Url(url), settings) { } + public FlurlRequest(Url url = null) { + Settings = new FlurlHttpSettings().Merge(FlurlHttp.GlobalSettings); + Url = url; + } /// /// Gets or sets the FlurlHttpSettings used by this request. @@ -79,7 +82,7 @@ public IFlurlClient Client { } set { _client = value; - Settings.Merge(_client.Settings); + Settings.Merge(_client?.Settings ?? FlurlHttp.GlobalSettings); } } diff --git a/src/Flurl.Http/SettingsExtensions.cs b/src/Flurl.Http/SettingsExtensions.cs index 4aa789f3..c767764b 100644 --- a/src/Flurl.Http/SettingsExtensions.cs +++ b/src/Flurl.Http/SettingsExtensions.cs @@ -1,8 +1,6 @@ using System; using System.Linq; using System.Net; -using System.Net.Http; -using System.Text; using Flurl.Http.Configuration; namespace Flurl.Http @@ -12,27 +10,6 @@ namespace Flurl.Http /// public static class SettingsExtensions { - /// - /// 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; - } - - /// - /// Fluently returns a new IFlurlRequest that can be used to call the given Url with this client. - /// - /// - /// The Url to call. - /// A new IFlurlRequest to use in calling the Url - public static IFlurlRequest WithUrl(this IFlurlClient client, Url url) { - return new FlurlRequest(url) { Client = client }; - } - /// /// Fluently specify the IFlurlClient to use with this IFlurlRequest. /// @@ -51,7 +28,7 @@ public static IFlurlRequest WithClient(this IFlurlRequest request, IFlurlClient /// 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.WithUrl(url); + return client.Request(url); } /// @@ -61,7 +38,7 @@ public static IFlurlRequest WithClient(this Url url, IFlurlClient 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.WithUrl(url); + return client.Request(url); } /// From 5536fa04757f621791e989324cc27a945b1e266e Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Fri, 1 Sep 2017 11:12:21 -0500 Subject: [PATCH 24/56] moved HttpClientFactory from FlurlClient to FlurlClient.Settings --- Test/Flurl.Test/Http/SettingsTests.cs | 2 +- .../Configuration/FlurlHttpSettings.cs | 27 +++++++++++++------ src/Flurl.Http/FlurlClient.cs | 24 +++++++---------- 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/Test/Flurl.Test/Http/SettingsTests.cs b/Test/Flurl.Test/Http/SettingsTests.cs index 0cfff7d7..06c928e2 100644 --- a/Test/Flurl.Test/Http/SettingsTests.cs +++ b/Test/Flurl.Test/Http/SettingsTests.cs @@ -207,7 +207,7 @@ public class ClientSettingsTests : SettingsTestsBase [Test] public void can_provide_custom_client_factory() { var client = new FlurlClient(); - client.HttpClientFactory = new SomeCustomHttpClientFactory(); + client.Settings.HttpClientFactory = new SomeCustomHttpClientFactory(); Assert.IsInstanceOf(client.HttpClient); Assert.IsInstanceOf(client.HttpMessageHandler); } diff --git a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs index df68e58a..b2b92673 100644 --- a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs +++ b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs @@ -162,10 +162,28 @@ public FlurlHttpSettings Merge(FlurlHttpSettings other) { } } + public class ClientFlurlHttpSettings : FlurlHttpSettings + { + /// + /// Creates a new FlurlHttpSettings object using another FlurlHttpSettings object as its default values. + /// + public ClientFlurlHttpSettings(FlurlHttpSettings defaults) : base(defaults) { } + + /// + /// 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 : FlurlHttpSettings + public class GlobalFlurlHttpSettings : ClientFlurlHttpSettings { internal GlobalFlurlHttpSettings() : base(null) { ResetDefaults(); @@ -177,13 +195,6 @@ internal GlobalFlurlHttpSettings() : base(null) { /// public IFlurlClientFactory FlurlClientFactory { get; set; } - /// - /// 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; set; } - /// /// Resets all global settings to their Flurl.Http-defined default values. /// diff --git a/src/Flurl.Http/FlurlClient.cs b/src/Flurl.Http/FlurlClient.cs index 822c5d0d..c61fd67a 100644 --- a/src/Flurl.Http/FlurlClient.cs +++ b/src/Flurl.Http/FlurlClient.cs @@ -42,7 +42,6 @@ public interface IFlurlClient : IHttpSettingsContainer, IDisposable { /// public class FlurlClient : IFlurlClient { - private IHttpClientFactory _httpClientFactory; private readonly Lazy _httpClient; private readonly Lazy _httpMessageHandler; @@ -52,9 +51,9 @@ public class FlurlClient : IFlurlClient /// The base URL associated with this client. public FlurlClient(string baseUrl = null) { BaseUrl = baseUrl; - Settings = new FlurlHttpSettings(FlurlHttp.GlobalSettings); - _httpClient = new Lazy(() => HttpClientFactory.CreateHttpClient(HttpMessageHandler)); - _httpMessageHandler = new Lazy(() => HttpClientFactory.CreateMessageHandler()); + Settings = new ClientFlurlHttpSettings(FlurlHttp.GlobalSettings); + _httpClient = new Lazy(() => Settings.HttpClientFactory.CreateHttpClient(HttpMessageHandler)); + _httpMessageHandler = new Lazy(() => Settings.HttpClientFactory.CreateMessageHandler()); } /// @@ -65,17 +64,7 @@ public FlurlClient(string baseUrl = null) { /// /// Gets or sets the FlurlHttpSettings object used by this client. /// - public FlurlHttpSettings Settings { get; set; } - - /// - /// 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 => _httpClientFactory ?? FlurlHttp.GlobalSettings.HttpClientFactory; - set => _httpClientFactory = value; - } + public ClientFlurlHttpSettings Settings { get; set; } /// /// Collection of headers sent on all requests using this client. @@ -120,6 +109,11 @@ public IFlurlRequest Request(params object[] urlSegments) { return new FlurlRequest(this, Url.Combine(urlSegments.Select(s => s.ToInvariantString()).ToArray())); } + FlurlHttpSettings IHttpSettingsContainer.Settings { + get => Settings; + set => Settings = value as ClientFlurlHttpSettings; + } + /// /// Disposes the underlying HttpClient and HttpMessageHandler. /// From 47d0263dbfd5e64f91fa4417acbed57e6d974450 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Fri, 1 Sep 2017 14:52:00 -0500 Subject: [PATCH 25/56] missed a few xml comments in an earlier commit --- src/Flurl.Http/Configuration/FlurlHttpSettings.cs | 3 +++ src/Flurl.Http/Flurl.Http.csproj | 4 ++++ src/Flurl.Http/UrlBuilderExtensions.cs | 14 ++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs index b2b92673..1e0ef411 100644 --- a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs +++ b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs @@ -162,6 +162,9 @@ public FlurlHttpSettings Merge(FlurlHttpSettings other) { } } + /// + /// Client-level settings for Flurl.Http + /// public class ClientFlurlHttpSettings : FlurlHttpSettings { /// diff --git a/src/Flurl.Http/Flurl.Http.csproj b/src/Flurl.Http/Flurl.Http.csproj index 0e2a9876..551b0e04 100644 --- a/src/Flurl.Http/Flurl.Http.csproj +++ b/src/Flurl.Http/Flurl.Http.csproj @@ -77,6 +77,10 @@ portable-net45+win8+wp8 + + + bin\Debug\net45\Flurl.Http.xml + diff --git a/src/Flurl.Http/UrlBuilderExtensions.cs b/src/Flurl.Http/UrlBuilderExtensions.cs index 2c45ee46..64345208 100644 --- a/src/Flurl.Http/UrlBuilderExtensions.cs +++ b/src/Flurl.Http/UrlBuilderExtensions.cs @@ -11,6 +11,7 @@ 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 . @@ -22,6 +23,7 @@ public static IFlurlRequest AppendPathSegment(this IFlurlRequest request, object /// /// 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) { @@ -32,6 +34,7 @@ public static IFlurlRequest AppendPathSegments(this IFlurlRequest request, param /// /// 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) { @@ -42,6 +45,7 @@ public static IFlurlRequest AppendPathSegments(this IFlurlRequest request, IEnum /// /// 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) @@ -54,6 +58,7 @@ public static IFlurlRequest SetQueryParam(this IFlurlRequest request, string nam /// /// 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 @@ -68,6 +73,7 @@ public static IFlurlRequest SetQueryParam(this IFlurlRequest request, string nam /// /// 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) { @@ -78,6 +84,7 @@ public static IFlurlRequest SetQueryParam(this IFlurlRequest request, string nam /// /// 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 @@ -89,6 +96,7 @@ public static IFlurlRequest SetQueryParams(this IFlurlRequest request, object va /// /// 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) { @@ -99,6 +107,7 @@ public static IFlurlRequest SetQueryParams(this IFlurlRequest request, IEnumerab /// /// 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) { @@ -109,6 +118,7 @@ public static IFlurlRequest SetQueryParams(this IFlurlRequest request, params st /// /// 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) { @@ -119,6 +129,7 @@ public static IFlurlRequest RemoveQueryParam(this IFlurlRequest request, string /// /// 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) { @@ -129,6 +140,7 @@ public static IFlurlRequest RemoveQueryParams(this IFlurlRequest request, params /// /// 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) { @@ -139,6 +151,7 @@ public static IFlurlRequest RemoveQueryParams(this IFlurlRequest request, IEnume /// /// 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) { @@ -149,6 +162,7 @@ public static IFlurlRequest SetFragment(this IFlurlRequest request, string fragm /// /// Removes the URL fragment including the #. /// + /// The IFlurlRequest associated with the URL /// This IFlurlRequest public static IFlurlRequest RemoveFragment(this IFlurlRequest request) { request.Url.RemoveFragment(); From 1fe170b2747ca28925b2ded1dc7ce1c887ddc196 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Fri, 1 Sep 2017 15:03:14 -0500 Subject: [PATCH 26/56] ensure that disposed FlurlClients are not reused --- Test/Flurl.Test/Http/FlurlClientTests.cs | 14 ++++++++++++++ .../DefaultFlurlClientFactory.cs | 19 +++++++++++++++---- src/Flurl.Http/FlurlClient.cs | 15 +++++++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/Test/Flurl.Test/Http/FlurlClientTests.cs b/Test/Flurl.Test/Http/FlurlClientTests.cs index ec767e23..dd44304d 100644 --- a/Test/Flurl.Test/Http/FlurlClientTests.cs +++ b/Test/Flurl.Test/Http/FlurlClientTests.cs @@ -77,5 +77,19 @@ public void cannot_create_request_without_base_url_or_segments_comprising_full_u 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/src/Flurl.Http/Configuration/DefaultFlurlClientFactory.cs b/src/Flurl.Http/Configuration/DefaultFlurlClientFactory.cs index ce6cfc60..5f0b9e95 100644 --- a/src/Flurl.Http/Configuration/DefaultFlurlClientFactory.cs +++ b/src/Flurl.Http/Configuration/DefaultFlurlClientFactory.cs @@ -12,14 +12,25 @@ public class DefaultFlurlClientFactory : IFlurlClientFactory private static readonly ConcurrentDictionary _clients = new ConcurrentDictionary(); /// - /// 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. + /// 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) { - var key = new Uri(url).Host; - return _clients.GetOrAdd(key, _ => new FlurlClient()); + return _clients.AddOrUpdate( + GetCacheKey(url), + _ => new FlurlClient(), + (_, client) => client.IsDisposed ? new FlurlClient() : 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 virtual string GetCacheKey(Url url) => new Uri(url).Host; } } diff --git a/src/Flurl.Http/FlurlClient.cs b/src/Flurl.Http/FlurlClient.cs index c61fd67a..27db2694 100644 --- a/src/Flurl.Http/FlurlClient.cs +++ b/src/Flurl.Http/FlurlClient.cs @@ -35,6 +35,11 @@ public interface IFlurlClient : IHttpSettingsContainer, IDisposable { /// 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 a value indicating whether this instance (and its underlying HttpClient) has been disposed. + /// + bool IsDisposed { get; } } /// @@ -114,14 +119,24 @@ FlurlHttpSettings IHttpSettingsContainer.Settings { set => Settings = value as ClientFlurlHttpSettings; } + /// + /// Gets a value indicating whether this instance (and its underlying HttpClient) has been disposed. + /// + public bool IsDisposed { get; private set; } + /// /// Disposes the underlying HttpClient and HttpMessageHandler. /// public void Dispose() { + if (IsDisposed) + return; + if (_httpMessageHandler.IsValueCreated) _httpMessageHandler.Value.Dispose(); if (_httpClient.IsValueCreated) _httpClient.Value.Dispose(); + + IsDisposed = true; } } } \ No newline at end of file From 47f63677df801cfeb6e7e215191ce3e042803511 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Fri, 1 Sep 2017 18:17:20 -0500 Subject: [PATCH 27/56] Added verb to error messages, so we have "POST http://... failed" instead of "Request to http://... failed" --- src/Flurl.Http/FlurlHttpException.cs | 18 +++++++----------- src/Flurl.Http/HttpCall.cs | 8 ++++++++ 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/Flurl.Http/FlurlHttpException.cs b/src/Flurl.Http/FlurlHttpException.cs index ccc67a9a..30e02720 100644 --- a/src/Flurl.Http/FlurlHttpException.cs +++ b/src/Flurl.Http/FlurlHttpException.cs @@ -38,18 +38,14 @@ 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}"; - } + if (call.Response != null && !call.Succeeded) + return $"{call} failed with status code {(int)call.Response.StatusCode} ({call.Response.ReasonPhrase})."; + + if (inner != null) + return $"{call} failed. {inner.Message}"; // in theory we should never get here. - return $"Request to {call.Request.RequestUri.AbsoluteUri} failed."; + return $"{call} failed."; } /// @@ -93,7 +89,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/HttpCall.cs b/src/Flurl.Http/HttpCall.cs index 39c01418..261c3032 100644 --- a/src/Flurl.Http/HttpCall.cs +++ b/src/Flurl.Http/HttpCall.cs @@ -108,5 +108,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} {Request.RequestUri.AbsoluteUri}"; + } } } From 84791f9b5adedfb106093744cdac002430be1a5e Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Sat, 2 Sep 2017 08:41:39 -0500 Subject: [PATCH 28/56] Url.ConfigureRequest is better than Url.Configure --- Test/Flurl.Test/Http/GetTests.cs | 2 +- Test/Flurl.Test/Http/RealHttpTests.cs | 2 +- Test/Flurl.Test/Http/SettingsExtensionsTests.cs | 2 +- Test/Flurl.Test/Http/TestingTests.cs | 2 +- src/Flurl.Http.CodeGen/Program.cs | 2 +- src/Flurl.Http.CodeGen/UrlExtensionMethod.cs | 8 ++++++-- src/Flurl.Http/GeneratedExtensions.cs | 4 ++-- 7 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Test/Flurl.Test/Http/GetTests.cs b/Test/Flurl.Test/Http/GetTests.cs index f213b1f4..6d6fd23e 100644 --- a/Test/Flurl.Test/Http/GetTests.cs +++ b/Test/Flurl.Test/Http/GetTests.cs @@ -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" - .Configure(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/RealHttpTests.cs b/Test/Flurl.Test/Http/RealHttpTests.cs index 9f1c6bcd..b02e1d3d 100644 --- a/Test/Flurl.Test/Http/RealHttpTests.cs +++ b/Test/Flurl.Test/Http/RealHttpTests.cs @@ -147,7 +147,7 @@ public async Task can_handle_error() { var handlerCalled = false; try { - await "https://httpbin.org/status/500".Configure(c => { + await "https://httpbin.org/status/500".ConfigureRequest(c => { c.OnError = call => { call.ExceptionHandled = true; handlerCalled = true; diff --git a/Test/Flurl.Test/Http/SettingsExtensionsTests.cs b/Test/Flurl.Test/Http/SettingsExtensionsTests.cs index 5b2dadf4..f83b7412 100644 --- a/Test/Flurl.Test/Http/SettingsExtensionsTests.cs +++ b/Test/Flurl.Test/Http/SettingsExtensionsTests.cs @@ -148,7 +148,7 @@ public void can_override_settings_fluently() { var client = new FlurlClient().Configure(s => s.AllowedHttpStatusRange = "*"); test.RespondWith("epic fail", 500); Assert.ThrowsAsync(async () => await "http://www.api.com" - .Configure(c => c.AllowedHttpStatusRange = "2xx") + .ConfigureRequest(c => c.AllowedHttpStatusRange = "2xx") .WithClient(client) // client-level settings shouldn't win .GetAsync()); } diff --git a/Test/Flurl.Test/Http/TestingTests.cs b/Test/Flurl.Test/Http/TestingTests.cs index 45c158f8..003b501e 100644 --- a/Test/Flurl.Test/Http/TestingTests.cs +++ b/Test/Flurl.Test/Http/TestingTests.cs @@ -140,7 +140,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" - .Configure(c => c.OnError = call => call.ExceptionHandled = true) + .ConfigureRequest(c => c.OnError = call => call.ExceptionHandled = true) .GetAsync(); Assert.IsNull(result); } diff --git a/src/Flurl.Http.CodeGen/Program.cs b/src/Flurl.Http.CodeGen/Program.cs index 01d8934d..3fc59ff4 100644 --- a/src/Flurl.Http.CodeGen/Program.cs +++ b/src/Flurl.Http.CodeGen/Program.cs @@ -157,7 +157,7 @@ private static void WriteExtensionMethods(CodeWriter writer) 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($"return new FlurlRequest(url).{xm.RequestMethodName}({string.Join(", ", xm.Params.Select(p => p.Name))});"); writer.WriteLine("}"); } } diff --git a/src/Flurl.Http.CodeGen/UrlExtensionMethod.cs b/src/Flurl.Http.CodeGen/UrlExtensionMethod.cs index f5e3b3c9..20aae7a7 100644 --- a/src/Flurl.Http.CodeGen/UrlExtensionMethod.cs +++ b/src/Flurl.Http.CodeGen/UrlExtensionMethod.cs @@ -35,7 +35,7 @@ public static IEnumerable GetAll() { .AddParam("expires", "DateTime?", "Expiration for all cookies (optional). If excluded, cookies only live for duration of session.", "null"); // settings extensions - yield return new UrlExtensionMethod("Configure", "Creates a new FlurlRequest with the URL and allows changing its Settings inline.") + yield return new UrlExtensionMethod("ConfigureRequest", "Configure", "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."); @@ -49,11 +49,15 @@ public static IEnumerable GetAll() { } public string Name { get; } + public string RequestMethodName { get; } public string Description { get; } public IList Params { get; } = new List(); - public UrlExtensionMethod(string name, string description) { + public UrlExtensionMethod(string name, string description) : this(name, name, description) { } + + public UrlExtensionMethod(string name, string requestMethodName, string description) { Name = name; + RequestMethodName = requestMethodName; Description = description; } diff --git a/src/Flurl.Http/GeneratedExtensions.cs b/src/Flurl.Http/GeneratedExtensions.cs index 707cfa43..8052ad6e 100644 --- a/src/Flurl.Http/GeneratedExtensions.cs +++ b/src/Flurl.Http/GeneratedExtensions.cs @@ -908,7 +908,7 @@ public static IFlurlRequest WithCookies(this Url url, object cookies, DateTime? /// The URL. /// A delegate defining the Settings changes. /// The IFlurlRequest. - public static IFlurlRequest Configure(this Url url, Action action) { + public static IFlurlRequest ConfigureRequest(this Url url, Action action) { return new FlurlRequest(url).Configure(action); } /// @@ -1037,7 +1037,7 @@ public static IFlurlRequest WithCookies(this string url, object cookies, DateTim /// The URL. /// A delegate defining the Settings changes. /// The IFlurlRequest. - public static IFlurlRequest Configure(this string url, Action action) { + public static IFlurlRequest ConfigureRequest(this string url, Action action) { return new FlurlRequest(url).Configure(action); } /// From 60fa56c04cd81ec92053b33396030b6f8447fc34 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Sat, 2 Sep 2017 16:31:56 -0500 Subject: [PATCH 29/56] Shoring up some settings related stuff --- Test/Flurl.Test/Http/TestingTests.cs | 10 +++++- src/Flurl.Http.CodeGen/Program.cs | 2 +- src/Flurl.Http.CodeGen/UrlExtensionMethod.cs | 8 ++--- .../Configuration/FlurlHttpSettings.cs | 2 +- src/Flurl.Http/FlurlClient.cs | 7 +++- src/Flurl.Http/FlurlRequest.cs | 4 +-- src/Flurl.Http/GeneratedExtensions.cs | 4 +-- src/Flurl.Http/SettingsExtensions.cs | 33 ++++++++++++------- src/Flurl.Http/Testing/HttpTest.cs | 10 ++++++ 9 files changed, 55 insertions(+), 25 deletions(-) diff --git a/Test/Flurl.Test/Http/TestingTests.cs b/Test/Flurl.Test/Http/TestingTests.cs index 003b501e..decec3f6 100644 --- a/Test/Flurl.Test/Http/TestingTests.cs +++ b/Test/Flurl.Test/Http/TestingTests.cs @@ -195,7 +195,15 @@ public async Task can_deserialize_default_response_more_than_once() { Assert.IsNull(resp); } - // parallel testing not supported in PCL + [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() { diff --git a/src/Flurl.Http.CodeGen/Program.cs b/src/Flurl.Http.CodeGen/Program.cs index 3fc59ff4..01d8934d 100644 --- a/src/Flurl.Http.CodeGen/Program.cs +++ b/src/Flurl.Http.CodeGen/Program.cs @@ -157,7 +157,7 @@ private static void WriteExtensionMethods(CodeWriter writer) 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.RequestMethodName}({string.Join(", ", xm.Params.Select(p => p.Name))});"); + writer.WriteLine($"return new FlurlRequest(url).{xm.Name}({string.Join(", ", xm.Params.Select(p => p.Name))});"); writer.WriteLine("}"); } } diff --git a/src/Flurl.Http.CodeGen/UrlExtensionMethod.cs b/src/Flurl.Http.CodeGen/UrlExtensionMethod.cs index 20aae7a7..426414f6 100644 --- a/src/Flurl.Http.CodeGen/UrlExtensionMethod.cs +++ b/src/Flurl.Http.CodeGen/UrlExtensionMethod.cs @@ -35,7 +35,7 @@ public static IEnumerable GetAll() { .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", "Configure", "Creates a new FlurlRequest with the URL and allows changing its Settings inline.") + 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."); @@ -49,15 +49,11 @@ public static IEnumerable GetAll() { } public string Name { get; } - public string RequestMethodName { get; } public string Description { get; } public IList Params { get; } = new List(); - public UrlExtensionMethod(string name, string description) : this(name, name, description) { } - - public UrlExtensionMethod(string name, string requestMethodName, string description) { + public UrlExtensionMethod(string name, string description) { Name = name; - RequestMethodName = requestMethodName; Description = description; } diff --git a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs index 1e0ef411..999c5025 100644 --- a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs +++ b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs @@ -18,7 +18,7 @@ public class FlurlHttpSettings // 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 IDictionary _vals = new Dictionary(); + private readonly IDictionary _vals = new Dictionary(); /// /// Creates a new FlurlHttpSettings object using another FlurlHttpSettings object as its default values. diff --git a/src/Flurl.Http/FlurlClient.cs b/src/Flurl.Http/FlurlClient.cs index 27db2694..da5f05d0 100644 --- a/src/Flurl.Http/FlurlClient.cs +++ b/src/Flurl.Http/FlurlClient.cs @@ -12,6 +12,11 @@ namespace Flurl.Http /// Interface defining FlurlClient's contract (useful for mocking and DI) /// public interface IFlurlClient : IHttpSettingsContainer, IDisposable { + /// + /// Gets or sets the FlurlHttpSettings object used by this client. + /// + new ClientFlurlHttpSettings Settings { get; set; } + /// /// 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. @@ -25,7 +30,7 @@ public interface IFlurlClient : IHttpSettingsContainer, IDisposable { HttpMessageHandler HttpMessageHandler { get; } /// - /// The base URL associated with this client. + /// Gets or sets base URL associated with this client. /// string BaseUrl { get; set; } diff --git a/src/Flurl.Http/FlurlRequest.cs b/src/Flurl.Http/FlurlRequest.cs index bc5090d8..c84b7643 100644 --- a/src/Flurl.Http/FlurlRequest.cs +++ b/src/Flurl.Http/FlurlRequest.cs @@ -50,7 +50,7 @@ public class FlurlRequest : IFlurlRequest /// The IFlurlClient used to send the request. /// The URL to call with this FlurlRequest instance. public FlurlRequest(IFlurlClient client, Url url = null) { - Settings = new FlurlHttpSettings().Merge(FlurlHttp.GlobalSettings); + Settings = new FlurlHttpSettings(); Client = client; Url = url; } @@ -60,7 +60,7 @@ public FlurlRequest(IFlurlClient client, Url url = null) { /// /// The URL to call with this FlurlRequest instance. public FlurlRequest(Url url = null) { - Settings = new FlurlHttpSettings().Merge(FlurlHttp.GlobalSettings); + Settings = new FlurlHttpSettings(); Url = url; } diff --git a/src/Flurl.Http/GeneratedExtensions.cs b/src/Flurl.Http/GeneratedExtensions.cs index 8052ad6e..e27569a7 100644 --- a/src/Flurl.Http/GeneratedExtensions.cs +++ b/src/Flurl.Http/GeneratedExtensions.cs @@ -909,7 +909,7 @@ public static IFlurlRequest WithCookies(this Url url, object cookies, DateTime? /// A delegate defining the Settings changes. /// The IFlurlRequest. public static IFlurlRequest ConfigureRequest(this Url url, Action action) { - return new FlurlRequest(url).Configure(action); + return new FlurlRequest(url).ConfigureRequest(action); } /// /// Creates a new FlurlRequest with the URL and sets the request timeout. @@ -1038,7 +1038,7 @@ public static IFlurlRequest WithCookies(this string url, object cookies, DateTim /// A delegate defining the Settings changes. /// The IFlurlRequest. public static IFlurlRequest ConfigureRequest(this string url, Action action) { - return new FlurlRequest(url).Configure(action); + return new FlurlRequest(url).ConfigureRequest(action); } /// /// Creates a new FlurlRequest with the URL and sets the request timeout. diff --git a/src/Flurl.Http/SettingsExtensions.cs b/src/Flurl.Http/SettingsExtensions.cs index c767764b..7fa4e38d 100644 --- a/src/Flurl.Http/SettingsExtensions.cs +++ b/src/Flurl.Http/SettingsExtensions.cs @@ -10,6 +10,28 @@ namespace Flurl.Http /// 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. /// @@ -41,17 +63,6 @@ public static IFlurlRequest WithClient(this string url, IFlurlClient client) { return client.Request(url); } - /// - /// Change FlurlHttpSettings for this IFlurlClient or IFlurlRequest. - /// - /// The IFlurlClient or IFlurlRequest. - /// Action defining the settings changes. - /// The T with the modified HttpClient - public static T Configure(this T obj, Action action) where T : IHttpSettingsContainer { - action(obj.Settings); - return obj; - } - /// /// Sets the timeout for this IFlurlRequest or all requests made with this IFlurlClient. /// diff --git a/src/Flurl.Http/Testing/HttpTest.cs b/src/Flurl.Http/Testing/HttpTest.cs index 632358d6..c52c0264 100644 --- a/src/Flurl.Http/Testing/HttpTest.cs +++ b/src/Flurl.Http/Testing/HttpTest.cs @@ -49,6 +49,16 @@ public HttpTest() { /// 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. /// From be24c86e360905d19d03bd6e097a8c3d8a02d961 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Sat, 2 Sep 2017 16:39:53 -0500 Subject: [PATCH 30/56] FlurlRequest probably doesn't need a ctor that takes a FlurlClient --- src/Flurl.Http/FlurlClient.cs | 6 +++--- src/Flurl.Http/FlurlRequest.cs | 11 ----------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/src/Flurl.Http/FlurlClient.cs b/src/Flurl.Http/FlurlClient.cs index da5f05d0..fbe3d68e 100644 --- a/src/Flurl.Http/FlurlClient.cs +++ b/src/Flurl.Http/FlurlClient.cs @@ -107,16 +107,16 @@ public IFlurlRequest Request(params object[] urlSegments) { if (!urlSegments.Any()) { if (string.IsNullOrEmpty(BaseUrl)) throw new ArgumentException("Cannot create a Request. No URL segments were passed, and this Client does not have a BaseUrl defined."); - return new FlurlRequest(this, BaseUrl); + return new FlurlRequest(BaseUrl).WithClient(this); } if (!Url.IsValid(urlSegments[0]?.ToString())) { if (string.IsNullOrEmpty(BaseUrl)) throw new ArgumentException("Cannot create a Request. This Client does not have a BaseUrl defined, and the first segment passed is not a valid URL."); - return new FlurlRequest(this, BaseUrl.AppendPathSegments(urlSegments)); + return new FlurlRequest(BaseUrl.AppendPathSegments(urlSegments)).WithClient(this); } - return new FlurlRequest(this, Url.Combine(urlSegments.Select(s => s.ToInvariantString()).ToArray())); + return new FlurlRequest(Url.Combine(urlSegments.Select(s => s.ToInvariantString()).ToArray())).WithClient(this); } FlurlHttpSettings IHttpSettingsContainer.Settings { diff --git a/src/Flurl.Http/FlurlRequest.cs b/src/Flurl.Http/FlurlRequest.cs index c84b7643..b19d4cb2 100644 --- a/src/Flurl.Http/FlurlRequest.cs +++ b/src/Flurl.Http/FlurlRequest.cs @@ -44,17 +44,6 @@ public class FlurlRequest : IFlurlRequest { private IFlurlClient _client; - /// - /// Initializes a new instance of the class. - /// - /// The IFlurlClient used to send the request. - /// The URL to call with this FlurlRequest instance. - public FlurlRequest(IFlurlClient client, Url url = null) { - Settings = new FlurlHttpSettings(); - Client = client; - Url = url; - } - /// /// Initializes a new instance of the class. /// From 3b0a5cb0ecf8b780725b09d1afc8cd1b8add8b7a Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Sun, 3 Sep 2017 08:37:46 -0500 Subject: [PATCH 31/56] #195 support array values in URL-encoded requests (jpiggyback on SetQueryParams logic) --- Test/Flurl.Test/Http/PostTests.cs | 4 ++-- .../DefaultUrlEncodedSerializer.cs | 23 ++++++++----------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/Test/Flurl.Test/Http/PostTests.cs b/Test/Flurl.Test/Http/PostTests.cs index 40207449..cba272f6 100644 --- a/Test/Flurl.Test/Http/PostTests.cs +++ b/Test/Flurl.Test/Http/PostTests.cs @@ -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/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 From a4442c3f46605c6a4a20de87b7ce406a02c2551d Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Sun, 3 Sep 2017 12:02:33 -0500 Subject: [PATCH 32/56] Update to #211 - FlurlClient.Request always uses Url.Combine (instead of AppendPathSegments) so something like Request("endpoint?x=1") works as expected --- Test/Flurl.Test/Http/FlurlClientTests.cs | 4 ++-- src/Flurl.Http/FlurlClient.cs | 23 ++++++++++------------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/Test/Flurl.Test/Http/FlurlClientTests.cs b/Test/Flurl.Test/Http/FlurlClientTests.cs index dd44304d..85f8142f 100644 --- a/Test/Flurl.Test/Http/FlurlClientTests.cs +++ b/Test/Flurl.Test/Http/FlurlClientTests.cs @@ -37,8 +37,8 @@ public void extension_methods_consistently_supported() { [Test] public void can_create_request_without_base_url() { var cli = new FlurlClient(); - var req = cli.Request("http://myapi.com/foo"); - Assert.AreEqual("http://myapi.com/foo", req.Url.ToString()); + 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] diff --git a/src/Flurl.Http/FlurlClient.cs b/src/Flurl.Http/FlurlClient.cs index fbe3d68e..526ae314 100644 --- a/src/Flurl.Http/FlurlClient.cs +++ b/src/Flurl.Http/FlurlClient.cs @@ -104,19 +104,16 @@ public FlurlClient(string baseUrl = null) { /// 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 public IFlurlRequest Request(params object[] urlSegments) { - if (!urlSegments.Any()) { - if (string.IsNullOrEmpty(BaseUrl)) - throw new ArgumentException("Cannot create a Request. No URL segments were passed, and this Client does not have a BaseUrl defined."); - return new FlurlRequest(BaseUrl).WithClient(this); - } - - if (!Url.IsValid(urlSegments[0]?.ToString())) { - if (string.IsNullOrEmpty(BaseUrl)) - throw new ArgumentException("Cannot create a Request. This Client does not have a BaseUrl defined, and the first segment passed is not a valid URL."); - return new FlurlRequest(BaseUrl.AppendPathSegments(urlSegments)).WithClient(this); - } - - return new FlurlRequest(Url.Combine(urlSegments.Select(s => s.ToInvariantString()).ToArray())).WithClient(this); + var parts = new List(urlSegments.Select(s => s.ToInvariantString())); + if (!Url.IsValid(parts.FirstOrDefault()) && !string.IsNullOrEmpty(BaseUrl)) + parts.Insert(0, BaseUrl); + + 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."); + + return new FlurlRequest(Url.Combine(parts.ToArray())).WithClient(this); } FlurlHttpSettings IHttpSettingsContainer.Settings { From 8556164f7baf73cf4d747f3d4cd653763fa76d30 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Sun, 3 Sep 2017 12:04:27 -0500 Subject: [PATCH 33/56] #202 - Find CookieContainer correctly when HttpClientHandler is wrapped in 1 or more DelegatingHandlers --- Test/Flurl.Test/Http/RealHttpTests.cs | 39 +++++++++++++++++++++++++++ src/Flurl.Http/FlurlRequest.cs | 19 +++++++++++-- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/Test/Flurl.Test/Http/RealHttpTests.cs b/Test/Flurl.Test/Http/RealHttpTests.cs index b02e1d3d..000c26aa 100644 --- a/Test/Flurl.Test/Http/RealHttpTests.cs +++ b/Test/Flurl.Test/Http/RealHttpTests.cs @@ -1,8 +1,10 @@ using System; 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; @@ -217,5 +219,42 @@ public void can_set_timeout_and_cancellation_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); + } + + 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) { } + } + } } } \ No newline at end of file diff --git a/src/Flurl.Http/FlurlRequest.cs b/src/Flurl.Http/FlurlRequest.cs index b19d4cb2..b930ff09 100644 --- a/src/Flurl.Http/FlurlRequest.cs +++ b/src/Flurl.Http/FlurlRequest.cs @@ -143,7 +143,7 @@ private void WriteHeaders(HttpRequestMessage request) { private void WriteRequestCookies(HttpRequestMessage request) { if (!Cookies.Any()) return; var uri = request.RequestUri; - var cookieHandler = Client.HttpMessageHandler as HttpClientHandler; + 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) { @@ -167,7 +167,7 @@ private void ReadResponseCookies(HttpResponseMessage response) { // 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 = (Client.HttpMessageHandler as HttpClientHandler)?.CookieContainer; + var jar = FindHttpClientHandler(Client.HttpMessageHandler)?.CookieContainer; if (jar == null) { // http://stackoverflow.com/a/15588878/62600 IEnumerable cookieHeaders; @@ -183,5 +183,20 @@ private void ReadResponseCookies(HttpResponseMessage response) { 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; + } } } \ No newline at end of file From 45f5514933d11de0e292b8ece72d0d48a8e863b7 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Sun, 3 Sep 2017 12:22:30 -0500 Subject: [PATCH 34/56] #208 better null handling when reading cookies --- src/Flurl.Http/FlurlRequest.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Flurl.Http/FlurlRequest.cs b/src/Flurl.Http/FlurlRequest.cs index b930ff09..ca4cdd9a 100644 --- a/src/Flurl.Http/FlurlRequest.cs +++ b/src/Flurl.Http/FlurlRequest.cs @@ -161,9 +161,9 @@ private void WriteRequestCookies(HttpRequestMessage request) { } private void ReadResponseCookies(HttpResponseMessage response) { - if (response?.RequestMessage == null) return; - - var uri = response.RequestMessage.RequestUri; + 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. From aebe8e3da4ca568c1f67e170c1778c66207418eb Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Mon, 4 Sep 2017 08:45:09 -0500 Subject: [PATCH 35/56] Flurl version change --- src/Flurl/Flurl.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Flurl/Flurl.csproj b/src/Flurl/Flurl.csproj index ff47de5d..69c5da9e 100644 --- a/src/Flurl/Flurl.csproj +++ b/src/Flurl/Flurl.csproj @@ -3,7 +3,7 @@ net40;netstandard1.3;netstandard1.0; True - 3.0.0 + 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 @@ -13,7 +13,7 @@ git fluent url uri querystring builder - 3.0.0 - Drop PCL target + 2.5.0 - Drop PCL target. add .NET Standard 1.0 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) From 124235d0779f480ab4215ff17d8efaf80f73b92f Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Mon, 4 Sep 2017 09:03:27 -0500 Subject: [PATCH 36/56] nuget - added PackageIds, dropped detailed release notes --- src/Flurl.Http/Flurl.Http.csproj | 39 ++------------------------------ src/Flurl/Flurl.csproj | 28 ++--------------------- 2 files changed, 4 insertions(+), 63 deletions(-) diff --git a/src/Flurl.Http/Flurl.Http.csproj b/src/Flurl.Http/Flurl.Http.csproj index 551b0e04..93b8c39e 100644 --- a/src/Flurl.Http/Flurl.Http.csproj +++ b/src/Flurl.Http/Flurl.Http.csproj @@ -3,6 +3,7 @@ net45;netstandard1.3;netstandard1.1; True + Flurl.Http 2.0.0 Todd Menier A fluent, portable, testable HTTP client library that extends Flurl's URL builder chain. @@ -12,43 +13,7 @@ https://github.com/tmenier/Flurl.git git httpclient rest json http fluent url uri tdd assert async - - 3.0.0 - Drop PCL target - 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). - + See https://github.com/tmenier/Flurl/releases false diff --git a/src/Flurl/Flurl.csproj b/src/Flurl/Flurl.csproj index 69c5da9e..783170d7 100644 --- a/src/Flurl/Flurl.csproj +++ b/src/Flurl/Flurl.csproj @@ -3,6 +3,7 @@ net40;netstandard1.3;netstandard1.0; True + Flurl 2.5.0 Todd Menier A fluent, portable URL builder. To make HTTP calls off the fluent chain, check out Flurl.Http. @@ -12,32 +13,7 @@ https://github.com/tmenier/Flurl.git git fluent url uri querystring builder - - 2.5.0 - Drop PCL target. add .NET Standard 1.0 - 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. - + See https://github.com/tmenier/Flurl/releases false From ee0b62c332ae13ad1e032380b1a0bff8a8eff851 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Mon, 4 Sep 2017 09:22:01 -0500 Subject: [PATCH 37/56] fixed Flurl version in a few references --- PackageTesters/PackageTester.NET45/packages.config | 2 +- PackageTesters/PackageTester.NET461/packages.config | 2 +- src/Flurl.Http/Flurl.Http.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PackageTesters/PackageTester.NET45/packages.config b/PackageTesters/PackageTester.NET45/packages.config index d9e0f962..8b8a6d01 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/packages.config b/PackageTesters/PackageTester.NET461/packages.config index cab6b76f..f29f7bd4 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/src/Flurl.Http/Flurl.Http.csproj b/src/Flurl.Http/Flurl.Http.csproj index 93b8c39e..535f31c5 100644 --- a/src/Flurl.Http/Flurl.Http.csproj +++ b/src/Flurl.Http/Flurl.Http.csproj @@ -27,7 +27,7 @@ - + From 861589eb9ad354fe4360ebd923764061b7ae51cf Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Mon, 4 Sep 2017 11:22:40 -0500 Subject: [PATCH 38/56] Flurl.Http 2.0 prerelease 1 --- src/Flurl.Http/Flurl.Http.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Flurl.Http/Flurl.Http.csproj b/src/Flurl.Http/Flurl.Http.csproj index 535f31c5..966eac5a 100644 --- a/src/Flurl.Http/Flurl.Http.csproj +++ b/src/Flurl.Http/Flurl.Http.csproj @@ -4,7 +4,7 @@ net45;netstandard1.3;netstandard1.1; True Flurl.Http - 2.0.0 + 2.0.0-pre1 Todd Menier A fluent, portable, testable HTTP client library that extends Flurl's URL builder chain. http://tmenier.github.io/Flurl From 32b6f013eb2be571333de2db8f5cbb1069d20ff9 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Mon, 4 Sep 2017 12:27:45 -0500 Subject: [PATCH 39/56] updated package testers for Flurl.Http-pre1 --- .../PackageTester.NET45/PackageTester.NET45.csproj | 6 +++--- PackageTesters/PackageTester.NET45/packages.config | 2 +- .../PackageTester.NET461/PackageTester.NET461.csproj | 6 +++--- PackageTesters/PackageTester.NET461/packages.config | 2 +- .../PackageTester.NETCore/PackageTester.NETCore.csproj | 2 +- PackageTesters/PackageTester.NETCore/Program.cs | 8 +------- 6 files changed, 10 insertions(+), 16 deletions(-) diff --git a/PackageTesters/PackageTester.NET45/PackageTester.NET45.csproj b/PackageTesters/PackageTester.NET45/PackageTester.NET45.csproj index 6a58e0d7..7f8c90ff 100644 --- a/PackageTesters/PackageTester.NET45/PackageTester.NET45.csproj +++ b/PackageTesters/PackageTester.NET45/PackageTester.NET45.csproj @@ -31,11 +31,11 @@ 4 - - ..\..\packages\Flurl.3.0.0\lib\net40\Flurl.dll + + ..\..\packages\Flurl.2.5.0\lib\net40\Flurl.dll - ..\..\packages\Flurl.Http.2.0.0\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.NET45/packages.config b/PackageTesters/PackageTester.NET45/packages.config index 8b8a6d01..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 9566a523..e7a57446 100644 --- a/PackageTesters/PackageTester.NET461/PackageTester.NET461.csproj +++ b/PackageTesters/PackageTester.NET461/PackageTester.NET461.csproj @@ -33,11 +33,11 @@ 4 - - ..\..\packages\Flurl.3.0.0\lib\net40\Flurl.dll + + ..\..\packages\Flurl.2.5.0\lib\net40\Flurl.dll - ..\..\packages\Flurl.Http.2.0.0\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 f29f7bd4..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 33667649..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.NETCore/Program.cs b/PackageTesters/PackageTester.NETCore/Program.cs index 38f2f523..8bf4da40 100644 --- a/PackageTesters/PackageTester.NETCore/Program.cs +++ b/PackageTesters/PackageTester.NETCore/Program.cs @@ -1,18 +1,12 @@ using System; -using Flurl.Http; namespace PackageTester { public class Program { public static void Main(string[] args) { - var client = new FlurlClient().EnableCookies(); - //client.Request("https://httpbin.org/cookies/set?z=999").HeadAsync().Wait(); - Console.WriteLine("999" == client.Cookies["z"].Value); + new Tester().DoTestsAsync().Wait(); Console.ReadLine(); - - //new Tester().DoTestsAsync().Wait(); - //Console.ReadLine(); } } } \ No newline at end of file From a801d381a4eded9dac3f099a4e33ac2c21f9ef57 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Tue, 5 Sep 2017 15:42:53 -0500 Subject: [PATCH 40/56] Factored out FlurlMessageHandler --- .../Configuration/DefaultHttpClientFactory.cs | 2 +- .../Configuration/FlurlMessageHandler.cs | 56 ------------------- src/Flurl.Http/FlurlHttp.cs | 52 +---------------- src/Flurl.Http/FlurlRequest.cs | 45 +++++++++++---- 4 files changed, 37 insertions(+), 118 deletions(-) delete mode 100644 src/Flurl.Http/Configuration/FlurlMessageHandler.cs diff --git a/src/Flurl.Http/Configuration/DefaultHttpClientFactory.cs b/src/Flurl.Http/Configuration/DefaultHttpClientFactory.cs index e4c1197b..44396763 100644 --- a/src/Flurl.Http/Configuration/DefaultHttpClientFactory.cs +++ b/src/Flurl.Http/Configuration/DefaultHttpClientFactory.cs @@ -16,7 +16,7 @@ public class DefaultHttpClientFactory : IHttpClientFactory /// customize the result. /// public virtual HttpClient CreateHttpClient(HttpMessageHandler handler) { - return new HttpClient(new FlurlMessageHandler(handler)) { + return new HttpClient(handler) { // Timeouts handled per request via FlurlHttpSettings.Timeout Timeout = System.Threading.Timeout.InfiniteTimeSpan }; diff --git a/src/Flurl.Http/Configuration/FlurlMessageHandler.cs b/src/Flurl.Http/Configuration/FlurlMessageHandler.cs deleted file mode 100644 index cdd95d6e..00000000 --- a/src/Flurl.Http/Configuration/FlurlMessageHandler.cs +++ /dev/null @@ -1,56 +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 base.SendAsync(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); - } - } - } -} \ No newline at end of file diff --git a/src/Flurl.Http/FlurlHttp.cs b/src/Flurl.Http/FlurlHttp.cs index 3936e0f3..d9f4641d 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; using Flurl.Http.Testing; @@ -12,9 +10,8 @@ namespace Flurl.Http public static class FlurlHttp { private static readonly object _configLock = new object(); - private static readonly Task _completedTask = Task.FromResult(0); - private static Lazy _settings = + private static Lazy _settings = new Lazy(() => new GlobalFlurlHttpSettings()); /// @@ -32,52 +29,5 @@ public static void Configure(Action configAction) { configAction(GlobalSettings); } } - - /// - /// Triggers the specified sync and async event handlers, usually defined on - /// - public static Task RaiseEventAsync(HttpRequestMessage request, FlurlEventType eventType) { - var call = HttpCall.Get(request); - var settings = call?.Settings; - - if (settings == null) - return _completedTask; - - 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 _completedTask; - } - } - - private static Task HandleEventAsync(Action syncHandler, Func asyncHandler, HttpCall call) { - syncHandler?.Invoke(call); - if (asyncHandler != null) - return asyncHandler(call); - return _completedTask; - } - } - - /// - /// 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/FlurlRequest.cs b/src/Flurl.Http/FlurlRequest.cs index ca4cdd9a..2e28c585 100644 --- a/src/Flurl.Http/FlurlRequest.cs +++ b/src/Flurl.Http/FlurlRequest.cs @@ -100,6 +100,11 @@ public IFlurlClient Client { /// 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); + + await HandleEventAsync(Settings.BeforeCall, Settings.BeforeCallAsync, call).ConfigureAwait(false); + var userToken = cancellationToken ?? CancellationToken.None; var token = userToken; @@ -109,28 +114,41 @@ public async Task SendAsync(HttpMethod verb, HttpContent co token = cts.Token; } - var request = new HttpRequestMessage(verb, Url) { Content = content }; - var call = new HttpCall(request, Settings); - + call.StartedUtc = DateTime.UtcNow; try { WriteHeaders(request); if (Settings.CookiesEnabled) WriteRequestCookies(request); - return await Client.HttpClient.SendAsync(request, completionOption, token).ConfigureAwait(false); - } - catch (Exception) when (call.ExceptionHandled) { - return call.Response; - } - catch (OperationCanceledException ex) when (!userToken.IsCancellationRequested) { - throw new FlurlHttpTimeoutException(call, ex); + + call.Response = await Client.HttpClient.SendAsync(request, completionOption, token).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 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); + 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); } } @@ -198,5 +216,12 @@ private HttpClientHandler FindHttpClientHandler(HttpMessageHandler handler) { // 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 From 7e949aac97f67ca439dc9077f195783b4e1bb717 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Tue, 5 Sep 2017 15:58:41 -0500 Subject: [PATCH 41/56] HttpCall refactor - added FlurlRequest, removed Url & Settings --- src/Flurl.Http/FlurlHttpException.cs | 4 ++-- src/Flurl.Http/FlurlRequest.cs | 3 ++- src/Flurl.Http/HttpCall.cs | 22 +++++-------------- .../HttpResponseMessageExtensions.cs | 2 +- src/Flurl.Http/Testing/HttpCallAssertion.cs | 14 ++++++------ 5 files changed, 18 insertions(+), 27 deletions(-) diff --git a/src/Flurl.Http/FlurlHttpException.cs b/src/Flurl.Http/FlurlHttpException.cs index 30e02720..9ed12367 100644 --- a/src/Flurl.Http/FlurlHttpException.cs +++ b/src/Flurl.Http/FlurlHttpException.cs @@ -63,8 +63,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); } /// diff --git a/src/Flurl.Http/FlurlRequest.cs b/src/Flurl.Http/FlurlRequest.cs index 2e28c585..f8272863 100644 --- a/src/Flurl.Http/FlurlRequest.cs +++ b/src/Flurl.Http/FlurlRequest.cs @@ -101,9 +101,10 @@ public IFlurlClient Client { /// 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); + 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; diff --git a/src/Flurl.Http/HttpCall.cs b/src/Flurl.Http/HttpCall.cs index 261c3032..5e0ece20 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,12 +26,12 @@ 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; } @@ -83,11 +78,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 +87,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. diff --git a/src/Flurl.Http/HttpResponseMessageExtensions.cs b/src/Flurl.Http/HttpResponseMessageExtensions.cs index ffa4e087..6c6eb305 100644 --- a/src/Flurl.Http/HttpResponseMessageExtensions.cs +++ b/src/Flurl.Http/HttpResponseMessageExtensions.cs @@ -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; diff --git a/src/Flurl.Http/Testing/HttpCallAssertion.cs b/src/Flurl.Http/Testing/HttpCallAssertion.cs index 3ac6567a..67406db6 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))); } /// From 84a45805e2a7c6445e52953b15b0567aa50bbf0d Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Wed, 6 Sep 2017 19:51:25 -0500 Subject: [PATCH 42/56] Configure a specific FlurlClient from global config (FlurlHttp.ConfigureClient) --- Test/Flurl.Test/Http/SettingsTests.cs | 13 +++++++++++++ src/Flurl.Http/FlurlHttp.cs | 16 ++++++++++++++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/Test/Flurl.Test/Http/SettingsTests.cs b/Test/Flurl.Test/Http/SettingsTests.cs index 06c928e2..cf1d7c3b 100644 --- a/Test/Flurl.Test/Http/SettingsTests.cs +++ b/Test/Flurl.Test/Http/SettingsTests.cs @@ -179,6 +179,19 @@ public void can_provide_custom_client_factory() { 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] diff --git a/src/Flurl.Http/FlurlHttp.cs b/src/Flurl.Http/FlurlHttp.cs index d9f4641d..99024232 100644 --- a/src/Flurl.Http/FlurlHttp.cs +++ b/src/Flurl.Http/FlurlHttp.cs @@ -22,12 +22,24 @@ public static class FlurlHttp /// /// 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. + /// the action to perform against the GlobalSettings public static void Configure(Action configAction) { lock (_configLock) { configAction(GlobalSettings); } } + + /// + /// 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. + /// + /// 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); + } + } } } \ No newline at end of file From e5063ee4e5e5950acf0115402b2e75225e226b57 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Wed, 6 Sep 2017 19:53:06 -0500 Subject: [PATCH 43/56] Flurl.Http-pre2 --- src/Flurl.Http/Flurl.Http.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Flurl.Http/Flurl.Http.csproj b/src/Flurl.Http/Flurl.Http.csproj index 966eac5a..5b09f132 100644 --- a/src/Flurl.Http/Flurl.Http.csproj +++ b/src/Flurl.Http/Flurl.Http.csproj @@ -4,7 +4,7 @@ net45;netstandard1.3;netstandard1.1; True Flurl.Http - 2.0.0-pre1 + 2.0.0-pre2 Todd Menier A fluent, portable, testable HTTP client library that extends Flurl's URL builder chain. http://tmenier.github.io/Flurl From 79cec6ee6dfd7bd8c029332c2395be560920e01b Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Sat, 23 Sep 2017 09:47:14 -0500 Subject: [PATCH 44/56] 2 IFlurlClientFactory impls now provided: PerHost (default) and PerBaseUrl (handy for IoC) --- ...ntFactory.cs => FlurlClientFactoryBase.cs} | 19 ++++++++---- .../Configuration/FlurlHttpSettings.cs | 2 +- .../PerBaseUrlFlurlClientFactory.cs | 31 +++++++++++++++++++ .../PerHostFlurlClientFactory.cs | 25 +++++++++++++++ src/Flurl.Http/Testing/TestFactories.cs | 11 ++++++- 5 files changed, 80 insertions(+), 8 deletions(-) rename src/Flurl.Http/Configuration/{DefaultFlurlClientFactory.cs => FlurlClientFactoryBase.cs} (61%) create mode 100644 src/Flurl.Http/Configuration/PerBaseUrlFlurlClientFactory.cs create mode 100644 src/Flurl.Http/Configuration/PerHostFlurlClientFactory.cs diff --git a/src/Flurl.Http/Configuration/DefaultFlurlClientFactory.cs b/src/Flurl.Http/Configuration/FlurlClientFactoryBase.cs similarity index 61% rename from src/Flurl.Http/Configuration/DefaultFlurlClientFactory.cs rename to src/Flurl.Http/Configuration/FlurlClientFactoryBase.cs index 5f0b9e95..b0e89291 100644 --- a/src/Flurl.Http/Configuration/DefaultFlurlClientFactory.cs +++ b/src/Flurl.Http/Configuration/FlurlClientFactoryBase.cs @@ -4,12 +4,12 @@ namespace Flurl.Http.Configuration { /// - /// Default implementation of IFlurlClientFactory used by Flurl.Http. Custom factories looking to extend + /// 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 class DefaultFlurlClientFactory : IFlurlClientFactory + public abstract class FlurlClientFactoryBase : IFlurlClientFactory { - private static readonly ConcurrentDictionary _clients = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _clients = new ConcurrentDictionary(); /// /// By defaykt, uses a caching strategy of one FlurlClient per host. This maximizes reuse of @@ -20,8 +20,8 @@ public class DefaultFlurlClientFactory : IFlurlClientFactory public virtual IFlurlClient Get(Url url) { return _clients.AddOrUpdate( GetCacheKey(url), - _ => new FlurlClient(), - (_, client) => client.IsDisposed ? new FlurlClient() : client); + u => Create(u), + (u, client) => client.IsDisposed ? Create(u) : client); } /// @@ -31,6 +31,13 @@ public virtual IFlurlClient Get(Url url) { /// /// The URL. /// The cache key - protected virtual string GetCacheKey(Url url) => new Uri(url).Host; + 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 999c5025..be70d9d7 100644 --- a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs +++ b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs @@ -207,7 +207,7 @@ public override void ResetDefaults() { CookiesEnabled = false; JsonSerializer = new NewtonsoftJsonSerializer(null); UrlEncodedSerializer = new DefaultUrlEncodedSerializer(); - FlurlClientFactory = new DefaultFlurlClientFactory(); + FlurlClientFactory = new PerHostFlurlClientFactory(); HttpClientFactory = new DefaultHttpClientFactory(); } } 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/Testing/TestFactories.cs b/src/Flurl.Http/Testing/TestFactories.cs index 1d3960ba..98e6b5ac 100644 --- a/src/Flurl.Http/Testing/TestFactories.cs +++ b/src/Flurl.Http/Testing/TestFactories.cs @@ -21,7 +21,7 @@ public override HttpMessageHandler CreateMessageHandler() { /// /// IFlurlClientFactory implementation used to fake and record calls in tests. /// - public class TestFlurlClientFactory : DefaultFlurlClientFactory + public class TestFlurlClientFactory : FlurlClientFactoryBase { private readonly Lazy _client = new Lazy(() => new FlurlClient()); @@ -33,5 +33,14 @@ public class TestFlurlClientFactory : DefaultFlurlClientFactory 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 From 1322837d2e54aa9ecacb1e9b05d2e49dff80aab2 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Sat, 23 Sep 2017 09:49:43 -0500 Subject: [PATCH 45/56] made FlurlClient.Dispose virtual --- src/Flurl.Http/FlurlClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Flurl.Http/FlurlClient.cs b/src/Flurl.Http/FlurlClient.cs index 526ae314..b76477d2 100644 --- a/src/Flurl.Http/FlurlClient.cs +++ b/src/Flurl.Http/FlurlClient.cs @@ -84,7 +84,7 @@ public FlurlClient(string baseUrl = null) { /// /// Collection of HttpCookies sent and received on all requests using this client. /// - public IDictionary Cookies { get; private set; } = new Dictionary(); + public IDictionary Cookies { get; } = new Dictionary(); /// /// Gets the HttpClient to be used in subsequent HTTP calls. Creation (when necessary) is delegated @@ -129,7 +129,7 @@ FlurlHttpSettings IHttpSettingsContainer.Settings { /// /// Disposes the underlying HttpClient and HttpMessageHandler. /// - public void Dispose() { + public virtual void Dispose() { if (IsDisposed) return; From 0f1ccb356d8bc00a9e60286b29010a6ad5cc2972 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Sat, 23 Sep 2017 12:19:14 -0500 Subject: [PATCH 46/56] #44 WithHeaders(object) convert underscores in property names to hyphens --- .../Http/SettingsExtensionsTests.cs | 19 +++++++++++++++++++ src/Flurl.Http.CodeGen/UrlExtensionMethod.cs | 3 ++- src/Flurl.Http/GeneratedExtensions.cs | 10 ++++++---- src/Flurl.Http/HeaderExtensions.cs | 10 ++++++++-- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/Test/Flurl.Test/Http/SettingsExtensionsTests.cs b/Test/Flurl.Test/Http/SettingsExtensionsTests.cs index f83b7412..36009bdd 100644 --- a/Test/Flurl.Test/Http/SettingsExtensionsTests.cs +++ b/Test/Flurl.Test/Http/SettingsExtensionsTests.cs @@ -52,6 +52,25 @@ public void can_set_headers_from_dictionary() { 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"); diff --git a/src/Flurl.Http.CodeGen/UrlExtensionMethod.cs b/src/Flurl.Http.CodeGen/UrlExtensionMethod.cs index 426414f6..b523de9e 100644 --- a/src/Flurl.Http.CodeGen/UrlExtensionMethod.cs +++ b/src/Flurl.Http.CodeGen/UrlExtensionMethod.cs @@ -15,7 +15,8 @@ public static IEnumerable GetAll() { .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("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."); diff --git a/src/Flurl.Http/GeneratedExtensions.cs b/src/Flurl.Http/GeneratedExtensions.cs index e27569a7..3b535c9e 100644 --- a/src/Flurl.Http/GeneratedExtensions.cs +++ b/src/Flurl.Http/GeneratedExtensions.cs @@ -841,9 +841,10 @@ public static IFlurlRequest WithHeader(this Url url, string name, object value) /// /// 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) { - return new FlurlRequest(url).WithHeaders(headers); + 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. @@ -970,9 +971,10 @@ public static IFlurlRequest WithHeader(this string url, string name, object valu /// /// 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) { - return new FlurlRequest(url).WithHeaders(headers); + 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. diff --git a/src/Flurl.Http/HeaderExtensions.cs b/src/Flurl.Http/HeaderExtensions.cs index bc82ef61..0d854d52 100644 --- a/src/Flurl.Http/HeaderExtensions.cs +++ b/src/Flurl.Http/HeaderExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; @@ -29,13 +30,18 @@ public static T WithHeader(this T clientOrRequest, string name, object value) /// /// 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) where T : IHttpSettingsContainer { + 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()) { - clientOrRequest.WithHeader(kv.Key, kv.Value); + var key = replaceUnderscoreWithHyphen ? kv.Key.Replace("_", "-") : kv.Key; + clientOrRequest.WithHeader(key, kv.Value); } return clientOrRequest; From 1ba1d9efa5b8e0e5a551c3a0e1e995d35694ccdf Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Sun, 24 Sep 2017 20:10:31 -0500 Subject: [PATCH 47/56] #217 more robust exception message, minor tweaks to exception handling --- .../Http/FlurlHttpExceptionTests.cs | 41 +++++++++++++++++++ Test/Flurl.Test/Http/TestingTests.cs | 18 -------- src/Flurl.Http/FlurlHttpException.cs | 19 ++++++--- src/Flurl.Http/FlurlRequest.cs | 5 +++ src/Flurl.Http/HttpCall.cs | 13 ++---- 5 files changed, 63 insertions(+), 33 deletions(-) create mode 100644 Test/Flurl.Test/Http/FlurlHttpExceptionTests.cs 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/TestingTests.cs b/Test/Flurl.Test/Http/TestingTests.cs index decec3f6..7c5a65f0 100644 --- a/Test/Flurl.Test/Http/TestingTests.cs +++ b/Test/Flurl.Test/Http/TestingTests.cs @@ -165,24 +165,6 @@ public async Task can_fake_cookies() { Assert.AreEqual("foo", rec.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); - } - } - } - // https://github.com/tmenier/Flurl/issues/175 [Test] public async Task can_deserialize_default_response_more_than_once() { diff --git a/src/Flurl.Http/FlurlHttpException.cs b/src/Flurl.Http/FlurlHttpException.cs index 9ed12367..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,14 +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) { + var sb = new StringBuilder(); + if (call.Response != null && !call.Succeeded) - return $"{call} failed with status code {(int)call.Response.StatusCode} ({call.Response.ReasonPhrase})."; + 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 (inner != null) - return $"{call} failed. {inner.Message}"; + if (!string.IsNullOrWhiteSpace(call.ErrorResponseBody)) + sb.AppendLine("Response body:").AppendLine(call.ErrorResponseBody); - // in theory we should never get here. - return $"{call} failed."; + return sb.ToString().Trim(); } /// diff --git a/src/Flurl.Http/FlurlRequest.cs b/src/Flurl.Http/FlurlRequest.cs index f8272863..8520bb3c 100644 --- a/src/Flurl.Http/FlurlRequest.cs +++ b/src/Flurl.Http/FlurlRequest.cs @@ -123,9 +123,11 @@ public async Task SendAsync(HttpMethod verb, HttpContent co 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); @@ -141,6 +143,9 @@ public async Task SendAsync(HttpMethod verb, HttpContent co if (ex is OperationCanceledException && !userToken.IsCancellationRequested) throw new FlurlHttpTimeoutException(call, ex); + if (ex is FlurlHttpException) + throw; + throw new FlurlHttpException(call, ex); } finally { diff --git a/src/Flurl.Http/HttpCall.cs b/src/Flurl.Http/HttpCall.cs index 5e0ece20..3176a1f4 100644 --- a/src/Flurl.Http/HttpCall.cs +++ b/src/Flurl.Http/HttpCall.cs @@ -36,16 +36,9 @@ internal static HttpCall Get(HttpRequestMessage request) { 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. @@ -104,7 +97,7 @@ public string RequestBody { /// /// public override string ToString() { - return $"{Request.Method:U} {Request.RequestUri.AbsoluteUri}"; + return $"{Request.Method:U} {FlurlRequest.Url}"; } } } From 7965c12b7f8f7bf96f97c3f45656c3a136a06bf5 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Sat, 30 Sep 2017 10:42:58 -0500 Subject: [PATCH 48/56] #223 assert headers in HttpTest --- Test/Flurl.Test/Http/TestingTests.cs | 18 ++++++++++++++ src/Flurl.Http/Testing/HttpCallAssertion.cs | 26 +++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/Test/Flurl.Test/Http/TestingTests.cs b/Test/Flurl.Test/Http/TestingTests.cs index 7c5a65f0..68aad41e 100644 --- a/Test/Flurl.Test/Http/TestingTests.cs +++ b/Test/Flurl.Test/Http/TestingTests.cs @@ -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(); diff --git a/src/Flurl.Http/Testing/HttpCallAssertion.cs b/src/Flurl.Http/Testing/HttpCallAssertion.cs index 67406db6..46a23b7a 100644 --- a/src/Flurl.Http/Testing/HttpCallAssertion.cs +++ b/src/Flurl.Http/Testing/HttpCallAssertion.cs @@ -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. /// From 49a67ecf4b33e17df16ff520deee6f366249c6f9 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Sat, 30 Sep 2017 12:37:25 -0500 Subject: [PATCH 49/56] #222 ConnectionLeaseTimeout --- Test/Flurl.Test/Http/RealHttpTests.cs | 18 +++++ Test/Flurl.Test/Http/SettingsTests.cs | 34 ++++++++- .../Configuration/FlurlHttpSettings.cs | 10 +++ src/Flurl.Http/FlurlClient.cs | 69 +++++++++++-------- src/Flurl.Http/FlurlRequest.cs | 3 + 5 files changed, 104 insertions(+), 30 deletions(-) diff --git a/Test/Flurl.Test/Http/RealHttpTests.cs b/Test/Flurl.Test/Http/RealHttpTests.cs index 000c26aa..4a035b59 100644 --- a/Test/Flurl.Test/Http/RealHttpTests.cs +++ b/Test/Flurl.Test/Http/RealHttpTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Threading; @@ -243,6 +244,23 @@ public async Task can_get_response_cookies_with_a_delegating_handler() { 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. + } + public class DelegatingHandlerHttpClientFactory : DefaultHttpClientFactory { public override HttpMessageHandler CreateMessageHandler() { diff --git a/Test/Flurl.Test/Http/SettingsTests.cs b/Test/Flurl.Test/Http/SettingsTests.cs index cf1d7c3b..c8154ad1 100644 --- a/Test/Flurl.Test/Http/SettingsTests.cs +++ b/Test/Flurl.Test/Http/SettingsTests.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; -using System.Text; using System.Threading.Tasks; using Flurl.Http; using Flurl.Http.Configuration; @@ -224,6 +223,39 @@ public void can_provide_custom_client_factory() { Assert.IsInstanceOf(client.HttpClient); Assert.IsInstanceOf(client.HttpMessageHandler); } + + [Test] + public async Task connection_lease_timeout_sets_connection_close_header() { + using (var test = new HttpTest()) { + var client = new FlurlClient("http://api.com"); + client.Settings.ConnectionLeaseTimeout = TimeSpan.FromMilliseconds(50); + + await client.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( + client.Request("2").GetAsync(), + client.Request("2").GetAsync(), + client.Request("2").GetAsync(), + client.Request("2").GetAsync(), + client.Request("2").GetAsync(), + client.Request("2").GetAsync(), + client.Request("2").GetAsync(), + client.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 client.Request("3").GetAsync(); + test.ShouldHaveCalled("http://api.com/3").WithoutHeader("Connection"); + } + } } [TestFixture, Parallelizable] diff --git a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs index be70d9d7..e7e1c10e 100644 --- a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs +++ b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs @@ -172,6 +172,16 @@ public class ClientFlurlHttpSettings : FlurlHttpSettings /// 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, diff --git a/src/Flurl.Http/FlurlClient.cs b/src/Flurl.Http/FlurlClient.cs index b76477d2..69d05fa3 100644 --- a/src/Flurl.Http/FlurlClient.cs +++ b/src/Flurl.Http/FlurlClient.cs @@ -41,6 +41,12 @@ public interface IFlurlClient : IHttpSettingsContainer, IDisposable { /// A new IFlurlRequest IFlurlRequest Request(params object[] urlSegments); + /// + /// 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. + /// + bool CheckAndRenewConnectionLease(); + /// /// Gets a value indicating whether this instance (and its underlying HttpClient) has been disposed. /// @@ -63,46 +69,31 @@ 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()); + _httpMessageHandler = new Lazy(() => { + _connectionLeaseStart = DateTime.UtcNow; + return Settings.HttpClientFactory.CreateMessageHandler(); + }); } - /// - /// The base URL associated with this client. - /// + /// public string BaseUrl { get; set; } - /// - /// Gets or sets the FlurlHttpSettings object used by this client. - /// + /// public ClientFlurlHttpSettings Settings { get; set; } - /// - /// Collection of headers sent on all requests using this client. - /// + /// public IDictionary Headers { get; } = new Dictionary(); - /// - /// Collection of HttpCookies sent and received on all requests using this client. - /// + /// public IDictionary Cookies { get; } = new Dictionary(); - /// - /// 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. - /// + /// public HttpClient HttpClient => _httpClient.Value; - /// - /// Gets the HttpMessageHandler to be used in subsequent HTTP calls. Creation (when necessary) is delegated - /// to FlurlHttp.FlurlClientFactory. - /// + /// public HttpMessageHandler HttpMessageHandler => _httpMessageHandler.Value; - /// - /// Instantiates a new IFlurClient, optionally appending path segments to the BaseUrl. - /// - /// 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 + /// public IFlurlRequest Request(params object[] urlSegments) { var parts = new List(urlSegments.Select(s => s.ToInvariantString())); if (!Url.IsValid(parts.FirstOrDefault()) && !string.IsNullOrEmpty(BaseUrl)) @@ -121,9 +112,29 @@ FlurlHttpSettings IHttpSettingsContainer.Settings { set => Settings = value as ClientFlurlHttpSettings; } - /// - /// Gets a value indicating whether this instance (and its underlying HttpClient) has been disposed. - /// + private DateTime? _connectionLeaseStart = null; + private readonly object _connectionLeaseLock = new object(); + + private bool IsConnectionLeaseExpired => + _connectionLeaseStart.HasValue && + Settings.ConnectionLeaseTimeout.HasValue && + DateTime.UtcNow - _connectionLeaseStart > Settings.ConnectionLeaseTimeout; + + /// + public bool CheckAndRenewConnectionLease() { + // do double-check locking to avoid lock overhead most of the time + if (IsConnectionLeaseExpired) { + lock (_connectionLeaseLock) { + if (IsConnectionLeaseExpired) { + _connectionLeaseStart = DateTime.UtcNow; + return true; + } + } + } + return false; + } + + /// public bool IsDisposed { get; private set; } /// diff --git a/src/Flurl.Http/FlurlRequest.cs b/src/Flurl.Http/FlurlRequest.cs index 8520bb3c..5cbbd896 100644 --- a/src/Flurl.Http/FlurlRequest.cs +++ b/src/Flurl.Http/FlurlRequest.cs @@ -121,6 +121,9 @@ public async Task SendAsync(HttpMethod verb, HttpContent co 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; From 1760be0e511466f738f5fcdf049229c709759c35 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Sat, 30 Sep 2017 12:45:57 -0500 Subject: [PATCH 50/56] fixed some variable naming inconsistencies in tests --- Test/Flurl.Test/Http/RealHttpTests.cs | 24 +++++++------- .../Http/SettingsExtensionsTests.cs | 20 ++++++------ Test/Flurl.Test/Http/SettingsTests.cs | 32 +++++++++---------- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/Test/Flurl.Test/Http/RealHttpTests.cs b/Test/Flurl.Test/Http/RealHttpTests.cs index 4a035b59..222b5048 100644 --- a/Test/Flurl.Test/Http/RealHttpTests.cs +++ b/Test/Flurl.Test/Http/RealHttpTests.cs @@ -29,8 +29,8 @@ public async Task can_download_file() { [Test] public async Task can_set_request_cookies() { - var client = new FlurlClient(); - var resp = await client.Request("https://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); @@ -39,33 +39,33 @@ public async Task can_set_request_cookies() { [Test] public async Task can_set_cookies_before_setting_url() { - var client = new FlurlClient().WithCookie("z", "999"); - var resp = await client.Request("https://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 client = new FlurlClient().EnableCookies(); - await client.Request("https://httpbin.org/cookies/set?z=999").HeadAsync(); - Assert.AreEqual("999", client.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 can_persist_cookies() { - var client = new FlurlClient("https://httpbin.org/cookies"); - var req = client.Request().WithCookie("z", 999); + var cli = new FlurlClient("https://httpbin.org/cookies"); + var req = cli.Request().WithCookie("z", 999); // cookie should be set - Assert.AreEqual("999", client.Cookies["z"].Value); + Assert.AreEqual("999", cli.Cookies["z"].Value); Assert.AreEqual("999", req.Cookies["z"].Value); await req.HeadAsync(); // FlurlClient should be re-used, so cookie should stick - Assert.AreEqual("999", client.Cookies["z"].Value); + 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 client.Request().GetJsonAsync(); + var resp = await cli.Request().GetJsonAsync(); Assert.AreEqual("999", resp.cookies.z); } diff --git a/Test/Flurl.Test/Http/SettingsExtensionsTests.cs b/Test/Flurl.Test/Http/SettingsExtensionsTests.cs index 36009bdd..3b3b70c9 100644 --- a/Test/Flurl.Test/Http/SettingsExtensionsTests.cs +++ b/Test/Flurl.Test/Http/SettingsExtensionsTests.cs @@ -130,10 +130,10 @@ public class ClientSettingsExtensionsTests : SettingsExtensionsTests s.AllowedHttpStatusRange = "*"); + 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(client) // client-level settings shouldn't win + .WithClient(cli) // client-level settings shouldn't win .GetAsync()); } } diff --git a/Test/Flurl.Test/Http/SettingsTests.cs b/Test/Flurl.Test/Http/SettingsTests.cs index c8154ad1..1d283b93 100644 --- a/Test/Flurl.Test/Http/SettingsTests.cs +++ b/Test/Flurl.Test/Http/SettingsTests.cs @@ -218,19 +218,19 @@ public class ClientSettingsTests : SettingsTestsBase [Test] public void can_provide_custom_client_factory() { - var client = new FlurlClient(); - client.Settings.HttpClientFactory = new SomeCustomHttpClientFactory(); - Assert.IsInstanceOf(client.HttpClient); - Assert.IsInstanceOf(client.HttpMessageHandler); + 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 client = new FlurlClient("http://api.com"); - client.Settings.ConnectionLeaseTimeout = TimeSpan.FromMilliseconds(50); + var cli = new FlurlClient("http://api.com"); + cli.Settings.ConnectionLeaseTimeout = TimeSpan.FromMilliseconds(50); - await client.Request("1").GetAsync(); + await cli.Request("1").GetAsync(); test.ShouldHaveCalled("http://api.com/1").WithoutHeader("Connection"); // exceed the timeout @@ -238,21 +238,21 @@ public async Task connection_lease_timeout_sets_connection_close_header() { // slam it many times concurrently await Task.WhenAll( - client.Request("2").GetAsync(), - client.Request("2").GetAsync(), - client.Request("2").GetAsync(), - client.Request("2").GetAsync(), - client.Request("2").GetAsync(), - client.Request("2").GetAsync(), - client.Request("2").GetAsync(), - client.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(), + 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 client.Request("3").GetAsync(); + await cli.Request("3").GetAsync(); test.ShouldHaveCalled("http://api.com/3").WithoutHeader("Connection"); } } From 014e5c20fd7b5644177535f2d0df112ea45e673a Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Sat, 30 Sep 2017 15:33:55 -0500 Subject: [PATCH 51/56] upped Flurl.Http to 2.0-pre3 --- src/Flurl.Http/Flurl.Http.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Flurl.Http/Flurl.Http.csproj b/src/Flurl.Http/Flurl.Http.csproj index 5b09f132..cd7151ea 100644 --- a/src/Flurl.Http/Flurl.Http.csproj +++ b/src/Flurl.Http/Flurl.Http.csproj @@ -4,7 +4,7 @@ net45;netstandard1.3;netstandard1.1; True Flurl.Http - 2.0.0-pre2 + 2.0.0-pre3 Todd Menier A fluent, portable, testable HTTP client library that extends Flurl's URL builder chain. http://tmenier.github.io/Flurl From 22ab91b43a1d3456feba6ba558a08733c50cbea0 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Tue, 3 Oct 2017 16:43:10 -0500 Subject: [PATCH 52/56] #229 HttpTest.Settings should always take precedence --- Test/Flurl.Test/Http/RealHttpTests.cs | 29 +++++++++++++++++++ Test/Flurl.Test/Http/TestingTests.cs | 2 +- .../Configuration/FlurlHttpSettings.cs | 24 +++++++++++++-- src/Flurl.Http/FlurlClient.cs | 17 +++++------ src/Flurl.Http/FlurlHttp.cs | 3 +- src/Flurl.Http/Testing/HttpTest.cs | 23 +++++++++------ 6 files changed, 74 insertions(+), 24 deletions(-) diff --git a/Test/Flurl.Test/Http/RealHttpTests.cs b/Test/Flurl.Test/Http/RealHttpTests.cs index 222b5048..fbb1401a 100644 --- a/Test/Flurl.Test/Http/RealHttpTests.cs +++ b/Test/Flurl.Test/Http/RealHttpTests.cs @@ -261,6 +261,35 @@ public async Task connection_lease_timeout_doesnt_disrupt_calls() { 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() { diff --git a/Test/Flurl.Test/Http/TestingTests.cs b/Test/Flurl.Test/Http/TestingTests.cs index 68aad41e..48e25d90 100644 --- a/Test/Flurl.Test/Http/TestingTests.cs +++ b/Test/Flurl.Test/Http/TestingTests.cs @@ -179,7 +179,7 @@ public async Task can_fake_cookies() { var rec = "http://www.api.com".EnableCookies(); await rec.GetAsync(); - Assert.AreEqual(1, rec.Cookies.Count()); + Assert.AreEqual(1, rec.Cookies.Count); Assert.AreEqual("foo", rec.Cookies["c1"].Value); } diff --git a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs index e7e1c10e..ea4381b6 100644 --- a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs +++ b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs @@ -3,6 +3,7 @@ using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; +using Flurl.Http.Testing; namespace Flurl.Http.Configuration { @@ -137,7 +138,9 @@ public virtual void ResetDefaults() { /// 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); @@ -206,7 +209,10 @@ internal GlobalFlurlHttpSettings() : base(null) { /// Gets or sets the factory that defines creating, caching, and reusing FlurlClient instances and, /// by proxy, HttpClient instances. /// - public IFlurlClientFactory FlurlClientFactory { get; set; } + public IFlurlClientFactory FlurlClientFactory { + get => Get(() => FlurlClientFactory); + set => Set(() => FlurlClientFactory, value); + } /// /// Resets all global settings to their Flurl.Http-defined default values. @@ -214,11 +220,25 @@ internal GlobalFlurlHttpSettings() : base(null) { public override void ResetDefaults() { base.ResetDefaults(); Timeout = TimeSpan.FromSeconds(100); // same as HttpClient - CookiesEnabled = false; JsonSerializer = new NewtonsoftJsonSerializer(null); UrlEncodedSerializer = new DefaultUrlEncodedSerializer(); FlurlClientFactory = new PerHostFlurlClientFactory(); HttpClientFactory = new DefaultHttpClientFactory(); } } + + /// + /// Settings overrides within the context of an HttpTest + /// + public class TestFlurlHttpSettings : GlobalFlurlHttpSettings + { + /// + /// Resets all test settings to their Flurl.Http-defined default values. + /// + public override void ResetDefaults() { + base.ResetDefaults(); + FlurlClientFactory = new TestFlurlClientFactory(); + HttpClientFactory = new TestHttpClientFactory(); + } + } } diff --git a/src/Flurl.Http/FlurlClient.cs b/src/Flurl.Http/FlurlClient.cs index 69d05fa3..94565bfd 100644 --- a/src/Flurl.Http/FlurlClient.cs +++ b/src/Flurl.Http/FlurlClient.cs @@ -4,6 +4,7 @@ using System.Net.Http; using System.Linq; using Flurl.Http.Configuration; +using Flurl.Http.Testing; using Flurl.Util; namespace Flurl.Http @@ -69,10 +70,7 @@ public FlurlClient(string baseUrl = null) { BaseUrl = baseUrl; Settings = new ClientFlurlHttpSettings(FlurlHttp.GlobalSettings); _httpClient = new Lazy(() => Settings.HttpClientFactory.CreateHttpClient(HttpMessageHandler)); - _httpMessageHandler = new Lazy(() => { - _connectionLeaseStart = DateTime.UtcNow; - return Settings.HttpClientFactory.CreateMessageHandler(); - }); + _httpMessageHandler = new Lazy(() => Settings.HttpClientFactory.CreateMessageHandler()); } /// @@ -88,10 +86,10 @@ public FlurlClient(string baseUrl = null) { public IDictionary Cookies { get; } = new Dictionary(); /// - public HttpClient HttpClient => _httpClient.Value; + public HttpClient HttpClient => HttpTest.Current?.HttpClient ?? _httpClient.Value; /// - public HttpMessageHandler HttpMessageHandler => _httpMessageHandler.Value; + public HttpMessageHandler HttpMessageHandler => HttpTest.Current?.HttpMessageHandler ?? _httpMessageHandler.Value; /// public IFlurlRequest Request(params object[] urlSegments) { @@ -112,13 +110,12 @@ FlurlHttpSettings IHttpSettingsContainer.Settings { set => Settings = value as ClientFlurlHttpSettings; } - private DateTime? _connectionLeaseStart = null; + private Lazy _connectionLeaseStart = new Lazy(() => DateTime.UtcNow); private readonly object _connectionLeaseLock = new object(); private bool IsConnectionLeaseExpired => - _connectionLeaseStart.HasValue && Settings.ConnectionLeaseTimeout.HasValue && - DateTime.UtcNow - _connectionLeaseStart > Settings.ConnectionLeaseTimeout; + DateTime.UtcNow - _connectionLeaseStart.Value > Settings.ConnectionLeaseTimeout; /// public bool CheckAndRenewConnectionLease() { @@ -126,7 +123,7 @@ public bool CheckAndRenewConnectionLease() { if (IsConnectionLeaseExpired) { lock (_connectionLeaseLock) { if (IsConnectionLeaseExpired) { - _connectionLeaseStart = DateTime.UtcNow; + _connectionLeaseStart = new Lazy(() => DateTime.UtcNow); return true; } } diff --git a/src/Flurl.Http/FlurlHttp.cs b/src/Flurl.Http/FlurlHttp.cs index 99024232..9386fba5 100644 --- a/src/Flurl.Http/FlurlHttp.cs +++ b/src/Flurl.Http/FlurlHttp.cs @@ -1,6 +1,5 @@ using System; using Flurl.Http.Configuration; -using Flurl.Http.Testing; namespace Flurl.Http { @@ -17,7 +16,7 @@ public static class FlurlHttp /// /// Globally configured Flurl.Http settings. Should normally be written to by calling FlurlHttp.Configure once application at startup. /// - public static GlobalFlurlHttpSettings GlobalSettings => HttpTest.Current?.Settings ?? _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. diff --git a/src/Flurl.Http/Testing/HttpTest.cs b/src/Flurl.Http/Testing/HttpTest.cs index c52c0264..da8adc95 100644 --- a/src/Flurl.Http/Testing/HttpTest.cs +++ b/src/Flurl.Http/Testing/HttpTest.cs @@ -15,20 +15,25 @@ namespace Flurl.Http.Testing /// public class HttpTest : IDisposable { - /// - /// Initializes a new instance of the class. - /// - /// A delegate callback throws an exception. - public HttpTest() { - Settings = new GlobalFlurlHttpSettings { - HttpClientFactory = new TestHttpClientFactory(), - FlurlClientFactory = new TestFlurlClientFactory() - }; + private readonly Lazy _httpClient; + private readonly Lazy _httpMessageHandler; + + /// + /// Initializes a new instance of the class. + /// + /// 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. /// From 06e9129094be7fb3dc7394781c9891ee526ebdb6 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Tue, 3 Oct 2017 16:45:10 -0500 Subject: [PATCH 53/56] Flurl.Http 2.0-pre4 --- src/Flurl.Http/Flurl.Http.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Flurl.Http/Flurl.Http.csproj b/src/Flurl.Http/Flurl.Http.csproj index cd7151ea..2439a02d 100644 --- a/src/Flurl.Http/Flurl.Http.csproj +++ b/src/Flurl.Http/Flurl.Http.csproj @@ -4,7 +4,7 @@ net45;netstandard1.3;netstandard1.1; True Flurl.Http - 2.0.0-pre3 + 2.0.0-pre4 Todd Menier A fluent, portable, testable HTTP client library that extends Flurl's URL builder chain. http://tmenier.github.io/Flurl From 232bf5d31b3b44138dd94ed9df76f494e4822905 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Tue, 3 Oct 2017 16:51:48 -0500 Subject: [PATCH 54/56] Not so subtle breaking changes warning for 2.0 --- src/Flurl.Http/Flurl.Http.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Flurl.Http/Flurl.Http.csproj b/src/Flurl.Http/Flurl.Http.csproj index 2439a02d..6b95d518 100644 --- a/src/Flurl.Http/Flurl.Http.csproj +++ b/src/Flurl.Http/Flurl.Http.csproj @@ -6,7 +6,7 @@ Flurl.Http 2.0.0-pre4 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 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 From f52cbe808a7fdab6a3ea59b6e755e2b0d1d13df7 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Tue, 3 Oct 2017 16:52:49 -0500 Subject: [PATCH 55/56] grammar fail --- src/Flurl.Http/Flurl.Http.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Flurl.Http/Flurl.Http.csproj b/src/Flurl.Http/Flurl.Http.csproj index 6b95d518..f7f8f4a2 100644 --- a/src/Flurl.Http/Flurl.Http.csproj +++ b/src/Flurl.Http/Flurl.Http.csproj @@ -6,7 +6,7 @@ Flurl.Http 2.0.0-pre4 Todd Menier - WARNING: 2.0 CONTAINS BREAKING CHANGES - REVIEW RELEASE NOTES CAREFULLY! Flurl.Http 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 From c4eb214f3a1eb987cf7ed81ebbc27f4dd38343fb Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Fri, 6 Oct 2017 07:26:41 -0500 Subject: [PATCH 56/56] Flurl.Http 2.0 official release --- src/Flurl.Http/Flurl.Http.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Flurl.Http/Flurl.Http.csproj b/src/Flurl.Http/Flurl.Http.csproj index f7f8f4a2..35d4b625 100644 --- a/src/Flurl.Http/Flurl.Http.csproj +++ b/src/Flurl.Http/Flurl.Http.csproj @@ -4,7 +4,7 @@ net45;netstandard1.3;netstandard1.1; True Flurl.Http - 2.0.0-pre4 + 2.0.0 Todd Menier 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