How to Improve Performance and Memory Efficiency When Accessing APIs with Stream Using HTTP Client in .NET

Rajat Awasthi
7 min readSep 16, 2023

--

Introduction

In today’s digital world, efficient API consumption is crucial for modern software applications. APIs serve as the backbone of data exchange between systems, and how efficiently applications interact with them impacts performance and user satisfaction. Slow API calls can lead to sluggish user experiences, increased resource usage, and hinder scalability. Efficient API consumption enhances user experiences, optimizes resource utilization, supports scalability, ensures reliability, and provides a competitive edge. In this article, we’ll explore how to optimize API consumption in .NET, focusing on Streams and the HTTP Client to improve performance and memory usage, equipping you to create efficient, competitive, and responsive applications.

What is Stream?

Streams are a fundamental concept in computer programming, and they are crucial for efficient data processing. Essentially, a Stream is a continuous flow of data that can be read from or written to incrementally, rather than loading all the data into memory at once.

Streams are like the magic wand of efficient data handling in software. They are the secret sauce for keeping your application lean and mean in terms of memory usage while turbocharging its performance. In this article, we’ll demystify Streams and uncover how they can supercharge your applications, especially when working with the HTTP Client.

Think of a Stream as a versatile data conduit that abstracts the complexities of dealing with various data sources. Whether it’s files, input/output devices, or data flowing over the internet, Streams provide a universal language for your code to interact with them. The beauty of Streams lies in their ability to shield you from the nitty-gritty technical details of how these data sources work under the hood.

In simpler terms, Streams are your reliable bridge to connect with all sorts of data, without needing to become an expert in each data source’s peculiarities. They offer a standardized way to read and write data, making your code more elegant and efficient.

In this article, we’ll take a deep dive into Streams and uncover the secrets they hold for optimizing your application’s performance, especially when dealing with HTTP Client requests. We’ll learn how to harness the power of Streams to read and send data, unlocking a whole new level of performance improvements for your software.

Clarifying Streams

Let’s clarify something crucial before we dive deeper: the term “Stream” can have a dual meaning, and it’s essential to differentiate between the two.

First, we have the coding concept of a Stream, which is a powerful abstraction provided by the Stream class in programming. It’s a way to handle sequences of bytes in a versatile and unified manner. This Stream concept applies to various levels of coding, including clients, APIs, and intermediary stages. Importantly, these levels are distinct; using Streams in APIs doesn’t necessarily require clients to use them, and vice versa. The knowledge you gain in this article is universally applicable, regardless of how APIs are coded.

Now, let’s shift our focus to the context of data transfer. Here, “Stream” also refers to the movement of data. This is where the potential for confusion arises. Since we’re working with HTTP, data transfer involves the transmission of bytes to and from the API. Because HTTP operates over TCP, data is sent in packets, each with a maximum size i.e. 64KB, often relatively small. Whether you’re streaming movies, browsing a website, or interacting with an HTTP-based API, data arrives sequentially in these packets.

But the confusion doesn’t stop there. APIs can manage data streaming in different ways. They might send all the data at once or stream it asynchronously, allowing for gradual transmission. With asynchronous streaming support, the API can send response portions individually over the network as a continuous flow. This can be especially useful when data is coming directly from an underlying database. Importantly, this distinction in data streaming doesn’t directly involve client-side stream support.

So, in this article, we’re primarily focusing on the coding concept of Streams, which is a powerful tool for handling data efficiently at various levels of your software, and how it relates to the HTTP Client in enhancing your application’s performance. Keep in mind that while data streaming is a significant part of web interactions, it’s not the primary focus of our discussion here.

Reading Data From API

Let’s take a closer look at what happens when we make an API call, with our initial focus on data retrieval. When we send a request, such as requesting data from the API, we eventually receive a response. Due to the nature of HTTP, this response arrives as a continuous stream of data over the network, regardless of whether the client or API is specifically designed for streaming or supports it.

Once all the data has been transmitted, we can then extract it from the response.Content object or similar constructs provided by the HTTP client. This is where we gain access to the actual content of the response, allowing us to work with the data in our application. Whether it's a small JSON response or a large file download, the content is received in a flowing fashion, and our code needs to be equipped to handle it efficiently, especially when dealing with potentially large datasets.


