ClickHouse C# client
The official C# client for connecting to ClickHouse. The client source code is available in the GitHub repository. Originally developed by Oleg V. Kozlyuk.
The library provides two main APIs:
-
ClickHouseClient(recommended): A high-level, thread-safe client designed for singleton use. Provides a simple async API for queries and bulk inserts. Best for most applications. -
ADO.NET (
ClickHouseConnection,ClickHouseCommand): Standard .NET database abstractions. Required for ORM integration (Dapper, Linq2db) and when you need ADO.NET compatibility.
Both APIs share the same underlying HTTP connection pool and can be used together in the same application.
Migration guide
- Update your
.csprojfile with the new package nameClickHouse.Driverand the latest version on NuGet. - Update all
ClickHouse.Clientreferences toClickHouse.Driverin your codebase.
Supported .NET versions
ClickHouse.Driver supports the following .NET versions:
- .NET Framework 4.6.2
- .NET Framework 4.8
- .NET Standard 2.1
- .NET 6.0
- .NET 8.0
- .NET 9.0
- .NET 10.0
Installation
Install the package from NuGet:
Or using the NuGet Package Manager:
Quick start
Configuration
There are two ways of configuring your connection to ClickHouse:
- Connection string: Semicolon-separated key/value pairs that specify the host, authentication credentials, and other connection options.
ClickHouseClientSettingsobject: A strongly typed configuration object that can be loaded from configuration files or set in code.
Below is a full list of all the settings, their default values, and their effects.
Connection settings
| Property | Type | Default | Connection String Key | Description |
|---|---|---|---|---|
| Host | string | "localhost" | Host | Hostname or IP address of the ClickHouse server |
| Port | ushort | 8123 (HTTP) / 8443 (HTTPS) | Port | Port number; defaults based on protocol |
| Username | string | "default" | Username | Authentication username |
| Password | string | "" | Password | Authentication password |
| Database | string | "" | Database | Default database; empty uses server/user default |
| Protocol | string | "http" | Protocol | Connection protocol: "http" or "https" |
| Path | string | null | Path | URL path for reverse proxy scenarios (e.g., /clickhouse) |
| Timeout | TimeSpan | 2 minutes | Timeout | Operation timeout (stored as seconds in connection string) |
Data format & serialization
| Property | Type | Default | Connection String Key | Description |
|---|---|---|---|---|
| UseCompression | bool | true | Compression | Enable gzip compression for data transfer |
| UseCustomDecimals | bool | true | UseCustomDecimals | Use ClickHouseDecimal for arbitrary precision; if false, uses .NET decimal (128-bit limit) |
| ReadStringsAsByteArrays | bool | false | ReadStringsAsByteArrays | Read String and FixedString columns as byte[] instead of string; useful for binary data |
| UseFormDataParameters | bool | false | UseFormDataParameters | Send parameters as form data instead of URL query string |
| JsonReadMode | JsonReadMode | Binary | JsonReadMode | How JSON data is returned: Binary (returns JsonObject) or String (returns raw JSON string) |
| JsonWriteMode | JsonWriteMode | String | JsonWriteMode | How JSON data is sent: String (serializes via JsonSerializer, accepts all inputs) or Binary (registered POCOs only with type hints) |
Session management
| Property | Type | Default | Connection String Key | Description |
|---|---|---|---|---|
| UseSession | bool | false | UseSession | Enable stateful sessions; serializes requests |
| SessionId | string | null | SessionId | Session ID; auto-generates GUID if null and UseSession is true |
The UseSession flag enables persistence of the server session, allowing use of SET statements and temporary tables. Sessions will be reset after 60 seconds of inactivity (default timeout). Session lifetime can be extended by setting session settings via ClickHouse statements or the server configuration.
The ClickHouseConnection class normally allows for parallel operation (multiple threads can run queries concurrently). However, enabling UseSession flag will limit that to one active query per connection at any moment of time (this is a server-side limitation).
Security
| Property | Type | Default | Connection String Key | Description |
|---|---|---|---|---|
| SkipServerCertificateValidation | bool | false | — | Skip HTTPS certificate validation; not for production use |
HTTP client configuration
| Property | Type | Default | Connection String Key | Description |
|---|---|---|---|---|
| HttpClient | HttpClient | null | — | Custom pre-configured HttpClient instance |
| HttpClientFactory | IHttpClientFactory | null | — | Custom factory for creating HttpClient instances |
| HttpClientName | string | null | — | Name for HttpClientFactory to create specific client |
Logging & debugging
| Property | Type | Default | Connection String Key | Description |
|---|---|---|---|---|
| LoggerFactory | ILoggerFactory | null | — | Logger factory for diagnostic logging |
| EnableDebugMode | bool | false | — | Enable .NET network tracing (requires LoggerFactory with level set to Trace); significant performance impact |
Custom settings & roles
| Property | Type | Default | Connection String Key | Description |
|---|---|---|---|---|
| CustomSettings | IDictionary<string, object> | Empty | set_* prefix | ClickHouse server settings, see note below |
| Roles | IReadOnlyList<string> | Empty | Roles | Comma-separated ClickHouse roles (e.g., Roles=admin,reader) |
When using a connection string to set custom settings, use the set_ prefix, e.g. "set_max_threads=4". When using a ClickHouseClientSettings object, do not use the set_ prefix.
For a full list of available settings, see here.
Connection string examples
Basic connection
With custom ClickHouse settings
QueryOptions
QueryOptions allows you to override client-level settings on a per-query basis. All properties are optional and only override the client defaults when specified.
| Property | Type | Description |
|---|---|---|
| QueryId | string | Custom query identifier for tracking in system.query_log or cancellation |
| Database | string | Override the default database for this query |
| Roles | IReadOnlyList<string> | Override client roles for this query |
| CustomSettings | IDictionary<string, object> | ClickHouse server settings for this query (e.g., max_threads) |
| CustomHeaders | IDictionary<string, string> | Additional HTTP headers for this query |
| UseSession | bool? | Override session behavior for this query |
| SessionId | string | Session ID for this query (requires UseSession = true) |
| BearerToken | string | Override authentication token for this query |
| Timeout | TimeSpan? | Override client timeout for this query |
Example:
InsertOptions
InsertOptions extends QueryOptions with settings specific to bulk insert operations via InsertBinaryAsync.
| Property | Type | Default | Description |
|---|---|---|---|
| BatchSize | int | 100,000 | Number of rows per batch |
| MaxDegreeOfParallelism | int | 1 | Number of parallel batch uploads |
| Format | RowBinaryFormat | RowBinary | Binary format: RowBinary or RowBinaryWithDefaults |
All QueryOptions properties are also available on InsertOptions.
Example:
ClickHouseClient
ClickHouseClient is the recommended API for interacting with ClickHouse. It is thread-safe, designed for singleton use, and manages HTTP connection pooling internally.
Creating a client
Create a ClickHouseClient with a connection string or a ClickHouseClientSettings object. See the Configuration section for available options.
The details for your ClickHouse Cloud service are available in the ClickHouse Cloud console.
Select a service and click Connect:
Choose C#. Connection details are displayed below.
If you are using self-managed ClickHouse, the connection details are set by your ClickHouse administrator.
Using a connection string:
Or using ClickHouseClientSettings:
For dependency injection scenarios, use IHttpClientFactory:
ClickHouseClient is designed to be long-lived and shared across your application. Create it once (typically as a singleton) and reuse it for all database operations. The client manages HTTP connection pooling internally.
Executing queries
Use ExecuteNonQueryAsync for statements that don't return results:
Use ExecuteScalarAsync to retrieve a single value:
Inserting data
Parameterized inserts
Insert data using parameterized queries with ExecuteNonQueryAsync. Parameter types must be specified in the SQL using {name:Type} syntax:
Bulk inserts
Use InsertBinaryAsync for inserting large numbers of rows efficiently. It streams data using ClickHouse's native row binary format, supports parallel batch uploads, and avoids "URL too long" errors that can occur with parameterized queries.
For large datasets, configure batching and parallelism with InsertOptions:
- The client automatically fetches table structure via
SELECT * FROM <table> WHERE 1=0before inserting. Provided values must match the target column types. - When
MaxDegreeOfParallelism > 1, batches are uploaded in parallel. Sessions are not compatible with parallel insertion; either disable sessions or setMaxDegreeOfParallelism = 1. - Use
RowBinaryFormat.RowBinaryWithDefaultsinInsertOptions.Formatif you want the server to apply DEFAULT values for columns not provided.
Reading data
Use ExecuteReaderAsync to execute SELECT queries. The returned ClickHouseDataReader provides typed access to result columns via methods like GetInt64(), GetString(), and GetFieldValue<T>().
Call Read() to advance to the next row. It returns false when there are no more rows. Access columns by index (0-based) or by column name.
SQL parameters
In ClickHouse, the standard format for query parameters in SQL queries is {parameter_name:DataType}.
Examples:
SQL 'bind' parameters are passed as HTTP URI query parameters, so using too many of them may result in a "URL too long" exception. Use InsertBinaryAsync for bulk data insertion to avoid this limitation.
Query ID
Every query is assigned a unique query_id that can be used to fetch data from the system.query_log table or cancel long-running queries. You can specify a custom query ID via QueryOptions:
If you are specifying a custom QueryId, ensure it is unique for every call. A random GUID is a good choice.
Raw streaming
Use ExecuteRawResultAsync to stream query results in a specific format directly, bypassing the data reader. This is useful for exporting data to files or passing through to other systems:
Common formats: JSONEachRow, CSV, TSV, Parquet, Native. See the formats documentation for all options.
Raw stream insert
Use InsertRawStreamAsync to insert data directly from file or memory streams in formats like CSV, JSON, Parquet, or any supported ClickHouse format.
Insert from a CSV file:
See the format settings documentation for options to control data ingestion behavior.
More examples
For additional practical usage examples, see the examples directory in the GitHub repository.
ADO.NET
The library provides full ADO.NET support through ClickHouseConnection, ClickHouseCommand, and ClickHouseDataReader. This API is required for ORM integration (Dapper, Linq2db) and when you need standard .NET database abstractions.
Lifetime management with ClickHouseDataSource
Always create connections from a ClickHouseDataSource to ensure proper lifetime management and connection pooling. The DataSource manages a single ClickHouseClient internally, and all connections share its HTTP connection pool.
For dependency injection:
Do not create ClickHouseConnection directly in production code. Each direct instantiation creates a new HTTP client and connection pool, which can lead to socket exhaustion under load:
Instead, always use ClickHouseDataSource or share a single ClickHouseClient instance.
Using ClickHouseCommand
Create commands from a connection to execute SQL:
Command methods:
ExecuteNonQueryAsync()- For INSERT, UPDATE, DELETE, DDL statementsExecuteScalarAsync()- Returns first column of first rowExecuteReaderAsync()- Returns aClickHouseDataReaderfor iterating results
Using ClickHouseDataReader
The ClickHouseDataReader provides typed access to query results:
Best practices
Connection lifetime and pooling
ClickHouse.Driver uses System.Net.Http.HttpClient under the hood. HttpClient has a per-endpoint connection pool. As a consequence:
- Database sessions are multiplexed through HTTP connections managed by the connection pool.
- HTTP connections are recycled automatically by the pool.
- Connections can stay alive after
ClickHouseClientorClickHouseConnectionobjects are disposed.
Recommended patterns:
| Scenario | Recommended Approach |
|---|---|
| General use | Use a singleton ClickHouseClient |
| ADO.NET / ORMs | Use ClickHouseDataSource (creates connections that share the same pool) |
| DI environments | Register ClickHouseClient or ClickHouseDataSource as singleton with IHttpClientFactory |
When using a custom HttpClient or HttpClientFactory, ensure that the PooledConnectionIdleTimeout is set to a value smaller than the server's keep_alive_timeout, in order to avoid errors due to half-closed connections. The default keep_alive_timeout for Cloud deployments is 10 seconds.
Avoid creating multiple ClickHouseClient or standalone ClickHouseConnection instances without a shared HttpClient. Each instance creates its own connection pool.
DateTime handling
-
Use UTC whenever possible. Store timestamps as
DateTime('UTC')columns and useDateTimeKind.Utcin your code. This eliminates timezone ambiguity. -
Use
DateTimeOffsetfor explicit timezone handling. It always represents a specific instant and includes the offset information. -
Specify timezone in SQL type hints. When using parameters with
UnspecifiedDateTime values targeting non-UTC columns, include the timezone in the SQL:
Async inserts
Async inserts shift batching responsibility from the client to the server. Instead of requiring client-side batching, the server buffers incoming data and flushes it to storage based on configurable thresholds. This is useful for high-concurrency scenarios like observability workloads where many agents send small payloads.
Enable async inserts via CustomSettings or the connection string:
Two modes (controlled by wait_for_async_insert):
| Mode | Behavior | Use case |
|---|---|---|
wait_for_async_insert=1 | Insert returns after data is flushed to disk. Errors are returned to the client. | Recommended for most workloads |
wait_for_async_insert=0 | Insert returns immediately when data is buffered. No guarantee data will be persisted. | Only when data loss is acceptable |
With wait_for_async_insert=0, errors only surface during flush and cannot be traced back to the original insert. The client also provides no backpressure, risking server overload.
Key settings:
| Setting | Description |
|---|---|
async_insert_max_data_size | Flush when buffer reaches this size (bytes) |
async_insert_busy_timeout_ms | Flush after this timeout (milliseconds) |
async_insert_max_query_number | Flush after this many queries accumulate |
Sessions
Only enable sessions when you need stateful server-side features, e.g.:
- Temporary tables (
CREATE TEMPORARY TABLE) - Maintaining query context across multiple statements
- Session-level settings (
SET max_threads = 4)
When sessions are enabled, requests are serialized to prevent concurrent use of the same session. This adds overhead for workloads that don't require session state.
Using ADO.NET (for ORM compatibility):
Supported data types
ClickHouse.Driver supports all ClickHouse data types. The tables below show the mappings between ClickHouse types and native .NET types when reading data from the database.
Type mapping: reading from ClickHouse
Integer types
| ClickHouse Type | .NET Type |
|---|---|
| Int8 | sbyte |
| UInt8 | byte |
| Int16 | short |
| UInt16 | ushort |
| Int32 | int |
| UInt32 | uint |
| Int64 | long |
| UInt64 | ulong |
| Int128 | BigInteger |
| UInt128 | BigInteger |
| Int256 | BigInteger |
| UInt256 | BigInteger |
Floating point types
| ClickHouse Type | .NET Type |
|---|---|
| Float32 | float |
| Float64 | double |
| BFloat16 | float |
Decimal types
| ClickHouse Type | .NET Type |
|---|---|
| Decimal(P, S) | decimal / ClickHouseDecimal |
| Decimal32(S) | decimal / ClickHouseDecimal |
| Decimal64(S) | decimal / ClickHouseDecimal |
| Decimal128(S) | decimal / ClickHouseDecimal |
| Decimal256(S) | decimal / ClickHouseDecimal |
Decimal type conversion is controlled via the UseCustomDecimals setting.
Boolean type
| ClickHouse Type | .NET Type |
|---|---|
| Bool | bool |
String types
| ClickHouse Type | .NET Type |
|---|---|
| String | string |
| FixedString(N) | string |
By default, both String and FixedString(N) columns are returned as string. Set ReadStringsAsByteArrays=true in your connection string to read them as byte[] instead. This is useful when storing binary data that may not be valid UTF-8.
Date and time types
| ClickHouse Type | .NET Type |
|---|---|
| Date | DateTime |
| Date32 | DateTime |
| DateTime | DateTime |
| DateTime32 | DateTime |
| DateTime64 | DateTime |
| Time | TimeSpan |
| Time64 | TimeSpan |
ClickHouse stores DateTime and DateTime64 values internally as Unix timestamps (seconds or sub-second units since epoch). While the storage is always in UTC, columns can have an associated timezone that affects how values are displayed and interpreted.
When reading DateTime values, the DateTime.Kind property is set based on the column's timezone:
| Column Definition | Returned DateTime.Kind | Notes |
|---|---|---|
DateTime('UTC') | Utc | Explicit UTC timezone |
DateTime('Europe/Amsterdam') | Unspecified | Offset applied |
DateTime | Unspecified | Wall-clock time preserved as-is |
For non-UTC columns, the returned DateTime represents the wall-clock time in that timezone. Use ClickHouseDataReader.GetDateTimeOffset() to get a DateTimeOffset with the correct offset for that timezone:
For columns without an explicit timezone (i.e., DateTime instead of DateTime('Europe/Amsterdam')), the driver returns a DateTime with Kind=Unspecified. This preserves the wall-clock time exactly as stored without making assumptions about timezone.
If you need timezone-aware behavior for columns without explicit timezones, either:
- Use explicit timezones in your column definitions:
DateTime('UTC')orDateTime('Europe/Amsterdam') - Apply the timezone yourself after reading.
JSON type
| ClickHouse Type | .NET Type | Notes |
|---|---|---|
| Json | JsonObject | Default (JsonReadMode=Binary) |
| Json | string | When JsonReadMode=String |
The return type for JSON columns is controlled by the JsonReadMode setting:
-
Binary(default): ReturnsSystem.Text.Json.Nodes.JsonObject. Provides structured access to JSON data, but specialized ClickHouse types (like IP addresses, UUIDs, large decimals) are converted to their string representations within the JSON structure. -
String: Returns the raw JSON as astring. Preserves the exact JSON representation from ClickHouse, which is useful when you need to pass the JSON through without parsing, or when you want to handle deserialization yourself.
Other types
| ClickHouse Type | .NET Type |
|---|---|
| UUID | Guid |
| IPv4 | IPAddress |
| IPv6 | IPAddress |
| Nothing | DBNull |
| Dynamic | See note |
| Array(T) | T[] |
| Tuple(T1, T2, ...) | Tuple<T1, T2, ...> / LargeTuple |
| Map(K, V) | Dictionary<K, V> |
| Nullable(T) | T? |
| Enum8 | string |
| Enum16 | string |
| LowCardinality(T) | Same as T |
| SimpleAggregateFunction | Same as underlying type |
| Nested(...) | Tuple[] |
| Variant(T1, T2, ...) | See note |
| QBit(T, dimension) | T[] |
The Dynamic and Variant types will be converted to the corresponding type for the actual underlying type in each row.
Geometry types
| ClickHouse Type | .NET Type |
|---|---|
| Point | Tuple<double, double> |
| Ring | Tuple<double, double>[] |
| LineString | Tuple<double, double>[] |
| Polygon | Ring[] |
| MultiLineString | LineString[] |
| MultiPolygon | Polygon[] |
| Geometry | See note |
The Geometry type is a Variant type that can hold any of the geometry types. It will be converted to the corresponding type.
Type mapping: writing to ClickHouse
When inserting data, the driver converts .NET types to their corresponding ClickHouse types. The tables below show which .NET types are accepted for each ClickHouse column type.
Integer types
| ClickHouse Type | Accepted .NET Types | Notes |
|---|---|---|
| Int8 | sbyte, any Convert.ToSByte() compatible | |
| UInt8 | byte, any Convert.ToByte() compatible | |
| Int16 | short, any Convert.ToInt16() compatible | |
| UInt16 | ushort, any Convert.ToUInt16() compatible | |
| Int32 | int, any Convert.ToInt32() compatible | |
| UInt32 | uint, any Convert.ToUInt32() compatible | |
| Int64 | long, any Convert.ToInt64() compatible | |
| UInt64 | ulong, any Convert.ToUInt64() compatible | |
| Int128 | BigInteger, decimal, double, float, int, uint, long, ulong, any Convert.ToInt64() compatible | |
| UInt128 | BigInteger, decimal, double, float, int, uint, long, ulong, any Convert.ToInt64() compatible | |
| Int256 | BigInteger, decimal, double, float, int, uint, long, ulong, any Convert.ToInt64() compatible | |
| UInt256 | BigInteger, decimal, double, float, int, uint, long, ulong, any Convert.ToInt64() compatible |
Floating point types
| ClickHouse Type | Accepted .NET Types | Notes |
|---|---|---|
| Float32 | float, any Convert.ToSingle() compatible | |
| Float64 | double, any Convert.ToDouble() compatible | |
| BFloat16 | float, any Convert.ToSingle() compatible | Truncates to 16-bit brain float format |
Boolean type
| ClickHouse Type | Accepted .NET Types | Notes |
|---|---|---|
| Bool | bool |
String types
| ClickHouse Type | Accepted .NET Types | Notes |
|---|---|---|
| String | string, byte[], ReadOnlyMemory<byte>, Stream | Binary types written directly; streams can be seekable or non-seekable |
| FixedString(N) | string, byte[], ReadOnlyMemory<byte>, Stream | String is UTF-8 encoded and padded; binary types must be exactly N bytes |
Date and time types
| ClickHouse Type | Accepted .NET Types | Notes |
|---|---|---|
| Date | DateTime, DateTimeOffset, DateOnly, NodaTime types | Converted to Unix days as UInt16 |
| Date32 | DateTime, DateTimeOffset, DateOnly, NodaTime types | Converted to Unix days as Int32 |
| DateTime | DateTime, DateTimeOffset, DateOnly, NodaTime types | See below for details |
| DateTime32 | DateTime, DateTimeOffset, DateOnly, NodaTime types | Same as DateTime |
| DateTime64 | DateTime, DateTimeOffset, DateOnly, NodaTime types | Precision based on Scale parameter |
| Time | TimeSpan, int | Clamped to ±999:59:59; int treated as seconds |
| Time64 | TimeSpan, decimal, double, float, int, long, string | String parsed as [-]HHH:MM:SS[.fraction]; clamped to ±999:59:59.999999999 |
The driver respects DateTime.Kind when writing values:
| DateTime.Kind | HTTP Parameters | Bulk |
|---|---|---|
| Utc | Instant preserved | Instant preserved |
| Local | Instant preserved | Instant preserved |
| Unspecified | Treated as wall-clock in parameter type's timezone (defaults to UTC) | Treated as wall-clock in column's timezone |
DateTimeOffset values always preserve the exact instant.
Example: UTC DateTime (instant preserved)
Example: unspecified DateTime (wall-clock time)
Recommendation: for simplest and most predictable behavior, use DateTimeKind.Utc or DateTimeOffset for all DateTime operations. This ensures your code works consistently regardless of server timezone, client timezone, or column timezone.
HTTP parameters vs bulk copy
There is an important difference between HTTP parameter binding and bulk copy when writing Unspecified DateTime values:
Bulk Copy knows the target column's timezone and correctly interprets Unspecified values in that timezone.
HTTP Parameters do not automatically know the column timezone. You must specify it in the SQL type hint:
DateTime.Kind | Target Column | HTTP Param (with tz hint) | HTTP Param (no tz hint) | Bulk Copy |
|---|---|---|---|---|
Utc | UTC | Instant preserved | Instant preserved | Instant preserved |
Utc | Europe/Amsterdam | Instant preserved | Instant preserved | Instant preserved |
Local | Any | Instant preserved | Instant preserved | Instant preserved |
Unspecified | UTC | Treated as UTC | Treated as UTC | Treated as UTC |
Unspecified | Europe/Amsterdam | Treated as Amsterdam time | Treated as UTC | Treated as Amsterdam time |
Decimal types
| ClickHouse Type | Accepted .NET Types | Notes |
|---|---|---|
| Decimal(P,S) | decimal, ClickHouseDecimal, any Convert.ToDecimal() compatible | Throws OverflowException if exceeds precision |
| Decimal32 | decimal, ClickHouseDecimal, any Convert.ToDecimal() compatible | Max precision 9 |
| Decimal64 | decimal, ClickHouseDecimal, any Convert.ToDecimal() compatible | Max precision 18 |
| Decimal128 | decimal, ClickHouseDecimal, any Convert.ToDecimal() compatible | Max precision 38 |
| Decimal256 | decimal, ClickHouseDecimal, any Convert.ToDecimal() compatible | Max precision 76 |
JSON type
| ClickHouse Type | Accepted .NET Types | Notes |
|---|---|---|
| Json | string, JsonObject, JsonNode, any object | Behavior depends on JsonWriteMode setting |
The behavior when writing JSON is controlled by the JsonWriteMode setting:
| Input Type | JsonWriteMode.String (default) | JsonWriteMode.Binary |
|---|---|---|
string | Passed through directly | Throws ArgumentException |
JsonObject | Serialized via ToJsonString() | Throws ArgumentException |
JsonNode | Serialized via ToJsonString() | Throws ArgumentException |
| Registered POCO | Serialized via JsonSerializer.Serialize() | Binary encoding with type hints, custom path attributes supported |
| Unregistered POCO / Anonymous object | Serialized via JsonSerializer.Serialize() | Throws ClickHouseJsonSerializationException |
-
String(default): Acceptsstring,JsonObject,JsonNode, or any object. All inputs are serialized viaSystem.Text.Json.JsonSerializerand sent as JSON strings for server-side parsing. This is the most flexible mode and works without type registration. -
Binary: Only accepts registered POCO types. Data is converted to ClickHouse's binary JSON format client-side with full type hint support. Requires callingconnection.RegisterJsonSerializationType<T>()before use. WritingstringorJsonNodevalues in this mode throwsArgumentException.
Typed JSON columns
When a JSON column has type hints (e.g., JSON(id UInt64, price Decimal128(2))), the driver uses these hints to serialize values with full type fidelity. This preserves precision for types like UInt64, Decimal, UUID, and DateTime64 that would otherwise lose precision when serialized as generic JSON.
POCO serialization
POCOs can be written to JSON columns in two ways depending on the JsonWriteMode:
String mode (default): POCOs are serialized via System.Text.Json.JsonSerializer. No type registration is required. This is the simplest approach and works with anonymous objects.
Binary mode: POCOs are serialized using the driver's binary JSON format with full type hint support. Types must be registered with connection.RegisterJsonSerializationType<T>() before use. This mode supports custom path mappings via attributes:
-
[ClickHouseJsonPath("path")]: Maps a property to a custom JSON path. Useful for nested structures or when the property name differs from the desired JSON key. Only works in Binary mode. -
[ClickHouseJsonIgnore]: Excludes a property from serialization. Only works in Binary mode.
Property name matching with column type hints is case-sensitive. A property UserId will only match a hint defined as UserId, not userid. This matches ClickHouse behavior which allows paths like userName and UserName to coexist as separate fields.
Limitations (Binary mode only):
- POCO types must be registered on the connection with
connection.RegisterJsonSerializationType<T>()before serialization. Attempting to serialize an unregistered type throwsClickHouseJsonSerializationException. - Dictionary and array/list properties require type hints in the column definition to be serialized correctly. Without hints, use String mode instead.
- Null values in POCO properties are only written when the path has a
Nullable(T)type hint in the column definition. ClickHouse doesn't allowNullabletypes inside dynamic JSON paths, so un-hinted null properties are skipped. ClickHouseJsonPathandClickHouseJsonIgnoreattributes are ignored in String mode (they only work in Binary mode).
Other types
| ClickHouse Type | Accepted .NET Types | Notes |
|---|---|---|
| UUID | Guid, string | String parsed as Guid |
| IPv4 | IPAddress, string | Must be IPv4; string parsed via IPAddress.Parse() |
| IPv6 | IPAddress, string | Must be IPv6; string parsed via IPAddress.Parse() |
| Nothing | Any | Writes nothing (no-op) |
| Dynamic | — | Not supported (throws NotImplementedException) |
| Array(T) | IList, null | Null writes empty array |
| Tuple(T1, T2, ...) | ITuple, IList | Element count must match tuple arity |
| Map(K, V) | IDictionary | |
| Nullable(T) | null, DBNull, or types accepted by T | Writes null flag byte before value |
| Enum8 | string, sbyte, numeric types | String looked up in enum dictionary |
| Enum16 | string, short, numeric types | String looked up in enum dictionary |
| LowCardinality(T) | Types accepted by T | Delegates to underlying type |
| SimpleAggregateFunction | Types accepted by underlying type | Delegates to underlying type |
| Nested(...) | IList of tuples | Element count must match field count |
| Variant(T1, T2, ...) | Value matching one of T1, T2, ... | Throws ArgumentException if no type match |
| QBit(T, dim) | IList | Delegates to Array; dimension is metadata only |
Geometry types
| ClickHouse Type | Accepted .NET Types | Notes |
|---|---|---|
| Point | System.Drawing.Point, ITuple, IList (2 elements) | |
| Ring | IList of Points | |
| LineString | IList of Points | |
| Polygon | IList of Rings | |
| MultiLineString | IList of LineStrings | |
| MultiPolygon | IList of Polygons | |
| Geometry | Any geometry type above | Variant of all geometry types |
Not supported for writing
| ClickHouse Type | Notes |
|---|---|
| Dynamic | Throws NotImplementedException |
| AggregateFunction | Throws AggregateFunctionException |
Nested type handling
ClickHouse nested types (Nested(...)) can be read and written using array semantics.
Logging and diagnostics
The ClickHouse .NET client integrates with the Microsoft.Extensions.Logging abstractions to offer lightweight, opt-in logging. When enabled, the driver emits structured messages for connection lifecycle events, command execution, transport operations, and bulk insert operations. Logging is entirely optional—applications that do not configure a logger continue to run without additional overhead.
Quick start
Using appsettings.json
You can configure logging levels using standard .NET configuration:
Using in-memory configuration
You can also configure logging verbosity by category in code:
Categories and emitters
The driver uses dedicated categories so that you can fine-tune log levels per component:
| Category | Source | Highlights |
|---|---|---|
ClickHouse.Driver.Connection | ClickHouseConnection | Connection lifecycle, HTTP client factory selection, connection opening/closing, session management. |
ClickHouse.Driver.Command | ClickHouseCommand | Query execution start/completion, timing, query IDs, server statistics, and error details. |
ClickHouse.Driver.Transport | ClickHouseConnection | Low-level HTTP streaming requests, compression flags, response status codes, and transport failures. |
ClickHouse.Driver.Client | ClickHouseClient | Binary insert, queries, and other operations |
ClickHouse.Driver.NetTrace | TraceHelper | Network tracing, only when debug mode is enabled |
Example: Diagnosing connection issues
This will log:
- HTTP client factory selection (default pool vs single connection)
- HTTP handler configuration (SocketsHttpHandler or HttpClientHandler)
- Connection pool settings (MaxConnectionsPerServer, PooledConnectionLifetime, etc.)
- Timeout settings (ConnectTimeout, Expect100ContinueTimeout, etc.)
- SSL/TLS configuration
- Connection open/close events
- Session ID tracking
Debug mode: network tracing and diagnostics
To help with diagnosing networking issues, the driver library includes a helper that enables low-level tracing of .NET networking internals. To enable it you must pass a LoggerFactory with the level set to Trace, and set EnableDebugMode to true (or manually enable it via the ClickHouse.Driver.Diagnostic.TraceHelper class). Events will be logged to the ClickHouse.Driver.NetTrace category. Warning: this will generate extremely verbose logs, and impact performance. It is not recommended to enable debug mode in production.
OpenTelemetry
The driver provides built-in support for OpenTelemetry distributed tracing via the .NET System.Diagnostics.Activity API. When enabled, the driver emits spans for database operations that can be exported to observability backends like Jaeger or ClickHouse itself (via the OpenTelemetry Collector).
Enabling tracing
In ASP.NET Core applications, add the ClickHouse driver's ActivitySource to your OpenTelemetry configuration:
For console applications, testing, or manual setup:
Span attributes
Each span includes standard OpenTelemetry database attributes plus ClickHouse-specific query statistics that can be used for debugging.
| Attribute | Description |
|---|---|
db.system | Always "clickhouse" |
db.name | Database name |
db.user | Username |
db.statement | SQL query (if enabled) |
db.clickhouse.read_rows | Rows read by the query |
db.clickhouse.read_bytes | Bytes read by the query |
db.clickhouse.written_rows | Rows written by the query |
db.clickhouse.written_bytes | Bytes written by the query |
db.clickhouse.elapsed_ns | Server-side execution time in nanoseconds |
Configuration options
Control tracing behavior via ClickHouseDiagnosticsOptions:
Enabling IncludeSqlInActivityTags may expose sensitive data in your traces. Use with caution in production environments.
TLS configuration
When connecting to ClickHouse over HTTPS, you can configure TLS/SSL behavior in several ways.
Custom certificate validation
For production environments requiring custom certificate validation logic, provide your own HttpClient with a configured ServerCertificateCustomValidationCallback handler:
Important considerations when providing a custom HttpClient
- Automatic decompression: You must enable
AutomaticDecompressionif compression is not disabled (compression is enabled by default). - Idle timeout: Set
PooledConnectionIdleTimeoutsmaller than the server'skeep_alive_timeout(10 seconds for ClickHouse Cloud) to avoid connection errors from half-open connections.
ORM support
ORMs require the ADO.NET API (ClickHouseConnection). For proper connection lifetime management, create connections from a ClickHouseDataSource:
Dapper
ClickHouse.Driver can be used with Dapper, but anonymous objects are not supported.
Working example:
Not supported:
Linq2db
This driver is compatible with linq2db, a lightweight ORM and LINQ provider for .NET. See the project website for detailed documentation.
Example usage:
Create a DataConnection using the ClickHouse provider:
Table mappings can be defined using attributes or fluent configuration. If your class and property names match the table and column names exactly, no configuration is needed:
Querying:
Bulk Copy:
Use BulkCopyAsync for efficient bulk inserts.
Entity framework core
Entity Framework Core is currently not supported.
Limitations
AggregateFunction columns
Columns of type AggregateFunction(...) cannot be queried or inserted directly.
To insert:
To select: