laitimes

How to create and use the HTTP Client SDK in .NET 6

Author | Oleksii Nikiforov

Translated by | Hirakawa

Planning | Ding Xiaoyun

Today, cloud-based, microservices, or IoT-based applications often rely on communicating with other systems over the network. Each service runs in its own process and solves a limited set of problems. Communication between services is based on a lightweight mechanism, typically an HTTP resource API.

From a .NET developer's perspective, we want to provide a consistent, manageable way to integrate specific services in the form of distributable packages. The best approach is to make the service integration code we develop available as a NuGet package and share it with others, teams, and even organizations. In this article I will share in . All aspects of creating and using the HTTP Client SDK in NET 6.

The client SDK provides a meaningful layer of abstraction on top of the remote service. Essentially, it allows for remote procedure calls (RPCs). The client SDK's responsibilities are to serialize some data, send it to the remote destination, and deserialize the received data and process the response.

The HTTP Client SDK is used with the API:

Accelerate the API integration process;

Provide a consistent, standardized approach;

Gives the service owner partial control over how apIs are consumed.

1

Write an HTTP client SDK

In this article, we'll write a complete Dad Jokes API client to provide Dad Jokes; let's play with it. The source code is on GitHub.

When developing a client SDK for use with an API, it's a good idea to start with the interface contract (between the API and the SDK):

The contract is created based on the API you want to integrate. I generally recommend developing generic APIs that follow the robustness principle and the minimal surprise principle. But if you want to modify and transform the data contract according to your own needs, it is also completely fine, just from the perspective of the consumer. HttpClient is the foundation for HTTP-based integration. It contains everything you need to work with HTTP abstractions.

Usually, HTTP APIs use JSON, which is why from . Starting with NET 5, BCL added the System.Net.Http.Json namespace. It provides many extensions for HttpClient and HttpContent, allowing us to serialize and deserialize using System.Text.Json. If there are no complex special needs, I recommend using System.Net.Http.Json because it saves you from writing template code. Not only is that boring, but it's also hard to be efficient and bug-free. I recommend reading Steves Gordon's blog post "Sending and Receiving JSON Using HttpClient":

Tip: You can create some centralized places to manage endpoint URLs, like this:

Tip: If you need to deal with complex URIs, use Flurl. It provides a smooth URL-building experience:

Next, we must specify the required header files (and other required configurations). We want to provide a flexible mechanism to configure HttpClient as part of the SDK. In this case, we need to provide the certificate in the custom header and specify a well-known "Accept". Tip: Expose high-level building blocks to HttpClientExtensions. This makes it easier to discover API-specific configurations. For example, if you have a custom authorization mechanism, the SDK should provide support (at a minimum of documentation).

Client lifecycle

In order to build The DadJokesApiClient, we need to create an HttpClient. As you know, HttpClient implements IDisposable because it has an unmanaged underlying resource , TCP connections. The number of concurrent TCP connections that are open at the same time on a single machine is limited. This consideration also raises an important question – "Should I create HttpClient every time I need it, or only once when the application starts?" ”

HttpClient is a shared object. This means that, at the bottom level, it is reentrant and thread-safe. Instead of creating a new HttpClient instance at each execution, share an HttpClient instance. However, this approach also has a range of problems. For example, a client will keep the connection open for the lifetime of the application, it will not respect the DNS TTL settings, and it will never receive DNS updates. So it's not a perfect solution either.

You need to manage a pool of TCP connections that destroy connections from time to time to get DNS updates. That's exactly what the HttpClientFactory does. The official documentation describes the HttpClientFactory as "a factory dedicated to creating httpClient instances that can be used in applications." We'll cover how to use it later.

Each time an HttpClient object is fetched from the IHttpClientFactory, a new instance is returned. However, each HttpClient uses an HttpMessageHandler that is pooled and reused by the IHttpClientFactory, reducing resource consumption. The pooling of handlers is worth it because typically each handler manages its underlying HTTP connections. Some handlers also keep the connection open indefinitely, preventing the handler from reacting to changes in DNS. HttpMessageHandler has a limited lifecycle.

Below, we take a look at how the HttpClientFactory works when using HttpClient managed by Dependency Injection (DI).

How to create and use the HTTP Client SDK in .NET 6

2

Consuming API clients

In our case, a basic scenario for consuming APIs is a console application that injects a container without dependencies. The goal here is to give consumers the fastest way to access existing APIs.

Create a static factory method to create an API client.

In this way, we can use IDadJokesApiClient from a console application:

Consuming API client: HttpClientFactory

The next step is to configure HttpClient as part of the dependency injection container. There's a lot of good stuff on the Web, and I won't discuss it in detail. Steve Gordon also has a very good article titled "HttpClientFactory in ASP.NET Core."

