Up until now, we were using strings to create a request body and also to read the content of the response. However, we can optimize our application by improving performance and memory usage with streams. So, in this article, we are going to learn how to use streams with HttpClient while sending requests and reading the content from responses. We are going to use streams with only GET and POST requests because the logic from the POST request can be applied to PUT, and PATCH.
So, let’s start.
VIDEO: Using Streams With HttpClient in .NET To Improve Performance and Memory Allocations.
More About Streams
The stream represents an abstraction of a sequence of bytes in the form of files, input/output devices, or network traffic. The Stream class in C# is an abstract class that provides methods to transfer bytes – read from or write to the source. Since we can read from or write to a stream, this enables us to skip creating variables in the middle (for the request body or response content) that can increase memory usage or decrease performance.
The vital thing to know here is that working with streams on the client side doesn’t have to do anything with the API level. This is totally a separate process. Our API may or may not work with streams but this doesn’t affect the client side. In the client application, we can use streams to prepare a request body or to read from a response regardless of the API implementation. This is an advantage for sure since we can use streams in the client apps to increase performance and decrease memory usage and still consume any API.
Using Streams with HttpClient to Fetch the Data
In the first article of this series, we have learned that while fetching the data from the API, we have to:
- Send a request to the API’s URI
- Wait for the response to arrive
- Read the content from the response body with the
ReadAsStringAsync
method - And deserialize the content using System.Text.Json
As we said, with streams, we can remove that action in the middle where we use the ReadAsStringAsync
method to read the string content from the response body.
So, let’s see how to do that.
First of all, we are going to create a new HttpClientStreamService
in the client application:
public class HttpClientStreamService : IHttpClientServiceImplementation { private static readonly HttpClient _httpClient = new HttpClient(); private readonly JsonSerializerOptions _options; public HttpClientStreamService() { _httpClient.BaseAddress = new Uri("https://localhost:5001/api/"); _httpClient.Timeout = new TimeSpan(0, 0, 30); _httpClient.DefaultRequestHeaders.Clear(); _options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; } public async Task Execute() { throw new NotImplementedException(); } }
This is a standard configuration that we’ve seen a couple of times in this series.
Next, we can create a method to send a GET request using streams:
private async Task GetCompaniesWithStream() { using (var response = await _httpClient.GetAsync("companies")) { response.EnsureSuccessStatusCode(); var stream = await response.Content.ReadAsStreamAsync(); var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options); } }
In this method, we use the GetAsync
shortcut method to fetch the data from the API. But as we have explained in the first article of this series, you can use the HttpRequestMessage
class to do the same thing with higher control over the request. Also, pay attention that this time we are wrapping our response inside the using
directive since we are working with streams now.
After we ensure the successful status code, we use the ReadAsStreamAsync
method to serialize the HTTP content and return it as a stream. With this in place, we remove the need for string serialization and crating a string variable.
As soon as we have our stream, we call the JsonSerializer.DeserializeAsync
method to read from a stream and deserialize the result into the list of company objects.
Before we start our apps, we have to call this method in the Execute
method:
public async Task Execute() { await GetCompaniesWithStream(); }
And also, register this new service in the Program
class:
private static void ConfigureServices(IServiceCollection services) { //services.AddScoped<IHttpClientServiceImplementation, HttpClientCrudService>(); //services.AddScoped<IHttpClientServiceImplementation, HttpClientPatchService>(); services.AddScoped<IHttpClientServiceImplementation, HttpClientStreamService>(); }
That’s it. We can start both apps and inspect the result:
We can see, we have our result read from a stream.
Additional Improvements with HttpCompletionMode
In the previous example, we removed a string creation action when we read the content from the response. And as such, we made an improvement. But, we can improve the solution even more by using HttpCompletionMode
. It is an enumeration having two values that control at what point the HttpClient’s actions are considered completed.
The default value is HttpCompletionMode.ResponseContentRead
. It means that the HTTP operation is complete only when the entire response is read together with content. This is the case with our previous example.
The second value is HttpCompletionMode.ResponseHeadersRead
. When we choose this option in our HTTP request, we state that the operation is complete when the response headers are fully read. At this point, the response body doesn’t have to be fully processed at all. This obviously means that we are going to use less memory because we don’t have to keep an entire content inside the memory. Also, this affects performance since we can work with the data faster.
To implement this improvement, all we have to do is to modify the GetAsync
method in the GetCompaniesWithStream
method:
private async Task GetCompaniesWithStream() { using (var response = await _httpClient.GetAsync("companies", HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); var stream = await response.Content.ReadAsStreamAsync(); var companies = await JsonSerializer.DeserializeAsync<List<CompanyDto>>(stream, _options); } }
And that’s all it takes.
If we run our application, we will see the same result as we had in a previous example. But this time, we improve our request even more.
Steve Gordon has an excellent article on this topic, so feel free to check it out to see the benchmark results as well.
Now, let’s see how to use streams with a POST request.
Sending a POST Request Using Streams with HttpClient
In our second article of the series, we have learned how to send a POST request using HttpClient. In that example, we were serializing our payload into a JSON string before we send the request. Of course, with streams, we can skip that part. Let’s see how.
First, let’s create a new method:
private async Task CreateCompanyWithStream() { var companyForCreation = new CompanyForCreationDto { Name = "Eagle IT Ltd.", Country = "USA", Address = "Eagle IT Street 289" }; var ms = new MemoryStream(); await JsonSerializer.SerializeAsync(ms, companyForCreation); ms.Seek(0, SeekOrigin.Begin); var request = new HttpRequestMessage(HttpMethod.Post, "companies"); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); using (var requestContent = new StreamContent(ms)) { request.Content = requestContent; requestContent.Headers.ContentType = new MediaTypeHeaderValue("application/json"); using (var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStreamAsync(); var createdCompany = await JsonSerializer.DeserializeAsync<CompanyDto>(content, _options); } } }
In this method, we start by creating a new companyForCreation
object with all the required properties. Then, we need a memory stream object. With the JsonSerializer.SerializeAsync
method, we serialize our companyForCreation
object into the created memory stream. Also, we use the Seek
method to set a position at the beginning of the stream. Then, we initialize a new instance of the HttpReqestMessage
object with the required arguments and set the accept header to application/json
.
After that, we create a new stream content object named requestContent
using the previously created memory stream. The StreamContent
object is going to be the content of our request so, we state that in the code, and we set up the ContentType
of our request.
Finally, we send our request using the SendAsync
method and providing the HttpCompletionOption
argument, ensure that the response is successful, and read our content as a stream. After reading the content, we just deserialize it into the createdCompany
object.
So, as you can see, through the entire method, we work with streams avoiding unnecessary memory usage with large strings. Also, we are using the ResponseHeadersRead
completion option because it makes sense while working with streams.
All we have to do now is to call this method in the Execute method:
public async Task Execute() { //await GetCompaniesWithStream(); await CreateCompanyWithStream(); }
Great.
Let’s run the app and inspect the result:
There we go. We have created a new company.
Conclusion
Using streams with HTTP requests can help us reduce memory consumption and optimize the performance of our app. In this article, we have seen how to use streams to fetch data from the server and also to create a StreamContent for our request body while sending a POST request. Additionally, we’ve learned more about completion options and how this can help us in achieving better optimization for our application.
In the next article, we are going to learn about the cancelation operations while sending HTTP requests.
Until then,
Best regards.
I have implemented this post call in my project, but im getting 415 unsupported media type. Any code changes need in server api side?
If your client app sends some custom media types with the request, you have to add support for that on your API level. Also, try providing the Content-Type header and maybe even Accept header as well. Again, I am not sure, I am just throwing suggestions here.
Maybe I’m missing something obvious, but there seems to be little difference in memory consumption if you’re simply serializing to a MemoryStream. Granted, you’ll almost certainly save memory by using a UTF-8 byte encoding (compared to a UTF-16 encoded string) but the point is that you should be serializing directly to the output stream, which actually does reduce memory usage because the JSON serializer can write a single node at a time while the HttpClient sends that output in small chunks.
Great Post, Thank you for sharing it.
I am glad you liked it. Thanks for reading it.
Hi Marinko, Excellent article. I used it in my API project (the “CreateCompanyWithStream()” method). If I have related data included using .Include statement, I get InternelServerError at response.EnsureSuccessStatusCode(); Otherwise its perfect and faster as you mentioned. I am sure there will be some settings. Can you please help me.
Hello Jacob. I would gladly help you, but I am not sure why the problem exists at first. Could you share with me the client code (stream method) and the server’s controller action maybe? Or just share some more details. I am a bit confused since you said that you used the CreateCompanyWithStream logic in your code, and then you mentioned the .Include method, which shouldn’t be related to the POST Http request. Do you want to say that you have an error with a GET request sent from the client?
Thank you for your response. I tried debugging but no clue. I created another project with the relevant items and the same is published on GitHub. Please see or download the code and see whether you can help me. github.com/jpthomas74/JsonIncludeProblem. The error is in the below code, as mentioned the same function works perfectly ok if related data is not included. You can see it in the uploded application
private async Task> GetEnumerableDataAsyncBySend(string apiEndpoint, object[] parameters)>(content, _options);
{
var ms = new MemoryStream();
await JsonSerializer.SerializeAsync(ms, parameters);
ms.Seek(0, SeekOrigin.Begin);
var request = new HttpRequestMessage(HttpMethod.Post, apiEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(“application/json”));
using (var requestContent = new StreamContent(ms))
{
request.Content = requestContent;
requestContent.Headers.ContentType = new MediaTypeHeaderValue(“application/json”);
using (var response = await _httpGatewayClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead))
{
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStreamAsync();
var data = await JsonSerializer.DeserializeAsync
return data;
}
else
{
throw new Exception($”Error getting data from {apiEndpoint}. Error status is {response.StatusCode}”);
}
}
}
}
I don’t have your database, so I can’t test the code. But let me tell right away that you are doing things wrong. First of all, your API controller:
[HttpPost]
[Route("getincluded")]
public async Task
{
var data = await _appDbContext.Customers.Include(ca=> ca.CustomerAddresses).ToListAsync();
return data;
}
[HttpPost]> GetSelectedValuesNotInclude()
[Route("getexcluded")]
public async Task
{
var data = await _appDbContext.Customers.ToListAsync();
return data;
}
This is not good. Both of these actions should be GET actions not POST, so you can modify them to this:
> GetSelectedValues()
[HttpGet("getincluded")]
public async Task
{
var data = await _appDbContext.Customers.Include(ca=> ca.CustomerAddresses).ToListAsync();
return data;
}
[HttpGet("getexcluded")]> GetSelectedValuesNotInclude()
public async Task
{
var data = await _appDbContext.Customers.ToListAsync();
return data;
}
I would strongly suggest, purchasing and reading our Ultimate ASP.NET Core Web API book. It will help you greatly to create awesome APIs using best practices. Yes, we charge for that, but if you don’t want to purchase it, then just try reading our .NET Core series, they will explain it good to you (of course, not in that much detail and not all the API functionalities).
After you modify the API, don’t use all that POST HttpClient code in your MVC app. Just prepare a GET request as it is prepared in this article:
> GetEnumerableDataAsyncBySend(string apiEndpoint, object[] parameters)
private async Task
{
using (var response = await _httpGatewayClient.GetAsync(apiEndpoint, HttpCompletionOption.ResponseHeadersRead))>(content, _options);
{
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStreamAsync();
var data = await JsonSerializer.DeserializeAsync
return data;
}
else
{
throw new Exception($"Error getting data from {apiEndpoint}. Error status is {response.StatusCode}");
}
}
}
Again, I didn’t test this, but I’ve tested my project this morning with complex objects and it works without a single problem. So this should be working as well.
Hi Marinko, Database is attached as MsCustomers.dacpac in the root folder itself. I will go through your suggestions and revert. Thankyou very much for the efforts. I would like to buy the book. Can you please send me the link, I was not able to get through Ultimate ASP.NET Core Web API book. In the actual code, I have to check for a number of parameters before passing the data. How will I pass my object[] as a parameter to GetAsync
This is the book link https://code-maze.com/ultimate-aspnet-core-web-api/?source=nav . Regarding your question, if you want to send an array as a parameter with HttpClient to the Web API, you can place it as a query string params in the URI with the QueryHelper class. You can read our Paging with Blazor WASM article: https://code-maze.com/blazor-webassembly-pagination/ to see how I did it there. Also you can use ModelBinding on the server side with API, but that is explained in the book.
I tried the code you send still same error. I have registered myself and is in the process of getting the premium edition. I added two more files to github a bacpac file and a bak file. This is the database. Please download and see whether you can solve it. Please see the image above
I tried the code you send still same error. I have registered myself and is in the process of getting the premium edition. I added two more files to github a bacpac file and a bak file. This is the database. Please download and see whether you can solve it. https://uploads.disquscdn.com/images/349595c7614efb08448254cfd653baca32eb74336ac1e097e8969472a6e03df0.jpg https://uploads.disquscdn.com/images/2480f817eedc731602c8578a1dea3759efae8b458aa4922691fdae89f34f826f.jpg
It seems that your API is throwing an error. I usually don’t do this, since it is taking my time, but this time, I will try it.
OK Marinko. Thankyou for your efforts. I will send it. Because your code gave me much performance improvement and its the best, if this can be handled. A file is created and is available in https://github.com/jpthomas74/JsonIncludeProblem/blob/main/FilesToMarinko
Hello Jacob. I solved it out. You are having a self reference loop problem in your Web API. This is also something that we solved in our book. Basically since your child model has a reference towards the parent model and the parent model has the same, JSON serializer can’t handle that well. To solve this out, you can either install Microsoft.AspNetCore.Mvc.NewtonsoftJson library in the Web API project and then add this code in the Startup:
services.AddControllers()
.AddNewtonsoftJson(o => o.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);
This solved the issue, but this is something I don’t recommend since now Newtonsoft is taking over the serialization actions over the Json.Text, which is faster and better optimized.
The second solution is to never work with Models when accepting parameters from the client or when returning responses (We elaborated on that in our book). So, you should create DTOs and map results to these classes (records).
So, there you go, as I said in one of my previous answers, it was a Web API issue.
Please could you place the script file (for table creation and data seed) inside the github folder. I can’t restore your backup since my SQL Server version is lower than yours.
Trying to use this in my Blazor Wasm application. The .ReadAsStreamAsync() in the Get method seems to fail. Unfortunately, it doesn’t give an exception in VS, since it’s Wasm, so I can’t see why exactly it’s failing. Any ideas?
Hi Elyl. Well, just today, I’ve written an example with HttpClient in Blazor WASM using Streams and it works without a single problem. I am really not sure why you have that problem, and it is pretty hard for me to help you since I have no idea what the problem is.
Yeah, Wasm is a pain. I am stepping through the code and it gets to var stream = await response.Content.ReadAsStreamAsync(); then exits out of the using() section without raising an exception. Console output in the browser is just complaining about non-rendered components (because the httpclient errored out, nothing useful as to what the actual issue was).
The Post example works perfectly in my code, no issues there.
Do I need to put some kind of special annotation in my ControllerBase API to note that the Get call can be streamed? Or is that completely independent from the HttpClient using ReadAsStream?
As I stated in the article, working with streams in the client app has nothing to do with the API. So, there is nothing you have to do with the API side. Just to make sure that the error is related to using streams, have you tried using ReadAsString instead?