http 客户端开发

随着云计算的发展,微服务架构已经成为了许多项目的首选,各个子服务必须协同工作才能对用户提供完整的功能,这就使得子服务间的通信成为开发工作的重要环节。作为网络通讯中最常用的协议,http 是微服务架构下通讯协议的首选,子服务间通过网络通讯请求完成彼此之间的交互,访问网络中的各种静态资源或 API 接口。但是编写 HTTP 客户端代码是一项耗时的工作,并且常常会因为网络等原因出现各种异常情况,开发时还需要考虑到各种情况。因此,在本章中笔者介绍 .NET 中如何使用 HttpClient 请求网络资源,探讨如何定制 HTTP 请求代码,并且借助相关工具的力量,减少重复性劳动,提升工作效率。

HttpClient 基础

本节讲解了 HttpClient 的一些基础知识和如何应对常见的 http 请求操作,代码示例请参考 Demo6.API、Demo6.Console 两个项目。

请求参数

http 请求携带参数常用有四种方式,query、header、表单、json 等,接下来笔者逐一介绍。

Query

Query 请求实际上是在地址中拼接参数,使用 & 符号将参数拼接起来,比如 https://localhost:5001/test?a=1&b=2 中有 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?a=1&b=2

在 ASP.NET Core 中也有一个类似的方法拼接 query 参数。

var dic = new Dictionary<string, string>()
            {
                { "a","1"},
                { "b","2"}
            };
var query = QueryHelpers.AddQueryString("https://localhost:5001/query", dic);

在 url 中携带参数的值包含特殊字符时,需要对特殊字符进行转义。比如设置 a=http://localhost:5001/query?a=1&b=2,直接拼接 url 地址,会输出:

http://localhost:5001/query?a=http://localhost:5001/query?a=1&b=2&b=2

实际上在服务端接收请求后,解析出的结果为:

a=http://localhost:5001/query?a=1
b=2

因此,当 query 参数中包含特殊字符时,需要先将特殊字符转义,才能拼接到 url 中。

在 C# 中可以使用 Uri.EscapeDataString 转义 query 参数中的特殊字符,示例如下:

a = Uri.EscapeDataString("http://localhost:5001/query?a=1&b=2");
b = "2";
var response = await httpClient.GetAsync($"https://localhost:5001/query?a={a}&b={b}");

最后生成的 url 为:

http://localhost:5001/query?a=http%3A%2F%2Flocalhost%3A5001%2Fquery%3Fa%3D1%26b%3D2&b=2

Header

header 是 http 通讯协议中的一部分,heder 包含了许多重要的信息。

HttpClient 以键值对的形式存储 Header,参考示例如下:

public static async Task Header()
{
    using var httpClient = new HttpClient(httpclientHandler);
    // Header 头
    httpClient.DefaultRequestHeaders.Add("MyEmail", "123@qq.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 KeyValuePair<string,string>("Id","1"),
                new KeyValuePair<string,string>("Name","痴者工良"),
                new KeyValuePair<string, 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] Dictionary<string, 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 实现了 IEnumerable<HttpContent> 接口,因此 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; }
}

JSON

http 请求携带 body json 数据时,需要在 header 中通过 Content-Type 指出 body 的类型。

常见 body 类型有以下五种:

类型 content-type
Text text/plain
JavaScript application/javascript
HTML text/html
JSON application/json
XML application/xml

在 HttpClient 中,请求体可以使用 StringContent 来表示,然后在标头中添加 Content-Type。

// Json 等
public static async Task Json<T>(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 Task<string> 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 Task<string> 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 Task<HttpClient> 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 KeyValuePair<string,string>("user",user),
             new KeyValuePair<string, 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 Task<string> 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.AddScoped<Test1>();
    services.AddScoped<Test2>();
    services.AddScoped<Test3>();

    // 1
    services.AddHttpClient();

    // 2
    services.AddHttpClient("Default");

    // 3
    services.AddHttpClient<Program>();
}
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.AddTransient<MyDelegatingHandler>();
services.AddHttpClient("Default")
    .ConfigureHttpClient(x =>
    {
        x.MaxResponseContentBufferSize = 1024;
        x.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "xxxxx");
    })
    .AddHttpMessageHandler<MyDelegatingHandler>();

为了能够清晰的记录请求的日志便于排查问题,或者需要拦截 http 请求和响应,可以实现一个 DelegatingHandler 类型,通过容器注册为 Transient 服务,最后使用 .AddHttpMessageHandler<MyDelegatingHandler>(); 注入 DelegatingHandler 服务。

DelegatingHandler 示例如下:

public class MyDelegatingHandler : DelegatingHandler
{
    private readonly ILogger<MyDelegatingHandler> _logger;

    public MyDelegatingHandler(ILogger<MyDelegatingHandler> logger)
    {
        _logger = logger;
    }

