New Features:
- Experimental native TCP client.
ClickHouseTcpClientconnects to ClickHouse over the native protocol (port 9000, or 9440 with TLS). Configure it with a connection string orClickHouseTcpClientOptions. It reads rows, POCOs, or columnar blocks, and inserts rows, POCOs, or typed columns. In our benchmarks against the HTTP client in v1.4.0, reads are up to 2x faster, inserts are up to 1.5x faster, and managed allocations are much lower. It ships in theClickHouse.Driverpackage for .NET 8 and later. It is a separate API fromClickHouseClientand has no ADO.NET layer. The API can change in a future release, so using it causes warningCHTCP0001. See the documentation and the examples.
Improvements:
- Added
RowIndextoClickHouseBulkCopySerializationExceptionso binary-insert failures report the zero-based index of the offending row within the batch, making it easy to locate bad data in large inserts. (Thanks to @mbtolou - #606 and @Skyuzii - #608)
Bug Fixes:
- Fixed a wall-clock shift when reading a
DateTime/DateTime64column whoseFixed/UTC±HH:MM:SStimezone name has a minute or second field above 59. The server carries such a field into the next one, soFixed/UTC+05:60:00means +06:00; the driver rejected the name and read the value as UTC. (#612)
Breaking Changes:
-
Query responses are now compressed with zstd instead of gzip by default. The driver advertises
zstd, lz4, gzip, deflate, and ClickHouse's fixed preference order resolves that to zstd. Bodies are decoded transparently as before, so no code change is needed. -
Binary inserts are now compressed with zstd instead of gzip by default.
InsertOptions.Compressordefaults toZstdCompressor.Default(level 3) rather thanGZipCompressor.Default(gzip, fastest). You can control this viaInsertOptions. -
If you set
AcceptEncodingand then read a raw result, you now get compressed bytes.ExecuteRawResultAsync,PostStreamAsyncandInsertRawStreamAsynchand back the response body exactly as the server sent it. CallClickHouseRawResult.ReadDecompressedStreamAsync()to have the driver decompress it, or checkContentEncodingand decompress it yourself. Raw exports with noAcceptEncodingset are unaffected: plaintext before, plaintext now. -
Reading a column value from
ClickHouseDataReaderwith no current row now throwsInvalidOperationException— that is, before the firstRead()or afterRead()has returnedfalse.
New Features:
- Added
InsertOptions.QueryPlacement. Set it toInsertQueryPlacement.Urlto send a binary insert'sINSERTstatement as thequeryURL parameter, where proxies and access logs can read it, instead of in the request body; body placement remains the default. If the .NET runtime rejects an oversized URL, the error directs callers back to body placement. (#580) - Pluggable binary-insert compression via
InsertOptions.Compressor(IClickHouseCompressor). The presence of a compressor is the on/off switch: set it tonullto send the payload uncompressed (useful over fast/local links where the compression CPU outweighs the bandwidth savings). GZip, Brotli, LZ4, and ZStd compressors are available.- Tuning guidance: the best codec depends on where the server is. Over a fast/local link, compression is pure overhead, so
Compressor = nullis worth trying. Over a remote/cloud connection the payload reduction dominates, depending on bandwidth.
- Tuning guidance: the best codec depends on where the server is. Over a fast/local link, compression is pure overhead, so
- Built-in LZ4 codec for binary inserts (
Lz4Compressor; HTTPContent-Encoding: lz4plus the native-protocol block path). LZ4 now ships in the core driver with no third-party runtime dependency. - Built-in ZSTD codec (
ZstdCompressor; HTTPContent-Encoding: zstdplus the native-protocol block path). - You can now choose how ClickHouse compresses query responses, via
AcceptEncoding— client-wide (ClickHouseClientSettings.AcceptEncoding), in the connection string (AcceptEncoding=lz4), or for one query (QueryOptions.AcceptEncoding).lz4,gzip,deflateandbrare all decompressed for you;identityturns response compression off. Previously onlygzipanddeflateworked: asking forlz4orbrreturned unreadable data, andidentitydid not actually switch compression off. To decompress a raw export, use the newClickHouseRawResult.ReadDecompressedStreamAsync(). - zstd responses are now decoded transparently, so
AcceptEncoding = "zstd"works with every read API instead of failing as an unsupported codec. - Added
MapReadMode(connection stringMapReadMode=KeyValuePairs, orClickHouseClientSettings.MapReadMode), which readsMap(K, V)columns asList<KeyValuePair<K, V>>instead ofDictionary<K, V>. A ClickHouse map may repeat a key and a dictionary cannot, so the default representation silently drops pairs. Also accepted on the write path.
Improvements:
- A large number of performance improvements have been made in this version, significantly cutting heap allocations, reducing GC pressure, and improving speed. You will see the best results by using the POCO read and write methods, as they avoid boxing values.
- A custom
HttpClientno longer needsAutomaticDecompression. The driver decompresses responses itself, so supplying your ownHttpClient(orIHttpClientFactory) no longer risks unreadable data. If you do setAutomaticDecompression, prefer leaving it at its default ofNone. - Clearer failures around compression. A response compressed with a codec the driver cannot read (e.g.
snappy) now raises an error naming that codec and how to fix it, instead of failing as a confusing type-parse error. Server error messages are now readable when the response is compressed, rather than surfacing as binary. - Lower GC pressure on binary inserts: the built-in compressors (
GZipCompressor,BrotliCompressor,Lz4Compressor) no longer allocate a fresh 256 KiBBufferedStreambuffer per batch on the large object heap. The buffering now rents its backing buffer fromArrayPool<byte>. - Binary inserts (
InsertBinaryAsync) now stream the serialized batch directly into the HTTP request body instead of buffering the whole payload. Requests are now sent with chunked transfer-encoding (noContent-Length). - Removed a 4KiB/batch allocation in the binary-insert serializers (for writing the SQL query). Pooled buffers are now used instead.
- Faster multidimensional array binary inserts: rectangular multidimensional CLR arrays (
int[,],double[,,], …) of fixed-width primitive leaves (Int8/16/32/64,UInt8/16/32/64,Float32/64,Bool) are now serialized by blitting each contiguous inner row in a single write. - Reduced allocations on the binary write path. Writing an
Int128/UInt128/Int256/UInt256value no longer allocates two temporary arrays per value, and a rectangular multidimensional array whose leaf is a fixed-width primitive behind a wire-transparent wrapper (LowCardinality(Int32),SimpleAggregateFunction(any, Int32), …) now takes the same blit fast path as a bare leaf instead of boxing every element.Nullable(...)leaves keep the per-element path. Output bytes are unchanged. - Reduced allocations when writing
Decimal/Decimal128/Decimal256values (binary inserts and parameters): the write path no longer allocates aBigInteger.ToByteArray()array plus a separate destination buffer per value. - Mostly eliminated allocations in POCO binary inserts: value-type properties are now written through box-free delegate instead of boxing each value into
objectand unboxing it inside the type'sWrite. Note that if you useRowBinaryWithDefaults, the path is unchanged and still incurs the boxing cost. The change only applies toRowBinary(the default). - Faster
Variantwrite-type resolution: matching a value to its variant subtype now uses an O(1) hash lookup by runtime type instead of an O(n) linear scan for variants with 3 or more underlying types. - Lower GC pressure when reading query responses: the reader no longer allocates a fresh
BufferedStreambuffer per query. The response read buffering now rents its backing buffer fromArrayPool<byte>and returns it when the reader is disposed. The defaultReadBufferSizehas been raised from 8 KiB to 64 KiB now that pooling removes the per-query allocation that motivated the small default (see v1.3.0), reducing read refills on large responses. - Faster scalar reads from query responses: the response stream wrappers now implement
Stream.Read(Span<byte>)directly instead of falling through to the base implementation, which rents and copies through a pooled array on every call, and mid-stream exception detection now records from below the read buffer so it observes one read per buffer refill rather than one per value decoded. - Reduced per-query request-URI allocations:
ClickHouseUriBuilder.ToString()now composes the URI into a thread-reusedStringBuilderinstead of allocating aUriBuilder, two dictionaries, and a LINQ-projected string per parameter. Cuts URI-assembly allocations ~65% on the common parameter-free path. The produced URI string and behavior are unchanged. - Reduced per-row allocations when reading fixed-size binary columns.
Int128/Int256/UInt128/UInt256,Decimal128/Decimal256,IPv4, andIPv6no longer allocate a temporarybyte[]per value during deserialization. - Reading an
Int128orUInt128column into a nativeInt128/UInt128property no longer allocates a 16-byte array per value (40 bytes per value on 64-bit). - Reduced the per-value allocations on the read and write paths for
UUID,Array(...)andDecimalcolumns. - Lower GC pressure when reading
Dynamiccolumns: every value in aDynamiccolumn carries its own binary type header, so the driver decoded (and allocated) a fresh type descriptor per row. Stateless, parameterless types now decode to a shared immutable singleton instead, eliminating the per-row type allocation. - Faster reading of
Tuple(...)columns with up to 7 elements:TupleType.Readnow constructs theSystem.Tuple<...>directly through a cached compiled factory instead of allocating two intermediateobject[]buffers and invoking the constructor reflectively viaActivator.CreateInstance. - Faster reading of
Arraycolumns with common leaf element types (integers, floats,Bool,Decimal, date/time types,UUID,String,Enum,FixedString, and theirNullableforms):ArrayType.Readnow fills a strongly-typedT[]through the array indexer instead of allocating viaArray.CreateInstanceand storing each element with reflectiveArray.SetValue. - Reading
Mapcolumns now pre-sizes the resultDictionaryto the known entry count. - Reduced allocations in POCO reads (
client.QueryAsync<T>(...)): scalar,StringandFixedStringcolumns are now materialized straight into the target property instead of through a boxedobject[]row buffer, removing one box and one unbox per value-type property per row. SimpleAggregateFunction(f, T)columns now take the box-free fast path inQueryAsync<T>, asLowCardinality(T)already did. Previously they fell back to the slower boxed read.QueryAsync<T>can now bind one column to more than one CLR type, chosen by the property:DateTime/DateTime64/DateintoDateTime,DateTimeOffsetorDateOnly;String/FixedStringintostringorbyte[];DecimalintodecimalorClickHouseDecimal;Enum8/Enum16intostring(the label) orint(the stored numeric value); and, on .NET 8+,Int128/UInt128into the nativeSystem.Int128/System.UInt128as well asBigInteger.client.QueryAsync<T>(...)can now read aMap(K, V)column into aList<KeyValuePair<K, V>>orKeyValuePair<K, V>[]POCO property, in addition toDictionary<K, V>.- With an
IReadValueConverterconfigured,client.QueryAsync<T>(...)applies it per column on the overload that matches how the column was read:ConvertValue<T>, withTthe property type, for a column read without boxing, and the boxedConvertValuefor a composite one. - Reduced per-row allocations for
ClickHouseDataReadertyped accessors (GetInt64,GetDouble,GetDateTime,GetGuid, …),GetFieldValue<T>,IsDBNulland ORMs such as linq2db by decoding scalar columns into reusable typed slots instead of eagerly boxing every value-type cell. Allocations drop by roughly two thirds on a typical multi-column typed read and to zero per row for all-numeric typed reads. - With an
IReadValueConverterconfigured,ClickHouseDataReader's typed accessors (GetInt64,GetDouble,GetDecimal,GetString, …) keep their reduced allocations: each reads its typed slot and converts throughConvertValue<T>, the overloadGetFieldValue<T>uses. These accessors previously routed throughGetValueand so through the boxedConvertValue. - Added
ClickHouseDataReader.TryGetEnumOrdinal(int ordinal, out int value)for reading the underlying integer ordinal of anEnum8/Enum16(orNullable(Enum...)) column. - You can now pass
UseFormDataParametersthrough the connection string. AddedUseFormDataParametersproperty toClickHouseConnectionStringBuilder. - Reading
Mapcolumns now builds the resultDictionarythrough a cached compiled factory instead of a reflection invoke, removing the per-row constructor resolution that the pre-sizing change added.
Deprecations:
ClickHouseClient.MemoryStreamManageris now[Obsolete]. Since binary inserts stream directly into the request body (see above), this property is no longer used and has no effect; it will be removed in a future version.
Bug Fixes:
- Breaking Change: Fixed a
JSONcolumn with overlapping paths silently losing data. ClickHouse accepts a column which declares a path both as a value and as the parent of another path — for exampleJSON(a Int64, a.b Int64)— and sends both paths in every row, so the row renders as a document with a duplicate key ({"a":0,"a":{"b":7}}). AJsonObjectcannot hold two values for one key, so the driver silently kept whichever path the server sent last: reading{"a":{"b":7}}back out of that column returned{"a":0}and the7was gone. The same loss hit aMappath overlapped by a deeper path (JSON(a Map(String, Int64))holding{"a":{"b":1},"a.b":7}returned{"a":{"b":7}}, dropping the map's own entry), except that there the two values were merged into one object rather than one replacing the other. A row where both sides of such an overlap hold a value now throws aSerializationExceptionnaming the two paths. With non-Nullablepaths the driver cannot tell a stored0from an absent path, so no rule can recover the real value — the choice is between a wrong answer and a clear failure. A side which holds nothing is not a collision: a null, an empty object, and an all-null subtree all give way to the side which has the data, whichever of the two paths the server sends first, soNullableoverlaps continue to read as before. UseJsonReadMode.Stringto read such a column as the server's JSON text, duplicate key included. To restore the previous behavior and keep reading the column as aJsonObject, set the newAllowDuplicateJsonKeyssetting (connection string keyAllowDuplicateJsonKeys=true, orClickHouseClientSettings.AllowDuplicateJsonKeys); the driver then keeps whichever value the row carries last, with the data loss described above. A path which holds a value and whose parent holds a scalar or an array still throws — that shape raisedInvalidCastExceptionbefore and is now aSerializationException. (#583) - Fixed uncompressed binary inserts (
InsertOptions.Compressor = null) inflating the HTTP request body. The request body is sent chunked, and the row serializer wrote straight to it, so every field write became its own HTTP chunk. - Fixed binary inserts flushing the compression stream once per value written by copying a stream — binary-mode
JSONPOCOs, andString/FixedStringvalues given as aStream. - Fixed
InsertOptions.WithColumnTypes()andInsertOptions.WithQueryId()silently dropping some caller-set options (such asAcceptEncoding) when copying. - Fixed POCO binary inserts writing
JSONcolumns against the wrong typed-path hints when the same POCO type is inserted into two tables whoseJSONcolumns declare different hints. The second insert reused the first table's cached write delegates, so values were written silently mis-typed. - Fixed
ClickHouseServerExceptioncarrying a blankMessageand anErrorCodeof-1when the server — or an upstream component such as a load balancer or the ClickHouse Cloud edge — returned a non-2xx HTTP response with an empty (or whitespace-only) body. The exception now reports the HTTP status code and reason phrase, and uses theX-ClickHouse-Exception-Coderesponse header as the error code when the server sets it. Non-empty error bodies are unaffected. - Fixed
ClickHouseClientleaking the query'sHttpResponseMessageon the paths that fully consume it (ExecuteNonQueryAsync,InsertBinaryAsync) and on any request that fails. Paths that hand the response to the caller (ExecuteReaderAsync/ExecuteRawResultAsync/InsertRawStreamAsync/PostStreamAsync) are unchanged. - Fixed
ClickHouseClient.InsertRawStreamAsyncdisposing the suppliedStreamtwice when the request failed: the request message already disposes the content it carries, so the extraDispose()in the failure path was redundant. The stream-taking write paths now document that the supplied stream is disposed once the request completes. - Fixed
PingAsyncignoring thePathconnection setting: the ping request now targets<Path>/ping, so clients behind a reverse-proxy prefix no longer report a healthy server as unreachable. - Fixed mid-stream server exceptions never surfacing on the streaming read path (
ExecuteReader/ExecuteReaderAsync). A query that fails after the HTTP response is committed (for example athrowIfpartway through a large result) now raises aClickHouseServerExceptionwith the real server error, instead of a bareHttpIOExceptionorEndOfStreamException. - Fixed
ClickHouseCommandreturning wrong results (or a syntax error) forCommandBehavior.SchemaOnlyandCommandBehavior.SingleRowwhenCommandTextended with a single-line comment (--/#) or a statement-terminating;. - Fixed
ClickHouseDataReader.GetSchemaTable()leavingNumericScaleunset (DBNull) forDateTime64(N)andTime64(N)columns (including theirNullable(...)variants). The schema table now reports the fractional-seconds precisionNinNumericScale, matching howDecimalcolumns are already reported. - Fixed
DbConnection.GetSchema("Columns", ...)not disposing the command it creates internally, which delayed the release of that command's cancellation-token source until garbage collection. - Fixed
ClickHouseConnection.GetSchema("Columns", ...)building invalid SQL when the table restriction is supplied without the database restriction (for example[null, "functions"]). TheWHEREclause is now composed from the restrictions that are actually set, so filtering by table alone no longer fails with a server syntax error. - Fixed
ClickHouseConnection.GetSchema("Columns", ...)silently ignoring restriction values beyond the supporteddatabaseandtablepositions. - Fixed JSON typed paths whose names start with
max_dynamic_pathsormax_dynamic_typesbeing mistaken for JSON settings and decoded as dynamic values. - Fixed
JSONcolumns being unreadable when a typed path name requires backtick quoting — for exampleJSON(`a b` Int64)orJSON(`a,b` Int64). Such a path made the whole query fail withSerializationException: Unsupported path in JSON hint, because the type parser split each hint on every space and did not treat backticks as quotes. Quoted path names (including ones containing spaces, commas, parentheses and escaped characters) are now parsed and unescaped correctly. - Fixed reading a
JSONcolumn where a typed path holdsNULL. The path was dropped from the returnedJsonObjectentirely, so{"x": null}came back as{}and callers could not tell "path not present in this row" from "path present but null"; for a nested typed path such asJSON(a.b Nullable(Int64))the whole parent subtree disappeared. Typed paths are now materialized with an explicit JSON null, matching the server's own JSON rendering. Dynamic (unhinted) paths are unchanged and stay absent, as the server also omits them. - Fixed string values inside a
JSONcolumn being returned as base64 whenReadStringsAsByteArrays = true, andMap(String, ...)keys throwingInvalidCastException. String leaves inside aJSONcolumn are now always decoded as UTF-8 text regardless of the setting — remove any base64 workaround; the setting is unchanged for ordinaryString/FixedStringcolumns. - Fixed named
TupleandNestedcolumns being unreadable when an element name requires backtick quoting and contains a space — for exampleTuple(`p q` Int64, r String)orNested(`a b` Decimal(10, 2), c String). Reading such a column failed withArgumentException: Unknown type, because the type parser split the element declaration on its first space and so cut the quoted name in half. The element name is now skipped as a whole before the name/type separator is located. - Fixed the type parser building a self-referential node for an empty parameter list, such as the
Tuple()column type that ClickHouse accepts and reports back insystem.columns. - Fixed enum type names rendering as invalid ClickHouse syntax. Enum labels are now quoted and escaped, and the declaration includes its closing parenthesis.
- Fixed HTTP query parameters mangling a
byte[]/ReadOnlyMemory<byte>bound toString/FixedString(abyte[]was sent as the literal textSystem.Byte[]) and rejecting aTimeOnlybound toTime/Time64. Byte payloads are now escaped byte-for-byte, so data that is not valid UTF-8 round-trips losslessly;TimeOnlybinds on both the HTTP and binary write paths, and infers asTime64(7)when no type hint is given. - Fixed
{name:Type}parameter type hints being mis-detected in queries containing//comments, nested block comments, backtick/double-quoted identifiers, backslash escapes or$tag$heredocs. A bare#no longer starts a comment (only#and#!do). - Fixed
{name:Type}parameter type hints being dropped, or a hint being invented for a parameter that does not exist, when the query contains another{that is not a type hint — for example aSETTINGSmap value such asadditional_table_filters = {'t': 'a > 0'}. A dropped hint fell back to CLR-type inference, losing precision. - Fixed ADO-style
@nameparameter placeholders being rewritten inside string literals, quoted identifiers, heredocs and comments, which corrupted values such as'user@id'into'user{id:Int32}'. Placeholders are now only replaced in code positions. - Fixed a
$inside an unquoted identifier being mistaken for the start of a$tag$heredoc. Everything up to the next occurrence of the same$...$text was skipped as heredoc body, silently dropping any{name:Type}type hint (and any ADO-style@nameplaceholder) in between — for example inWITH 1 AS b$c$ SELECT {d:Date} AS v, b$c$ AS x. A heredoc is now only recognized where a token starts, matching the server lexer. - Fixed ADO-style
@nameplaceholders not working when the parameter name contains a$, which ClickHouse accepts in a query parameter name:@id$xcould not be bound at all (the name was interpolated into a regex, where$is an end-of-input anchor), and a shorter name won over a longer one, so with onlyiddefinedSELECT @id$xwas silently rewritten into a different, still valid query that aliased the value as$xinstead of being left for the server to reject. A$is now part of the placeholder name, matching the server lexer.
New Features:
- POCO reads: stream query results directly into your own classes.
ClickHouseClient.QueryAsync<T>(...)returnsIAsyncEnumerable<T>; rows are materialized lazily and the underlying reader is disposed when enumeration completes, faults, or stops early.ClickHouseDataReader.MapTo<T>()materializes the current row into a registered POCO without advancing the reader.- Registration: use
RegisterPocoType<T>(), it sets up both the insert and read mappings, validating both up front.RegisterBinaryInsertType<T>()is unchanged and remains insert-only for backwards compatibility. - Type requirements: a public parameterless constructor and at least one public property with a public non-init setter.
requiredproperties are supported. - Column matching is case-sensitive (
StringComparer.Ordinal); missing result columns leave properties at their default value, extra result columns are ignored. - No automatic conversions: type mismatches throw
InvalidOperationExceptionwith the POCO type, property, column, and returned CLR type. Static mismatches fail fast at firstMapTo<T>()call (or first iteration ofQueryAsync<T>()) before any rows are materialized. - Registration diagnostics: when a
LoggerFactoryis configured,RegisterPocoType<T>()/RegisterBinaryInsertType<T>()emit aDebug-level log (categoryClickHouse.Driver.Client) listing which properties mapped to which columns and which were skipped and why.
- Nested array parameters and multidimensional arrays (issue #320):
Array(Array(T))and deeper nestings are now supported end-to-end via parameterized queries, binary inserts, and bulk inserts. Both jagged CLR shapes (T[][],List<List<T>>) and rectangular multidimensional CLR shapes (T[,],T[,,], …) are accepted on the write path. Reads return jaggedT[][]viaGetValue; callers who know their data is rectangular can usereader.GetFieldValue<T[,]>(ordinal)and the driver materialises the column as that CLR shape (throwsInvalidOperationExceptionon ragged data). - ValueTuple support on write path:
System.ValueTuplevalues (C# tuple literals like(1, "hello")) are now supported in binary inserts, HTTP parameterized queries, and automatic type inference. Tuples with more than 7 elements are correctly flattened from the compiler-generated rest-nesting structure. Note: if you need exactly 7 scalar elements followed by a nested tuple as the 8th element, wrap the inner tuple in an extra layer (e.g.,Tuple.Create(1,...,7, Tuple.Create(Tuple.Create("a","b")))) so the driver can distinguish it from TRest nesting. - Configurable parameter value formatting: new
IParameterFormatterinterface allowing configuration of how parameter values are serialized for HTTP transport (sibling toIParameterTypeResolver, which governs type resolution). SetParameterFormatteronClickHouseClientSettingsto override the built-in serialization logic for any CLR type (e.g., customDateTimeprecision, decimal culture, string escaping). Includes one implementation,DictionaryParameterFormatter, for simple CLR-type → format-function mappings. Returnnullfrom the formatter to fall through to the built-in formatter. Can also be set per-query viaQueryOptions.ParameterFormatter. The formatter is also invoked for every element inside composite values (Array, Tuple, Map, Nested); see docs for quoting caveats when formatting string-like types inside composites. - Per-query
Accept-Encodingoverride: newQueryOptions.AcceptEncoding(mirrored onClickHouseCommand.AcceptEncoding) replaces the defaultgzip, deflateAccept-Encoding header for a single request. Supports multiple algorithms with quality weights (e.g."zstd, gzip;q=0.5") and forcesenable_http_compression=1on the URL so ClickHouse honours the header. For codecs the BCL cannot decode (zstd, lz4) the underlyingHttpClientmust be configured withAutomaticDecompression = Noneand the body consumed viaExecuteRawResultAsync. ClickHouseRawResult.ContentEncoding: exposes the response body'sContent-Encodingfor callers usingExecuteRawResultAsyncto decode it themselves;identityis normalized tonull.- Customizable read value conversion: new
IReadValueConverterinterface allows transformation of values returned by the data reader after deserialization (e.g.,DateTime.SpecifyKindto set UTC kind, string trimming/normalization). SetReadValueConverteronClickHouseClientSettingsto apply transformations globally, or override per-query viaQueryOptions.ReadValueConverter. The converter interceptsGetValue()andGetFieldValue<T>()calls, and is also applied during POCO materialization (MapTo<T>()/QueryAsync<T>()). When no converter is set, there is zero performance overhead. Includes one implementation,DictionaryReadValueConverter, with a fluent.For<T>(value => …)registration for dispatch based on CLR type. - Added a
ClickHouseDataReader.GetFieldValue<T>(string name)overload that resolves the column by name, complementing the existing ordinal-based overload. - Application-identity tagging in User-Agent: new
ClickHouseClientSettings.ApplicationInfoproperty: anIReadOnlyDictionary<string, string>of free-form tags (e.g.app,ver,env) appended to the HTTPUser-Agentheader as a comment token (e.g.(app:MyApp; ver:2.3.1; env:prod)). - Server-side
Identifierquery-parameter type:{name:Identifier}parameters (and explicitClickHouseDbParameter.ClickHouseType = "Identifier") are now supported, letting you safely bind a database/table/column name instead of a quoted string literal — e.g.CREATE DATABASE {name:Identifier}orSELECT {col:Identifier} FROM t. The value is sent verbatim and the server substitutes it as a bare SQL identifier, applying its own backtick quoting/escaping, so identifiers containing special characters (including backticks) round-trip safely with no client-side escaping. Previously these threwArgumentException: Unknown type: Identifier(surfaced from clickhouse-go#1635). - Configurable response read buffer size: new
ClickHouseClientSettings.ReadBufferSize(and connection-string keyReadBufferSize) controls the size of the buffer used when reading HTTP query responses. Behavioral change: the default has been lowered from 512 KiB to 8 KiB. The previous 512KiB size exceeded the large object heap limit and could cause substantial performance issues due to GC. Workloads with large responses that prefer fewer buffer refills can set a larger value.
Improvements:
- HTTP parameter mismatch errors now include the parameter name, the full ClickHouse type, and the value's runtime CLR type. The previous message (
"Cannot convert 219 to Array(UInt8)") collapsed the outer type, omitted which parameter failed, and didn't say what the value actually was. GetFieldValue<T[,]>errors are now categorised by cause.InvalidCastExceptioncovers type-structure mismatches: the column is null/DBNull, the value isn't a collection, the source's structural depth differs from the target rank (shallower or deeper), or a leaf is the wrong scalar type ornullinto a non-nullable value-type target. Messages include the column ordinal, target CLR type, and offending indices where applicable.InvalidOperationExceptionis reserved for shape-validation failures — the value's structure matchesTbut rows are ragged or an intermediate row is null. Previously, structural-depth mismatches and anullleaf into e.g.int[,]either threwInvalidOperationExceptionor were silently coerced todefault(int).
Bug Fixes:
- Breaking Change: Fixed timezone shift for
@-styleDateTime/DateTimeOffsetparameters on non-UTC servers (issue #350). When a parameter's ClickHouse type was inferred (no explicit{name:Type}hint in SQL and noparameter.ClickHouseTypeset), the driver previously emitted a bareDateTimehint and the server parsed the wire wall-clock insession_timezone/server tz — silently shifting instant-bearing values by the server's offset. Inferred types forDateTime { Kind: Utc or Local }andDateTimeOffsetare now sent asDateTime('UTC'), preserving the instant across any server timezone. Explicit hints ({name:DateTime}in SQL orparameter.ClickHouseType) are untouched, users who specify the type continue to own timezone correctness. - Fixed silent 32-bit wraparound on the binary write path for
DateTime/DateTime32values outside ClickHouse's supported range (1970-01-01 .. 2106-02-07 06:28:15 UTC). Out-of-range values, e.g.new DateTime(1900, 1, 1), were being cast through a 32-bit signed int and reinterpreted asUInt32by the server, producing real-but-wrong timestamps (e.g.2036-02-07). The same audit fixed analogous silent out-of-range writes forDate32(server would clamp to[1900-01-01, 2299-12-31]) and tightened the existingDateOverflowExceptioninto a descriptiveArgumentOutOfRangeException. All four types now throwArgumentOutOfRangeExceptionatWritetime naming the column type and supported range. - Fixed silent wall-clock shift for
DateTimeandDateTime64columns declared with ClickHouse's syntheticFixed/UTC±HH:MM:SStimezone names (e.g.DateTime('Fixed/UTC+05:30:00')). These names are not in the IANA TZDB, soGetZoneOrNullreturnednull, causing the driver to interpret the stored instant as UTC and return a value shifted by the column's offset. The driver now recognises theFixed/UTCpattern and constructs a correct fixed-offsetDateTimeZonefrom it (issue #370). - Fixed HTTP parameter serialization for
Date,DateTime, andDateTime64values inside composite types such asArray,Tuple,Map, andVariant. These values are now quoted correctly when sent over HTTP. - Fixed type inference for
System.Tuplewith more than 7 elements. The TRest nesting was not being flattened, causing the 8th+ elements to be inferred as nested tuple types instead of their actual flat types. This could lead to incorrect ClickHouse type inference and serialization errors. - Fixed parsing of enum labels containing escaped quotes, parentheses, and
=characters. This fixes cases likevariantType()onVariant(String, DateTime('UTC')), which could previously round-trip through the driver as an empty string. - Fixed
ClickHouseServerException.Messagereturning compressed binary bytes when the server returned a non-2xx response with aContent-Encodingheader. The driver now decompresses gzip / deflate / brotli error bodies before attaching them to the exception. Unknown codecs (zstd, lz4) yield a placeholder pointing atsystem.query_log.
New Features:
- POCO binary inserts: new
InsertBinaryAsync<T>overload onClickHouseClientacceptsIEnumerable<T>directly, mapping public properties to columns automatically. Register types upfront withRegisterBinaryInsertType<T>(). Customize column names and ClickHouse types with[ClickHouseColumn(Name = "...", Type = "...")], or exclude properties with[ClickHouseNotMapped]. When all properties specify explicit types via the attribute, the schema probe is skipped entirely. - Configurable parameter type resolution: new
IParameterTypeResolverinterface allowing configuration of type mapping for@-style parameterized queries. SetParameterTypeResolveronClickHouseClientSettingsto override how .NET types are mapped to ClickHouse types (e.g.,DateTime→DateTime64(3),decimal→Decimal64(4)). Includes one implementation,DictionaryParameterTypeResolverfor simple type→type mappings, and supports custom implementations for value-aware or name-based resolution. Can also be set per-query viaQueryOptions.ParameterTypeResolver.
Improvements:
- Type inference now inspects
IPAddress.AddressFamilyto correctly distinguish between IPv4 and IPv6 types. Previously, allIPAddressvalues were inferred as IPv4. This also works for collections, tuples, and maps containingIPAddressvalues.
Internal Improvements:
- Centralized parameter type resolution into
ParameterTypeResolution, replacing previously scattered logic inClickHouseDbParameter.QueryFormandHttpParameterFormatter. Each parameter's type is now resolved exactly once per request, ensuring consistency between SQL placeholder generation and HTTP value formatting.
Bug Fixes:
JsonReadModeandJsonWriteModewill now correcly set the corresponding settings when set toBinarymode.
New Features:
InsertOptions.ColumnTypes: provide a dictionary of column name → ClickHouse type string to skip the schema probe query (SELECT ... WHERE 1=0) entirely. Ideal when the table schema is known at compile time.InsertOptions.UseSchemaCache: whentrue, the full table schema is cached per (database, table) for the lifetime of theClickHouseClientinstance. Subsequent inserts to the same table reuse the cached schema regardless of which columns are selected, eliminating redundant round-trips.
Breaking Changes:
InsertBinaryAsyncnow throwsInvalidOperationExceptionwhen sessions are enabled andMaxDegreeOfParallelism > 1. ClickHouse only allows one concurrent query per session, so parallel batch inserts would causeSESSION_IS_LOCKEDerrors and partial writes. This also affects the deprecatedClickHouseBulkCopy, which defaults toMaxDegreeOfParallelism = 4. To fix, setMaxDegreeOfParallelismto 1, or disable sessions for the insert viaInsertOptions.UseSession = false.
Bug Fixes:
- Fixed
IndexOutOfRangeExceptionwhen reading NULL values fromVariantcolumns. The VariantNonediscriminator (used for NULLs) was not handled, causing an out-of-bounds array access instead of returningDBNull.Value. - Fixed writing NULL values to
Variantcolumns. Writing null/DBNull now correctly emits theNonediscriminator (0xFF) for binary writes, and null marker\Nwhen using HTTP parameters. Note: null Variant HTTP parameter parsing is broken in server versions prior to 26.3.
Bug Fixes:
- Fixed
QUERY_WITH_SAME_ID_IS_ALREADY_RUNNINGerrors when usingInsertBinaryAsyncwith aQueryId. The schema probe and all batch inserts were sharing the same query ID. The schema probe now uses the base query ID, and each batch insert receives a unique suffixed ID ({queryId}-1,{queryId}-2, etc.).
- Marked ClickHouseConnection.ServerVersion property as Obsolete.
Documentation and Usage Examples: Coinciding with the 1.0.0 release of the driver, we have greatly expanded the documentation and usage examples.
- Documentation: https://clickhouse.com/docs/integrations/csharp
- Usage examples: https://github.com/ClickHouse/clickhouse-cs/tree/main/examples
New: ClickHouseClient - Simplified Primary API
ClickHouseClient is the new recommended way to interact with ClickHouse. Thread-safe, singleton-friendly, and simpler than ADO.NET classes.
using var client = new ClickHouseClient("Host=localhost");| Method | Description |
|---|---|
ExecuteNonQueryAsync |
Execute DDL/DML (CREATE, INSERT, ALTER, DROP) |
ExecuteScalarAsync |
Return first column of first row |
ExecuteReaderAsync |
Stream results via ClickHouseDataReader |
InsertBinaryAsync |
High-performance bulk insert (replaces ClickHouseBulkCopy) |
ExecuteRawResultAsync |
Get raw result stream bypassing the parser |
InsertRawStreamAsync |
Insert from stream (CSV, JSON, Parquet, etc.) |
PingAsync |
Check server connectivity |
CreateConnection() |
Get ClickHouseConnection for ORM compatibility |
Per-query configuration via QueryOptions.
Parameters via ClickHouseParameterCollection:
var parameters = new ClickHouseParameterCollection();
parameters.Add("id", 42UL);
await client.ExecuteReaderAsync("SELECT * FROM t WHERE id = {id:UInt64}", parameters);Deprecation: ClickHouseBulkCopy is deprecated. Use client.InsertBinaryAsync(table, columns, rows) instead.
Breaking Changes:
-
Dropped support for .NET Framework and .NET Standard. The library now targets only
net6.0,net8.0,net9.0, andnet10.0. Removed support fornet462,net48, andnetstandard2.1. If you are using .NET Framework, you will need to stay on the previous version or migrate to .NET 6.0+. -
Removed feature discovery query from
OpenAsync. The connection'sOpenAsync()method no longer executesSELECT version()to discover server capabilities. This makes connection opening instantaneous (no network round-trip) but removes theSupportedFeaturesproperty fromClickHouseConnection. TheServerVersionproperty now throwsInvalidOperationException.Migration guidance: If you need to check the server version:
using var reader = await connection.ExecuteReaderAsync("SELECT version()"); reader.Read(); var version = reader.GetString(0);
-
DateTime reading behavior changed for columns without explicit timezone. Previously,
DateTimecolumns without a timezone (e.g.,DateTimevsDateTime('Europe/Amsterdam')) would use the server timezone (withUseServerTimezone=true) or client timezone to interpret the stored value. Now, these columns returnDateTimewithKind=Unspecified, preserving the wall-clock time exactly as stored without making assumptions about timezone.Column Type Old Behavior New Behavior DateTime(no timezone)Returned with server/client timezone applied DateTimewithKind=UnspecifiedDateTime('UTC')DateTimewithKind=UtcDateTimewithKind=Utc(unchanged)DateTime('Europe/Amsterdam')DateTimewithKind=UnspecifiedDateTimewithKind=Unspecified(unchanged). Reading as DateTimeOffset has correct offset applied.Migration guidance: If you need timezone-aware behavior, either:
- Use explicit timezones in your column definitions:
DateTime('UTC')orDateTime('Europe/Amsterdam') - Apply the timezone yourself after reading.
- Use explicit timezones in your column definitions:
-
DateTime writing now respects
DateTime.Kindproperty. Previously, allDateTimevalues were treated as wall-clock time in the target column's timezone regardless of theirKindproperty. The new behavior:DateTime.Kind Old Behavior New Behavior UtcTreated as wall-clock time in column timezone Preserved as-is (instant is maintained) LocalTreated as wall-clock time in column timezone Instant is maintained (inserted as UTC timestamp) UnspecifiedTreated as wall-clock time in column timezone Treated as wall-clock time in column timezone (unchanged) Migration guidance: If you were relying on the old behavior where UTC
DateTimevalues were reinterpreted in the column timezone, you should change these toDateTimeKind.Unspecified:// Old code (worked by accident): var utcTime = DateTime.UtcNow; // Would be reinterpreted in column timezone // New code (explicit intent): var wallClockTime = DateTime.SpecifyKind(myTime, DateTimeKind.Unspecified);
Important: When using parameters, you must specify the timezone in the parameter type hint to have string values interpreted in the column timezone:
command.AddParameter("dt", myDateTime); // Correct: timezone in type hint ensures proper interpretation command.CommandText = "INSERT INTO table (dt_column) VALUES ({dt:DateTime('Europe/Amsterdam')})"; // Gotcha: without timezone hint, UTC is used for interpretation command.CommandText = "INSERT INTO table (dt_column) VALUES ({dt:DateTime})"; // ^ String value interpreted in UTC, not column timezone!
This differs from bulk copy operations where the column timezone is known and used automatically.
-
Removed
UseServerTimezonesetting. This setting has been removed from the connection string,ClickHouseClientSettings, andClickHouseConnectionStringBuilder. It no longer has any effect since columns without timezones now returnUnspecifiedDateTime values without any timezone changes applied to what is returned from the server. -
Moved
ServerTimezoneproperty fromClickHouseConnectiontoClickHouseCommand. The server timezone is now available onClickHouseCommand.ServerTimezoneafter any query execution (the timezone is now extracted from theX-ClickHouse-Timezoneresponse header instead of requiring a separate query). -
Helper and extension methods made internal: DateTimeConversions, DataReaderExtensions, DictionaryExtensions, EnumerableExtensions, MathUtils, StringExtensions.
-
JSON writing default behavior changed. The default
JsonWriteModehas changed fromBinarytoString. This affects how JSON data is written to ClickHouse:Input Type Old Default (Binary) New Default (String) JsonObject/JsonNodeBinary encoding Serialized via JsonSerializer.Serialize()stringBinary encoding (parsed client-side) Passed through directly POCO (registered) Binary encoding with type hints Serialized via JsonSerializer.Serialize()POCO (unregistered) Exception Serialized via JsonSerializer.Serialize()Impact if you don't modify your code:
- JSON writing will still work, but uses string serialization instead of binary encoding; the JSON string will be parsed on the server instead of the client. This could lead to subtle changes in paths without type hints, e.g., values previously parsed as ints may be parsed as longs.
ClickHouseJsonPathandClickHouseJsonIgnoreattributes are ignored in String mode (they only work in Binary mode). Serialization happens viaSystem.Text.Json, so you can use those attributes instead.- Server setting
input_format_binary_read_json_as_string=1is automatically set when using String write mode
New Features/Improvements:
-
Automatic parameter type extraction from SQL. Types specified in the SQL query using
{name:Type}syntax are now automatically used for parameter formatting, eliminating the need to specify the type twice:// Before: type specified twice command.CommandText = "SELECT {dt:DateTime('Europe/Amsterdam')}"; command.AddParameter("dt", "DateTime('Europe/Amsterdam')", value); // After: type extracted from SQL automatically command.CommandText = "SELECT {dt:DateTime('Europe/Amsterdam')}"; command.AddParameter("dt", value);
The
AddParameter(name, type, value)overload is now marked obsolete. UseAddParameterWithTypeOverride()if you need to explicitly override the SQL type hint. -
POCO serialization support for JSON columns. When writing POCOs to JSON columns with typed hints (e.g.,
JSON(id Int64, name String)), the driver serializes properties using the hinted types for full type fidelity. Properties without a corresponding hinted path will have their ClickHouse types inferred automatically. Two attributes are available:[ClickHouseJsonPath("path")]for custom JSON paths and[ClickHouseJsonIgnore]to exclude properties. Property name matching to hint paths is case-sensitive (matching ClickHouse behavior which allows paths likeuserNameandUserNameto coexist). Register types viaclient.RegisterJsonSerializationType<T>(). -
JsonReadModeandJsonWriteModeconnection string settings for configurable JSON handling:JsonReadMode.Binary(default): ReturnsSystem.Text.Json.Nodes.JsonObjectJsonReadMode.String: Returns raw JSON string. Sets server settingoutput_format_binary_write_json_as_string=1.JsonWriteMode.String(default): AcceptsJsonObject,JsonNode, strings, and any object (serialized viaSystem.Text.Json.JsonSerializer). Sets server settinginput_format_binary_read_json_as_string=1.JsonWriteMode.Binary: Only accepts registered POCO types with full type hint support and custom path attributes. WritingstringorJsonNodevalues withJsonWriteMode.Binarythrows an exception.
-
QBit data type support. QBit is a transposed vector column, designed to allow the user to choose a desired quantization level at runtime, speeding up approximate similarity searches. See the GitHub repo for usage examples.
-
Dynamic type binary writing support via
InsertBinaryAsync. Values are automatically type-inferred from their .NET types and serialized with the appropriate binary type header. Supports all common types including integers, floating point, strings, booleans, DateTime, Guid, decimal, arrays, lists, and dictionaries. -
Binary data in String/FixedString columns. Write
byte[],ReadOnlyMemory<byte>, orStreamvalues to String and FixedString columns viaInsertBinaryAsync. Read binary data back using theReadStringsAsByteArraysconnection string setting, which returns String columns asbyte[]instead ofstring. Useful for storing binary data that may not be valid UTF-8. -
First-class support for roles, with query-level override.
-
Custom HTTP headers at the connection level for proxy/infrastructure integration.
-
Support for JWT authentication, with query-level override.
-
Mid-stream exception detection via
X-ClickHouse-Exception-Tagheader (ClickHouse 25.11+). Whenhttp_write_exception_in_output_formatis set to 0 on the server, exceptions that occur while streaming results are now properly detected and thrown asClickHouseServerException(which includes the exception message) instead ofEndOfStreamException. -
Query ID auto-generation. When the query ID has not been set, it will now be automatically generated by the client.
-
AddParameter()convenience method forClickHouseParameterCollection, simplifying parameter creation.
Bug Fixes:
- Fixed a crash when reading a Map with duplicate keys. The current behavior is to return only the last value for a given key.
Breaking Changes:
- FixedString is now returned as byte[] rather than String. FixedStrings are not necessarily valid UTF-8 strings, and the string transformation caused loss of information in some cases. Use Encoding.UTF8.GetString() on the resulting byte[] array to emulate the old behavior. String can still be used as a parameter or when inserting using BulkCopy into a FixedString column. When part of a json object, FixedString is still returned as a string.
- Removed obsolete MySQL compatibility mapping TIME -> Int64.
- Json serialization of bool arrays now uses the Boolean type instead of UInt8 (it is now consistent with how bool values outside arrays were handled).
- GEOMETRY is no longer an alias for String.
New Features/Improvements:
- Sessions can now be used with custom HttpClient or HttpClientFactory. Previously this combination was not allowed. Note that when sessions are enabled, ClickHouseConnection will allow only one request at a time, and responses are fully buffered before returning to ensure proper request serialization.
- Added support for BFloat16. It is converted to and from a 32-bit float.
- Added support for Time and Time64, which are converted to and from TimeSpan. The types are available since ClickHouse 25.6 and using them requires the enable_time_time64_type flag to be set.
- The Dynamic type now offers full support for all underlying types.
- Added support for LineString and MultiLineString geo types.
- Added support for the Geometry type, which can hold any geo subtype (Point, Ring, LineString, Polygon, MultiLineString, MultiPolygon). Available since ClickHouse 25.11. Requires allow_suspicious_variant_types to be set to 1.
- Json support has been improved in many ways:
- Now supports parsing Json that includes Maps; they are read into JsonObjects.
- Added support for decoding BigInteger types, UUID, IPv4, IPv6, and ClickHouseDecimal types (they are handled as strings).
- Expanded binary parsing to cover all types.
- Improved handling of numeric types when writing Json using BulkCopy: now properly detects and preserves Int32/In64 in addition to double (previously all numeric types were handled as double).
- Parsing null values in arrays is now handled properly.
- ClickHouseConnection.ConnectionString can now be set after creating the connection, to support cases where passing the connection string to the constructor is not possible.
- ClickHouseConnection.CreateCommand() now has an optional argument for the command text.
- Fixed a NullReferenceException when adding a parameter with null value and no provided type. The driver now simply sends '\N' (null value special character) when encountering this scenario.
Bug Fixes:
- Fixed a bug where serializing to json with an array of bools with both true and false elements would fail.
Improvements:
- Fixed NuGet readme file.
Breaking Changes:
- Trying to set ClickHouseConnection.ConnectionString will now throw a NotSupportedException. Create a new connection with the desired settings instead.
- When a default database is not provided, the client no longer uses "default" (now uses empty string). This allows default user database settings to function as expected.
- ClickHouseDataSource.Logger (ILogger) property changed to LoggerFactory (ILoggerFactory).
- Removed support for loading configuration from environment variables (CLICKHOUSE_DB, CLICKHOUSE_USER, CLICKHOUSE_PASSWORD). Use connection strings or ClickHouseClientSettings instead.
- The default PooledConnectionIdleTimeout has been changed to 5 seconds, to prevent issues with half-open connections when using ClickHouse Cloud (where the default server-side idle timetout is 10s).
New Features:
- Added .NET 10 as a target.
- The NuGet package is now signed.
- Enabled strong naming for the library.
- Added a new way to configure ClickHouseConnection: the ClickHouseClientSettings class. You can initialize it from a connection string by calling ClickHouseClientSettings.FromConnectionString(), or simply by setting its properties.
- Added settings validation to prevent incorrect configurations.
- Added logging in the library, enable it by passing a LoggerFactory through the settings. Logging level configuration is configured through the factory. For more info, see the documentation: https://clickhouse.com/docs/integrations/language-clients/csharp/http#logging-and-diagnostics
- Added EnableDebugMode setting to ClickHouseClientSettings for low-level .NET network tracing (.NET 5+). When enabled, traces System.Net events (HTTP, Sockets, DNS, TLS) to help diagnose network issues. Requires ILoggerFactory with Trace-level logging enabled. WARNING: Significant performance impact - not recommended for production use.
- AddClickHouseDataSource now automatically injects ILoggerFactory from the service provider when not explicitly provided.
- Improvements to ActivitySource for tracing: stopped adding tags when it was not necessary, and made it configurable through ClickHouseDiagnosticsOptions.
- Added new AddClickHouseDataSource extension methods that accept ClickHouseClientSettings for strongly-typed configuration in DI scenarios.
- Added new AddClickHouseDataSource extension method that accepts IHttpClientFactory for better DI integration.
- Optimized response header parsing.
- Added list type conversion, so List can now be passed to the library (converts to Array() in ClickHouse). Thanks to @jorgeparavicini.
- Optimized EnumType value lookups.
- Avoid unnecessarily parsing the X-ClickHouse-Summary headers twice. Thanks to @verdie-g.
- Added the ability to pass a query id to ClickHouseConnection.PostStreamAsync(). Thanks to @dorki.
- The user agent string now also contains information on the host operating system, .NET version, and processor architecture.
Bug fixes:
- Fixed a crash when processing a tuple with an enum in it.
- Fixed a potential sync-over-async issue in the connection. Thanks to @verdie-g.
- Fixed a bug with parsing table definitions with parametrized json fields. Thanks to @dorki.