New Features:
- 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.
Bug Fixes:
- 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 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 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.
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.