Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

ClickHouseDataSource: DisposeAsync() (and await using) does not dispose the owned ClickHouseClient/HttpClient

Open Beginner friendly
#689 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 2 days

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
78/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
clickhouse, csharp
Domain
database

Research direction

Start in ClickHouse.Driver/ADO/ClickHouseDataSource.cs around lines 123-132, where the Dispose(bool disposing) override releases the owned client and httpClient only when disposing is true. Add a DisposeAsyncCore() override that performs the same disposeClient-guarded cleanup, then run the regression test comparing Dispose() and DisposeAsync() with a tracking handler. Done when the owned HttpClient handler is disposed after DisposeAsync() and the caller's HttpClient is still left alone when disposeHttpClient is false.

Written by the indexing model from the issue text.

Description

bug
Describe the bug

ClickHouseDataSource.DisposeAsync() does not dispose the ClickHouseClient and HttpClient that the data source owns. Only the synchronous Dispose() releases them.

This affects every async disposal path:

  • await using var dataSource = new ClickHouseDataSource(...)
  • await dataSource.DisposeAsync()
  • a data source registered with AddClickHouseDataSource(...) when the container is disposed with ServiceProvider.DisposeAsync(). The ASP.NET Core / Generic Host root provider is disposed this way at shutdown.

After DisposeAsync(), the internal client is not marked disposed, and it still runs queries. The HTTP handler and its connection pool stay alive.

Root cause: ClickHouse.Driver/ADO/ClickHouseDataSource.cs:123-132 overrides only Dispose(bool disposing) and releases the owned objects only when disposing == true:

protected override void Dispose(bool disposing)
{
    base.Dispose(disposing);

    if (disposing && disposeClient)
    {
        client?.Dispose();
        httpClient?.Dispose();
    }
}

DbDataSource.DisposeAsync() calls DisposeAsyncCore() and then Dispose(disposing: false) (dotnet/runtime DbDataSource.cs):

public async ValueTask DisposeAsync()
{
    await DisposeAsyncCore().ConfigureAwait(false);

    Dispose(disposing: false);
    GC.SuppressFinalize(this);
}

protected virtual ValueTask DisposeAsyncCore()
    => default;

ClickHouseDataSource does not override DisposeAsyncCore(), and the Dispose(false) call skips the managed cleanup. So nothing disposes the owned objects. Connections from the data source do not own the client, so they do not dispose it either.

DbDataSource is different from DbConnection, DbCommand and DbDataReader here: their default DisposeAsync() calls Dispose(). So this problem is specific to the data source.

Steps to reproduce
  1. Create a ClickHouseDataSource that owns its HTTP client: the default constructor, or a custom HttpClient with disposeHttpClient: true.
  2. Dispose it with await dataSource.DisposeAsync() (or await using).
  3. Examine the HTTP handler, or run a query through dataSource.GetClient().
Expected behaviour

DisposeAsync() releases the same resources as Dispose(). After it, the owned HttpClient/handler is disposed, and the internal client throws ObjectDisposedException, as it does after Dispose().

Code example

Run on net10.0 against a live server. Throwaway NUnit tests in ClickHouse.Driver.Tests; TrackingHandler is a DelegatingHandler over SocketsHttpHandler that records Dispose. "client.disposed" is the private ClickHouseClient.disposed field, read with reflection.

// Custom HttpClient, disposeHttpClient: true
var handler = new TrackingHandler();
var ds = new ClickHouseDataSource(cs, new HttpClient(handler), disposeHttpClient: true);
await ds.DisposeAsync();
// handler.Disposed == false   (ds.Dispose() gives true)

// Default constructor (driver-created pooled handler)
var ds2 = new ClickHouseDataSource(cs);
await ds2.DisposeAsync();
await ds2.GetClient().ExecuteScalarAsync("SELECT 1");   // returns 1
// (after ds2.Dispose() the same call throws ObjectDisposedException)

// DI singleton
var sp = new ServiceCollection().AddClickHouseDataSource(cs, new HttpClient(handler)).BuildServiceProvider();
_ = sp.GetRequiredService<ClickHouseDataSource>();
await sp.DisposeAsync();
// handler.Disposed == false   (sp.Dispose() gives true)

Results:

Disposal path Owned client / handler disposed?
ds.Dispose(), custom HttpClient yes
await ds.DisposeAsync(), custom HttpClient no
await using (var ds = ...) { query }, custom HttpClient no
ds.Dispose(), default constructor yes; a later query throws ObjectDisposedException
await ds.DisposeAsync(), default constructor no; a later query returns 1
ServiceProvider.Dispose(), AddClickHouseDataSource(cs, httpClient) yes
ServiceProvider.DisposeAsync(), AddClickHouseDataSource(cs, httpClient) no
ServiceProvider.DisposeAsync(), AddClickHouseDataSource(cs) (DI creates the HttpClient) no; a later query returns 1
DisposeAsync() + Dispose(), disposeHttpClient: false not disposed, as expected (contrast case)
Error log

No error. The resources are not released, without any message.

Suggested fix

Override DisposeAsyncCore() in ClickHouseDataSource so that the async path releases the same owned objects as Dispose(true), under the same disposeClient condition. Keep the current contrast case: with disposeHttpClient: false, neither path disposes the caller's HttpClient. A regression test can compare Dispose() and DisposeAsync() with a tracking handler, and also ServiceProvider.DisposeAsync() after AddClickHouseDataSource.

Provenance: found by automated analysis while working on #686. Verified by running the tests above against a live server, not only by reading the code.

Configuration
Environment
  • Client version: main at 860fe1a (the same code is in 1.5.0)
  • Language version: C# (SDK default)
  • .NET version: .NET 10.0.203 (net10.0 target; the code path is the same on every NET7_0_OR_GREATER target)
  • OS: Linux (Docker)
ClickHouse server
  • ClickHouse Server version: 26.9.1.1629
  • ClickHouse Server non-default settings, if any: none
  • CREATE TABLE statements for tables involved: none (SELECT 1 only)
Dominant language
C#
Stars
97
Forks
24
Avg merge
2d 12h
Merged PRs (30d)
21

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from ClickHouse/clickhouse-cs

All issues in ClickHouse/clickhouse-cs

Similar issues

More C# issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.