From 2279253c5110dea14c03a1fa778d9cd7ee23cc0a Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Fri, 6 Oct 2017 07:50:46 -0500 Subject: [PATCH 01/10] updated package testers for 2.0 --- PackageTesters/PackageTester.NET45/PackageTester.NET45.csproj | 2 +- PackageTesters/PackageTester.NET45/packages.config | 2 +- PackageTesters/PackageTester.NET461/PackageTester.NET461.csproj | 2 +- PackageTesters/PackageTester.NET461/packages.config | 2 +- .../PackageTester.NETCore/PackageTester.NETCore.csproj | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/PackageTesters/PackageTester.NET45/PackageTester.NET45.csproj b/PackageTesters/PackageTester.NET45/PackageTester.NET45.csproj index 7f8c90ff..1c1c3708 100644 --- a/PackageTesters/PackageTester.NET45/PackageTester.NET45.csproj +++ b/PackageTesters/PackageTester.NET45/PackageTester.NET45.csproj @@ -35,7 +35,7 @@ ..\..\packages\Flurl.2.5.0\lib\net40\Flurl.dll - ..\..\packages\Flurl.Http.2.0.0-pre1\lib\net45\Flurl.Http.dll + ..\..\packages\Flurl.Http.2.0.0\lib\net45\Flurl.Http.dll ..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll diff --git a/PackageTesters/PackageTester.NET45/packages.config b/PackageTesters/PackageTester.NET45/packages.config index cebbe1f6..8b8a6d01 100644 --- a/PackageTesters/PackageTester.NET45/packages.config +++ b/PackageTesters/PackageTester.NET45/packages.config @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/PackageTesters/PackageTester.NET461/PackageTester.NET461.csproj b/PackageTesters/PackageTester.NET461/PackageTester.NET461.csproj index e7a57446..6747debb 100644 --- a/PackageTesters/PackageTester.NET461/PackageTester.NET461.csproj +++ b/PackageTesters/PackageTester.NET461/PackageTester.NET461.csproj @@ -37,7 +37,7 @@ ..\..\packages\Flurl.2.5.0\lib\net40\Flurl.dll - ..\..\packages\Flurl.Http.2.0.0-pre1\lib\net45\Flurl.Http.dll + ..\..\packages\Flurl.Http.2.0.0\lib\net45\Flurl.Http.dll ..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll diff --git a/PackageTesters/PackageTester.NET461/packages.config b/PackageTesters/PackageTester.NET461/packages.config index 6d7e87b9..f29f7bd4 100644 --- a/PackageTesters/PackageTester.NET461/packages.config +++ b/PackageTesters/PackageTester.NET461/packages.config @@ -1,6 +1,6 @@  - + \ No newline at end of file diff --git a/PackageTesters/PackageTester.NETCore/PackageTester.NETCore.csproj b/PackageTesters/PackageTester.NETCore/PackageTester.NETCore.csproj index ca76ca1e..33667649 100644 --- a/PackageTesters/PackageTester.NETCore/PackageTester.NETCore.csproj +++ b/PackageTesters/PackageTester.NETCore/PackageTester.NETCore.csproj @@ -8,7 +8,7 @@ - + \ No newline at end of file From ffab17ac39f4d5d92e4f83125a2a637ec370c5d8 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Tue, 28 Nov 2017 12:00:17 -0600 Subject: [PATCH 02/10] #239 glitch with request inheriting correct default settings when client changes --- Test/Flurl.Test/Http/SettingsTests.cs | 37 +++++++++++ .../Configuration/FlurlClientFactoryBase.cs | 3 + .../Configuration/FlurlHttpSettings.cs | 27 ++------ src/Flurl.Http/FlurlClient.cs | 7 ++- src/Flurl.Http/FlurlRequest.cs | 61 +++++++++++-------- 5 files changed, 86 insertions(+), 49 deletions(-) diff --git a/Test/Flurl.Test/Http/SettingsTests.cs b/Test/Flurl.Test/Http/SettingsTests.cs index 1d283b93..1676c89c 100644 --- a/Test/Flurl.Test/Http/SettingsTests.cs +++ b/Test/Flurl.Test/Http/SettingsTests.cs @@ -265,6 +265,43 @@ public class RequestSettingsTests : SettingsTestsBase protected override FlurlHttpSettings GetSettings() => _req.Value.Settings; protected override IFlurlRequest GetRequest() => _req.Value; + + [Test, NonParallelizable] // github #239 + public void request_default_settings_change_when_client_changes() { + FlurlHttp.ConfigureClient("http://test.com", settings => settings.CookiesEnabled = true); + var req = new FlurlRequest("http://test.com"); + var cli1 = req.Client; + Assert.IsTrue(req.Settings.CookiesEnabled, "pre-configured client should provide defaults to new request"); + + req.Url = "http://test.com/foo"; + Assert.AreSame(cli1, req.Client, "new URL with same host should hold onto same client"); + Assert.IsTrue(req.Settings.CookiesEnabled); + + req.Url = "http://test2.com"; + Assert.AreNotSame(cli1, req.Client, "new host should trigger new client"); + Assert.IsFalse(req.Settings.CookiesEnabled); + + FlurlHttp.ConfigureClient("http://test2.com", settings => settings.CookiesEnabled = true); + Assert.IsTrue(req.Settings.CookiesEnabled, "changing client settings should be reflected in request"); + + req.Settings = new FlurlHttpSettings(); + Assert.IsTrue(req.Settings.CookiesEnabled, "entirely new settings object should still inherit current client settings"); + + req.Client = new FlurlClient(); + Assert.IsFalse(req.Settings.CookiesEnabled, "entirely new client should provide new defaults"); + + req.Url = "http://test.com"; + Assert.AreNotSame(cli1, req.Client, "client was explicitly set on request, so it shouldn't change even if the URL changes"); + Assert.IsFalse(req.Settings.CookiesEnabled); + } + + [Test] + public void request_gets_global_settings_when_no_client() { + var req = new FlurlRequest(); + Assert.IsNull(req.Client); + Assert.IsNull(req.Url); + Assert.AreEqual(FlurlHttp.GlobalSettings.JsonSerializer, req.Settings.JsonSerializer); + } } public class SomeCustomHttpClientFactory : IHttpClientFactory diff --git a/src/Flurl.Http/Configuration/FlurlClientFactoryBase.cs b/src/Flurl.Http/Configuration/FlurlClientFactoryBase.cs index b0e89291..8b18e069 100644 --- a/src/Flurl.Http/Configuration/FlurlClientFactoryBase.cs +++ b/src/Flurl.Http/Configuration/FlurlClientFactoryBase.cs @@ -18,6 +18,9 @@ public abstract class FlurlClientFactoryBase : IFlurlClientFactory /// The URL. /// The FlurlClient instance. public virtual IFlurlClient Get(Url url) { + if (url == null) + throw new ArgumentNullException(nameof(url)); + return _clients.AddOrUpdate( GetCacheKey(url), u => Create(u), diff --git a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs index ea4381b6..e6709b1a 100644 --- a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs +++ b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs @@ -12,26 +12,21 @@ namespace Flurl.Http.Configuration /// public class FlurlHttpSettings { - // We need to maintain order of precedence (request > client > global) in some tricky scenarios. - // e.g. if we explicitly set some FlurlRequest.Settings, then set the FlurlClient, we want the - // client-level settings to override the global settings but not the request-level settings. - private FlurlHttpSettings _defaults; - // Values are dictionary-backed so we can check for key existence. Can't do null-coalescing // because if a setting is set to null at the request level, that should stick. private readonly IDictionary _vals = new Dictionary(); /// - /// Creates a new FlurlHttpSettings object using another FlurlHttpSettings object as its default values. + /// Creates a new FlurlHttpSettings object, optionally using another FlurlHttpSettings object as its default values. /// - public FlurlHttpSettings(FlurlHttpSettings defaults) { - _defaults = defaults; + public FlurlHttpSettings(FlurlHttpSettings defaults = null) { + Defaults = defaults; } /// - /// Creates a new FlurlHttpSettings object. + /// Gets or sets the default values to fall back on when values are not explicitly set on this instance. /// - public FlurlHttpSettings() : this(FlurlHttp.GlobalSettings) { } + public FlurlHttpSettings Defaults { get; set; } /// /// Gets or sets the HTTP request timeout. @@ -142,7 +137,7 @@ protected T Get(Expression> property) { return testVals?.ContainsKey(p.Name) == true ? (T)testVals[p.Name] : _vals.ContainsKey(p.Name) ? (T)_vals[p.Name] : - _defaults != null ? (T)p.GetValue(_defaults) : + Defaults != null ? (T)p.GetValue(Defaults) : default(T); } @@ -153,16 +148,6 @@ protected void Set(Expression> property, T value) { var p = (property.Body as MemberExpression).Member as PropertyInfo; _vals[p.Name] = value; } - - /// - /// Merges other settings with this one. Overrides defaults, but does NOT override - /// this settings' explicitly set values. - /// - /// The settings to merge. - public FlurlHttpSettings Merge(FlurlHttpSettings other) { - _defaults = other; - return this; - } } /// diff --git a/src/Flurl.Http/FlurlClient.cs b/src/Flurl.Http/FlurlClient.cs index 94565bfd..6875513e 100644 --- a/src/Flurl.Http/FlurlClient.cs +++ b/src/Flurl.Http/FlurlClient.cs @@ -59,6 +59,7 @@ public interface IFlurlClient : IHttpSettingsContainer, IDisposable { /// public class FlurlClient : IFlurlClient { + private ClientFlurlHttpSettings _settings; private readonly Lazy _httpClient; private readonly Lazy _httpMessageHandler; @@ -68,7 +69,6 @@ public class FlurlClient : IFlurlClient /// The base URL associated with this client. public FlurlClient(string baseUrl = null) { BaseUrl = baseUrl; - Settings = new ClientFlurlHttpSettings(FlurlHttp.GlobalSettings); _httpClient = new Lazy(() => Settings.HttpClientFactory.CreateHttpClient(HttpMessageHandler)); _httpMessageHandler = new Lazy(() => Settings.HttpClientFactory.CreateMessageHandler()); } @@ -77,7 +77,10 @@ public FlurlClient(string baseUrl = null) { public string BaseUrl { get; set; } /// - public ClientFlurlHttpSettings Settings { get; set; } + public ClientFlurlHttpSettings Settings { + get => _settings ?? (_settings = new ClientFlurlHttpSettings(FlurlHttp.GlobalSettings)); + set => _settings = value; + } /// public IDictionary Headers { get; } = new Dictionary(); diff --git a/src/Flurl.Http/FlurlRequest.cs b/src/Flurl.Http/FlurlRequest.cs index 5cbbd896..92638201 100644 --- a/src/Flurl.Http/FlurlRequest.cs +++ b/src/Flurl.Http/FlurlRequest.cs @@ -42,43 +42,60 @@ public interface IFlurlRequest : IHttpSettingsContainer /// public class FlurlRequest : IFlurlRequest { + private FlurlHttpSettings _settings; private IFlurlClient _client; + private Url _url; /// /// Initializes a new instance of the class. /// /// The URL to call with this FlurlRequest instance. public FlurlRequest(Url url = null) { - Settings = new FlurlHttpSettings(); - Url = url; + _url = url; } /// /// Gets or sets the FlurlHttpSettings used by this request. /// - public FlurlHttpSettings Settings { get; set; } - - /// - /// Gets or sets the IFlurlClient to use when sending the request. - /// - public IFlurlClient Client { + public FlurlHttpSettings Settings { get { - if (_client == null) { - _client = FlurlHttp.GlobalSettings.FlurlClientFactory.Get(Url); - Settings.Merge(_client.Settings); + if (_settings == null) { + _settings = new FlurlHttpSettings(); + MergeDefaultSettings(); } - return _client; + return _settings; } + set { + _settings = value; + MergeDefaultSettings(); + } + } + + /// + public IFlurlClient Client { + get => + (_client != null) ? _client : + (Url != null) ? FlurlHttp.GlobalSettings.FlurlClientFactory.Get(Url) : + null; set { _client = value; - Settings.Merge(_client?.Settings ?? FlurlHttp.GlobalSettings); + MergeDefaultSettings(); } } - /// - /// Gets or sets the URL to be called. - /// - public Url Url { get; set; } + /// + public Url Url { + get => _url; + set { + _url = value; + MergeDefaultSettings(); + } + } + + private void MergeDefaultSettings() { + if (_settings != null) + _settings.Defaults = Client?.Settings ?? FlurlHttp.GlobalSettings; + } /// /// Collection of headers sent on this request. @@ -90,15 +107,7 @@ public IFlurlClient Client { /// public IDictionary Cookies => Client.Cookies; - /// - /// Creates and asynchronously sends an HttpRequestMessage. - /// Mainly used to implement higher-level extension methods (GetJsonAsync, etc). - /// - /// The HTTP method used to make the request. - /// Contents of the request body. - /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. Optional. - /// The HttpCompletionOption used in the request. Optional. - /// A Task whose result is the received HttpResponseMessage. + /// public async Task SendAsync(HttpMethod verb, HttpContent content = null, CancellationToken? cancellationToken = null, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) { var request = new HttpRequestMessage(verb, Url) { Content = content }; var call = new HttpCall(this, request); From 6f3fea84f3f56733d4c36b002dfaae156f704995 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Tue, 28 Nov 2017 15:10:07 -0600 Subject: [PATCH 03/10] #246 test settings fix and some minor settings refactoring --- Test/Flurl.Test/Http/SettingsTests.cs | 26 ++++++++++++ .../Configuration/FlurlHttpSettings.cs | 40 +++++++++++++------ src/Flurl.Http/FlurlClient.cs | 2 +- src/Flurl.Http/FlurlRequest.cs | 12 +++--- src/Flurl.Http/Testing/HttpTest.cs | 4 +- 5 files changed, 63 insertions(+), 21 deletions(-) diff --git a/Test/Flurl.Test/Http/SettingsTests.cs b/Test/Flurl.Test/Http/SettingsTests.cs index 1676c89c..2e3ab43e 100644 --- a/Test/Flurl.Test/Http/SettingsTests.cs +++ b/Test/Flurl.Test/Http/SettingsTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net.Http; using System.Threading.Tasks; @@ -206,6 +207,31 @@ public class HttpTestSettingsTests : SettingsTestsBase protected override FlurlHttpSettings GetSettings() => HttpTest.Current.Settings; protected override IFlurlRequest GetRequest() => new FlurlRequest("http://api.com"); + + [Test] // github #246 + public void test_settings_dont_override_request_settings_when_not_set_explicitily() { + var ser1 = new FakeSerializer(); + var ser2 = new FakeSerializer(); + + using (var test = new HttpTest()) { + var cli = new FlurlClient(); + cli.Settings.JsonSerializer = ser1; + Assert.AreSame(ser1, cli.Settings.JsonSerializer); + + var req = new FlurlRequest { Client = cli }; + Assert.AreSame(ser1, req.Settings.JsonSerializer); + + req.Settings.JsonSerializer = ser2; + Assert.AreSame(ser2, req.Settings.JsonSerializer); + } + } + + private class FakeSerializer : ISerializer + { + public string Serialize(object obj) => "foo"; + public T Deserialize(string s) => default(T); + public T Deserialize(Stream stream) => default(T); + } } [TestFixture, Parallelizable] diff --git a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs index e6709b1a..18d744ce 100644 --- a/src/Flurl.Http/Configuration/FlurlHttpSettings.cs +++ b/src/Flurl.Http/Configuration/FlurlHttpSettings.cs @@ -16,17 +16,21 @@ public class FlurlHttpSettings // because if a setting is set to null at the request level, that should stick. private readonly IDictionary _vals = new Dictionary(); + private FlurlHttpSettings _defaults; + /// - /// Creates a new FlurlHttpSettings object, optionally using another FlurlHttpSettings object as its default values. + /// Creates a new FlurlHttpSettings object. /// - public FlurlHttpSettings(FlurlHttpSettings defaults = null) { - Defaults = defaults; + public FlurlHttpSettings() { + ResetDefaults(); } - /// /// Gets or sets the default values to fall back on when values are not explicitly set on this instance. /// - public FlurlHttpSettings Defaults { get; set; } + public virtual FlurlHttpSettings Defaults { + get => _defaults ?? FlurlHttp.GlobalSettings; + set => _defaults = value; + } /// /// Gets or sets the HTTP request timeout. @@ -155,11 +159,6 @@ protected void Set(Expression> property, T value) { /// public class ClientFlurlHttpSettings : FlurlHttpSettings { - /// - /// Creates a new FlurlHttpSettings object using another FlurlHttpSettings object as its default values. - /// - public ClientFlurlHttpSettings(FlurlHttpSettings defaults) : base(defaults) { } - /// /// Specifies the time to keep the underlying HTTP/TCP conneciton open. When expired, a Connection: close header /// is sent with the next request, which should force a new connection and DSN lookup to occur on the next call. @@ -186,10 +185,18 @@ public IHttpClientFactory HttpClientFactory { /// public class GlobalFlurlHttpSettings : ClientFlurlHttpSettings { - internal GlobalFlurlHttpSettings() : base(null) { + internal GlobalFlurlHttpSettings() { ResetDefaults(); } + /// + /// Defaults at the global level do not make sense and will always be null. + /// + public override FlurlHttpSettings Defaults { + get => null; + set => throw new Exception("Global settings cannot be backed by any higher-level defauts."); + } + /// /// Gets or sets the factory that defines creating, caching, and reusing FlurlClient instances and, /// by proxy, HttpClient instances. @@ -215,8 +222,17 @@ public override void ResetDefaults() { /// /// Settings overrides within the context of an HttpTest /// - public class TestFlurlHttpSettings : GlobalFlurlHttpSettings + public class TestFlurlHttpSettings : ClientFlurlHttpSettings { + /// + /// Gets or sets the factory that defines creating, caching, and reusing FlurlClient instances + /// within the context of this HttpTest + /// + public IFlurlClientFactory FlurlClientFactory { + get => Get(() => FlurlClientFactory); + set => Set(() => FlurlClientFactory, value); + } + /// /// Resets all test settings to their Flurl.Http-defined default values. /// diff --git a/src/Flurl.Http/FlurlClient.cs b/src/Flurl.Http/FlurlClient.cs index 6875513e..dc6b788f 100644 --- a/src/Flurl.Http/FlurlClient.cs +++ b/src/Flurl.Http/FlurlClient.cs @@ -78,7 +78,7 @@ public FlurlClient(string baseUrl = null) { /// public ClientFlurlHttpSettings Settings { - get => _settings ?? (_settings = new ClientFlurlHttpSettings(FlurlHttp.GlobalSettings)); + get => _settings ?? (_settings = new ClientFlurlHttpSettings()); set => _settings = value; } diff --git a/src/Flurl.Http/FlurlRequest.cs b/src/Flurl.Http/FlurlRequest.cs index 92638201..c2e846eb 100644 --- a/src/Flurl.Http/FlurlRequest.cs +++ b/src/Flurl.Http/FlurlRequest.cs @@ -61,13 +61,13 @@ public FlurlHttpSettings Settings { get { if (_settings == null) { _settings = new FlurlHttpSettings(); - MergeDefaultSettings(); + ResetDefaultSettings(); } return _settings; } set { _settings = value; - MergeDefaultSettings(); + ResetDefaultSettings(); } } @@ -79,7 +79,7 @@ public IFlurlClient Client { null; set { _client = value; - MergeDefaultSettings(); + ResetDefaultSettings(); } } @@ -88,13 +88,13 @@ public Url Url { get => _url; set { _url = value; - MergeDefaultSettings(); + ResetDefaultSettings(); } } - private void MergeDefaultSettings() { + private void ResetDefaultSettings() { if (_settings != null) - _settings.Defaults = Client?.Settings ?? FlurlHttp.GlobalSettings; + _settings.Defaults = Client?.Settings; } /// diff --git a/src/Flurl.Http/Testing/HttpTest.cs b/src/Flurl.Http/Testing/HttpTest.cs index da8adc95..0bae6900 100644 --- a/src/Flurl.Http/Testing/HttpTest.cs +++ b/src/Flurl.Http/Testing/HttpTest.cs @@ -37,7 +37,7 @@ public HttpTest() { /// /// Gets or sets the FlurlHttpSettings object used by this test. /// - public GlobalFlurlHttpSettings Settings { get; set; } + public TestFlurlHttpSettings Settings { get; set; } /// /// Gets the current HttpTest from the logical (async) call context @@ -59,7 +59,7 @@ public HttpTest() { /// /// Action defining the settings changes. /// This HttpTest - public HttpTest Configure(Action action) { + public HttpTest Configure(Action action) { action(Settings); return this; } From eed12148bebf02c2aebe4b4be2e68aec72d0d21b Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Tue, 28 Nov 2017 15:45:42 -0600 Subject: [PATCH 04/10] #240 DownloadFileAsync should get default filename from Content-Disposition header when it exists --- Test/Flurl.Test/Http/RealHttpTests.cs | 37 +++++++++++++++++++++++---- src/Flurl.Http/DownloadExtensions.cs | 10 +++++--- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/Test/Flurl.Test/Http/RealHttpTests.cs b/Test/Flurl.Test/Http/RealHttpTests.cs index fbb1401a..2c26539d 100644 --- a/Test/Flurl.Test/Http/RealHttpTests.cs +++ b/Test/Flurl.Test/Http/RealHttpTests.cs @@ -20,11 +20,38 @@ public class RealHttpTests [Test] public async Task can_download_file() { var folder = "c:\\flurl-test-" + Guid.NewGuid(); // random so parallel tests don't trip over each other - var path = await "https://www.google.com".DownloadFileAsync(folder, "google.txt"); - Assert.AreEqual($@"{folder}\google.txt", path); - Assert.That(File.Exists(path)); - File.Delete(path); - Directory.Delete(folder, true); + try { + var path = await "https://www.google.com".DownloadFileAsync(folder, "google.txt"); + Assert.AreEqual($@"{folder}\google.txt", path); + Assert.That(File.Exists(path)); + } + finally { + Directory.Delete(folder, true); + } + } + + [Test] + public async Task can_download_file_with_default_name() { + var folder = "c:\\flurl-test-" + Guid.NewGuid(); // random so parallel tests don't trip over each other + try { + // no Content-Dispositon header, use last part of URL + var path = await "https://www.google.com".DownloadFileAsync(folder); + Assert.AreEqual($@"{folder}\www.google.com", path); + Assert.That(File.Exists(path)); + + // has Content-Disposition header but no filename in it, use last part of URL + path = await "https://httpbin.org/response-headers?Content-Disposition=attachment".DownloadFileAsync(folder); + Assert.AreEqual($@"{folder}\response-headers", path); + Assert.That(File.Exists(path)); + + // has header Content-Disposition: attachment; filename="myfile.txt" + path = await "https://httpbin.org/response-headers?Content-Disposition=attachment%3B%20filename%3D%22myfile.txt%22".DownloadFileAsync(folder); + Assert.AreEqual($@"{folder}\myfile.txt", path); + Assert.That(File.Exists(path)); + } + finally { + Directory.Delete(folder, true); + } } [Test] diff --git a/src/Flurl.Http/DownloadExtensions.cs b/src/Flurl.Http/DownloadExtensions.cs index 1b9259ff..6436dc38 100644 --- a/src/Flurl.Http/DownloadExtensions.cs +++ b/src/Flurl.Http/DownloadExtensions.cs @@ -14,15 +14,17 @@ public static class DownloadExtensions /// /// The flurl request. /// Path of local folder where file is to be downloaded. - /// Name of local file. If not specified, the source filename (last segment of the URL) is used. + /// Name of local file. If not specified, the source filename (from Content-Dispostion header, or last segment of the URL) is used. /// Buffer size in bytes. Default is 4096. /// A Task whose result is the local path of the downloaded file. public static async Task DownloadFileAsync(this IFlurlRequest request, string localFolderPath, string localFileName = null, int bufferSize = 4096) { - if (localFileName == null) - localFileName = request.Url.Path.Split('/').Last(); - var response = await request.SendAsync(HttpMethod.Get, completionOption: HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); + localFileName = + localFileName ?? + response.Content?.Headers.ContentDisposition?.FileName?.Trim().TrimStart('"').TrimEnd('"') ?? + request.Url.Path.Split('/').Last(); + // http://codereview.stackexchange.com/a/18679 using (var httpStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) using (var filestream = await FileUtil.OpenWriteAsync(localFolderPath, localFileName, bufferSize).ConfigureAwait(false)) { From f990350979e838100e6c7b3f5c8a2c3e3a86bd00 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Tue, 28 Nov 2017 16:14:41 -0600 Subject: [PATCH 05/10] #247 ConfigureClient takes Action instead of Action --- Test/Flurl.Test/Http/SettingsTests.cs | 6 +++--- src/Flurl.Http/FlurlHttp.cs | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Test/Flurl.Test/Http/SettingsTests.cs b/Test/Flurl.Test/Http/SettingsTests.cs index 2e3ab43e..5ad942ce 100644 --- a/Test/Flurl.Test/Http/SettingsTests.cs +++ b/Test/Flurl.Test/Http/SettingsTests.cs @@ -188,7 +188,7 @@ public void can_configure_global_from_FlurlHttp_object() { [Test] public void can_configure_client_from_FlurlHttp_object() { - FlurlHttp.ConfigureClient("http://host1.com/foo", settings => settings.CookiesEnabled = true); + FlurlHttp.ConfigureClient("http://host1.com/foo", cli => cli.Settings.CookiesEnabled = true); Assert.IsTrue(new FlurlRequest("https://host1.com/bar").Client.Settings.CookiesEnabled); // different URL but same host, so should use same client Assert.IsFalse(new FlurlRequest("http://host2.com").Client.Settings.CookiesEnabled); } @@ -294,7 +294,7 @@ public class RequestSettingsTests : SettingsTestsBase [Test, NonParallelizable] // github #239 public void request_default_settings_change_when_client_changes() { - FlurlHttp.ConfigureClient("http://test.com", settings => settings.CookiesEnabled = true); + FlurlHttp.ConfigureClient("http://test.com", cli => cli.Settings.CookiesEnabled = true); var req = new FlurlRequest("http://test.com"); var cli1 = req.Client; Assert.IsTrue(req.Settings.CookiesEnabled, "pre-configured client should provide defaults to new request"); @@ -307,7 +307,7 @@ public void request_default_settings_change_when_client_changes() { Assert.AreNotSame(cli1, req.Client, "new host should trigger new client"); Assert.IsFalse(req.Settings.CookiesEnabled); - FlurlHttp.ConfigureClient("http://test2.com", settings => settings.CookiesEnabled = true); + FlurlHttp.ConfigureClient("http://test2.com", cli => cli.Settings.CookiesEnabled = true); Assert.IsTrue(req.Settings.CookiesEnabled, "changing client settings should be reflected in request"); req.Settings = new FlurlHttpSettings(); diff --git a/src/Flurl.Http/FlurlHttp.cs b/src/Flurl.Http/FlurlHttp.cs index 9386fba5..505a481f 100644 --- a/src/Flurl.Http/FlurlHttp.cs +++ b/src/Flurl.Http/FlurlHttp.cs @@ -29,15 +29,15 @@ public static void Configure(Action configAction) { } /// - /// Provides thread-safe access to the Settings associated with a specific IFlurlClient. The URL is used to find the client, - /// but keep in mind that the same client will be used in all calls to the same host by default. + /// Provides thread-safe access to a specific IFlurlClient, typically to configure settings and default headers. + /// The URL is used to find the client, but keep in mind that the same client will be used in all calls to the same host by default. /// /// the URL used to find the IFlurlClient - /// the action to perform against the IFlurlClient's Settings - public static void ConfigureClient(string url, Action configAction) { + /// the action to perform against the IFlurlClient + public static void ConfigureClient(string url, Action configAction) { var client = GlobalSettings.FlurlClientFactory.Get(url); lock (_configLock) { - configAction(client.Settings); + configAction(client); } } } From 93767062612e2658011ea0f37a1e98fdfabf0115 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Tue, 28 Nov 2017 16:37:39 -0600 Subject: [PATCH 06/10] just reformatting --- Test/Flurl.Test/UrlBuilderTests.cs | 84 ++++++++++-------------------- 1 file changed, 28 insertions(+), 56 deletions(-) diff --git a/Test/Flurl.Test/UrlBuilderTests.cs b/Test/Flurl.Test/UrlBuilderTests.cs index 66b136db..c2c031d6 100644 --- a/Test/Flurl.Test/UrlBuilderTests.cs +++ b/Test/Flurl.Test/UrlBuilderTests.cs @@ -13,14 +13,12 @@ public class UrlBuilderTests { [Test] // check that for every Url method, we have an equivalent string extension - public void extension_methods_consistently_supported() - { + public void extension_methods_consistently_supported() { var urlMethods = typeof(Url).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly).Where(m => !m.IsSpecialName); var stringExts = ReflectionHelper.GetAllExtensionMethods(typeof(Url).GetTypeInfo().Assembly); var whitelist = new[] { "ToString", "IsValid" }; // cases where string extension of the same name was excluded intentionally - foreach (var method in urlMethods) - { + foreach (var method in urlMethods) { if (whitelist.Contains(method.Name)) continue; @@ -40,8 +38,7 @@ public void can_parse_url_parts() { } [Test] - public void can_parse_query_params() - { + public void can_parse_query_params() { var q = new Url("http://www.mysite.com/more?x=1&y=2&z=3&y=4&abc&xyz&foo=&=bar&y=6").QueryParams; Assert.AreEqual(9, q.Count); @@ -72,8 +69,7 @@ public void can_set_query_params() { } [Test] - public void can_modify_query_param_array() - { + public void can_modify_query_param_array() { var url = new Url("http://www.mysite.com/more?x=1&y=2&x=2&z=4"); // go from 2 values to 3, order should be preserved url.QueryParams["x"] = new[] { 8, 9, 10 }; @@ -170,8 +166,7 @@ public void removing_nonexisting_query_params_is_ignored() { } [Test] - public void can_sort_query_params() - { + public void can_sort_query_params() { var url = new Url("http://www.mysite.com/more?z=1&y=2&x=3"); url.QueryParams.Sort((x, y) => x.Name.CompareTo(y.Name)); Assert.AreEqual("http://www.mysite.com/more?x=3&y=2&z=1", url.ToString()); @@ -185,8 +180,7 @@ public string can_control_null_value_behavior_in_query_params(NullValueHandling } [Test] - public void constructor_requires_nonnull_arg() - { + public void constructor_requires_nonnull_arg() { Assert.Throws(() => new Url(null)); } @@ -213,8 +207,7 @@ public void Combine_encodes_illegal_chars() { } [Test] - public void GetRoot_works() - { + public void GetRoot_works() { // simple case var root = Url.GetRoot("http://mysite.com/one/two/three"); Assert.AreEqual("http://mysite.com", root); @@ -225,28 +218,24 @@ public void GetRoot_works() } [Test] - public void can_append_path_segment() - { + public void can_append_path_segment() { var url = "http://www.mysite.com".AppendPathSegment("endpoint"); Assert.AreEqual("http://www.mysite.com/endpoint", url.ToString()); } [Test] - public void appending_null_path_segment_throws_arg_null_ex() - { + public void appending_null_path_segment_throws_arg_null_ex() { Assert.Throws(() => "http://www.mysite.com".AppendPathSegment(null)); } [Test] - public void can_append_multiple_path_segments_by_multi_args() - { + public void can_append_multiple_path_segments_by_multi_args() { var url = "http://www.mysite.com".AppendPathSegments("category", "/endpoint/"); Assert.AreEqual("http://www.mysite.com/category/endpoint/", url.ToString()); } [Test] - public void can_append_multiple_path_segments_by_enumerable() - { + public void can_append_multiple_path_segments_by_enumerable() { IEnumerable segments = new[] { "/category/", "endpoint" }; var url = "http://www.mysite.com".AppendPathSegments(segments); Assert.AreEqual("http://www.mysite.com/category/endpoint", url.ToString()); @@ -254,8 +243,7 @@ public void can_append_multiple_path_segments_by_enumerable() #if !NETCOREAPP1_1 [Test] - public void url_ToString_uses_invariant_culture() - { + public void url_ToString_uses_invariant_culture() { Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("es-ES"); var url = "http://www.mysite.com".SetQueryParam("x", 1.1); Assert.AreEqual("http://www.mysite.com?x=1.1", url.ToString()); @@ -263,15 +251,13 @@ public void url_ToString_uses_invariant_culture() #endif [Test] - public void can_reset_to_root() - { + public void can_reset_to_root() { var url = "http://www.mysite.com/more?x=1&y=2#foo".ResetToRoot(); Assert.AreEqual("http://www.mysite.com", url.ToString()); } [Test] - public void can_do_crazy_long_fluent_expression() - { + public void can_do_crazy_long_fluent_expression() { var url = "http://www.mysite.com" .SetQueryParams(new { a = 1, b = 2, c = 999 }) .SetFragment("fooey") @@ -286,65 +272,56 @@ public void can_do_crazy_long_fluent_expression() } [Test] - public void encodes_invalid_path_chars() - { + public void encodes_invalid_path_chars() { var url = "http://www.mysite.com".AppendPathSegment("hey there how are ya"); Assert.AreEqual("http://www.mysite.com/hey%20there%20how%20are%20ya", url.ToString()); } [Test] - public void does_not_reencode_path_escape_chars() - { + public void does_not_reencode_path_escape_chars() { var url = "http://www.mysite.com".AppendPathSegment("hey%20there%20how%20are%20ya"); Assert.AreEqual("http://www.mysite.com/hey%20there%20how%20are%20ya", url.ToString()); } [Test] - public void encodes_query_params() - { + public void encodes_query_params() { var url = "http://www.mysite.com".SetQueryParams(new { x = "$50", y = "2+2=4" }); Assert.AreEqual("http://www.mysite.com?x=%2450&y=2%2B2%3D4", url.ToString()); } [Test] - public void does_not_reencode_encoded_query_values() - { + public void does_not_reencode_encoded_query_values() { var url = "http://www.mysite.com".SetQueryParam("x", "%CD%EE%E2%FB%E9%20%E3%EE%E4", true); Assert.AreEqual("http://www.mysite.com?x=%CD%EE%E2%FB%E9%20%E3%EE%E4", url.ToString()); } [Test] - public void reencodes_encoded_query_values_when_isEncoded_false() - { + public void reencodes_encoded_query_values_when_isEncoded_false() { var url = "http://www.mysite.com".SetQueryParam("x", "%CD%EE%E2%FB%E9%20%E3%EE%E4", false); Assert.AreEqual("http://www.mysite.com?x=%25CD%25EE%25E2%25FB%25E9%2520%25E3%25EE%25E4", url.ToString()); } [Test] - public void Url_implicitly_converts_to_string() - { + public void Url_implicitly_converts_to_string() { var url = new Url("http://www.mysite.com/more?x=1&y=2"); var someMethodThatTakesAString = new Action(s => { }); someMethodThatTakesAString(url); // if this compiles, test passed. } [Test] - public void interprets_plus_as_space() - { + public void interprets_plus_as_space() { var url = new Url("http://www.mysite.com/foo+bar?x=1+2"); Assert.AreEqual("1 2", url.QueryParams["x"]); } [Test] - public void can_encode_space_as_plus() - { + public void can_encode_space_as_plus() { var url = new Url("http://www.mysite.com/foo+bar?x=1+2"); Assert.AreEqual("http://www.mysite.com/foo+bar?x=1+2", url.ToString(true)); } [Test] - public void encodes_plus() - { + public void encodes_plus() { var url = new Url("http://www.mysite.com").SetQueryParam("x", "1+2"); Assert.AreEqual("http://www.mysite.com?x=1%2B2", url.ToString()); } @@ -357,16 +334,14 @@ public void encodes_plus() [TestCase("blah", false)] [TestCase("http:/www.mysite.com", false)] [TestCase("www.mysite.com", false)] - public void IsUrl_works(string s, bool isValid) - { + public void IsUrl_works(string s, bool isValid) { Assert.AreEqual(isValid, Url.IsValid(s)); Assert.AreEqual(isValid, new Url(s).IsValid()); } // #56 [Test] - public void does_not_alter_url_passed_to_constructor() - { + public void does_not_alter_url_passed_to_constructor() { var expected = "http://www.mysite.com/hi%20there/more?x=%CD%EE%E2%FB%E9%20%E3%EE%E4"; var url = new Url(expected); Assert.AreEqual(expected, url.ToString()); @@ -374,8 +349,7 @@ public void does_not_alter_url_passed_to_constructor() // #29 [Test] - public void can_add_and_remove_fragment_fluently() - { + public void can_add_and_remove_fragment_fluently() { var url = "http://www.mysite.com".SetFragment("foo"); Assert.AreEqual("http://www.mysite.com#foo", url.ToString()); url = "http://www.mysite.com#foo".RemoveFragment(); @@ -391,8 +365,7 @@ public void can_add_and_remove_fragment_fluently() } [Test] - public void has_fragment_after_SetQueryParam() - { + public void has_fragment_after_SetQueryParam() { var expected = "http://www.mysite.com/more?x=1#first"; var url = new Url(expected) .SetQueryParam("x", 3) @@ -404,8 +377,7 @@ public void has_fragment_after_SetQueryParam() [TestCase("http://www.mysite.com/with/path?x=1#foo", "http://www.mysite.com/with/path", "x=1", "foo")] [TestCase("http://www.mysite.com/with/path?x=1?y=2", "http://www.mysite.com/with/path", "x=1?y=2", "")] [TestCase("http://www.mysite.com/#with/path?x=1?y=2", "http://www.mysite.com/", "", "with/path?x=1?y=2")] - public void constructor_parses_url_correctly(string full, string path, string query, string fragment) - { + public void constructor_parses_url_correctly(string full, string path, string query, string fragment) { var url = new Url(full); Assert.AreEqual(path, url.Path); Assert.AreEqual(query, url.Query); From 19ea54d98077c36324040549c349085e0841163b Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Wed, 29 Nov 2017 09:29:18 -0600 Subject: [PATCH 07/10] added StripQuotes utility method --- Test/Flurl.Test/CommonExtensionsTests.cs | 5 +++++ src/Flurl.Http/DownloadExtensions.cs | 3 ++- src/Flurl.Http/HttpResponseMessageExtensions.cs | 3 ++- src/Flurl/Util/CommonExtensions.cs | 5 +++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Test/Flurl.Test/CommonExtensionsTests.cs b/Test/Flurl.Test/CommonExtensionsTests.cs index 9621761b..7c4e220e 100644 --- a/Test/Flurl.Test/CommonExtensionsTests.cs +++ b/Test/Flurl.Test/CommonExtensionsTests.cs @@ -103,5 +103,10 @@ public void SplitOnFirstOccurence_works() { var result = "hello/how/are/you".SplitOnFirstOccurence('/'); Assert.AreEqual(new[] { "hello", "how/are/you" }, result); } + + [TestCase(" \"\thi there \" \t\t ", ExpectedResult = "\thi there ")] + [TestCase(" ' hi there ' ", ExpectedResult = " hi there ")] + [TestCase(" hi there ", ExpectedResult = " hi there ")] + public string StripQuotes_works(string s) => s.StripQuotes(); } } diff --git a/src/Flurl.Http/DownloadExtensions.cs b/src/Flurl.Http/DownloadExtensions.cs index 6436dc38..c083fe25 100644 --- a/src/Flurl.Http/DownloadExtensions.cs +++ b/src/Flurl.Http/DownloadExtensions.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Net.Http; using System.Threading.Tasks; +using Flurl.Util; namespace Flurl.Http { @@ -22,7 +23,7 @@ public static async Task DownloadFileAsync(this IFlurlRequest request, s localFileName = localFileName ?? - response.Content?.Headers.ContentDisposition?.FileName?.Trim().TrimStart('"').TrimEnd('"') ?? + response.Content?.Headers.ContentDisposition?.FileName?.StripQuotes() ?? request.Url.Path.Split('/').Last(); // http://codereview.stackexchange.com/a/18679 diff --git a/src/Flurl.Http/HttpResponseMessageExtensions.cs b/src/Flurl.Http/HttpResponseMessageExtensions.cs index 6c6eb305..2d7cad87 100644 --- a/src/Flurl.Http/HttpResponseMessageExtensions.cs +++ b/src/Flurl.Http/HttpResponseMessageExtensions.cs @@ -7,6 +7,7 @@ using System.Text; #endif using System.Threading.Tasks; +using Flurl.Util; namespace Flurl.Http { @@ -101,7 +102,7 @@ public static async Task ReceiveBytes(this Task res internal static HttpContent StripCharsetQuotes(this HttpContent content) { var header = content?.Headers?.ContentType; if (header?.CharSet != null) - header.CharSet = header.CharSet.Trim().TrimStart('"').TrimEnd('"'); + header.CharSet = header.CharSet.StripQuotes(); return content; } } diff --git a/src/Flurl/Util/CommonExtensions.cs b/src/Flurl/Util/CommonExtensions.cs index d9041854..aaf25098 100644 --- a/src/Flurl/Util/CommonExtensions.cs +++ b/src/Flurl/Util/CommonExtensions.cs @@ -123,5 +123,10 @@ public static void Merge(this IDictionary d1, IDicti foreach (var kv in d2.Where(x => !d1.Keys.Contains(x.Key))) d1.Add(kv); } + + /// + /// Strips any single quotes or double quotes from the beginning and end of a string. + /// + public static string StripQuotes(this string s) => Regex.Replace(s, "^\\s*['\"]+|['\"]+\\s*$", ""); } } \ No newline at end of file From b9628b12f025ad2f4974accbaca8903adf500b36 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Wed, 29 Nov 2017 13:00:13 -0600 Subject: [PATCH 08/10] #249 use ISO 8601 for all date serialization --- Test/Flurl.Test/CommonExtensionsTests.cs | 7 +++++++ src/Flurl/Util/CommonExtensions.cs | 19 +++++++++---------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Test/Flurl.Test/CommonExtensionsTests.cs b/Test/Flurl.Test/CommonExtensionsTests.cs index 7c4e220e..7a949bfe 100644 --- a/Test/Flurl.Test/CommonExtensionsTests.cs +++ b/Test/Flurl.Test/CommonExtensionsTests.cs @@ -108,5 +108,12 @@ public void SplitOnFirstOccurence_works() { [TestCase(" ' hi there ' ", ExpectedResult = " hi there ")] [TestCase(" hi there ", ExpectedResult = " hi there ")] public string StripQuotes_works(string s) => s.StripQuotes(); + + [Test] + public void ToInvariantString_serializes_dates_to_iso() { + Assert.AreEqual("2017-12-01T02:34:56.7890000", new DateTime(2017, 12, 1, 2, 34, 56, 789, DateTimeKind.Unspecified).ToInvariantString()); + Assert.AreEqual("2017-12-01T02:34:56.7890000Z", new DateTime(2017, 12, 1, 2, 34, 56, 789, DateTimeKind.Utc).ToInvariantString()); + Assert.AreEqual("2017-12-01T02:34:56.7890000-06:00", new DateTimeOffset(2017, 12, 1, 2, 34, 56, 789, TimeSpan.FromHours(-6)).ToInvariantString()); + } } } diff --git a/src/Flurl/Util/CommonExtensions.cs b/src/Flurl/Util/CommonExtensions.cs index aaf25098..2e39a19c 100644 --- a/src/Flurl/Util/CommonExtensions.cs +++ b/src/Flurl/Util/CommonExtensions.cs @@ -27,27 +27,26 @@ public static IEnumerable> ToKeyValuePairs(this obj throw new ArgumentNullException(nameof(obj)); return - (obj is string) ? StringToKV((string)obj) : - (obj is IEnumerable) ? CollectionToKV((IEnumerable)obj) : + obj is string s ? StringToKV(s) : + obj is IEnumerable e ? CollectionToKV(e) : ObjectToKV(obj); } /// /// Returns a string that represents the current object, using CultureInfo.InvariantCulture where possible. + /// Dates are represented in IS0 8601. /// public static string ToInvariantString(this object obj) { // inspired by: http://stackoverflow.com/a/19570016/62600 + return + obj is DateTime dt ? dt.ToString("o", CultureInfo.InvariantCulture) : + obj is DateTimeOffset dto ? dto.ToString("o", CultureInfo.InvariantCulture) : #if !NETSTANDARD1_0 - var c = obj as IConvertible; - if (c != null) - return c.ToString(CultureInfo.InvariantCulture); + obj is IConvertible c ? c.ToString(CultureInfo.InvariantCulture) : #endif - var f = obj as IFormattable; - if (f != null) - return f.ToString(null, CultureInfo.InvariantCulture); - - return obj.ToString(); + obj is IFormattable f ? f.ToString(null, CultureInfo.InvariantCulture) : + obj.ToString(); } /// From 09508fde07d92d82ddee893240eb003d5f4d2576 Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Wed, 29 Nov 2017 13:21:58 -0600 Subject: [PATCH 09/10] a couple null checks and code tweaks --- src/Flurl/QueryParameter.cs | 2 +- src/Flurl/Url.cs | 6 +++--- src/Flurl/Util/CommonExtensions.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Flurl/QueryParameter.cs b/src/Flurl/QueryParameter.cs index 5ff73bb4..73c89330 100644 --- a/src/Flurl/QueryParameter.cs +++ b/src/Flurl/QueryParameter.cs @@ -38,7 +38,7 @@ public QueryParameter(string name, object value, bool isEncoded = false) { /// The value (right side) of the query parameter. /// public object Value { - get { return _value; } + get => _value; set { _value = value; _encodedValue = null; diff --git a/src/Flurl/Url.cs b/src/Flurl/Url.cs index 08b3405c..7e0ce1ba 100644 --- a/src/Flurl/Url.cs +++ b/src/Flurl/Url.cs @@ -19,8 +19,8 @@ public class Url /// The query part of the URL (after the ?, RFC 3986). /// public string Query { - get { return QueryParams.ToString(); } - set { QueryParams = ParseQueryParams(value); } + get => QueryParams.ToString(); + set => QueryParams = ParseQueryParams(value); } /// @@ -377,7 +377,7 @@ public string ToString(bool encodeSpaceAsPlus) { /// the Url object /// The string public static implicit operator string(Url url) { - return url.ToString(); + return url?.ToString(); } /// diff --git a/src/Flurl/Util/CommonExtensions.cs b/src/Flurl/Util/CommonExtensions.cs index 2e39a19c..f6a09034 100644 --- a/src/Flurl/Util/CommonExtensions.cs +++ b/src/Flurl/Util/CommonExtensions.cs @@ -38,8 +38,8 @@ public static IEnumerable> ToKeyValuePairs(this obj /// public static string ToInvariantString(this object obj) { // inspired by: http://stackoverflow.com/a/19570016/62600 - return + obj == null ? null : obj is DateTime dt ? dt.ToString("o", CultureInfo.InvariantCulture) : obj is DateTimeOffset dto ? dto.ToString("o", CultureInfo.InvariantCulture) : #if !NETSTANDARD1_0 From 1c62603b1c80cbdd4f0ab28ded49f8096ed581ac Mon Sep 17 00:00:00 2001 From: Todd Menier Date: Wed, 29 Nov 2017 14:53:07 -0600 Subject: [PATCH 10/10] bump Flurl version --- src/Flurl/Flurl.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Flurl/Flurl.csproj b/src/Flurl/Flurl.csproj index 783170d7..a2469a19 100644 --- a/src/Flurl/Flurl.csproj +++ b/src/Flurl/Flurl.csproj @@ -4,7 +4,7 @@ net40;netstandard1.3;netstandard1.0; True Flurl - 2.5.0 + 2.5.1 Todd Menier A fluent, portable URL builder. To make HTTP calls off the fluent chain, check out Flurl.Http. http://tmenier.github.io/Flurl