cookies, headers, and new collection types (#541)
This commit is contained in:
parent
b2658232c4
commit
5f59f4f071
@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net45;netcoreapp2.0;</TargetFrameworks>
|
||||
<TargetFrameworks>net471;netcoreapp2.0;</TargetFrameworks>
|
||||
<TargetFrameworks Condition="'$(OS)' != 'Windows_NT'">netcoreapp2.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
<ProjectReference Include="..\..\src\Flurl.Http\Flurl.Http.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net45'">
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net471'">
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
</ItemGroup>
|
||||
|
@ -28,7 +28,7 @@ namespace Flurl.Test.Http
|
||||
HttpTest.ShouldHaveMadeACall().WithCookies(new { y = "bar", z = "fizz" }).Times(1);
|
||||
HttpTest.ShouldHaveMadeACall().WithoutCookies().Times(1);
|
||||
|
||||
Assert.AreEqual("bar", responses[0].Cookies.TryGetValue("x", out var c) ? c.Value : null);
|
||||
Assert.AreEqual("bar", responses[0].Cookies.FirstOrDefault(c => c.Name == "x")?.Value);
|
||||
Assert.IsEmpty(responses[1].Cookies);
|
||||
Assert.IsEmpty(responses[2].Cookies);
|
||||
}
|
||||
@ -57,8 +57,8 @@ namespace Flurl.Test.Http
|
||||
HttpTest.ShouldHaveMadeACall().WithCookies(new { x = "foo", y = "bazz" }).Times(1);
|
||||
|
||||
Assert.AreEqual(2, jar.Count);
|
||||
Assert.AreEqual("foo", jar["x"].Value);
|
||||
Assert.AreEqual("bazz", jar["y"].Value);
|
||||
Assert.AreEqual(1, jar.Count(c => c.Name == "x" && c.Value == "foo"));
|
||||
Assert.AreEqual(1, jar.Count(c => c.Name == "y" && c.Value == "bazz"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -82,8 +82,8 @@ namespace Flurl.Test.Http
|
||||
HttpTest.ShouldHaveMadeACall().WithCookies(new { x = "foo", y = "bazz" }).Times(1);
|
||||
|
||||
Assert.AreEqual(2, jar.Count);
|
||||
Assert.AreEqual("foo", jar["x"].Value);
|
||||
Assert.AreEqual("bazz", jar["y"].Value);
|
||||
Assert.AreEqual(1, jar.Count(c => c.Name == "x" && c.Value == "foo"));
|
||||
Assert.AreEqual(1, jar.Count(c => c.Name == "y" && c.Value == "bazz"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -106,15 +106,15 @@ namespace Flurl.Test.Http
|
||||
HttpTest.ShouldHaveMadeACall().WithCookies(new { x = "foo", y = "bazz" }).Times(1);
|
||||
|
||||
Assert.AreEqual(2, cs.Cookies.Count);
|
||||
Assert.AreEqual("foo", cs.Cookies["x"].Value);
|
||||
Assert.AreEqual("bazz", cs.Cookies["y"].Value);
|
||||
Assert.AreEqual(1, cs.Cookies.Count(c => c.Name == "x" && c.Value == "foo"));
|
||||
Assert.AreEqual(1, cs.Cookies.Count(c => c.Name == "y" && c.Value == "bazz"));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void can_parse_set_cookie_header() {
|
||||
var start = DateTimeOffset.UtcNow;
|
||||
var cookie = CookieCutter.FromResponseHeader("https://www.cookies.com/a/b", "x=foo ; DoMaIn=cookies.com ; path=/ ; MAX-AGE=999 ; expires= ; secure ;HTTPONLY ;samesite=none");
|
||||
var cookie = CookieCutter.ParseResponseHeader("https://www.cookies.com/a/b", "x=foo ; DoMaIn=cookies.com ; path=/ ; MAX-AGE=999 ; expires= ; secure ;HTTPONLY ;samesite=none");
|
||||
Assert.AreEqual("https://www.cookies.com/a/b", cookie.OriginUrl.ToString());
|
||||
Assert.AreEqual("x", cookie.Name);
|
||||
Assert.AreEqual("foo", cookie.Value);
|
||||
@ -130,7 +130,7 @@ namespace Flurl.Test.Http
|
||||
|
||||
// simpler case
|
||||
start = DateTimeOffset.UtcNow;
|
||||
cookie = CookieCutter.FromResponseHeader("https://www.cookies.com/a/b", "y=bar");
|
||||
cookie = CookieCutter.ParseResponseHeader("https://www.cookies.com/a/b", "y=bar");
|
||||
Assert.AreEqual("https://www.cookies.com/a/b", cookie.OriginUrl.ToString());
|
||||
Assert.AreEqual("y", cookie.Name);
|
||||
Assert.AreEqual("bar", cookie.Value);
|
||||
@ -145,68 +145,79 @@ namespace Flurl.Test.Http
|
||||
Assert.LessOrEqual(cookie.DateReceived, DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void cannot_change_cookie_after_adding_to_jar() {
|
||||
var cookie = new FlurlCookie("x", "foo", "https://cookies.com");
|
||||
|
||||
// good
|
||||
cookie.Value = "value2";
|
||||
cookie.Path = "/";
|
||||
cookie.Secure = true;
|
||||
|
||||
var jar = new CookieJar().AddOrUpdate(cookie);
|
||||
|
||||
// bad
|
||||
Assert.Throws<Exception>(() => cookie.Value = "value3");
|
||||
Assert.Throws<Exception>(() => cookie.Path = "/a");
|
||||
Assert.Throws<Exception>(() => cookie.Secure = false);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void url_decodes_cookie_value() {
|
||||
var cookie = CookieCutter.FromResponseHeader("https://cookies.com", "x=one%3A%20for%20the%20money");
|
||||
var cookie = CookieCutter.ParseResponseHeader("https://cookies.com", "x=one%3A%20for%20the%20money");
|
||||
Assert.AreEqual("one: for the money", cookie.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void unquotes_cookie_value() {
|
||||
var cookie = CookieCutter.FromResponseHeader("https://cookies.com", "x=\"hello there\"" );
|
||||
var cookie = CookieCutter.ParseResponseHeader("https://cookies.com", "x=\"hello there\"" );
|
||||
Assert.AreEqual("hello there", cookie.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void jar_syncs_to_request_cookies() {
|
||||
var jar = new CookieJar().AddOrUpdate("x", "foo", "https://cookies.com");
|
||||
public void jar_overwrites_request_cookies() {
|
||||
var jar = new CookieJar()
|
||||
.AddOrUpdate("b", 10, "https://cookies.com")
|
||||
.AddOrUpdate("c", 11, "https://cookies.com");
|
||||
|
||||
var req = new FlurlRequest("http://cookies.com").WithCookies(jar);
|
||||
Assert.IsTrue(req.Cookies.ContainsKey("x"));
|
||||
Assert.AreEqual("foo", req.Cookies["x"]);
|
||||
var req = new FlurlRequest("http://cookies.com")
|
||||
.WithCookies(new { a = 1, b = 2 })
|
||||
.WithCookies(jar);
|
||||
|
||||
jar["x"].Value = "new val!";
|
||||
Assert.AreEqual("new val!", req.Cookies["x"]);
|
||||
|
||||
jar.AddOrUpdate("y", "bar", "https://cookies.com");
|
||||
Assert.IsTrue(req.Cookies.ContainsKey("y"));
|
||||
Assert.AreEqual("bar", req.Cookies["y"]);
|
||||
|
||||
jar["x"].Secure = true;
|
||||
Assert.IsFalse(req.Cookies.ContainsKey("x"));
|
||||
|
||||
jar.Clear();
|
||||
Assert.IsFalse(req.Cookies.Any());
|
||||
Assert.AreEqual(3, req.Cookies.Count());
|
||||
Assert.IsTrue(req.Cookies.Contains(("a", "1")));
|
||||
Assert.IsTrue(req.Cookies.Contains(("b", "10"))); // the important one
|
||||
Assert.IsTrue(req.Cookies.Contains(("c", "11")));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task request_cookies_do_not_sync_to_jar() {
|
||||
var jar = new CookieJar().AddOrUpdate("x", "foo", "https://cookies.com");
|
||||
public async Task request_cookies_do_not_overwrite_jar() {
|
||||
var jar = new CookieJar()
|
||||
.AddOrUpdate("b", 10, "https://cookies.com")
|
||||
.AddOrUpdate("c", 11, "https://cookies.com");
|
||||
|
||||
var req = new FlurlRequest("http://cookies.com").WithCookies(jar);
|
||||
Assert.IsTrue(req.Cookies.ContainsKey("x"));
|
||||
Assert.AreEqual("foo", req.Cookies["x"]);
|
||||
var req = new FlurlRequest("http://cookies.com")
|
||||
.WithCookies(jar)
|
||||
.WithCookies(new { a = 1, b = 2 });
|
||||
|
||||
// changing cookie at request level shouldn't touch jar
|
||||
req.Cookies["x"] = "bar";
|
||||
Assert.AreEqual("foo", jar["x"].Value);
|
||||
|
||||
await req.GetAsync();
|
||||
HttpTest.ShouldHaveMadeACall().WithCookies(new { x = "bar" });
|
||||
Assert.AreEqual(3, req.Cookies.Count());
|
||||
Assert.IsTrue(req.Cookies.Contains(("a", "1")));
|
||||
Assert.IsTrue(req.Cookies.Contains(("b", "2"))); // value overwritten but just for this request
|
||||
Assert.IsTrue(req.Cookies.Contains(("c", "11")));
|
||||
|
||||
// re-check after send
|
||||
Assert.AreEqual("foo", jar["x"].Value);
|
||||
// b in jar wasn't touched
|
||||
Assert.AreEqual("10", jar.FirstOrDefault(c => c.Name == "b")?.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void request_cookies_sync_with_cookie_header() {
|
||||
var req = new FlurlRequest("http://cookies.com").WithCookie("x", "foo");
|
||||
Assert.AreEqual("x=foo", req.Headers.TryGetValue("Cookie", out var val) ? val : null);
|
||||
Assert.AreEqual("x=foo", req.Headers.FirstOrDefault("Cookie"));
|
||||
|
||||
// should flow from CookieJar too
|
||||
var jar = new CookieJar().AddOrUpdate("y", "bar", "http://cookies.com");
|
||||
req = new FlurlRequest("http://cookies.com").WithCookies(jar);
|
||||
Assert.AreEqual("y=bar", req.Headers.TryGetValue("Cookie", out val) ? val : null);
|
||||
Assert.AreEqual("y=bar", req.Headers.FirstOrDefault("Cookie"));
|
||||
}
|
||||
|
||||
[TestCase("https://domain1.com", "https://domain1.com", true)]
|
||||
@ -321,7 +332,7 @@ namespace Flurl.Test.Http
|
||||
Assert.IsEmpty(jar);
|
||||
// even though the CookieJar rejected the cookie, it doesn't change the fact
|
||||
// that it exists in the response.
|
||||
Assert.AreEqual("foo", resp.Cookies.TryGetValue("x", out var c) ? c.Value : null);
|
||||
Assert.AreEqual("foo", resp.Cookies.FirstOrDefault(c => c.Name == "x")?.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -336,25 +347,19 @@ namespace Flurl.Test.Http
|
||||
Assert.AreEqual(shouldAddToJar, jar.TryAddOrUpdate(cookie, out reason));
|
||||
|
||||
if (shouldAddToJar)
|
||||
CollectionAssert.Contains(jar.Keys, cookie.Name);
|
||||
Assert.AreEqual(cookie.Name, jar.SingleOrDefault()?.Name);
|
||||
else {
|
||||
Assert.Throws<InvalidCookieException>(() => jar.AddOrUpdate(cookie));
|
||||
CollectionAssert.DoesNotContain(jar.Keys, cookie.Name);
|
||||
CollectionAssert.IsEmpty(jar);
|
||||
}
|
||||
|
||||
var req = cookie.OriginUrl.WithCookies(jar);
|
||||
if (shouldAddToJar)
|
||||
CollectionAssert.Contains(req.Cookies.Keys, cookie.Name);
|
||||
else
|
||||
CollectionAssert.DoesNotContain(req.Cookies.Keys, cookie.Name);
|
||||
Assert.AreEqual(shouldAddToJar, req.Cookies.Contains((cookie.Name, cookie.Value)));
|
||||
|
||||
if (requestUrl != null) {
|
||||
Assert.AreEqual(shouldSend, cookie.ShouldSendTo(requestUrl, out reason), reason);
|
||||
req = requestUrl.WithCookies(jar);
|
||||
if (shouldSend)
|
||||
CollectionAssert.Contains(req.Cookies.Keys, cookie.Name);
|
||||
else
|
||||
CollectionAssert.DoesNotContain(req.Cookies.Keys, cookie.Name);
|
||||
Assert.AreEqual(shouldSend, req.Cookies.Contains((cookie.Name, cookie.Value)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ namespace Flurl.Test.Http
|
||||
|
||||
var resp = await "http://some-api.com".GetAsync();
|
||||
Assert.AreEqual(234, resp.StatusCode);
|
||||
Assert.IsTrue(resp.Headers.TryGetValue("my-header", out var headerVal));
|
||||
Assert.IsTrue(resp.Headers.TryGetFirst("my-header", out var headerVal));
|
||||
Assert.AreEqual("hi", headerVal);
|
||||
|
||||
var data = await resp.GetJsonAsync<TestData>();
|
||||
|
@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Flurl.Http;
|
||||
@ -289,8 +289,9 @@ namespace Flurl.Test.Http
|
||||
[Test]
|
||||
public async Task can_send_cookies() {
|
||||
var req = "https://httpbin.org/cookies".WithCookies(new { x = 1, y = 2 });
|
||||
Assert.AreEqual("1", req.Cookies["x"]);
|
||||
Assert.AreEqual("2", req.Cookies["y"]);
|
||||
Assert.AreEqual(2, req.Cookies.Count());
|
||||
Assert.IsTrue(req.Cookies.Contains(("x", "1")));
|
||||
Assert.IsTrue(req.Cookies.Contains(("y", "2")));
|
||||
|
||||
var s = await req.GetStringAsync();
|
||||
|
||||
@ -304,16 +305,17 @@ namespace Flurl.Test.Http
|
||||
public async Task can_receive_cookies() {
|
||||
// endpoint does a redirect, so we need to disable auto-redirect in order to see the cookie in the response
|
||||
var resp = await "https://httpbin.org/cookies/set?z=999".WithAutoRedirect(false).GetAsync();
|
||||
Assert.AreEqual("999", resp.Cookies["z"].Value);
|
||||
Assert.AreEqual("999", resp.Cookies.FirstOrDefault(c => c.Name == "z")?.Value);
|
||||
|
||||
|
||||
// but using WithCookies we can capture it even with redirects enabled
|
||||
await "https://httpbin.org/cookies/set?z=999".WithCookies(out var cookies).GetAsync();
|
||||
Assert.AreEqual("999", cookies["z"].Value);
|
||||
Assert.AreEqual("999", cookies.FirstOrDefault(c => c.Name == "z")?.Value);
|
||||
|
||||
// this works with redirects too
|
||||
using (var session = new CookieSession("https://httpbin.org/cookies")) {
|
||||
await session.Request("set?z=999").GetAsync();
|
||||
Assert.AreEqual("999", session.Cookies["z"].Value);
|
||||
Assert.AreEqual("999", session.Cookies.FirstOrDefault(c => c.Name == "z")?.Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,8 +32,7 @@ namespace Flurl.Test.Http
|
||||
[Test]
|
||||
public void can_set_header() {
|
||||
var sc = GetSettingsContainer().WithHeader("a", 1);
|
||||
Assert.AreEqual(1, sc.Headers.Count);
|
||||
Assert.AreEqual(1, sc.Headers["a"]);
|
||||
Assert.AreEqual(("a", 1), sc.Headers.Single());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -41,8 +40,8 @@ namespace Flurl.Test.Http
|
||||
// null values shouldn't be added
|
||||
var sc = GetSettingsContainer().WithHeaders(new { a = "b", one = 2, three = (object)null });
|
||||
Assert.AreEqual(2, sc.Headers.Count);
|
||||
Assert.AreEqual("b", sc.Headers["a"]);
|
||||
Assert.AreEqual(2, sc.Headers["one"]);
|
||||
Assert.IsTrue(sc.Headers.Contains("a", "b"));
|
||||
Assert.IsTrue(sc.Headers.Contains("one", 2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -51,48 +50,48 @@ namespace Flurl.Test.Http
|
||||
Assert.AreEqual(2, sc.Headers.Count);
|
||||
sc.WithHeader("b", null);
|
||||
Assert.AreEqual(1, sc.Headers.Count);
|
||||
Assert.AreEqual("a", sc.Headers.Keys.Single());
|
||||
Assert.IsFalse(sc.Headers.Contains("b"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void can_set_headers_from_dictionary() {
|
||||
var sc = GetSettingsContainer().WithHeaders(new Dictionary<string, object> { { "a", "b" }, { "one", 2 } });
|
||||
Assert.AreEqual(2, sc.Headers.Count);
|
||||
Assert.AreEqual("b", sc.Headers["a"]);
|
||||
Assert.AreEqual(2, sc.Headers["one"]);
|
||||
Assert.IsTrue(sc.Headers.Contains("a", "b"));
|
||||
Assert.IsTrue(sc.Headers.Contains("one", 2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void underscores_in_properties_convert_to_hyphens_in_header_names() {
|
||||
var sc = GetSettingsContainer().WithHeaders(new { User_Agent = "Flurl", Cache_Control = "no-cache" });
|
||||
Assert.IsTrue(sc.Headers.ContainsKey("User-Agent"));
|
||||
Assert.IsTrue(sc.Headers.ContainsKey("Cache-Control"));
|
||||
Assert.IsTrue(sc.Headers.Contains("User-Agent"));
|
||||
Assert.IsTrue(sc.Headers.Contains("Cache-Control"));
|
||||
|
||||
// make sure we can disable the behavior
|
||||
sc.WithHeaders(new { no_i_really_want_underscores = "foo" }, false);
|
||||
Assert.IsTrue(sc.Headers.ContainsKey("no_i_really_want_underscores"));
|
||||
Assert.IsTrue(sc.Headers.Contains("no_i_really_want_underscores"));
|
||||
|
||||
// dictionaries don't get this behavior since you can use hyphens explicitly
|
||||
sc.WithHeaders(new Dictionary<string, string> { { "exclude_dictionaries", "bar" } });
|
||||
Assert.IsTrue(sc.Headers.ContainsKey("exclude_dictionaries"));
|
||||
Assert.IsTrue(sc.Headers.Contains("exclude_dictionaries"));
|
||||
|
||||
// same with strings
|
||||
sc.WithHeaders("exclude_strings=123");
|
||||
Assert.IsTrue(sc.Headers.ContainsKey("exclude_strings"));
|
||||
Assert.IsTrue(sc.Headers.Contains("exclude_strings"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void can_setup_oauth_bearer_token() {
|
||||
var sc = GetSettingsContainer().WithOAuthBearerToken("mytoken");
|
||||
Assert.AreEqual(1, sc.Headers.Count);
|
||||
Assert.AreEqual("Bearer mytoken", sc.Headers["Authorization"]);
|
||||
Assert.IsTrue(sc.Headers.Contains("Authorization", "Bearer mytoken"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void can_setup_basic_auth() {
|
||||
var sc = GetSettingsContainer().WithBasicAuth("user", "pass");
|
||||
Assert.AreEqual(1, sc.Headers.Count);
|
||||
Assert.AreEqual("Basic dXNlcjpwYXNz", sc.Headers["Authorization"]);
|
||||
Assert.IsTrue(sc.Headers.Contains("Authorization", "Basic dXNlcjpwYXNz"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -352,9 +352,7 @@ namespace Flurl.Test.Http
|
||||
HttpTest.RespondWith(headers: new { h1 = "foo" });
|
||||
|
||||
var resp = await "http://www.api.com".GetAsync();
|
||||
Assert.AreEqual(1, resp.Headers.Count());
|
||||
Assert.AreEqual("h1", resp.Headers.First().Key);
|
||||
Assert.AreEqual("foo", resp.Headers.First().Value);
|
||||
Assert.AreEqual(("h1", "foo"), resp.Headers.Single());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -363,7 +361,7 @@ namespace Flurl.Test.Http
|
||||
|
||||
var resp = await "http://www.api.com".GetAsync();
|
||||
Assert.AreEqual(1, resp.Cookies.Count);
|
||||
Assert.AreEqual("foo", resp.Cookies["c1"].Value);
|
||||
Assert.AreEqual("foo", resp.Cookies.FirstOrDefault(c => c.Name == "c1")?.Value);
|
||||
}
|
||||
|
||||
// https://github.com/tmenier/Flurl/issues/175
|
||||
@ -423,7 +421,7 @@ namespace Flurl.Test.Http
|
||||
public async Task can_fake_content_headers() {
|
||||
HttpTest.RespondWith("<xml></xml>", 200, new { Content_Type = "text/xml" });
|
||||
await "http://api.com".GetAsync();
|
||||
HttpTest.ShouldHaveMadeACall().With(call => call.Response.Headers.Any(kv => kv.Key == "Content-Type" && kv.Value == "text/xml"));
|
||||
HttpTest.ShouldHaveMadeACall().With(call => call.Response.Headers.Contains(("Content-Type", "text/xml")));
|
||||
HttpTest.ShouldHaveMadeACall().With(call => call.HttpResponseMessage.Content.Headers.ContentType.MediaType == "text/xml");
|
||||
}
|
||||
|
||||
|
@ -83,7 +83,7 @@ namespace Flurl.Http.CodeGen
|
||||
yield return Create("WithHeader", "Creates a new FlurlRequest and sets a request header.")
|
||||
.AddArg("name", "string", "The header name.")
|
||||
.AddArg("value", "object", "The header value.");
|
||||
yield return Create("WithHeaders", "Creates a new FlurlRequest and sets request headers based on property names/values of the provided object, or keys/values if object is a dictionary, to be sent")
|
||||
yield return Create("WithHeaders", "Creates a new FlurlRequest and sets request headers based on property names/values of the provided object, or keys/values if object is a dictionary, to be sent.")
|
||||
.AddArg("headers", "object", "Names/values of HTTP headers to set. Typically an anonymous object or IDictionary.")
|
||||
.AddArg("replaceUnderscoreWithHyphen", "bool", "If true, underscores in property names will be replaced by hyphens. Default is true.", "true");
|
||||
yield return Create("WithBasicAuth", "Creates a new FlurlRequest and sets the Authorization header according to Basic Authentication protocol.")
|
||||
@ -93,11 +93,12 @@ namespace Flurl.Http.CodeGen
|
||||
.AddArg("token", "string", "The acquired oAuth bearer token.");
|
||||
|
||||
// cookie extensions
|
||||
yield return Create("WithCookie", "Creates a new FlurlRequest and sets an HTTP cookie to be sent with this request only. To maintain a cookie \"session\", consider using WithCookies(CookieJar) or FlurlClient.StartCookieSession instead.")
|
||||
yield return Create("WithCookie", "Creates a new FlurlRequest and adds a name-value pair to its Cookie header. " +
|
||||
"To automatically maintain a cookie \"session\", consider using a CookieJar or CookieSession instead.")
|
||||
.AddArg("name", "string", "The cookie name.")
|
||||
.AddArg("value", "object", "The cookie value.");
|
||||
yield return Create("WithCookies", "Creates a new FlurlRequest and sets HTTP cookies to be sent with this request only, based on property names/values of the provided object, or keys/values " +
|
||||
"if object is a dictionary. To maintain a cookie \"session\", consider using WithCookies(CookieJar) or FlurlClient.StartCookieSession instead")
|
||||
yield return Create("WithCookies", "Creates a new FlurlRequest and adds name-value pairs to its Cookie header based on property names/values of the provided object, or keys/values if object is a dictionary. " +
|
||||
"To automatically maintain a cookie \"session\", consider using a CookieJar or CookieSession instead.")
|
||||
.AddArg("values", "object", "Names/values of HTTP cookies to set. Typically an anonymous object or IDictionary.");
|
||||
yield return Create("WithCookies", "Creates a new FlurlRequest and sets the CookieJar associated with this request, which will be updated with any Set-Cookie headers present in the response and is suitable for reuse in subsequent requests.")
|
||||
.AddArg("cookieJar", "CookieJar", "The CookieJar.");
|
||||
|
@ -10,21 +10,29 @@ namespace Flurl.Http
|
||||
/// </summary>
|
||||
public static class CookieCutter
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses a Cookie request header to name-value pairs.
|
||||
/// </summary>
|
||||
/// <param name="headerValue">Value of the Cookie request header.</param>
|
||||
/// <returns></returns>
|
||||
public static IEnumerable<(string Name, string Value)> ParseRequestHeader(string headerValue) {
|
||||
if (string.IsNullOrEmpty(headerValue)) yield break;
|
||||
|
||||
foreach (var pair in GetPairs(headerValue))
|
||||
yield return (pair.Name, Url.Decode(pair.Value, false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a Set-Cookie response header to a FlurlCookie.
|
||||
/// </summary>
|
||||
/// <param name="url">The URL that sent the response.</param>
|
||||
/// <param name="headerValue">Value of the Set-Cookie header.</param>
|
||||
/// <returns></returns>
|
||||
public static FlurlCookie FromResponseHeader(string url, string headerValue) {
|
||||
public static FlurlCookie ParseResponseHeader(string url, string headerValue) {
|
||||
if (string.IsNullOrEmpty(headerValue)) return null;
|
||||
var pairs = (
|
||||
from part in headerValue.Split(';')
|
||||
let pair = part.SplitOnFirstOccurence("=")
|
||||
select new { Name = pair[0].Trim(), Value = pair.Last().Trim() });
|
||||
|
||||
FlurlCookie cookie = null;
|
||||
foreach (var pair in pairs) {
|
||||
foreach (var pair in GetPairs(headerValue)) {
|
||||
if (cookie == null)
|
||||
cookie = new FlurlCookie(pair.Name, Url.Decode(pair.Value.Trim('"'), false), url, DateTimeOffset.UtcNow);
|
||||
|
||||
@ -49,15 +57,22 @@ namespace Flurl.Http
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a Cookie request header value from a key-value dictionary.
|
||||
/// Parses list of semicolon-delimited "name=value" pairs.
|
||||
/// </summary>
|
||||
/// <param name="values">Cookie values.</param>
|
||||
/// <returns>a header value if cookie values are present, otherwise null.</returns>
|
||||
public static string ToRequestHeader(IDictionary<string, object> values) {
|
||||
if (values?.Any() != true) return null;
|
||||
private static IEnumerable<(string Name, string Value)> GetPairs(string list) =>
|
||||
from part in list.Split(';')
|
||||
let pair = part.SplitOnFirstOccurence("=")
|
||||
select (pair[0].Trim(), pair.Last().Trim());
|
||||
|
||||
return string.Join("; ", values.Select(c =>
|
||||
$"{Url.Encode(c.Key)}={Url.Encode(c.Value.ToInvariantString())}"));
|
||||
/// <summary>
|
||||
/// Creates a Cookie request header value from a list of cookie name-value pairs.
|
||||
/// </summary>
|
||||
/// <returns>A header value if cookie values are present, otherwise null.</returns>
|
||||
public static string ToRequestHeader(IEnumerable<(string Name, string Value)> cookies) {
|
||||
if (cookies?.Any() != true) return null;
|
||||
|
||||
return string.Join("; ", cookies.Select(c =>
|
||||
$"{Url.Encode(c.Name)}={Url.Encode(c.Value)}"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,3 +1,4 @@
|
||||
using System.Linq;
|
||||
using Flurl.Util;
|
||||
|
||||
namespace Flurl.Http
|
||||
@ -8,31 +9,38 @@ namespace Flurl.Http
|
||||
public static class CookieExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets an HTTP cookie to be sent with this request only.
|
||||
/// To maintain a cookie "session", consider using WithCookies(CookieJar) or FlurlClient.StartCookieSession instead.
|
||||
/// Adds or updates a name-value pair in this request's Cookie header.
|
||||
/// To automatically maintain a cookie "session", consider using a CookieJar or CookieSession instead.
|
||||
/// </summary>
|
||||
/// <param name="request">The IFlurlRequest.</param>
|
||||
/// <param name="name">The cookie name.</param>
|
||||
/// <param name="value">The cookie value.</param>
|
||||
/// <returns>This IFlurlClient instance.</returns>
|
||||
public static IFlurlRequest WithCookie(this IFlurlRequest request, string name, object value) {
|
||||
request.Cookies[name] = value;
|
||||
return request;
|
||||
var cookies = new NameValueList<string>(request.Cookies);
|
||||
cookies.AddOrReplace(name, value.ToInvariantString());
|
||||
return request.WithHeader("Cookie", CookieCutter.ToRequestHeader(cookies));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets HTTP cookies to be sent with this request only, based on property names/values of the provided object, or
|
||||
/// keys/values if object is a dictionary. To maintain a cookie "session", consider using WithCookies(CookieJar)
|
||||
/// or FlurlClient.StartCookieSession instead.
|
||||
/// Adds or updates name-value pairs in this request's Cookie header, based on property names/values
|
||||
/// of the provided object, or keys/values if object is a dictionary.
|
||||
/// To automatically maintain a cookie "session", consider using a CookieJar or CookieSession instead.
|
||||
/// </summary>
|
||||
/// <param name="request">The IFlurlRequest.</param>
|
||||
/// <param name="values">Names/values of HTTP cookies to set. Typically an anonymous object or IDictionary.</param>
|
||||
/// <returns>This IFlurlClient.</returns>
|
||||
public static IFlurlRequest WithCookies(this IFlurlRequest request, object values) {
|
||||
foreach (var kv in values.ToKeyValuePairs())
|
||||
request.Cookies[kv.Key] = kv.Value?.ToInvariantString();
|
||||
|
||||
return request;
|
||||
var cookies = new NameValueList<string>(request.Cookies);
|
||||
// although rare, we need to accommodate the possibility of multiple cookies with the same name
|
||||
foreach (var group in values.ToKeyValuePairs().GroupBy(x => x.Key)) {
|
||||
// add or replace the first one (by name)
|
||||
cookies.AddOrReplace(group.Key, group.First().Value.ToInvariantString());
|
||||
// append the rest
|
||||
foreach (var kv in group.Skip(1))
|
||||
cookies.Add(kv.Key, kv.Value.ToInvariantString());
|
||||
}
|
||||
return request.WithHeader("Cookie", CookieCutter.ToRequestHeader(cookies));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -8,20 +8,15 @@ using Flurl.Util;
|
||||
namespace Flurl.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// A collection of FlurlCookies that can be passed to one or more FlurlRequests, either
|
||||
/// explicitly via WithCookies or implicitly via FlurlClient.StartCookieSession. Automatically
|
||||
/// populated/synchronized with cookies received via Set-Cookie response headers. Chooses
|
||||
/// which cookies to send in Cookie request per RFC 6265.
|
||||
/// A collection of FlurlCookies that can be attached to one or more FlurlRequests, either explicitly via WithCookies
|
||||
/// or implicitly via a CookieSession. Stores cookies received via Set-Cookie response headers.
|
||||
/// </summary>
|
||||
public class CookieJar : IReadOnlyDictionary<string, FlurlCookie>
|
||||
public class CookieJar : IReadOnlyCollection<FlurlCookie>
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, FlurlCookie> _dict = new ConcurrentDictionary<string, FlurlCookie>();
|
||||
|
||||
// requests whose Cookies collection should be kept in sync with changes to this CookieJar
|
||||
private readonly HashSet<IFlurlRequest> _syncdRequests = new HashSet<IFlurlRequest>();
|
||||
|
||||
/// <summary>
|
||||
/// Add a cookie to the jar or update if one with the same Name already exists.
|
||||
/// Adds a cookie to the jar or updates if one with the same Name/Domain/Path already exists.
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the cookie.</param>
|
||||
/// <param name="value">Value of the cookie.</param>
|
||||
@ -31,7 +26,7 @@ namespace Flurl.Http
|
||||
AddOrUpdate(new FlurlCookie(name, value.ToInvariantString(), originUrl, dateReceived));
|
||||
|
||||
/// <summary>
|
||||
/// Adds a cookie to the jar or updates if one with the same Name already exists.
|
||||
/// Adds a cookie to the jar or updates if one with the same Name/Domain/Path already exists.
|
||||
/// Throws FlurlHttpException if cookie is invalid.
|
||||
/// </summary>
|
||||
public CookieJar AddOrUpdate(FlurlCookie cookie) {
|
||||
@ -42,28 +37,27 @@ namespace Flurl.Http
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a cookie to the jar or updates if one with the same Name already exists, if it is valid.
|
||||
/// Returns true if cookie is valid and was added. If false, provides descriptive reason.
|
||||
/// Adds a cookie to the jar or updates if one with the same Name/Domain/Path already exists,
|
||||
/// but only if it is valid and not expired.
|
||||
/// </summary>
|
||||
/// <returns>true if cookie is valid and was added or updated. If false, provides descriptive reason.</returns>
|
||||
public bool TryAddOrUpdate(FlurlCookie cookie, out string reason) {
|
||||
if (!cookie.IsValid(out reason) || cookie.IsExpired(out reason))
|
||||
return false;
|
||||
|
||||
cookie.Changed += (_, name) => SyncToRequests(cookie, false);
|
||||
_dict[cookie.Name] = cookie;
|
||||
SyncToRequests(cookie, false);
|
||||
cookie.Lock(); // makes immutable
|
||||
_dict[cookie.GetKey()] = cookie;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a cookie from the CookieJar.
|
||||
/// Removes all cookies matching the given predicate.
|
||||
/// </summary>
|
||||
/// <param name="name">The cookie name.</param>
|
||||
public CookieJar Remove(string name) {
|
||||
if (_dict.TryRemove(name, out var cookie))
|
||||
SyncToRequests(cookie, true);
|
||||
|
||||
public CookieJar Remove(Func<FlurlCookie, bool> predicate) {
|
||||
var keys = _dict.Where(kv => predicate(kv.Value)).Select(kv => kv.Key).ToList();
|
||||
foreach (var key in keys)
|
||||
_dict.TryRemove(key, out _);
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -71,59 +65,18 @@ namespace Flurl.Http
|
||||
/// Removes all cookies from this CookieJar
|
||||
/// </summary>
|
||||
public CookieJar Clear() {
|
||||
var all = _dict.Values;
|
||||
_dict.Clear();
|
||||
foreach (var cookie in all)
|
||||
SyncToRequests(cookie, true);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures changes to the CookieJar are kept in sync with the Cookies collection of the FlurlRequest
|
||||
/// </summary>
|
||||
internal void SyncWith(IFlurlRequest req) {
|
||||
foreach (var cookie in this.Values.Where(c => c.ShouldSendTo(req.Url, out _)))
|
||||
req.Cookies[cookie.Name] = cookie.Value;
|
||||
_syncdRequests.Add(req);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops synchronization of changes to the CookieJar with the Cookies collection of the FlurlRequest
|
||||
/// </summary>
|
||||
internal void UnsyncWith(IFlurlRequest req) => _syncdRequests.Remove(req);
|
||||
|
||||
private void SyncToRequests(FlurlCookie cookie, bool removed) {
|
||||
foreach (var req in _syncdRequests) {
|
||||
if (removed || !cookie.ShouldSendTo(req.Url, out _))
|
||||
req.Cookies.Remove(cookie.Name);
|
||||
else
|
||||
req.Cookies[cookie.Name] = cookie.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerator<KeyValuePair<string, FlurlCookie>> GetEnumerator() => _dict.GetEnumerator();
|
||||
public IEnumerator<FlurlCookie> GetEnumerator() => _dict.Values.GetEnumerator();
|
||||
|
||||
/// <inheritdoc/>
|
||||
IEnumerator IEnumerable.GetEnumerator() => _dict.GetEnumerator();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public int Count => _dict.Count;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool ContainsKey(string key) => _dict.ContainsKey(key);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryGetValue(string key, out FlurlCookie value) => _dict.TryGetValue(key, out value);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public FlurlCookie this[string key] => _dict[key];
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<string> Keys => _dict.Keys;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<FlurlCookie> Values => _dict.Values;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -90,10 +90,7 @@ namespace Flurl.Http
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDictionary<string, object> Headers { get; } = new ConcurrentDictionary<string, object>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDictionary<string, string> Cookies { get; set; } = new ConcurrentDictionary<string, string>();
|
||||
public INameValueList<object> Headers { get; } = new NameValueList<object>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public HttpClient HttpClient => HttpTest.Current?.HttpClient ?? _injectedClient ?? GetHttpClient();
|
||||
|
@ -37,6 +37,8 @@ namespace Flurl.Http
|
||||
private bool _httpOnly;
|
||||
private SameSite? _sameSite;
|
||||
|
||||
private bool _locked;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new FlurlCookie.
|
||||
/// </summary>
|
||||
@ -51,11 +53,6 @@ namespace Flurl.Http
|
||||
DateReceived = dateReceived ?? DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when a cookie is changed.
|
||||
/// </summary>
|
||||
internal event EventHandler<string> Changed;
|
||||
|
||||
/// <summary>
|
||||
/// The URL that originally sent the Set-Cookie response header. If adding to a CookieJar, this is required unless
|
||||
/// both Domain AND Path are specified.
|
||||
@ -78,7 +75,7 @@ namespace Flurl.Http
|
||||
/// </summary>
|
||||
public string Value {
|
||||
get => _value;
|
||||
set => UpdateAndNotify(ref _value, value);
|
||||
set => Update(ref _value, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -86,7 +83,7 @@ namespace Flurl.Http
|
||||
/// </summary>
|
||||
public DateTimeOffset? Expires {
|
||||
get => _expires;
|
||||
set => UpdateAndNotify(ref _expires, value);
|
||||
set => Update(ref _expires, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -94,7 +91,7 @@ namespace Flurl.Http
|
||||
/// </summary>
|
||||
public int? MaxAge {
|
||||
get => _maxAge;
|
||||
set => UpdateAndNotify(ref _maxAge, value);
|
||||
set => Update(ref _maxAge, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -102,7 +99,7 @@ namespace Flurl.Http
|
||||
/// </summary>
|
||||
public string Domain {
|
||||
get => _domain;
|
||||
set => UpdateAndNotify(ref _domain, value);
|
||||
set => Update(ref _domain, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -110,7 +107,7 @@ namespace Flurl.Http
|
||||
/// </summary>
|
||||
public string Path {
|
||||
get => _path;
|
||||
set => UpdateAndNotify(ref _path, value);
|
||||
set => Update(ref _path, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -118,7 +115,7 @@ namespace Flurl.Http
|
||||
/// </summary>
|
||||
public bool Secure {
|
||||
get => _secure;
|
||||
set => UpdateAndNotify(ref _secure, value);
|
||||
set => Update(ref _secure, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -126,7 +123,7 @@ namespace Flurl.Http
|
||||
/// </summary>
|
||||
public bool HttpOnly {
|
||||
get => _httpOnly;
|
||||
set => UpdateAndNotify(ref _httpOnly, value);
|
||||
set => Update(ref _httpOnly, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -134,17 +131,37 @@ namespace Flurl.Http
|
||||
/// </summary>
|
||||
public SameSite? SameSite {
|
||||
get => _sameSite;
|
||||
set => UpdateAndNotify(ref _sameSite, value);
|
||||
set => Update(ref _sameSite, value);
|
||||
}
|
||||
|
||||
private void UpdateAndNotify<T>(ref T field, T newVal, [CallerMemberName]string propName = null) {
|
||||
/// <summary>
|
||||
/// Generates a key based on cookie Name, Domain, and Path (using OriginalUrl in the absence of Domain/Path).
|
||||
/// Used by CookieJar to determine whether to add a cookie or update an existing one.
|
||||
/// </summary>
|
||||
public string GetKey() {
|
||||
var domain = string.IsNullOrEmpty(Domain) ? "*" + OriginUrl.Host : Domain;
|
||||
var path = string.IsNullOrEmpty(Path) ? OriginUrl.Path : Path;
|
||||
if (path.Length == 0) path = "/";
|
||||
return $"{domain}{path}:{Name.ToLowerInvariant()}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes this cookie immutable. Call when added to a jar.
|
||||
/// </summary>
|
||||
internal void Lock() {
|
||||
_locked = true;
|
||||
}
|
||||
|
||||
private void Update<T>(ref T field, T newVal, [CallerMemberName]string propName = null) {
|
||||
// == throws with generics (strangely), and .Equals needs a null check. Jon Skeet to the rescue.
|
||||
// https://stackoverflow.com/a/390974/62600
|
||||
if (EqualityComparer<T>.Default.Equals(field, newVal))
|
||||
return;
|
||||
|
||||
if (_locked)
|
||||
throw new Exception("After a cookie has been added to a CookieJar, it becomes immutable and cannot be changed.");
|
||||
|
||||
field = newVal;
|
||||
Changed?.Invoke(this, propName);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
@ -22,8 +21,8 @@ namespace Flurl.Http
|
||||
IFlurlClient Client { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The HTTP method of the request. Normally you don't need to set this explicitly; it will be set
|
||||
/// when you call the sending method (GetAsync, PostAsync, etc.)
|
||||
/// Gets or sets the HTTP method of the request. Normally you don't need to set this explicitly; it will be set
|
||||
/// when you call the sending method, such as GetAsync, PostAsync, etc.
|
||||
/// </summary>
|
||||
HttpMethod Verb { get; set; }
|
||||
|
||||
@ -33,17 +32,14 @@ namespace Flurl.Http
|
||||
Url Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Collection of HTTP cookie values to be sent in this request's Cookie header. If a CookieJar is used, values
|
||||
/// from the jar that will be sent in this request will be sync'd to this collection automatically, but NOT
|
||||
/// vice-versa. Therefore, you can use this collection to override values set by the jar for this request only,
|
||||
/// but for a multi-request cookie "session" it is better to set values in the CookieJar and reuse it.
|
||||
/// Gets Name/Value pairs parsed from the Cookie request header.
|
||||
/// </summary>
|
||||
IDictionary<string, object> Cookies { get; }
|
||||
IEnumerable<(string Name, string Value)> Cookies { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Collection of HTTP cookies that can be shared between multiple requests. Automatically adds/updates cookies
|
||||
/// received via Set-Cookie headers in this response. Processes rules based on attributes (Domain, Path, Expires, etc.)
|
||||
/// to determine which cookies to send with this specific request, and synchronizes those with the Cookies collection.
|
||||
/// Gets or sets the collection of HTTP cookies that can be shared between multiple requests. When set, values that
|
||||
/// should be sent with this request (based on Domain, Path, and other rules) are immediately copied to the Cookie
|
||||
/// request header, and any Set-Cookie headers received in the response will be written to the CookieJar.
|
||||
/// </summary>
|
||||
CookieJar CookieJar { get; set; }
|
||||
|
||||
@ -64,9 +60,8 @@ namespace Flurl.Http
|
||||
private FlurlHttpSettings _settings;
|
||||
private IFlurlClient _client;
|
||||
private Url _url;
|
||||
private IDictionary<string, object> _headers = new ConcurrentDictionary<string, object>();
|
||||
private CookieJar _cookieJar;
|
||||
private FlurlCall _redirectedFrom;
|
||||
private CookieJar _jar;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FlurlRequest"/> class.
|
||||
@ -139,27 +134,24 @@ namespace Flurl.Http
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDictionary<string, object> Headers {
|
||||
get {
|
||||
if (Cookies.Any())
|
||||
_headers["Cookie"] = CookieCutter.ToRequestHeader(Cookies);
|
||||
return _headers;
|
||||
}
|
||||
}
|
||||
public INameValueList<object> Headers { get; } = new NameValueList<object>();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDictionary<string, object> Cookies { get; } = new ConcurrentDictionary<string, object>();
|
||||
public IEnumerable<(string Name, string Value)> Cookies =>
|
||||
CookieCutter.ParseRequestHeader(Headers.FirstOrDefault("Cookie")?.ToInvariantString());
|
||||
|
||||
/// <inheritdoc />
|
||||
public CookieJar CookieJar {
|
||||
get => _cookieJar;
|
||||
get => _jar;
|
||||
set {
|
||||
_cookieJar?.UnsyncWith(this);
|
||||
_cookieJar = value;
|
||||
_cookieJar?.SyncWith(this);
|
||||
_jar = value;
|
||||
this.WithCookies(
|
||||
from c in CookieJar
|
||||
where c.ShouldSendTo(this.Url, out _)
|
||||
select (c.Name, c.Value));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IFlurlResponse> SendAsync(HttpMethod verb, HttpContent content = null, CancellationToken cancellationToken = default, HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead) {
|
||||
_client = Client; // "freeze" the client at this point to avoid excessive calls to FlurlClientFactory.Get (#374)
|
||||
@ -187,13 +179,12 @@ namespace Flurl.Http
|
||||
|
||||
call.StartedUtc = DateTime.UtcNow;
|
||||
try {
|
||||
SyncHeadersAndCookies(request);
|
||||
_cookieJar?.UnsyncWith(this);
|
||||
SyncHeaders(request);
|
||||
|
||||
var response = await Client.HttpClient.SendAsync(request, completionOption, cancellationTokenWithTimeout).ConfigureAwait(false);
|
||||
call.HttpResponseMessage = response;
|
||||
call.HttpResponseMessage.RequestMessage = request;
|
||||
call.Response = new FlurlResponse(call.HttpResponseMessage, _cookieJar);
|
||||
call.Response = new FlurlResponse(call.HttpResponseMessage, CookieJar);
|
||||
|
||||
if (Settings.Redirects.Enabled)
|
||||
call.Redirect = GetRedirect(call);
|
||||
@ -242,18 +233,19 @@ namespace Flurl.Http
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncHeadersAndCookies(HttpRequestMessage request) {
|
||||
// copy any client-level (default) to FlurlRequest
|
||||
Headers.Merge(Client.Headers);
|
||||
private void SyncHeaders(HttpRequestMessage request) {
|
||||
// copy any client-level (default) headers to this request
|
||||
foreach (var header in Client.Headers.Where(h => !this.Headers.Contains(h.Name)))
|
||||
this.Headers.Add(header.Name, header.Value);
|
||||
|
||||
// copy headers from FlurlRequest to HttpRequestMessage
|
||||
foreach (var header in Headers)
|
||||
request.SetHeader(header.Key, header.Value);
|
||||
request.SetHeader(header.Name, header.Value);
|
||||
|
||||
// copy headers from HttpContent to FlurlRequest
|
||||
if (request.Content != null) {
|
||||
foreach (var header in request.Content.Headers)
|
||||
Headers[header.Key] = string.Join(",", header.Value);
|
||||
Headers.AddOrReplace(header.Key, string.Join(",", header.Value));
|
||||
}
|
||||
}
|
||||
|
||||
@ -262,7 +254,7 @@ namespace Flurl.Http
|
||||
if (call.Response.StatusCode < 300 || call.Response.StatusCode > 399)
|
||||
return null;
|
||||
|
||||
if (!call.Response.Headers.TryGetValue("Location", out var location))
|
||||
if (!call.Response.Headers.TryGetFirst("Location", out var location))
|
||||
return null;
|
||||
|
||||
var redir = new FlurlRedirect();
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Dynamic;
|
||||
using System.IO;
|
||||
@ -18,12 +18,12 @@ namespace Flurl.Http
|
||||
/// <summary>
|
||||
/// Gets the collection of response headers received.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<string, string> Headers { get; }
|
||||
IReadOnlyNameValueList<string> Headers { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of HTTP cookies received in this response via Set-Cookie headers.
|
||||
/// </summary>
|
||||
IReadOnlyDictionary<string, FlurlCookie> Cookies { get; }
|
||||
IReadOnlyList<FlurlCookie> Cookies { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the raw HttpResponseMessage that this IFlurlResponse wraps.
|
||||
@ -85,17 +85,17 @@ namespace Flurl.Http
|
||||
/// <inheritdoc />
|
||||
public class FlurlResponse : IFlurlResponse
|
||||
{
|
||||
private readonly Lazy<IReadOnlyDictionary<string, string>> _headers;
|
||||
private readonly Lazy<IReadOnlyDictionary<string, FlurlCookie>> _cookies;
|
||||
private readonly Lazy<IReadOnlyNameValueList<string>> _headers;
|
||||
private readonly Lazy<IReadOnlyList<FlurlCookie>> _cookies;
|
||||
private object _capturedBody = null;
|
||||
private bool _streamRead = false;
|
||||
private ISerializer _serializer = null;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyDictionary<string, string> Headers => _headers.Value;
|
||||
public IReadOnlyNameValueList<string> Headers => _headers.Value;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyDictionary<string, FlurlCookie> Cookies => _cookies.Value;
|
||||
public IReadOnlyList<FlurlCookie> Cookies => _cookies.Value;
|
||||
|
||||
/// <inheritdoc />
|
||||
public HttpResponseMessage ResponseMessage { get; }
|
||||
@ -108,28 +108,38 @@ namespace Flurl.Http
|
||||
/// </summary>
|
||||
public FlurlResponse(HttpResponseMessage resp, CookieJar cookieJar = null) {
|
||||
ResponseMessage = resp;
|
||||
_headers = new Lazy<IReadOnlyDictionary<string, string>>(LoadHeaders);
|
||||
_cookies = new Lazy<IReadOnlyDictionary<string, FlurlCookie>>(LoadCookies);
|
||||
_headers = new Lazy<IReadOnlyNameValueList<string>>(LoadHeaders);
|
||||
_cookies = new Lazy<IReadOnlyList<FlurlCookie>>(LoadCookies);
|
||||
LoadCookieJar(cookieJar);
|
||||
}
|
||||
|
||||
private IReadOnlyDictionary<string, string> LoadHeaders() => ResponseMessage.Headers
|
||||
.Concat(ResponseMessage.Content?.Headers ?? Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>())
|
||||
.GroupBy(h => h.Key)
|
||||
.ToDictionary(g => g.Key, g => string.Join(", ", g.SelectMany(h => h.Value)));
|
||||
private IReadOnlyNameValueList<string> LoadHeaders() {
|
||||
var result = new NameValueList<string>();
|
||||
|
||||
private IReadOnlyDictionary<string, FlurlCookie> LoadCookies() {
|
||||
foreach (var h in ResponseMessage.Headers)
|
||||
foreach (var v in h.Value)
|
||||
result.Add(h.Key, v);
|
||||
|
||||
if (ResponseMessage.Content?.Headers == null)
|
||||
return result;
|
||||
|
||||
foreach (var h in ResponseMessage.Content.Headers)
|
||||
foreach (var v in h.Value)
|
||||
result.Add(h.Key, v);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private IReadOnlyList<FlurlCookie> LoadCookies() {
|
||||
var url = ResponseMessage.RequestMessage.RequestUri.ToString();
|
||||
return ResponseMessage.Headers.TryGetValues("Set-Cookie", out var headerValues) ?
|
||||
headerValues
|
||||
.Select(hv => CookieCutter.FromResponseHeader(url, hv))
|
||||
.ToDictionary(c => c.Name) :
|
||||
new Dictionary<string, FlurlCookie>();
|
||||
headerValues.Select(hv => CookieCutter.ParseResponseHeader(url, hv)).ToList() :
|
||||
new List<FlurlCookie>();
|
||||
}
|
||||
|
||||
private void LoadCookieJar(CookieJar jar) {
|
||||
if (jar == null) return;
|
||||
foreach (var cookie in Cookies.Values)
|
||||
foreach (var cookie in Cookies)
|
||||
jar.TryAddOrUpdate(cookie, out _); // not added if cookie fails validation
|
||||
}
|
||||
|
||||
|
@ -589,7 +589,7 @@ namespace Flurl.Http
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new FlurlRequest and sets request headers based on property names/values of the provided object, or keys/values if object is a dictionary, to be sent
|
||||
/// Creates a new FlurlRequest and sets request headers based on property names/values of the provided object, or keys/values if object is a dictionary, to be sent.
|
||||
/// </summary>
|
||||
/// <param name="url">This Flurl.Url.</param>
|
||||
/// <param name="headers">Names/values of HTTP headers to set. Typically an anonymous object or IDictionary.</param>
|
||||
@ -621,7 +621,7 @@ namespace Flurl.Http
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new FlurlRequest and sets an HTTP cookie to be sent with this request only. To maintain a cookie "session", consider using WithCookies(CookieJar) or FlurlClient.StartCookieSession instead.
|
||||
/// Creates a new FlurlRequest and adds a name-value pair to its Cookie header. To automatically maintain a cookie "session", consider using a CookieJar or CookieSession instead.
|
||||
/// </summary>
|
||||
/// <param name="url">This Flurl.Url.</param>
|
||||
/// <param name="name">The cookie name.</param>
|
||||
@ -632,7 +632,7 @@ namespace Flurl.Http
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new FlurlRequest and sets HTTP cookies to be sent with this request only, based on property names/values of the provided object, or keys/values if object is a dictionary. To maintain a cookie "session", consider using WithCookies(CookieJar) or FlurlClient.StartCookieSession instead
|
||||
/// Creates a new FlurlRequest and adds name-value pairs to its Cookie header based on property names/values of the provided object, or keys/values if object is a dictionary. To automatically maintain a cookie "session", consider using a CookieJar or CookieSession instead.
|
||||
/// </summary>
|
||||
/// <param name="url">This Flurl.Url.</param>
|
||||
/// <param name="values">Names/values of HTTP cookies to set. Typically an anonymous object or IDictionary.</param>
|
||||
@ -1058,7 +1058,7 @@ namespace Flurl.Http
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new FlurlRequest and sets request headers based on property names/values of the provided object, or keys/values if object is a dictionary, to be sent
|
||||
/// Creates a new FlurlRequest and sets request headers based on property names/values of the provided object, or keys/values if object is a dictionary, to be sent.
|
||||
/// </summary>
|
||||
/// <param name="url">This URL.</param>
|
||||
/// <param name="headers">Names/values of HTTP headers to set. Typically an anonymous object or IDictionary.</param>
|
||||
@ -1090,7 +1090,7 @@ namespace Flurl.Http
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new FlurlRequest and sets an HTTP cookie to be sent with this request only. To maintain a cookie "session", consider using WithCookies(CookieJar) or FlurlClient.StartCookieSession instead.
|
||||
/// Creates a new FlurlRequest and adds a name-value pair to its Cookie header. To automatically maintain a cookie "session", consider using a CookieJar or CookieSession instead.
|
||||
/// </summary>
|
||||
/// <param name="url">This URL.</param>
|
||||
/// <param name="name">The cookie name.</param>
|
||||
@ -1101,7 +1101,7 @@ namespace Flurl.Http
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new FlurlRequest and sets HTTP cookies to be sent with this request only, based on property names/values of the provided object, or keys/values if object is a dictionary. To maintain a cookie "session", consider using WithCookies(CookieJar) or FlurlClient.StartCookieSession instead
|
||||
/// Creates a new FlurlRequest and adds name-value pairs to its Cookie header based on property names/values of the provided object, or keys/values if object is a dictionary. To automatically maintain a cookie "session", consider using a CookieJar or CookieSession instead.
|
||||
/// </summary>
|
||||
/// <param name="url">This URL.</param>
|
||||
/// <param name="values">Names/values of HTTP cookies to set. Typically an anonymous object or IDictionary.</param>
|
||||
@ -1527,7 +1527,7 @@ namespace Flurl.Http
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new FlurlRequest and sets request headers based on property names/values of the provided object, or keys/values if object is a dictionary, to be sent
|
||||
/// Creates a new FlurlRequest and sets request headers based on property names/values of the provided object, or keys/values if object is a dictionary, to be sent.
|
||||
/// </summary>
|
||||
/// <param name="uri">This System.Uri.</param>
|
||||
/// <param name="headers">Names/values of HTTP headers to set. Typically an anonymous object or IDictionary.</param>
|
||||
@ -1559,7 +1559,7 @@ namespace Flurl.Http
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new FlurlRequest and sets an HTTP cookie to be sent with this request only. To maintain a cookie "session", consider using WithCookies(CookieJar) or FlurlClient.StartCookieSession instead.
|
||||
/// Creates a new FlurlRequest and adds a name-value pair to its Cookie header. To automatically maintain a cookie "session", consider using a CookieJar or CookieSession instead.
|
||||
/// </summary>
|
||||
/// <param name="uri">This System.Uri.</param>
|
||||
/// <param name="name">The cookie name.</param>
|
||||
@ -1570,7 +1570,7 @@ namespace Flurl.Http
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new FlurlRequest and sets HTTP cookies to be sent with this request only, based on property names/values of the provided object, or keys/values if object is a dictionary. To maintain a cookie "session", consider using WithCookies(CookieJar) or FlurlClient.StartCookieSession instead
|
||||
/// Creates a new FlurlRequest and adds name-value pairs to its Cookie header based on property names/values of the provided object, or keys/values if object is a dictionary. To automatically maintain a cookie "session", consider using a CookieJar or CookieSession instead.
|
||||
/// </summary>
|
||||
/// <param name="uri">This System.Uri.</param>
|
||||
/// <param name="values">Names/values of HTTP cookies to set. Typically an anonymous object or IDictionary.</param>
|
||||
|
@ -21,10 +21,10 @@ namespace Flurl.Http
|
||||
/// <param name="value">HTTP header value.</param>
|
||||
/// <returns>This IFlurlClient or IFlurlRequest.</returns>
|
||||
public static T WithHeader<T>(this T clientOrRequest, string name, object value) where T : IHttpSettingsContainer {
|
||||
if (value == null && clientOrRequest.Headers.ContainsKey(name))
|
||||
if (value == null)
|
||||
clientOrRequest.Headers.Remove(name);
|
||||
else if (value != null)
|
||||
clientOrRequest.Headers[name] = value;
|
||||
else
|
||||
clientOrRequest.Headers.AddOrReplace(name, value);
|
||||
return clientOrRequest;
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Flurl.Http.Content;
|
||||
using Flurl.Util;
|
||||
|
||||
@ -68,8 +64,9 @@ namespace Flurl.Http
|
||||
Content.Headers.TryAddWithoutValidation(name, new[] { value.ToInvariantString() });
|
||||
break;
|
||||
default:
|
||||
// it's a request-level header
|
||||
Headers.Remove(name);
|
||||
// it's a request/response-level header
|
||||
if (!name.Equals("Set-Cookie", StringComparison.OrdinalIgnoreCase)) // multiple set-cookie headers are allowed
|
||||
Headers.Remove(name);
|
||||
if (value != null)
|
||||
Headers.TryAddWithoutValidation(name, new[] { value.ToInvariantString() });
|
||||
break;
|
||||
|
@ -14,9 +14,9 @@ namespace Flurl.Http
|
||||
/// </summary>
|
||||
FlurlHttpSettings Settings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Collection of headers sent on this request or all requests using this client.
|
||||
/// </summary>
|
||||
IDictionary<string, object> Headers { get; }
|
||||
/// <summary>
|
||||
/// Collection of headers sent on this request or all requests using this client.
|
||||
/// </summary>
|
||||
INameValueList<object> Headers { get; }
|
||||
}
|
||||
}
|
||||
|
135
src/Flurl.Http/NameValueList.cs
Normal file
135
src/Flurl.Http/NameValueList.cs
Normal file
@ -0,0 +1,135 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Flurl.Http
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines common methods for INameValueList and IReadOnlyNameValueList.
|
||||
/// </summary>
|
||||
public interface INameValueListBase<TValue>
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the first Value of the given Name if one exists, otherwise null or default value.
|
||||
/// </summary>
|
||||
TValue FirstOrDefault(string name);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the first Value of the given Name, if one exists.
|
||||
/// </summary>
|
||||
/// <returns>true if any item of the given name is found, otherwise false.</returns>
|
||||
bool TryGetFirst(string name, out TValue value);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all Values of the given Name.
|
||||
/// </summary>
|
||||
IEnumerable<TValue> GetAll(string name);
|
||||
|
||||
/// <summary>
|
||||
/// True if any items with the given Name exist.
|
||||
/// </summary>
|
||||
bool Contains(string name);
|
||||
|
||||
/// <summary>
|
||||
/// True if any item with the given Name and Value exists.
|
||||
/// </summary>
|
||||
bool Contains(string name, TValue value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines an ordered collection of Name/Value pairs where duplicate names are allowed but aren't typical.
|
||||
/// </summary>
|
||||
public interface INameValueList<TValue> : IList<(string Name, TValue Value)>, INameValueListBase<TValue>
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a new Name/Value pair.
|
||||
/// </summary>
|
||||
void Add(string name, TValue value);
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the first occurence of the given Name with the given Value and removes any others,
|
||||
/// or adds a new Name/Value pair if none exist.
|
||||
/// </summary>
|
||||
void AddOrReplace(string name, TValue value);
|
||||
|
||||
/// <summary>
|
||||
/// Removes all items of the given Name.
|
||||
/// </summary>
|
||||
/// <returns>true if any item of the given name is found, otherwise false.</returns>
|
||||
bool Remove(string name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines a read-only ordered collection of Name/Value pairs where duplicate names are allowed but aren't typical.
|
||||
/// </summary>
|
||||
public interface IReadOnlyNameValueList<TValue> : IReadOnlyList<(string Name, TValue Value)>, INameValueListBase<TValue>
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An ordered collection of Name/Value pairs where duplicate names are allowed but aren't typical.
|
||||
/// Useful for things where a dictionary would work great if not for those pesky edge cases (headers, cookies, etc).
|
||||
/// </summary>
|
||||
public class NameValueList<TValue> : List<(string Name, TValue Value)>, INameValueList<TValue>, IReadOnlyNameValueList<TValue>
|
||||
{
|
||||
/// <summary>
|
||||
/// Instantiates a new empty NameValueList.
|
||||
/// </summary>
|
||||
public NameValueList() { }
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates a new NameValueList with the Name/Value pairs provided.
|
||||
/// </summary>
|
||||
public NameValueList(IEnumerable<(string Name, TValue Value)> items) {
|
||||
AddRange(items);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Add(string name, TValue value) => Add((name, value));
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AddOrReplace(string name, TValue value) {
|
||||
var i = 0;
|
||||
var replaced = false;
|
||||
while (i < this.Count) {
|
||||
if (this[i].Name != name)
|
||||
i++;
|
||||
else if (replaced)
|
||||
this.RemoveAt(i);
|
||||
else {
|
||||
this[i] = (name, value);
|
||||
replaced = true;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!replaced)
|
||||
this.Add(name, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Remove(string name) => RemoveAll(x => x.Name == name) > 0;
|
||||
|
||||
/// <inheritdoc />
|
||||
public TValue FirstOrDefault(string name) => GetAll(name).FirstOrDefault();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryGetFirst(string name, out TValue value) {
|
||||
foreach (var v in GetAll(name)) {
|
||||
value = v;
|
||||
return true;
|
||||
}
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TValue> GetAll(string name) => this.Where(x => x.Name == name).Select(x => x.Value);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Contains(string name) => this.Any(x => x.Name == name);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Contains(string name, TValue value) => Contains((name, value));
|
||||
}
|
||||
}
|
@ -210,7 +210,7 @@ namespace Flurl.Http.Testing
|
||||
var descrip = $"any header {string.Join(", ", names)}".Trim();
|
||||
return With(call => {
|
||||
if (!names.Any()) return call.Request.Headers.Any();
|
||||
return call.Request.Headers.Select(h => h.Key).Intersect(names).Any();
|
||||
return call.Request.Headers.Select(h => h.Name).Intersect(names).Any();
|
||||
}, descrip);
|
||||
}
|
||||
|
||||
@ -271,7 +271,7 @@ namespace Flurl.Http.Testing
|
||||
var descrip = $"any cookie {string.Join(", ", names)}".Trim();
|
||||
return With(call => {
|
||||
if (!names.Any()) return call.Request.Cookies.Any();
|
||||
return call.Request.Cookies.Select(c => c.Key).Intersect(names).Any();
|
||||
return call.Request.Cookies.Select(c => c.Name).Intersect(names).Any();
|
||||
}, descrip);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
@ -44,7 +44,7 @@ namespace Flurl.Http.Testing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an HttpResponseMessage to the response queue.
|
||||
/// Adds a fake HTTP response to the response queue.
|
||||
/// </summary>
|
||||
/// <param name="body">The simulated response body string.</param>
|
||||
/// <param name="status">The simulated HTTP status. Default is 200.</param>
|
||||
@ -57,7 +57,7 @@ namespace Flurl.Http.Testing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an HttpResponseMessage to the response queue with the given data serialized to JSON as the content body.
|
||||
/// Adds a fake HTTP response to the response queue with the given data serialized to JSON as the content body.
|
||||
/// </summary>
|
||||
/// <param name="body">The object to be JSON-serialized and used as the simulated response body.</param>
|
||||
/// <param name="status">The simulated HTTP status. Default is 200.</param>
|
||||
@ -71,7 +71,7 @@ namespace Flurl.Http.Testing
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an HttpResponseMessage to the response queue.
|
||||
/// Adds a fake HTTP response to the response queue.
|
||||
/// </summary>
|
||||
/// <param name="buildContent">A function that builds the simulated response body content. Optional.</param>
|
||||
/// <param name="status">The simulated HTTP status. Optional. Default is 200.</param>
|
||||
|
@ -70,8 +70,8 @@ namespace Flurl.Http.Testing
|
||||
/// </summary>
|
||||
internal static bool HasHeader(this FlurlCall call, string name, object value = null) {
|
||||
return (value == null) ?
|
||||
call.Request.Headers.ContainsKey(name) :
|
||||
call.Request.Headers.TryGetValue(name, out var val) && MatchesValueOrPattern(val, value);
|
||||
call.Request.Headers.Contains(name) :
|
||||
call.Request.Headers.TryGetFirst(name, out var val) && MatchesValueOrPattern(val, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -79,8 +79,8 @@ namespace Flurl.Http.Testing
|
||||
/// </summary>
|
||||
internal static bool HasCookie(this FlurlCall call, string name, object value = null) {
|
||||
return (value == null) ?
|
||||
call.Request.Cookies.ContainsKey(name) :
|
||||
call.Request.Cookies.TryGetValue(name, out var val) && MatchesValueOrPattern(val, value);
|
||||
call.Request.Cookies.Any(c => c.Name == name) :
|
||||
MatchesValueOrPattern(call.Request.Cookies.FirstOrDefault(c => c.Name == name).Value, value);
|
||||
}
|
||||
|
||||
private static bool MatchesValueOrPattern(object valueToMatch, object value) {
|
||||
|
@ -39,11 +39,20 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\icon.png" Pack="true" PackagePath="\"/>
|
||||
<None Include="..\..\icon.png" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'net40'">
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard1.0'">
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard1.3'">
|
||||
<PackageReference Include="System.Reflection.TypeExtensions" Version="4.4.0" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
@ -3,10 +3,8 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
#if !NET40
|
||||
using System.Reflection;
|
||||
#endif
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Flurl.Util
|
||||
{
|
||||
@ -89,34 +87,51 @@ namespace Flurl.Util
|
||||
}
|
||||
|
||||
private static IEnumerable<KeyValuePair<string, object>> CollectionToKV(IEnumerable col) {
|
||||
// Accepts KeyValuePairs or any arbitrary types that contain a property called "Key" or "Name" and a property called "Value".
|
||||
bool TryGetProp(object obj, string name, out object value) {
|
||||
#if NETSTANDARD1_0
|
||||
var prop = obj.GetType().GetRuntimeProperty(name);
|
||||
var field = obj.GetType().GetRuntimeField(name);
|
||||
#else
|
||||
var prop = obj.GetType().GetProperty(name);
|
||||
var field = obj.GetType().GetField(name);
|
||||
#endif
|
||||
if (prop != null) {
|
||||
value = prop.GetValue(obj, null);
|
||||
return true;
|
||||
}
|
||||
if (field != null) {
|
||||
value = field.GetValue(obj);
|
||||
return true;
|
||||
}
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsTuple2(object item, out object name, out object val) {
|
||||
name = null;
|
||||
val = null;
|
||||
return
|
||||
item.GetType().Name.Contains("Tuple") &&
|
||||
TryGetProp(item, "Item1", out name) &&
|
||||
TryGetProp(item, "Item2", out val) &&
|
||||
!TryGetProp(item, "Item3", out _);
|
||||
}
|
||||
|
||||
bool LooksLikeKV(object item, out object name, out object val) {
|
||||
name = null;
|
||||
val = null;
|
||||
return
|
||||
(TryGetProp(item, "Key", out name) || TryGetProp(item, "key", out name) || TryGetProp(item, "Name", out name) || TryGetProp(item, "name", out name)) &&
|
||||
(TryGetProp(item, "Value", out val) || TryGetProp(item, "value", out val));
|
||||
}
|
||||
|
||||
foreach (var item in col) {
|
||||
if (item == null)
|
||||
continue;
|
||||
|
||||
string key;
|
||||
object val;
|
||||
|
||||
var type = item.GetType();
|
||||
#if NETSTANDARD1_0
|
||||
var keyProp = type.GetRuntimeProperty("Key") ?? type.GetRuntimeProperty("key") ?? type.GetRuntimeProperty("Name") ?? type.GetRuntimeProperty("name");
|
||||
var valProp = type.GetRuntimeProperty("Value") ?? type.GetRuntimeProperty("value");
|
||||
#else
|
||||
var keyProp = type.GetProperty("Key") ?? type.GetProperty("key") ?? type.GetProperty("Name") ?? type.GetProperty("name");
|
||||
var valProp = type.GetProperty("Value") ?? type.GetProperty("value");
|
||||
#endif
|
||||
|
||||
if (keyProp != null && valProp != null) {
|
||||
key = keyProp.GetValue(item, null)?.ToInvariantString();
|
||||
val = valProp.GetValue(item, null);
|
||||
}
|
||||
else {
|
||||
key = item.ToInvariantString();
|
||||
val = null;
|
||||
}
|
||||
|
||||
if (key != null)
|
||||
yield return new KeyValuePair<string, object>(key, val);
|
||||
if (!IsTuple2(item, out var name, out var val) && !LooksLikeKV(item, out name, out val))
|
||||
yield return new KeyValuePair<string, object>(item.ToInvariantString(), null);
|
||||
else if (name != null)
|
||||
yield return new KeyValuePair<string, object>(name.ToInvariantString(), val);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user