
本节讲解了 HttpClient 的一些基础知识和如何应对常见的 http 请求操作代码示例请参考 Demo6.API、Demo6.Console 两个项目。请求参数http 请求携带参数常用有四种方式query、header、表单、json 等接下来笔者逐一介绍。QueryQuery 请求实际上是在地址中拼接参数使用符号将参数拼接起来比如https://localhost:5001/test?a1b2中有 a 和 b 两个参数由于 query 是直接存在 url 中所以可以手动通过字符串插值或拼接的方法生成一个带 query 参数的 url。// URL Query 参数 public static async Task Query(string a, string b) { using var httpClient new HttpClient(httpclientHandler); var response await httpClient.GetAsync($https://localhost:5001/query?a{a}b{b}); }在 ASP.NET Core 中可以使用[FromQuery]特性从 URL 参数中解析值。[HttpGet(/query)] public string Query([FromQuery] string a, [FromQuery] string b) { return a b; }使用[FromQuery]修饰的参数会忽略大小写。但是构建 query 参数比较麻烦也很容易出错我们可以使用 System.Web.HttpUtility 类来处理 query 参数。var nv System.Web.HttpUtility.ParseQueryString(string.Empty); nv.Add(a, 1); nv.Add(b, 2); var query nv.ToString(); var url https://localhost:5001/query? nv; // https://localhost:5001/query?a1b2在 ASP.NET Core 中也有一个类似的方法拼接 query 参数。var dic new Dictionarystring, string() { { a,1}, { b,2} }; var query QueryHelpers.AddQueryString(https://localhost:5001/query, dic);在 url 中携带参数的值包含特殊字符时需要对特殊字符进行转义。比如设置ahttp://localhost:5001/query?a1b2直接拼接 url 地址会输出http://localhost:5001/query?ahttp://localhost:5001/query?a1b2b2实际上在服务端接收请求后解析出的结果为ahttp://localhost:5001/query?a1 b2因此当 query 参数中包含特殊字符时需要先将特殊字符转义才能拼接到 url 中。在 C# 中可以使用Uri.EscapeDataString转义 query 参数中的特殊字符示例如下a Uri.EscapeDataString(http://localhost:5001/query?a1b2); b 2; var response await httpClient.GetAsync($https://localhost:5001/query?a{a}b{b});最后生成的 url 为http://localhost:5001/query?ahttp%3A%2F%2Flocalhost%3A5001%2Fquery%3Fa%3D1%26b%3D2b2Headerheader 是 http 通讯协议中的一部分heder 包含了许多重要的信息。HttpClient 以键值对的形式存储 Header参考示例如下public static async Task Header() { using var httpClient new HttpClient(httpclientHandler); // Header 头 httpClient.DefaultRequestHeaders.Add(MyEmail, 123qq.com); var response await httpClient.GetAsync($https://localhost:5001/header); var result await response.Content.ReadAsStringAsync(); }在 ASP.NET Core 中编写 API可以使用[FromHeader]在函数参数中获得 header 的参数而不需要通过 HttpContext 获取有助于简化代码。[HttpGet(/header)] public string Header([FromHeader] string? myEmail) { return myEmail; }使用[FromHeader]修饰的参数会忽略大小写使用myEmail还是MyEmail都一样。HttpRequestHeaders 已经定义了一些常用的 Header 头封装处理逻辑到相关的属性中常用 Header 如下Accept AcceptCharset AcceptEncoding AcceptLanguage Authorization在 HttpClient 中可以直接设置不需要通过键值对的形式插入使用示例httpClient.DefaultRequestHeaders.Authorization new AuthenticationHeaderValue(Bearer, token); httpClient.DefaultRequestHeaders.AcceptLanguage ...;表单表单有 x-www-form-urlencoded、form-data 两种请求类型。form-data兼容x-www-form-urlencoded并且可以用于上传文件而x-www-form-urlencoded只能传递字符串键值对。由于x-www-form-urlencoded性能比form-data高一些所以不需要上传文件时使用x-www-form-urlencoded比较好。使用x-www-form-urlencoded携带请求参数的代码示例如下// 表单提交 // application/x-www-form-urlencoded public static async Task From() { var fromContent new FormUrlEncodedContent(new[] { new KeyValuePairstring,string(Id,1), new KeyValuePairstring,string(Name,痴者工良), new KeyValuePairstring, string(Number,666666) }); using var httpClient new HttpClient(httpclientHandler); var response await httpClient.PostAsync(http://localhost:5001/form1, fromContent); }ASP.NET Core 中可以使用字典类型来接收相应的参数。[HttpPost(/form1)] public string Form1([FromForm] Dictionarystring, string dic) { return success; }也可以使用模型类来接收表单参数。[HttpPost(/form2)] public string Form2([FromForm] Form2Model model) { return success; } public class Form2Model { public string Id { get; set; } public string Name { get; set; } public string Number { get; set; } }HttpClient 还可以使用form-data上传文件代码示例如下// 上传文件 public static async Task SendFile(string filePath, string fromName, string url) { using var client new HttpClient(); FileStream imagestream System.IO.File.OpenRead(filePath); // multipartFormDataContent.Add( ... ...); var multipartFormDataContent new MultipartFormDataContent() { { new StreamContent(File.OpenRead(filePath)), // 对应 服务器 WebAPI 的传入参数 fromName, // 上传的文件名称 Path.GetFileName(filePath) }, // 可上传多个文件 }; HttpResponseMessage response await client.PostAsync(url, multipartFormDataContent); }HttpClient 使用 HttpContent 来抽象 body 参数类型相关的派生类型有很多比如 MultipartFormDataContent 、StreamContent、StringContent 、FormUrlEncodedContent 等。MultipartFormDataContent 实现了IEnumerableHttpContent接口因此 MultipartFormDataContent 中还可以嵌套其它 HttpContent 类型比如在上传文件时可以携带其他表单参数。MultipartContent multipartContent new MultipartContent(); multipartContent.Add(multipartFormDataContent); multipartContent.Add(fromContent);var multipartFormDataContent new MultipartFormDataContent() { // 文件 { new StreamContent(File.OpenRead(filePath)), fromName, Path.GetFileName(filePath) }, // 表单 fromContent, };ASP.NET Core 中如果只接收文件则使用 IFormFile 类型作为函数参数即可参数的名称要跟表单中文件字段的名称一致。[HttpPost(/form3)] public string Form3([FromForm] IFormFile img) { return success; }var multipartFormDataContent new MultipartFormDataContent() { { new StreamContent(File.OpenRead(filePath)), // 该名称要与 API 的参数名称一致 img, Path.GetFileName(filePath) }, };如果有批量上传文件则可以使用 IFormFileCollection 接收。[HttpPost(/form4)] public string Form4([FromForm] IFormFileCollection imgs) { return success; }如果需要同时接收文件和表单可以使用模型类[HttpPost(/form5)] public string Form5([FromForm] Form5Model model) { return success; } public class Form5Model { public string Id { get; set; } public string Name { get; set; } public IFormFile Img { get; set; } }JSONhttp 请求携带 body json 数据时需要在 header 中通过 Content-Type 指出 body 的类型。常见 body 类型有以下五种类型content-typeTexttext/plainJavaScriptapplication/javascriptHTMLtext/htmlJSONapplication/jsonXMLapplication/xml在 HttpClient 中请求体可以使用 StringContent 来表示然后在标头中添加 Content-Type。// Json 等 public static async Task JsonT(T obj) where T : class { var json System.Text.Json.JsonSerializer.Serialize(obj); var jsonContent new StringContent(json); // 请求时要指定 Content-Type 属性 jsonContent.Headers.ContentType new System.Net.Http.Headers.MediaTypeHeaderValue(application/json); using var httpClient new HttpClient(); var response httpClient.PostAsync(https://localhost:5001/json, jsonContent).Result; var result await response.Content.ReadAsStringAsync(); } public class JsonModel { public string Id { get; set; } public string Name { get; set; } } await HttpClientHelper.Json(new JsonModel { Id 1, Name 工良 });ASP.NET Core 中通过模型类来接收 json[HttpPost(/json)] public string Json([FromBody] JsonModel model) { return success; } public class JsonModel { public string Id { get; set; } public string Name { get; set; } }如果不确定推送的数据有什么字段或 json 的结构是动态的可以 object 类型接收参数[HttpPost(/json1)] public string Json1([FromBody] object model) { return success; }但是这个 object 是何种类型这个跟 ASP.NET Core 配置的序列化框架有关系。[HttpPost(/json2)] public string Json2([FromBody] object model) { if (model is System.Text.Json.Nodes.JsonObject jsonObject) { } else if (model is Newtonsoft.Json.Linq.JObject jObject) { } ... ... return success; }请求凭证客户端请求服务器时需要通过授权认证许可方能获取服务器资源目前比较常见的认证方式有 basic、jwt、cookie 等。basic 认证示例basic 认证比较简单主要在可信任网络中使用多用于路由器和嵌入式设备账号密码仅仅使用 base64 进行编码安全性较差。// basic 认证 public static async Taskstring Basic(string url, string user, string password) { using HttpClient client new HttpClient(httpclientHandler); AuthenticationHeaderValue authentication new AuthenticationHeaderValue( Basic, Convert.ToBase64String(Encoding.UTF8.GetBytes(${user}:{password}) )); client.DefaultRequestHeaders.Authorization authentication; var response await client.GetAsync(url); return await response.Content.ReadAsStringAsync(); }jwt 认证示例jwt 是目前最常用的认证方式之一在微服务时代的应用越来越广泛通过是在 header 中添加 Authentication 参数传递 jwt。// jwt认证 public static async Taskstring Jwt(string token, string url) { using var client new HttpClient(httpclientHandler); // 创建身份认证 // System.Net.Http.Headers.AuthenticationHeaderValue; client.DefaultRequestHeaders.Authorization new AuthenticationHeaderValue(Bearer, token); var response await client.GetAsync(url); return await response.Content.ReadAsStringAsync(); }cookie 示例HttpClient 中cookie有两种处理方式。一种是已经有 cookie 值直接将 cookie 存储到 HttpClient 中另一种是还没有 cookie通过账号密码登录获取到 cookie自动存储到 HttpClient 对象中接着使用当前 HttpClient 对象请求其它 URL。很多时候我们需要复用同一个 HttpClient可以设置 HttpClientHandler 的 UseCookies 属性UseCookies 获取或设置一个值该值指示处理程序是否使用 CookieContainer 属性存储服务器 Cookie并在发送请求时使用这些 cookie这样在同一个 HttpClient 中携带的 Cookie 是一致的。httpclientHandler 配置var httpclientHandler new HttpClientHandler() { UseCookies true };先用账号密码登陆再请求成功后服务器会向 http 客户端写入 cookie因此复用同一个 HttpClient 即可携带 cookie。// 获取登录后的 HttpClient public static async TaskHttpClient Cookie(string user, string password, string loginUrl) { var httpclientHandler new HttpClientHandler() { ServerCertificateCustomValidationCallback (message, cert, chain, error) true, UseCookies true }; var loginContent new FormUrlEncodedContent(new[] { new KeyValuePairstring,string(user,user), new KeyValuePairstring, string(password,password) }); var httpClient new HttpClient(httpclientHandler); var response await httpClient.PostAsync(loginUrl, loginContent); if (response.IsSuccessStatusCode) return httpClient; throw new Exception($请求失败http 状态码{response.StatusCode}); }当已经获取 cookie 时可以直接设置其值// 自行设置 cookie public static async Taskstring Cookie(string cookie, string url) { var httpclientHandler new HttpClientHandler() { ServerCertificateCustomValidationCallback (message, cert, chain, error) true, }; using var client new HttpClient(httpclientHandler); client.DefaultRequestHeaders.Add(Cookie, cookie); var response await client.GetAsync(url); return await response.Content.ReadAsStringAsync(); }异常处理HttpClient 异常主要有三种请求异常 HttpRequestException 、操作取消异常 OperationCanceledException 、超时异常 TimeoutException此外还有一些特殊情况导致的异常。HttpClient 收到响应之后如果状态码小于 0 或大于 999则视为无效状态码弹出 ArgumentOutOfRangeException 异常public HttpResponseMessage(HttpStatusCode statusCode) { if (((int)statusCode 0) || ((int)statusCode 999)) { throw new ArgumentOutOfRangeException(statusCode); } }HttpClient 将 200-299 的状态码视为正常状态码当响应出现其它状态码时会弹出 HttpRequestException 异常public bool IsSuccessStatusCode { get { return ((int)statusCode 200) ((int)statusCode 299); } } public HttpResponseMessage EnsureSuccessStatusCode() { if (!IsSuccessStatusCode) { ... ... throw new HttpRequestException(... ...); } ... ... }OperationCanceledException 会在 CancellationToken 超时或取消后抛出而 TimeoutException 主要在两种情况下会出现一种是网络请求超时一种是 CancellationToken 超时。由于两种异常都会因为 CancellationToken 超时而抛出捕获异常时需要注意区分两者catch (OperationCanceledException ex) when (ex.InnerException is TimeoutException tex) { Console.WriteLine($Timed out: {ex.Message}, {tex.Message}); }IHttpClientFactory一般来说直接使用 HttpClient 需要自行管理生命周期手动释放连接资源而且在短时间内大量使用 HttpClient 会导致系统性能严重下降。所以 C# 推出了 IHttpClientFactory 可以通过 IHttpClientFactory 创建自动管理的 HttpClient 对象统一控制 HttpClient 对象的行为如统一配置 HttpClientMessageHandler、添加超时重试机制等。IHttpClientFactory 基础本节示例代码可参考 Demo6.HttpFactory 项目。只需要添加 Microsoft.Extensions.Http 包即可使用 IHttpClientFactory。IHttpClientFactory 主要有三种注入方式static void Main() { var services new ServiceCollection(); services.AddScopedTest1(); services.AddScopedTest2(); services.AddScopedTest3(); // 1 services.AddHttpClient(); // 2 services.AddHttpClient(Default); // 3 services.AddHttpClientProgram(); } public class Test1 { public Test1(IHttpClientFactory httpClientFactory) { var httpClient httpClientFactory.CreateClient(); // 也可以使用 // httpClientFactory.CreateClient(Default); } } public class Test2 { public Test2(IHttpClientFactory httpClientFactory) { var httpClient httpClientFactory.CreateClient(Default); } } public class Test3 { public Test3(HttpClient httpClient) { } }通过第二、三种方式注入 HttpClient 可以配置 HttpClient 的行为比如携带默认参数、绑定 HttpMessageHandler 等。// 2 services.AddTransientMyDelegatingHandler(); services.AddHttpClient(Default) .ConfigureHttpClient(x { x.MaxResponseContentBufferSize 1024; x.DefaultRequestHeaders.Authorization new AuthenticationHeaderValue(Bearer, xxxxx); }) .AddHttpMessageHandlerMyDelegatingHandler();为了能够清晰的记录请求的日志便于排查问题或者需要拦截 http 请求和响应可以实现一个 DelegatingHandler 类型通过容器注册为 Transient 服务最后使用.AddHttpMessageHandlerMyDelegatingHandler();注入 DelegatingHandler 服务。DelegatingHandler 示例如下public class MyDelegatingHandler : DelegatingHandler { private readonly ILoggerMyDelegatingHandler _logger; public MyDelegatingHandler(ILoggerMyDelegatingHandler logger) { _logger logger; } protected override async TaskHttpResponseMessage SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { HttpResponseMessage httpResponseMessage null; try { httpResponseMessage await base.SendAsync(request, cancellationToken); #if DEBUG _logger.LogDebug(MyException.CreateMessage(request, httpResponseMessage)); #endif if (httpResponseMessage.IsSuccessStatusCode) { return httpResponseMessage; } throw new MyException(request, httpResponseMessage); } catch (Exception) { _logger.LogError(MyException.CreateMessage(request, httpResponseMessage)); throw; } } }请求策略Microsoft.Extensions.Http.Polly 是一个 HttpClient 扩展库可以以流畅且线程安全的方式处理 http 请求重试、断路器、超时、Bulkhead 隔离和回退。示例代码参考 Demo6.Polly 项目。public static void Test() { var services new ServiceCollection(); services.AddHttpClient(Default, client { client.BaseAddress new Uri(http://localhost:5000); }) .AddPolicyHandler(GetRetryPolicy()) .SetHandlerLifetime(TimeSpan.FromMinutes(5)); } static IAsyncPolicyHttpResponseMessage GetRetryPolicy() { return HttpPolicyExtensions .HandleTransientHttpError() .OrResult(msg msg.StatusCode System.Net.HttpStatusCode.NotFound) .WaitAndRetryAsync(6, retryAttempt TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); }此外 Polly.Contrib.WaitAndRetry 包中具有很多扩展可以更加细粒度地定制重试等策略。示例代码参考 Demo6.PollyContrib 项目。public async Taskstring GetAsync() { var delay Backoff.DecorrelatedJitterBackoffV2(medianFirstRetryDelay: TimeSpan.FromSeconds(1), retryCount: 5); var retryPolicy Policy .HandleHttpRequestException(ex { // 请求时出现这几种情况允许重试 if (ex.StatusCode HttpStatusCode.BadGateway || ex.StatusCode HttpStatusCode.GatewayTimeout || ex.StatusCode HttpStatusCode.ServiceUnavailable) return true; return false; }) // 其它方面的异常捕获 .WaitAndRetryAsync(delay); var result await retryPolicy.ExecuteAsyncstring(async () { var responseMessage await _httpClient.GetAsync(https://www.baidu.com); return await responseMessage.Content.ReadAsStringAsync(); }); return result; }Refit 框架的使用方法Refit 是一个动态代码生成库开发者只需要编写接口 Refit 自动生成对应的 HttpClient 代码。直接使用 HttpClient 发起 http 请求我们需要编写大量的调用代码以及封装参数传递和异常处理如果我们使用 Refit 框架编写客户端可以减少大量的重复工作。本书不会对 Refit 做详细的介绍如需了解 Refit请参考官方文档 Refit | The automatic type-safe REST library for Xamarin and .NET本节示例代码请参考 Demo6.Refit 项目。通过 nuget 搜索引入 Refit.HttpClientFactory、Refit.Newtonsoft.Json有这样一个 API[ApiController] [Route([controller])] public class IndexController : ControllerBase { [HttpGet(name)] public string GetName([FromQuery] string name) { return name; } }使用 Refit 时客户端只需要编写接口然后填写对应的参数类型和返回值。public interface IDemo6Client { [Get(/index/name)] Taskstring GetAsync([Query] string name); }通过依赖注入注册 IDemo6Client 服务。services.AddRefitClientIDemo6Client() .ConfigureHttpClient(c c.BaseAddress new Uri(url)) .SetHandlerLifetime(TimeSpan.FromSeconds(3));或者使用静态方法构造生成var client RestService.ForIDemo6Client(url, new RefitSettings());可以定制 Http 请求时如何序列化以及反序列化。JsonSerializerSettings j1 new JsonSerializerSettings() { DateFormatString yyyy-MM-dd HH:mm:ss }; RefitSettings r1 new RefitSettings(new NewtonsoftJsonContentSerializer(j1)); //JsonSerializerOptions j2 new JsonSerializerOptions(); //RefitSettings r2 new RefitSettings(new SystemTextJsonContentSerializer(j2)); services.AddRefitClientIDemo6Client(r1) .ConfigureHttpClient(c c.BaseAddress new Uri(https://baidu.com));很多时候请求地址是动态的在运行时才能确定地址。比如在物联网中有很多设备每个设备上都有 web 服务我们编写控制台应用向设备发送请求。可以在编写接口时添加HttpClient Client属性Refit 会自动注入 HttpClient 服务。public interface IDemo6ClientDynamic { HttpClient Client { get; } [Get(/index/name)] Taskstring GetAsync([Query] string name); }在获得 IDemo6ClientDynamic 服务实例之后再配置新的地址。services.AddRefitClientIDemo6ClientDynamic() .ConfigureHttpClient(c c.BaseAddress new Uri(https://baidu.com)) .SetHandlerLifetime(TimeSpan.FromSeconds(3)); ioc services.BuildServiceProvider(); var clientDynamic ioc.GetRequiredServiceIDemo6ClientDynamic(); clientDynamic.Client.BaseAddress new Uri(https://baidu.com); await clientDynamic.GetAsync(test);当然Refit 也可以跟 Policy 类的框架一起使用。services.AddRefitClientIDemo6Client() .ConfigureHttpClient(c c.BaseAddress new Uri(https://baidu.com)) .SetHandlerLifetime(TimeSpan.FromSeconds(3)) .AddPolicyHandler(BuildRetryPolicy()); // 构建重试策略 static IAsyncPolicyHttpResponseMessage BuildRetryPolicy() { return HttpPolicyExtensions .HandleTransientHttpError() .OrResult(msg msg.StatusCode System.Net.HttpStatusCode.NotFound) .WaitAndRetryAsync(6, retryAttempt TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); }编写 HttpClient 时尽量使用 IHttpClientFactory为了快速开发我们可以使用类似 Refit 之类的框架动态生成代码要注意处理请求中出现的错误注意请求的超时时间、重试策略、不同请求失败原因处理。cli 工具Refit 代码生成工具但是手动配置每个 API这个工作实在过于重复低效因此我们需要一个工具能够直接将 swagger 文档转换为 Refit 代码。安装 Refitter 工具。dotnet tool install --global Refitter启动 Demo6.Api 服务。然后执行命令从 swagger 文件生成 C# 代码。refitter http://localhost:5001/swagger/v1/swagger.json --namespace MyApi --output ./IDemo6Api.cs命令执行完毕之后可以在目录中找到 IDemo6Api.cs 文件。使用 Refit 框架我们只需要定义接口即可不需要编写大量繁杂的 HttpClient 代码大大简化了配置 HttpClient 、请求策略控制等。而使用 Refitter 框架更是把编写接口的过程省略了。制作 .NET 工具包.NET 工具包是一种特殊的 NuGet 包比如 dotnet-dump、Refitter 等都属于工具包。在企业内部开发时为了提高工作效率往往需要制作一些脚本之类的工具而 .NET 工具包非常时候用于制作这些脚本然后下方给其他人使用。本节示例代码请参考 Maomi.Curl 项目Maomi.Curl 是一个类似 curl 的工具可以发起 http 请求在正式开发之前先安装 Maomi.Curl 了解其使用方式。通过命令安装 Maomi.Curldotnet tool install --global Maomi.Curl --version 2.0.0Maomi.Curl 别名为 mmurl在终端中输入 mmurl 查看参数列表和使用示例。jsonplaceholder.typicode.com 是一个用于测试测试 API 请求的网站我们可以通过相关接口测试 mmurl 的功能使用 mmurl 发起 get 请求mmurl https://jsonplaceholder.typicode.com/todos/1request: https://jsonplaceholder.typicode.com/todos/1 { userId: 1, id: 1, title: delectus aut autem, completed: false }使用 mmurl 发起 post 请求mmurl -X POST -d {\userId\:2} https://jsonplaceholder.typicode.com/postsrequest: https://jsonplaceholder.typicode.com/posts { userId: 2, id: 101 }在了解 Maomi.Curl 之后下面正式开始编写工具包。编写工具包需要掌握两方面的知识点一个是工具包项目配置一个是命令行工具包的使用方式。工具包项目其实就是控制台项目只是在 .csproj 中需要添加一些属性配置Project SdkMicrosoft.NET.Sdk PropertyGroup OutputTypeExe/OutputType TargetFrameworknet8.0/TargetFramework ImplicitUsingsenable/ImplicitUsings Nullableenable/Nullable RootNamespaceMaomi.Curl/RootNamespace /PropertyGroup PropertyGroup !--设置为工具包项目-- PackAsTooltrue/PackAsTool !--命令行工具名称-- ToolCommandNamemmurl/ToolCommandName Version2.0.0/Version Description一个类似 curl 的工具/Description PackageIdMaomi.Curl/PackageId /PropertyGroup ItemGroup PackageReference IncludeSystem.CommandLine Version2.0.0-beta4.22272.1 / /ItemGroup /Project首先编写 get、post 两个请求方法这两个方法其实就是使用 HttpClient 进行请求代码比较简单。private static async Task GetAsync(string url, IReadOnlyDictionarystring, string headers, string? cookie null) { var client new HttpClient(); BuildHeader(headers, cookie, client); var response await client.GetAsync(new Uri(url)); Console.WriteLine(await response.Content.ReadAsStringAsync()); } private static async Task PostAsync(string url, IReadOnlyDictionarystring, string headers, string body, string? cookie null) { var client new HttpClient(); BuildHeader(headers, cookie, client); var jsonContent new StringContent(body); jsonContent.Headers.ContentType new System.Net.Http.Headers.MediaTypeHeaderValue(application/json); var response await client.PostAsync(new Uri(url), jsonContent); Console.WriteLine(await response.Content.ReadAsStringAsync()); } private static void BuildHeader(IReadOnlyDictionarystring, string headers, string? cookie, HttpClient client) { if (headers ! null headers.Count 0) { foreach (var item in headers) client.DefaultRequestHeaders.Add(item.Key, item.Value); } if (!string.IsNullOrEmpty(cookie)) { client.DefaultRequestHeaders.Add(Cookie, cookie); } }那么怎么从命令行参数中解析出对应的参数呢System.CommandLine 是一个命令行工具包能够帮助开发者简化解析命令行参数的步骤。static async Taskint Main(string[] args) { // 定义命令参数 // http header var headers new OptionDictionarystring, string?( name: -H, description: header,ex: -H \Accept-Languagezh-CN\., parseArgument: result { var dic new Dictionarystring, string(); if (result.Tokens.Count 0) return dic; foreach (var item in result.Tokens) { var header item.Value.Split(); dic.Add(header[0], header[1]); } return dic; }) { // 可以出现 0 或多次 Arity ArgumentArity.ZeroOrMore, }; var cookie new Optionstring?( name: -b, description: cookie.) { Arity ArgumentArity.ZeroOrOne }; var body new Optionstring?( name: -d, description: post body.) { Arity ArgumentArity.ZeroOrOne }; var httpMethod new Optionstring?( name: -X, description: GET/POST ..., getDefaultValue: () GET) { Arity ArgumentArity.ZeroOrOne }; // 其它无名的参数 var otherArgument new Argumentstring(); // 构建命令行参数 var rootCommand new RootCommand(输入参数请求 url 地址); rootCommand.AddOption(headers); rootCommand.AddOption(cookie); rootCommand.AddOption(body); rootCommand.AddOption(httpMethod); rootCommand.Add(otherArgument); // 解析参数调用 rootCommand.SetHandler(async (headers, cookie, body, httpMethod, otherArgument) { Console.WriteLine($request: {otherArgument}); if (headers null) headers new Dictionarystring, string(); try { if (!string.IsNullOrEmpty(body) || POST.Equals(httpMethod, StringComparison.InvariantCultureIgnoreCase)) { ArgumentNullException.ThrowIfNull(body); await PostAsync(otherArgument, headers, body, cookie); } else { await GetAsync(otherArgument, headers, cookie); } } catch (Exception ex) { Console.ForegroundColor ConsoleColor.Red; Console.WriteLine(ex.Message); Console.ResetColor(); } }, headers, cookie, body, httpMethod, otherArgument); return await rootCommand.InvokeAsync(args); }