diff --git a/docs/integrations/language-clients/csharp.md b/docs/integrations/language-clients/csharp.md index 6e04ad14319..dc300718b7d 100644 --- a/docs/integrations/language-clients/csharp.md +++ b/docs/integrations/language-clients/csharp.md @@ -7,24 +7,28 @@ description: 'The official C# client for connecting to ClickHouse.' title: 'ClickHouse C# Driver' doc_type: 'guide' integration: - - support_level: 'core' - - category: 'language_client' - - website: 'https://github.com/ClickHouse/clickhouse-cs' + support_level: 'core' + category: 'language_client' + website: 'https://github.com/ClickHouse/clickhouse-cs' --- # ClickHouse C# Client -The official C# client for connecting to ClickHouse. +The official C# client for connecting to ClickHouse. The client source code is available in the [GitHub repository](https://github.com/ClickHouse/clickhouse-cs). Originally developed by [Oleg V. Kozlyuk](https://github.com/DarkWanderer). -## Migration guide {#migration-guide} -1. Update `.csproj` with `ClickHouse.Driver` name and [the latest version of the package](https://www.nuget.org/packages/ClickHouse.Driver). -2. Update your code to use the new `ClickHouse.Driver` namespace and classes. +## Migration Guide {#migration-guide} + +1. Update your `.csproj` file with the new package name `ClickHouse.Driver` and [the latest version on NuGet](https://www.nuget.org/packages/ClickHouse.Driver). +2. Update all `ClickHouse.Client` references to `ClickHouse.Driver` in your codebase. + +--- ## Supported .NET Versions {#supported-net-versions} `ClickHouse.Driver` supports the following .NET versions: + * .NET Framework 4.6.2 * .NET Framework 4.8 * .NET Standard 2.1 @@ -32,26 +36,25 @@ Originally developed by [Oleg V. Kozlyuk](https://github.com/DarkWanderer). * .NET 8.0 * .NET 9.0 +--- + ## Installation {#installation} Install the package from NuGet: - ```bash dotnet add package ClickHouse.Driver ``` Or using the NuGet Package Manager: - ```bash Install-Package ClickHouse.Driver ``` +--- + ## Usage {#usage} ### Creating a Connection {#creating-a-connection} - -Create a connection using a connection string: - ```csharp using ClickHouse.Driver.ADO; @@ -63,94 +66,130 @@ using (var connection = new ClickHouseConnection(connectionString)) } ``` -### Creating a Table {#creating-a-table} +--- + +### Connection String Parameters {#connection-string} + +| Parameter | Description | Default | +| ------------------- | ----------------------------------------------- | ------------------- | +| `Host` | ClickHouse server address | `localhost` | +| `Port` | Server port (`8123` for HTTP, `8443` for HTTPS) | Depends on protocol | +| `Database` | Initial database | `default` | +| `Username` | Authentication username | `default` | +| `Password` | Authentication password | *(empty)* | +| `Protocol` | Connection protocol (`http` or `https`) | `http` | +| `Compression` | Enables GZip compression | `true` (v2.0+) | +| `UseSession` | Enables persistent server session | `false` | +| `SessionId` | Custom session ID | Random GUID | +| `Timeout` | HTTP timeout (seconds) | `120` | +| `UseServerTimezone` | Use server timezone for datetime columns | `true` (≥6.0.0) | +| `UseCustomDecimals` | Use `ClickHouseDecimal` for decimals | `false` | + +> **Note:** When `UseSession` is enabled, only one active query per connection is allowed (server limitation). +> Session expires after 60s of inactivity by default. + +--- + +### Connection Lifetime & Pooling {#connection-lifetime} + +`ClickHouse.Driver` uses `System.Net.Http.HttpClient` internally. +Each endpoint maintains a small connection pool (2 TCP connections by default). +Disposing a `ClickHouseConnection` doesn't immediately close underlying sockets. + +**Recommendations:** -Create a table using standard SQL syntax: +* Create one `ClickHouseConnection` per large transaction. +* Reuse connections for multiple queries. +* Use `IHttpClientFactory` for DI scenarios: +```csharp + new ClickHouseConnection(connectionString, httpClientFactory, "clickhouse"); +``` + +--- +### Creating a Table {#creating-a-table} ```csharp using ClickHouse.Driver.ADO; using (var connection = new ClickHouseConnection(connectionString)) { connection.Open(); - - using (var command = connection.CreateCommand()) - { - command.CommandText = "CREATE TABLE IF NOT EXISTS default.my_table (id Int64, name String) ENGINE = Memory"; - command.ExecuteNonQuery(); - } + using var command = connection.CreateCommand(); + command.CommandText = "CREATE TABLE IF NOT EXISTS default.my_table (id Int64, name String) ENGINE = Memory"; + command.ExecuteNonQuery(); } ``` -### Inserting Data {#inserting-data} - -Insert data using parameterized queries: +--- +### Inserting Data {#inserting-data} ```csharp using ClickHouse.Driver.ADO; using (var connection = new ClickHouseConnection(connectionString)) { connection.Open(); - - using (var command = connection.CreateCommand()) - { - command.AddParameter("id", "Int64", 1); - command.AddParameter("name", "String", "test"); - command.CommandText = "INSERT INTO default.my_table (id, name) VALUES ({id:Int64}, {name:String})"; - command.ExecuteNonQuery(); - } + using var command = connection.CreateCommand(); + command.AddParameter("id", "Int64", 1); + command.AddParameter("name", "String", "test"); + command.CommandText = "INSERT INTO default.my_table (id, name) VALUES ({id:Int64}, {name:String})"; + command.ExecuteNonQuery(); } ``` -### Bulk Insert {#bulk-insert} +--- +### Bulk Insert {#bulk-insert} ```csharp using ClickHouse.Driver.ADO; using ClickHouse.Driver.Copy; -using (var connection = new ClickHouseConnection(connectionString)) +using var connection = new ClickHouseConnection(connectionString); +connection.Open(); + +using var bulkInsert = new ClickHouseBulkCopy(connection) { - connection.Open(); + DestinationTableName = "default.my_table", + MaxDegreeOfParallelism = 2, + BatchSize = 100 +}; - using var bulkInsert = new ClickHouseBulkCopy(connection) - { - DestinationTableName = "default.my_table", - MaxDegreeOfParallelism = 2, - BatchSize = 100 - }; - - var values = Enumerable.Range(0, 100).Select(i => new object[] { (long)i, "value" + i.ToString() }); - await bulkInsert.WriteToServerAsync(values); - Console.WriteLine($"Rows written: {bulkInsert.RowsWritten}"); -} +var values = Enumerable.Range(0, 100) + .Select(i => new object[] { (long)i, "value" + i }); + +await bulkInsert.WriteToServerAsync(values); +Console.WriteLine($"Rows written: {bulkInsert.RowsWritten}"); ``` -### Performing SELECT Queries {#performing-select-queries} +**Notes:** -Execute SELECT queries and process results: +* Uses TPL with up to 4 parallel insertion tasks. +* `Columns`, `BatchSize`, and `MaxDegreeOfParallelism` are configurable. +* Automatically queries target table schema (`SELECT * FROM LIMIT 0`) to verify types. +* When using sessions, set `MaxDegreeOfParallelism = 1`. +--- + +### Performing SELECT Queries {#performing-select-queries} ```csharp -using ClickHouse.Client.ADO; -using System.Data; +using ClickHouse.Driver.ADO; -using (var connection = new ClickHouseConnection(connectionString)) +using var connection = new ClickHouseConnection(connectionString); +connection.Open(); + +using var command = connection.CreateCommand(); +command.AddParameter("id", "Int64", 10); +command.CommandText = "SELECT * FROM default.my_table WHERE id < {id:Int64}"; +using var reader = command.ExecuteReader(); +while (reader.Read()) { - connection.Open(); - - using (var command = connection.CreateCommand()) - { - command.AddParameter("id", "Int64", 10); - command.CommandText = "SELECT * FROM default.my_table WHERE id < {id:Int64}"; - using var reader = command.ExecuteReader(); - while (reader.Read()) - { - Console.WriteLine($"select: Id: {reader.GetInt64(0)}, Name: {reader.GetString(1)}"); - } - } + Console.WriteLine($"Id: {reader.GetInt64(0)}, Name: {reader.GetString(1)}"); } ``` -### Raw streaming {#raw-streaming} + +--- + +### Raw Streaming {#raw-streaming} ```csharp using var command = connection.CreateCommand(); command.Text = "SELECT * FROM default.my_table LIMIT 100 FORMAT JSONEachRow"; @@ -160,62 +199,192 @@ using var reader = new StreamReader(stream); var json = reader.ReadToEnd(); ``` +--- + +### Nested Columns Support {#nested-columns} + +ClickHouse nested types (`Nested(...)`) can be read and written using array semantics. +```sql +CREATE TABLE test.nested ( + id UInt32, + params Nested (param_id UInt8, param_val String) +) ENGINE = Memory +``` +```csharp +using var bulkCopy = new ClickHouseBulkCopy(connection) +{ + DestinationTableName = "test.nested" +}; + +var row1 = new object[] { 1, new[] { 1, 2, 3 }, new[] { "v1", "v2", "v3" } }; +var row2 = new object[] { 2, new[] { 4, 5, 6 }, new[] { "v4", "v5", "v6" } }; + +await bulkCopy.WriteToServerAsync(new[] { row1, row2 }); +``` + +--- + +### AggregateFunction Columns {#aggregatefunction-columns} + +Columns of type `AggregateFunction(...)` cannot be queried or inserted directly. + +To insert: +```sql +INSERT INTO t VALUES (uniqState(1)); +``` + +To select: +```sql +SELECT uniqMerge(c) FROM t; +``` + +--- + +### SQL Parameters {#sql-parameters} + +ClickHouse uses a custom syntax for parameters: +```sql +SELECT * FROM table WHERE id = {id:Int32} +``` + +Notes: + +* Parameters are passed as HTTP query args. +* Avoid excessive parameters (can cause "URL too long"). +* Use Bulk Insert for large datasets. + +--- + ## Supported Data Types {#supported-data-types} -`ClickHouse.Driver` supports the following ClickHouse data types: -**Boolean Type** -* `Bool` (bool) - -**Numeric Types**: -* `Int8` (sbyte) -* `Int16` (short) -* `Int32` (int) -* `Int64` (long) -* `Int128` (BigInteger) -* `Int256` (BigInteger) -* `UInt8` (byte) -* `UInt16` (ushort) -* `UInt32` (uint) -* `UInt64` (ulong) -* `UInt128` (BigInteger) -* `UInt256` (BigInteger) -* `Float32` (float) -* `Float64` (double) -* `Decimal` (decimal) -* `Decimal32` (decimal) -* `Decimal64` (decimal) -* `Decimal256` (BigDecimal) - -**String Types** -* `String` (string) -* `FixedString` (string) - -**Date and Time Types** -* `Date` (DateTime) -* `Date32` (DateTime) -* `DateTime` (DateTime) -* `DateTime32` (DateTime) -* `DateTime64` (DateTime) - -**Network Types** -* `IPv4` (IPAddress) -* `IPv6` (IPAddress) - -**Geo Types** -* `Point` (Tuple) -* `Ring` (Array of Points) -* `Polygon` (Array of Rings) - -**Complex Types** -* `Array` (Array of any type) -* `Tuple` (Tuple of any types) -* `Nullable` (Nullable version of any type) - -### DateTime handling {#datetime-handling} -`ClickHouse.Driver` tries to correctly handle timezones and `DateTime.Kind` property. Specifically: - -`DateTime` values are returned as UTC. User can then convert them themselves or use `ToLocalTime()` method on `DateTime` instance. -When inserting, `DateTime` values are handled in following way: -- UTC `DateTime` are inserted as is, because ClickHouse stores them in UTC internally -- Local `DateTime` are converted to UTC according to user's local timezone settings -- Unspecified `DateTime` are considered to be in target column's timezone, and hence are converted to UTC according to that timezone +`ClickHouse.Driver` supports the following ClickHouse data types with their corresponding .NET type mappings: + +### Boolean Types + +* `Bool` → `bool` + +### Numeric Types + +**Signed Integers:** +* `Int8` → `sbyte` +* `Int16` → `short` +* `Int32` → `int` +* `Int64` → `long` +* `Int128` → `BigInteger` +* `Int256` → `BigInteger` + +**Unsigned Integers:** +* `UInt8` → `byte` +* `UInt16` → `ushort` +* `UInt32` → `uint` +* `UInt64` → `ulong` +* `UInt128` → `BigInteger` +* `UInt256` → `BigInteger` + +**Floating Point:** +* `Float32` → `float` +* `Float64` → `double` + +**Decimal:** +* `Decimal` → `decimal` +* `Decimal32` → `decimal` +* `Decimal64` → `decimal` +* `Decimal128` → `decimal` +* `Decimal256` → `BigDecimal` + +### String Types + +* `String` → `string` +* `FixedString` → `string` + +### Date and Time Types + +* `Date` → `DateTime` +* `Date32` → `DateTime` +* `DateTime` → `DateTime` +* `DateTime32` → `DateTime` +* `DateTime64` → `DateTime` + +### Network Types + +* `IPv4` → `IPAddress` +* `IPv6` → `IPAddress` + +### Geographic Types + +* `Point` → `Tuple` +* `Ring` → `Array of Points` +* `Polygon` → `Array of Rings` + +### Complex Types + +* `Array(T)` → `Array of any type` +* `Tuple(T1, T2, ...)` → `Tuple of any types` +* `Nullable(T)` → `Nullable version of any type` +* `Map(K, V)` → `Dictionary` + +--- + +### DateTime Handling {#datetime-handling} + +* Returned values are **UTC**. +* Inserting: + + * UTC → stored as-is. + * Local → converted to UTC. + * Unspecified → assumed to match column timezone. +* `UseServerTimezone=true` allows using server TZ for ambiguous columns. + +--- + +### Environment Variables {#environment-variables} + +You can set defaults using environment variables: + +| Variable | Purpose | +| --------------------- | ---------------- | +| `CLICKHOUSE_DB` | Default database | +| `CLICKHOUSE_USER` | Default username | +| `CLICKHOUSE_PASSWORD` | Default password | + +--- + +### ORM & Dapper Support {#orm-support} + +`ClickHouse.Driver` supports Dapper (with limitations). + +**Working example:** +```csharp +connection.QueryAsync( + "SELECT {p1:Int32}", + new Dictionary { { "p1", 42 } } +); +``` + +**Not supported:** +```csharp +connection.QueryAsync( + "SELECT {p1:Int32}", + new { p1 = 42 } +); +``` + +--- + +### Quick Start {#quick-start} +```csharp +using ClickHouse.Driver.ADO; + +var connection = new ClickHouseConnection("Host=my.clickhouse;Protocol=https;Port=8443;Username=user"); +var version = await connection.ExecuteScalarAsync("SELECT version()"); +Console.WriteLine(version); +``` + +Using **Dapper**: +```csharp +using Dapper; + +using var connection = new ClickHouseConnection("Host=my.clickhouse"); +var result = await connection.QueryAsync("SELECT name FROM system.databases"); +Console.WriteLine(string.Join('\n', result)); +```