ASP.NET Core – HttpClient
前言
以前写过的文章 Asp.net core 学习笔记 ( HttpClient ).
其实 HttpClient 内容是挺多的, 但是我自己用到的很少. 所以这篇记入一下自己用到的就好了.
参考
3 ways to use HTTPClientFactory in ASP.NET Core 2.1
Docs – Make HTTP requests using IHttpClientFactory in ASP.NET Core
3 大用法介绍
其实是 4 种哦, 只是第 4 种我不熟就忽略掉呗.
Basic usage 的方式是注入 HttpClientFactory > 创建 HttpClient > 发 request
Named clients 的方式是先在 program.cs provide 好 config, 比如 Uri, default header 等等. 然后注入 HttpClientFactory > 创建 HttpClient with named config > 发 request.
如果你要发多个不同的 path 但是相同的 origin 的话, 这个管理方式会比 Basic usage 好.
Typed clients 的方式是先做一个 service class, 在 constructor 注入 HttpClient 然后配置 config, 提供各做方法里面使用 HttpClient 发 request.
这个方式主要就是封装了所以对这个 client 的请求, 外部只要 call service 的 method 就好.
对比 3 大用法
可以看出来, 主要就是在管理上的区别. 个人建议, 如果只是 1 个请求, 那么直接用 basic 就好了, 如果多个请求, 或者多个地方用到, 那么就用 Type clients. 至于 Named clients 感觉有点不上不下就不要用了.
Get Request (Basic usage)
要到这里 Github – CountryCodes.json 请求 Country Code JSON
Provide HttpClient
builder.Services.AddHttpClient();
注入 HttpClientFactory
private readonly IHttpClientFactory _httpClientFactory; public IndexModel( IHttpClientFactory httpClientFactory ) { _httpClientFactory = httpClientFactory; }
创建 Request Message
using var httpRequestMessage = new HttpRequestMessage { RequestUri = new Uri("https://gist.githubusercontent.com/Goles/3196253/raw/9ca4e7e62ea5ad935bb3580dc0a07d9df033b451/CountryCodes.json"), Method = HttpMethod.Get, Headers = { { "Accept", "application/json; charset=utf-8" } } }; httpRequestMessage.Headers.Add("Accept", "application/json; charset=utf-8"); // 或者后来添加也可以
调用 HttpClient 发请求
var httpClient = _httpClientFactory.CreateClient(); using var response = await httpClient.SendAsync(httpRequestMessage); if (response.IsSuccessStatusCode) { var json = await response.Content.ReadAsStringAsync(); }
以上就是 basic usage 的方式. 类似发 SMTP 请求那样.
注:httpClient 可以不需要 using;而 HttpRequestMessage 和 HttpResponseMessage 则需要 using 释放资源。
解析 JSON response
另外, 也可以直接解析 JSON response
public class Country { public string Name { get; set; } = ""; [JsonPropertyName("dial_code")] public string DialCode { get; set; } = ""; public string Code { get; set; } = ""; } var counties = await response.Content.ReadFromJsonAsync<List<Country>>();
Download Image
下载图片也是可以的
var buffer = await response.Content.ReadAsByteArrayAsync(); using var fileStream = System.IO.File.Create("abc.jpg", buffer.Length); await fileStream.WriteAsync(buffer); fileStream.Flush();
Get Request (Typed clients)
创建 Client
public class CountryCodeHttpClient { private readonly HttpClient _httpClient; public CountryCodeHttpClient( HttpClient httpClient ) { _httpClient = httpClient; _httpClient.BaseAddress = new Uri("https://gist.githubusercontent.com"); _httpClient.DefaultRequestHeaders.Add("Accept", "application/json; charset=utf-8"); } public async Task<List<Country>> GetCountriesAsync() { using var response = await _httpClient.GetAsync("/Goles/3196253/raw/9ca4e7e62ea5ad935bb3580dc0a07d9df033b451/CountryCodes.json"); if (response.IsSuccessStatusCode) { return (await response.Content.ReadFromJsonAsync<List<Country>>())!; } else { throw new Exception("error"); } } }
通常 Client 在 construtor 会配置通用的 URI 和 Header. 在方法内则控制 path 和 extra Header.
再给一个复杂例子
var queryBuilder = new QueryBuilder(); queryBuilder.Add("param1", "value1"); var queryString = queryBuilder.ToQueryString().Value!; using var requestMessage = new HttpRequestMessage { Method = HttpMethod.Get, RequestUri = new Uri($"/Goles/3196253/raw/9ca4e7e62ea5ad935bb3580dc0a07d9df033b451/CountryCodes.json{queryString}", UriKind.Relative), Headers = { { "Accept", "application/json; charset=utf-8" } } }; using var response = await _httpClient.SendAsync(requestMessage);
Provide HttpClient
builder.Services.AddHttpClient<CountryCodeHttpClient>();
调用
var counties = await _countryCodeHttpClient.GetCountriesAsync();
BaseAddress 和 HttpRequestMessage.RequestUri
HttpClient 可以 set BaseAddress。
HttpRequestMessage 可以 set RequestUri。
这 2 个的搭配玩法是这样的。
每当我们要发送 request 之前
HttpClient 会跑一个 CheckRequestBeforeSend(HttpRequestMessage)
里面有一个 PrepareRequestMessage
它就是处理 BaseAddress 和 RequestUri 的
1. 当 BaseAddress 和 RequestUri 都没有定义时,报错。
2. 当 RequestUri 没有定义时,用 BaseAddress 来发送。
3. 当 RequestUri 是绝对路径时,无视 BaseAddress 只用 RequestUri 来发送。
4. 当 RequestUri 是相对路径时,combine BaseAddress 和 RequestUri 来发送。
这个 combine 的代码是
new Uri(_baseAddress, request.RequestUri);
这个 combine 的逻辑是这样的。
假设 base 是 https://example.com/a/b/c/
它的 path 是 ["a", "b", "c", ""],最后一个是 empty。
假设 relative 开头是 /d 表示 override 之前所有 path,结果就是 .com/d,a,b,c,empty 都不要了。
假设 relative 开头是 d 表示 override 最后一个(最后一个是 empty 而不是 c 哦),结果是 .com/a/b/c/d。
假设 relative 开头是 ../d 表示 override 最后两个, 结果是 .com/a/b/d,c 和 empty 不要了。
假设 relative 开头是 ? 表示不需要 override path,把 query 加进去就可以了,结果是 .com/a/b/c/?name=derrick
我们来看几个常见的例子:
var baseUri = new Uri("https://example.com"); var relativeUri = new Uri("/products", UriKind.Relative); var combineUri = new Uri(baseUri, relativeUri); Console.WriteLine(combineUri.ToString());
第一,当 baseUri 只是一个 domain,我们加不加 trailing slash 都可以。下面 4 种组合的结果是一样的。
// https://example.com + products = https://example.com/products // https://example.com + /products = https://example.com/products // https://example.com/ + products = https://example.com/products // https://example.com/ + /products = https://example.com/products // https://example.com/ + ?name=derrick = https://example.com/name=derrick // https://example.com + ?name=derrick = https://example.com/name=derrick (没有 trailing slash 会自动加)
第二,当 baseUrl 是一个 folder,我们必须加上 trailing slash
// https://example.com/products + mk100 = https://example.com/mk100 (错误) // https://example.com/products + /mk100 = https://example.com/mk100 (错误) // https://example.com/products/ + mk100 = https://example.com/products/mk100 (正确) // https://example.com/products/ + /mk100 = https://example.com/mk100 (错误,除非你想要 override base uri 的 path,那就正确)
第三,当 baseUrl 是一个 file,我们不要加上 trailing slash
// https://example.com/products + ?name=derrick = https://example.com/products?name=derrick (正确) // https://example.com/products/ + ?name=derrick = https://example.com/products/?name=derrick (错误)
第四,奇葩,在做 Google – Cloud Translation API 看到一个比较奇葩的 url。
// 这个是最终要的 url // https://translation.googleapis.com/v3/projects/projectid:translateText // 如果 base 是 // https://translation.googleapis.com/v3/projects/projectid/ // relative 是 // :translateText // 结果 // https://translation.googleapis.com/v3/projects/projectid/:translateText // 这个不是我们要的 url,这题没有办法用 base + relative 的方式搞,因为 projectid:translateText 是不能被拆开的。
Post Request (Basic usage)
Post 和 Get 差不多, 我们 focus 不一样的地方就好了.
using var jsonContent = JsonContent.Create(new { refresh_token = "", client_id = "", client_secret = "", redirect_uri = "https://developers.google.com/oauthplayground", grant_type = "refresh_token" }); using var httpRequestMessage = new HttpRequestMessage { RequestUri = new Uri($"https://www.googleapis.com/oauth2/v4/token"), Method = HttpMethod.Post, Headers = { { "Accept", "application/json" }, // { "Content-Type", "application/json" } // 不要在这里声明 Content-Type, 下面的 Content 会负责 // { "Authorization", $"Bearer {accessToken}" } // 常用的 Bearer Token Header }, Content = jsonContent // 或者直接传 string 也是可以的 // Content = new StringContent(JsonSerializer.Serialize(new // { // refresh_token = "", // client_id = "", // client_secret = "", // redirect_uri = "https://developers.google.com/oauthplayground", // grant_type = "refresh_token" // }), Encoding.UTF8, mediaType: "application/json") }; var httpClient = _httpClientFactory.CreateClient(); using var response = await httpClient.SendAsync(httpRequestMessage); if (response.IsSuccessStatusCode) { var json = await response.Content.ReadAsStringAsync(); }
1. Method 换成了 POST
2. Header 不可以放 Content-Type. 参考: Stack Overflow – How do you set the Content-Type header for an HttpClient request?
3. Content 可以用 JsonContent 或者 StringContent 记得提供 mediaType, Content 会负责 set header Content-Type
注:JsonContent 要使用 using 释放资源哦
JsonContent.cs