Introduction: why a proper HTTP client in C# is important
In modern C# (.NET) development, working with HTTP requests is a basic necessity. Whether it's calling a REST API, downloading data from a web server, or integrating with cloud services — a reliable HTTP client is essential. However, many developers, especially beginners, make typical mistakes: improper connection management, ignoring timeouts, or inefficient response handling. In this article, we will break down creating an HTTP client in C# from the basics to advanced techniques.
We will look at the HttpClient class — the primary tool for working with HTTP in .NET, as well as alternative approaches. You will learn how to properly send GET and POST requests, handle JSON, manage headers, and avoid common performance issues.
Basics: creating and configuring HttpClient
Proper initialization of HttpClient
The HttpClient class is designed for reuse. Creating a new instance for each request is an anti-pattern that can lead to socket exhaustion. Instead, use a single instance for the entire application or dependency injection (DI) via IHttpClientFactory.
// Incorrect: creating for each requestpublic async Task<string> BadRequest(string url){ using (var client = new HttpClient()) { return await client.GetStringAsync(url); }}
// Correct: static instancepublic static readonly HttpClient SharedClient = new HttpClient();
// Or via IHttpClientFactory (recommended for ASP.NET Core)services.AddHttpClient("MyClient", client =>{ client.BaseAddress = new Uri("https://api.example.com"); client.DefaultRequestHeaders.Add("Accept", "application/json");});Configuring timeouts and base address
Timeouts protect the application from hanging when the server is unavailable. HttpClient has its own timeout, but it can be overridden.
HttpClient client = new HttpClient{ BaseAddress = new Uri("https://jsonplaceholder.typicode.com"), Timeout = TimeSpan.FromSeconds(10) // 10 seconds for the entire request};It is also useful to set MaxResponseContentBufferSize to limit the size of received data.
Sending requests: GET, POST, PUT, and DELETE
GET request: retrieving data
The simplest way to get data is GetStringAsync or GetAsync with manual handling.
public async Task<string> GetUsersAsync(){ HttpResponseMessage response = await SharedClient.GetAsync("/users"); response.EnsureSuccessStatusCode(); // throws an exception on error return await response.Content.ReadAsStringAsync();}
// Or even simpler:string json = await SharedClient.GetStringAsync("/users");POST request: sending data
To send JSON, use PostAsJsonAsync (requires the System.Net.Http.Json package).
var newUser = new { name = "Ivan", email = "ivan@example.com" };HttpResponseMessage response = await SharedClient.PostAsJsonAsync("/users", newUser);if (response.IsSuccessStatusCode){ var createdUser = await response.Content.ReadFromJsonAsync<User>();}To send a form, use FormUrlEncodedContent.
var formData = new Dictionary<string, string>{ { "username", "admin" }, { "password", "secret" }};var content = new FormUrlEncodedContent(formData);HttpResponseMessage response = await SharedClient.PostAsync("/login", content);PUT and DELETE requests
Updating and deleting are done similarly:
// PUTvar updateData = new { id = 1, name = "Updated name" };HttpResponseMessage putR