C#
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C#
-
Section 4: Working with Data
-
Section 5: Error Handling
-
Section 6: Delegates and Events
-
Section 7: Async Programming
-
Section 8: More Language Features
-
Section 9: File I/O and Serialization
-
Section 10: Networking in .NET
-
Section 11: The .NET Ecosystem
-
Section 12: Memory and Performance
-
Section 13: Concurrency Beyond Async
-
Section 14: Reflection and Attributes
-
Section 15: Testing and Best Practices
-
Section 16: Design Patterns in C#
-
Section 17: Standard Library Deep Dive
-
Section 18: Data Structures and Algorithms in C#
-
Section 19: GUI and Desktop Development Overview
-
Section 20: Practical Projects
-
Section 21: More Practice Exercises
-
Section 22: More Standard Library and Text Processing
-
Section 23: More Design Patterns
-
Section 24: More Projects
-
Section 25: Interview Practice
-
Section 26: C# Keywords Reference (Modifiers)
-
Section 27: C# Keywords Reference (Statements)
-
Section 28: C# Keywords Reference (Operators)
-
Section 29: BCL: System.Collections.Generic
-
Section 30: BCL: System.Linq
-
Section 31: BCL: System.Threading
-
Section 32: BCL: System.IO
-
Section 33: BCL: System.Text and System.Text.Json
-
Section 34: BCL: System.Net.Http
-
Section 35: C# Language Specification Topics
-
Section 36: More Practice Exercises
-
Section 37: More Async Patterns
-
Section 38: More BCL: System.Reflection and System.Diagnostics
-
Section 39: Nullable Reference Types In Depth
-
Section 40: C# Records and Pattern Matching In Depth
-
Section 41: Dependency Injection Deep Dive
-
Section 42: More Interview and Whiteboard Practice
208: HttpClient Configuration and Lifetime
If you've been following standard C# patterns, you've likely internalized a golden rule: if a class implements IDisposable, wrap it in a using block. It feels like the responsible thing to do. You don't want memory leaks, and you certainly don't want to leave unmanaged resources hanging around. However, when it comes to HttpClient, following this rule is one of the most common ways to accidentally crash a high-traffic production server.
The "Dispose it Every Time" Trap
I've seen this countless times in code reviews. A developer writes a service to fetch data from an external API—let's say a stock price ticker—and writes it like this:
public async Task<decimal> GetStockPrice(string symbol)
{
using (var client = new HttpClient())
{
var response = await client.GetStringAsync($"https://api.stocks.com/v1/quote?s={symbol}");
return ParsePrice(response);
}
}
On the surface, this looks perfect. It's clean, it's scoped, and the client is disposed of immediately after the call. But here is the catch: disposing of the HttpClient does not immediately close the underlying TCP socket. The operating system keeps that socket in a TIME_WAIT state for a period of time to ensure any late-arriving packets are handled correctly.
If your application is under heavy load—say, processing 500 requests per second—you will spin up 500 clients and dispose of them, but those 500 sockets stay open in the OS. Very quickly, you'll hit the "socket exhaustion" limit. Your app will start throwing SocketException errors, not because the API is down, but because your own server has run out of available ports to talk to the outside world. I've spent a few late nights debugging "intermittent network failures" only to realize the code was too diligent about disposing of its clients.
Long-Lived Instances and DNS Stale-Out
The immediate fix is usually to make the HttpClient a static or singleton instance. By reusing one client for the life of the application, you reuse the same underlying sockets, and the socket exhaustion problem vanishes.
But wait—there's a second trap. If you use a single static HttpClient for the entire life of a long-running process, that client never checks for DNS changes. If the API you're calling updates its IP address (which happens often in cloud environments with blue-green deployments or failovers), your static client will keep trying to hit the old, dead IP address until you restart your entire application. It's a classic "out of the frying pan, into the fire" scenario.
Letting IHttpClientFactory Handle the Heavy Lifting
In modern .NET, we solve both of these problems using IHttpClientFactory. This is the professional way to handle lifetimes. Instead of you managing the HttpClient object, the factory manages a pool of HttpMessageHandler objects. It rotates them periodically to ensure DNS records are refreshed, but it keeps them alive long enough to prevent socket exhaustion.
You typically configure this in your Program.cs or Startup.cs. I highly recommend using "Typed Clients." This allows you to encapsulate all the configuration—like base addresses and default headers—into a specific class rather than scattering strings all over your business logic.
// Registration in Program.cs
builder.Services.AddHttpClient<StockService>(client =>
{
client.BaseAddress = new Uri("https://api.stocks.com/");
client.DefaultRequestHeaders.Add("Accept", "application/json");
client.Timeout = TimeSpan.FromSeconds(5);
});
// The actual service implementation
public class StockService
{
private readonly HttpClient _httpClient;
public StockService(HttpClient httpClient)
{
// The factory provides a pre-configured client here
_httpClient = httpClient;
}
public async Task<decimal> GetStockPrice(string symbol)
{
var response = await _httpClient.GetStringAsync($"v1/quote?s={symbol}");
return ParsePrice(response);
}
}
By doing this, the StockService doesn't care about the lifetime of the client; it just uses the one it was given. The framework handles the pooling and the rotation under the hood. It's cleaner, it's safer, and it won't wake you up at 3 AM with a socket exhaustion alert.
📋 Practical Task
Refactoring a Socket-Leaking Weather Service
You have inherited a legacy WeatherReportService that is causing intermittent connection errors in production. The current implementation creates a new HttpClient inside a foreach loop to fetch weather data for multiple cities.
Your Task: Refactor the provided code to use a Typed Client pattern. You must:
- Create a
WeatherClientclass that accepts anHttpClientin its constructor. - Move the base URL (
https://api.weather-service.net/) and the API key header (X-API-KEY: secret-key-123) into the service registration configuration. - Update the
WeatherReportServiceto use theWeatherClientinstead of manually instantiatingHttpClient.
// CURRENT LEAKY CODE
public class WeatherReportService
{
public async Task GetRegionalReport(List<string> cities)
{
foreach(var city in cities)
{
// This is the source of the socket exhaustion!
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Add("X-API-KEY", "secret-key-123");
var data = await client.GetStringAsync($"https://api.weather-service.net/weather?q={city}");
Console.WriteLine($"Weather in {city}: {data}");
}
}
}
}There are no comments for now.