In order to add a pooled HttpClient instance using DI, you need to use IServiceCollection.AddHttpClient from Microsoft.Extensions.Http.

Provides a custom extension method for adding typed HttpClient to DI.

Use the extension method as follows:

As you can see, IHttpClientFactory can be used outside of ASP.NET Core. For example, console applications, workers, lambdas, and so on. Let's see how it works:

Interestingly, clients created by DI automatically log requests made, making both development and troubleshooting very easy.

If you manipulate the format of the log template and add SourceContext and EventId, you'll see that the HttpClientFactory adds an additional handler on its own. This is useful when you're trying to troubleshoot issues related to HTTP request handling.

The most common scenario is a Web application. Here it is. NET 6 MinimalAPI example:

How to create and use the HTTP Client SDK in .NET 6

3

Extend the HTTP Client SDK to add cross-cutting concerns through DelegatingHandler

HttpClient also provides an extensibility point: a message handler. It is a class that receives an HTTP request and returns an HTTP response. There are many issues that can be expressed as cross-cutting concerns. For example, logs, authentication, caching, header forwarding, auditing, and so on. Aspect-oriented programming is designed to encapsulate cross-cutting concerns into aspects to maintain modularity. Typically, a series of message handlers are chained together. The first handler receives an HTTP request, does some processing, and then passes the request over to the next handler. Sometimes, the response is created and returns upstream of the chain.

How to create and use the HTTP Client SDK in .NET 6

Task: Suppose you need to copy a series of headers from ASP.NET Core's HttpContext and pass them to all outgoing requests made by the Dad Jokes API client.

We want to "insert" a DelegatingHandler into the HttpClient request pipeline. For non-IttpClientFactory scenarios, we want the client to be able to specify a DelegatingHandler list to build an underlying chain for HttpClient.

This way, in the absence of a DI container, The DadJokesApiClient can be extended like this:

On the other hand, in the DI container scenario, we want to provide a secondary extension method to easily insert TheHeadPropagationMessageHandler using IHttpClientBuilder.Add HttpMessageHandler.

An example of an extended MinimalAPI is as follows:

Sometimes, features like this are reused by other services. You might want to go a step further and extract all your shared code into a public NuGet package and use it in the HTTP Client SDK.

Third-party extensions

We can write our own message handlers, but the .NET OSS community also provides many useful NuGet packages. Here are my favorites.

Elastic patterns – retry, caching, fallback, etc.: Many times, in a world where the system is unreliable, you need to ensure high availability by adding some resiliency policies. Fortunately, we have a built-in solution that can be used in . Strategy is built and defined in NET, and that's Polly. Polly provides out-of-the-box integration with the IHttpClientFactory. It uses a convenient method IHttpClientBuilder.AddTransientHttpErrorPolicy. It configures a policy to handle typical errors for HTTP calls: HttpRequestExceptionHTTP 5XX status code (server error), HTTP 408 status code (request timeout).

For example, you can use retry and circuit breaker modes to proactively handle transient errors. Typically, when downstream services are expected to self-correct, we use retry mode. The wait time between retries is a stable window for downstream services to recover. Retries often use an exponential backoff algorithm. This sounds good on paper, but in real-world scenarios, the use of retry mode may be excessive. Additional retries can result in additional loads or spikes. In the worst case scenario, the caller's resources can be exhausted or over-blocked, waiting for a never-to-be-come recovery, resulting in cascading failures upstream. That's when circuit breaker mode comes into play. It detects the level of failure and blocks calls to downstream services when a failure exceeds a threshold. This model can be used if there is no chance of success, for example, when a subsystem is completely offline or overwhelmed. The idea of a circuit breaker is very simple, although you might build something more complicated based on it. When a failure exceeds the threshold, the call breaks, so instead of handling the request, we practice the fast failure method, throwing an exception immediately.

Polly is really powerful, and it provides a way to combine elastic strategies, see PolicyWrap.

Here's a classification of policies that might be useful to you:

How to create and use the HTTP Client SDK in .NET 6

Designing a reliable system can be a very challenging task, and I recommend you research this yourself. Here's a good introduction to ——.net Microservices Architecture eBook: Implementing Elastic Applications. (https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/)

