linux 之父出席、干货分享、圆桌讨论,精彩尽在 opencloudos 社区开放日,报名戳
写点什么

在.net 6中如何创建和使用http客户端sdk-金马国际

作者:oleksii nikiforov

  • 2022 年 3 月 02 日
  • 本文字数:13889 字

    阅读完需:约 46 分钟

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


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


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


http 客户端 sdk 与 api 一同使用:


  1. 加速 api 集成过程;

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

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

编写一个 http 客户端 sdk

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


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


public interface idadjokesapiclient{  task searchasync(      string term, cancellationtoken cancellationtoken);
task getjokebyidasync( string id, cancellationtoken cancellationtoken);
task getrandomjokeasync(cancellationtoken cancellationtoken);}
public class jokesearchresponse{ public bool success { get; init; }
public list 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 进行集成的基础。它包含你处理抽象时所需要的一切东西。


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 的博文“”:


public async task getrandomjokeasync(cancellationtoken cancellationtoken){  var jokes = await this.httpclient.getfromjsonasync(      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,请使用。它提供了流畅的 url 构建(url-building)体验:


public async task getjokebyidasync(string id, cancellationtoken cancellationtoken){  // $"{apiurlconstants.getjokebyid}/{id}"  var path = apiurlconstants.getjokebyid.appendpathsegment(id);
var joke = await this.httpclient.getfromjsonasync(path, cancellationtoken);
return joke ?? new();}
复制代码


接下来,我们必须指定所需的头文件(和其他所需的配置)。我们希望提供一种灵活的机制来配置作为 sdk 组成部分的httpclient。在这种情况下,我们需要在自定义头中提供证书,并指定一个众所周知的“”。小提示:将高层的构建块暴露为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 更新。所以这也不是一个完美的金马国际的解决方案。


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


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


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


消费 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 = "";
var client = dadjokesapiclientfactory.create(host, apikey);var joke = await client.getrandomjokeasync();
console.writeline($"{joke.setup} {joke.punchline}");
复制代码


消费 api 客户端:httpclientfactory

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


为了使用 di 添加一个池化的httpclient实例,你需要使用来自microsoft.extensions.httpiservicecollection.addhttpclient


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


public static class servicecollectionextensions{  public static ihttpclientbuilder adddadjokesapiclient(      this iservicecollection services,      action configureclient) =>          services.addhttpclient((httpclient) =>          {              dadjokesapiclientfactory.configurehttpclientcore(httpclient);              configureclient(httpclient);          });}
复制代码


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


var host = "https://da-jokes.p.rapidapi.com";var apikey = "";
var services = new servicecollection();
services.adddadjokesapiclient(httpclient =>{ httpclient.baseaddress = new(host); httpclient.adddadjokesheaders(host, apikey);});
var provider = services.buildserviceprovider();var client = provider.getrequiredservice();
var joke = await client.getrandomjokeasync();
logger.information($"{joke.setup} {joke.punchline}");
复制代码


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



有趣的是,由 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();
复制代码



{  "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{}
复制代码


任务:假如你需要从 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 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 headernames { get; set; } = new list();}
复制代码


我们想把一个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 configure)  {      builder.services.configure(configure);      builder.addhttpmessagehandler((sp) =>      {          return new headerpropagationmessagehandler(                              sp.getrequiredservice>().value,              sp.getrequiredservice());      });
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 提供了与ihttpclientfactory开箱即用的集成。它使用了一个便捷的方法。它配置了一个策略来处理 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 真的很强大,它提供了一种组合弹性策略的方法,见。


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



设计可靠的系统可能是一项非常具有挑战性的任务,我建议你自己研究下这个问题。这里有一个很好的介绍——。


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


// 添加用户和客户端访问令牌管理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 createmessagehandlerwithresult(      t result, httpstatuscode code = httpstatuscode.ok)  {      var messagehandler = new mock();      messagehandler.protected()          .setup>(              "sendasync",              itexpr.isany(),              itexpr.isany())          .returnsasync(new httpresponsemessage()          {              statuscode = code,              content = new stringcontent(jsonserializer.serialize(result)),          });
return messagehandler; }
public static httpclient createhttpclientwithresult( 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(); }}
复制代码


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


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

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

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

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

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

使用声明式方法编写 http 客户端 sdk

代码越少,bug 越少。是一个用于.net 的、自动化的、类型安全的 rest 库。它将 rest api 变成一个随时可用的接口。refit 默认使用system.text.json作为 json 序列化器。


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


using refit;
public interface idadjokesapiclient{ /// /// 根据词语搜索笑话。 /// [get("/joke/search")] task searchasync( string term, cancellationtoken cancellationtoken = default);
/// /// 根据id获取一个笑话。 /// [get("/joke/{id}")] task getjokebyidasync( string id, cancellationtoken cancellationtoken = default);
/// /// 随机获取一个笑话。 /// [get("/random/joke")] task 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(httpclient); } // ...}
复制代码


对于 di 容器场景,我们可以使用refit.httpclientfactoryextensions.addrefitclient扩展方法。


public static class servicecollectionextensions{  public static ihttpclientbuilder adddadjokesapiclient(      this iservicecollection services,      action configureclient)  {      var settings = new refitsettings()      {          contentserializer = new systemtextjsoncontentserializer(new jsonserializeroptions()          {              propertynamecaseinsensitive = true,              writeindented = true,          })      };
return services.addrefitclient(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 (idadjokesapiclient client) =>{ var jokeresponse = await client.getrandomjokeasync();
return jokeresponse.body.first(); // unwraps jokesearchresponse});
app.run();
复制代码


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



{  "punchline": "forgery.",  "setup": "why was the blacksmith charged with?",  "type": "forgery"}
复制代码


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


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

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

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

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

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

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

使用自动化方法编写 http 客户端 sdk

有一种方法可以完全自动地生成 http 客户端 sdk。openapi/swagger 规范使用 json 和 json schema 来描述 restful web api。项目提供的工具可以从这些 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。让我们借助。生成的

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



[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 getjokebyidasync(string id);          system.threading.tasks.task getjokebyidasync(string id, system.threading.cancellationtoken cancellationtoken);          system.threading.tasks.task getrandomjokeasync();          system.threading.tasks.task getrandomjokeasync(system.threading.cancellationtoken cancellationtoken);          system.threading.tasks.task searchjokeasync();          system.threading.tasks.task searchjokeasync(system.threading.cancellationtoken cancellationtoken);  }
复制代码


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


public static class servicecollectionextensions{  public static ihttpclientbuilder adddadjokesapiclient(      this iservicecollection services, action configureclient) =>          services.addhttpclient(              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 (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 客户端的契约。感兴趣的读者还可以了解下、。

选择合适的方法

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


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


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


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


决策图如下:


总结

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


作者简介:


oleksii nikiforov 是 epam systems 的高级软件工程师和团队负责人。他拥有应用数学学士学位和信息技术硕士学位,从事软件开发已有 6 年多,热衷于.net、分布式系统和生产效率,是的作者。


原文链接:

2022 年 3 月 02 日 14:293368

评论

发布
暂无评论
发现更多内容

gpu容器虚拟化:用户态和内核态的技术和实践详解

gpu容器虚拟化:用户态和内核态的技术和实践详解

网站地图