找回密码
 立即注册
快捷导航

[C#] 流畅的URL构建方式 - Flurl

[复制链接]

流畅的URL构建方式

using Flurl;

var url = "https://www.so.com"
    .AppendPathSegment("endpoint")
    .SetQueryParams(new {
        api_key = "sdfdsfsdfs",
        max_results = 20,
        q = "Don't worry, I'll get encoded!"
    })
    .SetFragment("after-hash");

string urlStr = url.toString();
// https://www.so.com/endpoint?api_key=sdfdsfsdfs&max_results=20&q=Don%27t%20worry%2C%20I%27ll%20get%20encoded%21#after-hash

上面例子(以及其他绝大多数示例)使用String扩展方法隐式地创建 Url 对象;

如果您愿意,你可以使用Flurl内置的Url实例显式构建类似的Url对象,如:

var url = new Url("https://www.so.com").AppendPathSegment(...

除了上面的对象表示法外,SetQueryParams 还接受任何 键值对集合、Tuples 或 Dictionary 对象;这些替代方案对于不是有效的c#标识符的参数名特别有用。如果你想逐个设置他们,也可以使用 SetQueryParam;

在任何情况下,这些方法都会覆盖以前设置的相同名称的值,但是您可以通过传递一个集合来设置多个相同名称的值:

var url = "http://www.mysite.com".SetQueryParam("x", new[] { 1, 2, 3 });
Assert.AreEqual("http://www.mysite.com?x=1&x=2&x=3", url);   // true

在Flurl中,构建器方法及其重载是高度显式的、直观的,并且总是链式的,一些破坏性的方法如:RemoveQueryParam、 RemovePathSegment 和 ResetToRoot 也包括在内。

Parsing/URL解析

除了构建 URL 外, Flurl还可以有效地解析现有的URL,如下:

var url = new Url("https://user:pass@www.mysite.com:1234/with/path?x=1&y=2#foo");
Assert.AreEqual("https", url.Scheme);
Assert.AreEqual("user:pass", url.UserInfo);
Assert.AreEqual("www.mysite.com", url.Host);
Assert.AreEqual(1234, url.Port);
Assert.AreEqual("user:pass@www.mysite.com:1234", url.Authority);
Assert.AreEqual("https://user:pass@www.mysite.com:1234", url.Root);
Assert.AreEqual("/with/path", url.Path);
Assert.AreEqual("x=1&y=2", url.Query);
Assert.AreEqual("foo", url.Fragment);

虽然这些解析功能类似于C#内置的URL解析功能,但Flurl的目标是与RFC 3986更加兼容,并且更符合的实际字符串应用场景,因此有以下方面的不同之处:

  • Uri.Query 方法包含 ?字符,而 Url.Query 方法不包含;
  • Uri.Fragment 方法包含 # 字符,Url.Fragment 方法不包含;
  • Uri.AbsolutePath 方法始终包含一个开头 / 字符,而 Url.Path 只有在它实际存在于原始字符串中时才会包含它,例如,对于 http://foo.com,Url.Path 方法返回的是一个空字符串。
  • Uri.Authority 方法不包含用户信息(如 user:pass@); Url.Authority 方法则会包含获取到的用户信息;
  • Uri.Port 方法在未指定端口时会返回默认的端口值,而Url.Port方法在未指定端口时为nullable;
  • Uri 不会尝试去解析相对 URL 地址;而 Url的类中,假设Url字符串不是以{scheme}://开头,那么它将以一个路径开始并相应地解析它。

Url.QueryParams 是一种维护了参数顺序的特殊集合类型,它允许重复名称,但针对唯一名称的典型情况进行了优化,如下:

var url = new Url("https://www.mysite.com?x=1&y=2&y=3");
Assert.AreEqual("1", url.QueryParams.FirstOrDefault("x"));    // true
Assert.AreEqual(new[] { "2", "3" }, url.QueryParams.GetAll("y"));      // true

Mutability/可变性

Url 实际上是隐式转换为字符串的可变生成器对象。

如果您需要一个不可变的 URL,比如将 基URL 作为类的一个成员变量,常见的模式是将其作为字符串输入,如:

public class MyServiceClass
{
    private readonly string _baseUrl; // 使用了 readonly ,_baseUrl 是不可变的;

    public Task CallServiceAsync(string endpoint, object data) {
        return _baseUrl
            .AppendPathSegment(endpoint)    // 这里会生成一个 Url 对象
            .PostAsync(data);  // 请引用Flurl.Http命名空间
    }
}

这里调用AppendPathSegment方法创建了一个新的Url对象。结果是_baseUrl没有被修改,与将其声明为Url相比,也没有添加额外的信息。

另一种避免 Url 可变性的方法是使用 Clone ()方法:

var url2 = url1.Clone().AppendPathSegment("next");

在这里,使用Clone()方法复制并获得了基于另一个Url对象的新Url对象,因此可以在不改变原始Url的情况下对复制的Url进行修改。

编码

Flurl负责对url中的字符进行编码,但对路径段(path segment)采用的方法与对查询字符串值采用的方法不同。

假设查询字符串值是高度可变的(例如来自用户输入) ,而路径段往往更“固定”(可能已经编码了),在这种情况下,您不希望进行双重编码。以下是 Flurl 遵循的规则:

  • 查询字符串值 是完全 URL-encoded 的。
  • 对于 路径段,不对 /% 等保留字符进行编码。
  • 对于 路径段,会对非法字符(如空格)进行编码。
  • 对于路径段,? 字符会被编码,因为 查询字符串 会进行特殊处理。

在某些情况下,你可能希望设置一个已经知道编码的查询参数。SetQueryParam 具有一个可选的 isEncode 参数来设置:

url.SetQueryParam("x", "don%27t%20touch%20me", true);

虽然空格字符的官方URL编码是 %20,但在查询参数中使用字符+是很常见的。您可以通过 Url.ToString 的可选 encodeSpaceAsPlus 参数来指定是否将空格编码成+号,示例如下:

var url = "http://foo.com".SetQueryParam("x", "hi there");
Assert.AreEqual("http://foo.com?x=hi%20there", url.ToString());
Assert.AreEqual("http://foo.com?x=hi+there", url.ToString(true));

其他一些实用方法

Url 还包含一些方便的静态方法,比如 Combine,它是一个用来组合URL地址地类似C#文件系统中的Path.Combine方法URL地址,确保各部分之间只有一个分隔符,如:

var url = Url.Combine(
    "http://foo.com/",
    "/too/", "/many/", "/slashes/",
    "too", "few?",
    "x=1", "y=2"

// http://www.foo.com/too/many/slashes/too/few?x=1&y=2

并且为了帮助您避免一些与.NET 中的各种 URL endoing/decoding 方法相关的臭名昭著的怪癖,Flurl 提供了一些“无怪癖”的替代方案:

Url.Encode(string s, bool encodeSpaceAsPlus); // 包含保留字符,如 / 和 ?
Url.EncodeIllegalCharacters(string s, bool encodeSpaceAsPlus); // 保留的字符不会被 touched
Url.Decode(string s, bool interpretPlusAsSpace);

Fluent HTTP

注意: 除了 URL building 和 parsing 之外的所有操作都需要安装 Flurl.Http,而不是基本的 Flurl 包。

using Flurl;
using Flurl.Http;

// 发送一个 Get 请求,返回 IFlurlResponse 对象;
var result = await baseUrl.AppendPathSegment("endpoint").GetAsync();

返回的 IFlurlResponse 对象,可以通过 GetStringAsyncGetJsonAsync<T> 等 方法获得属性如 StatusCode、 Header 和 body 内容。

但是通常你只想获得 body 内容,Flurl 提供了更加便捷的方式:

T poco = await "http://api.foo.com".GetJsonAsync<T>();
string text = await "http://site.com/readme.txt".GetStringAsync();
byte[] bytes = await "http://site.com/image.jpg".GetBytesAsync();
Stream stream = await "http://site.com/music.mp3".GetStreamAsync();

对于JSON API,通常最好定义一个与预期JSON响应数据类型匹配的类(T)。但如果你不想创建这个匹配的类,也可以换动态(dynamic)的对象,比如:

dynamic d = await "http://api.foo.com".GetJsonAsync();

或者,从返回JSON数组的API中获取一个动态列表,比如:

var list = await "http://api.foo.com".GetJsonListAsync();

下载一个文件:

// 文件名 在这里是可选的; 如果不指定,默认会使用远程文件的文件名
var path = await "http://files.foo.com/image.jpg"
    .DownloadFileAsync("c:\\downloads", filename);

从请求中读取其他(如:头部信息等):

var headResponse = await "http://api.foo.com".HeadAsync();      // 获取Head参数
var optionsResponse = await "http://api.foo.com".OptionsAsync();

发送数据

await "http://api.foo.com".PostJsonAsync(new { a = 1, b = 2 });      // Post
await "http://api.foo.com/1".PatchJsonAsync(new { c = 3 });         // Patch
await "http://api.foo.com/2".PutStringAsync("hello");         // Put

上面的所有方法都返回 Task<IFurlResponse>。当然,您可能希望在响应主体中返回一些数据:

T poco = await url.PostAsync(content).ReceiveJson<T>();
string s = await url.PatchJsonAsync(partial).ReceiveString();

或者一些奇特的HTTP请求需求,比如:

await url.PostAsync(content); // 这里的content为System.Net.Http.HttpContent对象
await url.SendJsonAsync(HttpMethod.Trace, data);
await url.SendAsync(
    new HttpMethod("CONNECT"),
    httpContent, // 可选的
    cancellationToken,  // 可选的
    HttpCompletionOption.ResponseHeaderRead);  // 可选的

设置请求头:

// 一个:
await url.WithHeader("Accept", "text/plain").GetJsonAsync();
// 多个:
await url.WithHeaders(new { Accept = "text/plain", User_Agent = "Flurl" }).GetJsonAsync();

User_Agent 将在 header 中自动呈现为 User-Agent。(连字符在 header names 中非常常见,但是在 C# 标识符中不允许; 而下划线恰恰相反)

指定超时时间:

await url.WithTimeout(10).DownloadFileAsync(); // 10 秒
await url.WithTimeout(TimeSpan.FromMinutes(2)).DownloadFileAsync();   // 超时时间设置成了2分钟

取消请求:

var cts = new CancellationTokenSource();
var task = url.GetAsync(cts.Token);
...
cts.Cancel();

使用 Basic authentication 进行身份验证:

await url.WithBasicAuth("username", "password").GetJsonAsync();

或者一个 OAuth 2.0 bearer token 身份认证:

await url.WithOAuthBearerToken("mytoken").GetJsonAsync();

模拟 HTML post表单:

await "http://site.com/login".PostUrlEncodedAsync(new { 
    user = "user", 
    pass = "pass"
});

或一个 multipart/form-data 的 POST表单:

var resp = await "http://api.com".PostMultipartAsync(mp => mp
    .AddString("name", "hello!")                // individual string
    .AddStringParts(new {a = 1, b = 2})         // multiple strings
    .AddFile("file1", path1)                    // local file path
    .AddFile("file2", stream, "foo.txt")        // file stream
    .AddJson("json", new { foo = "x" })         // json
    .AddUrlEncoded("urlEnc", new { bar = "y" }) // URL-encoded                      
    .Add(content));    

携带 cookie 的请求:

var resp = await "https://cookies.com"
    .WithCookie("name", "value")
    .WithCookies(new { cookie1 = "foo", cookie2 = "bar" })
    .GetAsync();

更好的做法是,从第一个请求中获取响应 cookie,让 Flurl 决定何时将它们发送回去(根据 RFC6265) ,比如:

await "https://cookies.com/login".WithCookies(out var jar).PostUrlEncodedAsync(credentials);
await "https://cookies.com/a".WithCookies(jar).GetAsync();
await "https://cookies.com/b".WithCookies(jar).GetAsync();

或者使用CookieSession来代替WithCookies,如:

using (var session = new CookieSession("https://cookies.com")) {
    // set any initial cookies on session.Cookies
    // 在 sessin.Cookies 中设置初始cookies
    await session.Request("a").GetAsync();
    await session.Request("b").GetAsync();
    // read cookies at any point using session.Cookies
    // 在任何时候使用 session.Cookies 读取 cookies
}

CookieJar可以显式地被创建和修改,对需要长期保存的Cookies有可能会有用,比如:

var jar = new CookieJar()
    .AddOrUpdate("cookie1", "foo", "https://cookies.com") // 必须指定原始的URL地址
    .AddOrUpdate("cookie2", "bar", "https://cookies.com");

await "https://cookies.com/a".WithCookies(jar).GetAsync();

CookieJar是Flurl在HttpClient堆栈中的CookieContainer的对等物,它有一个主要优点: 它没有绑定到 HttpMessageHandler 对象,因此你可以在单个 HttClient/Handler 实例上模拟多个 cookie “sessions”。

Testable HTTP/可测试HTTP

Flurl.Http 提供了一组测试特性,这些特性让 arrange-act-assert 测试变得非常简单。

其核心是 HttpTest;

using Flurl.Http.Testing;

[Test]
public void Test_Some_Http_Calling_Method() {
    using (var httpTest = new HttpTest()) {
        // Flurl 现在进入测试模式
        sut.CallThingThatUsesFlurlHttp(); // HTTP 调用是伪造的!
    }
}

...

Configuration/配置

Flurl.Http 行为可以通过分层设置系统进行配置,每一层按如下配置顺序继承/覆盖前一层:

  • FlurlHttp.GlobalSettings (静态的)
  • IFlurlClient.Settings
  • IFlurlRequest.Settings
  • HttpTest.Settings (配置的测试参数总是有效性最高)

所有4个级别的可用属性大多相同,只有少数例外。下面是一个完整的列表,包括它们的位置和不支持的位置:

流畅的URL构建方式 - Flurl1889 作者:清风拂柳夜微凉 帖子ID:327

请注意,只有没有显式地设置值时,才表示要从层次结构的上层继承。null的意思是null,而不是继承。

配置设置

设置属性都是 可读/写 的,但是如果您希望一次(原子地)更改多个设置参数,你通常应该用一个 Configure* 方法之一,该方法使用 Action<Settings> 委托。

配置全局默认值:

// 在用户程序启动时调用一次即可
FlurlHttp.Configure(settings => ...);

您还可以配置用于调用给定URL的FlurlClient。需要注意的是,在默认情况下对同一主机的所有调用都使用相同的 FlurlClient,所以这可能会影响到的不仅仅是对提供的特定URL的调用:

// 在用户程序启动时调用一次即可
FlurlHttp.ConfigureClient(url, cli => ...);

或者,你也可以显式地配置FlurlClient:

flurlClient.Configure(settings => ...);

或者,链式地配置单个请求(通过字符串、Url或IFlurlRequest的扩展方法):

await url.ConfigureRequest(settings => ...).GetAsync();

还可以在测试中覆盖任何设置,无论这些设置在测试对象的哪个级别:

httpTest.Configure(settings => ...);

如果需要,您可以将任何级别的设置恢复为它们的默认值或继承值:

flurlClient.Settings.ResetDefaults();
FlurlHttp.GlobalSettings.ResetDefaults();

下面让我们来看看一些特定的设置。

HttpClientFactory

不要与.NET Core的 IHttpClientFactory 混淆;它们是非常不同的东西(Flurl的IHttpClientFactory比.NET Core的出现得早)。

对于高级场景,您可以自定义Flurl.Http构造HttpClient和HttpMessageHandler实例。尽管只需要实现Flurl.Http.Configuration.IHttpClientFactory,但建议从DefaultHttpClientFactory继承,并只在需要时扩展,如下:

public class MyCustomHttpClientFactory : DefaultHttpClientFactory
{
    // 重写 CreateHttpClient 方法,用以自定义 HttpClient 如何创建和配置
    public override HttpClient CreateHttpClient(HttpMessageHandler handler);

    // 重写 CreateMessageHandler 方法,用以自定义 HttpMessageHandler 如何创建和配置
    public override HttpMessageHandler CreateMessageHandler();
}

注册全局工厂:

FlurlHttp.Configure(settings => {
    settings.HttpClientFactory = new MyCustomHttpClientFactory();
});

或者在单个FlurlClient上配置(不常见):

var cli = new FlurlClient(BASE_URL).Configure(settings => {
    settings.HttpClientFactory = new MyCustomHttpClientFactory();
});

提醒你几句:

  1. Overriding CreateMessageHandler can be very useful for configuring things like proxies and client certificates, but some features that Flurl has re-implemented, such as cookies and redirects, require that the related settings on HttpClientHandler remain disabled in order to function properly. It's a best practice to call base.CreateMessageHandler() and configure/return that, rather than creating a new one yourself. If in doubt, have a look at what Flurl does by default and avoid straying farther from that implementation than necessary.

    1. 重写 CreateMessageHandler 对于配置代理和客户端证书之类的东西非常有用,但是 Flurl 重新实现的一些特性,如 cookies 和重定向,要求HttpClientHandler上的相关设置保持禁用状态,以便正常工作。最好的做法是调用 base.CreateMessageHandler() 并配置/返回它,而不是自己创建一个新的。如果有疑问,看看 Flurl 在默认情况下做了什么,避免偏离实现太远。
  2. A custom HttpClientFactory should be concerned only with creating these objects, not caching/reusing them. That's a concern of FlurlClientFactory.

    1. 自定义 HttpClientFactory 应该只关心创建这些对象,而不是缓存/重用它们。这是 FlurlClientFactory 所担心的。

FlurlClientFactory

IFlurlClientFactory接口定义了一个方法Get(Url),该方法负责提供应该用于调用该Url的IFlurlClient实例。在应用程序的生命周期内,默认实现了根据URL的主机/方案/端口的组合使用一个缓存的FlurlClient实例。

要改变这种行为,你可以通过直接实现IFlurlClientFactory来定义你自己的工厂,但是从FlurlClientFactoryBase继承要容易得多。它允许您通过返回基于Url的缓存键来定义缓存策略,而不必实现缓存本身,如下:

public abstract class FlurlClientFactoryBase : IFlurlClientFactory
{
    // 重写GetCacheKey方法
    protected abstract string GetCacheKey(Url url);

    // 重写Create方法(仅在需要时才调用此方法)
    protected virtual IFlurlClient Create(Url url);
}

然FlurlClientFactory配置设置仅在全局级别可用,但IFlurlClientFactory在依赖注入模式时也很有用。

序列化

JsonSerializer 和 UrlEncodedSerializer 都实现了ISerializer,这是一个简单的接口,用于将对象与字符串进行序列化。

public interface ISerializer
{
    string Serialize(object obj);
    T Deserialize<T>(string s);
    T Deserialize<T>(Stream stream);
}

两者都有一个全局注册的默认实现,你可以替换它们,但不是必要的。默认的JsonSerializer实现是NewtonsoftJsonSerializer,正如您可能猜到的那样,它使用了一直流行的Json.NET序列化类库。你可以使用NewtonsoftJsonSerializer来自定义序列化配置,如下:

FlurlHttp.Configure(settings => {
    var jsonSettings = new JsonSerializerSettings
    {
        NullValueHandling = NullValueHandling.Ignore,
        ObjectCreationHandling = ObjectCreationHandling.Replace
    };
    settings.JsonSerializer = new NewtonsoftJsonSerializer(jsonSettings);
});

事件处理

将关注点(如日志记录和错误处理)与正常的逻辑流分离开,通常会使代码更加清晰。Flurl.Http 为这些场景提供了一个事件模型。BeforeCall,AfterCall, OnError, OnRedirect 以及它们的*Async同名异步方法都在全局或客户端级别被定义,但如果需要的话,也可以在每个请求中定义。这些方法都采用了Action<HttpCall>委托,FlurlCall提供了你可以利用的关于调用的丰富细节:

public class FlurlCall
{
    public IFlurlRequest Request { get; set; }
    public HttpRequestMessage HttpRequestMessage { get; set; }
    public IFlurlResponse Response { get; set; }
    public HttpResponseMessage HttpResponseMessage { get; set; }
    public string RequestBody { get; }
    public FlurlRedirect Redirect { get; set; }
    public FlurlCall RedirectedFrom { get; set; }
    public Exception Exception { get; set; }
    public bool ExceptionHandled { get; set; }
    public DateTime StartedUtc { get; set; }
    public DateTime? EndedUtc { get; set; }
    public TimeSpan? Duration { get; }
    public bool Completed { get; }
    public bool Succeeded { get; }
}

与响应有关的属性值在BeforeCall中都是null,AfterCall在请求成功或者失败时都会被触发,在OnError中将ExceptionHandled设置为true可以防止出现异常。下面是一个注册全局异步错误处理程序的示例:

private async Task HandleFlurlErrorAsync(HttpCall call) {
    await LogErrorAsync(call.Exception.Message);
    call.ExceptionHandled = true;
}

FlurlHttp.Configure(settings => settings.OnErrorAsync = HandleFlurlErrorAsync);

重定向(Redirects)

像HttpClient一样,Flurl默认自动跟随3xx重定向,但是Flurl公开的设置和钩子提供了更高级别的可配置性,如下:

FlurlHttp.Configure(settings => {
    settings.Redirects.Enabled = true; // default true
    settings.Redirects.AllowSecureToInsecure = true; // default false
    settings.Redirects.ForwardAuthorizationHeader = true; // default false
    settings.Redirects.MaxAutoRedirects = 5; // default 10 (consecutive)
});

你还可以使用事件处理程序在每次调用的基础上配置重定向行为:

flurlClient.OnRedirect(call => {
    if (call.Redirect.Count > 5) {
        call.Redirect.Follow = false;
    }
    else {
        log.WriteInfo($"redirecting from {call.Request.Url} to {call.Redirect.Url}");
        call.Redirect.ChangeVerbToGet = (call.Response.Status == 301);
        call.Redirect.Follow = true;
    }
});

如果你只需要启用/禁用单个调用的自动重定向,你可以内联执行:

await url.WithAutoRedirect(false).GetAsync();

Error Handling/错误处理

与 HttpClient 不同,Flurl.HTTP 在默认情况下会抛出任何非2XX 的 HTTP 状态:

  • 非2xx情况往往是“异常的”,也就是说,在“正常”的环境和逻辑流中会是2xx状态,因此非2xx状态适合try/catch范式。
  • 特别是在 JSON API 中,错误响应体倾向于采用与常规响应不同的形态,并且如果您使用如url.GetJsonAsync<RegularShape>()这样的快捷处理方法的话, Flurl的try/catch模式提供了一种反序列化到catch块中不同内容的方法,如下:
try {
    var result = await url.PostJsonAsync(poco).ReceiveJson<T>();
}
catch (FlurlHttpException ex) {
    var error = await ex.GetResponseJsonAsync<TError>();
    logger.Write($"Error returned from {ex.Call.Request.Url}: {error.SomeDetails}");
}

以上的调用属性是事件处理程序使用的同一个FlurlCall对象的实例,它提供了关于调用的丰富细节。对于简单的日志和调试,FlurlHttpException.Message提供了错误的简易摘要,包括URL、HTTP谓词和接收到的状态代码等等。

FlurlHttpException还提供了一些反序列化body的快捷方式:

Task<string> GetResponseStringAsync();
Task<T> GetResponseJsonAsync<T>();

这些都是 FlurlHttpException.Call 上等效方法的简写,因此如果你需要获取例如流这样的不同数据,您可以使用该方式。

超时(Timeouts)

Flurl.Http为超时定义了一个特殊的异常类型:FlurlHttpTimeoutException,这种类型继承自FlurlHttpException,因此会在catch (FlurlHttpException)块中被捕获,同时,你也可以使用不同的方式来处理超时:

catch (FlurlHttpTimeoutException) {
    // handle timeout
}
catch (FlurlHttpException) {
    // handle error response
}

除了FlurlHttpTimeoutException之外没有其他属性,但因为超时意味着没有收到响应,所有与响应相关的属性都将始终为null。

默认超时是100秒(与HttpClient相同),但这可以在任何设置级别上配置,或者内联每个请求:

await url.WithTimeout(200).GetAsync(); // 200 seconds
await url.WithTimeout(TimeSpan.FromMinutes(10)).GetAsync();

允许非2xx响应

如果你不喜欢默认的抛出行为,你可以通过Settings.AllowedHttpStatusRange在任何设置级别更改它,这是一个基于字符串(除了通配符)的设置,所以如果你从来不想抛出,可将它设置为*。

你也可以在请求级别允许非2XX:

url.AllowHttpStatus("400-404,6xx").GetAsync();
url.AllowAnyHttpStatus().GetAsync();

第一个示例中的模式是显而易见的,AllowHttpStatus()方法允许的字符包括数字、逗号(分隔符)、连字符(范围)和通配符x或X或*。这些语法规则与Settings.AllowedHttpStatusRange是相同的,但是有一个细微的行为差异:上面的请求级别方法是可添加的,所以,例如如果每个设置都允许2xx,你就不必在请求级别模式中包含它。

在反序列化前检查响应

如果你喜欢把非2xx状态作为正常控制流的一部分来处理,这很简单:

var response = await url
    .AllowAnyHttpStatus()
    .GetAsync();

if (result.StatusCode < 300) {
    var result = await response.GetJsonAsync<T>();
    Console.WriteLine($"Success! {result}")
}
else if (result.StatusCode < 500) {
    var error = await response.GetJsonAsync<UserErrorData>();
    Console.WriteLine($"You did something wrong! {error}")
}
else {
    var error = await response.GetJsonAsync<ServerErrorData>();
    Console.WriteLine($"We did something wrong! {error}")
}

这里的response是IFlurlResponse的一个实例,它封装了System.Net.Http.HttpResponseMessage信息,除了StatusCode之外,你还可以通过多种方式检查头部、cookie以及获取正文内容:

Task<T> GetJsonAsync<T>();
Task<string> GetStringAsync();
Task<Stream> GetStreamAsync();
Task<byte[]> GetBytesAsync();

HttpClient 生命周期管理

Flurl.Http 是建立在 System.Net.Http 栈上的,如果您熟悉 HttpClient,您可能已经知道以下建议:

HttpClient 是实例化一次,并在应用程序的整个生命周期中复用。特别是在服务器应用程序中,为每个请求创建一个新的 HttpClient 实例将耗尽重负载下可用的 socket 数量。这将导致 SocketException 错误。

Flurl.Http 默认情况下遵守本指南。Fluent方法将创建一个 HttpClient,缓存它,并在对同一主机^*的每次调用中重用它:

var data = await "http://api.com/endpoint".GetJsonAsync();
  • 在3.0中,Scheme 是端口,也是 cache 密钥的一部分。因此,例如,如果您同时访问同一主机的 http 和 https 端点,您将有两个可以独立配置的客户端实例。

显式管理实例

FlurlClient 是 HttpClient 的轻量级包装器,与其生命周期紧密绑定。它实现 IDisposable,当释放时也将释放 HttpClient。FlurlClient 包括 BaseUrl 属性,以及 Header、 Settings 和许多您可能已经熟悉的fluent方法。这些属性和方法大多用于设置可在请求级别重写的默认值。

您可以显式创建 FlurlClient 并(可选)流畅地配置它:

var cli = new FlurlClient("https://api.com")
    .WithOAUthBearerToken(token))
    .Configure(settings => ...);

Fluent 从 Request 方法开始调用 FlurlClient,该方法可以选择接受一个或多个 URL 路径段:

await cli.Request("path", "to", "endpoint") // Request().AppendPathSegments(...) 的简写
    .SetQueryParams(args)
    .PostJsonAsync(data)
    .ReceiveJson<T>();

使用 IoC 容器的 Flurl

...

可扩展性(Extensibility)

由于Flurl的大部分功能是通过扩展方法提供的,所以使用Flurl本身使用的相同模式来扩展非常容易。

扩展URL构建器

链式的URL生成器方法通常有三种重载:一种是扩展Flurl.Url,一种是扩展System.Uri以及扩展String。所有这些扩展都应该返回修改后的Flurl.Url对象:

public static Url DoMyThing(this Url url) {
    // do something interesting with url
    return url;
}

// keep these overloads DRY by constructing a Url and deferring to the above method
public static Url DoMyThing(this Uri uri) => new Url(uri).DoMyThing(); 
public static Url DoMyThing(this string url) => new Url(url).DoMyThing();

扩展Flurl.Http

链式的Flurl.Http扩展方法通常包含4个,分别为:Flurl.UrlSystem.UriString以及IFlurlRequest。所有的都应该返回当前的IFlurlRequest,以便进一步的链式扩展,如下:

public static IFlurlRequest DoMyThing(this IFlurlRequest req) {
    // 使用 req.Settings, req.Headers, req.Url 等做一些有趣的事情。
    return req;
}

// keep these overloads DRY by constructing a Url and deferring to the above method
// 通过构造一个 Url 并遵从上面的方法来保持这些重载 DRY
public static IFlurlRequest DoMyThing(this Url url) => new FlurlRequest(url).DoMyThing();
public static IFlurlRequest DoMyThing(this Uri uri) => new FlurlRequest(uri).DoMyThing();
public static IFlurlRequest DoMyThing(this string url) => new FlurlRequest(url).DoMyThing();

现在,你可以使用如下的方式调用自定义扩展的链式方法:

result = await "http://api.com"
    .DoMyThing() // string extension
    .GetAsync();

result = "http://api.com"
    .AppendPathSegment("endpoint")
    .DoMyThing() // Url extension
    .GetAsync();

result = "http://api.com"
    .AppendPathSegment("endpoint")
    .WithBasicAuth(u, p)
    .DoMyThing() // IFlurlRequest extension
    .GetAsync();

在某些情况下,你可能还需要第五个重载:一个IFlurlClient扩展。 如果您的扩展仅与SettingsHeaders交互,请记住,它们的默认值存在于客户端级别。因此,为完整起见,您的扩展也应该支持客户端级别的默认值。

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

温馨提示

关于 注册码 问题

      由于近期经常大量注册机器人注册发送大量广告,本站开启免费入群领取注册码注册网站账号,注册码在群公告上贴着...

关于 注册码 问题

      由于近期经常大量注册机器人注册发送大量广告,本站开启免费入群领取注册码注册网站账号,注册码在群公告上贴着...

Archiver|手机版|小黑屋|DLSite

GMT+8, 2024-11-23 05:35

Powered by Discuz! X3.5 and PHP8

快速回复 返回顶部 返回列表