-
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 (
ClickHouseDataSource,ClickHouseConnection,ClickHouseCommand): Standard .NET database abstractions. Required for ORM integration (Dapper, Linq2db) and when you need ADO.NET compatibility.ClickHouseBulkCopyis a helper class for efficiently inserting data using an ADO.NET connection.ClickHouseBulkCopyis deprecated and will be removed in a future release; useClickHouseClient.InsertBinaryAsyncinstead.
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 6.0
- .NET 8.0
- .NET 9.0
- .NET 10.0
Supported ClickHouse versions
The client officially supports the last 3 releases plus the last two LTS releases.Installation
Install the package from NuGet: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.
Connection settings
Data format & serialization
Session management
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
HTTP client configuration
Logging & debugging
Custom settings & roles
When using a connection string to set custom settings, use the
set_ prefix, e.g. “set_max_threads=4”. When using a ClickHouseClientSettings object, don’t 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.
Example:
InsertOptions
InsertOptions extends QueryOptions with settings specific to bulk insert operations via InsertBinaryAsync.
All
QueryOptions properties are also available on InsertOptions.
Example:
Skipping the schema probe query
By default,InsertBinaryAsync sends a SELECT ... WHERE 1=0 query before each insert to discover column types. For high-throughput scenarios, you can eliminate this overhead with two options:
Option 1: Provide column types explicitly
When you know the table schema at compile time, pass it directly via ColumnTypes. No schema query is sent at all:
UseSchemaCache = true to query the schema once and reuse it for subsequent inserts on the same ClickHouseClient instance:
ColumnTypestakes priority overUseSchemaCache. If both are set, the explicit types are used.- The schema cache does not detect
ALTER TABLEchanges. If you modify the table schema, create a newClickHouseClientor avoidUseSchemaCachefor that table. - The cache is scoped to the
ClickHouseClientinstance and keyed by (database, table). Different column subsets on the same table share a single cached schema.
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 aClickHouseClient 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’re using self-managed ClickHouse, the connection details are set by your ClickHouse administrator.
Using a connection string:
ClickHouseClientSettings:
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
UseExecuteNonQueryAsync for statements that don’t return results:
ExecuteScalarAsync to retrieve a single value:
Inserting data
Parameterized inserts
Insert data using parameterized queries withExecuteNonQueryAsync. Parameter types must be specified in the SQL using {name:Type} syntax:
Bulk inserts
UseInsertBinaryAsync 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.
InsertOptions:
- The client automatically fetches table structure via
SELECT * FROM <table> WHERE 1=0before inserting. Provided values must match the target column types. To skip this query, useInsertOptions.ColumnTypesorInsertOptions.UseSchemaCache. - 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.
POCO inserts
Instead of constructingobject[] arrays, you can insert strongly-typed POCO objects directly. Register the type once, then pass IEnumerable<T>:
When all mapped properties specify an explicit
Type, the schema probe query is skipped entirely. When only some properties have explicit types, the driver falls back to the schema probe for the full column set.
InsertBinaryAsync<T> supports the same InsertOptions (batching, parallelism, schema caching) as the object[] overload.
Unlike the
object[] overload, InsertBinaryAsync<T> does not accept an explicit column list. Columns are determined by the registered type’s mapped properties. To control which columns are inserted, use [ClickHouseNotMapped] to exclude properties or [ClickHouseColumn(Name = "...")] to rename them.If ColumnTypes is set in InsertOptions, they will override the POCO attributes.Schema evolution
POCO inserts work seamlessly when columns are added to the target table after the type is registered. Because the driver only inserts the columns mapped by the POCO, any new columns withDEFAULT (or other default expressions) are filled in by the server automatically. No code changes or re-registration are needed.
Reading data
UseExecuteReaderAsync 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.
POCO reads
Instead of reading columns by index or name, you can stream query results directly into your own classes. Register the type once with the client, then useQueryAsync<T>:
RegisterPocoType<T>() sets up both the insert and read mappings and validates both up front. RegisterBinaryInsertType<T>() is unchanged and remains insert-only for backwards compatibility.
A registered type must have:
- A public parameterless constructor.
- At least one public property with a public, non-
initsetter.requiredproperties are supported.
InvalidOperationException.
When iterating a reader manually, use ClickHouseDataReader.MapTo<T>() to materialize the current row into a registered POCO without advancing the reader:
LoggerFactory is configured, RegisterPocoType<T>() and RegisterBinaryInsertType<T>() emit a Debug-level log (category ClickHouse.Driver.Client) listing which properties mapped to which columns, and which were skipped and why. See Logging and diagnostics.
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.Identifier parameters
TheIdentifier parameter type lets you safely bind a database, table, or column name instead of a quoted string literal. Use it via the {name:Identifier} syntax in SQL, or by setting ClickHouseDbParameter.ClickHouseType = "Identifier":
Query ID
Every query is assigned a uniquequery_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:
Custom parameter type mapping
When using@-style parameters (e.g., WHERE id = @id), the driver automatically infers the ClickHouse type from the .NET value type. For example, int maps to Int32.
To override these defaults, set ParameterTypeResolver on ClickHouseClientSettings. This is useful when you want all DateTime parameters to use DateTime64(3) for millisecond precision, or all decimals to use a specific scale, without setting ClickHouseType on every individual parameter.
Using DictionaryParameterTypeResolver for simple type mappings:
IParameterTypeResolver for advanced scenarios:
For value-aware or name-based resolution, implement the IParameterTypeResolver interface directly. Return null to fall through to the default inference:
QueryOptions.ParameterTypeResolver. When set, it takes precedence over the client-level resolver.
Type resolution precedence:
The resolver is one step in a precedence chain. From highest to lowest priority:
- Explicit
ClickHouseTypeset on the parameter - SQL type hint from
{name:Type}syntax in the query IParameterTypeResolver(fromQueryOptions.ParameterTypeResolver, falling back toClickHouseClientSettings.ParameterTypeResolver)- Built-in type inference (
TypeConverter.ToClickHouseType)
ClickHouseConnection path — the settings are inherited by connections created from the client.
Custom parameter value formatting
IParameterFormatter is a hook that decides how parameter values are serialized. Use it when the built-in formatting (e.g. DateTime precision, decimal culture, string escaping, number representation) does not match what your schema or downstream tools expect.
Set ParameterFormatter on ClickHouseClientSettings to install a formatter for all parameterized queries. The formatter receives the value, the resolved ClickHouse type name, and the parameter name, and returns the string representation that is sent to the server. Return null to fall through to the default formatter.
Using DictionaryParameterFormatter for simple per-CLR-type formatting:
IParameterFormatter for advanced scenarios:
QueryOptions.ParameterFormatter. When set, it takes precedence over the client-level formatter.
Composite values:
The formatter runs both for top-level collection parameters and every element inside composite values (Array, Tuple, Map, Nullable, LowCardinality, Variant). For example, a typeof(int) mapping formats every Int32 element of an Array(Int32) individually.
Single-quote wrapping in composite contexts:
For string-like ClickHouse types (String, FixedString, Enum8, Enum16, IPv4, IPv6, UUID) embedded inside a composite literal, the driver wraps the formatter’s output in single quotes but does not escape its contents. If your returned string contains an unescaped single quote or backslash, the composite literal will be malformed and the server will reject the query.
Top-level string parameters (not embedded in a composite) are used verbatim without wrapping, so escaping is not required there.
Formatter precedence:
IParameterFormatter(fromQueryOptions.ParameterFormatter, falling back toClickHouseClientSettings.ParameterFormatter). If it returns non-null, that value is used.- Built-in type-specific formatting in
HttpParameterFormatter.
null or DBNull values, those are always serialized as the ClickHouse null sentinel (\N).
Custom read value conversion
IReadValueConverter lets you transform values returned by the data reader after deserialization, without changing their CLR type. Typical uses: setting DateTime.Kind = Utc on a DateTime column that has no timezone, trimming or normalizing strings, or post-processing a JSON column before it reaches application code.
Set ReadValueConverter on ClickHouseClientSettings to install a converter for all reads. The converter is invoked once per column per row through both the boxed (GetValue) and generic (GetFieldValue<T>) paths. When no converter is set, there is zero overhead — the reader returns values directly.
Using DictionaryReadValueConverter for simple per-CLR-type conversion:
For<T> pass through unchanged. The dispatch is by exact CLR type, so register the actual type the reader produces (e.g., For<JsonObject> for a JSON column in JsonReadMode.Binary).
Custom IReadValueConverter for advanced scenarios:
If you need to dispatch on the ClickHouse-side type string (for example, to distinguish DateTime from DateTime('UTC') — both surface as the same CLR type), implement IReadValueConverter directly:
GetFieldType, GetSchemaTable) is not rerouted through it and must remain consistent with what is returned.
You can also set a converter per-query via QueryOptions.ReadValueConverter; when set, it takes precedence over the client-level converter.
Dispatch boundary:
The converter is invoked once per column with the entire deserialized cell value, it does not recurse into composite containers. For an Array(Int32) column the value passed in is an int[]; for Tuple(Int32, String) it is an ITuple.
The converter works with the ADO.NET ClickHouseConnection path — the settings are inherited by connections created from the client.
Raw streaming
UseExecuteRawResultAsync 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:
JSONEachRow, CSV, TSV, Parquet, Native. See the formats documentation for all options.
Per-query transport compression
By default, the client negotiatesgzip, deflate when Compression=true (the connection-string default) and the HTTP client decompresses the stream transparently.
For raw exports (e.g. Parquet, Arrow, Native) you may want to negotiate a different codec (e.g. zstd or lz4) to trade CPU for bandwidth without changing the connection-wide setting. QueryOptions.AcceptEncoding and ClickHouseCommand.AcceptEncoding set the HTTP Accept-Encoding header for a single request, replacing whatever default was attached, and force enable_http_compression=1 on the URL (which ClickHouse requires before it will honor Accept-Encoding).
HttpClient configuration
The defaultHttpClient the driver builds has AutomaticDecompression = GZip | Deflate, which transparently decompresses those algorithms and strips Content-Encoding from the response. This is what you want for normal queries, but not if you want to handle the raw, compressed data yourself.
In that case, pass an HttpClient or HttpClientFactory to the ClickHouseClient with AutomaticDecompression = DecompressionMethods.None
Error bodies
When the server responds with a 4xx/5xx andenable_http_compression=1 was set, it compresses the error body with the same codec it would have used for a successful response. The driver decompresses these for codecs the BCL ships with (gzip, deflate, br/brotli) so the message surfaced on ClickHouseServerException is readable. For codecs it cannot decode (zstd, lz4, …), the driver returns a placeholder message that names the codec and points at system.query_log for the original error text.
Raw stream insert
UseInsertRawStreamAsync 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 throughClickHouseConnection, 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 aClickHouseDataSource to ensure proper lifetime management and connection pooling. The DataSource manages a single ClickHouseClient internally, and all connections share its HTTP connection pool.
Using ClickHouseCommand
Create commands from a connection to execute SQL:ExecuteNonQueryAsync()- For INSERT, UPDATE, DELETE, DDL statementsExecuteScalarAsync()- Returns first column of first rowExecuteReaderAsync()- Returns aClickHouseDataReaderfor iterating results
Using ClickHouseDataReader
TheClickHouseDataReader 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.
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 viaCustomSettings or the connection string:
wait_for_async_insert):
Key settings:
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)
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
Floating point types
Decimal types
Decimal type conversion is controlled via the UseCustomDecimals setting.
Boolean type
String types
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 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:
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:
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
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
The Dynamic and Variant types will be converted to the corresponding type for the actual underlying type in each row.
Geometry types
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
Floating point types
Boolean type
String types
Date and time types
Out-of-range valuesOn the binary write path,
Date, Date32, DateTime, and DateTime32 values outside their supported range throw ArgumentOutOfRangeException at Write time, naming the column type and the supported range. Previously, out-of-range values could be silently truncated through a 32-bit integer and reinterpreted by the server, producing real-but-wrong timestamps.DateTime.Kind when writing values:
DateTimeOffset values always preserve the exact instant.
Example: UTC DateTime (instant preserved)
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 writingUnspecified 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:
Decimal types
JSON type
The behavior when writing JSON is controlled by the
JsonWriteMode setting:
-
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.
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.
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.
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
Geometry types
Not supported for writing
Nested type handling
ClickHouse nested types (Nested(...)) can be read and written using array semantics.
Logging and diagnostics
The ClickHouse .NET client integrates with theMicrosoft.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:Example: Diagnosing connection issues
- 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 theClickHouse.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 isn’t recommended to enable debug mode in production.
OpenTelemetry
The driver provides built-in support for OpenTelemetry distributed tracing via the .NETSystem.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’sActivitySource to your OpenTelemetry configuration:
Span attributes
Each span includes standard OpenTelemetry database attributes plus ClickHouse-specific query statistics that can be used for debugging.Configuration options
Control tracing behavior viaClickHouseDiagnosticsOptions:
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 ownHttpClient with a configured ServerCertificateCustomValidationCallback handler:
Important considerations when providing a custom HttpClient
- Automatic decompression: You must enable
AutomaticDecompressionif compression isn’t 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 works with Dapper. The driver automatically converts Dapper’s @parameter syntax to ClickHouse’s native {parameter:Type} syntax, with types inferred from .NET values.
Use ClickHouseDataSource for proper connection lifetime management:
Parameter passing styles
All standard Dapper parameter styles are supported: Anonymous objects:DynamicParameters (from dictionary or anonymous object):
Querying into POCOs
Dapper maps columns to properties by name (case-insensitive):ClickHouse-native parameter syntax
When you need explicit type control, use ClickHouse’s{param:Type} syntax directly in the SQL with a Dictionary<string, object> for the parameter values. Don’t combine @param syntax and {param:Type} syntax for the same parameter.
WHERE IN
Dapper’s native IN expansion works:WHERE id IN (@Ids1, @Ids2, @Ids3), and the driver converts each expanded parameter.
ClickHouse’s has() with Array parameter also works:
Custom type handlers
Some ClickHouse types, egITuple, BigInteger, and ClickHouseDecimal need handlers registered at startup:
Dapper.Contrib
GetAll<T>() and Get<T>(id) work. Insert<T>() does not — it generates SQL Server syntax (SCOPE_IDENTITY, []). It is recommended to use the ClickHouseClient native InsertBinaryAsync method instead.
Limitations
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 aDataConnection using the ClickHouse provider:
BulkCopyAsync for efficient bulk inserts.
Entity Framework Core
The official Entity Framework Core provider for ClickHouse. Map C# classes to ClickHouse tables, query with LINQ, and insert data viaSaveChanges — all using familiar EF Core patterns.
- NuGet:
ClickHouse.EntityFrameworkCore - Source: GitHub
This provider is in active development. Current release supports LINQ queries (including JOINs, subqueries, and set operations),
INSERT via SaveChanges / BulkInsertAsync, migrations with full DDL (CREATE / ALTER / DROP), and ClickHouse-specific table engine configuration. UPDATE / DELETE are not supported.Installation
Quick start
Define your entity andDbContext, then query with LINQ:
Supported types
Use
ClickHouseDecimal (from ClickHouse.Driver.Numerics) instead of decimal when you need the full precision of Decimal128/Decimal256 columns — .NET decimal is limited to 28–29 significant digits.
Supported LINQ operations
Queries:Where, OrderBy, Take, Skip, Select, First, Single, Any, All, Count, Distinct, AsNoTracking
GROUP BY & aggregates: GroupBy with Count, LongCount, Sum, Average, Min, Max — including HAVING (.Where() after .GroupBy()), multiple aggregates in a single projection, and OrderBy on aggregate results.
JOINs: Join (INNER), GroupJoin/SelectMany patterns (LEFT and CROSS). LEFT JOIN returns real null for non-matching rows (see LEFT JOIN null semantics below).
Subqueries: correlated Contains / IN, Any / EXISTS, All, and scalar subqueries in projections.
Set operations: Concat (→ UNION ALL), Union (→ UNION DISTINCT), Intersect, Except.
Inline local collections: joins and Contains against in-memory collections (int[], List<T>, etc.) translate into a series of UNIONs.
String methods: Contains, StartsWith, EndsWith, IndexOf, Replace, Substring, Trim/TrimStart/TrimEnd, ToLower, ToUpper, Length, IsNullOrEmpty, Concat (and + operator).
Math functions: standard Math and MathF methods translated to their ClickHouse equivalents — arithmetic, logarithmic, trigonometric, and utility functions.
The provider injects set_join_use_nulls=1 into every connection path automatically to match Entity Framework expectations on JOIN behavior.
If your ClickHouse server or profile forbids changing this setting (e.g. a readonly=1 profile), opt out with:
0 / "" instead of == null.
Inserting data
SaveChanges uses the driver’s native InsertBinaryAsync API — RowBinary encoding with GZip compression, far more efficient than parameterized SQL:
Added to Unchanged after save, just like any other EF Core provider.
Batch size is configurable (default 1000):
Bulk insert
For high-throughput loads, useBulkInsertAsync instead of SaveChanges. This is an extension method on DbContext that bypasses EF Core’s change tracker, identity resolution, and state management entirely — it calls the driver’s InsertBinaryAsync directly with RowBinary encoding and GZip compression.
This makes it suitable for loading large datasets where you don’t need entity tracking after insert:
IEnumerable<T> — it streams through the entities without loading them all into memory. The return value is the number of rows inserted. Entities are not attached to the DbContext after insert, so there is no Added → Unchanged state transition.
Enums
ClickHouseEnum8/Enum16 columns can be mapped as string properties or as C# enum types. When using C# enums, the provider automatically converts between the enum and its string representation:
Custom type conversions
EF Core’sValueConverter system lets you map custom types to types the provider already supports. The provider never sees your custom type — EF Core converts at the boundary.
Per-property conversion:
Column type annotations
For scalar types likestring, int, DateTime, etc., the provider infers the ClickHouse type automatically. For parameterized types and wrappers, you need to specify the ClickHouse type explicitly.
Using data annotations (attributes):
OnModelCreating:
Array(Nullable(Int32)) and LowCardinality(Nullable(String)) are supported — the provider unwraps Nullable and LowCardinality automatically at every nesting level.
Variant and Dynamic columns
ClickHouseVariant(T1, T2, ...) and Dynamic columns map to object in .NET. Since object is too generic for automatic type inference, you must declare the store type explicitly via .HasColumnType():
string, ulong, ulong[]).
JSON columns
The provider supports ClickHouse’sJson column type, mapping to System.Text.Json.Nodes.JsonNode (primary) or string (via automatic ValueConverter):
SaveChanges and BulkInsertAsync:
string with a Json column type — the provider applies a ValueConverter automatically:
- No JSON path translation —
entity.Data["name"]in LINQ does not translate to ClickHouse’sdata.nameSQL syntax. Filter on non-JSON columns and inspect JSON in memory. - NULL semantics — ClickHouse’s JSON type returns
{}(empty object) for NULL values rather than SQL NULL. - Integer precision — ClickHouse JSON stores all integers as
Int64. When reading viaJsonNode, useGetValue<long>()rather thanGetValue<int>().
Table engines
Configure ClickHouse table engines and engine-specific clauses via theToTable(name, t => ...) fluent API. When no engine is configured, the provider defaults to MergeTree with ORDER BY derived from the entity’s primary key.
Engine clauses:
WithOrderBy, WithPartitionBy, WithPrimaryKey, WithSampleBy, WithTtl, WithSettings. All attach to the engine builder returned from HasXxxEngine().
Column-level features: HasCodec, HasTtl, HasComment, HasDefault — all participate in migrations.
Data-skipping indexes — via HasIndex(...).HasSkippingIndexType(...):
Migrations
Standard EF Core migrations workflow:Migration limitations
Beyond migrations, the provider also does not yet support:
UPDATE/DELETE- Transactions:
BeginTransactionis a no-op. No support for ACID transactions in ClickHouse. - JSON path query translation:
entity.Data["key"]in LINQ does not translate to ClickHouse’sdata.keySQL syntax. Filter on non-JSON columns and inspect JSON in memory.
Limitations
Tuples with 8+ elements and a nested tuple in the last position
C#ValueTuple types with more than 7 elements use a compiler-generated nesting scheme: the 8th generic argument (TRest) is itself a ValueTuple holding the remaining elements. For example, (int, int, int, int, int, int, int, string, string) compiles to ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>.
This creates an ambiguity when the ClickHouse column is an 8-element tuple where the last element is itself a tuple — e.g., Tuple(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Tuple(String, String)). The driver cannot distinguish between:
- A 9-element flat tuple (compiler-generated TRest nesting)
- An 8-element tuple where the last element is a nested
Tuple(String, String)
ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>.
The driver treats the 8th argument as TRest (i.e., flattens it), which means the 8-element-with-nested-tuple case will be serialized incorrectly.
This affects both System.Tuple and ValueTuple since both use TRest nesting for >7 elements. Tuples with 7 or fewer elements, or tuples where the last element is not itself a tuple, are not affected.
Workaround: Wrap the inner tuple in an extra layer so the driver can distinguish it from TRest nesting:
AggregateFunction columns
Columns of typeAggregateFunction(...) can’t be queried or inserted directly.
To insert: