在.NET 6 中如何创建和使用 HTTP 客户端 SDK

如今,基于云、微服务或物联网的应用程序通常依赖于通过网络与其他系统通信。每个服务都在自己的进程中运行,并解决一组有限的问题。服务之间的通信是基于一种轻量级的机制,通常是一个 HTTP 资源 API。

从.NET 开发人员的角度来看,我们希望以可分发包的形式提供一种一致的、可管理的方式来集成特定的服务。最好的方法是将我们开发的服务集成代码以 NuGet 包的形式提供,并与其他人、团队、甚至组织分享。在这篇文章中,我将分享在.NET 6 中创建和使用 HTTP 客户端 SDK 的方方面面。

客户端 SDK 在远程服务之上提供了一个有意义的抽象层。本质上,它允许进行远程过程调用(RPC)。客户端 SDK 的职责是序列化一些数据,将其发送到远端目的地,以及反序列化接收到的数据,并处理响应。

HTTP 客户端 SDK 与 API 一同使用:

  1. 加速 API 集成过程;

  2. 提供一致、标准的方法;

  3. 让服务所有者可以部分地控制消费 API 的方式。

编写一个 HTTP 客户端 SDK

在本文中,我们将编写一个完备的Dad Jokes API客户端,为的是提供老爸笑话;让我们来玩一玩。源代码在GitHub上。

在开发与 API 一起使用的客户端 SDK 时,最好从接口契约(API 和 SDK 之间)入手:

public interface IDadJokesApiClient{  Task<JokeSearchResponse> SearchAsync(      string term, CancellationToken cancellationToken);Task<Joke> GetJokeByIdAsync(      string id, CancellationToken cancellationToken);Task<Joke> GetRandomJokeAsync(CancellationToken cancellationToken);}
public class JokeSearchResponse{  public bool Success { get; init; }public List<Joke> Body { get; init; } = new();}
public class Joke{  public string Punchline { get; set; } = default!;public string Setup { get; set; } = default!;public string Type { get; set; } = default!;}

复制代码

契约是基于你要集成的 API 创建的。我一般建议遵循健壮性原则和最小惊奇原则开发通用的 API。但如果你想根据自己的需要修改和转换数据契约,也是完全可以的,只需从消费者的角度考虑即可。HttpClient 是基于 HTTP 进行集成的基础。它包含你处理HTTP抽象时所需要的一切东西。

public class DadJokesApiClient : IDadJokesApiClient{  private readonly HttpClient httpClient;public DadJokesApiClient(HttpClient httpClient) =>        this.httpClient = httpClient;}

复制代码

通常,HTTP API 会使用 JSON,这就是为什么从.NET 5 开始,BCL 增加了System.Net.Http.Json命名空间。它为HttpClientHttpContent提供了许多扩展方法,让我们可以使用System.Text.Json进行序列化和反序列化。如果没有什么复杂的特殊需求,我建议你使用System.Net.Http.Json,因为它能让你免于编写模板代码。那不仅很枯燥,而且也很难保证高效、没有 Bug。我建议你读下 Steves Gordon 的博文“使用HttpClient发送和接收JSON”:

public async Task<Joke> GetRandomJokeAsync(CancellationToken cancellationToken){  var jokes = await this.httpClient.GetFromJsonAsync<JokeSearchResponse>(      ApiUrlConstants.GetRandomJoke, cancellationToken);if (jokes is { Body.Count: 0 } or { Success: false })  {      // 对于这种情况,考虑创建自定义的异常      throw new InvalidOperationException("This API is no joke.");  }return jokes.Body.First();}

复制代码

小提示:你可以创建一些集中式的地方来管理端点 URL,像下面这样:

public static class ApiUrlConstants{  public const string JokeSearch = "/joke/search";public const string GetJokeById = "/joke";public const string GetRandomJoke = "/random/joke";}

复制代码

小提示:如果你需要处理复杂的 URI,请使用Flurl。它提供了流畅的 URL 构建(URL-building)体验:

public async Task<Joke> GetJokeByIdAsync(string id, CancellationToken cancellationToken){  // $"{ApiUrlConstants.GetJokeById}/{id}"  var path = ApiUrlConstants.GetJokeById.AppendPathSegment(id);var joke = await this.httpClient.GetFromJsonAsync<Joke>(path, cancellationToken);return joke ?? new();}

复制代码

接下来,我们必须指定所需的头文件(和其他所需的配置)。我们希望提供一种灵活的机制来配置作为 SDK 组成部分的HttpClient。在这种情况下,我们需要在自定义头中提供证书,并指定一个众所周知的“Accept”。小提示:将高层的构建块暴露为HttpClientExtensions。这更便于发现特定于 API 的配置。例如,如果你有一个自定义的授权机制,则 SDK 应提供支持(至少要提供相关的文档)。

public static class HttpClientExtensions{  public static HttpClient AddDadJokesHeaders(        this HttpClient httpClient, string host, string apiKey)  {      var headers = httpClient.DefaultRequestHeaders;      headers.Add(ApiConstants.HostHeader, new Uri(host).Host);      headers.Add(ApiConstants.ApiKeyHeader, apiKey);return httpClient;  }}

复制代码

客户端生命周期

为了构建DadJokesApiClient,我们需要创建一个HttpClient。如你所知,HttpClient实现了IDisposable,因为它有一个非托管的底层资源——TCP 连接。在一台机器上同时打开的并发 TCP 连接数量是有限的。这种考虑也带来了一个重要的问题——“我应该在每次需要时创建HttpClient,还是只在应用程序启动时创建一次?”

HttpClient是一个共享对象。这就意味着,在底层,它是可重入和线程安全的。与其每次执行时新建一个HttpClient实例,不如共享一个HttpClient实例。然而,这种方法也有一系列的问题。例如,客户端在应用程序的生命周期内会保持连接打开,它不会遵守DNS TTL设置,而且它将永远无法收到 DNS 更新。所以这也不是一个完美的解决方案。

你需要管理一个不定时销毁连接的 TCP 连接池,以获取 DNS 更新。这正是HttpClientFactory所做的。官方文档将HttpClientFactory描述为“一个专门用于创建可在应用程序中使用的HttpClient实例的工厂”。我们稍后将介绍如何使用它。

每次从IHttpClientFactory获取一个HttpClient对象时,都会返回一个新的实例。但是,每个HttpClient都使用一个被IHttpClientFactory池化并重用的 HttpMessageHandler,减少了资源消耗。处理程序的池化是值得的,因为通常每个处理程序都要管理其底层的 HTTP 连接。有些处理程序还会无限期地保持连接开放,防止处理程序对 DNS 的变化做出反应。HttpMessageHandler有一个有限的生命周期。

下面,我们看下在使用由依赖注入(DI)管理的HttpClient时,HttpClientFactory是如何发挥作用的。

fcff916272207a29680c679cc697c961.png

消费 API 客户端

在我们的例子中,消费 API 的一个基本场景是无依赖注入容器的控制台应用程序。这里的目标是让消费者以最快的方式来访问已有的 API。

创建一个静态工厂方法来创建一个 API 客户端。

public static class DadJokesApiClientFactory{  public static IDadJokesApiClient Create(string host, string apiKey)  {      var httpClient = new HttpClient()      {            BaseAddress = new Uri(host);      }      ConfigureHttpClient(httpClient, host, apiKey);return new DadJokesApiClient(httpClient);  }internal static void ConfigureHttpClient(        HttpClient httpClient, string host, string apiKey)  {      ConfigureHttpClientCore(httpClient);      httpClient.AddDadJokesHeaders(host, apiKey);  }internal static void ConfigureHttpClientCore(HttpClient httpClient)  {      httpClient.DefaultRequestHeaders.Accept.Clear();      httpClient.DefaultRequestHeaders.Accept.Add(new("application/json"));  }}

复制代码

这样,我们可以从控制台应用程序使用IDadJokesApiClient

var host = "https://dad-jokes.p.rapidapi.com";var apiKey = "<token>";
var client = DadJokesApiClientFactory.Create(host, apiKey);var joke = await client.GetRandomJokeAsync();
Console.WriteLine($"{joke.Setup} {joke.Punchline}");

b60100a2e57cd19dd59a6c117f7d7a31.png

消费 API 客户端:HttpClientFactory

下一步是将HttpClient配置为依赖注入容器的一部分。关于这一点,网上有很多不错的内容,我就不做详细讨论了。Steve Gordon 也有一篇非常好的文章“ASP.NET Core中的HttpClientFactory”。

为了使用 DI 添加一个池化的HttpClient实例,你需要使用来自Microsoft.Extensions.HttpIServiceCollection.AddHttpClient

提供一个自定义的扩展方法用于在 DI 中添加类型化的HttpClient

public static class ServiceCollectionExtensions{  public static IHttpClientBuilder AddDadJokesApiClient(      this IServiceCollection services,      Action<HttpClient> configureClient) =>          services.AddHttpClient<IDadJokesApiClient, DadJokesApiClient>((httpClient) =>          {              DadJokesApiClientFactory.ConfigureHttpClientCore(httpClient);              configureClient(httpClient);          });}

复制代码

使用扩展方法的方式如下:

var host = "https://da-jokes.p.rapidapi.com";var apiKey = "<token>";
var services = new ServiceCollection();
services.AddDadJokesApiClient(httpClient =>{  httpClient.BaseAddress = new(host);  httpClient.AddDadJokesHeaders(host, apiKey);});
var provider = services.BuildServiceProvider();var client = provider.GetRequiredService<IDadJokesApiClient>();
var joke = await client.GetRandomJokeAsync();
logger.Information($"{joke.Setup} {joke.Punchline}");

复制代码

如你所见,IHttpClientFactory 可以在 ASP.NET Core 之外使用。例如,控制台应用程序、worker、lambdas 等。让我们看下它运行:

b3c9aa8279f47d3a13ade28ab5cd28d5.png

有趣的是,由 DI 创建的客户端会自动记录发出的请求,使得开发和故障排除都变得非常容易。

如果你操作日志模板的格式并添加SourceContextEventId,就会看到HttpClientFactory自己添加了额外的处理程序。当你试图排查与 HTTP 请求处理有关的问题时,这很有用。

{SourceContext}[{EventId}] // 模式
System.Net.Http.HttpClient.IDadJokesApiClient.LogicalHandler [{ Id: 100, Name: "RequestPipelineStart" }]  System.Net.Http.HttpClient.IDadJokesApiClient.ClientHandler [{ Id: 100, Name: "RequestStart" }]  System.Net.Http.HttpClient.IDadJokesApiClient.ClientHandler [{ Id: 101, Name: "RequestEnd" }]System.Net.Http.HttpClient.IDadJokesApiClient.LogicalHandler [{ Id: 101, Name: "RequestPipelineEnd" }]

复制代码

最常见的场景是 Web 应用程序。下面是.NET 6 MinimalAPI 示例:

var builder = WebApplication.CreateBuilder(args);var services = builder.Services;var configuration = builder.Configuration;var host = configuration["DadJokesClient:host"];
services.AddDadJokesApiClient(httpClient =>{  httpClient.BaseAddress = new(host);  httpClient.AddDadJokesHeaders(host, configuration["DADJOKES_TOKEN"]);});
var app = builder.Build();
app.MapGet("/", async (IDadJokesApiClient client) => await client.GetRandomJokeAsync());
app.Run();

3ffd363a68fdbed4e38c0411679ad1eb.png

{  "punchline": "They are all paid actors anyway,"  "setup": "We really shouldn't care what people at the Oscars say,"  "type": "actor"}

复制代码

扩展 HTTP 客户端 SDK,通过 DelegatingHandler 添加横切关注点

HttpClient还提供了一个扩展点:一个消息处理程序。它是一个接收 HTTP 请求并返回 HTTP 响应的类。有许多问题都可以表示为横切关注点。例如,日志、身份认证、缓存、头信息转发、审计等等。面向方面的编程旨在将横切关注点封装成方面,以保持模块化。通常情况下,一系列的消息处理程序被链接在一起。第一个处理程序接收一个 HTTP 请求,做一些处理,然后将请求交给下一个处理程序。有时候,响应创建后会回到链条上游。

// 支持大部分应用程序最常见的需求public abstract class HttpMessageHandler : IDisposable{}// 将一个处理程序加入到处理程序链public abstract class DelegatingHandler : HttpMessageHandler{}

8a64bde72d23e44297ea9de0ebe626bb.png

任务:假如你需要从 ASP.NET Core 的HttpContext复制一系列头信息,并将它们传递给 Dad Jokes API 客户端发出的所有外发请求。

public class HeaderPropagationMessageHandler : DelegatingHandler{  private readonly HeaderPropagationOptions options;  private readonly IHttpContextAccessor contextAccessor;public HeaderPropagationMessageHandler(      HeaderPropagationOptions options,      IHttpContextAccessor contextAccessor)  {      this.options = options;      this.contextAccessor = contextAccessor;  }protected override Task<HttpResponseMessage> SendAsync(      HttpRequestMessage request, CancellationToken cancellationToken)  {      if (this.contextAccessor.HttpContext != null)      {          foreach (var headerName in this.options.HeaderNames)          {              var headerValue = this.contextAccessor                  .HttpContext.Request.Headers[headerName];request.Headers.TryAddWithoutValidation(                  headerName, (string[])headerValue);          }      }return base.SendAsync(request, cancellationToken);  }}
public class HeaderPropagationOptions{  public IList<string> HeaderNames { get; set; } = new List<string>();}

复制代码

我们想把一个DelegatingHandler“插入”到HttpClient请求管道中。对于非IttpClientFactory场景,我们希望客户端能够指定一个DelegatingHandler列表来为HttpClient建立一个底层链。

//DadJokesApiClientFactory.cspublic static IDadJokesApiClient Create(  string host,  string apiKey,  params DelegatingHandler[] handlers){  var httpClient = new HttpClient();if (handlers.Length > 0)  {      _ = handlers.Aggregate((a, b) =>      {          a.InnerHandler = b;          return b;      });      httpClient = new(handlers[0]);  }  httpClient.BaseAddress = new Uri(host);ConfigureHttpClient(httpClient, host, apiKey);return new DadJokesApiClient(httpClient);}

复制代码

这样,在没有 DI 容器的情况下,可以像下面这样扩展 DadJokesApiClient

var loggingHandler = new LoggingMessageHandler(); //最外层var authHandler = new AuthMessageHandler();var propagationHandler = new HeaderPropagationMessageHandler();var primaryHandler = new HttpClientHandler();  // HttpClient使用的默认处理程序
DadJokesApiClientFactory.Create(  host, apiKey,  loggingHandler, authHandler, propagationHandler, primaryHandler);
// LoggingMessageHandler ➝ AuthMessageHandler ➝ HeaderPropagationMessageHandler ➝ HttpClientHandler

复制代码

另一方面,在 DI 容器场景中,我们希望提供一个辅助的扩展方法,使用IHttpClientBuilder.AddHttpMessageHandler轻松插入HeaderPropagationMessageHandler

public static class HeaderPropagationExtensions{  public static IHttpClientBuilder AddHeaderPropagation(      this IHttpClientBuilder builder,      Action<HeaderPropagationOptions> configure)  {      builder.Services.Configure(configure);      builder.AddHttpMessageHandler((sp) =>      {          return new HeaderPropagationMessageHandler(                              sp.GetRequiredService<IOptions<HeaderPropagationOptions>>().Value,              sp.GetRequiredService<IHttpContextAccessor>());      });return builder;  }}

复制代码

扩展后的 MinimalAPI 示例如下所示:

var builder = WebApplication.CreateBuilder(args);var services = builder.Services;var configuration = builder.Configuration;var host = configuration["DadJokesClient:host"];
services.AddDadJokesApiClient(httpClient =>{  httpClient.BaseAddress = new(host);  httpClient.AddDadJokesHeaders(host, configuration["DADJOKES_TOKEN"]);}).AddHeaderPropagation(o => o.HeaderNames.Add("X-Correlation-ID"));
var app = builder.Build();
app.MapGet("/", async (IDadJokesApiClient client) => await client.GetRandomJokeAsync());
app.Run();

复制代码

有时,像这样的功能会被其他服务所重用。你可能想更进一步,把所有共享的代码都提取到一个公共的 NuGet 包中,并在 HTTP 客户端 SDK 中使用它。

第三方扩展

我们可以编写自己的消息处理程序,但.NET OSS 社区也提供了许多有用的 NuGet 包。以下是我最喜欢的。

弹性模式——重试、缓存、回退等:很多时候,在一个系统不可靠的世界里,你需要通过加入一些弹性策略来确保高可用性。幸运的是,我们有一个内置的解决方案,可以在.NET 中构建和定义策略,那就是Polly。Polly 提供了与IHttpClientFactory开箱即用的集成。它使用了一个便捷的方法IHttpClientBuilder.AddTransientHttpErrorPolicy。它配置了一个策略来处理 HTTP 调用的典型错误:HttpRequestException HTTP 5XX 状态码(服务器错误)、HTTP 408 状态码(请求超时)。

services.AddDadJokesApiClient(httpClient =>{  httpClient.BaseAddress = new(host);}).AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync(new[]{  TimeSpan.FromSeconds(1),  TimeSpan.FromSeconds(5),  TimeSpan.FromSeconds(10)}));

复制代码

例如,可以使用重试断路器模式主动处理瞬时错误。通常,当下游服务有望自我纠正时,我们会使用重试模式。重试之间的等待时间对于下游服务而言是一个恢复稳定的窗口。重试经常使用指数退避算法。这纸面上听起来不错,但在现实世界的场景中,重试模式的使用可能过度了。额外的重试可能导致额外的负载或峰值。在最坏的情况下,调用者的资源可能会被耗尽或过分阻塞,等待永远不会到来的回复,导致上游发生了级联故障。这就是断路器模式发挥作用的时候了。它检测故障等级,并在故障超过阈值时阻止对下游服务的调用。如果没有成功的机会,就可以使用这种模式,例如,当一个子系统完全离线或不堪重负时。断路器的理念非常简单,虽然你可能会以它为基础构建一些更复杂的东西。当故障超过阈值时,调用就会断开,因此,我们不是处理请求,而是实践快速失败的方法,立即抛出一个异常。

Polly 真的很强大,它提供了一种组合弹性策略的方法,见PolicyWrap。

下面是一个可能对你有用的策略分类:

5b0ae4bc869c07ce3050930b7a898752.png

设计可靠的系统可能是一项非常具有挑战性的任务,我建议你自己研究下这个问题。这里有一个很好的介绍——.NET微服务架构电子书:实现弹性应用程序。

OAuth2/OIDC 中的身份认证:如果你需要管理用户和客户端访问令牌,我建议使用IdentityModel.AspNetCore。它可以帮你获取、缓存和轮换令牌,详情参见文档。

// 添加用户和客户端访问令牌管理services.AddAccessTokenManagement(options =>{  options.Client.Clients.Add("identity-provider", new ClientCredentialsTokenRequest  {      Address = "https://demo.identityserver.io/connect/token",      ClientId = "my-awesome-service",      ClientSecret = "secret",      Scope = "api"   });});// 使用托管的客户端访问令牌注册HTTP客户端// 向HTTP客户端注册添加令牌访问处理程序services.AddDadJokesApiClient(httpClient =>{  httpClient.BaseAddress = new(host);}).AddClientAccessTokenHandler();

复制代码

测试 HTTP 客户端 SDK

至此,对于设计和编写 HTTP 客户端 SDK,你应该已经比较熟悉了。剩下的工作就只是写一些测试来确保其行为符合预期了。请注意,跳过广泛的单元测试,编写更多的集成或 e2e 来确保集成的正确性,或许也不错。现在,我将展示如何对DadJokesApiClient进行单元测试。

如前所述,HttpClient是可扩展的。此外,我们可以用测试版本代替标准的HttpMessageHandler。这样,我们就可以使用模拟服务,而不是通过网络发送实际的请求。这种技术提供了大量的可能,因为我们可以模拟各种在正常情况下是很难复现的HttpClient行为。

我们定义一个可重用的方法,用于创建一个 HttpClient 模拟,并作为一个依赖项传递给DadJokesApiClient

public static class TestHarness{  public static Mock<HttpMessageHandler> CreateMessageHandlerWithResult<T>(      T result, HttpStatusCode code = HttpStatusCode.OK)  {      var messageHandler = new Mock<HttpMessageHandler>();      messageHandler.Protected()          .Setup<Task<HttpResponseMessage>>(              "SendAsync",              ItExpr.IsAny<HttpRequestMessage>(),              ItExpr.IsAny<CancellationToken>())          .ReturnsAsync(new HttpResponseMessage()          {              StatusCode = code,              Content = new StringContent(JsonSerializer.Serialize(result)),          });return messageHandler;  }public static HttpClient CreateHttpClientWithResult<T>(      T result, HttpStatusCode code = HttpStatusCode.OK)  {      var httpClient = new HttpClient(CreateMessageHandlerWithResult(result, code).Object)      {          BaseAddress = new("https://api-client-under-test.com"),      };Return httpClient;  }}

复制代码

从这点来看,单元测试是个非常简单的过程:

public class DadJokesApiClientTests{  [Theory, AutoData]  public async Task GetRandomJokeAsync_SingleJokeInResult_Returned(Joke joke)  {      // Arrange      var response = new JokeSearchResponse      {          Success = true,          Body = new() { joke }      };      var httpClient = CreateHttpClientWithResult(response);      var sut = new DadJokesApiClient(httpClient);// Act      var result = await sut.GetRandomJokeAsync();// Assert      result.Should().BeEquivalentTo(joke);  }[Fact]  public async Task GetRandomJokeAsync_UnsuccessfulJokeResult_ExceptionThrown()  {      // Arrange      var response = new JokeSearchResponse();      var httpClient = CreateHttpClientWithResult(response);      var sut = new DadJokesApiClient(httpClient);// Act      // Assert      await FluentActions.Invoking(() => sut.GetRandomJokeAsync())            .Should().ThrowAsync<InvalidOperationException>();  }}

使用HttpClient是最灵活的方法。你可以完全控制与 API 的集成。但是,也有一个缺点,你需要编写大量的样板代码。在某些情况下,你要集成的 API 并不重要,所以你并不需要HttpClientHttpRequestMessageHttpResponseMessage所提供的所有功能。优点➕:

  • 可以完全控制行为和数据契约。你甚至可以编写一个“智能”API 客户端,如果有需要的话,在特殊情况下,你可以把一些逻辑移到 SDK 里。例如,你可以抛出自定义的异常,转换请求和响应,提供默认头信息,等等。

  • 可以完全控制序列化和反序列化过程。

  • 易于调试和排查问题。堆栈容易跟踪,你可以随时启动调试器,看看后台正在发生的事情。缺点➖:

  • 需要编写大量的重复代码。

  • 需要有人维护代码库,以防 API 有变化和 Bug。这是一个繁琐的、容易出错的过程。

使用声明式方法编写 HTTP 客户端 SDK

代码越少,Bug 越少。Refit是一个用于.NET 的、自动化的、类型安全的 REST 库。它将 REST API 变成一个随时可用的接口。Refit 默认使用System.Text.Json作为 JSON 序列化器。

每个方法都必须有一个 HTTP 属性,提供请求方法和相对应的 URL。

using Refit;
public interface IDadJokesApiClient{  /// <summary>  /// 根据词语搜索笑话。  /// </summary>  [Get("/joke/search")]  Task<JokeSearchResponse> SearchAsync(      string term,      CancellationToken cancellationToken = default);/// <summary>  /// 根据id获取一个笑话。  /// </summary>  [Get("/joke/{id}")]  Task<Joke> GetJokeByIdAsync(      string id,      CancellationToken cancellationToken = default);/// <summary>  /// 随机获取一个笑话。  /// </summary>  [Get("/random/joke")]  Task<JokeSearchResponse> GetRandomJokeAsync(      CancellationToken cancellationToken = default);}

复制代码

Refit 根据Refit.HttpMethodAttribute提供的信息生成实现IDadJokesApiClient接口的类型。

消费 API 客户端:Refit

该方法与平常的HttpClient集成方法相同,但我们不是手动构建一个客户端,而是使用 Refit 提供的静态方法。

public static class DadJokesApiClientFactory{  public static IDadJokesApiClient Create(      HttpClient httpClient,      string host,      string apiKey)  {      httpClient.BaseAddress = new Uri(host);ConfigureHttpClient(httpClient, host, apiKey);return RestService.For<IDadJokesApiClient>(httpClient);  }  // ...}

对于 DI 容器场景,我们可以使用Refit.HttpClientFactoryExtensions.AddRefitClient扩展方法。

public static class ServiceCollectionExtensions{  public static IHttpClientBuilder AddDadJokesApiClient(      this IServiceCollection services,      Action<HttpClient> configureClient)  {      var settings = new RefitSettings()      {          ContentSerializer = new SystemTextJsonContentSerializer(new JsonSerializerOptions()          {              PropertyNameCaseInsensitive = true,              WriteIndented = true,          })      };return services.AddRefitClient<IDadJokesApiClient>(settings).ConfigureHttpClient((httpClient) =>      {          DadJokesApiClientFactory.ConfigureHttpClient(httpClient);          configureClient(httpClient);      });  }}

用法如下:

var builder = WebApplication.CreateBuilder(args);var configuration = builder.Configuration;
Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateBootstrapLogger();builder.Host.UseSerilog((ctx, cfg) => cfg.WriteTo.Console());
var services = builder.Services;
services.AddDadJokesApiClient(httpClient =>{  var host = configuration["DadJokesClient:host"];  httpClient.BaseAddress = new(host);  httpClient.AddDadJokesHeaders(host, configuration["DADJOKES_TOKEN"]);});
var app = builder.Build();
app.MapGet("/", async Task<Joke> (IDadJokesApiClient client) =>{  var jokeResponse = await client.GetRandomJokeAsync();return jokeResponse.Body.First(); // unwraps JokeSearchResponse});
app.Run();

注意,由于生成的客户端其契约应该与底层数据契约相匹配,所以我们不再控制契约的转换,这项职责被托付给了消费者。让我们看看上述代码在实践中是如何工作的。MinimalAPI 示例的输出有所不同,因为我加入了 Serilog 日志。

17d03512fd3136ca988226d4338dcaef.png

{  "punchline": "Forgery.",  "setup": "Why was the blacksmith charged with?",  "type": "forgery"}

复制代码

同样,这种方法也有其优缺点:优点➕:

  • 便于使用和开发 API 客户端。

  • 高度可配置。可以非常灵活地把事情做好。

  • 不需要额外的单元测试。缺点➖:

  • 故障排查困难。有时候很难理解生成的代码是如何工作的。例如,在配置上存在不匹配。

  • 需要团队其他成员了解如何阅读和编写使用 Refit 开发的代码。

  • 对于中/大型 API 来说,仍然有一些时间消耗。感兴趣的读者还可以了解下RestEase。

使用自动化方法编写 HTTP 客户端 SDK

有一种方法可以完全自动地生成 HTTP 客户端 SDK。OpenAPI/Swagger 规范使用 JSON 和 JSON Schema 来描述 RESTful Web API。NSwag项目提供的工具可以从这些 OpenAPI 规范生成客户端代码。所有东西都可以通过 CLI(通过 NuGet 工具、构建目标或 NPM 分发)自动化。

Dad Jokes API 不提供 OpenAPI,所以我手动编写了一个。幸运的是,这很容易:

openapi: '3.0.2'info:  title: Dad Jokes API  version: '1.0'servers:  - url: https://dad-jokes.p.rapidapi.compaths:  /joke/{id}:  get:    description: ''    operationId: 'GetJokeById'    parameters:    - name: "id"      in: "path"      description: ""      required: true      schema:        type: "string"    responses:      '200':        description: successful operation        content:          application/json:            schema:              "$ref": "#/components/schemas/Joke"  /random/joke:  get:    description: ''    operationId: 'GetRandomJoke'    parameters: []    responses:      '200':        description: successful operation        content:          application/json:            schema:              "$ref": "#/components/schemas/JokeResponse"  /joke/search:  get:    description: ''    operationId: 'SearchJoke'    parameters: []    responses:      '200':        description: successful operation        content:          application/json:            schema:              "$ref": "#/components/schemas/JokeResponse"components:  schemas:  Joke:    type: object    required:    - _id    - punchline    - setup    - type    properties:      _id:        type: string      type:        type: string      setup:        type: string      punchline:        type: string  JokeResponse:    type: object    properties:      sucess:        type: boolean      body:        type: array        items:          $ref: '#/components/schemas/Joke'

复制代码

现在,我们希望自动生成 HTTP 客户端 SDK。让我们借助NSwagStudio。生成的

IDadJokesApiClient 类似下面这样(简洁起见,删除了 XML 注释):

82548ba075607b5a80e937b019d4f5f2.png

[System.CodeDom.Compiler.GeneratedCode("NSwag", "13.10.9.0 (NJsonSchema v10.4.1.0 (Newtonsoft.Json v12.0.0.0))")]  public partial interface IDadJokesApiClient  {      System.Threading.Tasks.Task<Joke> GetJokeByIdAsync(string id);          System.Threading.Tasks.Task<Joke> GetJokeByIdAsync(string id, System.Threading.CancellationToken cancellationToken);          System.Threading.Tasks.Task<JokeResponse> GetRandomJokeAsync();          System.Threading.Tasks.Task<JokeResponse> GetRandomJokeAsync(System.Threading.CancellationToken cancellationToken);          System.Threading.Tasks.Task<JokeResponse> SearchJokeAsync();          System.Threading.Tasks.Task<JokeResponse> SearchJokeAsync(System.Threading.CancellationToken cancellationToken);  }

复制代码

同样,我们希望把类型化客户端的注册作为一个扩展方法来提供。

public static class ServiceCollectionExtensions{  public static IHttpClientBuilder AddDadJokesApiClient(      this IServiceCollection services, Action<HttpClient> configureClient) =>          services.AddHttpClient<IDadJokesApiClient, DadJokesApiClient>(              httpClient => configureClient(httpClient));}

用法如下:

var builder = WebApplication.CreateBuilder(args);var configuration = builder.Configuration;var services = builder.Services;
services.AddDadJokesApiClient(httpClient =>{  var host = configuration["DadJokesClient:host"];  httpClient.BaseAddress = new(host);  httpClient.AddDadJokesHeaders(host, configuration["DADJOKES_TOKEN"]);});
var app = builder.Build();
app.MapGet("/", async Task<Joke> (IDadJokesApiClient client) =>{  var jokeResponse = await client.GetRandomJokeAsync();return jokeResponse.Body.First();});
app.Run();

复制代码

让我们运行它,并欣赏本文最后一个笑话:

{  "punchline": "And it's really taken off,"  "setup": "So I invested in a hot air balloon company...",  "type": "air"}

复制代码

优点➕:

  • 基于众所周知的规范。

  • 有丰富的工具和活跃的社区支持。

  • 完全自动化,新 SDK 可以作为 CI/CD 流程的一部分在每次 OpenAPI 规范有变化时生成。

  • 可以生成多种语言的 SDK。

  • 由于可以看到工具链生成的代码,所以相对来说比较容易排除故障。缺点➖:

  • 如果不符合 OpenAPI 规范就无法使用。

  • 难以定制和控制生成的 API 客户端的契约。感兴趣的读者还可以了解下AutoRest、Visual Studio Connected Services。

选择合适的方法

在这篇文章中,我们学习了三种不同的构建 SDK 客户端的方法。简单来说,可以遵循以下规则选用正确的方法:

我是一个简单的人。我希望完全控制我的 HTTP 客户端集成。使用手动方法。

我是个大忙人,但我仍然希望有部分控制权。使用声明式方法。

我是个懒人。最好能帮我做。使用自动化方法。

决策图如下:

8b62a1046e726af88d75c41e882ce77b.png

总结

在这篇文章中,我们回顾了开发 HTTP 客户端 SDK 的不同方式。请根据具体的用例和需求选择正确的方法,希望这篇文章能让你有一个大概的了解,使你在设计客户端 SDK 时能做出最好的设计决策。感谢阅读。

作者简介:

Oleksii Nikiforov 是 EPAM Systems 的高级软件工程师和团队负责人。他拥有应用数学学士学位和信息技术硕士学位,从事软件开发已有 6 年多,热衷于.NET、分布式系统和生产效率,是N+1博客的作者。

1deab8d47cae295c17f6007e82bd0926.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/290329.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ttl接地是高电平还是低电平_功放技术参数1——高电平

在汽车音响中的功放或者DSP再或者是DSP功放中我们都会遇到高电平信号或者低电平信号输入&#xff0c;我们该如何判断主机输出的到底是高电平信号还是低电平信号呢&#xff1f;我们可以用一个很简单的方法来鉴定&#xff0c;那就是主机输出能够直接驱动喇叭的为高电平信号输出&a…

MultiProcessing中主进程与子进程之间通过管道(Pipe)通信

Python 中 Multiprocessing 实现进程通信1. 如何建立主进程与子进程之间的通信管道&#xff1f;2. 为什么一定要将Pipe中的某些端close()?本文参考自&#xff1a;python 学习笔记 - Queue & Pipes&#xff0c;进程间通讯 1. 如何建立主进程与子进程之间的通信管道&#xf…

如何为 .NET 项目自定义强制代码样式规则

前言每个人都有自己的代码样式习惯:命名约定、大括号、空格、换行等。但是&#xff0c;作为一个团队来说&#xff0c;应该使用同样的代码样式规则。这样可以有效减少编译器的警告/建议&#xff0c;保证阅读代码的人员理解一致。今天我们介绍一种为单独的 .NET 项目定义代码样式…

我是如何帮助创业公司改进企业工作的

前段时间在一家创业公司实习&#xff0c;几十个人的团队&#xff0c;正处在规模逐渐扩大的阶段&#xff0c;但是整个公司的协作工作和日常管理却越来越麻烦&#xff0c;鉴于我以前对Saas和协作平台都有过一点研究&#xff0c;于是leader叫我去找一个“简单&#xff0c;好用&…

PHP单例模式(精讲)

2019独角兽企业重金招聘Python工程师标准>>> 首先我们要明确单例模式这个概念&#xff0c;那么什么是单例模式呢&#xff1f; 单例模式顾名思义&#xff0c;就是只有一个实例。作为对象的创建模式&#xff0c;单例模式确保某一个类只有一个实例&#xff0c;而且自行…

【QMIX】一种基于Value-Based多智能体算法

文章目录1. QMIX 解决了什么问题&#xff08;Motivation&#xff09;2. QMIX 怎样解决团队收益最大化问题&#xff08;Method&#xff09;2.1 算法大框架 —— 基于 AC 框架的 CTDE&#xff08;Centralized Training Distributed Execution&#xff09; 模式2.2 Agent RNN Netw…

增强型的for循环linkedlist_LinkedList的复习

先摘选一段Testpublic void test_LinkedList() { // 初始化100万数据 List list new LinkedList(1000000);// 遍历求和int sum 0;for (int i 0; i sum list.get(i); }}乍一看可能觉得没什么问题&#xff0c;但是这个遍历求和会非常慢。主要因为链表的数据结构…

3月更新来了!Windows 11正式版22000.556发布

面向 Windows 11 正式版用户&#xff0c;微软现已发布累积更新 KB5011493&#xff0c;更新后版本号升级至 Build 22000.556。主要变化1.微软正在改变 Windows 11 "开始"菜单中推荐模块有关 Office 文件的打开方式。如果文件被同步到 OneDrive&#xff0c;“开始”菜单…

[C/C++]重读《The C Programming Language》

第一次读这本书的时候是大三初&#xff0c;现在打算重读一遍&#xff01;。 第一章 导言 1. 学习一门新程序设计语言的唯一途径就是用它来写程序。 2. 每个程序都从main函数的起点开始执行。 3. 在C语言中&#xff0c;所有变量必须先声明后使用。 4. C语言中的基本数据类型的大…

115怎么利用sha1下载东西_618“甩”度娘,拥抱115,体验和价格才是王道

网盘价钱​前天618&#xff0c;圈子里的朋友几乎都“甩”了度娘一巴掌&#xff0c;我才知道115搞活动&#xff0c;由原来500元1年的钻石会员&#xff0c;变成500元3年&#xff0c;算起来每天不到0.5元&#xff0c;确实比度娘实惠了很多&#xff0c;而且活动持续到6月底。自从发…

安装宝塔面板

安装宝塔面板&#xff1a; 1. 宝塔面板网站&#xff1a; https://www.bt.cn/ 2.安装教程 https://www.bt.cn/bbs/thread-1186-1-1.html 3.1 使用远程工具连接执行以下命令 yum install -y wget && wget -O install.sh http://download.bt.cn/install/install.sh &&…

【COMA】一种将团队回报拆分为独立回报的多智能体算法

文章目录1. COMA 解决了什么问题&#xff08;Motivation&#xff09;2. COMA 怎么解决独立回报分配问题&#xff08;Method&#xff09;2.1 核心思想 counterfactual baseline 的提出2.2 算法大框架 —— 基于 AC 框架的 CTDE&#xff08;Centralized Training Distributed Exe…

C#解析Markdown文档,实现替换图片链接操作

前言又是好久没写博客了其实也不是没写&#xff0c;是最近在「做一个博客」&#xff0c;从2月21日开始&#xff0c;大概一个多星期的时间&#xff0c;疯狂刷进度&#xff0c;边写代码边写了一整系列的博客开发笔记&#xff0c;目前为止已经写了16篇了&#xff0c;然后上3月之后…

LoadRunner测试下载功能点脚本(方法一)

性能需求&#xff1a;对系统某页面中&#xff0c;点击下载功能做并发测试&#xff0c;以获取在并发下载文件的情况下系统的性能指标。 备注&#xff1a;页面上点击下载时的文件可以是word、excel、pdf等。 问题1&#xff1a;录制完下载的场景后&#xff0c;发现脚本里面并没有包…

海南橡胶机器人成本_「图说」海垦看点:海南橡胶联合北京理工华汇智能科技首创我国林间智能割胶机器人...

1 海垦南繁产业集团长期以来高度重视改善职工居住条件&#xff0c;于去年启动了海燕队保障性住房项目&#xff0c;项目建成后将有效解决职工住房问题。图为近日正在加紧施工的建设工地。 蒙胜国 摄2 海南橡胶联合北京理工华汇智能科技有限公司&#xff0c;研发出来的最新一代林…

数据挖掘在轨迹信息上的应用实验

文章目录1. 实验概览2. 数据集下载3. 数据预处理3.1 异常点去除3.2 停留点检测与环绕点检测3.3 轨迹分段4. 基于轨迹信息的数据挖掘4.1 路口检测4.1.1 地图分割与轨迹点速度计算4.2 偏好学习通常&#xff0c;我们将一个连续的GPS信号点序列称为一个轨迹&#xff08;Trajectory&…

Avalonia跨平台入门第二十三篇之滚动字幕

在前面分享的几篇中咱已经玩耍了Popup、ListBox多选、Grid动态分、RadioButton模板、控件的拖放效果、控件的置顶和置底、控件的锁定、自定义Window样式、动画效果、Expander控件、ListBox折叠列表、聊天窗口、ListBox图片消息、窗口抖动、语音发送、语音播放、语音播放问题、玩…

oracle dba 手动创建数据实例

2019独角兽企业重金招聘Python工程师标准>>> 1.手动建库大致步骤 设置环境变量.bash_profile创建目录结构创建参数文件(位置:$ORACLE_HOME/dbs)生成密码文件执行建库脚本创建数据字典其他设置2.DBCA 脚本创建 2.1设置系统环境变量 ORACLE_HOME/app/oracle/11g/11.2.…

asp 强制转换浮点数值_C/C++中浮点数的编码存储

浮点数也称做实型数据(实数)&#xff0c;形式上就是数学中的小数。浮点型数据有两种表达方式&#xff1a; 一种是用数字和小数点表示的&#xff0c;如123.456&#xff1b; 另一种是用指数方式表示&#xff0c;如1.2e-6 或1.2E-6(1.2*10-6)。在计算机中实数是如何存储的呢&#…

PaddleNLP实战——信息抽取(InfoExtraction)

[ 文章目录 ]1. 信息抽取任务是什么&#xff1f;2. 基于PaddleNLP的信息抽取任务2.1 训练任务概览2.2 Predicate列表2.3 SPO列表2.4 代码解析1. 信息抽取任务是什么&#xff1f; 在NLP任务中&#xff0c;通常当我们拿到一段文本时&#xff0c;我们希望机器去理解这段文本描述的…