Properly Disposing HttpContent When Using HttpClient in C#

By FoxLearn 3/11/2025 8:27:15 AM   16
In versions prior to .NET Core 3.0 (including .NET Framework), database connection pooling was automatically managed by the framework, with the connection object being disposed of at the end of each operation.

While this might sound convenient, it introduced several issues. A notable problem was that once a connection was disposed of, it couldn’t be reused, leading to unnecessary overhead when attempting to retry database operations or reuse the connection.

There are various scenarios where you might want to reuse a database connection. For instance, when implementing a retry mechanism for transient errors, you may want to avoid the cost of re-establishing a connection for every attempt.

The .NET team recognized this behavior as problematic, and it was fixed in .NET Core 3.0. With this fix, you are now responsible for disposing of the connection objects when it makes sense for your particular use case.

In this example, we’ll perform a database operation (like an insert) with retry attempts, using Polly for the retry logic.

Before .NET Core 3.0 (including .NET Framework)

In this version, the automatic disposal behavior meant that a new connection had to be created with each retry, even if the previous connection hadn’t fully been disposed of yet. Here's how you might implement retries in that case:

using Polly;
using System.Data.SqlClient;

var retryPolicy = Policy.Handle<SqlException>()
    .WaitAndRetryAsync(retryCount: 3, sleepDurationProvider: _ => TimeSpan.FromSeconds(5));

return await retryPolicy.ExecuteAsync(async () =>
{
    using (var connection = new SqlConnection("YourConnectionString"))
    {
        await connection.OpenAsync();
        
        var command = new SqlCommand("INSERT INTO YourTable (Column) VALUES (@value)", connection);
        command.Parameters.AddWithValue("@value", "SomeData");

        await command.ExecuteNonQueryAsync();
    }
});

Here, a new SqlConnection object is created for each attempt, even though we’re retrying the same operation. This behavior is due to the automatic disposal of the connection after each attempt, meaning you cannot reuse it.

After .NET Core 3.0 and Beyond

Once the automatic disposal issue was resolved, we could now reuse the database connection across retries, which makes the retry logic more efficient.

using Polly;
using System.Data.SqlClient;

var retryPolicy = Policy.Handle<SqlException>(ex => ex.Number == 1205) // SQL Server deadlock error
    .WaitAndRetryAsync(retryCount: 3, sleepDurationProvider: _ => TimeSpan.FromSeconds(5));

using (var connection = new SqlConnection("YourConnectionString"))
{
    await connection.OpenAsync();

    var command = new SqlCommand("INSERT INTO YourTable (Column) VALUES (@value)", connection);
    command.Parameters.AddWithValue("@value", "SomeData");

    return await retryPolicy.ExecuteAsync(async () =>
    {
        Console.WriteLine("Executing command");
        await command.ExecuteNonQueryAsync();
        return "Operation successful";
    });
}

With this approach, the connection is created once, reused across retries, and then disposed of at the end (via the using statement). This separation allows for better resource management and efficiency, as the connection is no longer unnecessarily recreated.

This change in behavior offers a lot more flexibility, allowing for more efficient handling of database operations, especially in scenarios where retrying the same task is common. You can now manage database connections more precisely, making your code more performant and cleaner by only disposing of the connection when appropriate for your use case.