Authentication in OAuth2/OIDC: If you need to manage user and client access tokens, I recommend using IdentityModel.AspNetCore. It can help you acquire, cache, and rotate tokens, see the documentation for details. (https://identitymodel.readthedocs.io/en/latest/aspnetcore/overview.html)

4

Test the HTTP Client SDK

At this point, you should already be familiar with designing and writing the HTTP client SDK. All that's left to do is write some tests to make sure it behaves as expected. Note that it might be nice to skip extensive unit testing and write more integrations or e2E to ensure that the integrations are correct. Now, I'll show you how to unit test DadJokesApiClient.

As mentioned earlier, HttpClient is extensible. In addition, we can replace the standard HttpMessageHandler with a beta version. In this way, we can use the impersonation service instead of sending the actual request over the network. This technique offers a lot of possibilities because we can simulate a variety of HttpClient behaviors that would normally be difficult to reproduce.

We define a reusable method for creating an HttpClient mock and passing it as a dependency to DadJokesApiClient.

From this point of view, unit testing is a very simple process:

Using HttpClient is the most flexible method. You have full control over the integration with the API. However, there is also a drawback, you need to write a lot of boilerplate code. In some cases, the APIs you're integrating don't matter, so you don't need all the features that HttpClient, HttpRequestMessage, httpResponseMessage have to offer. merit:

You have full control over behavior and data contracts. You can even write a "smart" API client, and in special cases, you can move some logic into the SDK if needed. For example, you can throw custom exceptions, transform requests and responses, provide default header information, and so on.

You have full control over the serialization and deserialization process.

Easy to debug and troubleshoot. The stack is easy to trace, and you can always start the debugger to see what's going on in the background.

shortcoming:

A lot of repetitive code needs to be written.

Someone needs to maintain the code base in case of API changes and bugs. It's a tedious, error-prone process.

5

Write the HTTP client SDK using declarative methods

The less code, the fewer bugs. Refit is a . NET's, automated, type-safe REST library. It turns the REST API into a ready-to-use interface. Refit uses System.Text.Json as the JSON serializer by default.

Each method must have an HTTP property that provides the request method and the corresponding URL.

Refit generates types that implement the IDadJokesApiClient interface based on information provided by Refit.HttpMethodAttribute.

Consuming API client: Refit

The method is the same as the usual HttpClient integration method, but instead of building a client manually, we use the static method provided by Refit.

For the DI container scenario, we can use the Refit.HttpClientFactoryExtensions.AddRefitClient extension method.

The usage is as follows:

Note that since the resulting client's contract should match the underlying data contract, we no longer control the translation of the contract, and this responsibility is entrusted to the consumer. Let's see how the above code works in practice. The output of the MinimalAPI sample is different because I include the Serilog log.

Similarly, this approach has its advantages and disadvantages: advantages:

Makes it easy to use and develop API clients.

Highly configurable. Can be very flexible in getting things done.

No additional unit tests are required.

Troubleshooting is difficult. Sometimes it's hard to understand how the generated code works. For example, there is a mismatch on the configuration.

Other members of the team are required to understand how to read and write code developed using Refit.

For medium/large APIs, there is still some time consumption. Interested readers can also learn about RestEase.

6

Use automation methods to write the HTTP client SDK

There is a way to generate the HTTP Client SDK completely automatically. The OpenAPI/Swagger specification uses JSON and JSON Schema to describe the RESTful Web API. The NSwag project provides tools to generate client-side code from these OpenAPI specifications. Everything can be automated through the CLI (distributed through NuGet tools, build targets, or NPM).

The Dad Jokes API doesn't provide an OpenAPI, so I wrote one by hand. Fortunately, it's easy:

Now, we want to automatically generate the HTTP Client SDK. Let's take the help of NSwagStudio. The generated IDadJokesApiClient looks like this (for brevity, xml comments removed):

How to create and use the HTTP Client SDK in .NET 6

Again, we want to provide the registration of typed clients as an extension method.

Let's run it and enjoy the last joke of this article:

merit:

Based on well-known specifications.

There are rich tools and active community support.

Fully automated, the new SDK can be generated as part of the CI/CD process every time the OpenAPI specification changes.

You can build SDKs in multiple languages.

Since you can see the code generated by the toolchain, it is relatively easy to troubleshoot.

If it does not conform to the OpenAPI specification, it cannot be used.

It is difficult to customize and control the contracts of the generated API clients. Interested readers can also learn about AutoRest and Visual Studio Connected Services.

7

Choose the appropriate method

In this article, we learned three different ways to build sdk clients. In simple terms, you can choose the correct method by following these rules:

I am a simple person. I want full control over my HTTP client integration. Use manual methods.

I'm a busy person, but I still want to have some control. Use declarative methods.

I'm a slacker. Better do it for me. Use automated methods.

The decision diagram is as follows:

How to create and use the HTTP Client SDK in .NET 6

8

summary

In this article, we review the different ways of developing the HTTP client SDK. Please choose the right approach based on your specific use case and requirements, and hopefully this article will give you a general understanding so that you can make the best design decisions when designing the client SDK. Thanks for reading.

About the Author:

Oleksii Nikiforov is a Senior Software Engineer and Team Leader at EPAM Systems. He holds a bachelor's degree in applied mathematics and a master's degree in information technology and has been engaged in software development for over 6 years and is passionate. NET, Distributed Systems, and Productivity, is the author of the N+1 blog.

https://www.infoq.com/articles/creating-http-sdks-dotnet-6/

Read on