using var request = new HttpRequestMessage(HttpMethod.Get, "getData");

var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);

var responseString = await response.Content.ReadAsStringAsync();

var result = JsonSerializer.Deserialize<IEnumerable<ResponseModel>>(responseString);

Many developers often use the ReadAsStringAsync method, which reads the entire data response body into an in-memory string. Afterward, this string is transformed through deserialization into a data object using System.Text.Json or NewtonSoft.Json. While this is the current practice, there are several opportunities for improvement.

Let’s start with the creation of an intermediary string. Instead of deserializing the object from a string, we can take a more efficient approach by using a stream. This method eliminates the need to generate a potentially large intermediary string entirely. There’s no requirement to create the entire string in memory before processing it. Consequently, this approach significantly reduces memory usage and can greatly enhance the performance of your application.

Now we’ll explore how to implement this improvement, making your data retrieval and processing more efficient and resource-friendly.

using var request = new HttpRequestMessage(HttpMethod.Get, "getData");

using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);

using var responseStream = await response.Content.ReadAsStreamAsync();

var result = await JsonSerializer.DeserializeAsync<IEnumerable<ResponseModel>>(responseStream);

Send Data From API

When we send data to an API, the process is somewhat akin to what happens when we receive a response but in reverse. Here, we start by creating a data object, which is then transformed into a JSON string through a process called serialization. This JSON string is temporarily stored as an in-memory string before being transmitted as a whole to the API.

However, there’s room for improvement in this procedure. We can make it more efficient and resource-friendly by avoiding the creation of a large in-memory string for the entire payload. In other words, rather than preparing the entire payload as a single string, we can find more efficient ways to transmit the data to the API. This optimization can not only enhance performance but also reduce memory usage during data transmission. Here we’ll explore strategies to achieve this improvement.

var clientCommand = new CreateClientCommand();
var serializedClientForCreation = JsonSerializer.Serialize(clientCommand);
using var request = new HttpRequestMessage(HttpMethod.Post, "/api/clients")
{
Content = new StringContent(serializedClientForCreation, Encoding.Unicode, MediaTypeNames.Application.Json)
};
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);

response.EnsureSuccessStatusCode();

Instead of going through the process of converting data into a JSON string through serialization, we can take a more efficient approach. This involves using a stream to skip the creation of an in-memory string altogether. Furthermore, when it comes to sending content over the network, we can leverage a stream to avoid the necessity of sending the entire payload all at once.

To put this into action, we can make use of StreamContent instead of StringContent. By doing so, we enable the transmission of data in a more streamlined and resource-efficient manner. In the following sections, we'll delve into a practical demonstration to showcase how this approach works, illustrating the benefits of using streams for both data serialization and transmission.

var clientCommand = new CreateClientCommand();

using (var memoryContentStream = new MemoryStream())
{
await JsonSerializer.SerializeAsync(memoryContentStream, clientCommand);

memoryContentStream.Seek(0, SeekOrigin.Begin);

using var request = new HttpRequestMessage(HttpMethod.Post, "/api/clients");
using var streamContent = new StreamContent(memoryContentStream);
streamContent.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Application.Json);
request.Content = streamContent;

var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);

response.EnsureSuccessStatusCode();
}

Summary

In this article, we’ve delved into the art of enhancing performance and optimizing memory usage when interacting with APIs via the HTTP Client in .NET. We’ve harnessed the power of Streams to achieve these goals, leveraging their inherent advantages in the context of HTTP Client operations.

Our journey has taken us through the world of Streams, where we’ve explored their versatile capabilities and how they can be seamlessly integrated with the HTTP Client. We’ve witnessed the benefits of reading and sending data using Streams, unlocking significant performance improvements, and substantially reducing memory usage. These optimizations not only make our applications faster but also more resource-efficient, offering a win-win solution for developers seeking to make their software leaner and more responsive when dealing with APIs.

Additional Resources

To know more about using stream with HttpClient you can go to Microsoft.Learn

--

--