├── Build ├── test.cmd ├── test.sh ├── build.cmd ├── build.sh └── Flurl.netstandard.sln ├── src ├── Flurl.Http │ ├── Testing │ │ ├── TimeoutResponseMessage.cs │ │ ├── TestHttpClientFactory.cs │ │ ├── FakeHttpMessageHandler.cs │ │ ├── HttpCallAssertException.cs │ │ └── HttpTest.cs │ ├── Configuration │ │ ├── IFlurlClientFactory.cs │ │ ├── ISerializer.cs │ │ ├── PerHostFlurlClientFactory.cs │ │ ├── IHttpClientFactory.cs │ │ ├── PerBaseUrlFlurlClientFactory.cs │ │ ├── DefaultHttpClientFactory.cs │ │ ├── DefaultUrlEncodedSerializer.cs │ │ ├── NewtonsoftJsonSerializer.cs │ │ ├── FlurlClientFactoryBase.cs │ │ └── FlurlHttpSettings.cs │ ├── Content │ │ ├── CapturedJsonContent.cs │ │ ├── CapturedUrlEncodedContent.cs │ │ ├── CapturedStringContent.cs │ │ ├── FileContent.cs │ │ └── CapturedMultipartContent.cs │ ├── HttpRequestMessageExtensions.cs │ ├── IHttpSettingsContainer.cs │ ├── FlurlHttp.cs │ ├── HttpStatusRangeParser.cs │ ├── FileUtil.cs │ ├── MultipartExtensions.cs │ ├── Flurl.Http.csproj │ ├── HttpCall.cs │ ├── CookieExtensions.cs │ ├── DownloadExtensions.cs │ ├── HeaderExtensions.cs │ ├── FlurlHttpException.cs │ ├── HttpResponseMessageExtensions.cs │ ├── SettingsExtensions.cs │ ├── FlurlClient.cs │ └── UrlBuilderExtensions.cs ├── Flurl.Http.CodeGen │ ├── Flurl.Http.CodeGen.csproj │ ├── CodeWriter.cs │ ├── HttpExtensionMethod.cs │ ├── UrlExtensionMethod.cs │ └── Program.cs └── Flurl │ ├── NullValueHandling.cs │ ├── Flurl.csproj │ ├── QueryParameter.cs │ ├── QueryParamCollection.cs │ ├── Util │ └── CommonExtensions.cs │ └── StringExtensions.cs ├── .gitignore ├── PackageTesters ├── PackageTester.NET45 │ ├── App.config │ ├── Program.cs │ ├── packages.config │ └── PackageTester.NET45.csproj ├── PackageTester.NET461 │ ├── App.config │ ├── Program.cs │ ├── packages.config │ └── PackageTester.NET461.csproj ├── PackageTester.NETCore │ ├── Program.cs │ └── PackageTester.NETCore.csproj └── PackageTester.Shared │ ├── PackageTester.Shared.projitems │ ├── PackageTester.Shared.shproj │ └── Tester.cs ├── .gitattributes ├── appveyor.yml ├── Test └── Flurl.Test │ ├── Http │ ├── HttpTestFixtureBase.cs │ ├── DefaultUrlEncodedSerializerTests.cs │ ├── FlurlHttpExceptionTests.cs │ ├── HttpStatusRangeParserTests.cs │ ├── MultipartTests.cs │ ├── PostTests.cs │ ├── HttpMethodTests.cs │ ├── FlurlClientTests.cs │ ├── GetTests.cs │ └── SettingsExtensionsTests.cs │ ├── Flurl.Test.csproj │ ├── ReflectionHelper.cs │ └── CommonExtensionsTests.cs ├── NuGet.Config ├── LICENSE ├── README.md └── Flurl.sln /Build/test.cmd: -------------------------------------------------------------------------------- 1 | @cd ..\test\Flurl.Test\ 2 | @call dotnet test -c Release 3 | @cd ..\..\Build\ -------------------------------------------------------------------------------- /Build/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | SCRIPT_ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 5 | 6 | dotnet test -c Release "${SCRIPT_ROOT}/../Test/Flurl.Test/" -------------------------------------------------------------------------------- /src/Flurl.Http/Testing/TimeoutResponseMessage.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | 3 | namespace Flurl.Http.Testing 4 | { 5 | internal class TimeoutResponseMessage : HttpResponseMessage 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | packages 4 | *.suo 5 | *.nupkg 6 | *.DotSettings.user 7 | *.xproj.user 8 | .vs 9 | *.lock.json 10 | *.log 11 | .vscode 12 | publish 13 | TestResult.xml 14 | *.bak 15 | .idea 16 | -------------------------------------------------------------------------------- /PackageTesters/PackageTester.NET45/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/Flurl.Http.CodeGen/Flurl.Http.CodeGen.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.0 6 | 7 | 8 | -------------------------------------------------------------------------------- /PackageTesters/PackageTester.NET461/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto -------------------------------------------------------------------------------- /PackageTesters/PackageTester.NETCore/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PackageTester 4 | { 5 | public class Program 6 | { 7 | public static void Main(string[] args) { 8 | new Tester().DoTestsAsync().Wait(); 9 | Console.ReadLine(); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /PackageTesters/PackageTester.NET45/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PackageTester.NET45 4 | { 5 | public class Program 6 | { 7 | public static void Main(string[] args) { 8 | new Tester().DoTestsAsync().Wait(); 9 | Console.ReadLine(); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /PackageTesters/PackageTester.NET461/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PackageTester.NET461 4 | { 5 | public class Program 6 | { 7 | public static void Main(string[] args) { 8 | new Tester().DoTestsAsync().Wait(); 9 | Console.ReadLine(); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /PackageTesters/PackageTester.NET45/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /PackageTesters/PackageTester.NET461/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.0.0-{branch}-{build} 2 | 3 | image: Visual Studio 2017 4 | 5 | init: 6 | - dotnet --version 7 | 8 | pull_requests: 9 | do_not_increment_build_number: true 10 | 11 | build_script: 12 | - cmd: call cmd /C "cd .\build & build.cmd" 13 | 14 | test_script: 15 | - cmd: call cmd /C "cd .\build & test.cmd" 16 | 17 | artifacts: 18 | - path: '**\*.nupkg' -------------------------------------------------------------------------------- /PackageTesters/PackageTester.NETCore/PackageTester.NETCore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Test/Flurl.Test/Http/HttpTestFixtureBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Flurl.Http.Testing; 5 | using NUnit.Framework; 6 | 7 | namespace Flurl.Test.Http 8 | { 9 | public abstract class HttpTestFixtureBase 10 | { 11 | protected HttpTest HttpTest { get; private set; } 12 | 13 | [SetUp] 14 | public void CreateHttpTest() { 15 | HttpTest = new HttpTest(); 16 | } 17 | 18 | [TearDown] 19 | public void DisposeHttpTest() { 20 | HttpTest.Dispose(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Flurl.Http/Configuration/IFlurlClientFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Flurl.Http.Configuration 4 | { 5 | /// 6 | /// Interface for defining a strategy for creating, caching, and reusing IFlurlClient instances and, 7 | /// by proxy, their underlying HttpClient instances. 8 | /// 9 | public interface IFlurlClientFactory : IDisposable 10 | { 11 | /// 12 | /// Strategy to create a FlurlClient or reuse an exisitng one, based on URL being called. 13 | /// 14 | /// The URL being called. 15 | /// 16 | IFlurlClient Get(Url url); 17 | } 18 | } -------------------------------------------------------------------------------- /src/Flurl.Http/Testing/TestHttpClientFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using Flurl.Http.Configuration; 4 | 5 | namespace Flurl.Http.Testing 6 | { 7 | /// 8 | /// IHttpClientFactory implementation used to fake and record calls in tests. 9 | /// 10 | public class TestHttpClientFactory : DefaultHttpClientFactory 11 | { 12 | /// 13 | /// Creates an instance of FakeHttpMessageHander, which prevents actual HTTP calls from being made. 14 | /// 15 | /// 16 | public override HttpMessageHandler CreateMessageHandler() { 17 | return new FakeHttpMessageHandler(); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/Flurl/NullValueHandling.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Flurl 6 | { 7 | /// 8 | /// Describes how to handle null values in query parameters. 9 | /// 10 | public enum NullValueHandling 11 | { 12 | /// 13 | /// Set as name without value in query string. 14 | /// 15 | NameOnly, 16 | /// 17 | /// Don't add to query string, remove any existing value. 18 | /// 19 | Remove, 20 | /// 21 | /// Don't add to query string, but leave any existing value unchanged. 22 | /// 23 | Ignore 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Flurl.Http/Content/CapturedJsonContent.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Flurl.Http.Content 4 | { 5 | /// 6 | /// Provides HTTP content based on a serialized JSON object, with the JSON string captured to a property 7 | /// so it can be read without affecting the read-once content stream. 8 | /// 9 | public class CapturedJsonContent : CapturedStringContent 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// The json. 15 | public CapturedJsonContent(string json) : base(json, Encoding.UTF8, "application/json") { } 16 | } 17 | } -------------------------------------------------------------------------------- /PackageTesters/PackageTester.Shared/PackageTester.Shared.projitems: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 5 | true 6 | d4717aa7-5549-4bad-81c5-406844a12990 7 | 8 | 9 | PackageTester.Shared 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Test/Flurl.Test/Http/DefaultUrlEncodedSerializerTests.cs: -------------------------------------------------------------------------------- 1 | using Flurl.Http.Configuration; 2 | using NUnit.Framework; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Flurl.Test.Http 10 | { 11 | [TestFixture, Parallelizable] 12 | public class DefaultUrlEncodedSerializerTests 13 | { 14 | [Test] 15 | public void can_serialize_object() { 16 | var vals = new { 17 | a = "foo", 18 | b = 333, 19 | c = (string)null, // exlude 20 | d = "" 21 | }; 22 | 23 | var serialized = new DefaultUrlEncodedSerializer().Serialize(vals); 24 | Assert.AreEqual("a=foo&b=333&d=", serialized); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Flurl.Http/Configuration/ISerializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | 6 | namespace Flurl.Http.Configuration 7 | { 8 | /// 9 | /// Contract for serializing and deserializing objects. 10 | /// 11 | public interface ISerializer 12 | { 13 | /// 14 | /// Serializes an object to a string representation. 15 | /// 16 | string Serialize(object obj); 17 | /// 18 | /// Deserializes an object from a string representation. 19 | /// 20 | T Deserialize(string s); 21 | /// 22 | /// Deserializes an object from a stream representation. 23 | /// 24 | T Deserialize(Stream stream); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Build/build.cmd: -------------------------------------------------------------------------------- 1 | @call dotnet --info 2 | 3 | @call dotnet restore -v m ../ 4 | 5 | @if ERRORLEVEL 1 ( 6 | echo Error! Restoring dependicies failed. 7 | exit /b 1 8 | ) else ( 9 | echo Restoring dependicies was successful. 10 | ) 11 | 12 | @set project=..\src\Flurl.Http.CodeGen\Flurl.Http.CodeGen.csproj 13 | 14 | @call dotnet run -c Release -p %project% ..\src\Flurl.Http\GeneratedExtensions.cs 15 | @if ERRORLEVEL 1 ( 16 | echo Error! Generation cs file failed. 17 | exit /b 1 18 | ) 19 | 20 | @set project=..\src\Flurl\ 21 | 22 | @call dotnet build -c Release %project% 23 | 24 | @if ERRORLEVEL 1 ( 25 | echo Error! Build Flurl failed. 26 | exit /b 1 27 | ) 28 | 29 | @set project=..\src\Flurl.Http\ 30 | 31 | @call dotnet build -c Release %project% 32 | 33 | @if ERRORLEVEL 1 ( 34 | echo Error! Build Flurl.Http failed. 35 | exit /b 1 36 | ) -------------------------------------------------------------------------------- /src/Flurl.Http/Content/CapturedUrlEncodedContent.cs: -------------------------------------------------------------------------------- 1 | namespace Flurl.Http.Content 2 | { 3 | /// 4 | /// Provides HTTP content based on an object serialized to URL-encoded name-value pairs. 5 | /// Useful in simulating an HTML form POST. Serialized content is captured to Content property 6 | /// so it can be read without affecting the read-once content stream. 7 | /// 8 | public class CapturedUrlEncodedContent : CapturedStringContent 9 | { 10 | /// 11 | /// Initializes a new instance of the class. 12 | /// 13 | /// Content represented as a (typically anonymous) object, which will be parsed into name/value pairs. 14 | public CapturedUrlEncodedContent(string data) : base(data, null, "application/x-www-form-urlencoded") { } 15 | } 16 | } -------------------------------------------------------------------------------- /Build/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | SCRIPT_ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 5 | 6 | NETSTANDARD_SLN="${SCRIPT_ROOT}/Flurl.netstandard.sln" 7 | 8 | command -v dotnet >/dev/null 2>&1 || { 9 | echo >&2 "This script requires the dotnet core sdk tooling to be installed" 10 | exit 1 11 | } 12 | 13 | echo "!!WARNING!! This script only builds netstandard and netcoreapp targets" 14 | echo "!!WARNING!! Do not publish nupkgs generated from this script" 15 | 16 | dotnet --info 17 | 18 | dotnet restore -v m "${NETSTANDARD_SLN}" 19 | 20 | dotnet run -c Release -p "${SCRIPT_ROOT}/../src/Flurl.Http.CodeGen/Flurl.Http.CodeGen.csproj" "${SCRIPT_ROOT}/../src/Flurl.Http/GeneratedExtensions.cs" 21 | 22 | dotnet build -c Release "${SCRIPT_ROOT}/../src/Flurl/" 23 | dotnet build -c Release "${SCRIPT_ROOT}/../src/Flurl.Http/" 24 | -------------------------------------------------------------------------------- /Test/Flurl.Test/Flurl.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net45;netcoreapp2.0; 5 | netcoreapp2.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Flurl.Http/HttpRequestMessageExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Flurl.Http 9 | { 10 | internal static class HttpRequestMessageExtensions 11 | { 12 | /// 13 | /// Associate an HttpCall object with this request 14 | /// 15 | internal static void SetHttpCall(this HttpRequestMessage request, HttpCall call) { 16 | if (request?.Properties != null) 17 | request.Properties["FlurlHttpCall"] = call; 18 | } 19 | 20 | /// 21 | /// Get the HttpCall assocaited with this request, if any. 22 | /// 23 | internal static HttpCall GetHttpCall(this HttpRequestMessage request) { 24 | if (request?.Properties != null && request.Properties.TryGetValue("FlurlHttpCall", out var obj) && obj is HttpCall) 25 | return (HttpCall)obj; 26 | return null; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Flurl.Http/IHttpSettingsContainer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Text; 6 | using Flurl.Http.Configuration; 7 | using Flurl.Util; 8 | 9 | namespace Flurl.Http 10 | { 11 | /// 12 | /// Defines stateful aspects (headers, cookies, etc) common to both IFlurlClient and IFlurlRequest 13 | /// 14 | public interface IHttpSettingsContainer 15 | { 16 | /// 17 | /// Gets or sets the FlurlHttpSettings object used by this client. 18 | /// 19 | FlurlHttpSettings Settings { get; set; } 20 | 21 | /// 22 | /// Collection of headers sent on all requests using this client. 23 | /// 24 | IDictionary Headers { get; } 25 | 26 | /// 27 | /// Collection of HttpCookies sent and received with all requests using this client. 28 | /// 29 | IDictionary Cookies { get; } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Flurl.Http/Configuration/PerHostFlurlClientFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Flurl.Http.Configuration 8 | { 9 | /// 10 | /// An IFlurlClientFactory implementation that caches and reuses the same one instance of 11 | /// FlurlClient per host being called. Maximizes reuse of underlying HttpClient/Handler 12 | /// while allowing things like cookies to be host-specific. This is the default 13 | /// implementation used when calls are made fluently off Urls/strings. 14 | /// 15 | public class PerHostFlurlClientFactory : FlurlClientFactoryBase 16 | { 17 | /// 18 | /// Returns the host part of the URL (i.e. www.api.com) so that all calls to the same 19 | /// host use the same FlurlClient (and HttpClient/HttpMessageHandler) instance. 20 | /// 21 | /// The URL. 22 | /// The cache key 23 | protected override string GetCacheKey(Url url) => new Uri(url).Host; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Flurl.Http/Content/CapturedStringContent.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Text; 3 | 4 | namespace Flurl.Http.Content 5 | { 6 | /// 7 | /// Provides HTTP content based on a string, with the string itself captured to a property 8 | /// so it can be read without affecting the read-once content stream. 9 | /// 10 | public class CapturedStringContent : StringContent 11 | { 12 | /// 13 | /// The content body captured as a string. Can be read multiple times (unlike the content stream). 14 | /// 15 | public string Content { get; } 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// The content. 21 | /// The encoding. 22 | /// Type of the media. 23 | public CapturedStringContent(string content, Encoding encoding = null, string mediaType = null) : 24 | base(content, encoding, mediaType) 25 | { 26 | Content = content; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /PackageTesters/PackageTester.Shared/PackageTester.Shared.shproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | d4717aa7-5549-4bad-81c5-406844a12990 5 | 14.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Todd Menier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Flurl.Http/Testing/FakeHttpMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Flurl.Http.Testing 7 | { 8 | /// 9 | /// An HTTP message handler that prevents actual HTTP calls from being made and instead returns 10 | /// responses from a provided response factory. 11 | /// 12 | public class FakeHttpMessageHandler : HttpMessageHandler 13 | { 14 | /// 15 | /// Sends the request asynchronous. 16 | /// 17 | /// The request. 18 | /// The cancellation token. 19 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { 20 | HttpTest.Current?.CallLog.Add(request.GetHttpCall()); 21 | var tcs = new TaskCompletionSource(); 22 | var resp = HttpTest.Current?.GetNextResponse() ?? new HttpResponseMessage(); 23 | if (resp is TimeoutResponseMessage) 24 | tcs.SetCanceled(); 25 | else 26 | tcs.SetResult(resp); 27 | return tcs.Task; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/Flurl.Http/Configuration/IHttpClientFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | 3 | namespace Flurl.Http.Configuration 4 | { 5 | /// 6 | /// Interface defining creation of HttpClient and HttpMessageHandler used in all Flurl HTTP calls. 7 | /// Implementation can be added via FlurlHttp.Configure. However, in order not to lose much of 8 | /// Flurl.Http's functionality, it's almost always best to inherit DefaultHttpClientFactory and 9 | /// extend the base implementations, rather than implementing this interface directly. 10 | /// 11 | public interface IHttpClientFactory 12 | { 13 | /// 14 | /// Defines how HttpClient should be instantiated and configured by default. Do NOT attempt 15 | /// to cache/reuse HttpClient instances here - that should be done at the FlurlClient level 16 | /// via a custom FlurlClientFactory that gets registered globally. 17 | /// 18 | /// The HttpMessageHandler used to construct the HttpClient. 19 | /// 20 | HttpClient CreateHttpClient(HttpMessageHandler handler); 21 | 22 | /// 23 | /// Defines how the 24 | /// 25 | /// 26 | HttpMessageHandler CreateMessageHandler(); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Flurl.Http/Configuration/PerBaseUrlFlurlClientFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Flurl.Http.Configuration 8 | { 9 | /// 10 | /// An IFlurlClientFactory implementation that caches and reuses the same IFlurlClient instance 11 | /// per URL requested, which it assumes is a "base" URL, and sets the IFlurlClient.BaseUrl property 12 | /// to that value. Ideal for use with IoC containers - register as a singleton, inject into a service 13 | /// that wraps some web service, and use to set a private IFlurlClient field in the constructor. 14 | /// 15 | public class PerBaseUrlFlurlClientFactory : FlurlClientFactoryBase 16 | { 17 | /// 18 | /// Returns the entire URL, which is assumed to be some "base" URL for a service. 19 | /// 20 | /// The URL. 21 | /// The cache key 22 | protected override string GetCacheKey(Url url) => url.ToString(); 23 | 24 | /// 25 | /// Returns a new new FlurlClient with BaseUrl set to the URL passed. 26 | /// 27 | /// The URL 28 | /// 29 | protected override IFlurlClient Create(Url url) => new FlurlClient(url); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Flurl.Http/Testing/HttpCallAssertException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Flurl.Http.Testing 6 | { 7 | /// 8 | /// An exception thrown by HttpTest's assertion methods to indicate that the assertion failed. 9 | /// 10 | public class HttpCallAssertException : Exception 11 | { 12 | /// 13 | /// Initializes a new instance of the class. 14 | /// 15 | /// The expected call conditions. 16 | /// The expected number of calls. 17 | /// The actual number calls. 18 | public HttpCallAssertException(IList conditions, int? expectedCalls, int actualCalls) : base(BuildMessage(conditions, expectedCalls, actualCalls)) { } 19 | 20 | private static string BuildMessage(IList conditions, int? expectedCalls, int actualCalls) { 21 | var expected = 22 | (expectedCalls == null) ? "any calls to be made" : 23 | (expectedCalls == 0) ? "no calls to be made" : 24 | (expectedCalls == 1) ? "1 call to be made" : 25 | expectedCalls + " calls to be made"; 26 | var actual = 27 | (actualCalls == 0) ? "no matching calls were made" : 28 | (actualCalls == 1) ? "1 matching call was made" : 29 | actualCalls + " matching calls were made"; 30 | if (conditions.Any()) 31 | expected += " with " + string.Join(" and ", conditions); 32 | else 33 | actual = actual.Replace(" matching", ""); 34 | return $"Expected {expected}, but {actual}."; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/Flurl.Http/Configuration/DefaultHttpClientFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | 5 | namespace Flurl.Http.Configuration 6 | { 7 | /// 8 | /// Default implementation of IHttpClientFactory used by FlurlHttp. The created HttpClient includes hooks 9 | /// that enable FlurlHttp's testing features and respect its configuration settings. Therefore, custom factories 10 | /// should inherit from this class, rather than implementing IHttpClientFactory directly. 11 | /// 12 | public class DefaultHttpClientFactory : IHttpClientFactory 13 | { 14 | /// 15 | /// Override in custom factory to customize the creation of HttpClient used in all Flurl HTTP calls. 16 | /// In order not to lose Flurl.Http functionality, it is recommended to call base.CreateClient and 17 | /// customize the result. 18 | /// 19 | public virtual HttpClient CreateHttpClient(HttpMessageHandler handler) { 20 | return new HttpClient(handler) { 21 | // Timeouts handled per request via FlurlHttpSettings.Timeout 22 | Timeout = System.Threading.Timeout.InfiniteTimeSpan 23 | }; 24 | } 25 | 26 | /// 27 | /// Override in custom factory to customize the creation of HttpClientHandler used in all Flurl HTTP calls. 28 | /// In order not to lose Flurl.Http functionality, it is recommended to call base.CreateMessageHandler and 29 | /// customize the result. 30 | /// 31 | public virtual HttpMessageHandler CreateMessageHandler() { 32 | return new HttpClientHandler { 33 | // #266 34 | AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate 35 | }; 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/Flurl/Flurl.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net40;netstandard1.0;netstandard1.3;netstandard2.0; 5 | netstandard1.0;netstandard1.3;netstandard2.0; 6 | True 7 | Flurl 8 | 2.7.1 9 | Todd Menier 10 | A fluent, portable URL builder. To make HTTP calls off the fluent chain, check out Flurl.Http. 11 | http://tmenier.github.io/Flurl 12 | https://pbs.twimg.com/profile_images/534024476296376320/IuPGZ_bX_400x400.png 13 | https://raw.githubusercontent.com/tmenier/Flurl/master/LICENSE 14 | https://github.com/tmenier/Flurl.git 15 | git 16 | fluent url uri querystring builder 17 | https://github.com/tmenier/Flurl/releases 18 | false 19 | 20 | 21 | 22 | True 23 | 24 | 25 | 26 | bin\Release\Flurl.xml 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Flurl.Http/Content/FileContent.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | 6 | namespace Flurl.Http.Content 7 | { 8 | /// 9 | /// Represents HTTP content based on a local file. Typically used with PostMultipartAsync for uploading files. 10 | /// 11 | public class FileContent : HttpContent 12 | { 13 | /// 14 | /// The local file path. 15 | /// 16 | public string Path { get; } 17 | 18 | private readonly int _bufferSize; 19 | 20 | /// 21 | /// Initializes a new instance of the class. 22 | /// 23 | /// The local file path. 24 | /// The buffer size of the stream upload in bytes. Defaults to 4096. 25 | public FileContent(string path, int bufferSize = 4096) { 26 | Path = path; 27 | _bufferSize = bufferSize; 28 | } 29 | 30 | /// 31 | /// Serializes to stream asynchronous. 32 | /// 33 | /// The stream. 34 | /// The context. 35 | /// 36 | protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) { 37 | using (var source = await FileUtil.OpenReadAsync(Path, _bufferSize).ConfigureAwait(false)) { 38 | await source.CopyToAsync(stream, _bufferSize).ConfigureAwait(false); 39 | } 40 | } 41 | 42 | /// 43 | /// Tries the length of the compute. 44 | /// 45 | /// The length. 46 | /// 47 | protected override bool TryComputeLength(out long length) { 48 | length = -1; 49 | return false; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flurl 2 | 3 | [![Build status](https://ci.appveyor.com/api/projects/status/hec8ioqg0j07ttg5/branch/master?svg=true)](https://ci.appveyor.com/project/kroniak/flurl/branch/master) 4 | [![Flurl](https://img.shields.io/nuget/v/Flurl.svg?maxAge=3600)](https://www.nuget.org/packages/Flurl/) 5 | [![Flurl.Http](https://img.shields.io/nuget/v/Flurl.Http.svg?maxAge=3600)](https://www.nuget.org/packages/Flurl.Http/) 6 | 7 | Flurl is a modern, fluent, asynchronous, testable, portable, buzzword-laden URL builder and HTTP client library. 8 | 9 | ````c# 10 | var result = await "https://api.mysite.com" 11 | .AppendPathSegment("person") 12 | .SetQueryParams(new { api_key = "xyz" }) 13 | .WithOAuthBearerToken("my_oauth_token") 14 | .PostJsonAsync(new { first_name = firstName, last_name = lastName }) 15 | .ReceiveJson(); 16 | 17 | [Test] 18 | public void Can_Create_Person() { 19 | // fake & record all http calls in the test subject 20 | using (var httpTest = new HttpTest()) { 21 | // arrange 22 | httpTest.RespondWith(200, "OK"); 23 | 24 | // act 25 | await sut.CreatePersonAsync("Frank", "Underwood"); 26 | 27 | // assert 28 | httpTest.ShouldHaveCalled("http://api.mysite.com/*") 29 | .WithVerb(HttpMethod.Post) 30 | .WithContentType("application/json"); 31 | } 32 | } 33 | ```` 34 | 35 | Get it on NuGet: 36 | 37 | `PM> Install-Package Flurl.Http` 38 | 39 | Or get just the stand-alone URL builder without the HTTP features: 40 | 41 | `PM> Install-Package Flurl` 42 | 43 | For updates and announcements, [follow @FlurlHttp on Twitter](https://twitter.com/intent/user?screen_name=FlurlHttp). 44 | 45 | For detailed documentation, please visit the [main site](https://flurl.io). 46 | -------------------------------------------------------------------------------- /src/Flurl.Http/Configuration/DefaultUrlEncodedSerializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Flurl.Util; 4 | 5 | namespace Flurl.Http.Configuration 6 | { 7 | /// 8 | /// ISerializer implementation that converts an object representing name/value pairs to a URL-encoded string. 9 | /// Default serializer used in calls to PostUrlEncodedAsync, etc. 10 | /// 11 | public class DefaultUrlEncodedSerializer : ISerializer 12 | { 13 | /// 14 | /// Serializes the specified object. 15 | /// 16 | /// The object. 17 | public string Serialize(object obj) { 18 | if (obj == null) 19 | return null; 20 | 21 | var qp = new QueryParamCollection(); 22 | foreach (var kv in obj.ToKeyValuePairs()) 23 | qp.Merge(kv.Key, kv.Value, false, NullValueHandling.Ignore); 24 | 25 | return qp.ToString(true); 26 | } 27 | 28 | /// 29 | /// Deserializes the specified s. 30 | /// 31 | /// 32 | /// The s. 33 | /// Deserializing to UrlEncoded not supported. 34 | public T Deserialize(string s) { 35 | throw new NotImplementedException("Deserializing to UrlEncoded is not supported."); 36 | } 37 | 38 | /// 39 | /// Deserializes the specified stream. 40 | /// 41 | /// 42 | /// The stream. 43 | /// Deserializing to UrlEncoded not supported. 44 | public T Deserialize(Stream stream) { 45 | throw new NotImplementedException("Deserializing to UrlEncoded is not supported."); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/Flurl.Http/Configuration/NewtonsoftJsonSerializer.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Newtonsoft.Json; 3 | 4 | namespace Flurl.Http.Configuration 5 | { 6 | /// 7 | /// ISerializer implementation that uses Newtonsoft Json.NET. 8 | /// Default serializer used in calls to GetJsonAsync, PostJsonAsync, etc. 9 | /// 10 | public class NewtonsoftJsonSerializer : ISerializer 11 | { 12 | private readonly JsonSerializerSettings _settings; 13 | 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// The settings. 18 | public NewtonsoftJsonSerializer(JsonSerializerSettings settings) { 19 | _settings = settings; 20 | } 21 | 22 | /// 23 | /// Serializes the specified object. 24 | /// 25 | /// The object. 26 | /// 27 | public string Serialize(object obj) { 28 | return JsonConvert.SerializeObject(obj, _settings); 29 | } 30 | 31 | /// 32 | /// Deserializes the specified s. 33 | /// 34 | /// 35 | /// The s. 36 | /// 37 | public T Deserialize(string s) { 38 | return JsonConvert.DeserializeObject(s, _settings); 39 | } 40 | 41 | /// 42 | /// Deserializes the specified stream. 43 | /// 44 | /// 45 | /// The stream. 46 | /// 47 | public T Deserialize(Stream stream) { 48 | // http://james.newtonking.com/json/help/index.html?topic=html/Performance.htm 49 | using (var sr = new StreamReader(stream)) 50 | using (var jr = new JsonTextReader(sr)) { 51 | return JsonSerializer.CreateDefault(_settings).Deserialize(jr); 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/Flurl.Http/FlurlHttp.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Flurl.Http.Configuration; 3 | 4 | namespace Flurl.Http 5 | { 6 | /// 7 | /// A static container for global configuration settings affecting Flurl.Http behavior. 8 | /// 9 | public static class FlurlHttp 10 | { 11 | private static readonly object _configLock = new object(); 12 | 13 | private static Lazy _settings = 14 | new Lazy(() => new GlobalFlurlHttpSettings()); 15 | 16 | /// 17 | /// Globally configured Flurl.Http settings. Should normally be written to by calling FlurlHttp.Configure once application at startup. 18 | /// 19 | public static GlobalFlurlHttpSettings GlobalSettings => _settings.Value; 20 | 21 | /// 22 | /// Provides thread-safe access to Flurl.Http's global configuration settings. Should only be called once at application startup. 23 | /// 24 | /// the action to perform against the GlobalSettings 25 | public static void Configure(Action configAction) { 26 | lock (_configLock) { 27 | configAction(GlobalSettings); 28 | } 29 | } 30 | 31 | /// 32 | /// Provides thread-safe access to a specific IFlurlClient, typically to configure settings and default headers. 33 | /// 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. 34 | /// 35 | /// the URL used to find the IFlurlClient 36 | /// the action to perform against the IFlurlClient 37 | public static void ConfigureClient(string url, Action configAction) { 38 | var client = GlobalSettings.FlurlClientFactory.Get(url); 39 | lock (_configLock) { 40 | configAction(client); 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/Flurl/QueryParameter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using Flurl.Util; 7 | 8 | namespace Flurl 9 | { 10 | /// 11 | /// Represents an individual name/value pair within a URL query. 12 | /// 13 | public class QueryParameter 14 | { 15 | private object _value; 16 | private string _encodedValue; 17 | 18 | /// 19 | /// Creates a new instance of a query parameter. Allows specifying whether string value provided has 20 | /// already been URL-encoded. 21 | /// 22 | public QueryParameter(string name, object value, bool isEncoded = false) { 23 | Name = name; 24 | if (isEncoded && value != null) { 25 | _encodedValue = value as string; 26 | _value = Url.Decode(_encodedValue, true); 27 | } 28 | else { 29 | Value = value; 30 | } 31 | } 32 | 33 | /// 34 | /// The name (left side) of the query parameter. 35 | /// 36 | public string Name { get; set; } 37 | 38 | /// 39 | /// The value (right side) of the query parameter. 40 | /// 41 | public object Value { 42 | get => _value; 43 | set { 44 | _value = value; 45 | _encodedValue = null; 46 | } 47 | } 48 | 49 | /// 50 | /// Returns the string ("name=value") representation of the query parameter. 51 | /// 52 | /// Indicates whether to encode space characters with "+" instead of "%20". 53 | /// 54 | public string ToString(bool encodeSpaceAsPlus) { 55 | var name = Url.EncodeIllegalCharacters(Name, encodeSpaceAsPlus); 56 | var value = 57 | (_encodedValue != null) ? _encodedValue : 58 | (Value != null) ? Url.Encode(Value.ToInvariantString(), encodeSpaceAsPlus) : 59 | null; 60 | 61 | return (value == null) ? name : $"{name}={value}"; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Flurl.Http/HttpStatusRangeParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace Flurl.Http 7 | { 8 | /// 9 | /// The status range parser class. 10 | /// 11 | public static class HttpStatusRangeParser 12 | { 13 | /// 14 | /// Determines whether the specified pattern is match. 15 | /// 16 | /// The pattern. 17 | /// The value. 18 | /// pattern is invalid. 19 | public static bool IsMatch(string pattern, HttpStatusCode value) { 20 | return IsMatch(pattern, (int)value); 21 | } 22 | 23 | /// 24 | /// Determines whether the specified pattern is match. 25 | /// 26 | /// The pattern. 27 | /// The value. 28 | /// is invalid. 29 | public static bool IsMatch(string pattern, int value) { 30 | if (pattern == null) 31 | return false; 32 | 33 | foreach (var range in pattern.Split(',').Select(p => p.Trim())) { 34 | if (range == "") 35 | continue; 36 | 37 | if (range == "*") 38 | return true; // special case - allow everything 39 | 40 | var bounds = range.Split('-'); 41 | int lower = 0, upper = 0; 42 | 43 | var valid = 44 | bounds.Length <= 2 && 45 | int.TryParse(Regex.Replace(bounds.First().Trim(), "[*xX]", "0"), out lower) && 46 | int.TryParse(Regex.Replace(bounds.Last().Trim(), "[*xX]", "9"), out upper); 47 | 48 | if (!valid) { 49 | throw new ArgumentException( 50 | $"Invalid range pattern: \"{pattern}\". Examples of allowed patterns: \"400\", \"4xx\", \"300,400-403\", \"*\"."); 51 | } 52 | 53 | if (value >= lower && value <= upper) 54 | return true; 55 | } 56 | return false; 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /Test/Flurl.Test/Http/FlurlHttpExceptionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Flurl.Http; 7 | using NUnit.Framework; 8 | 9 | namespace Flurl.Test.Http 10 | { 11 | [TestFixture, Parallelizable] 12 | class FlurlHttpExceptionTests : HttpTestFixtureBase 13 | { 14 | [Test] 15 | public async Task exception_message_is_nice() { 16 | HttpTest.RespondWithJson(new { message = "bad data!" }, 400); 17 | 18 | try { 19 | await "http://myapi.com".PostJsonAsync(new { data = "bad" }); 20 | Assert.Fail("should have thrown 400."); 21 | } 22 | catch (FlurlHttpException ex) { 23 | Assert.AreEqual("Call failed with status code 400 (Bad Request): POST http://myapi.com", ex.Message); 24 | } 25 | } 26 | 27 | [Test] 28 | public async Task exception_message_excludes_request_response_labels_when_body_empty() { 29 | HttpTest.RespondWith("", 400); 30 | 31 | try { 32 | await "http://myapi.com".GetAsync(); 33 | Assert.Fail("should have thrown 400."); 34 | } 35 | catch (FlurlHttpException ex) { 36 | // no "Request body:", "Response body:", or line breaks 37 | Assert.AreEqual("Call failed with status code 400 (Bad Request): GET http://myapi.com", ex.Message); 38 | } 39 | } 40 | 41 | [Test] 42 | public async Task can_catch_parsing_error() { 43 | HttpTest.RespondWith("I'm not JSON!"); 44 | 45 | try { 46 | await "http://myapi.com".GetJsonAsync(); 47 | Assert.Fail("should have failed to parse response."); 48 | } 49 | catch (FlurlParsingException ex) { 50 | Assert.AreEqual("Response could not be deserialized to JSON: GET http://myapi.com", ex.Message); 51 | Assert.AreEqual("I'm not JSON!", await ex.GetResponseStringAsync()); 52 | // will differ if you're using a different serializer (which you probably aren't): 53 | Assert.IsInstanceOf(ex.InnerException); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Flurl.Http/FileUtil.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | #if NETSTANDARD1_1 3 | using System.Linq; 4 | #endif 5 | using System.Threading.Tasks; 6 | 7 | namespace Flurl.Http 8 | { 9 | internal static class FileUtil 10 | { 11 | #if NETSTANDARD1_1 12 | internal static string GetFileName(string path) { 13 | return path?.Split(PCLStorage.PortablePath.DirectorySeparatorChar).Last(); 14 | } 15 | 16 | internal static string CombinePath(params string[] paths) { 17 | return PCLStorage.PortablePath.Combine(paths); 18 | } 19 | 20 | internal static async Task OpenReadAsync(string path, int bufferSize) { 21 | var file = await PCLStorage.FileSystem.Current.GetFileFromPathAsync(path).ConfigureAwait(false); 22 | return await file.OpenAsync(PCLStorage.FileAccess.Read).ConfigureAwait(false); 23 | } 24 | 25 | internal static async Task OpenWriteAsync(string folderPath, string fileName, int bufferSize) { 26 | var folder = await PCLStorage.FileSystem.Current.LocalStorage.CreateFolderAsync(folderPath, PCLStorage.CreationCollisionOption.OpenIfExists).ConfigureAwait(false); 27 | var file = await folder.CreateFileAsync(fileName, PCLStorage.CreationCollisionOption.ReplaceExisting).ConfigureAwait(false); 28 | return await file.OpenAsync(PCLStorage.FileAccess.ReadAndWrite).ConfigureAwait(false); 29 | } 30 | #else 31 | internal static string GetFileName(string path) { 32 | return Path.GetFileName(path); 33 | } 34 | 35 | internal static string CombinePath(params string[] paths) { 36 | return Path.Combine(paths); 37 | } 38 | 39 | internal static Task OpenReadAsync(string path, int bufferSize) { 40 | return Task.FromResult(new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, useAsync: true)); 41 | } 42 | 43 | internal static Task OpenWriteAsync(string folderPath, string fileName, int bufferSize) { 44 | Directory.CreateDirectory(folderPath); // checks existence 45 | var filePath = Path.Combine(folderPath, fileName); 46 | return Task.FromResult(new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize, useAsync: true)); 47 | } 48 | #endif 49 | } 50 | } -------------------------------------------------------------------------------- /src/Flurl.Http/Configuration/FlurlClientFactoryBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | 4 | namespace Flurl.Http.Configuration 5 | { 6 | /// 7 | /// Encapsulates a creation/caching strategy for IFlurlClient instances. Custom factories looking to extend 8 | /// Flurl's behavior should inherit from this class, rather than implementing IFlurlClientFactory directly. 9 | /// 10 | public abstract class FlurlClientFactoryBase : IFlurlClientFactory 11 | { 12 | private readonly ConcurrentDictionary _clients = new ConcurrentDictionary(); 13 | 14 | /// 15 | /// By defaykt, uses a caching strategy of one FlurlClient per host. This maximizes reuse of 16 | /// underlying HttpClient/Handler while allowing things like cookies to be host-specific. 17 | /// 18 | /// The URL. 19 | /// The FlurlClient instance. 20 | public virtual IFlurlClient Get(Url url) { 21 | if (url == null) 22 | throw new ArgumentNullException(nameof(url)); 23 | 24 | return _clients.AddOrUpdate( 25 | GetCacheKey(url), 26 | u => Create(u), 27 | (u, client) => client.IsDisposed ? Create(u) : client); 28 | } 29 | 30 | /// 31 | /// Defines a strategy for getting a cache key based on a Url. Default implementation 32 | /// returns the host part (i.e www.api.com) so that all calls to the same host use the 33 | /// same FlurlClient (and HttpClient/HttpMessageHandler) instance. 34 | /// 35 | /// The URL. 36 | /// The cache key 37 | protected abstract string GetCacheKey(Url url); 38 | 39 | /// 40 | /// Creates a new FlurlClient 41 | /// 42 | /// The URL (not used) 43 | /// 44 | protected virtual IFlurlClient Create(Url url) => new FlurlClient(); 45 | 46 | /// 47 | /// Disposes all cached IFlurlClient instances and clears the cache. 48 | /// 49 | public void Dispose() { 50 | foreach (var kv in _clients) { 51 | if (!kv.Value.IsDisposed) 52 | kv.Value.Dispose(); 53 | } 54 | _clients.Clear(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Flurl.Http.CodeGen/CodeWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Flurl.Http.CodeGen 5 | { 6 | /// 7 | /// Wraps a StreamWriter. Mainly just keeps track of indentation. 8 | /// 9 | public class CodeWriter : IDisposable 10 | { 11 | private readonly StreamWriter _sw; 12 | private int _indent; 13 | private bool _wrapping; 14 | 15 | public CodeWriter(string filePath) 16 | { 17 | _sw = new StreamWriter(File.OpenWrite(filePath)); 18 | } 19 | 20 | /// 21 | /// use @0, @1, @2, etc for tokens. ({0} would be a pain because you'd alway need to escape "{" and "}") 22 | /// 23 | public CodeWriter WriteLine(string line, params object[] args) 24 | { 25 | line = line.Trim(); 26 | 27 | for (int i = 0; i < args.Length; i++) 28 | { 29 | var val = (args[i] == null) ? "" : args[i].ToString(); 30 | line = line.Replace("@" + i, val); 31 | } 32 | 33 | if (line == "}" || line == "{") 34 | { 35 | _indent--; 36 | } 37 | 38 | _sw.Write(new String('\t', _indent)); 39 | _sw.WriteLine(line); 40 | 41 | if (line == "" || line.StartsWith("//") || line.EndsWith("]")) 42 | { 43 | _wrapping = false; 44 | } 45 | else if (line.EndsWith(";") || line.EndsWith("}")) 46 | { 47 | if (_wrapping) 48 | _indent--; 49 | _wrapping = false; 50 | } 51 | else if (line.EndsWith("{")) 52 | { 53 | _indent++; 54 | _wrapping = false; 55 | } 56 | else 57 | { 58 | if (!_wrapping) 59 | _indent++; 60 | _wrapping = true; 61 | } 62 | 63 | return this; // fluent! 64 | } 65 | 66 | public CodeWriter WriteLine() 67 | { 68 | _sw.WriteLine(); 69 | return this; 70 | } 71 | 72 | public void Dispose() 73 | { 74 | _sw.Dispose(); 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /Test/Flurl.Test/Http/HttpStatusRangeParserTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Flurl.Http; 3 | using NUnit.Framework; 4 | 5 | namespace Flurl.Test.Http 6 | { 7 | [TestFixture, Parallelizable] 8 | public class HttpStatusRangeParserTests 9 | { 10 | [TestCase("4**", 399, ExpectedResult = false)] 11 | [TestCase("4**", 400, ExpectedResult = true)] 12 | [TestCase("4**", 499, ExpectedResult = true)] 13 | [TestCase("4**", 500, ExpectedResult = false)] 14 | 15 | [TestCase("4xx", 399, ExpectedResult = false)] 16 | [TestCase("4xx", 400, ExpectedResult = true)] 17 | [TestCase("4xx", 499, ExpectedResult = true)] 18 | [TestCase("4xx", 500, ExpectedResult = false)] 19 | 20 | [TestCase("4XX", 399, ExpectedResult = false)] 21 | [TestCase("4XX", 400, ExpectedResult = true)] 22 | [TestCase("4XX", 499, ExpectedResult = true)] 23 | [TestCase("4XX", 500, ExpectedResult = false)] 24 | 25 | [TestCase("400-499", 399, ExpectedResult = false)] 26 | [TestCase("400-499", 400, ExpectedResult = true)] 27 | [TestCase("400-499", 499, ExpectedResult = true)] 28 | [TestCase("400-499", 500, ExpectedResult = false)] 29 | 30 | [TestCase("100,3xx,600", 100, ExpectedResult = true)] 31 | [TestCase("100,3xx,600", 101, ExpectedResult = false)] 32 | [TestCase("100,3xx,600", 300, ExpectedResult = true)] 33 | [TestCase("100,3xx,600", 399, ExpectedResult = true)] 34 | [TestCase("100,3xx,600", 400, ExpectedResult = false)] 35 | [TestCase("100,3xx,600", 600, ExpectedResult = true)] 36 | 37 | [TestCase("400-409,490-499", 399, ExpectedResult = false)] 38 | [TestCase("400-409,490-499", 405, ExpectedResult = true)] 39 | [TestCase("400-409,490-499", 450, ExpectedResult = false)] 40 | [TestCase("400-409,490-499", 495, ExpectedResult = true)] 41 | [TestCase("400-409,490-499", 500, ExpectedResult = false)] 42 | 43 | [TestCase("*", 0, ExpectedResult = true)] 44 | [TestCase(",,,*", 9999, ExpectedResult = true)] 45 | 46 | [TestCase("", 0, ExpectedResult = false)] 47 | [TestCase(",,,", 9999, ExpectedResult = false)] 48 | public bool parser_works(string pattern, int value) { 49 | return HttpStatusRangeParser.IsMatch(pattern, value); 50 | } 51 | 52 | [TestCase("-100")] 53 | [TestCase("100-")] 54 | [TestCase("1yy")] 55 | public void parser_throws_on_invalid_pattern(string pattern) { 56 | Assert.Throws(() => HttpStatusRangeParser.IsMatch(pattern, 100)); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /PackageTesters/PackageTester.Shared/Tester.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using System.IO; 5 | using Flurl; 6 | using Flurl.Http; 7 | using Flurl.Http.Testing; 8 | 9 | namespace PackageTester 10 | { 11 | public class Tester 12 | { 13 | private int _pass; 14 | private int _fail; 15 | 16 | public async Task DoTestsAsync() { 17 | _pass = 0; 18 | _fail = 0; 19 | 20 | await Test("Testing real request to google.com...", async () => { 21 | var real = await "http://www.google.com".GetStringAsync(); 22 | Assert(real.Trim().StartsWith("<"), $"Response from google.com doesn't look right: {real}"); 23 | }); 24 | 25 | await Test("Testing fake request with HttpTest...", async () => { 26 | using (var test = new HttpTest()) { 27 | test.RespondWith("fake response"); 28 | var fake = await "http://www.google.com".GetStringAsync(); 29 | Assert(fake == "fake response", $"Fake response doesn't look right: {fake}"); 30 | } 31 | }); 32 | 33 | await Test("Testing file download...", async () => { 34 | var path = "c:\\google.txt"; 35 | if (File.Exists(path)) File.Delete(path); 36 | var result = await "http://www.google.com".DownloadFileAsync("c:\\", "google.txt"); 37 | Assert(result == path, $"Download result {result} doesn't match {path}"); 38 | Assert(File.Exists(path), $"File didn't appear to download to {path}"); 39 | if (File.Exists(path)) File.Delete(path); 40 | }); 41 | 42 | if (_fail > 0) { 43 | Console.ForegroundColor = ConsoleColor.Red; 44 | Console.WriteLine($"{_pass} passed, {_fail} failed"); 45 | } 46 | else { 47 | Console.ForegroundColor = ConsoleColor.Green; 48 | Console.WriteLine("Everything looks good"); 49 | } 50 | Console.ResetColor(); 51 | } 52 | 53 | private async Task Test(string msg, Func act) { 54 | Console.WriteLine(msg); 55 | try { 56 | await act(); 57 | Console.WriteLine("pass."); 58 | _pass++; 59 | } 60 | catch (Exception ex) { 61 | Console.ForegroundColor = ConsoleColor.Red; 62 | Console.WriteLine($"Fail! {ex.Message}"); 63 | Console.WriteLine(ex.StackTrace); 64 | _fail++; 65 | } 66 | finally { 67 | Console.ResetColor(); 68 | } 69 | } 70 | 71 | private void Assert(bool check, string msg) { 72 | if (!check) throw new Exception(msg); 73 | } 74 | } 75 | 76 | internal class TestResponse 77 | { 78 | public string TestString { get; set; } 79 | } 80 | } -------------------------------------------------------------------------------- /Test/Flurl.Test/Http/MultipartTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Flurl.Http; 9 | using Flurl.Http.Content; 10 | using NUnit.Framework; 11 | 12 | namespace Flurl.Test.Http 13 | { 14 | [TestFixture, Parallelizable] 15 | public class MultipartTests 16 | { 17 | [Test] 18 | public void can_build_multipart_content() { 19 | var content = new CapturedMultipartContent() 20 | .AddString("string", "foo") 21 | .AddStringParts(new { part1 = 1, part2 = 2, part3 = (string)null }) // part3 should be excluded 22 | .AddFile("file", Path.Combine("path", "to", "image.jpg"), "image/jpeg") 23 | .AddJson("json", new { foo = "bar" }) 24 | .AddUrlEncoded("urlEnc", new { fizz = "buzz" }); 25 | 26 | Assert.AreEqual(6, content.Parts.Length); 27 | 28 | Assert.AreEqual("string", content.Parts[0].Headers.ContentDisposition.Name); 29 | Assert.IsInstanceOf(content.Parts[0]); 30 | Assert.AreEqual("foo", (content.Parts[0] as CapturedStringContent).Content); 31 | 32 | Assert.AreEqual("part1", content.Parts[1].Headers.ContentDisposition.Name); 33 | Assert.IsInstanceOf(content.Parts[1]); 34 | Assert.AreEqual("1", (content.Parts[1] as CapturedStringContent).Content); 35 | 36 | Assert.AreEqual("part2", content.Parts[2].Headers.ContentDisposition.Name); 37 | Assert.IsInstanceOf(content.Parts[2]); 38 | Assert.AreEqual("2", (content.Parts[2] as CapturedStringContent).Content); 39 | 40 | Assert.AreEqual("file", content.Parts[3].Headers.ContentDisposition.Name); 41 | Assert.AreEqual("image.jpg", content.Parts[3].Headers.ContentDisposition.FileName); 42 | Assert.IsInstanceOf(content.Parts[3]); 43 | 44 | Assert.AreEqual("json", content.Parts[4].Headers.ContentDisposition.Name); 45 | Assert.IsInstanceOf(content.Parts[4]); 46 | Assert.AreEqual("{\"foo\":\"bar\"}", (content.Parts[4] as CapturedJsonContent).Content); 47 | 48 | Assert.AreEqual("urlEnc", content.Parts[5].Headers.ContentDisposition.Name); 49 | Assert.IsInstanceOf(content.Parts[5]); 50 | Assert.AreEqual("fizz=buzz", (content.Parts[5] as CapturedUrlEncodedContent).Content); 51 | } 52 | 53 | [Test] 54 | public void must_provide_required_args_to_builder() { 55 | var content = new CapturedMultipartContent(); 56 | Assert.Throws(() => content.AddStringParts(null)); 57 | Assert.Throws(() => content.AddString("other", null)); 58 | Assert.Throws(() => content.AddString(null, "hello!")); 59 | Assert.Throws(() => content.AddFile(" ", "path")); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Flurl.Http/MultipartExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Flurl.Http.Content; 6 | 7 | namespace Flurl.Http 8 | { 9 | /// 10 | /// Fluent extension menthods for sending multipart/form-data requests. 11 | /// 12 | public static class MultipartExtensions 13 | { 14 | /// 15 | /// Sends an asynchronous multipart/form-data POST request. 16 | /// 17 | /// A delegate for building the content parts. 18 | /// The IFlurlRequest. 19 | /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. 20 | /// A Task whose result is the received HttpResponseMessage. 21 | public static Task PostMultipartAsync(this IFlurlRequest request, Action buildContent, CancellationToken cancellationToken = default(CancellationToken)) { 22 | var cmc = new CapturedMultipartContent(request.Settings); 23 | buildContent(cmc); 24 | return request.SendAsync(HttpMethod.Post, cmc, cancellationToken); 25 | } 26 | 27 | /// 28 | /// Creates a FlurlRequest from the URL and sends an asynchronous multipart/form-data POST request. 29 | /// 30 | /// A delegate for building the content parts. 31 | /// The URL. 32 | /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. 33 | /// A Task whose result is the received HttpResponseMessage. 34 | public static Task PostMultipartAsync(this Url url, Action buildContent, CancellationToken cancellationToken = default(CancellationToken)) { 35 | return new FlurlRequest(url).PostMultipartAsync(buildContent, cancellationToken); 36 | } 37 | 38 | /// 39 | /// Creates a FlurlRequest from the URL and sends an asynchronous multipart/form-data POST request. 40 | /// 41 | /// A delegate for building the content parts. 42 | /// The URL. 43 | /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. 44 | /// A Task whose result is the received HttpResponseMessage. 45 | public static Task PostMultipartAsync(this string url, Action buildContent, CancellationToken cancellationToken = default(CancellationToken)) { 46 | return new FlurlRequest(url).PostMultipartAsync(buildContent, cancellationToken); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Flurl.Http/Flurl.Http.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net45;netstandard1.1;netstandard1.3;netstandard2.0; 5 | netstandard1.1;netstandard1.3;netstandard2.0; 6 | True 7 | Flurl.Http 8 | 2.3.1 9 | Todd Menier 10 | A fluent, portable, testable HTTP client library. 11 | http://tmenier.github.io/Flurl 12 | https://pbs.twimg.com/profile_images/534024476296376320/IuPGZ_bX_400x400.png 13 | https://raw.githubusercontent.com/tmenier/Flurl/master/LICENSE 14 | https://github.com/tmenier/Flurl.git 15 | git 16 | httpclient rest json http fluent url uri tdd assert async 17 | https://github.com/tmenier/Flurl/releases 18 | false 19 | 20 | 21 | 22 | True 23 | 24 | 25 | 26 | bin\Release\Flurl.Http.xml 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | portable-net45+win8+wp8 49 | 50 | 51 | 52 | bin\Debug\net45\Flurl.Http.xml 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/Flurl.Http/HttpCall.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | using Flurl.Http.Content; 5 | 6 | namespace Flurl.Http 7 | { 8 | /// 9 | /// Encapsulates request, response, and other details associated with an HTTP call. Useful for diagnostics and available in 10 | /// global event handlers and FlurlHttpException.Call. 11 | /// 12 | public class HttpCall 13 | { 14 | /// 15 | /// The IFlurlRequest associated with this call. 16 | /// 17 | public IFlurlRequest FlurlRequest { get; set; } 18 | 19 | /// 20 | /// The HttpRequestMessage associated with this call. 21 | /// 22 | public HttpRequestMessage Request { get; set; } 23 | 24 | /// 25 | /// Captured request body. Available ONLY if Request.Content is a Flurl.Http.Content.CapturedStringContent. 26 | /// 27 | public string RequestBody => (Request.Content as CapturedStringContent)?.Content; 28 | 29 | /// 30 | /// HttpResponseMessage associated with the call if the call completed, otherwise null. 31 | /// 32 | public HttpResponseMessage Response { get; set; } 33 | 34 | /// 35 | /// Exception that occurred while sending the HttpRequestMessage. 36 | /// 37 | public Exception Exception { get; set; } 38 | 39 | /// 40 | /// User code should set this to true inside global event handlers (OnError, etc) to indicate 41 | /// that the exception was handled and should not be propagated further. 42 | /// 43 | public bool ExceptionHandled { get; set; } 44 | 45 | /// 46 | /// DateTime the moment the request was sent. 47 | /// 48 | public DateTime StartedUtc { get; set; } 49 | 50 | /// 51 | /// DateTime the moment a response was received. 52 | /// 53 | public DateTime? EndedUtc { get; set; } 54 | 55 | /// 56 | /// Total duration of the call if it completed, otherwise null. 57 | /// 58 | public TimeSpan? Duration => EndedUtc - StartedUtc; 59 | 60 | /// 61 | /// True if a response was received, regardless of whether it is an error status. 62 | /// 63 | public bool Completed => Response != null; 64 | 65 | /// 66 | /// True if a response with a successful HTTP status was received. 67 | /// 68 | public bool Succeeded => Completed && 69 | (Response.IsSuccessStatusCode || HttpStatusRangeParser.IsMatch(FlurlRequest.Settings.AllowedHttpStatusRange, Response.StatusCode)); 70 | 71 | /// 72 | /// HttpStatusCode of the response if the call completed, otherwise null. 73 | /// 74 | public HttpStatusCode? HttpStatus => Completed ? (HttpStatusCode?)Response.StatusCode : null; 75 | 76 | /// 77 | /// Returns the verb and absolute URI associated with this call. 78 | /// 79 | /// 80 | public override string ToString() { 81 | return $"{Request.Method:U} {FlurlRequest.Url}"; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Flurl.Http.CodeGen/HttpExtensionMethod.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace Flurl.Http.CodeGen 5 | { 6 | public class HttpExtensionMethod 7 | { 8 | public static IEnumerable GetAll() { 9 | return 10 | from httpVerb in new[] { null, "Get", "Post", "Head", "Put", "Delete", "Patch", "Options" } 11 | from bodyType in new[] { null, "Json", /*"Xml",*/ "String", "UrlEncoded" } 12 | from extensionType in new[] { "IFlurlRequest", "Url", "string" } 13 | where SupportedCombo(httpVerb, bodyType, extensionType) 14 | from deserializeType in new[] { null, "Json", "JsonList", /*"Xml",*/ "String", "Stream", "Bytes" } 15 | where httpVerb == "Get" || deserializeType == null 16 | from isGeneric in new[] { true, false } 17 | where AllowDeserializeToGeneric(deserializeType) || !isGeneric 18 | select new HttpExtensionMethod { 19 | HttpVerb = httpVerb, 20 | BodyType = bodyType, 21 | ExtentionOfType = extensionType, 22 | DeserializeToType = deserializeType, 23 | IsGeneric = isGeneric 24 | }; 25 | } 26 | 27 | private static bool SupportedCombo(string verb, string bodyType, string extensionType) { 28 | switch (verb) { 29 | case null: // Send 30 | return bodyType != null || extensionType != "IFlurlRequest"; 31 | case "Post": 32 | return true; 33 | case "Put": 34 | case "Patch": 35 | return bodyType != "UrlEncoded"; 36 | default: // Get, Head, Delete, Options 37 | return bodyType == null; 38 | } 39 | } 40 | 41 | private static bool AllowDeserializeToGeneric(string deserializeType) { 42 | switch (deserializeType) { 43 | case "Json": 44 | return true; 45 | default: 46 | return false; 47 | } 48 | } 49 | 50 | public string HttpVerb { get; set; } 51 | public string BodyType { get; set; } 52 | public string ExtentionOfType { get; set; } 53 | public string DeserializeToType { get; set; } 54 | public bool IsGeneric { get; set; } 55 | 56 | public string Name => $"{HttpVerb ?? "Send"}{BodyType ?? DeserializeToType}Async"; 57 | 58 | public string TaskArg { 59 | get { 60 | switch (DeserializeToType) { 61 | case "Json": return IsGeneric ? "T" : "dynamic"; 62 | case "JsonList": return "IList"; 63 | //case "Xml": return ?; 64 | case "String": return "string"; 65 | case "Stream": return "Stream"; 66 | case "Bytes": return "byte[]"; 67 | default: return "HttpResponseMessage"; 68 | } 69 | } 70 | } 71 | 72 | public string ReturnTypeDescription { 73 | get { 74 | //var response = (xm.DeserializeToType == null) ? "" : "" + xm.TaskArg; 75 | switch (DeserializeToType) { 76 | case "Json": return "the JSON response body deserialized to " + (IsGeneric ? "an object of type T" : "a dynamic"); 77 | case "JsonList": return "the JSON response body deserialized to a list of dynamics"; 78 | //case "Xml": return ?; 79 | case "String": return "the response body as a string"; 80 | case "Stream": return "the response body as a Stream"; 81 | case "Bytes": return "the response body as a byte array"; 82 | default: return "the received HttpResponseMessage"; 83 | } 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /PackageTesters/PackageTester.NET45/PackageTester.NET45.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {AA8792B6-E0FA-46BA-BA03-C7971745F577} 8 | Exe 9 | PackageTester.NET45 10 | PackageTester.NET45 11 | v4.5 12 | 512 13 | 14 | 15 | AnyCPU 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | AnyCPU 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | ..\..\packages\Flurl.2.7.0\lib\net40\Flurl.dll 36 | 37 | 38 | ..\..\packages\Flurl.Http.2.2.1\lib\net45\Flurl.Http.dll 39 | 40 | 41 | ..\..\packages\Newtonsoft.Json.11.0.1\lib\net45\Newtonsoft.Json.dll 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/Flurl.Http/CookieExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Flurl.Util; 8 | 9 | namespace Flurl.Http 10 | { 11 | /// 12 | /// Fluent extension methods for working with HTTP cookies. 13 | /// 14 | public static class CookieExtensions 15 | { 16 | /// 17 | /// Allows cookies to be sent and received. Not necessary to call when setting cookies via WithCookie/WithCookies. 18 | /// 19 | /// The IFlurlClient or IFlurlRequest. 20 | /// This IFlurlClient. 21 | public static T EnableCookies(this T clientOrRequest) where T : IHttpSettingsContainer { 22 | clientOrRequest.Settings.CookiesEnabled = true; 23 | return clientOrRequest; 24 | } 25 | 26 | /// 27 | /// Sets an HTTP cookie to be sent with this IFlurlRequest or all requests made with this IFlurlClient. 28 | /// 29 | /// The IFlurlClient or IFlurlRequest. 30 | /// The cookie to set. 31 | /// This IFlurlClient. 32 | public static T WithCookie(this T clientOrRequest, Cookie cookie) where T : IHttpSettingsContainer { 33 | clientOrRequest.Settings.CookiesEnabled = true; 34 | clientOrRequest.Cookies[cookie.Name] = cookie; 35 | return clientOrRequest; 36 | } 37 | 38 | /// 39 | /// Sets an HTTP cookie to be sent with this IFlurlRequest or all requests made with this IFlurlClient. 40 | /// 41 | /// The IFlurlClient or IFlurlRequest. 42 | /// The cookie name. 43 | /// The cookie value. 44 | /// The cookie expiration (optional). If excluded, cookie only lives for duration of session. 45 | /// This IFlurlClient. 46 | public static T WithCookie(this T clientOrRequest, string name, object value, DateTime? expires = null) where T : IHttpSettingsContainer { 47 | var cookie = new Cookie(name, value?.ToInvariantString()) { Expires = expires ?? DateTime.MinValue }; 48 | return clientOrRequest.WithCookie(cookie); 49 | } 50 | 51 | /// 52 | /// 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. 53 | /// 54 | /// The IFlurlClient or IFlurlRequest. 55 | /// Names/values of HTTP cookies to set. Typically an anonymous object or IDictionary. 56 | /// Expiration for all cookies (optional). If excluded, cookies only live for duration of session. 57 | /// This IFlurlClient. 58 | public static T WithCookies(this T clientOrRequest, object cookies, DateTime? expires = null) where T : IHttpSettingsContainer { 59 | if (cookies == null) 60 | return clientOrRequest; 61 | 62 | foreach (var kv in cookies.ToKeyValuePairs()) 63 | clientOrRequest.WithCookie(kv.Key, kv.Value, expires); 64 | 65 | return clientOrRequest; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /PackageTesters/PackageTester.NET461/PackageTester.NET461.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {84FB572A-8B77-4B09-B825-2A240BCE1B7A} 8 | Exe 9 | PackageTester.NET461 10 | PackageTester.NET461 11 | v4.6.1 12 | 512 13 | true 14 | 15 | 16 | 17 | AnyCPU 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | AnyCPU 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | ..\..\packages\Flurl.2.7.0\lib\net40\Flurl.dll 38 | 39 | 40 | ..\..\packages\Flurl.Http.2.2.1\lib\net45\Flurl.Http.dll 41 | 42 | 43 | ..\..\packages\Newtonsoft.Json.11.0.1\lib\net45\Newtonsoft.Json.dll 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | Designer 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/Flurl.Http/DownloadExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | using Flurl.Util; 5 | 6 | namespace Flurl.Http 7 | { 8 | /// 9 | /// Fluent extension methods for downloading a file. 10 | /// 11 | public static class DownloadExtensions 12 | { 13 | /// 14 | /// Asynchronously downloads a file at the specified URL. 15 | /// 16 | /// The flurl request. 17 | /// Path of local folder where file is to be downloaded. 18 | /// Name of local file. If not specified, the source filename (from Content-Dispostion header, or last segment of the URL) is used. 19 | /// Buffer size in bytes. Default is 4096. 20 | /// A Task whose result is the local path of the downloaded file. 21 | public static async Task DownloadFileAsync(this IFlurlRequest request, string localFolderPath, string localFileName = null, int bufferSize = 4096) { 22 | var response = await request.SendAsync(HttpMethod.Get, completionOption: HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); 23 | 24 | localFileName = 25 | localFileName ?? 26 | response.Content?.Headers.ContentDisposition?.FileName?.StripQuotes() ?? 27 | request.Url.Path.Split('/').Last(); 28 | 29 | // http://codereview.stackexchange.com/a/18679 30 | using (var httpStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) 31 | using (var filestream = await FileUtil.OpenWriteAsync(localFolderPath, localFileName, bufferSize).ConfigureAwait(false)) { 32 | await httpStream.CopyToAsync(filestream, bufferSize).ConfigureAwait(false); 33 | } 34 | 35 | return FileUtil.CombinePath(localFolderPath, localFileName); 36 | } 37 | 38 | /// 39 | /// Asynchronously downloads a file at the specified URL. 40 | /// 41 | /// The Url. 42 | /// Path of local folder where file is to be downloaded. 43 | /// Name of local file. If not specified, the source filename (last segment of the URL) is used. 44 | /// Buffer size in bytes. Default is 4096. 45 | /// A Task whose result is the local path of the downloaded file. 46 | public static Task DownloadFileAsync(this string url, string localFolderPath, string localFileName = null, int bufferSize = 4096) { 47 | return new FlurlRequest(url).DownloadFileAsync(localFolderPath, localFileName, bufferSize); 48 | } 49 | 50 | /// 51 | /// Asynchronously downloads a file at the specified URL. 52 | /// 53 | /// The Url. 54 | /// Path of local folder where file is to be downloaded. 55 | /// Name of local file. If not specified, the source filename (last segment of the URL) is used. 56 | /// Buffer size in bytes. Default is 4096. 57 | /// A Task whose result is the local path of the downloaded file. 58 | public static Task DownloadFileAsync(this Url url, string localFolderPath, string localFileName = null, int bufferSize = 4096) { 59 | return new FlurlRequest(url).DownloadFileAsync(localFolderPath, localFileName, bufferSize); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /Test/Flurl.Test/Http/PostTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using System.Threading.Tasks; 3 | using Flurl.Http; 4 | using NUnit.Framework; 5 | 6 | namespace Flurl.Test.Http 7 | { 8 | [TestFixture, Parallelizable] 9 | public class PostTests : HttpMethodTests 10 | { 11 | public PostTests() : base(HttpMethod.Post) { } 12 | 13 | protected override Task CallOnString(string url) => url.PostAsync(null); 14 | protected override Task CallOnUrl(Url url) => url.PostAsync(null); 15 | protected override Task CallOnFlurlRequest(IFlurlRequest req) => req.PostAsync(null); 16 | 17 | [Test] 18 | public async Task can_post_string() { 19 | var expectedEndpoint = "http://some-api.com"; 20 | var expectedBody = "abc123"; 21 | await expectedEndpoint.PostStringAsync(expectedBody); 22 | HttpTest.ShouldHaveCalled(expectedEndpoint) 23 | .WithVerb(HttpMethod.Post) 24 | .WithRequestBody(expectedBody) 25 | .Times(1); 26 | } 27 | 28 | [Test] 29 | public async Task can_post_object_as_json() { 30 | var expectedEndpoint = "http://some-api.com"; 31 | var expectedBody = new {a = 1, b = 2}; 32 | await expectedEndpoint.PostJsonAsync(expectedBody); 33 | HttpTest.ShouldHaveCalled(expectedEndpoint) 34 | .WithVerb(HttpMethod.Post) 35 | .WithContentType("application/json") 36 | .WithRequestJson(expectedBody) 37 | .Times(1); 38 | } 39 | 40 | [Test] 41 | public async Task can_post_url_encoded() { 42 | await "http://some-api.com".PostUrlEncodedAsync(new { a = 1, b = 2, c = "hi there", d = new[] { 1, 2, 3 } }); 43 | HttpTest.ShouldHaveCalled("http://some-api.com") 44 | .WithVerb(HttpMethod.Post) 45 | .WithContentType("application/x-www-form-urlencoded") 46 | .WithRequestBody("a=1&b=2&c=hi+there&d=1&d=2&d=3") 47 | .Times(1); 48 | } 49 | 50 | [Test] 51 | public async Task can_receive_json() { 52 | HttpTest.RespondWithJson(new TestData { id = 1, name = "Frank" }); 53 | 54 | var data = await "http://some-api.com".PostJsonAsync(new { a = 1, b = 2 }).ReceiveJson(); 55 | 56 | Assert.AreEqual(1, data.id); 57 | Assert.AreEqual("Frank", data.name); 58 | } 59 | 60 | [Test] 61 | public async Task can_receive_json_dynamic() { 62 | HttpTest.RespondWithJson(new { id = 1, name = "Frank" }); 63 | 64 | var data = await "http://some-api.com".PostJsonAsync(new { a = 1, b = 2 }).ReceiveJson(); 65 | 66 | Assert.AreEqual(1, data.id); 67 | Assert.AreEqual("Frank", data.name); 68 | } 69 | 70 | [Test] 71 | public async Task can_receive_json_dynamic_list() { 72 | HttpTest.RespondWithJson(new[] { 73 | new { id = 1, name = "Frank" }, 74 | new { id = 2, name = "Claire" } 75 | }); 76 | 77 | var data = await "http://some-api.com".PostJsonAsync(new { a = 1, b = 2 }).ReceiveJsonList(); 78 | 79 | Assert.AreEqual(1, data[0].id); 80 | Assert.AreEqual("Frank", data[0].name); 81 | Assert.AreEqual(2, data[1].id); 82 | Assert.AreEqual("Claire", data[1].name); 83 | } 84 | 85 | [Test] 86 | public async Task can_receive_string() { 87 | HttpTest.RespondWith("good job"); 88 | 89 | var data = await "http://some-api.com".PostJsonAsync(new { a = 1, b = 2 }).ReceiveString(); 90 | 91 | Assert.AreEqual("good job", data); 92 | } 93 | 94 | private class TestData 95 | { 96 | public int id { get; set; } 97 | public string name { get; set; } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Test/Flurl.Test/Http/HttpMethodTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | using Flurl.Http; 5 | using NUnit.Framework; 6 | 7 | namespace Flurl.Test.Http 8 | { 9 | /// 10 | /// Each HTTP method with first-class support in Flurl (via PostAsync, GetAsync, etc.) should 11 | /// have a test fixture that inherits from this base class. 12 | /// 13 | public abstract class HttpMethodTests : HttpTestFixtureBase 14 | { 15 | private readonly HttpMethod _verb; 16 | 17 | protected HttpMethodTests(HttpMethod verb) { 18 | _verb = verb; 19 | } 20 | 21 | protected abstract Task CallOnString(string url); 22 | protected abstract Task CallOnUrl(Url url); 23 | protected abstract Task CallOnFlurlRequest(IFlurlRequest req); 24 | 25 | [Test] 26 | public async Task can_call_on_FlurlClient() { 27 | await CallOnFlurlRequest(new FlurlRequest("http://www.api.com")); 28 | HttpTest.ShouldHaveCalled("http://www.api.com").WithVerb(_verb).Times(1); 29 | } 30 | 31 | [Test] 32 | public async Task can_call_on_string() { 33 | await CallOnString("http://www.api.com"); 34 | HttpTest.ShouldHaveCalled("http://www.api.com").WithVerb(_verb).Times(1); 35 | } 36 | 37 | [Test] 38 | public async Task can_call_on_url() { 39 | await CallOnUrl(new Url("http://www.api.com")); 40 | HttpTest.ShouldHaveCalled("http://www.api.com").WithVerb(_verb).Times(1); 41 | } 42 | } 43 | 44 | [TestFixture, Parallelizable] 45 | public class PutTests : HttpMethodTests 46 | { 47 | public PutTests() : base(HttpMethod.Put) { } 48 | protected override Task CallOnString(string url) => url.PutAsync(null); 49 | protected override Task CallOnUrl(Url url) => url.PutAsync(null); 50 | protected override Task CallOnFlurlRequest(IFlurlRequest req) => req.PutAsync(null); 51 | } 52 | 53 | [TestFixture, Parallelizable] 54 | public class PatchTests : HttpMethodTests 55 | { 56 | public PatchTests() : base(new HttpMethod("PATCH")) { } 57 | protected override Task CallOnString(string url) => url.PatchAsync(null); 58 | protected override Task CallOnUrl(Url url) => url.PatchAsync(null); 59 | protected override Task CallOnFlurlRequest(IFlurlRequest req) => req.PatchAsync(null); 60 | } 61 | 62 | [TestFixture, Parallelizable] 63 | public class DeleteTests : HttpMethodTests 64 | { 65 | public DeleteTests() : base(HttpMethod.Delete) { } 66 | protected override Task CallOnString(string url) => url.DeleteAsync(); 67 | protected override Task CallOnUrl(Url url) => url.DeleteAsync(); 68 | protected override Task CallOnFlurlRequest(IFlurlRequest req) => req.DeleteAsync(); 69 | } 70 | 71 | [TestFixture, Parallelizable] 72 | public class HeadTests : HttpMethodTests 73 | { 74 | public HeadTests() : base(HttpMethod.Head) { } 75 | protected override Task CallOnString(string url) => url.HeadAsync(); 76 | protected override Task CallOnUrl(Url url) => url.HeadAsync(); 77 | protected override Task CallOnFlurlRequest(IFlurlRequest req) => req.HeadAsync(); 78 | } 79 | 80 | [TestFixture, Parallelizable] 81 | public class OptionsTests : HttpMethodTests 82 | { 83 | public OptionsTests() : base(HttpMethod.Options) { } 84 | protected override Task CallOnString(string url) => url.OptionsAsync(); 85 | protected override Task CallOnUrl(Url url) => url.OptionsAsync(); 86 | protected override Task CallOnFlurlRequest(IFlurlRequest req) => req.OptionsAsync(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Test/Flurl.Test/ReflectionHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace Flurl.Test 7 | { 8 | public static class ReflectionHelper 9 | { 10 | public static MethodInfo[] GetAllExtensionMethods(Assembly asm) { 11 | // http://stackoverflow.com/a/299526/62600 12 | return ( 13 | from type in asm.GetTypes() 14 | where type.GetTypeInfo().IsSealed && !type.GetTypeInfo().IsGenericType && !type.GetTypeInfo().IsNested 15 | from method in type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) 16 | where method.IsDefined(typeof(ExtensionAttribute), false) 17 | where method.GetParameters()[0].ParameterType == typeof(T) 18 | select method).ToArray(); 19 | } 20 | 21 | public static bool AreSameMethodSignatures(MethodInfo method1, MethodInfo method2) { 22 | if (method1.Name != method2.Name) 23 | return false; 24 | 25 | if (!AreSameType(method1.ReturnType, method2.ReturnType)) 26 | return false; 27 | 28 | var genArgs1 = method1.GetGenericArguments(); 29 | var genArgs2 = method2.GetGenericArguments(); 30 | 31 | if (genArgs1.Length != genArgs2.Length) 32 | return false; 33 | 34 | for (int i = 0; i < genArgs1.Length; i++) { 35 | if (!AreSameType(genArgs1[i], genArgs2[i])) 36 | return false; 37 | } 38 | 39 | var args1 = method1.GetParameters().Skip(IsExtensionMethod(method1) ? 1 : 0).ToArray(); 40 | var args2 = method2.GetParameters().Skip(IsExtensionMethod(method2) ? 1 : 0).ToArray(); 41 | 42 | if (args1.Length != args2.Length) 43 | return false; 44 | 45 | for (int i = 0; i < args1.Length; i++) { 46 | if (args1[i].Name != args2[i].Name) return false; 47 | if (!AreSameType(args1[i].ParameterType, args2[i].ParameterType)) return false; 48 | if (args1[i].IsOptional != args2[i].IsOptional) return false; 49 | if (!AreSameValue(args1[i].DefaultValue, args2[i].DefaultValue)) return false; 50 | if (args1[i].IsIn != args2[i].IsIn) return false; 51 | } 52 | return true; 53 | } 54 | 55 | public static bool IsExtensionMethod(MethodInfo method) { 56 | var type = method.DeclaringType; 57 | return 58 | type.GetTypeInfo().IsSealed && 59 | !type.GetTypeInfo().IsGenericType && 60 | !type.GetTypeInfo().IsNested && 61 | method.IsStatic && 62 | method.IsDefined(typeof(ExtensionAttribute), false); 63 | } 64 | 65 | public static bool AreSameValue(object a, object b) { 66 | if (a == null && b == null) 67 | return true; 68 | if (a == null ^ b == null) 69 | return false; 70 | // ok, neither is null 71 | return a.Equals(b); 72 | } 73 | 74 | public static bool AreSameType(Type a, Type b) { 75 | if (a.IsGenericParameter && b.IsGenericParameter) { 76 | 77 | var constraintsA = a.GetTypeInfo().GetGenericParameterConstraints(); 78 | var constraintsB = b.GetTypeInfo().GetGenericParameterConstraints(); 79 | 80 | if (constraintsA.Length != constraintsB.Length) 81 | return false; 82 | 83 | for (int i = 0; i < constraintsA.Length; i++) { 84 | if (!AreSameType(constraintsA[i], constraintsB[i])) 85 | return false; 86 | } 87 | return true; 88 | } 89 | 90 | if (a.GetTypeInfo().IsGenericType && b.GetTypeInfo().IsGenericType) { 91 | 92 | if (a.GetGenericTypeDefinition() != b.GetGenericTypeDefinition()) 93 | return false; 94 | 95 | var genArgsA = a.GetGenericArguments(); 96 | var genArgsB = b.GetGenericArguments(); 97 | 98 | if (genArgsA.Length != genArgsB.Length) 99 | return false; 100 | 101 | for (int i = 0; i < genArgsA.Length; i++) { 102 | if (!AreSameType(genArgsA[i], genArgsB[i])) 103 | return false; 104 | } 105 | 106 | return true; 107 | } 108 | 109 | return a == b; 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /Test/Flurl.Test/CommonExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using NUnit.Framework; 5 | using Flurl.Util; 6 | 7 | namespace Flurl.Test 8 | { 9 | [TestFixture, Parallelizable] 10 | public class CommonExtensionsTests 11 | { 12 | [Test] 13 | public void can_parse_object_to_kv() 14 | { 15 | var kv = new { 16 | one = 1, 17 | two = "foo", 18 | three = (string)null 19 | }.ToKeyValuePairs(); 20 | 21 | CollectionAssert.AreEquivalent(new Dictionary { 22 | { "one", 1 }, 23 | { "two", "foo" }, 24 | { "three", null } 25 | }, kv); 26 | } 27 | 28 | [Test] 29 | public void can_parse_dictionary_to_kv() 30 | { 31 | var kv = new Dictionary { 32 | { "one", 1 }, 33 | { "two", "foo" }, 34 | { "three", null } 35 | }.ToKeyValuePairs(); 36 | 37 | CollectionAssert.AreEquivalent(new Dictionary { 38 | { "one", 1 }, 39 | { "two", "foo" }, 40 | { "three", null } 41 | }, kv); 42 | } 43 | 44 | [Test] 45 | public void can_parse_collection_of_kvp_to_kv() 46 | { 47 | var kv = new[] { 48 | new KeyValuePair("one", 1), 49 | new KeyValuePair("two", "foo"), 50 | new KeyValuePair("three", null), 51 | new KeyValuePair(null, "four"), 52 | }.ToKeyValuePairs(); 53 | 54 | CollectionAssert.AreEquivalent(new Dictionary { 55 | { "one", 1 }, 56 | { "two", "foo" }, 57 | { "three", null } 58 | }, kv); 59 | } 60 | 61 | [Test] 62 | public void can_parse_collection_of_conventional_objects_to_kv() 63 | { 64 | // convention is to accept collection of any arbitrary type that contains 65 | // a property called Key or Name and a property called Value 66 | var kv = new object[] { 67 | new { Key = "one", Value = 1 }, 68 | new { key = "two", value = "foo" }, // lower-case should work too 69 | new { Key = (string)null, Value = 3 }, // null keys should get skipped 70 | new { Name = "three", Value = (string)null }, 71 | new { name = "four", value = "bar" } // lower-case should work too 72 | }.ToKeyValuePairs().ToList(); 73 | 74 | CollectionAssert.AreEquivalent(new Dictionary { 75 | { "one", 1 }, 76 | { "two", "foo" }, 77 | { "three", null }, 78 | { "four", "bar" } 79 | }, kv); 80 | } 81 | 82 | [Test] 83 | public void can_parse_string_to_kv() 84 | { 85 | var kv = "one=1&two=foo&three".ToKeyValuePairs(); 86 | 87 | CollectionAssert.AreEquivalent(new Dictionary { 88 | { "one", "1" }, 89 | { "two", "foo" }, 90 | { "three", null } 91 | }, kv); 92 | } 93 | 94 | [Test] 95 | public void cannot_parse_null_to_kv() 96 | { 97 | object obj = null; 98 | Assert.Throws(() => obj.ToKeyValuePairs()); 99 | } 100 | 101 | [Test] 102 | public void SplitOnFirstOccurence_works() { 103 | var result = "hello/how/are/you".SplitOnFirstOccurence('/'); 104 | Assert.AreEqual(new[] { "hello", "how/are/you" }, result); 105 | } 106 | 107 | [TestCase(" \"\thi there \" \t\t ", ExpectedResult = "\thi there ")] 108 | [TestCase(" ' hi there ' ", ExpectedResult = " hi there ")] 109 | [TestCase(" hi there ", ExpectedResult = " hi there ")] 110 | public string StripQuotes_works(string s) => s.StripQuotes(); 111 | 112 | [Test] 113 | public void ToInvariantString_serializes_dates_to_iso() { 114 | Assert.AreEqual("2017-12-01T02:34:56.7890000", new DateTime(2017, 12, 1, 2, 34, 56, 789, DateTimeKind.Unspecified).ToInvariantString()); 115 | Assert.AreEqual("2017-12-01T02:34:56.7890000Z", new DateTime(2017, 12, 1, 2, 34, 56, 789, DateTimeKind.Utc).ToInvariantString()); 116 | Assert.AreEqual("2017-12-01T02:34:56.7890000-06:00", new DateTimeOffset(2017, 12, 1, 2, 34, 56, 789, TimeSpan.FromHours(-6)).ToInvariantString()); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Flurl.Http/HeaderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Flurl.Util; 8 | 9 | namespace Flurl.Http 10 | { 11 | /// 12 | /// Fluent extension methods for working with HTTP request headers. 13 | /// 14 | public static class HeaderExtensions 15 | { 16 | /// 17 | /// Sets an HTTP header to be sent with this IFlurlRequest or all requests made with this IFlurlClient. 18 | /// 19 | /// The IFlurlClient or IFlurlRequest. 20 | /// HTTP header name. 21 | /// HTTP header value. 22 | /// This IFlurlClient or IFlurlRequest. 23 | public static T WithHeader(this T clientOrRequest, string name, object value) where T : IHttpSettingsContainer { 24 | if (value == null && clientOrRequest.Headers.ContainsKey(name)) 25 | clientOrRequest.Headers.Remove(name); 26 | else if (value != null) 27 | clientOrRequest.Headers[name] = value; 28 | return clientOrRequest; 29 | } 30 | 31 | /// 32 | /// 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. 33 | /// 34 | /// The IFlurlClient or IFlurlRequest. 35 | /// Names/values of HTTP headers to set. Typically an anonymous object or IDictionary. 36 | /// If true, underscores in property names will be replaced by hyphens. Default is true. 37 | /// This IFlurlClient or IFlurlRequest. 38 | public static T WithHeaders(this T clientOrRequest, object headers, bool replaceUnderscoreWithHyphen = true) where T : IHttpSettingsContainer { 39 | if (headers == null) 40 | return clientOrRequest; 41 | 42 | // underscore replacement only applies when object properties are parsed to kv pairs 43 | replaceUnderscoreWithHyphen = replaceUnderscoreWithHyphen && !(headers is string) && !(headers is IEnumerable); 44 | 45 | foreach (var kv in headers.ToKeyValuePairs()) { 46 | var key = replaceUnderscoreWithHyphen ? kv.Key.Replace("_", "-") : kv.Key; 47 | clientOrRequest.WithHeader(key, kv.Value); 48 | } 49 | 50 | return clientOrRequest; 51 | } 52 | 53 | /// 54 | /// Sets HTTP authorization header according to Basic Authentication protocol to be sent with this IFlurlRequest or all requests made with this IFlurlClient. 55 | /// 56 | /// The IFlurlClient or IFlurlRequest. 57 | /// Username of authenticating user. 58 | /// Password of authenticating user. 59 | /// This IFlurlClient or IFlurlRequest. 60 | public static T WithBasicAuth(this T clientOrRequest, string username, string password) where T : IHttpSettingsContainer { 61 | // http://stackoverflow.com/questions/14627399/setting-authorization-header-of-httpclient 62 | var encodedCreds = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}")); 63 | return clientOrRequest.WithHeader("Authorization", $"Basic {encodedCreds}"); 64 | } 65 | 66 | /// 67 | /// 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. 68 | /// 69 | /// The IFlurlClient or IFlurlRequest. 70 | /// The acquired bearer token to pass. 71 | /// This IFlurlClient or IFlurlRequest. 72 | public static T WithOAuthBearerToken(this T clientOrRequest, string token) where T : IHttpSettingsContainer { 73 | return clientOrRequest.WithHeader("Authorization", $"Bearer {token}"); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Test/Flurl.Test/Http/FlurlClientTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net.Http; 4 | using System.Reflection; 5 | using Flurl.Http; 6 | using NUnit.Framework; 7 | 8 | namespace Flurl.Test.Http 9 | { 10 | [TestFixture, Parallelizable] 11 | public class FlurlClientTests 12 | { 13 | [Test] 14 | // check that for every FlurlClient extension method, we have an equivalent Url and string extension 15 | public void extension_methods_consistently_supported() { 16 | var frExts = ReflectionHelper.GetAllExtensionMethods(typeof(FlurlClient).GetTypeInfo().Assembly) 17 | // URL builder methods on IFlurlClient get a free pass. We're looking for things like HTTP calling methods. 18 | .Where(mi => mi.DeclaringType != typeof(UrlBuilderExtensions)) 19 | .ToList(); 20 | var urlExts = ReflectionHelper.GetAllExtensionMethods(typeof(FlurlClient).GetTypeInfo().Assembly).ToList(); 21 | var stringExts = ReflectionHelper.GetAllExtensionMethods(typeof(FlurlClient).GetTypeInfo().Assembly).ToList(); 22 | 23 | Assert.That(frExts.Count > 20, $"IFlurlRequest only has {frExts.Count} extension methods? Something's wrong here."); 24 | 25 | // Url and string should contain all extension methods that IFlurlRequest has 26 | foreach (var method in frExts) { 27 | if (!urlExts.Any(m => ReflectionHelper.AreSameMethodSignatures(method, m))) { 28 | var args = string.Join(", ", method.GetParameters().Select(p => p.ParameterType.Name)); 29 | Assert.Fail($"No equivalent Url extension method found for IFlurlRequest.{method.Name}({args})"); 30 | } 31 | if (!stringExts.Any(m => ReflectionHelper.AreSameMethodSignatures(method, m))) { 32 | var args = string.Join(", ", method.GetParameters().Select(p => p.ParameterType.Name)); 33 | Assert.Fail($"No equivalent string extension method found for IFlurlRequest.{method.Name}({args})"); 34 | } 35 | } 36 | } 37 | 38 | [Test] 39 | public void can_create_request_without_base_url() { 40 | var cli = new FlurlClient(); 41 | var req = cli.Request("http://myapi.com/foo?x=1&y=2#foo"); 42 | Assert.AreEqual("http://myapi.com/foo?x=1&y=2#foo", req.Url.ToString()); 43 | } 44 | 45 | [Test] 46 | public void can_create_request_with_base_url() { 47 | var cli = new FlurlClient("http://myapi.com"); 48 | var req = cli.Request("foo", "bar"); 49 | Assert.AreEqual("http://myapi.com/foo/bar", req.Url.ToString()); 50 | } 51 | 52 | [Test] 53 | public void request_with_full_url_overrides_base_url() { 54 | var cli = new FlurlClient("http://myapi.com"); 55 | var req = cli.Request("http://otherapi.com", "foo"); 56 | Assert.AreEqual("http://otherapi.com/foo", req.Url.ToString()); 57 | } 58 | 59 | [Test] 60 | public void can_create_request_with_base_url_and_no_segments() { 61 | var cli = new FlurlClient("http://myapi.com"); 62 | var req = cli.Request(); 63 | Assert.AreEqual("http://myapi.com", req.Url.ToString()); 64 | } 65 | 66 | [Test] 67 | public void cannot_create_request_without_base_url_or_segments() { 68 | var cli = new FlurlClient(); 69 | Assert.Throws(() => { 70 | var req = cli.Request(); 71 | }); 72 | } 73 | 74 | [Test] 75 | public void cannot_create_request_without_base_url_or_segments_comprising_full_url() { 76 | var cli = new FlurlClient(); 77 | Assert.Throws(() => { 78 | var req = cli.Request("foo", "bar"); 79 | }); 80 | } 81 | 82 | [Test] 83 | public void default_factory_doesnt_reuse_disposed_clients() { 84 | var cli1 = "http://api.com".WithHeader("foo", "1").Client; 85 | var cli2 = "http://api.com".WithHeader("foo", "2").Client; 86 | cli1.Dispose(); 87 | var cli3 = "http://api.com".WithHeader("foo", "3").Client; 88 | 89 | Assert.AreEqual(cli1, cli2); 90 | Assert.IsTrue(cli1.IsDisposed); 91 | Assert.IsTrue(cli2.IsDisposed); 92 | Assert.AreNotEqual(cli1, cli3); 93 | Assert.IsFalse(cli3.IsDisposed); 94 | } 95 | 96 | [Test] 97 | public void can_create_FlurlClient_with_existing_HttpClient() { 98 | var hc = new HttpClient { 99 | BaseAddress = new Uri("http://api.com/"), 100 | Timeout = TimeSpan.FromSeconds(123) 101 | }; 102 | var cli = new FlurlClient(hc); 103 | 104 | Assert.AreEqual("http://api.com/", cli.HttpClient.BaseAddress.ToString()); 105 | Assert.AreEqual(123, cli.HttpClient.Timeout.TotalSeconds); 106 | Assert.AreEqual("http://api.com/", cli.BaseUrl); 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /src/Flurl/QueryParamCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace Flurl 7 | { 8 | /// 9 | /// Represents a URL query as a key-value dictionary. Insertion order is preserved. 10 | /// 11 | public class QueryParamCollection : List 12 | { 13 | /// 14 | /// Returns serialized, encoded query string. Insertion order is preserved. 15 | /// 16 | /// 17 | public override string ToString() { 18 | return ToString(false); 19 | } 20 | 21 | /// 22 | /// Returns serialized, encoded query string. Insertion order is preserved. 23 | /// 24 | /// 25 | public string ToString(bool encodeSpaceAsPlus) { 26 | return string.Join("&", this.Select(p => p.ToString(encodeSpaceAsPlus))); 27 | } 28 | 29 | /// 30 | /// Adds a new query parameter. 31 | /// 32 | public void Add(string key, object value) { 33 | Add(new QueryParameter(key, value)); 34 | } 35 | 36 | /// 37 | /// Adds a new query parameter, allowing you to specify whether the value is already encoded. 38 | /// 39 | public void Add(string key, string value, bool isEncoded) { 40 | Add(new QueryParameter(key, value, isEncoded)); 41 | } 42 | 43 | /// 44 | /// True if the collection contains a query parameter with the given name. 45 | /// 46 | public bool ContainsKey(string name) { 47 | return this.Any(p => p.Name == name); 48 | } 49 | 50 | /// 51 | /// Removes all parameters of the given name. 52 | /// 53 | /// The number of parameters that were removed 54 | /// is null. 55 | public int Remove(string name) { 56 | return RemoveAll(p => p.Name == name); 57 | } 58 | 59 | /// 60 | /// Replaces an existing QueryParameter or appends one to the end. If object is a collection type (array, IEnumerable, etc.), 61 | /// multiple paramters are added, i.e. x=1&x=2. If any of the same name already exist, they are overwritten one by one 62 | /// (preserving order) and any remaining are appended to the end. If fewer values are specified than already exist, 63 | /// remaining existing values are removed. 64 | /// 65 | public void Merge(string name, object value, bool isEncoded, NullValueHandling nullValueHandling) { 66 | if (value == null && nullValueHandling != NullValueHandling.NameOnly) { 67 | if (nullValueHandling == NullValueHandling.Remove) 68 | Remove(name); 69 | return; 70 | } 71 | 72 | // This covers some complex edge cases involving multiple values of the same name. 73 | // example: x has values at positions 2 and 4 in the query string, then we set x to 74 | // an array of 4 values. We want to replace the values at positions 2 and 4 with the 75 | // first 2 values of the new array, then append the remaining 2 values to the end. 76 | var parameters = this.Where(p => p.Name == name).ToArray(); 77 | var values = (!(value is string) && value is IEnumerable en) ? en.Cast().ToArray() : new[] { value }; 78 | 79 | for (int i = 0;; i++) { 80 | if (i < parameters.Length && i < values.Length) { 81 | if (values[i] is QueryParameter qp) 82 | this[IndexOf(parameters[i])] = qp; 83 | else 84 | parameters[i].Value = values[i]; 85 | } 86 | else if (i < parameters.Length) 87 | Remove(parameters[i]); 88 | else if (i < values.Length) { 89 | var qp = values[i] as QueryParameter ?? new QueryParameter(name, values[i], isEncoded); 90 | Add(qp); 91 | } 92 | else 93 | break; 94 | } 95 | } 96 | 97 | /// 98 | /// Gets or sets a query parameter value by name. A query may contain multiple values of the same name 99 | /// (i.e. "x=1&x=2"), in which case the value is an array, which works for both getting and setting. 100 | /// 101 | /// The query parameter name 102 | /// The query parameter value or array of values 103 | public object this[string name] { 104 | get { 105 | var all = this.Where(p => p.Name == name).Select(p => p.Value).ToArray(); 106 | if (all.Length == 0) 107 | return null; 108 | if (all.Length == 1) 109 | return all[0]; 110 | return all; 111 | } 112 | set => Merge(name, value, false, NullValueHandling.Remove); 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /src/Flurl.Http/FlurlHttpException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Dynamic; 3 | using System.Threading.Tasks; 4 | 5 | namespace Flurl.Http 6 | { 7 | /// 8 | /// An exception that is thrown when an HTTP call made by Flurl.Http fails, including when the response 9 | /// indicates an unsuccessful HTTP status code. 10 | /// 11 | public class FlurlHttpException : Exception 12 | { 13 | /// 14 | /// An object containing details about the failed HTTP call 15 | /// 16 | public HttpCall Call { get; } 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// The call. 22 | /// The message. 23 | /// The inner. 24 | public FlurlHttpException(HttpCall call, string message, Exception inner) : base(message, inner) { 25 | Call = call; 26 | } 27 | 28 | /// 29 | /// Initializes a new instance of the class. 30 | /// 31 | /// The call. 32 | /// The inner. 33 | public FlurlHttpException(HttpCall call, Exception inner) : this(call, BuildMessage(call, inner), inner) { } 34 | 35 | /// 36 | /// Initializes a new instance of the class. 37 | /// 38 | /// The call. 39 | public FlurlHttpException(HttpCall call) : this(call, BuildMessage(call, null), null) { } 40 | 41 | private static string BuildMessage(HttpCall call, Exception inner) { 42 | return 43 | (call.Response != null && !call.Succeeded) ? 44 | $"Call failed with status code {(int)call.Response.StatusCode} ({call.Response.ReasonPhrase}): {call}": 45 | $"Call failed. {inner?.Message} {call}"; 46 | } 47 | 48 | /// 49 | /// Gets the response body of the failed call. 50 | /// 51 | /// A task whose result is the string contents of the response body. 52 | public async Task GetResponseStringAsync() { 53 | var task = Call?.Response?.Content?.ReadAsStringAsync(); 54 | return (task == null) ? null : await task.ConfigureAwait(false); 55 | } 56 | 57 | /// 58 | /// Deserializes the JSON response body to an object of the given type. 59 | /// 60 | /// A type whose structure matches the expected JSON response. 61 | /// A task whose result is an object containing data in the response body. 62 | public async Task GetResponseJsonAsync() { 63 | var task = Call?.Response?.Content?.ReadAsStreamAsync(); 64 | if (task == null) return default(T); 65 | var ser = Call.FlurlRequest?.Settings?.JsonSerializer; 66 | if (ser == null) return default(T); 67 | return ser.Deserialize(await task.ConfigureAwait(false)); 68 | } 69 | 70 | /// 71 | /// Deserializes the JSON response body to a dynamic object. 72 | /// 73 | /// A task whose result is an object containing data in the response body. 74 | public async Task GetResponseJsonAsync() => await GetResponseJsonAsync().ConfigureAwait(false); 75 | } 76 | 77 | /// 78 | /// An exception that is thrown when an HTTP call made by Flurl.Http times out. 79 | /// 80 | public class FlurlHttpTimeoutException : FlurlHttpException 81 | { 82 | /// 83 | /// Initializes a new instance of the class. 84 | /// 85 | /// The HttpCall instance. 86 | /// The inner exception. 87 | public FlurlHttpTimeoutException(HttpCall call, Exception inner) : base(call, BuildMessage(call), inner) { } 88 | 89 | private static string BuildMessage(HttpCall call) { 90 | return $"Call timed out: {call}"; 91 | } 92 | } 93 | 94 | /// 95 | /// An exception that is thrown when an HTTP response could not be parsed to a particular format. 96 | /// 97 | public class FlurlParsingException : FlurlHttpException 98 | { 99 | /// 100 | /// Initializes a new instance of the class. 101 | /// 102 | /// The HttpCall instance. 103 | /// The format that could not be parsed to, i.e. JSON. 104 | /// The inner exception. 105 | public FlurlParsingException(HttpCall call, string expectedFormat, Exception inner) : base(call, BuildMessage(call, expectedFormat), inner) { 106 | ExpectedFormat = expectedFormat; 107 | } 108 | 109 | /// 110 | /// The format that could not be parsed to, i.e. JSON. 111 | /// 112 | public string ExpectedFormat { get; } 113 | 114 | private static string BuildMessage(HttpCall call, string expectedFormat) { 115 | return $"Response could not be deserialized to {expectedFormat}: {call}"; 116 | } 117 | } 118 | 119 | } -------------------------------------------------------------------------------- /src/Flurl/Util/CommonExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Globalization; 5 | using System.Linq; 6 | using System.Text.RegularExpressions; 7 | #if !NET40 8 | using System.Reflection; 9 | #endif 10 | 11 | namespace Flurl.Util 12 | { 13 | /// 14 | /// CommonExtensions for objects. 15 | /// 16 | public static class CommonExtensions 17 | { 18 | /// 19 | /// Converts an object's public properties to a collection of string-based key-value pairs. If the object happens 20 | /// to be an IDictionary, the IDictionary's keys and values converted to strings and returned. 21 | /// 22 | /// The object to parse into key-value pairs 23 | /// 24 | /// is . 25 | public static IEnumerable> ToKeyValuePairs(this object obj) { 26 | if (obj == null) 27 | throw new ArgumentNullException(nameof(obj)); 28 | 29 | return 30 | obj is string s ? StringToKV(s) : 31 | obj is IEnumerable e ? CollectionToKV(e) : 32 | ObjectToKV(obj); 33 | } 34 | 35 | /// 36 | /// Returns a string that represents the current object, using CultureInfo.InvariantCulture where possible. 37 | /// Dates are represented in IS0 8601. 38 | /// 39 | public static string ToInvariantString(this object obj) { 40 | // inspired by: http://stackoverflow.com/a/19570016/62600 41 | return 42 | obj == null ? null : 43 | obj is DateTime dt ? dt.ToString("o", CultureInfo.InvariantCulture) : 44 | obj is DateTimeOffset dto ? dto.ToString("o", CultureInfo.InvariantCulture) : 45 | #if !NETSTANDARD1_0 46 | obj is IConvertible c ? c.ToString(CultureInfo.InvariantCulture) : 47 | #endif 48 | obj is IFormattable f ? f.ToString(null, CultureInfo.InvariantCulture) : 49 | obj.ToString(); 50 | } 51 | 52 | /// 53 | /// Splits at the first occurence of the given seperator. 54 | /// 55 | /// The string to split. 56 | /// The separator to split on. 57 | /// Array of at most 2 strings. (1 if separator is not found.) 58 | public static string[] SplitOnFirstOccurence(this string s, char separator) { 59 | // Needed because full PCL profile doesn't support Split(char[], int) (#119) 60 | if (string.IsNullOrEmpty(s)) 61 | return new[] { s }; 62 | 63 | var i = s.IndexOf(separator); 64 | if (i == -1) 65 | return new[] { s }; 66 | 67 | return new[] { s.Substring(0, i), s.Substring(i + 1) }; 68 | } 69 | 70 | private static IEnumerable> StringToKV(string s) { 71 | return Url.ParseQueryParams(s).Select(p => new KeyValuePair(p.Name, p.Value)); 72 | } 73 | 74 | private static IEnumerable> ObjectToKV(object obj) { 75 | #if NETSTANDARD1_0 76 | return from prop in obj.GetType().GetRuntimeProperties() 77 | let val = prop.GetValue(obj, null) 78 | select new KeyValuePair(prop.Name, val); 79 | #else 80 | return from prop in obj.GetType().GetProperties() 81 | let val = prop.GetValue(obj, null) 82 | select new KeyValuePair(prop.Name, val); 83 | #endif 84 | } 85 | 86 | private static IEnumerable> CollectionToKV(IEnumerable col) { 87 | // Accepts KeyValuePairs or any arbitrary types that contain a property called "Key" or "Name" and a property called "Value". 88 | foreach (var item in col) { 89 | if (item == null) 90 | continue; 91 | 92 | string key; 93 | object val; 94 | 95 | var type = item.GetType(); 96 | #if NETSTANDARD1_0 97 | var keyProp = type.GetRuntimeProperty("Key") ?? type.GetRuntimeProperty("key") ?? type.GetRuntimeProperty("Name") ?? type.GetRuntimeProperty("name"); 98 | var valProp = type.GetRuntimeProperty("Value") ?? type.GetRuntimeProperty("value"); 99 | #else 100 | var keyProp = type.GetProperty("Key") ?? type.GetProperty("key") ?? type.GetProperty("Name") ?? type.GetProperty("name"); 101 | var valProp = type.GetProperty("Value") ?? type.GetProperty("value"); 102 | #endif 103 | 104 | if (keyProp != null && valProp != null) { 105 | key = keyProp.GetValue(item, null)?.ToInvariantString(); 106 | val = valProp.GetValue(item, null); 107 | } 108 | else { 109 | key = item.ToInvariantString(); 110 | val = null; 111 | } 112 | 113 | if (key != null) 114 | yield return new KeyValuePair(key, val); 115 | } 116 | } 117 | 118 | /// 119 | /// Merges the key/value pairs from d2 into d1, without overwriting those already set in d1. 120 | /// 121 | public static void Merge(this IDictionary d1, IDictionary d2) { 122 | foreach (var kv in d2.Where(x => !d1.Keys.Contains(x.Key))) 123 | d1.Add(kv); 124 | } 125 | 126 | /// 127 | /// Strips any single quotes or double quotes from the beginning and end of a string. 128 | /// 129 | public static string StripQuotes(this string s) => Regex.Replace(s, "^\\s*['\"]+|['\"]+\\s*$", ""); 130 | } 131 | } -------------------------------------------------------------------------------- /src/Flurl.Http/HttpResponseMessageExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Dynamic; 4 | using System.IO; 5 | using System.Net.Http; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Flurl.Util; 9 | 10 | namespace Flurl.Http 11 | { 12 | /// 13 | /// Async extension methods that can be chained off Task<HttpResponseMessage>, avoiding nested awaits. 14 | /// 15 | public static class HttpResponseMessageExtensions 16 | { 17 | /// 18 | /// Deserializes JSON-formatted HTTP response body to object of type T. Intended to chain off an async HTTP. 19 | /// 20 | /// A type whose structure matches the expected JSON response. 21 | /// A Task whose result is an object containing data in the response body. 22 | /// x = await url.PostAsync(data).ReceiveJson<T>() 23 | /// Condition. 24 | public static async Task ReceiveJson(this Task response) { 25 | var resp = await response.ConfigureAwait(false); 26 | if (resp == null) return default(T); 27 | 28 | var call = resp.RequestMessage.GetHttpCall(); 29 | using (var stream = await resp.Content.ReadAsStreamAsync().ConfigureAwait(false)) { 30 | try { 31 | return call.FlurlRequest.Settings.JsonSerializer.Deserialize(stream); 32 | } 33 | catch (Exception ex) { 34 | call.Exception = new FlurlParsingException(call, "JSON", ex); 35 | await FlurlRequest.HandleExceptionAsync(call, call.Exception, CancellationToken.None).ConfigureAwait(false); 36 | return default(T); 37 | } 38 | } 39 | } 40 | 41 | /// 42 | /// Deserializes JSON-formatted HTTP response body to a dynamic object. Intended to chain off an async call. 43 | /// 44 | /// A Task whose result is a dynamic object containing data in the response body. 45 | /// d = await url.PostAsync(data).ReceiveJson() 46 | /// Condition. 47 | public static async Task ReceiveJson(this Task response) { 48 | return await response.ReceiveJson().ConfigureAwait(false); 49 | } 50 | 51 | /// 52 | /// Deserializes JSON-formatted HTTP response body to a list of dynamic objects. Intended to chain off an async call. 53 | /// 54 | /// A Task whose result is a list of dynamic objects containing data in the response body. 55 | /// d = await url.PostAsync(data).ReceiveJsonList() 56 | /// Condition. 57 | public static async Task> ReceiveJsonList(this Task response) { 58 | dynamic[] d = await response.ReceiveJson().ConfigureAwait(false); 59 | return d; 60 | } 61 | 62 | /// 63 | /// Returns HTTP response body as a string. Intended to chain off an async call. 64 | /// 65 | /// A Task whose result is the response body as a string. 66 | /// s = await url.PostAsync(data).ReceiveString() 67 | public static async Task ReceiveString(this Task response) { 68 | #if NETSTANDARD1_3 || NETSTANDARD2_0 69 | // https://stackoverflow.com/questions/46119872/encoding-issues-with-net-core-2 (#86) 70 | System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); 71 | #endif 72 | var resp = await response.ConfigureAwait(false); 73 | if (resp == null) return null; 74 | 75 | return await resp.Content.StripCharsetQuotes().ReadAsStringAsync().ConfigureAwait(false); 76 | } 77 | 78 | /// 79 | /// Returns HTTP response body as a stream. Intended to chain off an async call. 80 | /// 81 | /// A Task whose result is the response body as a stream. 82 | /// stream = await url.PostAsync(data).ReceiveStream() 83 | public static async Task ReceiveStream(this Task response) { 84 | var resp = await response.ConfigureAwait(false); 85 | if (resp == null) return null; 86 | 87 | return await resp.Content.ReadAsStreamAsync().ConfigureAwait(false); 88 | } 89 | 90 | /// 91 | /// Returns HTTP response body as a byte array. Intended to chain off an async call. 92 | /// 93 | /// A Task whose result is the response body as a byte array. 94 | /// bytes = await url.PostAsync(data).ReceiveBytes() 95 | public static async Task ReceiveBytes(this Task response) { 96 | var resp = await response.ConfigureAwait(false); 97 | if (resp == null) return null; 98 | 99 | return await resp.Content.ReadAsByteArrayAsync().ConfigureAwait(false); 100 | } 101 | 102 | // https://github.com/tmenier/Flurl/pull/76, https://github.com/dotnet/corefx/issues/5014 103 | internal static HttpContent StripCharsetQuotes(this HttpContent content) { 104 | var header = content?.Headers?.ContentType; 105 | if (header?.CharSet != null) 106 | header.CharSet = header.CharSet.StripQuotes(); 107 | return content; 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /src/Flurl.Http.CodeGen/UrlExtensionMethod.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Flurl.Http.CodeGen 6 | { 7 | /// 8 | /// Extension methods manually defined on IFlurlRequest and IFlurlClient. We'll auto-gen overloads for Url and string. 9 | /// 10 | public class UrlExtensionMethod 11 | { 12 | public static IEnumerable GetAll() { 13 | // header extensions 14 | yield return new UrlExtensionMethod("WithHeader", "Creates a new FlurlRequest with the URL and sets a request header.") 15 | .AddParam("name", "string", "The header name.") 16 | .AddParam("value", "object", "The header value."); 17 | 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") 18 | .AddParam("headers", "object", "Names/values of HTTP headers to set. Typically an anonymous object or IDictionary.") 19 | .AddParam("replaceUnderscoreWithHyphen", "bool", "If true, underscores in property names will be replaced by hyphens. Default is true.", "true"); 20 | yield return new UrlExtensionMethod("WithBasicAuth", "Creates a new FlurlRequest with the URL and sets the Authorization header according to Basic Authentication protocol.") 21 | .AddParam("username", "string", "Username of authenticating user.") 22 | .AddParam("password", "string", "Password of authenticating user."); 23 | 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.") 24 | .AddParam("token", "string", "The acquired oAuth bearer token."); 25 | 26 | // cookie extensions 27 | 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."); 28 | yield return new UrlExtensionMethod("WithCookie", "Creates a new FlurlRequest with the URL and sets an HTTP cookie to be sent") 29 | .AddParam("cookie", "Cookie", ""); 30 | yield return new UrlExtensionMethod("WithCookie", "Creates a new FlurlRequest with the URL and sets an HTTP cookie to be sent.") 31 | .AddParam("name", "string", "The cookie name.") 32 | .AddParam("value", "object", "The cookie value.") 33 | .AddParam("expires", "DateTime?", "The cookie expiration (optional). If excluded, cookie only lives for duration of session.", "null"); 34 | 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.") 35 | .AddParam("cookies", "object", "Names/values of HTTP cookies to set. Typically an anonymous object or IDictionary.") 36 | .AddParam("expires", "DateTime?", "Expiration for all cookies (optional). If excluded, cookies only live for duration of session.", "null"); 37 | 38 | // settings extensions 39 | yield return new UrlExtensionMethod("ConfigureRequest", "Creates a new FlurlRequest with the URL and allows changing its Settings inline.") 40 | .AddParam("action", "Action", "A delegate defining the Settings changes."); 41 | yield return new UrlExtensionMethod("WithTimeout", "Creates a new FlurlRequest with the URL and sets the request timeout.") 42 | .AddParam("timespan", "TimeSpan", "Time to wait before the request times out."); 43 | yield return new UrlExtensionMethod("WithTimeout", "Creates a new FlurlRequest with the URL and sets the request timeout.") 44 | .AddParam("seconds", "int", "Seconds to wait before the request times out."); 45 | 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.") 46 | .AddParam("pattern", "string", "Examples: \"3xx\", \"100,300,600\", \"100-299,6xx\""); 47 | 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.") 48 | .AddParam("statusCodes", "params HttpStatusCode[]", "The HttpStatusCode(s) to allow."); 49 | 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."); 50 | } 51 | 52 | public string Name { get; } 53 | public string Description { get; } 54 | public IList Params { get; } = new List(); 55 | 56 | public UrlExtensionMethod(string name, string description) { 57 | Name = name; 58 | Description = description; 59 | } 60 | 61 | public UrlExtensionMethod AddParam(string name, string type, string description, string defaultVal = null) { 62 | Params.Add(new Param { Name = name, Type = type, Description = description, Default = defaultVal }); 63 | return this; 64 | } 65 | 66 | public class Param 67 | { 68 | public string Name { get; set; } 69 | public string Type { get; set; } 70 | public string Description { get; set; } 71 | public string Default { get; set; } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Test/Flurl.Test/Http/GetTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Flurl.Http; 8 | using NUnit.Framework; 9 | 10 | namespace Flurl.Test.Http 11 | { 12 | [TestFixture, Parallelizable] 13 | public class GetTests : HttpMethodTests 14 | { 15 | public GetTests() : base(HttpMethod.Get) { } 16 | 17 | protected override Task CallOnString(string url) => url.GetAsync(); 18 | protected override Task CallOnUrl(Url url) => url.GetAsync(); 19 | protected override Task CallOnFlurlRequest(IFlurlRequest req) => req.GetAsync(); 20 | 21 | [Test] 22 | public async Task can_get_json() { 23 | HttpTest.RespondWithJson(new TestData { id = 1, name = "Frank" }); 24 | 25 | var data = await "http://some-api.com".GetJsonAsync(); 26 | 27 | Assert.AreEqual(1, data.id); 28 | Assert.AreEqual("Frank", data.name); 29 | } 30 | 31 | [Test] 32 | public async Task can_get_json_dynamic() { 33 | HttpTest.RespondWithJson(new { id = 1, name = "Frank" }); 34 | 35 | var data = await "http://some-api.com".GetJsonAsync(); 36 | 37 | Assert.AreEqual(1, data.id); 38 | Assert.AreEqual("Frank", data.name); 39 | } 40 | 41 | [Test] 42 | public async Task can_get_json_dynamic_list() { 43 | HttpTest.RespondWithJson(new[] { 44 | new { id = 1, name = "Frank" }, 45 | new { id = 2, name = "Claire" } 46 | }); 47 | 48 | var data = await "http://some-api.com".GetJsonListAsync(); 49 | 50 | Assert.AreEqual(1, data[0].id); 51 | Assert.AreEqual("Frank", data[0].name); 52 | Assert.AreEqual(2, data[1].id); 53 | Assert.AreEqual("Claire", data[1].name); 54 | } 55 | 56 | [Test] 57 | public async Task can_get_string() { 58 | HttpTest.RespondWith("good job"); 59 | 60 | var data = await "http://some-api.com".GetStringAsync(); 61 | 62 | Assert.AreEqual("good job", data); 63 | } 64 | 65 | [Test] 66 | public async Task can_get_stream() { 67 | HttpTest.RespondWith("good job"); 68 | 69 | var data = await "http://some-api.com".GetStreamAsync(); 70 | 71 | Assert.AreEqual(new MemoryStream(Encoding.UTF8.GetBytes("good job")), data); 72 | } 73 | 74 | [Test] 75 | public async Task can_get_bytes() { 76 | HttpTest.RespondWith("good job"); 77 | 78 | var data = await "http://some-api.com".GetBytesAsync(); 79 | 80 | Assert.AreEqual(Encoding.UTF8.GetBytes("good job"), data); 81 | } 82 | 83 | [Test] 84 | public async Task failure_throws_detailed_exception() { 85 | HttpTest.RespondWith("bad job", status: 500); 86 | 87 | try { 88 | await "http://api.com".GetStringAsync(); 89 | Assert.Fail("FlurlHttpException was not thrown!"); 90 | } 91 | catch (FlurlHttpException ex) { 92 | Assert.AreEqual("http://api.com/", ex.Call.Request.RequestUri.AbsoluteUri); 93 | Assert.AreEqual(HttpMethod.Get, ex.Call.Request.Method); 94 | Assert.AreEqual(HttpStatusCode.InternalServerError, ex.Call.Response.StatusCode); 95 | Assert.AreEqual("bad job", await ex.GetResponseStringAsync()); 96 | } 97 | } 98 | 99 | [Test] 100 | public async Task can_get_error_json_typed() { 101 | HttpTest.RespondWithJson(new { code = 999, message = "our server crashed" }, 500); 102 | 103 | try { 104 | await "http://api.com".GetStringAsync(); 105 | } 106 | catch (FlurlHttpException ex) { 107 | var error = await ex.GetResponseJsonAsync(); 108 | Assert.IsNotNull(error); 109 | Assert.AreEqual(999, error.code); 110 | Assert.AreEqual("our server crashed", error.message); 111 | } 112 | } 113 | 114 | [Test] 115 | public async Task can_get_error_json_untyped() { 116 | HttpTest.RespondWithJson(new { code = 999, message = "our server crashed" }, 500); 117 | 118 | try { 119 | await "http://api.com".GetStringAsync(); 120 | } 121 | catch (FlurlHttpException ex) { 122 | var error = await ex.GetResponseJsonAsync(); // error is a dynamic this time 123 | Assert.IsNotNull(error); 124 | Assert.AreEqual(999, error.code); 125 | Assert.AreEqual("our server crashed", error.message); 126 | } 127 | } 128 | 129 | [Test] 130 | public async Task can_get_null_json_when_timeout_and_exception_handled() { 131 | HttpTest.SimulateTimeout(); 132 | var data = await "http://api.com" 133 | .ConfigureRequest(c => c.OnError = call => call.ExceptionHandled = true) 134 | .GetJsonAsync(); 135 | Assert.IsNull(data); 136 | } 137 | 138 | // https://github.com/tmenier/Flurl/pull/76 139 | // quotes around charset value is technically legal but there's a bug in .NET we want to avoid: https://github.com/dotnet/corefx/issues/5014 140 | [Test] 141 | public async Task can_get_string_with_quoted_charset_header() { 142 | var content = new StringContent("foo"); 143 | content.Headers.Clear(); 144 | content.Headers.Add("Content-Type", "text/javascript; charset=\"UTF-8\""); 145 | HttpTest.RespondWith(content); 146 | 147 | var resp = await "http://api.com".GetStringAsync(); // without StripCharsetQuotes, this fails 148 | Assert.AreEqual("foo", resp); 149 | } 150 | 151 | private class TestData 152 | { 153 | public int id { get; set; } 154 | public string name { get; set; } 155 | } 156 | 157 | private class TestError 158 | { 159 | public int code { get; set; } 160 | public string message { get; set; } 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Flurl.Http/SettingsExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net; 4 | using Flurl.Http.Configuration; 5 | 6 | namespace Flurl.Http 7 | { 8 | /// 9 | /// Fluent extension methods for tweaking FlurlHttpSettings 10 | /// 11 | public static class SettingsExtensions 12 | { 13 | /// 14 | /// Change FlurlHttpSettings for this IFlurlClient. 15 | /// 16 | /// The IFlurlClient. 17 | /// Action defining the settings changes. 18 | /// The IFlurlClient with the modified Settings 19 | public static IFlurlClient Configure(this IFlurlClient client, Action action) { 20 | action(client.Settings); 21 | return client; 22 | } 23 | 24 | /// 25 | /// Change FlurlHttpSettings for this IFlurlRequest. 26 | /// 27 | /// The IFlurlRequest. 28 | /// Action defining the settings changes. 29 | /// The IFlurlRequest with the modified Settings 30 | public static IFlurlRequest ConfigureRequest(this IFlurlRequest request, Action action) { 31 | action(request.Settings); 32 | return request; 33 | } 34 | 35 | /// 36 | /// Fluently specify the IFlurlClient to use with this IFlurlRequest. 37 | /// 38 | /// The IFlurlRequest. 39 | /// The IFlurlClient to use when sending the request. 40 | /// A new IFlurlRequest to use in calling the Url 41 | public static IFlurlRequest WithClient(this IFlurlRequest request, IFlurlClient client) { 42 | request.Client = client; 43 | return request; 44 | } 45 | 46 | /// 47 | /// Fluently returns a new IFlurlRequest that can be used to call this Url with the given client. 48 | /// 49 | /// 50 | /// The IFlurlClient to use to call the Url. 51 | /// A new IFlurlRequest to use in calling the Url 52 | public static IFlurlRequest WithClient(this Url url, IFlurlClient client) { 53 | return client.Request(url); 54 | } 55 | 56 | /// 57 | /// Fluently returns a new IFlurlRequest that can be used to call this Url with the given client. 58 | /// 59 | /// 60 | /// The IFlurlClient to use to call the Url. 61 | /// A new IFlurlRequest to use in calling the Url 62 | public static IFlurlRequest WithClient(this string url, IFlurlClient client) { 63 | return client.Request(url); 64 | } 65 | 66 | /// 67 | /// Sets the timeout for this IFlurlRequest or all requests made with this IFlurlClient. 68 | /// 69 | /// The IFlurlClient or IFlurlRequest. 70 | /// Time to wait before the request times out. 71 | /// This IFlurlClient or IFlurlRequest. 72 | public static T WithTimeout(this T obj, TimeSpan timespan) where T : IHttpSettingsContainer { 73 | obj.Settings.Timeout = timespan; 74 | return obj; 75 | } 76 | 77 | /// 78 | /// Sets the timeout for this IFlurlRequest or all requests made with this IFlurlClient. 79 | /// 80 | /// The IFlurlClient or IFlurlRequest. 81 | /// Seconds to wait before the request times out. 82 | /// This IFlurlClient or IFlurlRequest. 83 | public static T WithTimeout(this T obj, int seconds) where T : IHttpSettingsContainer { 84 | obj.Settings.Timeout = TimeSpan.FromSeconds(seconds); 85 | return obj; 86 | } 87 | 88 | /// 89 | /// 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. 90 | /// 91 | /// The IFlurlClient or IFlurlRequest. 92 | /// Examples: "3xx", "100,300,600", "100-299,6xx" 93 | /// This IFlurlClient or IFlurlRequest. 94 | public static T AllowHttpStatus(this T obj, string pattern) where T : IHttpSettingsContainer { 95 | if (!string.IsNullOrWhiteSpace(pattern)) { 96 | var current = obj.Settings.AllowedHttpStatusRange; 97 | if (string.IsNullOrWhiteSpace(current)) 98 | obj.Settings.AllowedHttpStatusRange = pattern; 99 | else 100 | obj.Settings.AllowedHttpStatusRange += "," + pattern; 101 | } 102 | return obj; 103 | } 104 | 105 | /// 106 | /// Adds an which (in addition to 2xx) will NOT result in a FlurlHttpException being thrown. 107 | /// 108 | /// The IFlurlClient or IFlurlRequest. 109 | /// Examples: HttpStatusCode.NotFound 110 | /// This IFlurlClient or IFlurlRequest. 111 | public static T AllowHttpStatus(this T obj, params HttpStatusCode[] statusCodes) where T : IHttpSettingsContainer { 112 | var pattern = string.Join(",", statusCodes.Select(c => (int)c)); 113 | return AllowHttpStatus(obj, pattern); 114 | } 115 | 116 | /// 117 | /// Prevents a FlurlHttpException from being thrown on any completed response, regardless of the HTTP status code. 118 | /// 119 | /// This IFlurlClient or IFlurlRequest. 120 | public static T AllowAnyHttpStatus(this T obj) where T : IHttpSettingsContainer { 121 | obj.Settings.AllowedHttpStatusRange = "*"; 122 | return obj; 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /Flurl.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2036 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flurl", "src\Flurl\Flurl.csproj", "{117B6C6E-53F9-45AE-9439-F4FB7E21B116}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B82E8094-AFA9-466E-9E60-473B7B89AFE2}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flurl.Http", "src\Flurl.Http\Flurl.Http.csproj", "{D7AC6172-73DD-468D-955A-3562F2BE303B}" 11 | ProjectSection(ProjectDependencies) = postProject 12 | {C0BC328E-BA47-467A-8893-91DBA8A24ACD} = {C0BC328E-BA47-467A-8893-91DBA8A24ACD} 13 | EndProjectSection 14 | EndProject 15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flurl.Http.CodeGen", "src\Flurl.Http.CodeGen\Flurl.Http.CodeGen.csproj", "{C0BC328E-BA47-467A-8893-91DBA8A24ACD}" 16 | EndProject 17 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{86A5ACB4-F3B3-4395-A5D5-924C9F35F628}" 18 | EndProject 19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flurl.Test", "Test\Flurl.Test\Flurl.Test.csproj", "{DF68EB0E-9566-4577-B709-291520383F8D}" 20 | EndProject 21 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PackageTesters", "PackageTesters", "{9A136878-A43E-4154-9B5E-EDAF27E8628D}" 22 | EndProject 23 | Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "PackageTester.Shared", "PackageTesters\PackageTester.Shared\PackageTester.Shared.shproj", "{D4717AA7-5549-4BAD-81C5-406844A12990}" 24 | EndProject 25 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PackageTester.NET461", "PackageTesters\PackageTester.NET461\PackageTester.NET461.csproj", "{84FB572A-8B77-4B09-B825-2A240BCE1B7A}" 26 | EndProject 27 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PackageTester.NETCore", "PackageTesters\PackageTester.NETCore\PackageTester.NETCore.csproj", "{0231607B-9CA3-4277-9F19-9925694D22E0}" 28 | EndProject 29 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PackageTester.NET45", "PackageTesters\PackageTester.NET45\PackageTester.NET45.csproj", "{AA8792B6-E0FA-46BA-BA03-C7971745F577}" 30 | EndProject 31 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{B6BF9238-4541-4E1F-955E-C95F1C2A1F46}" 32 | ProjectSection(SolutionItems) = preProject 33 | appveyor.yml = appveyor.yml 34 | Build\build.cmd = Build\build.cmd 35 | Build\test.cmd = Build\test.cmd 36 | EndProjectSection 37 | EndProject 38 | Global 39 | GlobalSection(SharedMSBuildProjectFiles) = preSolution 40 | PackageTesters\PackageTester.Shared\PackageTester.Shared.projitems*{84fb572a-8b77-4b09-b825-2a240bce1b7a}*SharedItemsImports = 4 41 | PackageTesters\PackageTester.Shared\PackageTester.Shared.projitems*{aa8792b6-e0fa-46ba-ba03-c7971745f577}*SharedItemsImports = 4 42 | PackageTesters\PackageTester.Shared\PackageTester.Shared.projitems*{d4717aa7-5549-4bad-81c5-406844a12990}*SharedItemsImports = 13 43 | EndGlobalSection 44 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 45 | Debug|Any CPU = Debug|Any CPU 46 | Release|Any CPU = Release|Any CPU 47 | EndGlobalSection 48 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 49 | {117B6C6E-53F9-45AE-9439-F4FB7E21B116}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {117B6C6E-53F9-45AE-9439-F4FB7E21B116}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {117B6C6E-53F9-45AE-9439-F4FB7E21B116}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {117B6C6E-53F9-45AE-9439-F4FB7E21B116}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {D7AC6172-73DD-468D-955A-3562F2BE303B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {D7AC6172-73DD-468D-955A-3562F2BE303B}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {D7AC6172-73DD-468D-955A-3562F2BE303B}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {D7AC6172-73DD-468D-955A-3562F2BE303B}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {C0BC328E-BA47-467A-8893-91DBA8A24ACD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 58 | {C0BC328E-BA47-467A-8893-91DBA8A24ACD}.Debug|Any CPU.Build.0 = Debug|Any CPU 59 | {C0BC328E-BA47-467A-8893-91DBA8A24ACD}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {C0BC328E-BA47-467A-8893-91DBA8A24ACD}.Release|Any CPU.Build.0 = Release|Any CPU 61 | {DF68EB0E-9566-4577-B709-291520383F8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 62 | {DF68EB0E-9566-4577-B709-291520383F8D}.Debug|Any CPU.Build.0 = Debug|Any CPU 63 | {DF68EB0E-9566-4577-B709-291520383F8D}.Release|Any CPU.ActiveCfg = Release|Any CPU 64 | {DF68EB0E-9566-4577-B709-291520383F8D}.Release|Any CPU.Build.0 = Release|Any CPU 65 | {84FB572A-8B77-4B09-B825-2A240BCE1B7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 66 | {84FB572A-8B77-4B09-B825-2A240BCE1B7A}.Debug|Any CPU.Build.0 = Debug|Any CPU 67 | {84FB572A-8B77-4B09-B825-2A240BCE1B7A}.Release|Any CPU.ActiveCfg = Release|Any CPU 68 | {84FB572A-8B77-4B09-B825-2A240BCE1B7A}.Release|Any CPU.Build.0 = Release|Any CPU 69 | {0231607B-9CA3-4277-9F19-9925694D22E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 70 | {0231607B-9CA3-4277-9F19-9925694D22E0}.Debug|Any CPU.Build.0 = Debug|Any CPU 71 | {0231607B-9CA3-4277-9F19-9925694D22E0}.Release|Any CPU.ActiveCfg = Release|Any CPU 72 | {0231607B-9CA3-4277-9F19-9925694D22E0}.Release|Any CPU.Build.0 = Release|Any CPU 73 | {AA8792B6-E0FA-46BA-BA03-C7971745F577}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 74 | {AA8792B6-E0FA-46BA-BA03-C7971745F577}.Debug|Any CPU.Build.0 = Debug|Any CPU 75 | {AA8792B6-E0FA-46BA-BA03-C7971745F577}.Release|Any CPU.ActiveCfg = Release|Any CPU 76 | {AA8792B6-E0FA-46BA-BA03-C7971745F577}.Release|Any CPU.Build.0 = Release|Any CPU 77 | EndGlobalSection 78 | GlobalSection(SolutionProperties) = preSolution 79 | HideSolutionNode = FALSE 80 | EndGlobalSection 81 | GlobalSection(NestedProjects) = preSolution 82 | {117B6C6E-53F9-45AE-9439-F4FB7E21B116} = {B82E8094-AFA9-466E-9E60-473B7B89AFE2} 83 | {D7AC6172-73DD-468D-955A-3562F2BE303B} = {B82E8094-AFA9-466E-9E60-473B7B89AFE2} 84 | {C0BC328E-BA47-467A-8893-91DBA8A24ACD} = {B82E8094-AFA9-466E-9E60-473B7B89AFE2} 85 | {DF68EB0E-9566-4577-B709-291520383F8D} = {86A5ACB4-F3B3-4395-A5D5-924C9F35F628} 86 | {D4717AA7-5549-4BAD-81C5-406844A12990} = {9A136878-A43E-4154-9B5E-EDAF27E8628D} 87 | {84FB572A-8B77-4B09-B825-2A240BCE1B7A} = {9A136878-A43E-4154-9B5E-EDAF27E8628D} 88 | {0231607B-9CA3-4277-9F19-9925694D22E0} = {9A136878-A43E-4154-9B5E-EDAF27E8628D} 89 | {AA8792B6-E0FA-46BA-BA03-C7971745F577} = {9A136878-A43E-4154-9B5E-EDAF27E8628D} 90 | EndGlobalSection 91 | GlobalSection(ExtensibilityGlobals) = postSolution 92 | SolutionGuid = {61289482-AC5A-44E1-AEA1-76A3F3CCB6A4} 93 | EndGlobalSection 94 | EndGlobal 95 | -------------------------------------------------------------------------------- /src/Flurl.Http/Content/CapturedMultipartContent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Net.Http.Headers; 6 | using System.Text; 7 | using Flurl.Http.Configuration; 8 | using Flurl.Util; 9 | 10 | namespace Flurl.Http.Content 11 | { 12 | /// 13 | /// Provides HTTP content for a multipart/form-data request. 14 | /// 15 | public class CapturedMultipartContent : MultipartContent 16 | { 17 | private readonly FlurlHttpSettings _settings; 18 | 19 | /// 20 | /// Gets an array of HttpContent objects that make up the parts of the multipart request. 21 | /// 22 | public HttpContent[] Parts => this.ToArray(); 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | /// The FlurlHttpSettings used to serialize each content part. (Defaults to FlurlHttp.GlobalSettings.) 28 | public CapturedMultipartContent(FlurlHttpSettings settings = null) : base("form-data") { 29 | _settings = settings ?? FlurlHttp.GlobalSettings; 30 | } 31 | 32 | /// 33 | /// Add a content part to the multipart request. 34 | /// 35 | /// The control name of the part. 36 | /// The HttpContent of the part. 37 | /// This CapturedMultipartContent instance (supports method chaining). 38 | public CapturedMultipartContent Add(string name, HttpContent content) { 39 | return AddInternal(name, content, null); 40 | } 41 | 42 | /// 43 | /// Add a simple string part to the multipart request. 44 | /// 45 | /// The control name of the part. 46 | /// The string content of the part. 47 | /// The encoding of the part. 48 | /// The media type of the part. 49 | /// This CapturedMultipartContent instance (supports method chaining). 50 | public CapturedMultipartContent AddString(string name, string content, Encoding encoding = null, string mediaType = null) { 51 | return Add(name, new CapturedStringContent(content, encoding, mediaType)); 52 | } 53 | 54 | /// 55 | /// Add multiple string parts to the multipart request by parsing an object's properties into control name/content pairs. 56 | /// 57 | /// The object (typically anonymous) whose properties are parsed into control name/content pairs. 58 | /// The encoding of the parts. 59 | /// The media type of the parts. 60 | /// This CapturedMultipartContent instance (supports method chaining). 61 | public CapturedMultipartContent AddStringParts(object data, Encoding encoding = null, string mediaType = null) { 62 | foreach (var kv in data.ToKeyValuePairs()) { 63 | if (kv.Value == null) 64 | continue; 65 | AddString(kv.Key, kv.Value.ToInvariantString(), encoding, mediaType); 66 | } 67 | return this; 68 | } 69 | 70 | /// 71 | /// Add a JSON-serialized part to the multipart request. 72 | /// 73 | /// The control name of the part. 74 | /// The content of the part, which will be serialized to JSON. 75 | /// This CapturedMultipartContent instance (supports method chaining). 76 | public CapturedMultipartContent AddJson(string name, object data) { 77 | return Add(name, new CapturedJsonContent(_settings.JsonSerializer.Serialize(data))); 78 | } 79 | 80 | /// 81 | /// Add a URL-encoded part to the multipart request. 82 | /// 83 | /// The control name of the part. 84 | /// The content of the part, whose properties will be parsed and serialized to URL-encoded format. 85 | /// This CapturedMultipartContent instance (supports method chaining). 86 | public CapturedMultipartContent AddUrlEncoded(string name, object data) { 87 | return Add(name, new CapturedUrlEncodedContent(_settings.UrlEncodedSerializer.Serialize(data))); 88 | } 89 | 90 | /// 91 | /// Adds a file to the multipart request from a stream. 92 | /// 93 | /// The control name of the part. 94 | /// The file stream to send. 95 | /// The filename, added to the Content-Disposition header of the part. 96 | /// The media type of the file. 97 | /// The buffer size of the stream upload in bytes. Defaults to 4096. 98 | /// This CapturedMultipartContent instance (supports method chaining). 99 | public CapturedMultipartContent AddFile(string name, Stream stream, string fileName, string mediaType = null, int bufferSize = 4096) { 100 | var content = new StreamContent(stream, bufferSize); 101 | if (mediaType != null) 102 | content.Headers.ContentType = new MediaTypeHeaderValue(mediaType); 103 | return AddInternal(name, content, fileName); 104 | } 105 | 106 | /// 107 | /// Adds a file to the multipart request from a local path. 108 | /// 109 | /// The control name of the part. 110 | /// The local path to the file. 111 | /// The media type of the file. 112 | /// The buffer size of the stream upload in bytes. Defaults to 4096. 113 | /// This CapturedMultipartContent instance (supports method chaining). 114 | public CapturedMultipartContent AddFile(string name, string path, string mediaType = null, int bufferSize = 4096) { 115 | var fileName = FileUtil.GetFileName(path); 116 | var content = new FileContent(path, bufferSize); 117 | if (mediaType != null) 118 | content.Headers.ContentType = new MediaTypeHeaderValue(mediaType); 119 | return AddInternal(name, content, fileName); 120 | } 121 | 122 | private CapturedMultipartContent AddInternal(string name, HttpContent content, string fileName) { 123 | if (string.IsNullOrWhiteSpace(name)) 124 | throw new ArgumentException("name must not be empty", nameof(name)); 125 | 126 | content.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") { 127 | Name = name, 128 | FileName = fileName, 129 | FileNameStar = fileName 130 | }; 131 | base.Add(content); 132 | return this; 133 | } 134 | } 135 | } -------------------------------------------------------------------------------- /src/Flurl.Http/FlurlClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Linq; 6 | using Flurl.Http.Configuration; 7 | using Flurl.Http.Testing; 8 | using Flurl.Util; 9 | 10 | namespace Flurl.Http 11 | { 12 | /// 13 | /// Interface defining FlurlClient's contract (useful for mocking and DI) 14 | /// 15 | public interface IFlurlClient : IHttpSettingsContainer, IDisposable { 16 | /// 17 | /// Gets or sets the FlurlHttpSettings object used by this client. 18 | /// 19 | new ClientFlurlHttpSettings Settings { get; set; } 20 | 21 | /// 22 | /// Gets the HttpClient to be used in subsequent HTTP calls. Creation (when necessary) is delegated 23 | /// to FlurlHttp.FlurlClientFactory. Reused for the life of the FlurlClient. 24 | /// 25 | HttpClient HttpClient { get; } 26 | 27 | /// 28 | /// Gets the HttpMessageHandler to be used in subsequent HTTP calls. Creation (when necessary) is delegated 29 | /// to FlurlHttp.FlurlClientFactory. 30 | /// 31 | HttpMessageHandler HttpMessageHandler { get; } 32 | 33 | /// 34 | /// Gets or sets base URL associated with this client. 35 | /// 36 | string BaseUrl { get; set; } 37 | 38 | /// 39 | /// Instantiates a new IFlurClient, optionally appending path segments to the BaseUrl. 40 | /// 41 | /// The URL or URL segments for the request. If BaseUrl is defined, it is assumed that these are path segments off that base. 42 | /// A new IFlurlRequest 43 | IFlurlRequest Request(params object[] urlSegments); 44 | 45 | /// 46 | /// Checks whether the connection lease timeout (as specified in Settings.ConnectionLeaseTimeout) has passed since 47 | /// connection was opened. If it has, resets the interval and returns true. 48 | /// 49 | bool CheckAndRenewConnectionLease(); 50 | 51 | /// 52 | /// Gets a value indicating whether this instance (and its underlying HttpClient) has been disposed. 53 | /// 54 | bool IsDisposed { get; } 55 | } 56 | 57 | /// 58 | /// A reusable object for making HTTP calls. 59 | /// 60 | public class FlurlClient : IFlurlClient 61 | { 62 | private ClientFlurlHttpSettings _settings; 63 | private readonly Lazy _httpClient; 64 | private readonly Lazy _httpMessageHandler; 65 | 66 | /// 67 | /// Initializes a new instance of the class. 68 | /// 69 | /// The base URL associated with this client. 70 | public FlurlClient(string baseUrl = null) { 71 | BaseUrl = baseUrl; 72 | _httpClient = new Lazy(() => Settings.HttpClientFactory.CreateHttpClient(HttpMessageHandler)); 73 | _httpMessageHandler = new Lazy(() => Settings.HttpClientFactory.CreateMessageHandler()); 74 | } 75 | 76 | /// 77 | /// Initializes a new instance of the class, wrapping an existing HttpClient. 78 | /// Generally you should let Flurl create and manage HttpClient instances for you, but you might, for 79 | /// example, have an HttpClient instance that was created by a 3rd-party library and you want to use 80 | /// Flurl to build and send calls with it. 81 | /// 82 | /// The instantiated HttpClient instance. 83 | public FlurlClient(HttpClient httpClient) { 84 | if (httpClient == null) 85 | throw new ArgumentNullException(nameof(httpClient)); 86 | 87 | BaseUrl = httpClient.BaseAddress?.ToString(); 88 | _httpClient = new Lazy(() => httpClient); 89 | } 90 | 91 | /// 92 | public string BaseUrl { get; set; } 93 | 94 | /// 95 | public ClientFlurlHttpSettings Settings { 96 | get => _settings ?? (_settings = new ClientFlurlHttpSettings()); 97 | set => _settings = value; 98 | } 99 | 100 | /// 101 | public IDictionary Headers { get; } = new Dictionary(); 102 | 103 | /// 104 | public IDictionary Cookies { get; } = new Dictionary(); 105 | 106 | /// 107 | public HttpClient HttpClient => HttpTest.Current?.HttpClient ?? _httpClient.Value; 108 | 109 | /// 110 | public HttpMessageHandler HttpMessageHandler => HttpTest.Current?.HttpMessageHandler ?? _httpMessageHandler.Value; 111 | 112 | /// 113 | public IFlurlRequest Request(params object[] urlSegments) { 114 | var parts = new List(urlSegments.Select(s => s.ToInvariantString())); 115 | if (!Url.IsValid(parts.FirstOrDefault()) && !string.IsNullOrEmpty(BaseUrl)) 116 | parts.Insert(0, BaseUrl); 117 | 118 | if (!parts.Any()) 119 | throw new ArgumentException("Cannot create a Request. BaseUrl is not defined and no segments were passed."); 120 | if (!Url.IsValid(parts[0])) 121 | throw new ArgumentException("Cannot create a Request. Neither BaseUrl nor the first segment passed is a valid URL."); 122 | 123 | return new FlurlRequest(Url.Combine(parts.ToArray())).WithClient(this); 124 | } 125 | 126 | FlurlHttpSettings IHttpSettingsContainer.Settings { 127 | get => Settings; 128 | set => Settings = value as ClientFlurlHttpSettings; 129 | } 130 | 131 | private Lazy _connectionLeaseStart = new Lazy(() => DateTime.UtcNow); 132 | private readonly object _connectionLeaseLock = new object(); 133 | 134 | private bool IsConnectionLeaseExpired => 135 | Settings.ConnectionLeaseTimeout.HasValue && 136 | DateTime.UtcNow - _connectionLeaseStart.Value > Settings.ConnectionLeaseTimeout; 137 | 138 | /// 139 | public bool CheckAndRenewConnectionLease() { 140 | // do double-check locking to avoid lock overhead most of the time 141 | if (IsConnectionLeaseExpired) { 142 | lock (_connectionLeaseLock) { 143 | if (IsConnectionLeaseExpired) { 144 | _connectionLeaseStart = new Lazy(() => DateTime.UtcNow); 145 | return true; 146 | } 147 | } 148 | } 149 | return false; 150 | } 151 | 152 | /// 153 | public bool IsDisposed { get; private set; } 154 | 155 | /// 156 | /// Disposes the underlying HttpClient and HttpMessageHandler. 157 | /// 158 | public virtual void Dispose() { 159 | if (IsDisposed) 160 | return; 161 | 162 | if (_httpMessageHandler.IsValueCreated) 163 | _httpMessageHandler.Value.Dispose(); 164 | if (_httpClient.IsValueCreated) 165 | _httpClient.Value.Dispose(); 166 | 167 | IsDisposed = true; 168 | } 169 | } 170 | } -------------------------------------------------------------------------------- /Build/Flurl.netstandard.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flurl", "..\src\Flurl\Flurl.csproj", "{B953D4B9-8E2D-4ED7-BC14-DC1E540DDEF9}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flurl.Http", "..\src\Flurl.Http\Flurl.Http.csproj", "{2760549B-586D-4660-A881-51767823BDCA}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flurl.Http.CodeGen", "..\src\Flurl.Http.CodeGen\Flurl.Http.CodeGen.csproj", "{DCB24996-3FE7-4218-B4A4-2748235A6235}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flurl.Test", "..\Test\Flurl.Test\Flurl.Test.csproj", "{0E1584BA-B4E7-4DAE-8B95-463F7414B2FC}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PackageTester.NETCore", "..\PackageTesters\PackageTester.NETCore\PackageTester.NETCore.csproj", "{28614B4E-EC7E-475D-A23D-4A3AA3D209C8}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6A6A4DAB-56EB-4176-A980-7042ED5B66F3}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{3225BD21-E498-48A1-A3CA-754E33C61E84}" 19 | EndProject 20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PackageTesters", "PackageTesters", "{81DECD72-7045-49C7-A065-4E71F5D50693}" 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Debug|x64 = Debug|x64 26 | Debug|x86 = Debug|x86 27 | Release|Any CPU = Release|Any CPU 28 | Release|x64 = Release|x64 29 | Release|x86 = Release|x86 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 35 | {B953D4B9-8E2D-4ED7-BC14-DC1E540DDEF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {B953D4B9-8E2D-4ED7-BC14-DC1E540DDEF9}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {B953D4B9-8E2D-4ED7-BC14-DC1E540DDEF9}.Debug|x64.ActiveCfg = Debug|x64 38 | {B953D4B9-8E2D-4ED7-BC14-DC1E540DDEF9}.Debug|x64.Build.0 = Debug|x64 39 | {B953D4B9-8E2D-4ED7-BC14-DC1E540DDEF9}.Debug|x86.ActiveCfg = Debug|x86 40 | {B953D4B9-8E2D-4ED7-BC14-DC1E540DDEF9}.Debug|x86.Build.0 = Debug|x86 41 | {B953D4B9-8E2D-4ED7-BC14-DC1E540DDEF9}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {B953D4B9-8E2D-4ED7-BC14-DC1E540DDEF9}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {B953D4B9-8E2D-4ED7-BC14-DC1E540DDEF9}.Release|x64.ActiveCfg = Release|x64 44 | {B953D4B9-8E2D-4ED7-BC14-DC1E540DDEF9}.Release|x64.Build.0 = Release|x64 45 | {B953D4B9-8E2D-4ED7-BC14-DC1E540DDEF9}.Release|x86.ActiveCfg = Release|x86 46 | {B953D4B9-8E2D-4ED7-BC14-DC1E540DDEF9}.Release|x86.Build.0 = Release|x86 47 | {2760549B-586D-4660-A881-51767823BDCA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {2760549B-586D-4660-A881-51767823BDCA}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {2760549B-586D-4660-A881-51767823BDCA}.Debug|x64.ActiveCfg = Debug|x64 50 | {2760549B-586D-4660-A881-51767823BDCA}.Debug|x64.Build.0 = Debug|x64 51 | {2760549B-586D-4660-A881-51767823BDCA}.Debug|x86.ActiveCfg = Debug|x86 52 | {2760549B-586D-4660-A881-51767823BDCA}.Debug|x86.Build.0 = Debug|x86 53 | {2760549B-586D-4660-A881-51767823BDCA}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {2760549B-586D-4660-A881-51767823BDCA}.Release|Any CPU.Build.0 = Release|Any CPU 55 | {2760549B-586D-4660-A881-51767823BDCA}.Release|x64.ActiveCfg = Release|x64 56 | {2760549B-586D-4660-A881-51767823BDCA}.Release|x64.Build.0 = Release|x64 57 | {2760549B-586D-4660-A881-51767823BDCA}.Release|x86.ActiveCfg = Release|x86 58 | {2760549B-586D-4660-A881-51767823BDCA}.Release|x86.Build.0 = Release|x86 59 | {DCB24996-3FE7-4218-B4A4-2748235A6235}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 60 | {DCB24996-3FE7-4218-B4A4-2748235A6235}.Debug|Any CPU.Build.0 = Debug|Any CPU 61 | {DCB24996-3FE7-4218-B4A4-2748235A6235}.Debug|x64.ActiveCfg = Debug|x64 62 | {DCB24996-3FE7-4218-B4A4-2748235A6235}.Debug|x64.Build.0 = Debug|x64 63 | {DCB24996-3FE7-4218-B4A4-2748235A6235}.Debug|x86.ActiveCfg = Debug|x86 64 | {DCB24996-3FE7-4218-B4A4-2748235A6235}.Debug|x86.Build.0 = Debug|x86 65 | {DCB24996-3FE7-4218-B4A4-2748235A6235}.Release|Any CPU.ActiveCfg = Release|Any CPU 66 | {DCB24996-3FE7-4218-B4A4-2748235A6235}.Release|Any CPU.Build.0 = Release|Any CPU 67 | {DCB24996-3FE7-4218-B4A4-2748235A6235}.Release|x64.ActiveCfg = Release|x64 68 | {DCB24996-3FE7-4218-B4A4-2748235A6235}.Release|x64.Build.0 = Release|x64 69 | {DCB24996-3FE7-4218-B4A4-2748235A6235}.Release|x86.ActiveCfg = Release|x86 70 | {DCB24996-3FE7-4218-B4A4-2748235A6235}.Release|x86.Build.0 = Release|x86 71 | {0E1584BA-B4E7-4DAE-8B95-463F7414B2FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 72 | {0E1584BA-B4E7-4DAE-8B95-463F7414B2FC}.Debug|Any CPU.Build.0 = Debug|Any CPU 73 | {0E1584BA-B4E7-4DAE-8B95-463F7414B2FC}.Debug|x64.ActiveCfg = Debug|x64 74 | {0E1584BA-B4E7-4DAE-8B95-463F7414B2FC}.Debug|x64.Build.0 = Debug|x64 75 | {0E1584BA-B4E7-4DAE-8B95-463F7414B2FC}.Debug|x86.ActiveCfg = Debug|x86 76 | {0E1584BA-B4E7-4DAE-8B95-463F7414B2FC}.Debug|x86.Build.0 = Debug|x86 77 | {0E1584BA-B4E7-4DAE-8B95-463F7414B2FC}.Release|Any CPU.ActiveCfg = Release|Any CPU 78 | {0E1584BA-B4E7-4DAE-8B95-463F7414B2FC}.Release|Any CPU.Build.0 = Release|Any CPU 79 | {0E1584BA-B4E7-4DAE-8B95-463F7414B2FC}.Release|x64.ActiveCfg = Release|x64 80 | {0E1584BA-B4E7-4DAE-8B95-463F7414B2FC}.Release|x64.Build.0 = Release|x64 81 | {0E1584BA-B4E7-4DAE-8B95-463F7414B2FC}.Release|x86.ActiveCfg = Release|x86 82 | {0E1584BA-B4E7-4DAE-8B95-463F7414B2FC}.Release|x86.Build.0 = Release|x86 83 | {28614B4E-EC7E-475D-A23D-4A3AA3D209C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 84 | {28614B4E-EC7E-475D-A23D-4A3AA3D209C8}.Debug|Any CPU.Build.0 = Debug|Any CPU 85 | {28614B4E-EC7E-475D-A23D-4A3AA3D209C8}.Debug|x64.ActiveCfg = Debug|x64 86 | {28614B4E-EC7E-475D-A23D-4A3AA3D209C8}.Debug|x64.Build.0 = Debug|x64 87 | {28614B4E-EC7E-475D-A23D-4A3AA3D209C8}.Debug|x86.ActiveCfg = Debug|x86 88 | {28614B4E-EC7E-475D-A23D-4A3AA3D209C8}.Debug|x86.Build.0 = Debug|x86 89 | {28614B4E-EC7E-475D-A23D-4A3AA3D209C8}.Release|Any CPU.ActiveCfg = Release|Any CPU 90 | {28614B4E-EC7E-475D-A23D-4A3AA3D209C8}.Release|Any CPU.Build.0 = Release|Any CPU 91 | {28614B4E-EC7E-475D-A23D-4A3AA3D209C8}.Release|x64.ActiveCfg = Release|x64 92 | {28614B4E-EC7E-475D-A23D-4A3AA3D209C8}.Release|x64.Build.0 = Release|x64 93 | {28614B4E-EC7E-475D-A23D-4A3AA3D209C8}.Release|x86.ActiveCfg = Release|x86 94 | {28614B4E-EC7E-475D-A23D-4A3AA3D209C8}.Release|x86.Build.0 = Release|x86 95 | EndGlobalSection 96 | GlobalSection(NestedProjects) = preSolution 97 | {B953D4B9-8E2D-4ED7-BC14-DC1E540DDEF9} = {6A6A4DAB-56EB-4176-A980-7042ED5B66F3} 98 | {2760549B-586D-4660-A881-51767823BDCA} = {6A6A4DAB-56EB-4176-A980-7042ED5B66F3} 99 | {DCB24996-3FE7-4218-B4A4-2748235A6235} = {6A6A4DAB-56EB-4176-A980-7042ED5B66F3} 100 | {0E1584BA-B4E7-4DAE-8B95-463F7414B2FC} = {3225BD21-E498-48A1-A3CA-754E33C61E84} 101 | {28614B4E-EC7E-475D-A23D-4A3AA3D209C8} = {81DECD72-7045-49C7-A065-4E71F5D50693} 102 | EndGlobalSection 103 | EndGlobal 104 | -------------------------------------------------------------------------------- /Test/Flurl.Test/Http/SettingsExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Threading.Tasks; 6 | using Flurl.Http; 7 | using Flurl.Http.Testing; 8 | using NUnit.Framework; 9 | 10 | namespace Flurl.Test.Http 11 | { 12 | // IFlurlClient and IFlurlRequest both implement IHttpSettingsContainer, which defines a number 13 | // of settings-related extension methods. This abstract test class allows those methods to be 14 | // tested against both both client-level and request-level implementations. 15 | public abstract class SettingsExtensionsTests where T : IHttpSettingsContainer 16 | { 17 | protected abstract T GetSettingsContainer(); 18 | protected abstract IFlurlRequest GetRequest(T sc); 19 | 20 | [Test] 21 | public void can_set_timeout() { 22 | var sc = GetSettingsContainer().WithTimeout(TimeSpan.FromSeconds(15)); 23 | Assert.AreEqual(TimeSpan.FromSeconds(15), sc.Settings.Timeout); 24 | } 25 | 26 | [Test] 27 | public void can_set_timeout_in_seconds() { 28 | var sc = GetSettingsContainer().WithTimeout(15); 29 | Assert.AreEqual(sc.Settings.Timeout, TimeSpan.FromSeconds(15)); 30 | } 31 | 32 | [Test] 33 | public void can_set_header() { 34 | var sc = GetSettingsContainer().WithHeader("a", 1); 35 | Assert.AreEqual(1, sc.Headers.Count); 36 | Assert.AreEqual(1, sc.Headers["a"]); 37 | } 38 | 39 | [Test] 40 | public void can_set_headers_from_anon_object() { 41 | // null values shouldn't be added 42 | var sc = GetSettingsContainer().WithHeaders(new { a = "b", one = 2, three = (object)null }); 43 | Assert.AreEqual(2, sc.Headers.Count); 44 | Assert.AreEqual("b", sc.Headers["a"]); 45 | Assert.AreEqual(2, sc.Headers["one"]); 46 | } 47 | 48 | [Test] 49 | public void can_remove_header_by_setting_null() { 50 | var sc = GetSettingsContainer().WithHeaders(new { a = 1, b = 2 }); 51 | Assert.AreEqual(2, sc.Headers.Count); 52 | sc.WithHeader("b", null); 53 | Assert.AreEqual(1, sc.Headers.Count); 54 | Assert.AreEqual("a", sc.Headers.Keys.Single()); 55 | } 56 | 57 | [Test] 58 | public void can_set_headers_from_dictionary() { 59 | var sc = GetSettingsContainer().WithHeaders(new Dictionary { { "a", "b" }, { "one", 2 } }); 60 | Assert.AreEqual(2, sc.Headers.Count); 61 | Assert.AreEqual("b", sc.Headers["a"]); 62 | Assert.AreEqual(2, sc.Headers["one"]); 63 | } 64 | 65 | [Test] 66 | public void underscores_in_properties_convert_to_hyphens_in_header_names() { 67 | var sc = GetSettingsContainer().WithHeaders(new { User_Agent = "Flurl", Cache_Control = "no-cache" }); 68 | Assert.IsTrue(sc.Headers.ContainsKey("User-Agent")); 69 | Assert.IsTrue(sc.Headers.ContainsKey("Cache-Control")); 70 | 71 | // make sure we can disable the behavior 72 | sc.WithHeaders(new { no_i_really_want_underscores = "foo" }, false); 73 | Assert.IsTrue(sc.Headers.ContainsKey("no_i_really_want_underscores")); 74 | 75 | // dictionaries don't get this behavior since you can use hyphens explicitly 76 | sc.WithHeaders(new Dictionary { { "exclude_dictionaries", "bar" } }); 77 | Assert.IsTrue(sc.Headers.ContainsKey("exclude_dictionaries")); 78 | 79 | // same with strings 80 | sc.WithHeaders("exclude_strings=123"); 81 | Assert.IsTrue(sc.Headers.ContainsKey("exclude_strings")); 82 | } 83 | 84 | [Test] 85 | public void can_setup_oauth_bearer_token() { 86 | var sc = GetSettingsContainer().WithOAuthBearerToken("mytoken"); 87 | Assert.AreEqual(1, sc.Headers.Count); 88 | Assert.AreEqual("Bearer mytoken", sc.Headers["Authorization"]); 89 | } 90 | 91 | [Test] 92 | public void can_setup_basic_auth() { 93 | var sc = GetSettingsContainer().WithBasicAuth("user", "pass"); 94 | Assert.AreEqual(1, sc.Headers.Count); 95 | Assert.AreEqual("Basic dXNlcjpwYXNz", sc.Headers["Authorization"]); 96 | } 97 | 98 | [Test] 99 | public async Task can_allow_specific_http_status() { 100 | using (var test = new HttpTest()) { 101 | test.RespondWith("Nothing to see here", 404); 102 | var sc = GetSettingsContainer().AllowHttpStatus(HttpStatusCode.Conflict, HttpStatusCode.NotFound); 103 | await GetRequest(sc).DeleteAsync(); // no exception = pass 104 | } 105 | } 106 | 107 | [Test] 108 | public void can_clear_non_success_status() { 109 | using (var test = new HttpTest()) { 110 | test.RespondWith("I'm a teapot", 418); 111 | // allow 4xx 112 | var sc = GetSettingsContainer().AllowHttpStatus("4xx"); 113 | // but then disallow it 114 | sc.Settings.AllowedHttpStatusRange = null; 115 | Assert.ThrowsAsync(async () => await GetRequest(sc).GetAsync()); 116 | } 117 | } 118 | 119 | [Test] 120 | public async Task can_allow_any_http_status() { 121 | using (var test = new HttpTest()) { 122 | test.RespondWith("epic fail", 500); 123 | try { 124 | var sc = GetSettingsContainer().AllowAnyHttpStatus(); 125 | var result = await GetRequest(sc).GetAsync(); 126 | Assert.IsFalse(result.IsSuccessStatusCode); 127 | } 128 | catch (Exception) { 129 | Assert.Fail("Exception should not have been thrown."); 130 | } 131 | } 132 | } 133 | } 134 | 135 | [TestFixture, Parallelizable] 136 | public class ClientSettingsExtensionsTests : SettingsExtensionsTests 137 | { 138 | protected override IFlurlClient GetSettingsContainer() => new FlurlClient(); 139 | protected override IFlurlRequest GetRequest(IFlurlClient client) => client.Request("http://api.com"); 140 | 141 | [Test] 142 | public void WithUrl_shares_client_but_not_Url() { 143 | var cli = new FlurlClient().WithCookie("mycookie", "123"); 144 | var req1 = cli.Request("http://www.api.com/for-req1"); 145 | var req2 = cli.Request("http://www.api.com/for-req2"); 146 | var req3 = cli.Request("http://www.api.com/for-req3"); 147 | 148 | CollectionAssert.AreEquivalent(req1.Cookies, req2.Cookies); 149 | CollectionAssert.AreEquivalent(req1.Cookies, req3.Cookies); 150 | var urls = new[] { req1, req2, req3 }.Select(c => c.Url.ToString()); 151 | CollectionAssert.AllItemsAreUnique(urls); 152 | } 153 | 154 | [Test] 155 | public void WithClient_shares_client_but_not_Url() { 156 | var cli = new FlurlClient().WithCookie("mycookie", "123"); 157 | var req1 = "http://www.api.com/for-req1".WithClient(cli); 158 | var req2 = "http://www.api.com/for-req2".WithClient(cli); 159 | var req3 = "http://www.api.com/for-req3".WithClient(cli); 160 | 161 | CollectionAssert.AreEquivalent(req1.Cookies, req2.Cookies); 162 | CollectionAssert.AreEquivalent(req1.Cookies, req3.Cookies); 163 | var urls = new[] { req1, req2, req3 }.Select(c => c.Url.ToString()); 164 | CollectionAssert.AllItemsAreUnique(urls); 165 | } 166 | 167 | [Test] 168 | public void can_use_uri_with_WithUrl() { 169 | var uri = new System.Uri("http://www.mysite.com/foo?x=1"); 170 | var req = new FlurlClient().Request(uri); 171 | Assert.AreEqual(uri.ToString(), req.Url.ToString()); 172 | } 173 | 174 | [Test] 175 | public void can_override_settings_fluently() { 176 | using (var test = new HttpTest()) { 177 | var cli = new FlurlClient().Configure(s => s.AllowedHttpStatusRange = "*"); 178 | test.RespondWith("epic fail", 500); 179 | Assert.ThrowsAsync(async () => await "http://www.api.com" 180 | .ConfigureRequest(c => c.AllowedHttpStatusRange = "2xx") 181 | .WithClient(cli) // client-level settings shouldn't win 182 | .GetAsync()); 183 | } 184 | } 185 | } 186 | 187 | [TestFixture, Parallelizable] 188 | public class RequestSettingsExtensionsTests : SettingsExtensionsTests 189 | { 190 | protected override IFlurlRequest GetSettingsContainer() => new FlurlRequest("http://api.com"); 191 | protected override IFlurlRequest GetRequest(IFlurlRequest req) => req; 192 | } 193 | } -------------------------------------------------------------------------------- /src/Flurl.Http/Testing/HttpTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Http; 6 | using Flurl.Http.Configuration; 7 | using Flurl.Http.Content; 8 | using Flurl.Util; 9 | 10 | namespace Flurl.Http.Testing 11 | { 12 | /// 13 | /// An object whose existence puts Flurl.Http into test mode where actual HTTP calls are faked. Provides a response 14 | /// queue, call log, and assertion helpers for use in Arrange/Act/Assert style tests. 15 | /// 16 | #if !NETSTANDARD1_1 17 | [Serializable] // fixes MSTest issue? #207 18 | #endif 19 | public class HttpTest : IDisposable 20 | { 21 | private readonly Lazy _httpClient; 22 | private readonly Lazy _httpMessageHandler; 23 | 24 | /// 25 | /// Initializes a new instance of the class. 26 | /// 27 | /// A delegate callback throws an exception. 28 | public HttpTest() { 29 | Settings = new TestFlurlHttpSettings(); 30 | ResponseQueue = new Queue(); 31 | CallLog = new List(); 32 | _httpClient = new Lazy(() => Settings.HttpClientFactory.CreateHttpClient(HttpMessageHandler)); 33 | _httpMessageHandler = new Lazy(() => Settings.HttpClientFactory.CreateMessageHandler()); 34 | SetCurrentTest(this); 35 | } 36 | 37 | internal HttpClient HttpClient => _httpClient.Value; 38 | internal HttpMessageHandler HttpMessageHandler => _httpMessageHandler.Value; 39 | 40 | /// 41 | /// Gets or sets the FlurlHttpSettings object used by this test. 42 | /// 43 | public TestFlurlHttpSettings Settings { get; set; } 44 | 45 | /// 46 | /// Gets the current HttpTest from the logical (async) call context 47 | /// 48 | public static HttpTest Current => GetCurrentTest(); 49 | 50 | /// 51 | /// Queue of HttpResponseMessages to be returned in place of real responses during testing. 52 | /// 53 | public Queue ResponseQueue { get; set; } 54 | 55 | /// 56 | /// List of all (fake) HTTP calls made since this HttpTest was created. 57 | /// 58 | public List CallLog { get; } 59 | 60 | /// 61 | /// Change FlurlHttpSettings for the scope of this HttpTest. 62 | /// 63 | /// Action defining the settings changes. 64 | /// This HttpTest 65 | public HttpTest Configure(Action action) { 66 | action(Settings); 67 | return this; 68 | } 69 | 70 | /// 71 | /// Adds an HttpResponseMessage to the response queue. 72 | /// 73 | /// The simulated response body string. 74 | /// The simulated HTTP status. Default is 200. 75 | /// The simulated response headers (optional). 76 | /// The simulated response cookies (optional). 77 | /// The current HttpTest object (so more responses can be chained). 78 | public HttpTest RespondWith(string body, int status = 200, object headers = null, object cookies = null) { 79 | return RespondWith(new StringContent(body), status, headers, cookies); 80 | } 81 | 82 | /// 83 | /// Adds an HttpResponseMessage to the response queue with the given data serialized to JSON as the content body. 84 | /// 85 | /// The object to be JSON-serialized and used as the simulated response body. 86 | /// The simulated HTTP status. Default is 200. 87 | /// The simulated response headers (optional). 88 | /// The simulated response cookies (optional). 89 | /// The current HttpTest object (so more responses can be chained). 90 | public HttpTest RespondWithJson(object body, int status = 200, object headers = null, object cookies = null) { 91 | var content = new CapturedJsonContent(Settings.JsonSerializer.Serialize(body)); 92 | return RespondWith(content, status, headers, cookies); 93 | } 94 | 95 | /// 96 | /// Adds an HttpResponseMessage to the response queue. 97 | /// 98 | /// The simulated response body content (optional). 99 | /// The simulated HTTP status. Default is 200. 100 | /// The simulated response headers (optional). 101 | /// The simulated response cookies (optional). 102 | /// The current HttpTest object (so more responses can be chained). 103 | public HttpTest RespondWith(HttpContent content = null, int status = 200, object headers = null, object cookies = null) { 104 | var response = new HttpResponseMessage { 105 | StatusCode = (HttpStatusCode)status, 106 | Content = content 107 | }; 108 | if (headers != null) { 109 | foreach (var kv in headers.ToKeyValuePairs()) 110 | response.Headers.Add(kv.Key, kv.Value.ToInvariantString()); 111 | } 112 | if (cookies != null) { 113 | foreach (var kv in cookies.ToKeyValuePairs()) { 114 | var value = new Cookie(kv.Key, kv.Value.ToInvariantString()).ToString(); 115 | response.Headers.Add("Set-Cookie", value); 116 | } 117 | } 118 | ResponseQueue.Enqueue(response); 119 | return this; 120 | } 121 | 122 | /// 123 | /// Adds a simulated timeout response to the response queue. 124 | /// 125 | public HttpTest SimulateTimeout() { 126 | ResponseQueue.Enqueue(new TimeoutResponseMessage()); 127 | return this; 128 | } 129 | 130 | internal HttpResponseMessage GetNextResponse() { 131 | return ResponseQueue.Any() ? ResponseQueue.Dequeue() : new HttpResponseMessage { 132 | StatusCode = HttpStatusCode.OK, 133 | Content = new StringContent("") 134 | }; 135 | } 136 | 137 | /// 138 | /// Asserts whether matching URL was called, throwing HttpCallAssertException if it wasn't. 139 | /// 140 | /// URL that should have been called. Can include * wildcard character. 141 | public HttpCallAssertion ShouldHaveCalled(string urlPattern) { 142 | return new HttpCallAssertion(CallLog).WithUrlPattern(urlPattern); 143 | } 144 | 145 | /// 146 | /// Asserts whether matching URL was NOT called, throwing HttpCallAssertException if it was. 147 | /// 148 | /// URL that should not have been called. Can include * wildcard character. 149 | public void ShouldNotHaveCalled(string urlPattern) { 150 | new HttpCallAssertion(CallLog, true).WithUrlPattern(urlPattern); 151 | } 152 | 153 | /// 154 | /// Asserts whether any HTTP call was made, throwing HttpCallAssertException if none were. 155 | /// 156 | public HttpCallAssertion ShouldHaveMadeACall() { 157 | return new HttpCallAssertion(CallLog).WithUrlPattern("*"); 158 | } 159 | 160 | /// 161 | /// Asserts whether no HTTP calls were made, throwing HttpCallAssertException if any were. 162 | /// 163 | public void ShouldNotHaveMadeACall() { 164 | new HttpCallAssertion(CallLog, true).WithUrlPattern("*"); 165 | } 166 | 167 | /// 168 | /// Releases unmanaged and - optionally - managed resources. 169 | /// 170 | public void Dispose() { 171 | SetCurrentTest(null); 172 | } 173 | 174 | #if NET45 175 | private static void SetCurrentTest(HttpTest test) => System.Runtime.Remoting.Messaging.CallContext.LogicalSetData("FlurlHttpTest", test); 176 | private static HttpTest GetCurrentTest() => System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("FlurlHttpTest") as HttpTest; 177 | #elif NETSTANDARD1_3 || NETSTANDARD2_0 178 | private static System.Threading.AsyncLocal _test = new System.Threading.AsyncLocal(); 179 | private static void SetCurrentTest(HttpTest test) => _test.Value = test; 180 | private static HttpTest GetCurrentTest() => _test.Value; 181 | #elif NETSTANDARD1_1 182 | private static HttpTest _test; 183 | private static void SetCurrentTest(HttpTest test) => _test = test; 184 | private static HttpTest GetCurrentTest() => _test; 185 | #endif 186 | } 187 | } -------------------------------------------------------------------------------- /src/Flurl.Http.CodeGen/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | 6 | namespace Flurl.Http.CodeGen 7 | { 8 | class Program 9 | { 10 | static int Main(string[] args) { 11 | var codePath = (args.Length > 0) ? args[0] : @"..\Flurl.Http\GeneratedExtensions.cs"; 12 | 13 | if (!File.Exists(codePath)) { 14 | Console.ForegroundColor = ConsoleColor.Red; 15 | Console.WriteLine("Code file not found: " + Path.GetFullPath(codePath)); 16 | Console.ReadLine(); 17 | return 2; 18 | } 19 | 20 | try { 21 | File.WriteAllText(codePath, ""); 22 | using (var writer = new CodeWriter(codePath)) 23 | { 24 | writer 25 | .WriteLine("// This file was auto-generated by Flurl.Http.CodeGen. Do not edit directly.") 26 | .WriteLine("using System;") 27 | .WriteLine("using System.Collections.Generic;") 28 | .WriteLine("using System.IO;") 29 | .WriteLine("using System.Net;") 30 | .WriteLine("using System.Net.Http;") 31 | .WriteLine("using System.Threading;") 32 | .WriteLine("using System.Threading.Tasks;") 33 | .WriteLine("using Flurl.Http.Configuration;") 34 | .WriteLine("using Flurl.Http.Content;") 35 | .WriteLine("") 36 | .WriteLine("namespace Flurl.Http") 37 | .WriteLine("{") 38 | .WriteLine("/// ") 39 | .WriteLine("/// Auto-generated fluent extension methods on String, Url, and IFlurlRequest.") 40 | .WriteLine("/// ") 41 | .WriteLine("public static class GeneratedExtensions") 42 | .WriteLine("{"); 43 | 44 | WriteExtensionMethods(writer); 45 | 46 | writer 47 | .WriteLine("}") 48 | .WriteLine("}"); 49 | } 50 | 51 | Console.WriteLine("File writing succeeded."); 52 | return 0; 53 | } 54 | catch (Exception ex) { 55 | Console.ForegroundColor = ConsoleColor.Red; 56 | Console.WriteLine(ex); 57 | Console.ReadLine(); 58 | return 2; 59 | } 60 | } 61 | 62 | private static void WriteExtensionMethods(CodeWriter writer) 63 | { 64 | string name = null; 65 | foreach (var xm in HttpExtensionMethod.GetAll()) { 66 | var hasRequestBody = (xm.HttpVerb == "Post" || xm.HttpVerb == "Put" || xm.HttpVerb == "Patch" || xm.HttpVerb == null); 67 | 68 | if (xm.Name != name) { 69 | Console.WriteLine($"writing {xm.Name}..."); 70 | name = xm.Name; 71 | } 72 | writer.WriteLine("/// "); 73 | var summaryStart = (xm.ExtentionOfType == "IFlurlRequest") ? "Sends" : "Creates a FlurlRequest from the URL and sends"; 74 | if (xm.HttpVerb == null) 75 | writer.WriteLine("/// @0 an asynchronous request.", summaryStart); 76 | else 77 | writer.WriteLine("/// @0 an asynchronous @1 request.", summaryStart, xm.HttpVerb.ToUpperInvariant()); 78 | writer.WriteLine("/// "); 79 | if (xm.ExtentionOfType == "IFlurlRequest") 80 | writer.WriteLine("/// The IFlurlRequest instance."); 81 | if (xm.ExtentionOfType == "Url" || xm.ExtentionOfType == "string") 82 | writer.WriteLine("/// The URL."); 83 | if (xm.HttpVerb == null) 84 | writer.WriteLine("/// The HTTP method used to make the request."); 85 | if (xm.BodyType != null) 86 | writer.WriteLine("/// Contents of the request body."); 87 | else if (hasRequestBody) 88 | writer.WriteLine("/// Contents of the request body."); 89 | writer.WriteLine("/// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional."); 90 | writer.WriteLine("/// The HttpCompletionOption used in the request. Optional."); 91 | writer.WriteLine("/// A Task whose result is @0.", xm.ReturnTypeDescription); 92 | 93 | var args = new List(); 94 | args.Add("this " + xm.ExtentionOfType + (xm.ExtentionOfType == "IFlurlRequest" ? " request" : " url")); 95 | if (xm.HttpVerb == null) 96 | args.Add("HttpMethod verb"); 97 | if (xm.BodyType != null) 98 | args.Add((xm.BodyType == "String" ? "string" : "object") + " data"); 99 | else if (hasRequestBody) 100 | args.Add("HttpContent content"); 101 | 102 | // http://stackoverflow.com/questions/22359706/default-parameter-for-cancellationtoken 103 | args.Add("CancellationToken cancellationToken = default(CancellationToken)"); 104 | args.Add("HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead"); 105 | 106 | writer.WriteLine("public static Task<@0> @1@2(@3) {", xm.TaskArg, xm.Name, xm.IsGeneric ? "" : "", string.Join(", ", args)); 107 | 108 | if (xm.ExtentionOfType == "IFlurlRequest") 109 | { 110 | args.Clear(); 111 | args.Add( 112 | xm.HttpVerb == null ? "verb" : 113 | xm.HttpVerb == "Patch" ? "new HttpMethod(\"PATCH\")" : // there's no HttpMethod.Patch 114 | "HttpMethod." + xm.HttpVerb); 115 | 116 | if (xm.BodyType != null || hasRequestBody) 117 | args.Add("content: content"); 118 | 119 | args.Add("cancellationToken: cancellationToken"); 120 | args.Add("completionOption: completionOption"); 121 | 122 | if (xm.BodyType != null) { 123 | writer.WriteLine("var content = new Captured@0Content(@1);", 124 | xm.BodyType, 125 | xm.BodyType == "String" ? "data" : $"request.Settings.{xm.BodyType}Serializer.Serialize(data)"); 126 | } 127 | 128 | var request = (xm.ExtentionOfType == "IFlurlRequest") ? "request" : "new FlurlRequest(url)"; 129 | var receive = (xm.DeserializeToType == null) ? "" : string.Format(".Receive{0}{1}()", xm.DeserializeToType, xm.IsGeneric ? "" : ""); 130 | writer.WriteLine("return @0.SendAsync(@1)@2;", request, string.Join(", ", args), receive); 131 | } 132 | else 133 | { 134 | writer.WriteLine("return new FlurlRequest(url).@0(@1);", 135 | xm.Name + (xm.IsGeneric ? "" : ""), 136 | string.Join(", ", args.Skip(1).Select(a => a.Split(' ')[1]))); 137 | } 138 | 139 | writer.WriteLine("}").WriteLine(); 140 | } 141 | 142 | foreach (var xtype in new[] { "Url", "string" }) { 143 | foreach (var xm in UrlExtensionMethod.GetAll()) { 144 | if (xm.Name != name) { 145 | Console.WriteLine($"writing {xm.Name}..."); 146 | name = xm.Name; 147 | } 148 | 149 | writer.WriteLine("/// "); 150 | writer.WriteLine($"/// {xm.Description}"); 151 | writer.WriteLine("/// "); 152 | writer.WriteLine("/// The URL."); 153 | foreach (var p in xm.Params) 154 | writer.WriteLine($"/// {p.Description}"); 155 | writer.WriteLine("/// The IFlurlRequest."); 156 | 157 | var argList = new List { $"this {xtype} url" }; 158 | argList.AddRange(xm.Params.Select(p => $"{p.Type} {p.Name}" + (p.Default == null ? "" : $" = {p.Default}"))); 159 | writer.WriteLine($"public static IFlurlRequest {xm.Name}({string.Join(", ", argList)}) {{"); 160 | writer.WriteLine($"return new FlurlRequest(url).{xm.Name}({string.Join(", ", xm.Params.Select(p => p.Name))});"); 161 | writer.WriteLine("}"); 162 | } 163 | } 164 | } 165 | } 166 | } -------------------------------------------------------------------------------- /src/Flurl/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Flurl 4 | { 5 | /// 6 | /// A set of string extension methods for working with Flurl URLs 7 | /// 8 | public static class StringExtensions 9 | { 10 | /// 11 | /// Creates a new Url object from the string and appends a segment to the URL path, 12 | /// ensuring there is one and only one '/' character as a separator. 13 | /// 14 | /// The URL. 15 | /// The segment to append 16 | /// If true, URL-encodes reserved characters such as '/', '+', and '%'. Otherwise, only encodes strictly illegal characters (including '%' but only when not followed by 2 hex characters). 17 | /// 18 | /// the resulting Url object 19 | /// 20 | public static Url AppendPathSegment(this string url, object segment, bool fullyEncode = false) { 21 | return new Url(url).AppendPathSegment(segment, fullyEncode); 22 | } 23 | 24 | /// 25 | /// Appends multiple segments to the URL path, ensuring there is one and only one '/' character as a seperator. 26 | /// 27 | /// The URL. 28 | /// The segments to append 29 | /// 30 | /// the Url object with the segments appended 31 | /// 32 | public static Url AppendPathSegments(this string url, params object[] segments) { 33 | return new Url(url).AppendPathSegments(segments); 34 | } 35 | 36 | /// 37 | /// Appends multiple segments to the URL path, ensuring there is one and only one '/' character as a seperator. 38 | /// 39 | /// The URL. 40 | /// The segments to append 41 | /// 42 | /// the Url object with the segments appended 43 | /// 44 | public static Url AppendPathSegments(this string url, IEnumerable segments) { 45 | return new Url(url).AppendPathSegments(segments); 46 | } 47 | 48 | /// 49 | /// Creates a new Url object from the string and adds a parameter to the query, overwriting the value if name exists. 50 | /// 51 | /// The URL. 52 | /// Name of query parameter 53 | /// Value of query parameter 54 | /// Indicates how to handle null values. Defaults to Remove (any existing) 55 | /// The Url object with the query parameter added 56 | public static Url SetQueryParam(this string url, string name, object value, NullValueHandling nullValueHandling = NullValueHandling.Remove) { 57 | return new Url(url).SetQueryParam(name, value, nullValueHandling); 58 | } 59 | 60 | /// 61 | /// Creates a new Url object from the string and adds a parameter to the query, overwriting the value if name exists. 62 | /// 63 | /// The URL. 64 | /// Name of query parameter 65 | /// Value of query parameter 66 | /// Set to true to indicate the value is already URL-encoded. Defaults to false. 67 | /// Indicates how to handle null values. Defaults to Remove (any existing). 68 | /// 69 | /// The Url object with the query parameter added 70 | /// 71 | public static Url SetQueryParam(this string url, string name, string value, bool isEncoded = false, NullValueHandling nullValueHandling = NullValueHandling.Remove) { 72 | return new Url(url).SetQueryParam(name, value, isEncoded, nullValueHandling); 73 | } 74 | 75 | /// 76 | /// Creates a new Url object from the string and adds a parameter without a value to the query, removing any existing value. 77 | /// 78 | /// The URL. 79 | /// Name of query parameter 80 | /// The Url object with the query parameter added 81 | public static Url SetQueryParam(this string url, string name) { 82 | return new Url(url).SetQueryParam(name); 83 | } 84 | 85 | /// 86 | /// Creates a new Url object from the string, parses values object into name/value pairs, and adds them to the query, 87 | /// overwriting any that already exist. 88 | /// 89 | /// The URL. 90 | /// Typically an anonymous object, ie: new { x = 1, y = 2 } 91 | /// Indicates how to handle null values. Defaults to Remove (any existing) 92 | /// 93 | /// The Url object with the query parameters added 94 | /// 95 | public static Url SetQueryParams(this string url, object values, NullValueHandling nullValueHandling = NullValueHandling.Remove) { 96 | return new Url(url).SetQueryParams(values, nullValueHandling); 97 | } 98 | 99 | /// 100 | /// Creates a new Url object from the string and adds multiple parameters without values to the query. 101 | /// 102 | /// The URL. 103 | /// Names of query parameters. 104 | /// The Url object with the query parameter added 105 | public static Url SetQueryParams(this string url, IEnumerable names) { 106 | return new Url(url).SetQueryParams(names); 107 | } 108 | 109 | /// 110 | /// Creates a new Url object from the string and adds multiple parameters without values to the query. 111 | /// 112 | /// The URL. 113 | /// Names of query parameters 114 | /// The Url object with the query parameter added. 115 | public static Url SetQueryParams(this string url, params string[] names) { 116 | return new Url(url).SetQueryParams(names); 117 | } 118 | 119 | /// 120 | /// Creates a new Url object from the string and removes a name/value pair from the query by name. 121 | /// 122 | /// The URL. 123 | /// Query string parameter name to remove 124 | /// 125 | /// The Url object with the query parameter removed 126 | /// 127 | public static Url RemoveQueryParam(this string url, string name) { 128 | return new Url(url).RemoveQueryParam(name); 129 | } 130 | 131 | /// 132 | /// Creates a new Url object from the string and removes multiple name/value pairs from the query by name. 133 | /// 134 | /// The URL. 135 | /// Query string parameter names to remove 136 | /// 137 | /// The Url object with the query parameters removed 138 | /// 139 | public static Url RemoveQueryParams(this string url, params string[] names) { 140 | return new Url(url).RemoveQueryParams(names); 141 | } 142 | 143 | /// 144 | /// Creates a new Url object from the string and removes multiple name/value pairs from the query by name. 145 | /// 146 | /// The URL. 147 | /// Query string parameter names to remove 148 | /// 149 | /// The Url object with the query parameters removed 150 | /// 151 | public static Url RemoveQueryParams(this string url, IEnumerable names) { 152 | return new Url(url).RemoveQueryParams(names); 153 | } 154 | 155 | /// 156 | /// Set the URL fragment fluently. 157 | /// 158 | /// The URL. 159 | /// The part of the URL afer # 160 | /// 161 | /// The Url object with the new fragment set 162 | /// 163 | public static Url SetFragment(this string url, string fragment) { 164 | return new Url(url).SetFragment(fragment); 165 | } 166 | 167 | /// 168 | /// Removes the URL fragment including the #. 169 | /// 170 | /// The Url object with the fragment removed 171 | public static Url RemoveFragment(this string url) { 172 | return new Url(url).RemoveFragment(); 173 | } 174 | 175 | /// 176 | /// Trims the URL to its root, including the scheme, any user info, host, and port (if specified). 177 | /// 178 | /// A Url object. 179 | public static Url ResetToRoot(this string url) { 180 | return new Url(url).ResetToRoot(); 181 | } 182 | } 183 | } -------------------------------------------------------------------------------- /src/Flurl.Http/UrlBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Flurl.Http 5 | { 6 | /// 7 | /// URL builder extension methods on FlurlRequest 8 | /// 9 | public static class UrlBuilderExtensions 10 | { 11 | /// 12 | /// Appends a segment to the URL path, ensuring there is one and only one '/' character as a seperator. 13 | /// 14 | /// The IFlurlRequest associated with the URL 15 | /// The segment to append 16 | /// If true, URL-encodes reserved characters such as '/', '+', and '%'. Otherwise, only encodes strictly illegal characters (including '%' but only when not followed by 2 hex characters). 17 | /// This IFlurlRequest 18 | /// is . 19 | public static IFlurlRequest AppendPathSegment(this IFlurlRequest request, object segment, bool fullyEncode = false) { 20 | request.Url.AppendPathSegment(segment, fullyEncode); 21 | return request; 22 | } 23 | 24 | /// 25 | /// Appends multiple segments to the URL path, ensuring there is one and only one '/' character as a seperator. 26 | /// 27 | /// The IFlurlRequest associated with the URL 28 | /// The segments to append 29 | /// This IFlurlRequest 30 | public static IFlurlRequest AppendPathSegments(this IFlurlRequest request, params object[] segments) { 31 | request.Url.AppendPathSegments(segments); 32 | return request; 33 | } 34 | 35 | /// 36 | /// Appends multiple segments to the URL path, ensuring there is one and only one '/' character as a seperator. 37 | /// 38 | /// The IFlurlRequest associated with the URL 39 | /// The segments to append 40 | /// This IFlurlRequest 41 | public static IFlurlRequest AppendPathSegments(this IFlurlRequest request, IEnumerable segments) { 42 | request.Url.AppendPathSegments(segments); 43 | return request; 44 | } 45 | 46 | /// 47 | /// Adds a parameter to the URL query, overwriting the value if name exists. 48 | /// 49 | /// The IFlurlRequest associated with the URL 50 | /// Name of query parameter 51 | /// Value of query parameter 52 | /// Indicates how to handle null values. Defaults to Remove (any existing) 53 | /// This IFlurlRequest 54 | public static IFlurlRequest SetQueryParam(this IFlurlRequest request, string name, object value, NullValueHandling nullValueHandling = NullValueHandling.Remove) { 55 | request.Url.SetQueryParam(name, value, nullValueHandling); 56 | return request; 57 | } 58 | 59 | /// 60 | /// Adds a parameter to the URL query, overwriting the value if name exists. 61 | /// 62 | /// The IFlurlRequest associated with the URL 63 | /// Name of query parameter 64 | /// Value of query parameter 65 | /// Set to true to indicate the value is already URL-encoded 66 | /// Indicates how to handle null values. Defaults to Remove (any existing) 67 | /// This IFlurlRequest 68 | /// is . 69 | public static IFlurlRequest SetQueryParam(this IFlurlRequest request, string name, string value, bool isEncoded = false, NullValueHandling nullValueHandling = NullValueHandling.Remove) { 70 | request.Url.SetQueryParam(name, value, isEncoded, nullValueHandling); 71 | return request; 72 | } 73 | 74 | /// 75 | /// Adds a parameter without a value to the URL query, removing any existing value. 76 | /// 77 | /// The IFlurlRequest associated with the URL 78 | /// Name of query parameter 79 | /// This IFlurlRequest 80 | public static IFlurlRequest SetQueryParam(this IFlurlRequest request, string name) { 81 | request.Url.SetQueryParam(name); 82 | return request; 83 | } 84 | 85 | /// 86 | /// Parses values (usually an anonymous object or dictionary) into name/value pairs and adds them to the URL query, overwriting any that already exist. 87 | /// 88 | /// The IFlurlRequest associated with the URL 89 | /// Typically an anonymous object, ie: new { x = 1, y = 2 } 90 | /// Indicates how to handle null values. Defaults to Remove (any existing) 91 | /// This IFlurlRequest 92 | public static IFlurlRequest SetQueryParams(this IFlurlRequest request, object values, NullValueHandling nullValueHandling = NullValueHandling.Remove) { 93 | request.Url.SetQueryParams(values, nullValueHandling); 94 | return request; 95 | } 96 | 97 | /// 98 | /// Adds multiple parameters without values to the URL query. 99 | /// 100 | /// The IFlurlRequest associated with the URL 101 | /// Names of query parameters. 102 | /// This IFlurlRequest 103 | public static IFlurlRequest SetQueryParams(this IFlurlRequest request, IEnumerable names) { 104 | request.Url.SetQueryParams(names); 105 | return request; 106 | } 107 | 108 | /// 109 | /// Adds multiple parameters without values to the URL query. 110 | /// 111 | /// The IFlurlRequest associated with the URL 112 | /// Names of query parameters 113 | /// This IFlurlRequest 114 | public static IFlurlRequest SetQueryParams(this IFlurlRequest request, params string[] names) { 115 | request.Url.SetQueryParams(names as IEnumerable); 116 | return request; 117 | } 118 | 119 | /// 120 | /// Removes a name/value pair from the URL query by name. 121 | /// 122 | /// The IFlurlRequest associated with the URL 123 | /// Query string parameter name to remove 124 | /// This IFlurlRequest 125 | public static IFlurlRequest RemoveQueryParam(this IFlurlRequest request, string name) { 126 | request.Url.RemoveQueryParam(name); 127 | return request; 128 | } 129 | 130 | /// 131 | /// Removes multiple name/value pairs from the URL query by name. 132 | /// 133 | /// The IFlurlRequest associated with the URL 134 | /// Query string parameter names to remove 135 | /// This IFlurlRequest 136 | public static IFlurlRequest RemoveQueryParams(this IFlurlRequest request, params string[] names) { 137 | request.Url.RemoveQueryParams(names); 138 | return request; 139 | } 140 | 141 | /// 142 | /// Removes multiple name/value pairs from the URL query by name. 143 | /// 144 | /// The IFlurlRequest associated with the URL 145 | /// Query string parameter names to remove 146 | /// This IFlurlRequest 147 | public static IFlurlRequest RemoveQueryParams(this IFlurlRequest request, IEnumerable names) { 148 | request.Url.RemoveQueryParams(names); 149 | return request; 150 | } 151 | 152 | /// 153 | /// Set the URL fragment fluently. 154 | /// 155 | /// The IFlurlRequest associated with the URL 156 | /// The part of the URL afer # 157 | /// This IFlurlRequest 158 | public static IFlurlRequest SetFragment(this IFlurlRequest request, string fragment) { 159 | request.Url.SetFragment(fragment); 160 | return request; 161 | } 162 | 163 | /// 164 | /// Removes the URL fragment including the #. 165 | /// 166 | /// The IFlurlRequest associated with the URL 167 | /// This IFlurlRequest 168 | public static IFlurlRequest RemoveFragment(this IFlurlRequest request) { 169 | request.Url.RemoveFragment(); 170 | return request; 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/Flurl.Http/Configuration/FlurlHttpSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using System.Reflection; 5 | using System.Threading.Tasks; 6 | using Flurl.Http.Testing; 7 | 8 | namespace Flurl.Http.Configuration 9 | { 10 | /// 11 | /// A set of properties that affect Flurl.Http behavior 12 | /// 13 | public class FlurlHttpSettings 14 | { 15 | // Values are dictionary-backed so we can check for key existence. Can't do null-coalescing 16 | // because if a setting is set to null at the request level, that should stick. 17 | private readonly IDictionary _vals = new Dictionary(); 18 | 19 | private FlurlHttpSettings _defaults; 20 | 21 | /// 22 | /// Creates a new FlurlHttpSettings object. 23 | /// 24 | public FlurlHttpSettings() { 25 | ResetDefaults(); 26 | } 27 | /// 28 | /// Gets or sets the default values to fall back on when values are not explicitly set on this instance. 29 | /// 30 | public virtual FlurlHttpSettings Defaults { 31 | get => _defaults ?? FlurlHttp.GlobalSettings; 32 | set => _defaults = value; 33 | } 34 | 35 | /// 36 | /// Gets or sets the HTTP request timeout. 37 | /// 38 | public TimeSpan? Timeout { 39 | get => Get(() => Timeout); 40 | set => Set(() => Timeout, value); 41 | } 42 | 43 | /// 44 | /// 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. 45 | /// Examples: "3xx", "100,300,600", "100-299,6xx", "*" (allow everything) 46 | /// 2xx will never throw regardless of this setting. 47 | /// 48 | public string AllowedHttpStatusRange { 49 | get => Get(() => AllowedHttpStatusRange); 50 | set => Set(() => AllowedHttpStatusRange, value); 51 | } 52 | 53 | /// 54 | /// Gets or sets a value indicating whether cookies should be sent/received with each HTTP request. 55 | /// 56 | public bool CookiesEnabled { 57 | get => Get(() => CookiesEnabled); 58 | set => Set(() => CookiesEnabled, value); 59 | } 60 | 61 | /// 62 | /// Gets or sets object used to serialize and deserialize JSON. Default implementation uses Newtonsoft Json.NET. 63 | /// 64 | public ISerializer JsonSerializer { 65 | get => Get(() => JsonSerializer); 66 | set => Set(() => JsonSerializer, value); 67 | } 68 | 69 | /// 70 | /// Gets or sets object used to serialize URL-encoded data. (Deserialization not supported in default implementation.) 71 | /// 72 | public ISerializer UrlEncodedSerializer { 73 | get => Get(() => UrlEncodedSerializer); 74 | set => Set(() => UrlEncodedSerializer, value); 75 | } 76 | 77 | /// 78 | /// Gets or sets a callback that is called immediately before every HTTP request is sent. 79 | /// 80 | public Action BeforeCall { 81 | get => Get(() => BeforeCall); 82 | set => Set(() => BeforeCall, value); 83 | } 84 | 85 | /// 86 | /// Gets or sets a callback that is asynchronously called immediately before every HTTP request is sent. 87 | /// 88 | public Func BeforeCallAsync { 89 | get => Get(() => BeforeCallAsync); 90 | set => Set(() => BeforeCallAsync, value); 91 | } 92 | 93 | /// 94 | /// Gets or sets a callback that is called immediately after every HTTP response is received. 95 | /// 96 | public Action AfterCall { 97 | get => Get(() => AfterCall); 98 | set => Set(() => AfterCall, value); 99 | } 100 | 101 | /// 102 | /// Gets or sets a callback that is asynchronously called immediately after every HTTP response is received. 103 | /// 104 | public Func AfterCallAsync { 105 | get => Get(() => AfterCallAsync); 106 | set => Set(() => AfterCallAsync, value); 107 | } 108 | 109 | /// 110 | /// Gets or sets a callback that is called when an error occurs during any HTTP call, including when any non-success 111 | /// HTTP status code is returned in the response. Response should be null-checked if used in the event handler. 112 | /// 113 | public Action OnError { 114 | get => Get(() => OnError); 115 | set => Set(() => OnError, value); 116 | } 117 | 118 | /// 119 | /// Gets or sets a callback that is asynchronously called when an error occurs during any HTTP call, including when any non-success 120 | /// HTTP status code is returned in the response. Response should be null-checked if used in the event handler. 121 | /// 122 | public Func OnErrorAsync { 123 | get => Get(() => OnErrorAsync); 124 | set => Set(() => OnErrorAsync, value); 125 | } 126 | 127 | /// 128 | /// Resets all overridden settings to their default values. For example, on a FlurlRequest, 129 | /// all settings are reset to FlurlClient-level settings. 130 | /// 131 | public virtual void ResetDefaults() { 132 | _vals.Clear(); 133 | } 134 | 135 | /// 136 | /// Gets a settings value from this instance if explicitly set, otherwise from the default settings that back this instance. 137 | /// 138 | protected T Get(Expression> property) { 139 | var p = (property.Body as MemberExpression).Member as PropertyInfo; 140 | var testVals = HttpTest.Current?.Settings._vals; 141 | return 142 | testVals?.ContainsKey(p.Name) == true ? (T)testVals[p.Name] : 143 | _vals.ContainsKey(p.Name) ? (T)_vals[p.Name] : 144 | Defaults != null ? (T)p.GetValue(Defaults) : 145 | default(T); 146 | } 147 | 148 | /// 149 | /// Sets a settings value for this instance. 150 | /// 151 | protected void Set(Expression> property, T value) { 152 | var p = (property.Body as MemberExpression).Member as PropertyInfo; 153 | _vals[p.Name] = value; 154 | } 155 | } 156 | 157 | /// 158 | /// Client-level settings for Flurl.Http 159 | /// 160 | public class ClientFlurlHttpSettings : FlurlHttpSettings 161 | { 162 | /// 163 | /// Specifies the time to keep the underlying HTTP/TCP conneciton open. When expired, a Connection: close header 164 | /// is sent with the next request, which should force a new connection and DSN lookup to occur on the next call. 165 | /// Default is null, effectively disabling the behavior. 166 | /// 167 | public TimeSpan? ConnectionLeaseTimeout { 168 | get => Get(() => ConnectionLeaseTimeout); 169 | set => Set(() => ConnectionLeaseTimeout, value); 170 | } 171 | 172 | /// 173 | /// Gets or sets a factory used to create the HttpClient and HttpMessageHandler used for HTTP calls. 174 | /// Whenever possible, custom factory implementations should inherit from DefaultHttpClientFactory, 175 | /// only override the method(s) needed, call the base method, and modify the result. 176 | /// 177 | public IHttpClientFactory HttpClientFactory { 178 | get => Get(() => HttpClientFactory); 179 | set => Set(() => HttpClientFactory, value); 180 | } 181 | } 182 | 183 | /// 184 | /// Global default settings for Flurl.Http 185 | /// 186 | public class GlobalFlurlHttpSettings : ClientFlurlHttpSettings 187 | { 188 | internal GlobalFlurlHttpSettings() { 189 | ResetDefaults(); 190 | } 191 | 192 | /// 193 | /// Defaults at the global level do not make sense and will always be null. 194 | /// 195 | public override FlurlHttpSettings Defaults { 196 | get => null; 197 | set => throw new Exception("Global settings cannot be backed by any higher-level defauts."); 198 | } 199 | 200 | /// 201 | /// Gets or sets the factory that defines creating, caching, and reusing FlurlClient instances and, 202 | /// by proxy, HttpClient instances. 203 | /// 204 | public IFlurlClientFactory FlurlClientFactory { 205 | get => Get(() => FlurlClientFactory); 206 | set => Set(() => FlurlClientFactory, value); 207 | } 208 | 209 | /// 210 | /// Resets all global settings to their Flurl.Http-defined default values. 211 | /// 212 | public override void ResetDefaults() { 213 | base.ResetDefaults(); 214 | Timeout = TimeSpan.FromSeconds(100); // same as HttpClient 215 | JsonSerializer = new NewtonsoftJsonSerializer(null); 216 | UrlEncodedSerializer = new DefaultUrlEncodedSerializer(); 217 | FlurlClientFactory = new PerHostFlurlClientFactory(); 218 | HttpClientFactory = new DefaultHttpClientFactory(); 219 | } 220 | } 221 | 222 | /// 223 | /// Settings overrides within the context of an HttpTest 224 | /// 225 | public class TestFlurlHttpSettings : ClientFlurlHttpSettings 226 | { 227 | /// 228 | /// Resets all test settings to their Flurl.Http-defined default values. 229 | /// 230 | public override void ResetDefaults() { 231 | base.ResetDefaults(); 232 | HttpClientFactory = new TestHttpClientFactory(); 233 | } 234 | } 235 | } 236 | --------------------------------------------------------------------------------