    protected override async Task<HttpResponseMessage> 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 IAsyncPolicy<HttpResponseMessage> 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 Task<string> GetAsync()
{
    var delay = Backoff.DecorrelatedJitterBackoffV2(medianFirstRetryDelay: TimeSpan.FromSeconds(1), retryCount: 5);

    var retryPolicy = Policy
        .Handle<HttpRequestException>(ex =>
        {
            // 请求时出现这几种情况,允许重试
            if (ex.StatusCode == HttpStatusCode.BadGateway ||
            ex.StatusCode == HttpStatusCode.GatewayTimeout ||
            ex.StatusCode == HttpStatusCode.ServiceUnavailable)
                return true;

            return false;
        })
        // 其它方面的异常捕获
        .WaitAndRetryAsync(delay);

    var result = await retryPolicy.ExecuteAsync<string>(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,请参考官方文档 https://reactiveui.github.io/refit/

本节示例代码请参考 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")]
    Task<string> GetAsync([Query] string name);
}

通过依赖注入注册 IDemo6Client 服务。

services.AddRefitClient<IDemo6Client>()
    .ConfigureHttpClient(c => c.BaseAddress = new Uri(url))
    .SetHandlerLifetime(TimeSpan.FromSeconds(3));

或者使用静态方法构造生成:

var client = RestService.For<IDemo6Client>(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.AddRefitClient<IDemo6Client>(r1)
    .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://baidu.com"));

很多时候请求地址是动态的,在运行时才能确定地址。比如在物联网中有很多设备,每个设备上都有 web 服务,我们编写控制台应用向设备发送请求。

可以在编写接口时添加 HttpClient Client 属性,Refit 会自动注入 HttpClient 服务。

public interface IDemo6ClientDynamic
{
    HttpClient Client { get; }

    [Get("/index/name")]
    Task<string> GetAsync([Query] string name);
}

在获得 IDemo6ClientDynamic 服务实例之后,再配置新的地址。

services.AddRefitClient<IDemo6ClientDynamic>()
    .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://baidu.com"))
    .SetHandlerLifetime(TimeSpan.FromSeconds(3));

ioc = services.BuildServiceProvider();
var clientDynamic = ioc.GetRequiredService<IDemo6ClientDynamic>();

clientDynamic.Client.BaseAddress = new Uri("https://baidu.com");
await clientDynamic.GetAsync("test");

当然,Refit 也可以跟 Policy 类的框架一起使用。

services.AddRefitClient<IDemo6Client>()
    .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://baidu.com"))
    .SetHandlerLifetime(TimeSpan.FromSeconds(3))
    .AddPolicyHandler(BuildRetryPolicy());

// 构建重试策略
static IAsyncPolicy<HttpResponseMessage> 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.Curl:

dotnet tool install --global Maomi.Curl --version 2.0.0

Maomi.Curl 别名为 mmurl,在终端中输入 mmurl 查看参数列表和使用示例。

image-20240207114707541

jsonplaceholder.typicode.com 是一个用于测试测试 API 请求的网站,我们可以通过相关接口测试 mmurl 的功能,使用 mmurl 发起 get 请求:

mmurl https://jsonplaceholder.typicode.com/todos/1
request: 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/posts
request: https://jsonplaceholder.typicode.com/posts
{
  "userId": 2,
  "id": 101
}

在了解 Maomi.Curl 之后,下面正式开始编写工具包。编写工具包需要掌握两方面的知识点,一个是工具包项目配置,一个是命令行工具包的使用方式。

工具包项目其实就是控制台项目,只是在 .csproj 中需要添加一些属性配置:

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net8.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <RootNamespace>Maomi.Curl</RootNamespace>
    </PropertyGroup>

    <PropertyGroup>
        <!--设置为工具包项目-->
        <PackAsTool>true</PackAsTool>
        <!--命令行工具名称-->
        <ToolCommandName>mmurl</ToolCommandName>
        <Version>2.0.0</Version>
        <Description>一个类似 curl 的工具</Description>
        <PackageId>Maomi.Curl</PackageId>
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
    </ItemGroup>

</Project>

首先编写 get、post 两个请求方法,这两个方法其实就是使用 HttpClient 进行请求,代码比较简单。

private static async Task GetAsync(string url, IReadOnlyDictionary<string, 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, IReadOnlyDictionary<string, 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(IReadOnlyDictionary<string, 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 Task<int> Main(string[] args)
{
    // 定义命令参数
    // http header
    var headers = new Option<Dictionary<string, string>?>(
        name: "-H",
        description: "header,ex: -H \"Accept-Language=zh-CN\".",
        parseArgument: result =>
        {
            var dic = new Dictionary<string, 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 Option<string?>(
        name: "-b",
        description: "cookie.")
    {
        Arity = ArgumentArity.ZeroOrOne
    };

    var body = new Option<string?>(
        name: "-d",
        description: "post body.")
    {
        Arity = ArgumentArity.ZeroOrOne
    };

    var httpMethod = new Option<string?>(
        name: "-X",
        description: "GET/POST ...",
        getDefaultValue: () => "GET")
    {
        Arity = ArgumentArity.ZeroOrOne
    };

    // 其它无名的参数
    var otherArgument = new Argument<string>();

    // 构建命令行参数
    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 Dictionary<string, 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);
}

当工具包项目编写完成后,可以制作为 nuget 包,上传到 nuget.org 中,其他人也可以使用到你开发的工具。

Copyright © 痴者工良 2024 all right reserved,powered by Gitbook文档最后更新时间: 2024-03-31 13:27:22

results matching ""

    No results matching ""