> ## Documentation Index
> Fetch the complete documentation index at: https://private-7c7dfe99-vortex-format.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

> Le client C# officiel pour se connecter à ClickHouse via HTTP.

# Client HTTP C# pour ClickHouse

export const Image = ({img, alt, size = "lg", background}) => {
  const normalizedSize = ["sm", "md", "lg"].includes(size) ? size : "lg";
  const backgroundColor = background === "white" ? "white" : background === "black" ? "rgb(31 31 28)" : undefined;
  return <div className={`ch-image-${normalizedSize}`}>
      <Frame>
        <img src={img} alt={alt} style={{
    backgroundColor
  }} />
      </Frame>
    </div>;
};

Le client C# officiel pour se connecter à ClickHouse.
Le code source du client est disponible dans le [dépôt GitHub](https://github.com/ClickHouse/clickhouse-cs).
Initialement développé par [Oleg V. Kozlyuk](https://github.com/DarkWanderer).

La bibliothèque propose deux API principales :

* **`ClickHouseClient`** (recommandé) : un client de haut niveau, thread-safe, conçu pour être utilisé comme singleton. Il fournit une API asynchrone simple pour les requêtes et les insertions en masse. C'est le choix le plus adapté à la plupart des applications.

* **ADO.NET** (`ClickHouseDataSource`, `ClickHouseConnection`, `ClickHouseCommand`) : les abstractions de base de données standard de .NET. Elles sont indispensables pour l'intégration avec un ORM (Dapper, Linq2db) et lorsque vous avez besoin de la compatibilité ADO.NET. `ClickHouseBulkCopy` est une classe utilitaire permettant d'insérer efficacement des données via une connexion ADO.NET. `ClickHouseBulkCopy` est obsolète et sera supprimée dans une version ultérieure ; utilisez plutôt `ClickHouseClient.InsertBinaryAsync`.

Les deux API partagent le même pool de connexions HTTP sous-jacent et peuvent être utilisées conjointement au sein d'une même application.

<h2 id="migration-guide">
  Guide de migration
</h2>

1. Mettez à jour votre fichier `.csproj` en indiquant le nouveau nom du paquet, `ClickHouse.Driver`, et [sa dernière version disponible sur NuGet](https://www.nuget.org/packages/ClickHouse.Driver).
2. Remplacez toutes les références à `ClickHouse.Client` par `ClickHouse.Driver` dans votre code source.

***

<h2 id="supported-net-versions">
  Versions de .NET prises en charge
</h2>

`ClickHouse.Driver` prend en charge les versions de .NET suivantes :

* .NET 6.0
* .NET 8.0
* .NET 9.0
* .NET 10.0

<h2 id="supported-clickhouse-versions">
  Versions de ClickHouse prises en charge
</h2>

Le client prend officiellement en charge les 3 dernières versions, ainsi que les deux dernières versions LTS.

<h2 id="installation">
  Installation
</h2>

Installez le paquet à partir de NuGet :

```bash theme={null}
dotnet add package ClickHouse.Driver
```

Ou via le gestionnaire de paquets NuGet (NuGet Package Manager) :

```bash theme={null}
Install-Package ClickHouse.Driver
```

<h2 id="quick-start">
  Démarrage rapide
</h2>

```csharp theme={null}
using ClickHouse.Driver;

// Create a client (typically as a singleton)
using var client = new ClickHouseClient("Host=my.clickhouse;Protocol=https;Port=8443;Username=user");

// Execute a query
var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine(version);
```

<h2 id="configuration">
  Configuration
</h2>

Vous pouvez configurer votre connexion à ClickHouse de deux façons :

* **Chaîne de connexion :** ensemble de paires clé/valeur séparées par des points-virgules, précisant l’hôte, les identifiants d’authentification et les autres options de connexion.
* **Objet `ClickHouseClientSettings` :** objet de configuration fortement typé, qui peut être chargé à partir de fichiers de configuration ou défini dans le code.

Vous trouverez ci-dessous la liste complète des paramètres, leurs valeurs par défaut et leurs effets.

<h3 id="connection-settings">
  Paramètres de connexion
</h3>

| Propriété | Type | Valeur par défaut | Clé de chaîne de connexion | Description |
| - | - | - | - | - |
| Host | `string` | `"localhost"` | `Host` | Nom d’hôte ou adresse IP du serveur ClickHouse |
| Port | `ushort` | 8123 (HTTP) / 8443 (HTTPS) | `Port` | Numéro de port ; la valeur par défaut dépend du protocole |
| Username | `string` | `"default"` | `Username` | Nom d’utilisateur pour l’authentification |
| Password | `string` | `""` | `Password` | Mot de passe pour l’authentification |
| Database | `string` | `""` | `Database` | Base de données par défaut ; si vide, celle définie par défaut pour le serveur ou l’utilisateur est utilisée |
| Protocol | `string` | `"http"` | `Protocol` | Protocole de connexion : `"http"` ou `"https"` |
| Path | `string` | `null` | `Path` | Chemin d’URL utilisé derrière un reverse proxy (par exemple, `/clickhouse`) |
| Timeout | `TimeSpan` | 2 minutes | `Timeout` | Délai d’expiration des opérations (stocké au format secondes dans la chaîne de connexion) |

<h3 id="data-format-serialization">
  Format de données et sérialisation
</h3>

| Propriété | Type | Par défaut | Clé de chaîne de connexion | Description |
| - | - | - | - | - |
| UseCompression | `bool` | `true` | `Compression` | Contrôle la compression du transport dans les deux sens pour une requête ordinaire : demande au serveur de compresser la réponse (`enable_http_compression` ; voir `AcceptEncoding` pour le codec, qu'une valeur explicite permet de demander même lorsque cette option est désactivée) **et** compresse en gzip le corps de la requête — sauf avec `UseFormDataParameters`, dont le corps multipart est toujours envoyé sans compression. Les insertions binaires n'en tiennent jamais compte ; elles utilisent `InsertOptions.Compressor` — voir [Compression des insertions](#insert-compression) |
| AcceptEncoding | `string` | `null` | `AcceptEncoding` | En-tête `Accept-Encoding` envoyé avec chaque requête, qui remplace les codecs annoncés par défaut par le driver (`zstd, lz4, gzip, deflate`). Quel que soit l'encodage de la réponse du serveur, celle-ci est décodée de manière transparente. Voir [Décompression des réponses](#response-decompression) |
| UseCustomDecimals | `bool` | `true` | `UseCustomDecimals` | Utilise `ClickHouseDecimal` pour une précision arbitraire ; si false, utilise le type .NET `decimal` (limité à 128 bits) |
| ReadStringsAsByteArrays | `bool` | `false` | `ReadStringsAsByteArrays` | Lit les colonnes `String` et `FixedString` en tant que `byte[]` au lieu de `string` ; utile pour les données binaires |
| UseFormDataParameters | `bool` | `false` | `UseFormDataParameters` | Envoie les paramètres sous forme de données de formulaire plutôt que dans la chaîne de requête de l’URL |
| ReadBufferSize | `int` | `65536` (64 KiB) | `ReadBufferSize` | Taille en octets du tampon utilisé pour lire les réponses HTTP aux requêtes. Le driver emprunte le tampon à un pool partagé et le restitue lorsqu'il libère le lecteur : il ne s'agit donc pas d'une allocation par requête. Augmentez cette valeur pour réduire le nombre de remplissages du tampon sur les grands jeux de résultats. Le driver conserve un tampon par lecteur concurrent : la consommation mémoire augmente donc avec la taille du tampon et le nombre de lecteurs concurrents. Voir [Tampons](#perf-buffers). |
| ParameterTypeResolver | `IParameterTypeResolver` | `null` | — | Résolveur personnalisé pour le mappage des types de paramètres de style `@` ; voir [Mappage personnalisé des types de paramètres](#parameter-type-mapping) |
| ParameterFormatter | `IParameterFormatter` | `null` | — | Formateur personnalisé pour la sérialisation des valeurs de paramètres ; voir [Formatage personnalisé des valeurs de paramètres](#parameter-value-formatting) |
| ReadValueConverter | `IReadValueConverter` | `null` | — | Transformation personnalisée appliquée aux valeurs renvoyées par le lecteur de données ; voir [Conversion personnalisée des valeurs lues](#read-value-conversion) |
| JsonReadMode | `JsonReadMode` | `Binary` | `JsonReadMode` | Mode de renvoi des données JSON : `Binary` (renvoie un `JsonObject`) ou `String` (renvoie la chaîne JSON brute) |
| JsonWriteMode | `JsonWriteMode` | `String` | `JsonWriteMode` | Mode d'envoi des données JSON : `String` (sérialise via `JsonSerializer`, accepte toutes les entrées) ou `Binary` (uniquement les POCO enregistrés, avec indications de type) |
| MapReadMode | `MapReadMode` | `Dictionary` | `MapReadMode` | Mode de renvoi des données `Map(K, V)` : `Dictionary` (renvoie un `Dictionary<K, V>` ; pour une clé répétée, seule la dernière valeur est conservée) ou `KeyValuePairs` (renvoie une `List<KeyValuePair<K, V>>` qui conserve toutes les paires). Voir [Type Map](#type-map-reading-map) |
| AllowDuplicateJsonKeys | `bool` | `false` | `AllowDuplicateJsonKeys` | Mode de lecture d'une ligne `JSON` dont deux chemins qui se chevauchent contiennent chacun une valeur. `false` lève une exception, car conserver l'une des valeurs reviendrait à perdre l'autre ; `true` conserve la dernière valeur présente dans la ligne. Voir [Chemins qui se chevauchent](#type-map-reading-json) |

<h3 id="session-management">
  Gestion des sessions
</h3>

| Propriété | Type | Valeur par défaut | Clé de la chaîne de connexion | Description |
| - | - | - | - | - |
| UseSession | `bool` | `false` | `UseSession` | Active les sessions avec conservation de l’état ; sérialise les requêtes |
| SessionId | `string` | `null` | `SessionId` | ID de session ; génère automatiquement un GUID si la valeur est null et que UseSession est true |

<Note>
  L’indicateur `UseSession` permet de conserver la session côté serveur, et ainsi d’utiliser des statements `SET` et des tables temporaires. Les sessions sont réinitialisées après 60 secondes d’inactivité (délai d’expiration par défaut). Leur durée de vie peut être prolongée en définissant des paramètres de session à l’aide de statements ClickHouse ou dans la configuration du serveur.

  La classe `ClickHouseConnection` permet normalement un fonctionnement en parallèle (plusieurs threads peuvent exécuter des requêtes simultanément). Toutefois, l’activation de l’indicateur `UseSession` limite l’exécution à une seule requête active par connexion à la fois (cette limite est imposée par le serveur).
</Note>

<h3 id="security">
  Sécurité
</h3>

| Propriété | Type | Valeur par défaut | Clé de chaîne de connexion | Description |
| - | - | - | - | - |
| SkipServerCertificateValidation | `bool` | `false` | — | Désactive la validation du certificat HTTPS ; **à ne pas utiliser en production** |

<h3 id="http-client-configuration">
  Configuration du client HTTP
</h3>

| Propriété | Type | Valeur par défaut | Clé de chaîne de connexion | Description |
| - | - | - | - | - |
| HttpClient | `HttpClient` | `null` | — | Instance HttpClient personnalisée et préconfigurée |
| HttpClientFactory | `IHttpClientFactory` | `null` | — | Fabrique personnalisée permettant de créer des instances HttpClient |
| HttpClientName | `string` | `null` | — | Nom transmis à HttpClientFactory pour créer un client spécifique |

<h3 id="logging-debugging">
  Journalisation et débogage
</h3>

| Propriété | Type | Valeur par défaut | Clé de la chaîne de connexion | Description |
| - | - | - | - | - |
| LoggerFactory | `ILoggerFactory` | `null` | — | Fabrique de composants de journalisation destinés au diagnostic |
| EnableDebugMode | `bool` | `false` | — | Active le traçage réseau .NET (nécessite que le niveau de LoggerFactory soit défini sur Trace) ; **incidence importante sur les performances** |

<h3 id="custom-settings-roles">
  Paramètres personnalisés et rôles
</h3>

| Propriété | Type | Valeur par défaut | Clé de chaîne de connexion | Description |
| - | - | - | - | - |
| CustomSettings | `IDictionary<string, object>` | Vide | Préfixe `set_*` | Paramètres du serveur ClickHouse, voir la remarque ci-dessous |
| Roles | `IReadOnlyList<string>` | Vide | `Roles` | Rôles ClickHouse séparés par des virgules (par ex. `Roles=admin,reader`) |
| ApplicationInfo | `IReadOnlyDictionary<string, string>` | Vide | — | Balises libres ajoutées à l'en-tête HTTP `User-Agent` afin d'attribuer les requêtes à chaque application. |

<Note>
  Pour définir des paramètres personnalisés dans une chaîne de connexion, utilisez le préfixe `set_`, par ex. « set\_max\_threads=4 ». Avec un objet ClickHouseClientSettings, n'utilisez pas le préfixe `set_`.

  Pour la liste complète des paramètres disponibles, consultez [cette page](/fr/reference/settings/session-settings).
</Note>

***

<h3 id="connection-string-examples">
  Exemples de chaînes de connexion
</h3>

<h4 id="basic-connection">
  Connexion de base
</h4>

```text theme={null}
Host=localhost;Port=8123;Username=default;Password=secret;Database=mydb
```

<h4 id="with-custom-clickhouse-settings">
  Avec des paramètres ClickHouse personnalisés
</h4>

```text theme={null}
Host=localhost;set_max_threads=4;set_readonly=1;set_max_memory_usage=10000000000
```

***

<h3 id="query-options">
  QueryOptions
</h3>

`QueryOptions` permet de remplacer, requête par requête, les paramètres définis au niveau du client. Toutes les propriétés sont facultatives et ne remplacent les valeurs par défaut du client que si elles sont spécifiées.

| Propriété | Type | Description |
| - | - | - |
| QueryId | `string` | Identifiant de requête personnalisé, utilisé pour le suivi dans `system.query_log` ou pour l'annulation |
| Database | `string` | Remplace la base de données par défaut pour cette requête |
| Roles | `IReadOnlyList<string>` | Remplace les rôles du client pour cette requête |
| CustomSettings | `IDictionary<string, object>` | Paramètres du serveur ClickHouse pour cette requête (par exemple, `max_threads`) |
| CustomHeaders | `IDictionary<string, string>` | En-têtes HTTP supplémentaires pour cette requête |
| UseSession | `bool?` | Remplace le comportement de session pour cette requête |
| SessionId | `string` | ID de session pour cette requête (nécessite `UseSession = true`) |
| BearerToken | `string` | Remplace le jeton d'authentification pour cette requête |
| ParameterTypeResolver | `IParameterTypeResolver` | Remplace le résolveur défini au niveau du client pour le mappage des types des paramètres de style `@` ; voir [Mappage personnalisé des types de paramètres](#parameter-type-mapping) |
| ParameterFormatter | `IParameterFormatter` | Remplace le formateur défini au niveau du client pour la sérialisation des valeurs des paramètres de style `@` ; voir [Formatage personnalisé des valeurs de paramètres](#parameter-value-formatting) |
| ReadValueConverter | `IReadValueConverter` | Remplace la transformation définie au niveau du client et appliquée aux valeurs renvoyées par le lecteur de données ; voir [Conversion personnalisée des valeurs lues](#read-value-conversion) |
| MaxExecutionTime | `TimeSpan?` | Délai d'expiration de la requête côté serveur (transmis via le paramètre `max_execution_time`) ; en cas de dépassement, le serveur annule la requête |
| AcceptEncoding | `string` | Valeur de `Accept-Encoding` propre à la requête (par exemple `"br"`, `"identity"`), prioritaire sur `ClickHouseClientSettings.AcceptEncoding` ; force également `enable_http_compression=1` dans l'URL. Voir [Compression du transport par requête](#per-query-accept-encoding). |

**Exemple :**

```csharp theme={null}
var options = new QueryOptions
{
    QueryId = "report-2024-001",
    Database = "analytics",
    CustomSettings = new Dictionary<string, object>
    {
        { "max_threads", 4 },
        { "max_memory_usage", 10_000_000_000 }
    },
    MaxExecutionTime = TimeSpan.FromMinutes(5)
};

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM large_table",
    parameters: null,
    options: options
);
```

***

<h3 id="insert-options">
  InsertOptions
</h3>

`InsertOptions` étend `QueryOptions` avec des paramètres propres aux insertions en masse effectuées via `InsertBinaryAsync`.

| Propriété | Type | Valeur par défaut | Description |
| - | - | - | - |
| BatchSize | `int` | 100,000 | Nombre de lignes par lot |
| MaxDegreeOfParallelism | `int` | 1 | Nombre de lots téléversés en parallèle |
| Format | `RowBinaryFormat` | `RowBinary` | Format binaire : `RowBinary` ou `RowBinaryWithDefaults` |
| Compressor | `IClickHouseCompressor` | `ZstdCompressor.Default` | Codec appliqué au corps de la requête d’insertion (`Content-Encoding`). Avec `null`, le corps est envoyé sans compression. Voir [Compression des insertions](#insert-compression) |
| QueryPlacement | `InsertQueryPlacement` | `Body` | Indique où est envoyé le statement `INSERT INTO ... FORMAT ...` : dans `Body` (avant les lignes) ou dans `Url` (en tant que paramètre d’URL `query`). Voir [Emplacement de la requête d’insertion](#insert-query-placement) |
| ColumnTypes | `IReadOnlyDictionary<string, string>` | `null` | Nom de colonne → chaîne de type ClickHouse. Lorsque cette option est définie, la requête de détection du schéma n’est pas exécutée. |
| UseSchemaCache | `bool` | `false` | Met en cache le schéma complet de chaque table, par couple (base de données, table), pendant toute la durée de vie du client. |

Toutes les propriétés de `QueryOptions` sont également disponibles dans `InsertOptions`.

**Exemple :**

```csharp theme={null}
var insertOptions = new InsertOptions
{
    BatchSize = 50_000,
    MaxDegreeOfParallelism = 4,
    QueryId = "bulk-import-001"
};

long rowsInserted = await client.InsertBinaryAsync(
    "my_table",
    columns,
    rows,
    insertOptions
);
```

<h4 id="skip-schema-query">
  Éviter la requête de détection du schéma
</h4>

Par défaut, `InsertBinaryAsync` envoie une requête `SELECT ... WHERE 1=0` avant chaque insertion afin de déterminer les types des colonnes. Pour les scénarios à haut débit, vous pouvez éliminer ce surcoût de deux manières :

**Option 1 : fournir explicitement les types de colonnes**

Si vous connaissez le schéma de la table au moment de la compilation, transmettez-le directement via `ColumnTypes`. Aucune requête de schéma n'est alors envoyée :

```csharp theme={null}
var options = new InsertOptions
{
    ColumnTypes = new Dictionary<string, string>
    {
        ["id"] = "UInt64",
        ["name"] = "Nullable(String)",
        ["score"] = "Float32",
    },
};

await client.InsertBinaryAsync("my_table", ["id", "name", "score"], rows, options);
```

**Option 2 : mettre le schéma en cache**

Lorsque vous insérez des données de manière répétée dans la même table, définissez `UseSchemaCache = true` pour ne récupérer le schéma qu'une seule fois et le réutiliser lors des insertions suivantes sur la même instance de `ClickHouseClient` :

```csharp theme={null}
var options = new InsertOptions { UseSchemaCache = true };

// First call fetches schema from the server
await client.InsertBinaryAsync("my_table", columns, batch1, options);

// Second call reuses cached schema — no extra round-trip
await client.InsertBinaryAsync("my_table", columns, batch2, options);
```

<Note>
  * `ColumnTypes` a priorité sur `UseSchemaCache`. Si les deux sont définis, ce sont les types explicites qui sont utilisés.
  * Le cache de schéma ne détecte pas les modifications effectuées par `ALTER TABLE`. Si vous modifiez le schéma de la table, créez un nouveau `ClickHouseClient` ou n'utilisez pas `UseSchemaCache` pour cette table.
  * Le cache est propre à chaque instance de `ClickHouseClient` et indexé par le couple (base de données, table). Des sous-ensembles de colonnes différents d'une même table partagent un seul et même schéma mis en cache.
</Note>

<h2 id="clickhouse-client">
  ClickHouseClient
</h2>

`ClickHouseClient` est l’API recommandée pour interagir avec ClickHouse. Conçue pour être utilisée comme singleton, elle peut être employée simultanément par plusieurs threads en toute sécurité et gère en interne un pool de connexions HTTP.

<h3 id="creating-a-client">
  Créer un client
</h3>

Créez un `ClickHouseClient` à l'aide d'une chaîne de connexion ou d'un objet `ClickHouseClientSettings`. Consultez la section [Configuration](#configuration) pour connaître les options disponibles.

Les informations de votre service ClickHouse Cloud sont disponibles dans la console ClickHouse Cloud.

Sélectionnez un service et cliquez sur **Connect** :

<Image img="https://mintcdn.com/private-7c7dfe99-vortex-format/tee0vtC1xMN3cEIV/images/_snippets/cloud-connect-button.webp?fit=max&auto=format&n=tee0vtC1xMN3cEIV&q=85&s=334245222c7a16f54a807110b260b71e" size="md" alt="Bouton Connect d'un service ClickHouse Cloud" border width="998" height="932" data-path="images/_snippets/cloud-connect-button.webp" />

Choisissez **C#**. Les informations de connexion s'affichent en dessous.

<Image img="https://mintcdn.com/private-7c7dfe99-vortex-format/tee0vtC1xMN3cEIV/images/_snippets/connection-details-csharp.webp?fit=max&auto=format&n=tee0vtC1xMN3cEIV&q=85&s=60f04e40db829dd87b2407094cf1765d" size="md" alt="Informations de connexion C# de ClickHouse Cloud" border width="851" height="805" data-path="images/_snippets/connection-details-csharp.webp" />

Si vous utilisez ClickHouse en autogéré, les informations de connexion vous sont fournies par votre administrateur ClickHouse.

Avec une chaîne de connexion :

```csharp theme={null}
using ClickHouse.Driver;

using var client = new ClickHouseClient("Host=localhost;Username=default;Password=secret");
```

Ou avec `ClickHouseClientSettings` :

```csharp theme={null}
using ClickHouse.Driver;

var settings = new ClickHouseClientSettings
{
    Host = "localhost",
    Username = "default",
    Password = "secret"
};
using var client = new ClickHouseClient(settings);
```

Pour les scénarios d'injection de dépendances, utilisez `IHttpClientFactory` :

```csharp theme={null}
// In your DI configuration. No AutomaticDecompression needed — the driver decodes
// compressed responses itself, and a mask here would widen its Accept-Encoding.
services.AddHttpClient("ClickHouse", client =>
{
    client.Timeout = TimeSpan.FromMinutes(5);
});

// Create client with factory
var factory = serviceProvider.GetRequiredService<IHttpClientFactory>();
var client = new ClickHouseClient("Host=localhost", factory, "ClickHouse");
```

<Note>
  `ClickHouseClient` est conçu pour être utilisé sur la durée et partagé dans l’ensemble de votre application. Créez une seule instance (généralement un singleton) et réutilisez-la pour toutes les opérations sur la base de données. Le client gère lui-même le pool de connexions HTTP.
</Note>

***

<h3 id="executing-queries">
  Exécution des requêtes
</h3>

Utilisez `ExecuteNonQueryAsync` pour les statements qui ne renvoient aucun résultat :

```csharp theme={null}
// Create a table
await client.ExecuteNonQueryAsync(
    "CREATE TABLE IF NOT EXISTS default.my_table (id Int64, name String) ENGINE = Memory"
);

// Drop a table
await client.ExecuteNonQueryAsync("DROP TABLE IF EXISTS default.my_table");
```

Utilisez `ExecuteScalarAsync` pour récupérer une valeur unique :

```csharp theme={null}
var count = await client.ExecuteScalarAsync("SELECT count() FROM default.my_table");
Console.WriteLine($"Row count: {count}");

var version = await client.ExecuteScalarAsync("SELECT version()");
Console.WriteLine($"Server version: {version}");
```

***

<h3 id="inserting-data">
  Insertion de données
</h3>

<h4 id="parameterized-inserts">
  Insertions paramétrées
</h4>

Pour insérer des données, utilisez des requêtes paramétrées avec `ExecuteNonQueryAsync`. Indiquez le type de chaque paramètre dans la requête SQL selon la syntaxe `{name:Type}` :

```csharp theme={null}
using ClickHouse.Driver;
using ClickHouse.Driver.ADO.Parameters;

var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("id", 1L);
parameters.AddParameter("name", "Alice");

await client.ExecuteNonQueryAsync(
    "INSERT INTO default.my_table (id, name) VALUES ({id:Int64}, {name:String})",
    parameters
);
```

***

<h4 id="bulk-insert">
  Insertions en masse
</h4>

Utilisez `InsertBinaryAsync` pour insérer efficacement un grand nombre de lignes. Cette méthode envoie les données en continu au format binaire natif des lignes de ClickHouse, permet de téléverser des lots en parallèle et évite les erreurs « URL trop longue » qui peuvent survenir avec les requêtes paramétrées.

```csharp theme={null}
// Prepare data as IEnumerable<object[]>
var rows = Enumerable.Range(0, 1_000_000)
    .Select(i => new object[] { (long)i, $"value{i}" });

var columns = new[] { "id", "name" };

// Basic insert
long rowsInserted = await client.InsertBinaryAsync("default.my_table", columns, rows);
Console.WriteLine($"Rows inserted: {rowsInserted}");
```

Pour les jeux de données volumineux, configurez le traitement par lots et le parallélisme à l'aide de `InsertOptions` :

```csharp theme={null}
var options = new InsertOptions
{
    BatchSize = 100_000,           // Rows per batch (default: 100,000)
    MaxDegreeOfParallelism = 4     // Parallel batch uploads (default: 1)
};
```

<Note>
  * Avant chaque insertion, le client récupère automatiquement la structure de la table à l’aide de `SELECT * FROM <table> WHERE 1=0`. Les valeurs fournies doivent correspondre aux types des colonnes cibles. Pour éviter cette requête, utilisez [`InsertOptions.ColumnTypes` ou `InsertOptions.UseSchemaCache`](#skip-schema-query).
  * Lorsque `MaxDegreeOfParallelism > 1`, les lots sont téléversés en parallèle. Les sessions ne sont pas compatibles avec l’insertion en parallèle : désactivez-les ou définissez `MaxDegreeOfParallelism = 1`.
  * Utilisez `RowBinaryFormat.RowBinaryWithDefaults` dans `InsertOptions.Format` pour que le serveur applique les valeurs DEFAULT aux colonnes pour lesquelles aucune valeur n’est fournie.
</Note>

<h4 id="poco-insert">
  Insertions de POCO
</h4>

Plutôt que de créer des tableaux `object[]`, vous pouvez insérer directement des objets POCO fortement typés. Enregistrez le type une fois, puis transmettez un `IEnumerable<T>` :

```csharp theme={null}
// Define a POCO matching your table columns
public class SensorReading
{
    public ulong Id { get; set; }
    public string SensorName { get; set; }
    public double Value { get; set; }
    public DateTime Timestamp { get; set; }
}

// Register the type (once per client lifetime)
client.RegisterBinaryInsertType<SensorReading>();

// Insert directly — column names are derived from property names
var readings = Enumerable.Range(0, 100_000)
    .Select(i => new SensorReading
    {
        Id = (ulong)i,
        SensorName = $"sensor_{i % 10}",
        Value = Random.Shared.NextDouble() * 100,
        Timestamp = DateTime.UtcNow,
    });

long rowsInserted = await client.InsertBinaryAsync("sensors", readings);
```

Par défaut, toutes les propriétés publiques accessibles en lecture sont mises en correspondance avec les colonnes selon une correspondance stricte des noms, sensible à la casse. Vous pouvez personnaliser cette mise en correspondance à l’aide d’attributs :

```csharp theme={null}
public class Event
{
    [ClickHouseColumn(Name = "event_id")]     // Map to a differently-named column
    public ulong Id { get; set; }

    [ClickHouseColumn(Type = "LowCardinality(String)")]  // Explicit ClickHouse type
    public string Category { get; set; }

    public string Payload { get; set; }

    [ClickHouseNotMapped]                     // Exclude from insert
    public string InternalTag { get; set; }
}
```

| Attribut | Fonction |
| - | - |
| `[ClickHouseColumn(Name = "...")]` | Redéfinir le nom de la colonne cible |
| `[ClickHouseColumn(Type = "...")]` | Déclarer explicitement le type ClickHouse |
| `[ClickHouseNotMapped]` | Exclure la propriété de l’insertion |

Lorsque **toutes** les propriétés mappées définissent explicitement un `Type`, la requête de détection du schéma est entièrement évitée. Si seules certaines propriétés définissent explicitement un type, le driver revient à cette requête pour l’ensemble des colonnes.

`InsertBinaryAsync<T>` prend en charge les mêmes `InsertOptions` (traitement par lots, parallélisme, mise en cache du schéma) que la surcharge `object[]`.

<Note>
  Contrairement à la surcharge `object[]`, `InsertBinaryAsync<T>` n’accepte pas de liste explicite de colonnes. Les colonnes sont déterminées par les propriétés mappées du type enregistré. Pour choisir les colonnes à insérer, utilisez `[ClickHouseNotMapped]` pour exclure des propriétés ou `[ClickHouseColumn(Name = "...")]` pour les renommer.

  Si `ColumnTypes` est défini dans `InsertOptions`, ses valeurs prévalent sur les attributs POCO.
</Note>

<h4 id="poco-insert-schema-evolution">
  Évolution du schéma
</h4>

Les insertions POCO continuent de fonctionner sans problème lorsque des colonnes sont ajoutées à la table cible après l’enregistrement du type. Comme le driver n’insère que les colonnes auxquelles les propriétés du POCO sont associées, le serveur renseigne automatiquement toute nouvelle colonne dotée d’une clause `DEFAULT` (ou d’une autre expression par défaut). Il n’est pas nécessaire de modifier le code ni de réenregistrer le type.

<h4 id="insert-query-placement">
  Emplacement de la requête d’insertion
</h4>

Lors d’une insertion binaire, le statement `INSERT INTO ... FORMAT ...` est placé sur la première ligne du corps de la requête, avant les lignes de données. Comme le corps est compressé par défaut, les mécanismes de routage et de journalisation qui n’inspectent que l’URL ne voient pas ce statement. Définissez `InsertOptions.QueryPlacement` sur `InsertQueryPlacement.Url` pour envoyer plutôt le statement dans le paramètre d’URL `query`, de sorte que le corps ne contienne que les lignes de données :

```csharp theme={null}
var options = new InsertOptions { QueryPlacement = InsertQueryPlacement.Url };
await client.InsertBinaryAsync("events", columns, rows, options);
```

Utilisez cette option lorsqu’un proxy, un répartiteur de charge ou une passerelle achemine ou inspecte les requêtes en fonction du paramètre `query`, ou si vous souhaitez que le statement apparaisse dans les logs d’accès et les outils d’observability. Elle doit être activée explicitement, car le statement entre alors dans le calcul de la longueur de l’URL. La limite applicable est la plus basse de celles imposées par le runtime .NET, un intermédiaire ou le serveur. De .NET 6 à .NET 9, `System.Uri` limite l’URI complète de la requête encodée à 65 519 caractères ; si cette limite est dépassée, le driver lève une `InvalidOperationException` qui vous invite à revenir à `InsertQueryPlacement.Body`. Dans ClickHouse, la valeur par défaut de `http_max_uri_size` est de 1 Mio, mais un intermédiaire peut imposer une limite inférieure. En mode corps, le statement et les lignes ne sont pas soumis à cette limite de longueur d’URL ; d’autres options de requête peuvent néanmoins figurer dans l’URL.

Ce paramètre est indépendant de `Compressor` : le corps est encodé de la même manière dans les deux modes.

***

<h3 id="reading-data">
  Lecture des données
</h3>

Utilisez `ExecuteReaderAsync` pour exécuter des requêtes SELECT. Le `ClickHouseDataReader` renvoyé permet d’accéder aux colonnes du résultat de façon typée, à l’aide de méthodes comme `GetInt64()`, `GetString()` et `GetFieldValue<T>()`.

Appelez `Read()` pour passer à la ligne suivante. Cette méthode renvoie `false` lorsqu’il n’y a plus de lignes. Accédez aux colonnes par leur position (à partir de 0) ou par leur nom.

```csharp theme={null}
using ClickHouse.Driver.ADO.Parameters;

var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("max_id", 100L);

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM default.my_table WHERE id < {max_id:Int64}",
    parameters
);

while (reader.Read())
{
    Console.WriteLine($"Id: {reader.GetInt64(0)}, Name: {reader.GetString(1)}");
}
```

<h4 id="poco-read">
  Lecture des données dans des POCO
</h4>

Plutôt que de lire les colonnes par index ou par nom, vous pouvez recevoir les résultats de la requête directement dans vos propres classes, au fil de l’eau. Enregistrez le type une seule fois auprès du client, puis utilisez `QueryAsync<T>` :

```csharp theme={null}
// Define a POCO matching your result columns
public class SensorReading
{
    public ulong Id { get; set; }
    public DateTime Timestamp { get; set; }

    [ClickHouseColumn(Name = "sensor_name")]
    public string SensorName { get; set; }
    public double Value { get; set; }

}

// Register the type (once per client lifetime)
client.RegisterPocoType<SensorReading>();

// Stream results as typed objects
await foreach (var reading in client.QueryAsync<SensorReading>(
    "SELECT Id, sensor_name, Value, Timestamp FROM sensors"))
{
    Console.WriteLine($"{reading.SensorName}: {reading.Value}");
}
```

<h5 id="poco-read-registration">
  Enregistrement
</h5>

`RegisterPocoType<T>()` configure les correspondances pour l’insertion et la lecture, puis les valide toutes deux dès l’enregistrement. `RegisterBinaryInsertType<T>()` reste inchangé et ne concerne que l’insertion, afin de préserver la compatibilité descendante.

Un type enregistré doit comporter :

* Un constructeur public sans paramètre.
* Au moins une propriété publique dotée d’un accesseur en écriture public qui n’utilise pas `init`. Les propriétés `required` sont prises en charge.

<h5 id="poco-read-column-matching">
  Correspondance des colonnes
</h5>

La correspondance des colonnes est sensible à la casse. Les colonnes manquantes dans le résultat laissent les propriétés à leur valeur par défaut ; les colonnes supplémentaires sont ignorées.

Le driver ne convertit pas les valeurs vers un type plus large ou plus restreint. En dehors des autres représentations répertoriées ci-dessous,
le type de la colonne dans le framework doit pouvoir être affecté au type de la propriété ; sinon, une incompatibilité de types lève
`InvalidOperationException`. Une propriété de type `object` accepte donc n’importe quelle colonne.

<h5 id="poco-read-types">
  Types de propriétés pris en charge
</h5>

`QueryAsync<T>` lit directement chacune de ces colonnes dans une propriété du type correspondant :

| Colonne ClickHouse | Type(s) de propriété |
| - | - |
| `Int8`/`Int16`/`Int32`/`Int64` | `sbyte`/`short`/`int`/`long` |
| `UInt8`/`UInt16`/`UInt32`/`UInt64` | `byte`/`ushort`/`uint`/`ulong` |
| `Int128`/`UInt128` | `BigInteger`, ou `System.Int128`/`System.UInt128` natifs à partir de .NET 8 |
| `Int256`/`UInt256` | `BigInteger` |
| `Float32`/`Float64`/`BFloat16` | `float`/`double`/`float` |
| `Bool` | `bool` |
| `Decimal` | `decimal` ou `ClickHouseDecimal` |
| `Date`/`Date32`/`DateTime`/`DateTime64` | `DateTime`, `DateTimeOffset` ou `DateOnly` |
| `Time`/`Time64` | `TimeSpan` |
| `UUID` | `Guid` |
| `IPv4`/`IPv6` | `IPAddress` |
| `Enum8`/`Enum16` | `string` (le libellé) ou `int` (la valeur ordinale transmise) |
| `String`/`FixedString` | `string` ou `byte[]` |

Chaque ligne accepte aussi la forme nullable du type de propriété (`long?`, `DateOnly?`, etc.),
que la colonne soit de type `Nullable(...)` ou non. Une propriété de type valeur non nullable associée à une colonne
`Nullable(T)` est acceptée à l'enregistrement, mais provoque une exception si une valeur NULL est lue.

Les types enveloppants comme `LowCardinality(T)`, `SimpleAggregateFunction(f, T)` et `Object(T)` se comportent exactement comme `T`.

Les colonnes composites sont également prises en charge et utilisent le type du framework indiqué dans la
[référence des types de lecture](#clickhouse-native-type-map-reading) : `Array(T)` devient `T[]`, `Tuple(...)`
devient `System.Tuple<...>`, `Nested(...)` devient `Tuple<...>[]`, `JSON` devient `JsonObject` (ou `string`
avec [`JsonReadMode=String`](#type-map-reading-json)), et `Variant`/`Dynamic` deviennent `object`.

Une colonne `Map(K, V)` est un cas particulier : une propriété `List<KeyValuePair<K, V>>` ou `KeyValuePair<K, V>[]`
est lue sans mise en boîte et conserve l'ordre de transmission ainsi que les clés répétées, quel que soit le
[`MapReadMode`](#type-map-reading-map). Une propriété `Dictionary<K, V>` ne fonctionne qu'en mode
par défaut. Les types des clés et des valeurs doivent correspondre exactement : ainsi,
`Map(String, Nullable(Int32))` nécessite `KeyValuePair<string, int?>`.

Lorsqu'une colonne admet plusieurs types de propriétés (une colonne `DateTime` sous forme de `DateTime`,
`DateTimeOffset` ou `DateOnly`, une colonne `String` sous forme de `string` ou `byte[]`), le type de propriété déclaré détermine la représentation. Ces autres représentations
sont propres à la lecture des POCO : `QueryAsync<T>` les prend donc en charge, mais pas `MapTo<T>`.

<h5 id="poco-read-mapto">
  Matérialiser une seule ligne
</h5>

Lorsque vous parcourez un lecteur manuellement, utilisez `ClickHouseDataReader.MapTo<T>()` pour matérialiser la ligne en cours sous forme de POCO enregistré, sans passer à la ligne suivante :

```csharp theme={null}
var reader = await client.ExecuteReaderAsync("SELECT Id, SensorName, Value, Timestamp FROM sensors");

while (reader.Read())
{
    SensorReading reading = reader.MapTo<SensorReading>();
    Console.WriteLine($"{reading.SensorName}: {reading.Value}");
}
```

Utilisez `MapTo<T>` lorsque vous devez gérer vous-même la boucle de lecture, par exemple pour combiner l’accès direct aux colonnes
avec la materialization des POCO. Cette méthode lit la ligne à partir des valeurs encapsulées du lecteur : elle ne prend donc pas en charge
les autres types de propriétés mentionnés ci-dessus et génère davantage d’allocations que `QueryAsync<T>`. Préférez
`QueryAsync<T>` si vous n’avez besoin que des lignes ; consultez
[choisir la méthode de materialization](#perf-read-path) pour connaître les chiffres.

<h5 id="poco-read-converters">
  Convertisseurs de valeurs à la lecture
</h5>

Un [convertisseur de valeurs à la lecture](#read-value-conversion) configuré au niveau du client ou pour une requête donnée s'applique aux deux chemins et
ne désactive pas la lecture sans mise en boîte. Le driver convertit chaque colonne au moyen de la surcharge correspondant à son mode de lecture : la surcharge typée `ConvertValue<T>` pour une colonne lue sans mise en boîte,
et la surcharge `ConvertValue` avec mise en boîte pour une colonne composite. Implémentez les deux surcharges
de manière cohérente, faute de quoi une même colonne produira des résultats différents selon le chemin emprunté.

<h5 id="poco-read-diagnostics">
  Diagnostics d’enregistrement
</h5>

Lorsqu’une `LoggerFactory` est configurée, `RegisterPocoType<T>()` et `RegisterBinaryInsertType<T>()` émettent un message de journalisation de niveau `Debug` (catégorie `ClickHouse.Driver.Client`) précisant quelles propriétés sont associées à quelles colonnes, et lesquelles ont été ignorées, avec les raisons de leur exclusion. Consultez [Journalisation et diagnostics](#logging-and-diagnostics).

***

<h3 id="sql-parameters">
  Paramètres SQL
</h3>

Dans ClickHouse, les paramètres des requêtes SQL s’écrivent selon le format standard `{parameter_name:DataType}`.

**Exemples :**

```sql theme={null}
SELECT {value:Array(UInt16)} as a
```

```sql theme={null}
SELECT * FROM table WHERE val = {tuple_in_tuple:Tuple(UInt8, Tuple(String, UInt8))}
```

```sql theme={null}
INSERT INTO table VALUES ({val1:Int32}, {val2:Array(UInt8)})
```

<Note>
  Les paramètres SQL 'bind' sont transmis sous forme de paramètres de requête dans l’URI HTTP. S’ils sont trop nombreux, une exception "URL trop longue" peut se produire. Pour éviter cette limitation lors d’insertions massives de données, utilisez `InsertBinaryAsync`.
</Note>

<h4 id="at-style-placeholders">
  Caractères de substitution `@name` de style ADO
</h4>

Le driver accepte également les caractères de substitution `@name` utilisés par des ORM comme Dapper. Ils simplifient
l'utilisation côté client : avant l'envoi de la requête, chacun est réécrit sous la forme
`{name:ResolvedType}`. Le serveur ne voit donc jamais de `@`. Consultez
la [résolution des types](#parameter-type-mapping) pour savoir comment le type est déterminé. Privilégiez la forme explicite
`{name:Type}` lorsque c'est possible.

Un `@name` sans paramètre correspondant est laissé tel quel et sera rejeté par le serveur. La correspondance est
sensible à la casse : `@ID` ne correspond donc pas au paramètre nommé `id`.

<Note>
  Pour désactiver cette réécriture, activez le commutateur AppContext `ClickHouse.Driver.DisableReplacingParameters`
  avant la première utilisation du driver. Seule la réécriture du texte est désactivée ; les paramètres sont toujours envoyés. Les
  requêtes utilisant la syntaxe native `{name:Type}` continuent donc de fonctionner.
</Note>

<h4 id="identifier-parameters">
  Paramètres d’identifiant
</h4>

Le type de paramètre `Identifier` permet de passer en toute sécurité un nom de base de données, de table ou de colonne, plutôt qu’une chaîne littérale entre guillemets. Utilisez la syntaxe `{name:Identifier}` en SQL ou définissez `ClickHouseDbParameter.ClickHouseType = "Identifier"` :

```csharp theme={null}
var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("name", "my_database");

await client.ExecuteNonQueryAsync("CREATE DATABASE {name:Identifier}", parameters);
```

```csharp theme={null}
var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("col", "user_id");

var reader = await client.ExecuteReaderAsync("SELECT {col:Identifier} FROM t", parameters);
```

La valeur est envoyée telle quelle, puis le serveur la substitue en tant qu’identifiant SQL sans délimiteurs, en se chargeant de l’encadrer d’accents graves et d’échapper les caractères nécessaires. Les identifiants contenant des caractères spéciaux (y compris des accents graves) sont préservés lors d’un aller-retour.

***

<h3 id="query-id">
  Identifiant de requête
</h3>

Chaque requête reçoit un `query_id` unique, qui permet de récupérer des données dans la table `system.query_log` ou d’annuler les requêtes de longue durée. Vous pouvez définir un identifiant de requête personnalisé à l’aide de `QueryOptions` :

```csharp theme={null}
var options = new QueryOptions
{
    QueryId = $"report-{Guid.NewGuid()}"
};

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM large_table",
    parameters: null,
    options: options
);
```

<Tip>
  Si vous définissez un `QueryId` personnalisé, veillez à utiliser une valeur unique pour chaque appel. Un GUID aléatoire est un bon choix.
</Tip>

***

<h3 id="parameter-type-mapping">
  Correspondance personnalisée des types de paramètres
</h3>

Lorsque vous utilisez des paramètres de la forme `@` (par exemple, `WHERE id = @id`), le driver déduit automatiquement le type ClickHouse du type de la valeur .NET. Par exemple, le type `int` correspond à `Int32`.

<Warning>
  **Comportement des paramètres DateTime dont le type est déduit**

  Pour les paramètres de la forme `@` sans indication `{name:Type}` dans la requête SQL ni `ClickHouseType` défini, les valeurs représentant un instant sont associées à `DateTime('UTC')` plutôt qu'à un simple `DateTime`. Les valeurs `DateTime` dont la propriété `Kind` vaut `Utc` ou `Local`, ainsi que toutes les valeurs `DateTimeOffset`, sont envoyées en tant que `DateTime('UTC')`, ce qui préserve l'instant quel que soit le fuseau horaire du serveur.

  Les indications explicites (`{name:DateTime}`) priment sur la déduction automatique et constituent la méthode recommandée pour construire des requêtes.
</Warning>

Pour modifier ce comportement par défaut, définissez `ParameterTypeResolver` sur `ClickHouseClientSettings`. Vous pouvez ainsi utiliser `DateTime64(3)` pour tous les paramètres `DateTime` afin d'obtenir une précision à la milliseconde, ou appliquer un scale donné à tous les nombres décimaux, sans définir `ClickHouseType` pour chaque paramètre.

**Utilisation de `DictionaryParameterTypeResolver` pour des correspondances de types simples :**

```csharp theme={null}
using ClickHouse.Driver.ADO.Parameters;

var settings = new ClickHouseClientSettings("Host=localhost")
{
    ParameterTypeResolver = new DictionaryParameterTypeResolver(new Dictionary<Type, string>
    {
        [typeof(DateTime)] = "DateTime64(3)",
        [typeof(decimal)] = "Decimal64(4)",
    }),
};
using var client = new ClickHouseClient(settings);

var parameters = new ClickHouseParameterCollection();
parameters.AddParameter("dt", DateTime.UtcNow);     // Mapped to DateTime64(3)
parameters.AddParameter("amount", 99.1234m);         // Mapped to Decimal64(4)

await client.ExecuteReaderAsync("SELECT @dt, @amount", parameters);
```

**Implémentation personnalisée de `IParameterTypeResolver` pour les cas d’usage avancés :**

Pour déterminer le type d’un paramètre en fonction de sa valeur ou de son nom, implémentez directement l’interface `IParameterTypeResolver`. Renvoyez `null` pour utiliser l’inférence par défaut :

```csharp theme={null}
public class SmartDecimalResolver : IParameterTypeResolver
{
    public string ResolveType(Type clrType, object value, string parameterName)
    {
        if (clrType != typeof(decimal))
            return null; // Fall through to default

        var scale = (decimal.GetBits((decimal)value)[3] >> 16) & 0x7F;
        return scale <= 4 ? $"Decimal64({scale})" : $"Decimal128({scale})";
    }
}
```

Vous pouvez également définir un résolveur pour une requête donnée via `QueryOptions.ParameterTypeResolver`. Dans ce cas, il prend le pas sur le résolveur configuré au niveau du client.

**Ordre de priorité pour la résolution des types :**

Le résolveur intervient dans une chaîne de priorité. Par ordre décroissant :

1. `ClickHouseType` défini explicitement sur le paramètre
2. Annotation de type SQL indiquée par la syntaxe `{name:Type}` dans la requête
3. `IParameterTypeResolver` (celui de `QueryOptions.ParameterTypeResolver` ou, à défaut, celui de `ClickHouseClientSettings.ParameterTypeResolver`)
4. Inférence de type intégrée (`TypeConverter.ToClickHouseType`)

Le résolveur fonctionne également avec `ClickHouseConnection` via ADO.NET : les connexions créées à partir du client héritent de ces paramètres.

***

<h3 id="parameter-value-formatting">
  Formatage personnalisé des valeurs de paramètres
</h3>

`IParameterFormatter` est un point d’extension qui détermine comment les valeurs des paramètres sont sérialisées. Utilisez-le lorsque le formatage intégré (par exemple, la précision de DateTime, les conventions locales pour les décimaux, l’échappement des chaînes ou la représentation des nombres) ne correspond pas à ce qu’attendent votre schéma ou les outils en aval.

Définissez `ParameterFormatter` dans `ClickHouseClientSettings` pour appliquer un formateur à toutes les requêtes paramétrées. Ce formateur reçoit la valeur, le nom du type ClickHouse résolu et le nom du paramètre, puis renvoie la représentation sous forme de chaîne envoyée au serveur. Renvoyez `null` pour laisser le formateur par défaut prendre le relais.

**Utilisation de `DictionaryParameterFormatter` pour un formatage simple selon le type CLR :**

```csharp theme={null}
using ClickHouse.Driver.ADO.Parameters;

var settings = new ClickHouseClientSettings("Host=localhost")
{
    ParameterFormatter = new DictionaryParameterFormatter(new Dictionary<Type, Func<object, string>>
    {
        [typeof(DateTime)] = v => ((DateTime)v).ToString("yyyy-MM-ddTHH:mm:ss.ffffff",
            System.Globalization.CultureInfo.InvariantCulture),
        [typeof(decimal)] = v => ((decimal)v).ToString("F4",
            System.Globalization.CultureInfo.InvariantCulture),
    }),
};
using var client = new ClickHouseClient(settings);
```

**`IParameterFormatter` personnalisé pour les cas d’usage avancés :**

```csharp theme={null}
public class FixedDecimalFormatter : IParameterFormatter
{
    public string Format(object value, string typeName, string parameterName)
    {
        if (value is decimal d)
            return d.ToString("F4", System.Globalization.CultureInfo.InvariantCulture);
        return null; // Fall through for anything else
    }
}
```

Vous pouvez également définir un formateur pour chaque requête via `QueryOptions.ParameterFormatter`. Dans ce cas, il prévaut sur celui configuré au niveau du client.

**Valeurs composites :**

Le formateur s’applique aussi bien aux paramètres de collection de premier niveau qu’à chaque élément des valeurs composites (`Array`, `Tuple`, `Map`, `Nullable`, `LowCardinality`, `Variant`). Par exemple, une correspondance pour `typeof(int)` formate séparément chaque élément `Int32` d’un `Array(Int32)`.

**Encadrement par des apostrophes dans les valeurs composites :**

Pour les types ClickHouse assimilables à des chaînes (`String`, `FixedString`, `Enum8`, `Enum16`, `IPv4`, `IPv6`, `UUID`) présents dans un littéral composite, le driver entoure la sortie du formateur d’apostrophes, sans toutefois échapper son contenu. Si la chaîne renvoyée contient une apostrophe ou un antislash non échappé, le littéral composite sera mal formé et le serveur rejettera la requête.

Les paramètres de chaîne de premier niveau (non intégrés dans une valeur composite) sont utilisés tels quels, sans être entourés d’apostrophes ; il n’est donc pas nécessaire de les échapper.

**Ordre de priorité des formateurs :**

1. `IParameterFormatter` (défini via `QueryOptions.ParameterFormatter`, ou à défaut via `ClickHouseClientSettings.ParameterFormatter`). S’il renvoie une valeur autre que `null`, celle-ci est utilisée.
2. Le formatage intégré propre à chaque type dans `HttpParameterFormatter`.

Le formateur n’est pas appelé pour les valeurs `null` ou `DBNull` : elles sont toujours sérialisées sous la forme du marqueur de valeur nulle de ClickHouse (`\N`).

***

<h3 id="read-value-conversion">
  Conversion personnalisée des valeurs lues
</h3>

`IReadValueConverter` permet de transformer les valeurs renvoyées par le lecteur de données après désérialisation, sans modifier leur type CLR. Parmi les cas d’usage courants : définir `DateTime.Kind = Utc` pour une colonne `DateTime` sans fuseau horaire, supprimer les espaces en début et en fin de chaîne ou normaliser les chaînes de caractères, ou encore traiter une colonne JSON avant que ses valeurs ne parviennent au code de l’application.

Définissez `ReadValueConverter` dans `ClickHouseClientSettings` pour appliquer un convertisseur à toutes les lectures. Celui-ci est appelé une fois par colonne et par ligne, aussi bien par la voie des valeurs encapsulées (`GetValue`) que par la voie générique (`GetFieldValue<T>`). Si aucun convertisseur n’est défini, il n’y a aucun surcoût : le lecteur renvoie directement les valeurs.

**Utilisation de `DictionaryReadValueConverter` pour une conversion simple par type CLR :**

```csharp theme={null}
using ClickHouse.Driver.ADO.Readers;

var converter = new DictionaryReadValueConverter()
    .For<DateTime>(dt => DateTime.SpecifyKind(dt, DateTimeKind.Utc))
    .For<string>(s => s.Trim());

var settings = new ClickHouseClientSettings("Host=localhost")
{
    ReadValueConverter = converter,
};
using var client = new ClickHouseClient(settings);
```

Les valeurs dont le type CLR à l’exécution n’a pas été enregistré avec `For<T>` restent inchangées. La sélection repose sur une correspondance exacte du type CLR : enregistrez donc le type effectivement produit par le lecteur (par exemple, `For<JsonObject>` pour une colonne JSON en mode `JsonReadMode.Binary`).

**Convertisseur `IReadValueConverter` personnalisé pour les cas avancés :**

Si la sélection doit reposer sur la chaîne représentant le type côté ClickHouse (par exemple, pour distinguer `DateTime` de `DateTime('UTC')`, qui sont tous deux représentés par le même type CLR), implémentez directement `IReadValueConverter` :

```csharp theme={null}
public class UtcKindForNoTzDateTimeConverter : IReadValueConverter
{
    public object ConvertValue(object value, string columnName, string clickHouseType)
    {
        if (value is DateTime dt && clickHouseType == "DateTime")
            return DateTime.SpecifyKind(dt, DateTimeKind.Utc);
        return value;
    }

    public T ConvertValue<T>(T value, string columnName, string clickHouseType)
    {
        if (typeof(T) == typeof(DateTime) && value is DateTime dt && clickHouseType == "DateTime")
            return (T)(object)DateTime.SpecifyKind(dt, DateTimeKind.Utc);
        return value;
    }
}
```

Le convertisseur doit préserver le type CLR à l’exécution ; les métadonnées de la colonne (`GetFieldType`, `GetSchemaTable`) ne passent pas par lui et doivent rester cohérentes avec la valeur renvoyée.

Vous pouvez également définir un convertisseur pour chaque requête via `QueryOptions.ReadValueConverter` ; s’il est défini, il prévaut sur le convertisseur configuré au niveau du client.

**Périmètre d’application du convertisseur :**

Le convertisseur est appelé une fois par colonne, avec la valeur complète de la cellule désérialisée ; il ne parcourt **pas** récursivement les conteneurs composites. Pour une colonne `Array(Int32)`, la valeur transmise est un `int[]` ; pour `Tuple(Int32, String)`, c’est un `ITuple`.

**Surcharge appelée :**

Les deux surcharges doivent produire des résultats cohérents, car celle qu’appelle le driver dépend de la façon dont le code appelant a lu la
colonne :

* `ConvertValue<T>` — les accesseurs typés `GetByte`, `GetSByte`, `GetInt16`/`32`/`64`,
  `GetUInt16`/`32`/`64`, `GetFloat`, `GetDouble`, `GetGuid`, `GetDateTime`, `GetIPAddress`,
  `GetBigInteger` et `GetFieldValue<T>`, ainsi que chaque colonne lue sans mise en boîte par le
  [chemin de lecture des POCO](#poco-read-converters).
* `ConvertValue` (avec mise en boîte) — `GetValue`, `GetValues`, les indexeurs, `GetChar`, `GetTuple` et les
  chemins de conversion de `GetBoolean`, `GetDecimal` et `GetString`.

`IsDBNull` n’appelle aucun convertisseur : il lit directement l’indicateur de valeur nulle. Un convertisseur ne peut donc jamais
modifier le statut nul d’une valeur. `TryGetEnumOrdinal` ne passe pas non plus par le convertisseur ; voir
[la lecture de la valeur ordinale d’une énumération](#ado-net-reader-enum-ordinal).

Le convertisseur fonctionne également avec `ClickHouseConnection` d’ADO.NET : les connexions créées à partir du client héritent de ces paramètres.

***

<h3 id="raw-streaming">
  Diffusion en flux brut
</h3>

Utilisez `ExecuteRawResultAsync` pour diffuser directement les résultats de la requête au format souhaité, sans passer par le lecteur de données. Cette méthode est utile pour exporter des données dans des fichiers ou les transmettre à d'autres systèmes :

```csharp theme={null}
using var result = await client.ExecuteRawResultAsync(
    "SELECT * FROM default.my_table LIMIT 100 FORMAT JSONEachRow"
);

await using var stream = await result.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
var json = await reader.ReadToEndAsync();
```

Formats courants : `JSONEachRow`, `CSV`, `TSV`, `Parquet`, `Native`. Consultez la [documentation sur les formats](/fr/reference/formats/index) pour connaître toutes les options.

***

<h3 id="per-query-accept-encoding">
  Compression des données transportées par requête
</h3>

Par défaut, le client négocie `zstd, lz4, gzip, deflate` lorsque `Compression=true` (valeur par défaut dans la chaîne de connexion) et décompresse lui-même le flux, de manière transparente.

Pour les exportations de données brutes (par exemple, Parquet, Arrow, Native), vous pouvez négocier un autre codec (par exemple, `zstd` ou `lz4`) afin de réduire le débit nécessaire au prix d’une utilisation accrue du CPU, sans modifier le paramètre de compression de la connexion. `QueryOptions.AcceptEncoding` et `ClickHouseCommand.AcceptEncoding` définissent l’en-tête HTTP `Accept-Encoding` pour une seule requête, à la place de toute valeur par défaut, et ajoutent `enable_http_compression=1` à l’URL (ce que ClickHouse exige pour prendre en compte `Accept-Encoding`).

```csharp theme={null}
using var result = await client.ExecuteRawResultAsync(
    "SELECT * FROM events FORMAT Parquet",
    options: new QueryOptions { AcceptEncoding = "zstd" });

// Decode yourself or write to a file
await using var body = await result.ReadAsStreamAsync();
```

<h4 id="per-query-accept-encoding-httpclient">
  Configuration de HttpClient
</h4>

Rien à configurer : le `HttpClient` créé par le driver conserve `AutomaticDecompression` à `DecompressionMethods.None`, et le driver décode lui-même les réponses. Ainsi, l’en-tête `Content-Encoding` n’est jamais supprimé à votre insu et vous recevez le corps brut exactement tel que le serveur l’a envoyé.

<Warning>
  Si vous fournissez votre propre `HttpClient`, laissez aussi `AutomaticDecompression` désactivé. Ce paramètre n’agit pas seulement sur les réponses : au moment de l’envoi, le handler **ajoute tous les algorithmes de son masque absents de l’en-tête `Accept-Encoding` sortant**. Un handler configuré avec `GZip | Deflate` transforme donc un `AcceptEncoding = "lz4"` explicite en `lz4, gzip, deflate`, et un `"identity"` explicite en `identity, gzip, deflate` dans les données transmises. Comme ClickHouse interprète l’en-tête selon un ordre de préférence des codecs qui lui est propre et qui est fixe (sans tenir compte de l’ordre ni des valeurs q), il peut répondre avec un codec que vous n’avez jamais demandé. Le handler décode alors la réponse et supprime l’en-tête, si bien que vous ne pouvez même pas vous en apercevoir. En laissant le masque désactivé, vous vous assurez que seuls les codecs choisis sont proposés.
</Warning>

<Warning>
  Si `AcceptEncoding` demande un codec que le driver ne peut pas décoder (`snappy`), seul `ExecuteRawResultAsync` peut être utilisé sans risque. `ExecuteReaderAsync`, `ExecuteScalarAsync` et `ExecuteNonQueryAsync` échouent en levant une `NotSupportedException` qui indique le codec concerné (auparavant, ils interprétaient les octets compressés comme le format du résultat et produisaient des données inutilisables).
</Warning>

<h4 id="per-query-accept-encoding-errors">
  Corps des erreurs
</h4>

Lorsque le serveur renvoie un code 4xx/5xx alors que `enable_http_compression=1` est défini, il compresse le corps de l’erreur avec le codec qu’il aurait utilisé pour une réponse réussie. Le driver décode ce corps pour chacun des codecs qu’il prend en charge (`lz4`, `zstd`, `gzip`, `deflate`, `br`/`brotli`), afin que le message de `ClickHouseServerException` soit lisible. Pour les autres codecs (`snappy`, …), il renvoie un caractère de substitution qui indique le nom du codec et renvoie à `system.query_log` pour retrouver le texte d’origine de l’erreur.

***

<h3 id="response-decompression">
  Décompression des réponses
</h3>

`Accept-Encoding` demande seulement au serveur de compresser la réponse : encore faut-il la décompresser. Le driver s’en charge lui-même en se fondant sur l’en-tête `Content-Encoding` de la réponse. Toutes les API de lecture habituelles (`ExecuteReaderAsync`, `ExecuteScalarAsync`, `ExecuteNonQueryAsync`, `QueryAsync<T>`, Dapper, EF Core, linq2db) fonctionnent donc avec les réponses compressées sans configuration supplémentaire. Il décompresse `lz4`, `zstd`, `gzip`, `deflate` et `br` ; `snappy` n’est pas pris en charge.

Par défaut, le driver annonce **`zstd, lz4, gzip, deflate`**, et ClickHouse répond en `zstd`. Pour utiliser un autre format, définissez vous-même `Accept-Encoding` pour l’ensemble du client :

```csharp theme={null}
using var client = new ClickHouseClient(new ClickHouseClientSettings("Host=localhost")
{
    AcceptEncoding = "br",      // decodable, but not advertised by default
});
```

par requête, qui prévaut :

```csharp theme={null}
using var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM events",
    options: new QueryOptions { AcceptEncoding = "identity" });   // opt this query out
```

ou dans la chaîne de connexion, pour les utilisateurs d’ORM qui ne modifient jamais `ClickHouseClientSettings` :

```text theme={null}
Host=localhost;AcceptEncoding=br, gzip
```

Le définir ajoute également `enable_http_compression=1` à l’URL, condition exigée par ClickHouse pour prendre en compte l’en-tête, même lorsque `UseCompression` vaut `false` : nommer explicitement un codec revient à en demander un. Si aucune valeur n’est définie, `UseCompression=false` n’envoie pas d’en-tête `Accept-Encoding`.

`Accept-Encoding` peut être défini à quatre endroits. Le premier à indiquer un codec prévaut :

1. `QueryOptions.AcceptEncoding` (ou `ClickHouseCommand.AcceptEncoding`)
2. `CustomHeaders["Accept-Encoding"]` sur la requête
3. `CustomHeaders["Accept-Encoding"]` sur le client
4. `ClickHouseClientSettings.AcceptEncoding`, ou le mot-clé `AcceptEncoding` de la chaîne de connexion

Si aucun n’indique de codec, le driver envoie sa liste par défaut. Une valeur qui n’indique aucun codec (null, vide,
composée uniquement d’espaces ou de virgules) est considérée comme non définie : c’est alors l’emplacement suivant qui est pris en compte. Pour désactiver la compression, utilisez `identity`.

**C’est le serveur, et non le client, qui choisit le codec.** ClickHouse examine `Accept-Encoding` selon son propre ordre de préférence, qui est fixe — `zstd` > `br` > `lz4` > `snappy` > `gzip` > `deflate` — sans tenir compte de l’ordre dans lequel vous indiquez les codecs ni des valeurs q. L’en-tête indique donc les codecs acceptés sans imposer de choix : pour orienter celui-ci, il faut omettre certains codecs. La liste par défaut contient `zstd` : une requête utilisant cette liste reçoit donc une réponse compressée avec zstd, les autres codecs servant de solutions de repli. `br` peut être décodé, mais ne figure pas dans la liste par défaut.

La taille des charges utiles, l’utilisation du CPU du serveur et celle du client varient selon le codec, vos données, votre connexion et le paramètre `http_zlib_compression_level` du serveur (valeur par défaut à la livraison : 3) — voir [Optimiser la compression](#tuning-compression).

* **`http_zlib_compression_level`.** Ce paramètre s’applique à tous les codecs HTTP et sa valeur par défaut est 3. Ajustez-le en fonction de vos données, du débit de votre connexion et de l’utilisation du CPU.
* **Un client limité par le CPU sur une connexion rapide.** Le driver décode le corps de la réponse sur le thread appelant. Lorsque le réseau n’est pas le goulot d’étranglement, la vitesse de décodage côté client peut donc devenir le facteur limitant.

Si l’un de ces cas s’applique, demandez un autre codec pour chaque requête ou pour l’ensemble du client :

```csharp theme={null}
using var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM events",
    options: new QueryOptions { AcceptEncoding = "lz4" });   // decode this one with lz4 instead
```

Comme la décision repose sur la réponse, le corps est décodé lorsque son en-tête `Content-Encoding` l’indique, quelle que soit la requête initiale : si cet en-tête est absent ou vaut `identity`, le corps reste inchangé ; s’il indique un codec pris en charge, le corps est décodé ; toute autre valeur déclenche une erreur qui la mentionne. Il n’y a aucun risque de double décodage : si la propriété `AutomaticDecompression` d’un handler fourni par l’appelant a déjà décodé le corps, elle supprime également l’en-tête `Content-Encoding`. Le driver reçoit donc des données en clair et les laisse telles quelles.

**Les résultats bruts n’annoncent aucun codec.** `ExecuteRawResultAsync` (ainsi que les méthodes publiques `PostStreamAsync` / `InsertRawStreamAsync`) vous transmettent le corps de la réponse tel quel. Si vous ne spécifiez pas vous-même de codec, ces méthodes n’en demandent donc aucun : le driver ne décode pas ce corps, et annoncer un codec dans ce cas transformerait silencieusement une exportation en fichier compressé. La règle est donc simple, quelle que soit la configuration de `HttpClient` : **le corps de la réponse arrive exactement tel que le serveur l’a envoyé, et le serveur envoie des données en clair sauf si vous avez demandé un codec.** Demander un codec (pour l’ensemble du client ou pour une requête donnée) permet d’exporter délibérément des octets compressés.

Un `AcceptEncoding` explicite (à l’un ou l’autre niveau) s’applique aussi aux requêtes brutes, et `ClickHouseRawResult.ReadDecompressedStreamAsync()` décode le résultat si vous le souhaitez ; `ReadAsStreamAsync`, `ReadAsByteArrayAsync`, `ReadAsStringAsync` et `CopyToAsync` renvoient toujours les octets exactement tels qu’ils ont été reçus.

```csharp theme={null}
using var result = await client.ExecuteRawResultAsync(
    "SELECT * FROM events FORMAT JSONEachRow",
    options: new QueryOptions { AcceptEncoding = "lz4" });

Console.WriteLine(result.ContentEncoding); // "lz4"

await using var body = await result.ReadDecompressedStreamAsync();
using var bodyReader = new StreamReader(body);
var json = await bodyReader.ReadToEndAsync();
```

Lisez le flux renvoyé jusqu’au bout avant qu’il ne sorte de portée, comme ci-dessus. Si la réponse *est* compressée, vous obtenez un décodeur créé avec `leaveOpen` : le libérer n’affecte donc pas la réponse. Si elle n’est **pas** compressée, vous obtenez directement le flux du contenu HTTP : le libérer ferme donc le corps de la réponse. Dans les deux cas, `ClickHouseRawResult` est propriétaire de la réponse : n’appelez plus ses autres méthodes de lecture après avoir libéré le flux. Vous devez toujours libérer `ClickHouseRawResult`, et cela suffit : cette opération libère la réponse ainsi que tout décodeur créé ici (les décodeurs conservent des tampons provenant d’un pool). Le `await using` ci-dessus est donc facultatif, mais vous pouvez le conserver sans risque. Plusieurs appels successifs renvoient le même flux ; ce type ne peut pas être utilisé simultanément par plusieurs opérations.

Consultez [Select\_007\_ResponseCompression.cs](https://github.com/ClickHouse/clickhouse-cs/blob/main/examples/Http/Select/Select_007_ResponseCompression.cs) pour un exemple exécutable.

<h4 id="insert-compression">
  Compression des requêtes d’insertion
</h4>

Zstd est le codec utilisé par défaut pour les insertions : `InsertOptions.Compressor` est initialisé à `ZstdCompressor.Default`,
qui correspond à zstd au niveau 3. Pour changer de codec, définissez un autre compresseur ; pour envoyer le
corps sans compression, définissez `null`.

```csharp theme={null}
var options = new InsertOptions { Compressor = GZipCompressor.Default };  // Content-Encoding: gzip
await client.InsertBinaryAsync("events", columns, rows, options);
```

Le driver est fourni avec quatre codecs. Chacun dispose d’une instance `Default` et d’un constructeur qui accepte un niveau
et une taille de tampon d’écriture :

| Compresseur | `Content-Encoding` | Constructeur | `Default` |
| - | - | - | - |
| `ZstdCompressor` | `zstd` | `(int level = 3, int bufferSize = 262144)` | niveau 3 |
| `Lz4Compressor` | `lz4` | `(Lz4Level level = Lz4Level.Fast, int bufferSize = 262144)` | `Lz4Level.Fast` |
| `GZipCompressor` | `gzip` | `(CompressionLevel level = CompressionLevel.Fastest, int bufferSize = 262144)` | `Fastest` |
| `BrotliCompressor` | `br` | `(CompressionLevel level = CompressionLevel.Fastest, int bufferSize = 262144)` | `Fastest` |

```csharp theme={null}
var options = new InsertOptions { Compressor = new ZstdCompressor(level: 1) };
```

<Note>
  *Partagez les instances de compresseur.* Chaque `Default` est une instance partagée, et les quatre compresseurs peuvent
  être utilisés simultanément par plusieurs threads sans risque — ce qui se produit lorsque
  `InsertOptions.MaxDegreeOfParallelism` est supérieur à 1, car une même insertion utilise un compresseur par
  lot. Aucun d’entre eux n’implémente `IDisposable`. Créez votre propre instance une seule fois, puis réutilisez-la
  comme vous le feriez avec `Default`.
</Note>

<h5 id="custom-compressor">
  Un codec personnalisé
</h5>

`IClickHouseCompressor` est public et toute implémentation ne doit fournir que deux membres :

```csharp theme={null}
public sealed class MyCompressor : IClickHouseCompressor
{
    public string ContentEncoding => "my-codec";

    public Stream Compress(Stream destination, bool leaveOpen) => /* a compressing write stream */;
}
```

Le serveur doit accepter la valeur de `Content-Encoding` que vous indiquez. Les autres membres —
`Decompress`, `MethodByte`, `MaxEncodedLength`, `Encode` et `Decode` — disposent d’implémentations
par défaut qui lèvent `NotSupportedException` ; ne redéfinissez donc que ceux dont votre codec a besoin.
Implémentez `Decompress` pour décoder les corps des réponses, en complément de la compression des requêtes.
Si un corps est corrompu ou dans un format incorrect, le flux renvoyé doit lever `InvalidDataException`.

`InsertOptions.Compressor` ne s’applique qu’aux insertions binaires. Les autres corps de requête du driver suivent d’autres règles de compression et ne passent pas par ce paramètre :

* **Toute requête contenant du texte SQL** (`ExecuteReaderAsync`, `ExecuteScalarAsync`, `ExecuteNonQueryAsync`, `QueryAsync<T>`, `ExecuteRawResultAsync`, la couche ADO.NET) envoie son statement avec `Content-Encoding: gzip` lorsque `UseCompression` vaut `true`, ce qui est le cas par défaut. Le codec n’est pas configurable : `AcceptEncoding` ne concerne que la réponse. Pour la requête, c’est donc gzip ou aucune compression. Avec `Compression=false`, le statement est envoyé sans compression. Les statements sont petits : il est rarement nécessaire de s’en préoccuper, mais cette distinction est utile lorsque vous examinez les requêtes dans un proxy ou une capture de paquets.
* **Un corps multipart** — une requête dont les paramètres sont envoyés sous forme de données de formulaire (`UseFormDataParameters=true`) — est toujours envoyé sans compression, quelle que soit la valeur de `UseCompression`.
* **Un téléversement brut** (`InsertRawStreamAsync`, `PostStreamAsync`) dispose de son propre indicateur, défini à chaque appel, et ne tient compte ni de `UseCompression` ni de `InsertOptions.Compressor` : il utilise gzip si cet indicateur est activé, et aucune compression sinon. Notez que le paramètre `useCompression` d’`InsertRawStreamAsync` vaut `true` par défaut : un téléversement brut est donc compressé avec gzip, sauf si vous passez `false`, même lorsque `Compression=false` est défini sur le client.

***

<h3 id="tuning-compression">
  Optimiser la compression
</h3>

La compression réduit le volume de données transférées au prix d’une consommation accrue de CPU. Son intérêt dépend presque entièrement du débit de votre
connexion par rapport à la vitesse d’exécution du codec. Aucun paramètre ne convient à
tous les cas.

<h4 id="the-one-number-that-decides-it">
  Le chiffre qui fait toute la différence
</h4>

La compression est avantageuse tant que le codec est plus rapide que le réseau.

Ce seuil est plus bas qu’on ne le pense généralement lors de la lecture, car ClickHouse compresse
les réponses HTTP sur un seul thread dans le tampon de sortie. Sur un service ClickHouse Cloud à 16 vCPU,
les mesures (`hits`, RowBinary, niveau 3) montrent que le serveur produit des données compressées à un débit d’environ 100 à 200 Mo/s.

Ainsi, pour un résultat volumineux, si une seule requête est traitée à la fois, la compression cesse d’être avantageuse aux alentours de 100 Mo/s. Un seul flux HTTPS
au sein d’une même région cloud dépasse généralement ce débit, tandis qu’un flux traversant l’Internet public, un VPN ou une frontière entre régions reste généralement en dessous.

Lors des insertions, la compression reste avantageuse jusqu’à des débits réseau plus élevés, car le client compresse les données sur un cœur qui lui est dédié et le fait généralement plus vite que le serveur ne compresse les réponses.

<h4 id="rough-guide-by-deployment">
  Repères selon le type de déploiement
</h4>

| Où s’exécute votre client | Débit typique | Lectures | Insertions |
| - | - | - | - |
| Même hôte / interface de bouclage | > 500 MB/s | `identity` | `lz4` est le plus rapide, ou aucune compression |
| Même région, même cloud | \~100–500 MB/s | `identity` ou `lz4` | `zstd:1` |
| Entre régions, même cloud | \~10–100 MB/s | `zstd` | `zstd:3` |
| Internet / VPN / cloud différent | \< 25 MB/s | `zstd` | `zstd:3` |
| Trafic facturé au volume ou connexion très limitée | \< 5 MB/s | `zstd` | `zstd:5`+ ou `br` |

Ce tableau ne tient pas compte de trois facteurs :

* **Coût du trafic sortant :** si le transfert de données vous est facturé, chaque octet a un coût, indépendamment de la latence, ce qui
  incite à choisir une compression plus forte, quelle que soit la vitesse de la connexion.
* **Petits résultats :** les indications ci-dessus concernent les charges utiles volumineuses. Pour les petites réponses, le choix du codec a peu
  d’importance : c’est le coût de traitement de chaque requête qui prédomine.
* **Les insertions parallèles repoussent les seuils de débit indiqués pour les insertions.** Chaque valeur de débit ci-dessus correspond à un *seul* thread. `InsertOptions.MaxDegreeOfParallelism` vaut `1` par défaut, mais augmenter cette valeur permet de compresser plusieurs lots en parallèle. Le débit d’encodage global du client augmente donc à peu près proportionnellement au nombre de cœurs qui lui sont alloués. Ainsi, même sur une connexion rapide, il peut rester intéressant de
  compresser une insertion parallèle à des débits bien supérieurs à ceux pour lesquels la compression cesse d’être avantageuse avec un seul thread. Considérez les débits des lignes du tableau consacrées aux insertions comme des *planchers* et, si vous traitez déjà les lots en parallèle, refaites des tests avant de conclure que votre connexion est trop rapide pour que la compression soit utile.

Les lectures ne peuvent être parallélisées qu’en exécutant plusieurs requêtes.

<h4 id="choosing-a-codec">
  Choisir un codec
</h4>

| Codec | Taux de compression | À privilégier quand | Point d’attention |
| - | - | - | - |
| `lz4` | le plus faible | La connexion est rapide et les ressources CPU sont plus limitées que le débit. Son décodage est de loin le moins coûteux, et c’est le plus rapide sur les petits résultats. C’est donc le codec à choisir pour remplacer `zstd`, utilisé par défaut. | Il n’a **pas de codeur entropique** : sur des données dont la distribution est déséquilibrée, mais qui sont peu répétitives (de longues suites de texte numérique, par exemple), son taux de compression est nettement inférieur à celui des autres. C’est aussi le codec le plus pénalisé par une augmentation de `http_zlib_compression_level` : passer du niveau 1 au niveau 3 multiplie sa consommation de CPU par environ 2,7 pour seulement 29 % d’octets en moins. |
| `zstd` | élevé | Le choix polyvalent dès que les données transitent sur un réseau. Il offre le meilleur taux de compression pour le coût CPU aux niveaux les plus utiles. Au niveau 3, il fait mieux que `lz4` sur le volume de données, la consommation de CPU côté serveur *et* le temps d’exécution. | Son **décodage** est plus coûteux que celui de `lz4` — 1,6 fois plus au niveau 3 dans nos mesures, même si les deux sont comparables au niveau 1 — et le driver effectue le décodage sur le thread appelant. Avec `http_zlib_compression_level=1`, il consomme légèrement *plus* de CPU côté serveur que `lz4`. |
| `gzip` | moyen | L’interopérabilité : tous les proxys et toutes les passerelles le prennent en charge. | Dans nos mesures, `lz4` et `zstd` font mieux sur tous les plans : `gzip` produit plus d’octets que `zstd`, tout en consommant plusieurs fois plus de CPU pour l’encodage et 5 à 9 fois plus pour le décodage. Choisissez-le pour la compatibilité, pas pour les performances. |
| `br` | le plus élevé aux niveaux bas | Le débit est véritablement le facteur limitant et vous pouvez lui consacrer davantage de ressources CPU. | Sa consommation de CPU augmente fortement aux niveaux élevés : avec `http_zlib_compression_level=6`, nous avons mesuré une consommation côté serveur 3 à 4 fois supérieure à celle de `zstd`. Il n’est pas proposé par défaut, car il est prioritaire sur toutes les options de repli de la liste par défaut. |

<h4 id="levels">
  Niveaux
</h4>

La compression des réponses est contrôlée par un seul paramètre de serveur, `http_zlib_compression_level`, qui s'applique à *tous* les codecs HTTP, et pas seulement à zlib. Sa valeur par défaut est 3.

Ne le modifiez que si vos mesures le justifient. Au-delà de la valeur par défaut, le gain en volume est faible au regard du coût en CPU (pour `zstd`, passer de 3 à 6 double environ l'utilisation du CPU du serveur pour seulement 14 % d'octets en moins), et le coût de `br` devient démesuré. À l'inverse, au niveau 1, la situation change nettement : `lz4` consomme beaucoup moins de CPU et `zstd` perd son avantage sur ce point. Si nécessaire, définissez ce paramètre pour chaque requête :

```csharp theme={null}
using var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM events",
    options: new QueryOptions
    {
        AcceptEncoding = "zstd",
        CustomSettings = new Dictionary<string, object> { ["http_zlib_compression_level"] = 1 },
    });
```

<h4 id="measuring-your-own-crossover">
  Mesurer votre propre seuil de bascule
</h4>

Pour choisir le codec et le niveau de compression les mieux adaptés, le plus simple est de mesurer le temps d’exécution d’une même requête avec plusieurs codecs, puis de comparer les résultats.

```csharp theme={null}
foreach (var codec in new[] { "identity", "lz4", "zstd" })
{
    var sw = Stopwatch.StartNew();
    using var reader = await client.ExecuteReaderAsync(
        "SELECT ... FROM big_table",
        options: new QueryOptions { AcceptEncoding = codec });
    while (await reader.ReadAsync()) { }
    Console.WriteLine($"{codec,-9} {sw.ElapsedMilliseconds} ms");
}
```

Pour obtenir la vue correspondante côté serveur, lisez `ProfileEvents` dans `system.query_log` — définissez
`QueryOptions.QueryId` pour pouvoir retrouver la ligne :

```sql theme={null}
SELECT ProfileEvents['UserTimeMicroseconds'] + ProfileEvents['SystemTimeMicroseconds'] AS cpu_us,
       ProfileEvents['NetworkSendBytes'] AS sent_bytes,
       query_duration_ms
FROM system.query_log
WHERE query_id = 'your-query-id' AND type = 'QueryFinish';
```

Attention si vous effectuez vous-même ce test de performance : un simple `LIMIT n` sans `ORDER BY` renvoie des *lignes différentes
à chaque exécution*. Chaque essai compresse donc des données différentes, et les ratios ne sont plus comparables. Faites la comparaison
sur un ensemble de résultats fixe.

***

<h3 id="raw-stream-insert">
  Insertion à partir d’un flux brut
</h3>

Utilisez `InsertRawStreamAsync` pour insérer des données directement à partir de flux provenant de fichiers ou de la mémoire, dans des formats tels que CSV, JSON, Parquet ou tout autre [format pris en charge par ClickHouse](/fr/reference/formats/index).

**Insertion à partir d’un fichier CSV :**

```csharp theme={null}
using var response = await client.InsertRawStreamAsync(
    table: "my_table",
    stream: File.OpenRead("data.csv"),
    format: "CSV",
    columns: ["id", "product", "price"] // Optional: specify columns
);
```

<Warning>
  *Le driver prend possession du flux.* `InsertRawStreamAsync` et `PostStreamAsync` libèrent le
  flux que vous leur transmettez dès que la requête se termine, qu'elle réussisse ou non. Ne le libérez pas
  vous-même et ne le réutilisez pas ensuite : c'est pourquoi, dans l'exemple ci-dessus, le
  `FileStream` n'est pas placé dans un bloc `using`.

  Si vous ajoutez un bloc `using`, il s'exécute alors que le driver a déjà libéré le flux. Pour un `FileStream` ou un
  `MemoryStream`, ce second appel est sans conséquence. En revanche, si la méthode `Dispose` du flux restitue un
  tampon à un pool ou décrémente un compteur de références, la ressource est libérée deux fois.

  Le driver ne prend possession du flux qu'une fois les arguments acceptés : si l'appel lève `ArgumentException` ou
  `ArgumentNullException` parce qu'il manque une table, un flux ou un format, le flux reste sous votre responsabilité.
</Warning>

<Note>
  Consultez la [documentation des paramètres de format](/fr/reference/settings/formats) pour connaître les options permettant de contrôler l'ingestion des données.
</Note>

***

<h3 id="more-examples">
  Autres exemples
</h3>

Pour découvrir d’autres exemples d’utilisation, consultez le [répertoire des exemples](https://github.com/ClickHouse/clickhouse-cs/tree/main/examples) du dépôt GitHub.

<h2 id="ado-net">
  ADO.NET
</h2>

La bibliothèque prend entièrement en charge ADO.NET via `ClickHouseConnection`, `ClickHouseCommand` et `ClickHouseDataReader`. Cette API est indispensable pour intégrer des ORM (Dapper, Linq2db) ou utiliser les abstractions standard de .NET pour les bases de données.

<h3 id="ado-net-datasource">
  Gestion du cycle de vie avec ClickHouseDataSource
</h3>

**Créez toujours les connexions à partir d’un `ClickHouseDataSource`** pour assurer une gestion correcte de leur cycle de vie et bénéficier du pool de connexions. Le `ClickHouseDataSource` gère en interne un seul `ClickHouseClient`, dont toutes les connexions partagent le pool de connexions HTTP.

```csharp theme={null}
using ClickHouse.Driver.ADO;

// Create DataSource once (register as singleton in DI)
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default;Password=secret");

// Create lightweight connections as needed
await using var connection = await dataSource.OpenConnectionAsync();

// Use the connection
await using var command = connection.CreateCommand("SELECT version()");
var version = await command.ExecuteScalarAsync();
```

Pour l’injection de dépendances :

```csharp theme={null}
// In Startup.cs or Program.cs
services.AddSingleton(sp =>
{
    var factory = sp.GetRequiredService<IHttpClientFactory>();
    return new ClickHouseDataSource("Host=localhost", factory, "ClickHouse");
});

// In your service
public class MyService
{
    private readonly ClickHouseDataSource _dataSource;

    public MyService(ClickHouseDataSource dataSource)
    {
        _dataSource = dataSource;
    }

    public async Task DoWorkAsync()
    {
        await using var connection = await _dataSource.OpenConnectionAsync();
        // Use connection...
    }
}
```

<Warning>
  **Ne créez pas directement de `ClickHouseConnection`** dans le code de production. Chaque instanciation crée un nouveau client HTTP et un nouveau pool de connexions, ce qui risque d’épuiser les sockets sous une forte charge :

  ```csharp theme={null}
  // À NE PAS FAIRE : crée un nouveau pool de connexions à chaque fois
  using var conn = new ClickHouseConnection("Host=localhost");
  await conn.OpenAsync();
  ```

  Utilisez plutôt systématiquement `ClickHouseDataSource` ou partagez une même instance de `ClickHouseClient`.
</Warning>

***

<h3 id="ado-net-command">
  Utilisation de ClickHouseCommand
</h3>

Créez des commandes à partir d’une connexion pour exécuter des requêtes SQL :

```csharp theme={null}
await using var connection = await dataSource.OpenConnectionAsync();

// Create command with SQL
await using var command = connection.CreateCommand("SELECT * FROM my_table WHERE id = {id:Int64}");
command.AddParameter("id", 42L);

// Execute and read results
await using var reader = await command.ExecuteReaderAsync();
while (reader.Read())
{
    Console.WriteLine($"Name: {reader.GetString("name")}");
}
```

Méthodes de commande :

* `ExecuteNonQueryAsync()` - Pour les statements de type INSERT, UPDATE, DELETE ou DDL
* `ExecuteScalarAsync()` - Renvoie la première colonne de la première ligne
* `ExecuteReaderAsync()` - Renvoie un `ClickHouseDataReader` permettant de parcourir les résultats

***

<h3 id="ado-net-reader">
  Utilisation de ClickHouseDataReader
</h3>

Le `ClickHouseDataReader` permet d’accéder aux résultats des requêtes avec des types de données définis :

```csharp theme={null}
await using var reader = await command.ExecuteReaderAsync();

while (reader.Read())
{
    // Access by column index
    var id = reader.GetInt64(0);
    var name = reader.GetString(1);

    // Access by column name
    var email = reader.GetString("email");

    // Generic access
    var timestamp = reader.GetFieldValue<DateTime>("created_at");

    // Check for null
    if (!reader.IsDBNull("optional_field"))
    {
        var value = reader.GetString("optional_field");
    }
}
```

<h4 id="ado-net-reader-enum-ordinal">
  Lecture de la valeur ordinale d'une énumération
</h4>

Une colonne `Enum8` ou `Enum16` se présente sous la forme de son libellé : `GetFieldType` renvoie `string`, et
`GetString`, `GetValue` et `GetFieldValue<string>` renvoient tous le libellé. Les accesseurs numériques
lèvent une `InvalidCastException` pour une colonne d'énumération, car la valeur stockée est une chaîne.

Utilisez `TryGetEnumOrdinal` pour obtenir la valeur numérique associée au libellé :

```csharp theme={null}
if (reader.TryGetEnumOrdinal(ordinal, out int value))
    Console.WriteLine(value);   // e.g. 1 for 'Active' in Enum8('Active' = 1)
```

Elle renvoie `true` et définit `value` pour une colonne `Enum8`/`Enum16`, ainsi que pour une colonne `Nullable(Enum...)`
dont la cellule n’est pas NULL. Elle renvoie `false` et fixe `value` à `0` pour une cellule NULL ou pour
toute colonne qui n’est pas de type enum. La valeur ordinale est la
valeur signée issue du format wire : elle peut donc être négative et, dans le cas d’un `Enum16`, dépasser la capacité d’un
octet.

<h2 id="best-practices">
  Bonnes pratiques
</h2>

<h3 id="best-practices-connection-lifetime">
  Durée de vie des connexions et pool de connexions
</h3>

`ClickHouse.Driver` utilise `System.Net.Http.HttpClient` en interne. `HttpClient` dispose d’un pool de connexions pour chaque endpoint. Il en résulte que :

* Les sessions de base de données sont multiplexées sur les connexions HTTP gérées par le pool.
* Le pool renouvelle automatiquement les connexions HTTP.
* Les connexions peuvent rester actives après la libération des objets `ClickHouseClient` ou `ClickHouseConnection`.

**Pratiques recommandées :**

| Scénario | Approche recommandée |
| - | - |
| Usage général | Utilisez une instance unique de `ClickHouseClient` |
| ADO.NET / ORM | Utilisez `ClickHouseDataSource` (les connexions créées partagent le même pool) |
| Environnements avec injection de dépendances | Enregistrez `ClickHouseClient` ou `ClickHouseDataSource` en tant que singleton avec `IHttpClientFactory` |

<Warning>
  Si vous utilisez un `HttpClient` ou un `HttpClientFactory` personnalisé, veillez à définir `PooledConnectionIdleTimeout` sur une valeur inférieure au `keep_alive_timeout` du serveur pour éviter les erreurs liées aux connexions semi-fermées. Dans les déploiements Cloud, la valeur par défaut de `keep_alive_timeout` est de 10 secondes.
</Warning>

<Warning>
  Évitez de créer plusieurs instances de `ClickHouseClient` ou des instances autonomes de `ClickHouseConnection` sans `HttpClient` partagé. Chaque instance crée son propre pool de connexions.
</Warning>

***

<h3 id="best-practice-datetime">
  Gestion des valeurs DateTime
</h3>

1. **Utilisez UTC chaque fois que possible.** Stockez les timestamps dans des colonnes `DateTime('UTC')` et utilisez `DateTimeKind.Utc` dans votre code. Vous éviterez ainsi toute ambiguïté liée aux fuseaux horaires.

2. **Utilisez `DateTimeOffset` pour gérer explicitement les fuseaux horaires.** Ce type représente toujours un instant précis et contient le décalage par rapport à UTC.

3. **Précisez le fuseau horaire dans les annotations de type SQL.** Si vous utilisez des paramètres contenant des valeurs DateTime `Unspecified` pour des colonnes qui ne sont pas en UTC, indiquez le fuseau horaire dans la requête SQL :
   ```csharp theme={null}
   var parameters = new ClickHouseParameterCollection();
   parameters.AddParameter("dt", myDateTime);

   await client.ExecuteNonQueryAsync(
       "INSERT INTO table (dt) VALUES ({dt:DateTime('Europe/Amsterdam')})",
       parameters
   );
   ```

***

<h3 id="async-inserts">
  Insertions asynchrones
</h3>

Les [insertions asynchrones](/fr/concepts/features/operations/insert/asyncinserts) confient au serveur, plutôt qu’au client, le regroupement des données en lots. Le serveur met les données entrantes en tampon, puis les écrit dans le stockage lorsque les seuils configurés sont atteints, sans nécessiter de regroupement en lots côté client. Cette approche est particulièrement utile lorsque de nombreux agents envoient de petites charges utiles simultanément, comme pour les charges de travail d’observability.

Activez les insertions asynchrones à l’aide de `CustomSettings` ou de la chaîne de connexion :

```csharp theme={null}
// Using CustomSettings
var settings = new ClickHouseClientSettings("Host=localhost");
settings.CustomSettings["async_insert"] = 1;
settings.CustomSettings["wait_for_async_insert"] = 1; // Recommended: wait for flush acknowledgment

// Or via connection string
// "Host=localhost;set_async_insert=1;set_wait_for_async_insert=1"
```

**Deux modes** (contrôlés par `wait_for_async_insert`) :

| Mode | Comportement | Cas d’utilisation |
| - | - | - |
| `wait_for_async_insert=1` | L’insertion ne renvoie une réponse qu’après l’écriture des données sur disque. Les erreurs sont renvoyées au client. | **Recommandé** pour la plupart des charges de travail |
| `wait_for_async_insert=0` | L’insertion renvoie immédiatement une réponse dès que les données sont mises en tampon. Leur persistance n’est pas garantie. | Uniquement si la perte de données est acceptable |

<Warning>
  Avec `wait_for_async_insert=0`, les erreurs ne sont signalées qu’au moment du vidage du tampon et ne peuvent pas être rattachées à l’insertion d’origine. Le client n’exerce pas non plus de régulation du flux, ce qui peut entraîner une surcharge du serveur.
</Warning>

**Principaux paramètres :**

| Paramètre | Description |
| - | - |
| `async_insert_max_data_size` | Vide le tampon lorsqu’il atteint cette taille (en octets) |
| `async_insert_busy_timeout_ms` | Vide le tampon après ce délai (en millisecondes) |
| `async_insert_max_query_number` | Vide le tampon lorsque ce nombre de requêtes a été atteint |

***

<h3 id="best-practices-sessions">
  Sessions
</h3>

N’activez les sessions que si vous avez besoin de fonctionnalités côté serveur nécessitant la conservation d’un état, par exemple :

* Les tables temporaires (`CREATE TEMPORARY TABLE`)
* La conservation du contexte de requête d’un statement à l’autre
* Les paramètres propres à la session (`SET max_threads = 4`)

Lorsque les sessions sont activées, les requêtes sont sérialisées pour empêcher l’utilisation simultanée d’une même session. Cela ajoute un surcoût aux charges de travail qui n’ont pas besoin de conserver l’état de la session.

```csharp theme={null}
var settings = new ClickHouseClientSettings
{
    Host = "localhost",
    UseSession = true,
    SessionId = "my-session", // Optional -- will be auto-generated if not provided
};

using var client = new ClickHouseClient(settings);

await client.ExecuteNonQueryAsync("CREATE TEMPORARY TABLE temp_ids (id UInt64)");
await client.ExecuteNonQueryAsync("INSERT INTO temp_ids VALUES (1), (2), (3)");

var reader = await client.ExecuteReaderAsync(
    "SELECT * FROM users WHERE id IN (SELECT id FROM temp_ids)"
);
```

**Utilisation d’ADO.NET (pour la compatibilité avec les ORM) :**

```csharp theme={null}
var settings = new ClickHouseClientSettings
{
    Host = "localhost",
    UseSession = true,
    SessionId = "my-session",
};

var dataSource = new ClickHouseDataSource(settings);
await using var connection = await dataSource.OpenConnectionAsync();

await using var cmd1 = connection.CreateCommand("CREATE TEMPORARY TABLE temp_ids (id UInt64)");
await cmd1.ExecuteNonQueryAsync();

await using var cmd2 = connection.CreateCommand("INSERT INTO temp_ids VALUES (1), (2), (3)");
await cmd2.ExecuteNonQueryAsync();

await using var cmd3 = connection.CreateCommand("SELECT * FROM users WHERE id IN (SELECT id FROM temp_ids)");
await using var reader = await cmd3.ExecuteReaderAsync();
```

***

<h2 id="supported-data-types">
  Types de données pris en charge
</h2>

`ClickHouse.Driver` prend en charge tous les types de données ClickHouse. Les tableaux ci-dessous indiquent les correspondances entre les types ClickHouse et les types .NET natifs utilisés pour lire les données de la base de données.

<h3 id="clickhouse-native-type-map-reading">
  Correspondance des types : lecture depuis ClickHouse
</h3>

<h4 id="type-map-reading-integer">
  Types entiers
</h4>

| Type ClickHouse | Type .NET |
| - | - |
| Int8 | `sbyte` |
| UInt8 | `byte` |
| Int16 | `short` |
| UInt16 | `ushort` |
| Int32 | `int` |
| UInt32 | `uint` |
| Int64 | `long` |
| UInt64 | `ulong` |
| Int128 | `BigInteger` |
| UInt128 | `BigInteger` |
| Int256 | `BigInteger` |
| UInt256 | `BigInteger` |

***

<h4 id="type-map-reading-floating-points">
  Types à virgule flottante
</h4>

| Type ClickHouse | Type .NET |
| - | - |
| Float32 | `float` |
| Float64 | `double` |
| BFloat16 | `float` |

***

<h4 id="type-map-reading-decimal">
  Types décimaux
</h4>

| Type ClickHouse | Type .NET |
| - | - |
| Decimal(P, S) | `decimal` / `ClickHouseDecimal` |
| Decimal32(S) | `decimal` / `ClickHouseDecimal` |
| Decimal64(S) | `decimal` / `ClickHouseDecimal` |
| Decimal128(S) | `decimal` / `ClickHouseDecimal` |
| Decimal256(S) | `decimal` / `ClickHouseDecimal` |

<Note>
  La conversion des types décimaux dépend du paramètre UseCustomDecimals.
</Note>

***

<h4 id="type-map-reading-boolean">
  Type booléen
</h4>

| Type ClickHouse | Type .NET |
| - | - |
| Bool | `bool` |

***

<h4 id="type-map-reading-strings">
  Types de chaînes de caractères
</h4>

| Type ClickHouse | Type .NET |
| - | - |
| String | `string` |
| FixedString(N) | `string` |

<Note>
  Par défaut, les colonnes `String` et `FixedString(N)` sont renvoyées sous forme de `string`. Pour les lire sous forme de `byte[]`, définissez `ReadStringsAsByteArrays=true` dans votre chaîne de connexion. C'est utile si vous stockez des données binaires qui ne sont pas forcément encodées en UTF-8 valide.

  Ce paramètre s'applique aussi aux chaînes de caractères imbriquées dans d'autres types : `Array(String)` est ainsi lu sous forme de `byte[][]`
  et `Map(String, String)` sous forme de `Dictionary<byte[], byte[]>`, y compris pour les clés. Seules les
  colonnes `JSON` font exception : leurs valeurs de type chaîne, même imbriquées, sont toujours renvoyées sous forme de texte ; voir [Type JSON](#type-map-reading-json).
</Note>

***

<h4 id="type-map-reading-datetime">
  Types de date et d'heure
</h4>

| Type ClickHouse | Type .NET |
| - | - |
| Date | `DateTime` |
| Date32 | `DateTime` |
| DateTime | `DateTime` |
| DateTime32 | `DateTime` |
| DateTime64 | `DateTime` |
| Time | `TimeSpan` |
| Time64 | `TimeSpan` |

ClickHouse stocke les valeurs `DateTime` et `DateTime64` en interne sous forme d'horodatages Unix (en secondes ou en fractions de seconde écoulées depuis l'époque Unix). Le stockage se fait toujours en UTC, mais un fuseau horaire peut être associé aux colonnes et influer sur l'affichage et l'interprétation des valeurs.

Lors de la lecture des valeurs `DateTime`, la propriété `DateTime.Kind` est définie selon le fuseau horaire de la colonne :

| Définition de la colonne | DateTime.Kind renvoyé | Remarques |
| - | - | - |
| `DateTime('UTC')` | `Utc` | Fuseau horaire UTC explicite |
| `DateTime('Europe/Amsterdam')` | `Unspecified` | Décalage appliqué |
| `DateTime` | `Unspecified` | Heure locale conservée telle quelle |

Pour les colonnes dont le fuseau horaire n'est pas UTC, la valeur `DateTime` renvoyée représente l'heure locale dans ce fuseau horaire. Utilisez `ClickHouseDataReader.GetDateTimeOffset()` pour obtenir une valeur `DateTimeOffset` avec le décalage horaire correspondant :

```csharp theme={null}
var reader = (ClickHouseDataReader)await connection.ExecuteReaderAsync(
    "SELECT toDateTime('2024-06-15 14:30:00', 'Europe/Amsterdam')");
reader.Read();

var dt = reader.GetDateTime(0);    // 2024-06-15 14:30:00, Kind=Unspecified
var dto = reader.GetDateTimeOffset(0); // 2024-06-15 14:30:00 +02:00 (CEST)
```

Pour les colonnes **sans** fuseau horaire explicite (c’est-à-dire `DateTime` plutôt que `DateTime('Europe/Amsterdam')`), le driver renvoie une valeur `DateTime` avec `Kind=Unspecified`. L’heure est ainsi conservée exactement telle qu’elle est stockée, sans présumer du fuseau horaire.

Si vous souhaitez tenir compte du fuseau horaire pour les colonnes qui n’en définissent pas explicitement, vous pouvez :

1. Définir un fuseau horaire explicite pour vos colonnes : `DateTime('UTC')` ou `DateTime('Europe/Amsterdam')`
2. Appliquer vous-même le fuseau horaire après la lecture.

***

<h4 id="type-map-reading-json">
  Type JSON
</h4>

| Type ClickHouse | Type .NET | Remarques |
| - | - | - |
| Json | `JsonObject` | Par défaut (`JsonReadMode=Binary`) |
| Json | `string` | Si `JsonReadMode=String` |

Le type renvoyé pour les colonnes JSON dépend du paramètre `JsonReadMode` :

* **`Binary` (par défaut)** : renvoie `System.Text.Json.Nodes.JsonObject`. Ce mode permet d’accéder aux données JSON de manière structurée, mais convertit les types ClickHouse spécialisés (comme les adresses IP, les UUID et les nombres décimaux de grande taille) en chaînes de caractères au sein de la structure JSON.

* **`String`** : renvoie le JSON brut sous forme de `string`. Ce mode conserve la représentation JSON exacte fournie par ClickHouse. Il est utile si vous devez transmettre le JSON sans l’analyser ou gérer vous-même la désérialisation.

```csharp theme={null}
// Configure string mode via settings
var settings = new ClickHouseClientSettings("Host=localhost")
{
    JsonReadMode = JsonReadMode.String
};

// Or via connection string
// "Host=localhost;JsonReadMode=String"
```

`None` est un troisième mode. Il lit les données de la même manière que `Binary`, mais n'envoie aucun paramètre serveur avec la
requête : utilisez-le sur une connexion qui n'autorise pas la définition d'un tel paramètre.

<h5 id="type-map-reading-json-nulls">
  Chemins typés et valeurs nulles
</h5>

Un chemin déclaré dans le type de la colonne est un **chemin typé** ; tout autre chemin du document est un
**chemin dynamique**. La différence apparaît lorsque la valeur est nulle.

Un chemin typé figure toujours dans le `JsonObject`. S'il est déclaré comme `Nullable(T)` ou `Dynamic`, il est renvoyé
sous forme de valeur nulle JSON, que la valeur stockée soit nulle ou que le document ne contienne pas ce chemin : ces deux
cas sont indiscernables :

```csharp theme={null}
// Column type JSON(x Nullable(Int64))
// stored '{"x":null}'  ->  {"x":null}
// stored '{}'          ->  {"x":null}
```

Lorsqu’un chemin absent est déclaré avec un type non nullable, il prend la valeur par défaut de ce type : `JSON(x String)`
donne `{"x":""}` et `JSON(x Int64)` donne `{"x":0}`.

Un chemin dynamique dont la valeur est nulle est entièrement omis de l’objet ; `ContainsKey` renvoie donc
false pour ce chemin. La lecture de `{"x":null}` dans une colonne `JSON` sans type de chemin déclaré donne `{}`.

Les chemins typés imbriqués créent leurs éléments parents : `JSON(a.b Nullable(Int64))` produit `{"a":{"b":null}}`
même pour un document vide.

<Note>
  C’est ainsi que le serveur lui-même représente ces valeurs : les modes `Binary` et `String` donnent désormais le même résultat. Avant la version 1.4.0, un
  chemin typé contenant une valeur nulle était omis du `JsonObject` ; `{"x":null}` était donc relu comme
  `{}` — et, pour un chemin imbriqué tel que `JSON(a.b Nullable(Int64))`, tout le sous-arbre `a` disparaissait.
</Note>

<h5 id="type-map-reading-json-strings">
  Chaînes de caractères dans une colonne JSON
</h5>

Les chaînes de caractères situées aux feuilles d'une colonne `JSON` sont toujours renvoyées sous forme de texte, quelle que soit
la valeur de `ReadStringsAsByteArrays` : `JsonValue` ne prend pas en charge les tableaux d'octets ; un `byte[]` serait donc
représenté en base64. Cela vaut pour `String` et `FixedString`, y compris lorsqu'ils sont encapsulés dans
`LowCardinality`, `Nullable` ou `SimpleAggregateFunction`, ainsi que pour les chaînes de caractères dans `Array` et `Map`,
y compris les clés des maps.

<Note>
  Un tableau d'octets dont le lecteur JSON ne peut pas connaître le type est néanmoins représenté en base64 : un
  chemin typé `Variant` ou `Dynamic` contient une valeur dont le type n'est connu qu'au niveau de chaque ligne. Une chaîne
  dans `Variant(Array(UInt8), String)` est donc renvoyée encodée en base64. Ce comportement est identique pour les deux valeurs du paramètre.

  Si le type d'une clé de map JSON n'est pas exactement `String` — par exemple,
  `Map(LowCardinality(String), String)` — une `NotSupportedException` est levée.
</Note>

<h5 id="overlapping-paths">
  Chemins qui se chevauchent
</h5>

ClickHouse accepte une colonne dans laquelle un chemin est déclaré à la fois comme valeur et comme parent d’un autre
chemin, par exemple `JSON(a Int64, a.b Int64)`. Les deux chemins sont présents dans chaque ligne : le serveur
produit donc une ligne avec une clé en double : `{"a":0,"a":{"b":7}}`. Un `JsonObject` ne peut pas contenir deux valeurs
pour une même clé. `JsonReadMode.Binary` lève donc une `SerializationException` qui indique les deux chemins. Il en va
de même lorsque la valeur est une `Map`, comme dans `JSON(a Map(String, Int64))`, si la ligne
contient aussi un chemin dynamique `a.b`.

Cela ne s’applique que si les deux chemins contiennent une valeur dans la ligne. Si l’un ne contient rien — une valeur nulle,
un objet vide ou un sous-arbre dont toutes les valeurs sont nulles —, il cède la place à celui qui contient des données,
quel que soit l’ordre dans lequel le serveur envoie les deux chemins. Avec des types `Nullable`, un seul des deux chemins est donc renseigné par ligne, et la lecture ne génère pas
d’erreur : `JSON(a Nullable(Int64), a.b Nullable(Int64))` donne `{"a":5}` et `{"a":{"b":7}}`, comme
prévu.

Lisez une telle colonne avec `JsonReadMode.String` pour obtenir le texte JSON tel que le serveur l'envoie, sans modification, y compris la clé
en double.

Définissez `AllowDuplicateJsonKeys` pour continuer à lire la colonne comme un `JsonObject` sans lever d’exception. Le
driver conserve alors celle des deux valeurs qui apparaît en dernier dans la ligne et ignore l’autre : la lecture entraîne donc
une perte de données. Ainsi, `JSON(a Int64, a.b Int64)` contenant `{"a.b":7}` est lu comme `{"a":0}`. Si un chemin
contient une valeur et que son parent contient un scalaire ou un tableau, une exception est tout de même levée, car un sous-arbre ne peut être
placé sous aucun des deux.

```csharp theme={null}
var settings = new ClickHouseClientSettings("Host=localhost")
{
    AllowDuplicateJsonKeys = true
};

// Or via connection string
// "Host=localhost;AllowDuplicateJsonKeys=true"
```

***

<h4 id="type-map-reading-map">
  Type Map
</h4>

| Type ClickHouse | Type .NET | Remarques |
| - | - | - |
| Map(K, V) | `Dictionary<K, V>` | Par défaut (`MapReadMode=Dictionary`) |
| Map(K, V) | `List<KeyValuePair<K, V>>` | Si `MapReadMode=KeyValuePairs` |

Dans ClickHouse, un `Map(K, V)` est stocké sous la forme d’un `Array(Tuple(K, V))` et peut contenir plusieurs entrées ayant la même clé. Ce n’est pas le cas d’un `Dictionary` : en mode par défaut, seule la dernière valeur associée à une clé répétée est conservée, et les paires précédentes sont supprimées. Le paramètre `MapReadMode` détermine la représentation utilisée :

* **`Dictionary` (par défaut)** : renvoie un `Dictionary<K, V>`.

* **`KeyValuePairs`** : renvoie un `List<KeyValuePair<K, V>>` dans l’ordre où le serveur a envoyé les paires. Toutes sont ainsi conservées, y compris celles dont la clé est répétée.

```csharp theme={null}
// Configure key-value-pair mode via settings
var settings = new ClickHouseClientSettings("Host=localhost")
{
    MapReadMode = MapReadMode.KeyValuePairs
};

// Or via connection string
// "Host=localhost;MapReadMode=KeyValuePairs"
```

Le mode détermine le type utilisé par le framework pour une colonne `Map` ; il s’applique donc aussi à `GetFieldValue<T>`, aux types de schéma renvoyés par le driver et au mappage des propriétés POCO. Il s’applique dès qu’un type Map apparaît dans l’arborescence des types d’une colonne, notamment dans `Array(Map(...))`, `Map(K, Map(...))`, `Tuple(..., Map(...))` et `Dynamic`.

Les deux représentations sont acceptées à l’écriture, quel que soit le mode ; voir [l’écriture des Map](#type-map-writing-other).

***

<h4 id="type-map-reading-other">
  Autres types
</h4>

| Type ClickHouse | Type .NET |
| - | - |
| UUID | `Guid` |
| IPv4 | `IPAddress` |
| IPv6 | `IPAddress` |
| Nothing | `DBNull` |
| Dynamic | Voir la note |
| Array(T) | `T[]` (le type imbriqué `Array(Array(T))` est lu comme un tableau de tableaux `T[][]` ; utilisez `reader.GetFieldValue<T[,]>(ordinal)` pour matérialiser des données rectangulaires dans un tableau CLR multidimensionnel) |
| Tuple(T1, T2, ...) | `Tuple<T1, T2, ...>` / `LargeTuple` |
| Map(K, V) | `Dictionary<K, V>`, ou `List<KeyValuePair<K, V>>` lorsque `MapReadMode=KeyValuePairs` — voir [le type Map](#type-map-reading-map) |
| Nullable(T) | `T?` |
| Enum8 | `string` |
| Enum16 | `string` |
| LowCardinality(T) | Identique à T |
| SimpleAggregateFunction | Identique au type sous-jacent |
| Nested(...) | `Tuple[]` |
| Variant(T1, T2, ...) | Voir la note |
| QBit(T, dimension) | `T[]` |

<Note>
  Pour chaque ligne, les valeurs des types Dynamic et Variant sont converties dans le type correspondant à leur type sous-jacent réel.
</Note>

***

<h4 id="type-map-reading-geometry">
  Types géométriques
</h4>

| Type ClickHouse | Type .NET |
| - | - |
| Point | `Tuple<double, double>` |
| Ring | `Tuple<double, double>[]` |
| LineString | `Tuple<double, double>[]` |
| Polygon | `Ring[]` |
| MultiLineString | `LineString[]` |
| MultiPolygon | `Polygon[]` |
| Geometry | Voir la note |

<Note>
  Le type Geometry est un type Variant qui peut contenir n'importe quel type géométrique. Sa valeur est convertie dans le type correspondant.
</Note>

***

<h3 id="clickhouse-native-type-map-writing">
  Correspondance des types : écriture dans ClickHouse
</h3>

Lors de l’insertion de données, le driver convertit les types .NET en types ClickHouse correspondants. Les tableaux ci-dessous indiquent les types .NET acceptés pour chaque type de colonne ClickHouse.

<h4 id="type-map-writing-integer">
  Types entiers
</h4>

| Type ClickHouse | Types .NET acceptés | Notes |
| - | - | - |
| Int8 | `sbyte`, tout type compatible avec `Convert.ToSByte()` | |
| UInt8 | `byte`, tout type compatible avec `Convert.ToByte()` | |
| Int16 | `short`, tout type compatible avec `Convert.ToInt16()` | |
| UInt16 | `ushort`, tout type compatible avec `Convert.ToUInt16()` | |
| Int32 | `int`, tout type compatible avec `Convert.ToInt32()` | |
| UInt32 | `uint`, tout type compatible avec `Convert.ToUInt32()` | |
| Int64 | `long`, tout type compatible avec `Convert.ToInt64()` | |
| UInt64 | `ulong`, tout type compatible avec `Convert.ToUInt64()` | |
| Int128 | `BigInteger`, `decimal`, `double`, `float`, `int`, `uint`, `long`, `ulong`, tout type compatible avec `Convert.ToInt64()` | |
| UInt128 | `BigInteger`, `decimal`, `double`, `float`, `int`, `uint`, `long`, `ulong`, tout type compatible avec `Convert.ToInt64()` | |
| Int256 | `BigInteger`, `decimal`, `double`, `float`, `int`, `uint`, `long`, `ulong`, tout type compatible avec `Convert.ToInt64()` | |
| UInt256 | `BigInteger`, `decimal`, `double`, `float`, `int`, `uint`, `long`, `ulong`, tout type compatible avec `Convert.ToInt64()` | |

***

<h4 id="type-map-writing-floating-point">
  Types à virgule flottante
</h4>

| Type ClickHouse | Types .NET acceptés | Notes |
| - | - | - |
| Float32 | `float`, tout type compatible avec `Convert.ToSingle()` | |
| Float64 | `double`, tout type compatible avec `Convert.ToDouble()` | |
| BFloat16 | `float`, tout type compatible avec `Convert.ToSingle()` | Tronque les valeurs au format « brain float » sur 16 bits |

***

<h4 id="type-map-writing-boolean">
  Type booléen
</h4>

| Type ClickHouse | Types .NET acceptés | Remarques |
| - | - | - |
| Bool | `bool` | |

***

<h4 id="type-map-writing-strings">
  Types de chaînes de caractères
</h4>

| Type ClickHouse | Types .NET acceptés | Notes |
| - | - | - |
| String | `string`, `byte[]`, `ReadOnlyMemory<byte>`, `Stream` | Les données binaires sont écrites directement ; les flux peuvent permettre ou non le repositionnement de la lecture |
| FixedString(N) | `string`, `byte[]`, `ReadOnlyMemory<byte>`, `Stream` | La chaîne de caractères est encodée en UTF-8 puis complétée ; les données binaires doivent contenir exactement N octets |

***

<h4 id="type-map-writing-datetime">
  Types de date et d’heure
</h4>

| Type ClickHouse | Types .NET acceptés | Remarques |
| - | - | - |
| Date | `DateTime`, `DateTimeOffset`, `DateOnly`, types NodaTime | Converti en nombre de jours Unix au format UInt16 ; plage prise en charge : `[1970-01-01, 2149-06-06]` |
| Date32 | `DateTime`, `DateTimeOffset`, `DateOnly`, types NodaTime | Converti en nombre de jours Unix au format Int32 ; plage prise en charge : `[1900-01-01, 2299-12-31]` |
| DateTime | `DateTime`, `DateTimeOffset`, `DateOnly`, types NodaTime | Voir les précisions ci-dessous ; plage prise en charge : `[1970-01-01, 2106-02-07 06:28:15]` UTC |
| DateTime32 | `DateTime`, `DateTimeOffset`, `DateOnly`, types NodaTime | Identique à DateTime |
| DateTime64 | `DateTime`, `DateTimeOffset`, `DateOnly`, types NodaTime | Précision déterminée par le paramètre Scale |
| Time | `TimeSpan`, `TimeOnly`, `int` | Valeur bornée à ±999:59:59 ; int est interprété comme un nombre de secondes |
| Time64 | `TimeSpan`, `TimeOnly`, `decimal`, `double`, `float`, `int`, `long`, `string` | Chaîne analysée au format `[-]HHH:MM:SS[.fraction]` ; valeur bornée à ±999:59:59.999999999 |

<Note>
  **Valeurs hors plage**

  Lors de l’écriture en binaire, les valeurs `Date`, `Date32`, `DateTime` et `DateTime32` hors de leur plage prise en charge déclenchent une `ArgumentOutOfRangeException` au moment de l’appel à `Write`. L’exception indique le type de la colonne et la plage prise en charge. Auparavant, ces valeurs pouvaient être tronquées sans avertissement lors de leur conversion en entier 32 bits, puis réinterprétées par le serveur, ce qui produisait des timestamps valides, mais erronés.
</Note>

Le driver tient compte de `DateTime.Kind` lors de l’écriture des valeurs :

| DateTime.Kind | Paramètres HTTP | Écriture en masse |
| - | - | - |
| Utc | Instant préservé | Instant préservé |
| Local | Instant préservé | Instant préservé |
| Unspecified | Heure interprétée telle quelle dans le fuseau horaire du type du paramètre (UTC par défaut) | Heure interprétée telle quelle dans le fuseau horaire de la colonne |

Les valeurs `DateTimeOffset` conservent toujours l’instant exact.

**Exemple : DateTime UTC (instant préservé)**

```csharp theme={null}
var utcTime = new DateTime(2024, 1, 15, 12, 0, 0, DateTimeKind.Utc);
// Stored as 12:00 UTC
// Read from DateTime('Europe/Amsterdam') column: 13:00 (UTC+1)
// Read from DateTime('UTC') column: 12:00 UTC
```

**Exemple : DateTime non spécifié (heure locale)**

```csharp theme={null}
var wallClock = new DateTime(2024, 1, 15, 14, 30, 0, DateTimeKind.Unspecified);
// Written to DateTime('Europe/Amsterdam') column: stored as 14:30 Amsterdam time
// Read back from DateTime('Europe/Amsterdam') column: 14:30
```

**Recommandation :** pour un comportement aussi simple et prévisible que possible, utilisez `DateTimeKind.Utc` ou `DateTimeOffset` pour toutes les opérations sur les valeurs DateTime. Votre code se comportera ainsi de la même manière, quel que soit le fuseau horaire du serveur, du client ou de la colonne.

<h4 id="datetime-http-param-vs-bulkcopy">
  Paramètres HTTP et copie en masse : quelle différence ?
</h4>

La liaison de paramètres HTTP et la copie en masse ne traitent pas de la même façon les valeurs DateTime `Unspecified` lors de l’écriture :

**La copie en masse** connaît le fuseau horaire de la colonne cible et interprète correctement les valeurs `Unspecified` dans ce fuseau horaire.

**Les paramètres HTTP** ne connaissent pas automatiquement le fuseau horaire de la colonne. Vous devez donc l’indiquer dans l’annotation de type SQL :

```csharp theme={null}
// CORRECT: Timezone in SQL type hint - type is extracted automatically
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime('Europe/Amsterdam')})";
command.AddParameter("dt", myDateTime);

// INCORRECT: Without timezone hint, interpreted as UTC
command.CommandText = "INSERT INTO table (dt_amsterdam) VALUES ({dt:DateTime})";
command.AddParameter("dt", myDateTime);
// String value "2024-01-15 14:30:00" interpreted as UTC, not Amsterdam time!
```

| `DateTime.Kind` | Colonne cible | Paramètre HTTP (avec indication du fuseau horaire) | Paramètre HTTP (sans indication du fuseau horaire) | Copie en masse |
| - | - | - | - | - |
| `Utc` | UTC | Instant préservé | Instant préservé | Instant préservé |
| `Utc` | Europe/Amsterdam | Instant préservé | Instant préservé | Instant préservé |
| `Local` | N’importe laquelle | Instant préservé | Instant préservé | Instant préservé |
| `Unspecified` | UTC | Interprété comme UTC | Interprété comme UTC | Interprété comme UTC |
| `Unspecified` | Europe/Amsterdam | Interprété comme l’heure d’Amsterdam | **Interprété comme UTC** | Interprété comme l’heure d’Amsterdam |

***

<h4 id="type-map-writing-decimal">
  Types décimaux
</h4>

| Type ClickHouse | Types .NET acceptés | Remarques |
| - | - | - |
| Decimal(P,S) | `decimal`, `ClickHouseDecimal`, tout type compatible avec `Convert.ToDecimal()` | Lève une `OverflowException` en cas de dépassement de la précision |
| Decimal32 | `decimal`, `ClickHouseDecimal`, tout type compatible avec `Convert.ToDecimal()` | Précision maximale : 9 |
| Decimal64 | `decimal`, `ClickHouseDecimal`, tout type compatible avec `Convert.ToDecimal()` | Précision maximale : 18 |
| Decimal128 | `decimal`, `ClickHouseDecimal`, tout type compatible avec `Convert.ToDecimal()` | Précision maximale : 38 |
| Decimal256 | `decimal`, `ClickHouseDecimal`, tout type compatible avec `Convert.ToDecimal()` | Précision maximale : 76 |

***

<h4 id="type-map-writing-json">
  Type JSON
</h4>

| Type ClickHouse | Types .NET acceptés | Notes |
| - | - | - |
| Json | `string`, `JsonObject`, `JsonNode`, tout objet | Le comportement dépend du paramètre `JsonWriteMode` |

Le paramètre `JsonWriteMode` détermine le comportement lors de l’écriture de données JSON :

| Type d’entrée | `JsonWriteMode.String` (par défaut) | `JsonWriteMode.Binary` |
| - | - | - |
| `string` | Transmis directement | Lève `ArgumentException` |
| `JsonObject` | Sérialisé avec `ToJsonString()` | Lève `ArgumentException` |
| `JsonNode` | Sérialisé avec `ToJsonString()` | Lève `ArgumentException` |
| POCO enregistré | Sérialisé avec `JsonSerializer.Serialize()` | Encodage binaire avec indications de type ; attributs de chemin personnalisés pris en charge |
| POCO non enregistré / objet anonyme | Sérialisé avec `JsonSerializer.Serialize()` | Lève `ClickHouseJsonSerializationException` |

* **`String` (par défaut)** : accepte `string`, `JsonObject`, `JsonNode` ou tout autre objet. Toutes les entrées sont sérialisées avec `System.Text.Json.JsonSerializer`, puis envoyées sous forme de chaînes JSON pour analyse côté serveur. C’est le mode le plus souple et il ne nécessite pas d’enregistrer les types.

* **`Binary`** : accepte uniquement les types POCO enregistrés. Les données sont converties côté client au format JSON binaire de ClickHouse, avec une prise en charge complète des indications de type. Il faut appeler `connection.RegisterJsonSerializationType<T>()` avant de l’utiliser. Dans ce mode, l’écriture de valeurs `string` ou `JsonNode` lève `ArgumentException`.

```csharp theme={null}
// Default String mode works with any input
await client.InsertBinaryAsync(
    "my_table",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new { name = "test", value = 42 } } }
);

// Binary mode requires explicit opt-in and type registration
var settings = new ClickHouseClientSettings("Host=localhost")
{
    JsonWriteMode = JsonWriteMode.Binary
};
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<MyPocoType>();
```

<h5 id="json-typed-columns">
  Colonnes JSON typées
</h5>

Lorsqu'une colonne JSON comporte des indications de type (par exemple, `JSON(id UInt64, price Decimal128(2))`), le driver s'en sert pour sérialiser les valeurs en respectant exactement leurs types. La précision des types tels que `UInt64`, `Decimal`, `UUID` et `DateTime64` est ainsi préservée, alors qu'elle serait perdue lors d'une sérialisation en JSON générique.

<h5 id="json-poco-serialization">
  Sérialisation des POCO
</h5>

Les POCO peuvent être écrits dans des colonnes JSON de deux manières, selon la valeur de `JsonWriteMode` :

**Mode String (par défaut)** : les POCO sont sérialisés avec `System.Text.Json.JsonSerializer`. Il n’est pas nécessaire d’enregistrer les types. C’est la méthode la plus simple, qui fonctionne aussi avec les objets anonymes.

**Mode Binary** : les POCO sont sérialisés au format JSON binaire du driver, avec une prise en charge complète des indications de type. Les types doivent être enregistrés avec `connection.RegisterJsonSerializationType<T>()` avant d’être utilisés. Ce mode permet de personnaliser la correspondance entre les propriétés et les chemins JSON à l’aide d’attributs :

* **`[ClickHouseJsonPath("path")]`** : associe une propriété à un chemin JSON personnalisé. Utile pour les structures imbriquées ou lorsque le nom de la propriété diffère de la clé JSON souhaitée. **Fonctionne uniquement en mode Binary.**

* **`[ClickHouseJsonIgnore]`** : exclut une propriété de la sérialisation. **Fonctionne uniquement en mode Binary.**

```sql theme={null}
CREATE TABLE events (
    id UInt32,
    data JSON(`user.id` Int64, `user.name` String, Timestamp DateTime64(3))
) ENGINE = MergeTree() ORDER BY id
```

```csharp theme={null}
using ClickHouse.Driver.Json;

public class UserEvent
{
    [ClickHouseJsonPath("user.id")]
    public long UserId { get; set; }

    [ClickHouseJsonPath("user.name")]
    public string UserName { get; set; }

    public DateTime Timestamp { get; set; }

    [ClickHouseJsonIgnore]
    public string InternalData { get; set; }  // Not serialized
}

// For Binary mode: Register the type and enable Binary mode
var settings = new ClickHouseClientSettings("Host=localhost") { JsonWriteMode = JsonWriteMode.Binary };
using var client = new ClickHouseClient(settings);
client.RegisterJsonSerializationType<UserEvent>();

// Insert POCO - serialized to JSON with nested structure via custom path attributes
await client.InsertBinaryAsync(
    "events",
    new[] { "id", "data" },
    new[] { new object[] { 1u, new UserEvent { UserId = 123, UserName = "Alice", Timestamp = DateTime.UtcNow } } }
);
// Resulting JSON: {"user": {"id": 123, "name": "Alice"}, "Timestamp": "2024-01-15T..."}
```

La correspondance entre les noms de propriétés et les indications de type des colonnes est sensible à la casse. Ainsi, la propriété `UserId` ne correspond qu’à une indication définie comme `UserId`, et non comme `userid`. Cela reflète le comportement de ClickHouse, qui permet à des chemins tels que `userName` et `UserName` de coexister en tant que champs distincts.

**Limitations (mode Binary uniquement) :**

* Les types POCO doivent être enregistrés sur la connexion à l’aide de `connection.RegisterJsonSerializationType<T>()` avant la sérialisation. La sérialisation d’un type non enregistré lève une exception `ClickHouseJsonSerializationException`.
* Pour être correctement sérialisées, les propriétés de type dictionnaire ou tableau/liste nécessitent des indications de type dans la définition de la colonne. À défaut, utilisez le mode String.
* Les valeurs nulles des propriétés POCO ne sont écrites que si le chemin possède une indication de type `Nullable(T)` dans la définition de la colonne. ClickHouse n'autorise pas les types `Nullable` dans les chemins JSON dynamiques ; les propriétés nulles sans indication de type sont donc ignorées.
* Les attributs `ClickHouseJsonPath` et `ClickHouseJsonIgnore` sont ignorés en mode String (ils ne fonctionnent qu’en mode Binary).

***

<h4 id="type-map-writing-other">
  Autres types
</h4>

| Type ClickHouse | Types .NET acceptés | Remarques |
| - | - | - |
| UUID | `Guid`, `string` | Chaîne convertie en Guid |
| IPv4 | `IPAddress`, `string` | Doit être une adresse IPv4 ; la chaîne est analysée avec `IPAddress.Parse()` |
| IPv6 | `IPAddress`, `string` | Doit être une adresse IPv6 ; la chaîne est analysée avec `IPAddress.Parse()` |
| Nothing | Tout type | N’écrit rien (opération sans effet) |
| Dynamic | — | **Non pris en charge** (lève `NotImplementedException`) |
| Array(T) | `IList`, `null` | Une valeur nulle entraîne l’écriture d’un tableau vide. Pour les types imbriqués (`Array(Array(T))` et au-delà), les tableaux irréguliers (`T[][]`, `List<List<T>>`) et les tableaux CLR multidimensionnels rectangulaires (`T[,]`, `T[,,]`, …) sont acceptés ; le nombre de dimensions du tableau CLR doit correspondre à la profondeur d’imbrication ClickHouse. |
| Tuple(T1, T2, ...) | `ITuple`, `IList` | Le nombre d’éléments doit correspondre à l’arité du tuple. Voir la [mise en garde concernant ValueTuple](#valuetuple-caveat) pour les tuples de plus de 7 éléments. |
| Map(K, V) | `IDictionary`, `IEnumerable<KeyValuePair<K, V>>` | Une séquence de paires (par exemple, le `List<KeyValuePair<K, V>>` produit par `MapReadMode=KeyValuePairs`) est acceptée quel que soit le mode de lecture et peut contenir plusieurs fois la même clé. Cela s’applique aux insertions binaires et aux paramètres de requête |
| Nullable(T) | `null`, `DBNull` ou types acceptés par T | Écrit l’octet indiquant si la valeur est nulle avant la valeur |
| Enum8 | `string`, `sbyte`, types numériques | Chaîne recherchée dans le dictionnaire de l’énumération |
| Enum16 | `string`, `short`, types numériques | Chaîne recherchée dans le dictionnaire de l’énumération |
| LowCardinality(T) | Types acceptés par T | Délègue au type sous-jacent |
| SimpleAggregateFunction | Types acceptés par le type sous-jacent | Délègue au type sous-jacent |
| Nested(...) | `IList` de tuples | Le nombre d’éléments doit correspondre au nombre de champs |
| Variant(T1, T2, ...) | Valeur correspondant à l’un des types T1, T2, ... | Lève `ArgumentException` si aucun type ne correspond |
| QBit(T, dim) | `IList` | Délègue à Array ; la dimension n’est qu’une métadonnée |

***

<h4 id="type-map-writing-geometry">
  Types géométriques
</h4>

| Type ClickHouse | Types .NET acceptés | Remarques |
| - | - | - |
| Point | `System.Drawing.Point`, `ITuple`, `IList` (2 éléments) | |
| Ring | `IList` de Point | |
| LineString | `IList` de Point | |
| Polygon | `IList` de Ring | |
| MultiLineString | `IList` de LineString | |
| MultiPolygon | `IList` de Polygon | |
| Geometry | N’importe lequel des types géométriques ci-dessus | Type Variant regroupant tous les types géométriques |

***

<h4 id="type-map-writing-not-supported">
  Non pris en charge en écriture
</h4>

| Type ClickHouse | Remarques |
| - | - |
| Dynamic | Lève `NotImplementedException` |
| AggregateFunction | Lève `AggregateFunctionException` |

***

<h3 id="nested-type-handling">
  Gestion des types imbriqués
</h3>

Les types imbriqués de ClickHouse (`Nested(...)`) se lisent et s’écrivent comme des tableaux.

```sql theme={null}
CREATE TABLE test.nested (
    id UInt32,
    params Nested (param_id UInt8, param_val String)
) ENGINE = Memory
```

```csharp theme={null}
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 client.InsertBinaryAsync(
    "test.nested",
    new[] { "id", "params.param_id", "params.param_val" },
    new[] { row1, row2 }
);
```

<h2 id="logging-and-diagnostics">
  Journalisation et diagnostic
</h2>

Le client .NET de ClickHouse s’appuie sur les abstractions de `Microsoft.Extensions.Logging` pour proposer une journalisation légère, activée uniquement si nécessaire. Lorsqu’elle est activée, le driver émet des messages structurés sur les événements liés au cycle de vie des connexions, l’exécution des commandes, les opérations de transport et les insertions en masse. La journalisation est entièrement facultative : les applications qui ne configurent pas de logger continuent de fonctionner sans surcoût.

<h3 id="logging-quick-start">
  Démarrage rapide
</h3>

```csharp theme={null}
using ClickHouse.Driver;
using Microsoft.Extensions.Logging;

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConsole()
        .SetMinimumLevel(LogLevel.Information);
});

var settings = new ClickHouseClientSettings("Host=localhost;Port=8123")
{
    LoggerFactory = loggerFactory
};

using var client = new ClickHouseClient(settings);
```

<h4 id="logging-appsettings-config">
  Utilisation d’appsettings.json
</h4>

Vous pouvez définir les niveaux de journalisation avec la configuration standard de .NET :

```csharp theme={null}
using ClickHouse.Driver;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

var configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json")
    .Build();

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConfiguration(configuration.GetSection("Logging"))
        .AddConsole();
});

var settings = new ClickHouseClientSettings("Host=localhost;Port=8123")
{
    LoggerFactory = loggerFactory
};

using var client = new ClickHouseClient(settings);
```

<h4 id="logging-inmemory-config">
  Utiliser une configuration en mémoire
</h4>

Vous pouvez également définir dans le code le niveau de détail de la journalisation pour chaque catégorie :

```csharp theme={null}
using ClickHouse.Driver;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

var categoriesConfiguration = new Dictionary<string, string>
{
    { "LogLevel:Default", "Warning" },
    { "LogLevel:ClickHouse.Driver.Connection", "Information" },
    { "LogLevel:ClickHouse.Driver.Command", "Debug" }
};

var config = new ConfigurationBuilder()
    .AddInMemoryCollection(categoriesConfiguration)
    .Build();

using var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConfiguration(config)
        .AddSimpleConsole();
});

var settings = new ClickHouseClientSettings("Host=localhost;Port=8123")
{
    LoggerFactory = loggerFactory
};

using var client = new ClickHouseClient(settings);
```

<h3 id="logging-categories">
  Catégories et émetteurs
</h3>

Le driver utilise des catégories distinctes pour vous permettre de régler le niveau de journalisation de chaque composant :

| Catégorie | Source | Points clés |
| - | - | - |
| `ClickHouse.Driver.Connection` | `ClickHouseConnection` | Cycle de vie des connexions, choix de la fabrique de clients HTTP, ouverture et fermeture des connexions, gestion des sessions. |
| `ClickHouse.Driver.Command` | `ClickHouseCommand` | Début et fin de l’exécution des requêtes, temps d’exécution, identifiants de requête, statistiques du serveur et détails des erreurs. |
| `ClickHouse.Driver.Transport` | `ClickHouseConnection` | Requêtes HTTP en streaming de bas niveau, indicateurs de compression, codes d’état des réponses et erreurs de transport. |
| `ClickHouse.Driver.Client` | `ClickHouseClient` | Insertions binaires, requêtes et autres opérations |
| `ClickHouse.Driver.NetTrace` | `TraceHelper` | Traçage réseau, uniquement lorsque le mode débogage est activé |

<h4 id="logging-config-example">
  Exemple : diagnostiquer les problèmes de connexion
</h4>

```json theme={null}
{
    "Logging": {
        "LogLevel": {
            "ClickHouse.Driver.Connection": "Trace",
            "ClickHouse.Driver.Transport": "Trace"
        }
    }
}
```

Les informations suivantes seront consignées dans le journal :

* Le choix de la fabrique de clients HTTP (pool par défaut ou connexion unique)
* La configuration du handler HTTP (SocketsHttpHandler ou HttpClientHandler)
* Les paramètres du pool de connexions (MaxConnectionsPerServer, PooledConnectionLifetime, etc.)
* Les paramètres de délai d’expiration (ConnectTimeout, Expect100ContinueTimeout, etc.)
* La configuration SSL/TLS
* Les événements d’ouverture et de fermeture des connexions
* Le suivi de l’ID de session

<h3 id="logging-debugmode">
  Mode de débogage : traçage réseau et diagnostics
</h3>

Pour faciliter le diagnostic des problèmes réseau, la bibliothèque du driver comprend un utilitaire permettant de tracer à bas niveau le fonctionnement interne du réseau .NET. Pour activer ce traçage, vous devez fournir une LoggerFactory avec le niveau défini sur Trace et définir EnableDebugMode sur true (ou l’activer manuellement à l’aide de la classe `ClickHouse.Driver.Diagnostic.TraceHelper`). Les événements seront consignés dans la catégorie `ClickHouse.Driver.NetTrace`. Avertissement : cette opération générera des logs extrêmement détaillés et nuira aux performances. Il est déconseillé d’activer le mode de débogage en production.

```csharp theme={null}
var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConsole()
        .SetMinimumLevel(LogLevel.Trace); // Must be Trace level to see network events
});

var settings = new ClickHouseClientSettings()
{
    LoggerFactory = loggerFactory,
    EnableDebugMode = true,  // Enable low-level network tracing
};
```

<h2 id="opentelemetry">
  OpenTelemetry
</h2>

Le driver prend en charge nativement le traçage distribué OpenTelemetry au moyen de l’API .NET [`System.Diagnostics.Activity`](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing). Une fois activé, il émet des spans pour les opérations de base de données. Ces spans peuvent être exportés vers des plateformes d’observability comme Jaeger ou ClickHouse lui-même (via l’[OpenTelemetry Collector](/fr/guides/use-cases/observability/build-your-own/integrating-opentelemetry)).

<h3 id="opentelemetry-enabling">
  Activer le traçage
</h3>

Dans les applications ASP.NET Core, ajoutez l’`ActivitySource` du driver ClickHouse à votre configuration OpenTelemetry :

```csharp theme={null}
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource(ClickHouseDiagnosticsOptions.ActivitySourceName)  // Subscribe to ClickHouse driver spans
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter());             // Or AddJaegerExporter(), etc.
```

Pour les applications en console, les tests ou la configuration manuelle :

```csharp theme={null}
using OpenTelemetry;
using OpenTelemetry.Trace;

var tracerProvider = Sdk.CreateTracerProviderBuilder()
    .AddSource(ClickHouseDiagnosticsOptions.ActivitySourceName)
    .AddConsoleExporter()
    .Build();
```

<h3 id="opentelemetry-attributes">
  Attributs des spans
</h3>

Chaque span comporte les attributs OpenTelemetry standard relatifs aux bases de données, ainsi que des statistiques sur les requêtes propres à ClickHouse, utiles pour le débogage.

| Attribut | Description |
| - | - |
| `db.system` | Toujours `"clickhouse"` |
| `db.name` | Nom de la base de données |
| `db.user` | Nom d’utilisateur |
| `db.statement` | Requête SQL (si activée) |
| `db.clickhouse.read_rows` | Lignes lues par la requête |
| `db.clickhouse.read_bytes` | Octets lus par la requête |
| `db.clickhouse.written_rows` | Lignes écrites par la requête |
| `db.clickhouse.written_bytes` | Octets écrits par la requête |
| `db.clickhouse.elapsed_ns` | Temps d’exécution côté serveur en nanosecondes |

<h3 id="opentelemetry-configuration">
  Options de configuration
</h3>

Configurez le comportement du traçage à l’aide de `ClickHouseDiagnosticsOptions` :

```csharp theme={null}
using ClickHouse.Driver.Diagnostic;

// Include SQL statements in spans (default: false for security)
ClickHouseDiagnosticsOptions.IncludeSqlInActivityTags = true;

// Truncate long SQL statements (default: 1000 characters)
ClickHouseDiagnosticsOptions.StatementMaxLength = 500;
```

<Warning>
  L’activation de `IncludeSqlInActivityTags` risque d’exposer des données sensibles dans vos traces. Utilisez cette option avec précaution en production.
</Warning>

<h2 id="tls-configuration">
  Configuration TLS
</h2>

Pour vous connecter à ClickHouse en HTTPS, vous pouvez configurer TLS/SSL de plusieurs manières.

<h3 id="custom-certificate-validation">
  Validation personnalisée des certificats
</h3>

Dans les environnements de production qui nécessitent une logique de validation des certificats personnalisée, fournissez votre propre `HttpClient` avec un handler `ServerCertificateCustomValidationCallback` configuré :

```csharp theme={null}
using System.Net;
using System.Net.Security;
using ClickHouse.Driver;

var handler = new HttpClientHandler
{
    // No AutomaticDecompression needed: the driver decodes compressed responses itself.
    ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) =>
    {
        // Example: Accept a specific certificate thumbprint
        if (cert?.Thumbprint == "YOUR_EXPECTED_THUMBPRINT")
            return true;

        // Example: Accept certificates from a specific issuer
        if (cert?.Issuer.Contains("YourOrganization") == true)
            return true;

        // Default: Use standard validation
        return sslPolicyErrors == SslPolicyErrors.None;
    },
};

var httpClient = new HttpClient(handler) { Timeout = TimeSpan.FromMinutes(5) };

var settings = new ClickHouseClientSettings
{
    Host = "my.clickhouse.server",
    Protocol = "https",
    HttpClient = httpClient,
};

using var client = new ClickHouseClient(settings);
```

<Note>
  Précautions à prendre si vous fournissez un HttpClient personnalisé

  * **Décompression automatique** : laissez `AutomaticDecompression` désactivé. Le driver décompresse lui-même les réponses : cette option est donc inutile. L’activer peut même poser problème côté requête : lors de l’envoi, le handler *ajoute également* tous les algorithmes de son masque à l’en-tête `Accept-Encoding` sortant, en plus de ceux annoncés par le driver. ClickHouse peut alors répondre avec un codec que vous n’avez pas demandé. Consultez [Décompression des réponses](#response-decompression).
  * **Délai d’inactivité** : définissez `PooledConnectionIdleTimeout` sur une valeur inférieure au `keep_alive_timeout` du serveur (10 secondes pour ClickHouse Cloud) afin d’éviter les erreurs de connexion dues à des connexions semi-ouvertes.
</Note>

<h2 id="performance-tuning">
  Optimisation des performances
</h2>

Cette section explique comment tirer le meilleur parti du client et quelles options ajuster pour optimiser ses performances en fonction de votre cas d’utilisation.

<h3 id="perf-at-a-glance">
  En bref
</h3>

\| Si vous | Faites ceci |
\|---|---|---|
\| Chargez des lignes dans des objets POCO | Utilisez [`QueryAsync<T>`](#perf-read-path) plutôt que `MapTo<T>` |
\| Effectuez des insertions volumineuses | Augmentez [`InsertOptions.BatchSize`](#perf-insert-batching) |
\| Exécutez une application console ou un worker qui effectue de nombreuses insertions | Activez le [Server GC](#perf-gc) |
\| Lisez des résultats volumineux via le réseau | Gardez la compression des réponses activée (c’est le cas par défaut) |
\| Effectuez des insertions via une connexion rapide | Essayez [`InsertOptions.Compressor = null`](#perf-compression) |
\| Effectuez de nombreuses insertions dans la même table | Utilisez [`UseSchemaCache` ou `ColumnTypes`](#skip-schema-query) |
\| Lisez des résultats très volumineux | Augmentez [`ReadBufferSize`](#perf-buffers) |

***

<h3 id="perf-read-path">
  Lecture : choisir le mode de matérialisation
</h3>

Il existe trois façons de lire une ligne dans un résultat, et leur coût varie. Certaines impliquent une mise en boîte des valeurs, ce qui augmente les allocations mémoire et réduit les performances.

| Méthode de lecture | Mise en boîte de chaque valeur | Remarques |
| - | - | - |
| `QueryAsync<T>` | **Non** | Lit directement le flux dans les propriétés de votre objet. C’est le chemin le plus rapide. |
| Accesseurs typés du lecteur (`GetInt32`, `GetInt64`, `GetDouble`, `GetGuid`, `GetDateTime`, `GetFieldValue<T>`) | **Non** | Lit une valeur typée sans mise en boîte. |
| `MapTo<T>` | Oui | Matérialise d’abord la ligne, puis en copie les valeurs. |
| `GetValue` et `GetValues` | Oui | Ces méthodes renvoient `object` : chaque valeur doit donc subir une mise en boîte lors de sa récupération. |

Pour la lecture de 1 000 000 de lignes comportant 105 colonnes du jeu de données *hits* :

| API | Mémoire allouée |
| - | -: |
| `QueryAsync<T>` | **1 372 Mo** |
| `MapTo<T>` | 3 133 Mo |

```csharp theme={null}
// Fast path: register the type once, then stream rows directly into it.
client.RegisterPocoType<HitRow>();

await foreach (var row in client.QueryAsync<HitRow>("SELECT * FROM hits"))
    Process(row);
```

<Note>
  *Les ORM empruntent le chemin d’exécution optimisé lorsqu’ils utilisent des accesseurs typés.* linq2db enregistre `GetInt64`,
  `GetDouble` et `GetDateTime` pour chaque colonne, ce qui permet de lire les valeurs sans mise en boîte. Le code qui passe par
  `GetValue` (y compris pour un résultat `dynamic` de Dapper) met chaque valeur en boîte. Si une requête ORM est exécutée fréquemment
  et passe par `GetValue`, utilisez `QueryAsync<T>` pour cette requête uniquement.
</Note>

***

<h3 id="perf-insert-batching">
  Insertion : taille des lots et parallélisme
</h3>

La taille des lots est le facteur qui influe le plus sur le débit des insertions. Par défaut, `InsertOptions.BatchSize` est fixé à
100 000 lignes.

**Utilisez de grands lots.** Pour une insertion de 1 000 000 de lignes, le passage de 10 000 à 100 000 lignes par
lot a donné les résultats suivants :

| Insertion | 10 000 lignes/lot | 100 000 lignes/lot | |
| - | -: | -: | -: |
| POCO | 15 308 ms | 7 853 ms | −49 % |
| `object[]` | 17 027 ms | 10 671 ms | −37 % |

Si vous ne pouvez pas maîtriser la taille des lots (par exemple, lorsque de nombreux petits producteurs envoient des lignes séparément), utilisez les [insertions asynchrones](#async-inserts) et laissez le serveur regrouper les lignes en lots.

**Téléversements parallèles.** Par défaut, `InsertOptions.MaxDegreeOfParallelism` est fixé à `1`. Augmentez cette valeur pour envoyer
plusieurs lots simultanément. Le gain est particulièrement important lorsque la compression est activée, car chaque lot est alors compressé
dans son propre thread. Les sessions sont incompatibles avec les insertions parallèles : désactivez-les ou conservez
`MaxDegreeOfParallelism = 1`.

**Supprimez la requête d'inspection du schéma.** Chaque appel à `InsertBinaryAsync` envoie d'abord une requête `SELECT ... WHERE 1=0`
pour déterminer les types des colonnes. Consultez [Éviter la requête d'inspection du schéma](#skip-schema-query) pour éviter cet
aller-retour grâce à `ColumnTypes` ou à `UseSchemaCache`.

<Note>
  Le chemin d'insertion sans mise en boîte s'applique au format `RowBinary` par défaut. `RowBinaryWithDefaults` doit
  examiner chaque valeur pour repérer le marqueur `DBDefault` ; il emprunte donc le chemin plus lent.
</Note>

***

<h3 id="perf-compression">
  Compression : les résultats diffèrent selon le sens du transfert
</h3>

La compression réduit le volume de données transférées au prix d'une consommation accrue de CPU. Son intérêt dépend du sens du
transfert, du débit de votre connexion au serveur ClickHouse, de la façon dont vos données se prêtent à l'algorithme de compression choisi et du coût éventuel de chaque octet transféré.

**Lectures :** laissez la compression activée, sauf si le serveur s'exécute sur la même machine. Elle est activée par défaut. Par rapport à un transfert sans compression, `zstd` au niveau 1
a donné les résultats suivants :

| Client vers serveur | Effet de la compression |
| - | - |
| Même hôte (bouclage local) | Surcoût de 8 % |
| Même région cloud | **Gain de 16 %** |
| Région voisine | **Gain de 33 %** |

**Insertions :** mesurez les performances avant d'activer la compression. Les gains peuvent être insuffisants pour la justifier. N'oubliez pas non plus que la décompression accroît la charge du serveur : cette charge est modérée avec Zstd et LZ4, mais peut être élevée avec d'autres algorithmes, comme Brotli.

Pour désactiver la compression des insertions :

```csharp theme={null}
var options = new InsertOptions { Compressor = null };
await client.InsertBinaryAsync("my_table", columns, rows, options);
```

Pour choisir un codec, régler les niveaux de compression et trouver votre propre seuil de bascule, consultez
[Optimiser la compression](#tuning-compression).

***

<h3 id="perf-buffers">
  Tampons
</h3>

`ReadBufferSize` définit la taille du tampon utilisé pour lire les réponses HTTP. Par défaut, cette taille est de 64 Kio.

Le driver emprunte ce tampon à un pool partagé et le restitue lorsqu'il libère le lecteur : il n'effectue donc
pas d'allocation à chaque requête. Augmentez cette taille pour limiter les remplissages du tampon lorsque les
résultats sont volumineux. Le driver conserve un tampon par lecteur ouvert simultanément : la consommation de mémoire
augmente donc avec la taille du tampon et le nombre de lecteurs ouverts en parallèle.

```csharp theme={null}
var settings = new ClickHouseClientSettings("Host=localhost") { ReadBufferSize = 256 * 1024 };
```

<Warning>
  *Libérez toujours les lecteurs.* Lorsqu’un lecteur est libéré, il restitue son tampon au pool et ferme sa connexion HTTP.
  Si vous abandonnez un lecteur sans le libérer, son tampon n’est pas restitué au pool et sa connexion HTTP peut rester
  indisponible ; la récupération automatique de la mémoire ne remplace pas une libération explicite.
</Warning>

***

<h3 id="perf-gc">
  Environnement d’exécution et ramasse-miettes
</h3>

**Activez Server GC pour les applications qui effectuent de nombreuses insertions.** À code identique et à nombre
d’octets alloués égal, les insertions étaient jusqu’à 97 % plus lentes avec Workstation GC qu’avec Server GC.

```xml theme={null}
<PropertyGroup>
  <ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
```

Les projets ASP.NET Core activent déjà ce paramètre. Ce n’est pas le cas des applications console, des services worker ni de la plupart des images
de conteneur.

La différence tient à la taille du budget de génération 0. Le Workstation GC utilise un budget réduit : les
tampons temporaires créés lors d’une insertion ne sont donc pas libérés en génération 0. Ils passent en génération 1,
ce qui multiplie les promotions et accroît considérablement le travail en génération 2. Dans un cas d’insertion,
on comptait 4 000 collectes de génération 2 pour 1 000 opérations avec le Server GC, contre 73 000 avec
le Workstation GC.

<Note>
  Le Server GC est un paramètre qui privilégie le débit, pas la latence. Lors de ces mêmes mesures, le temps total
  passé en pause était plus de deux fois moindre avec le Server GC, mais chaque pause était plus longue
  (114,6 ms au 95e percentile, contre 61,9 ms). Si votre service est sensible à la latence des requêtes les plus lentes, mesurez
  les deux modes avant de faire votre choix.
</Note>

***

<h3 id="perf-latency">
  Latence : réutiliser les connexions
</h3>

L’établissement d’une nouvelle connexion TCP et la négociation TLS prennent beaucoup de temps.
Réutiliser les connexions réduit considérablement la latence de vos requêtes.

* Ne créez pas de client pour chaque requête. Chaque nouveau client disposant de son propre `HttpClient` crée un nouveau
  pool de connexions et doit répéter la négociation TLS. Utilisez un seul `ClickHouseClient` pendant toute la durée de vie de l’application. Il peut être utilisé simultanément par plusieurs threads et est conçu pour
  être utilisé comme singleton.
* Pour ADO.NET et les ORM, utilisez `ClickHouseDataSource` afin que toutes les connexions partagent le même pool.

Pour découvrir toutes les approches possibles, consultez
[Durée de vie des connexions et pool de connexions](#best-practices-connection-lifetime).

***

<h3 id="perf-measuring">
  Mesurez les performances par vous-même
</h3>

Dans bien des cas, les performances dépendent de la structure de vos données, du débit de votre connexion au serveur,
de la façon dont vous répartissez la charge CPU entre le client et le serveur, des limites de votre matériel, etc.
Il est donc recommandé de mesurer les performances avec vos propres données, dans votre environnement.

Pour connaître la part du travail effectuée par le serveur, définissez `QueryOptions.QueryId` et récupérez les compteurs :

```sql theme={null}
SELECT ProfileEvents['UserTimeMicroseconds'] + ProfileEvents['SystemTimeMicroseconds'] AS cpu_us,
       ProfileEvents['NetworkSendBytes'] AS sent_bytes,
       query_duration_ms
FROM system.query_log
WHERE query_id = 'your-query-id' AND type = 'QueryFinish';
```

***

<h2 id="orm-support">
  Prise en charge des ORM
</h2>

Les ORM nécessitent l’API ADO.NET (`ClickHouseConnection`). Pour gérer correctement le cycle de vie des connexions, créez-les à partir d’un `ClickHouseDataSource` :

```csharp theme={null}
// Register DataSource as singleton
var dataSource = new ClickHouseDataSource("Host=localhost;Username=default");

// Create connections for ORM use
await using var connection = await dataSource.OpenConnectionAsync();
// Pass connection to your ORM...
```

<h3 id="orm-support-dapper">
  Dapper
</h3>

`ClickHouse.Driver` est compatible avec Dapper. Le driver convertit automatiquement la syntaxe `@parameter` de Dapper en syntaxe native `{parameter:Type}` de ClickHouse et déduit les types des valeurs .NET.

Utilisez `ClickHouseDataSource` pour gérer correctement le cycle de vie des connexions :

```csharp theme={null}
var dataSource = new ClickHouseDataSource("Host=localhost");
services.AddSingleton(dataSource); // Register as singleton in DI

using var connection = dataSource.CreateConnection();
```

<h4 id="dapper-parameter-passing">
  Modes de passage des paramètres
</h4>

Dapper prend en charge tous les modes standard de passage des paramètres :

**Objets anonymes :**

```csharp theme={null}
await connection.ExecuteAsync(
    "INSERT INTO users (id, name, balance) VALUES (@Id, @Name, @Balance)",
    new { Id = 1, Name = "alice", Balance = 3.14 });
```

**Classes POCO :**

```csharp theme={null}
class InsertParams
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Balance { get; set; }
}

var param = new InsertParams { Id = 42, Name = "bob", Balance = 99.9 };
await connection.ExecuteAsync(
    "INSERT INTO users (id, name, balance) VALUES (@Id, @Name, @Balance)", param);
```

**Dictionnaire :**

```csharp theme={null}
var parameters = new Dictionary<string, object> { { "Id", 2 } };
var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE id = @Id", parameters);
```

**`DynamicParameters` (à partir d’un dictionnaire ou d’un objet anonyme) :**

```csharp theme={null}
var dynParams = new DynamicParameters(new { Id = 1 });
// or: new DynamicParameters(new Dictionary<string, object> { { "Id", 1 } });

var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE id = @Id", dynParams);
```

<h4 id="dapper-pocos">
  Requêtes avec des POCO
</h4>

Dapper associe les colonnes aux propriétés par leur nom (sans tenir compte de la casse) :

```csharp theme={null}
class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public double Balance { get; set; }
}

// From a table
var users = (await connection.QueryAsync<User>("SELECT id, name, balance FROM users")).ToList();

// From a literal
var row = (await connection.QueryAsync<User>("SELECT 1 as id, 'hello' as name, 2.5 as balance")).Single();
```

<h4 id="dapper-clickhouse-param-syntax">
  Syntaxe des paramètres native de ClickHouse
</h4>

Si vous devez définir explicitement le type d’un paramètre, utilisez directement la syntaxe `{param:Type}` de ClickHouse dans le code SQL, avec un `Dictionary<string, object>` contenant les valeurs des paramètres. N’utilisez pas à la fois les syntaxes `@param` et `{param:Type}` pour un même paramètre.

```csharp theme={null}
var parameters = new Dictionary<string, object> { { "value", 42 } };
var result = await connection.QueryAsync<int>("SELECT {value:Int32}", parameters);
```

<h4 id="dapper-where-in">
  WHERE IN
</h4>

**L'expansion native de la clause IN par Dapper fonctionne :**

```csharp theme={null}
var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE id IN @Ids ORDER BY id",
    new { Ids = new[] { 1, 3, 5 } });
```

Dapper transforme cette expression en `WHERE id IN (@Ids1, @Ids2, @Ids3)`, puis le driver convertit chaque paramètre issu de cette expansion.

**La fonction `has()` de ClickHouse fonctionne aussi avec un paramètre Array :**

```csharp theme={null}
var parameters = new Dictionary<string, object> { { "ids", new[] { 1, 3, 5 } } };
var rows = await connection.QueryAsync<User>(
    "SELECT id, name FROM users WHERE has({ids:Array(Int32)}, id) ORDER BY id",
    parameters);
```

<h4 id="dapper-type-handlers">
  Handlers de type personnalisés
</h4>

Certains types ClickHouse, comme `ITuple`, `BigInteger` et `ClickHouseDecimal`, nécessitent l’enregistrement de handlers au démarrage :

```csharp theme={null}
// ClickHouseDecimal (for Decimal64/128/256 columns)
SqlMapper.AddTypeHandler(new ClickHouseDecimalHandler());

// BigInteger (for Int128/Int256/UInt128/UInt256 columns)
SqlMapper.AddTypeHandler(new BigIntegerHandler());

// IPAddress (for IPv4/IPv6 columns)
SqlMapper.AddTypeHandler(new IpAddressHandler());
```

Consultez l’[exemple Dapper](https://github.com/ClickHouse/clickhouse-cs/blob/main/examples/Http/ORM/ORM_001_Dapper.cs) pour découvrir une implémentation de type handler.

<h4 id="dapper-contrib">
  Dapper.Contrib
</h4>

`GetAll<T>()` et `Get<T>(id)` fonctionnent. En revanche, `Insert<T>()` ne fonctionne pas : cette méthode génère une syntaxe propre à SQL Server (`SCOPE_IDENTITY`, `[]`). Utilisez plutôt la méthode native `InsertBinaryAsync` de `ClickHouseClient`.

```csharp theme={null}
[Table("test.users")]
record class UserRecord(int Id, string Name, DateTime Timestamp);

var all = await connection.GetAllAsync<UserRecord>();
var one = await connection.GetAsync<UserRecord>(1);
```

Les noms des propriétés doivent correspondre exactement aux noms des colonnes ClickHouse (la casse doit être respectée).

<h4 id="dapper-limitations">
  Limitations
</h4>

| Élément | Statut | Détails |
| - | - | - |
| Tuple comme **résultat** | Fonctionne | Nécessite l’enregistrement de `SqlMapper.TypeHandler<ITuple>` |
| Tuple comme **paramètre** | Non pris en charge | Dapper ne peut pas sérialiser `ITuple`/`Tuple<>` comme valeur de `DbParameter` |
| Types imbriqués comme paramètres | Non pris en charge | Pour la même raison : Dapper refuse les types complexes comme valeurs de paramètres |
| Types géospatiaux comme paramètres | Non pris en charge | Point, Ring, Polygon, LineString, MultiLineString, MultiPolygon |
| `Dapper.Contrib.Insert<T>()` | Non pris en charge | Génère une syntaxe propre à SQL Server |
| Type `Nothing` | Non pris en charge | Aucune représentation .NET pertinente |

<h3 id="orm-support-linq2db">
  Linq2db
</h3>

Ce driver est compatible avec [linq2db](https://github.com/linq2db/linq2db), un ORM léger et un fournisseur LINQ pour .NET. Pour plus de détails, consultez la documentation sur le site du projet.

**Exemple d'utilisation :**

Créez une `DataConnection` à l'aide du fournisseur ClickHouse :

```csharp theme={null}
using LinqToDB;
using LinqToDB.Data;
using LinqToDB.DataProvider.ClickHouse;

var connectionString = "Host=localhost;Port=8123;Database=default";
var options = new DataOptions()
    .UseClickHouse(connectionString, ClickHouseProvider.ClickHouseDriver);

await using var db = new DataConnection(options);
```

Les correspondances entre classes et tables peuvent être définies à l’aide d’attributs ou d’une configuration fluide. Si les noms de vos classes et de leurs propriétés correspondent exactement aux noms des tables et des colonnes, aucune configuration n’est nécessaire :

```csharp theme={null}
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}
```

**Requêtes :**

```csharp theme={null}
await using var db = new DataConnection(options);

var products = await db.GetTable<Product>()
    .Where(p => p.Price > 100)
    .OrderByDescending(p => p.Name)
    .ToListAsync();
```

**Copie en masse :**

Utilisez `BulkCopyAsync` pour effectuer des insertions en masse efficacement.

```csharp theme={null}
await using var db = new DataConnection(options);
var table = db.GetTable<Product>();

var options = new BulkCopyOptions
{
    MaxBatchSize = 100000,
    MaxDegreeOfParallelism = 1,
    WithoutSession = true
};

await table.BulkCopyAsync(options, products);
```

<h3 id="orm-support-ef-core">
  Entity Framework Core
</h3>

Le fournisseur officiel Entity Framework Core pour ClickHouse. Associez des classes C# à des tables ClickHouse, interrogez-les avec LINQ et insérez des données via `SaveChanges` — le tout selon les pratiques habituelles d’EF Core.

* **NuGet** : [`ClickHouse.EntityFrameworkCore`](https://www.nuget.org/packages/ClickHouse.EntityFrameworkCore)
* **Code source** : [GitHub](https://github.com/ClickHouse/ClickHouse.EntityFrameworkCore)

<Note>
  Ce fournisseur est activement développé. La version actuelle prend en charge les requêtes LINQ (y compris les jointures, les sous-requêtes et les opérations ensemblistes), les insertions avec `INSERT` via `SaveChanges` / `BulkInsertAsync`, les migrations avec une prise en charge complète du DDL (CREATE / ALTER / DROP) et la configuration des table engines spécifiques à ClickHouse. `UPDATE` / `DELETE` ne sont pas pris en charge.
</Note>

<h4 id="ef-core-installation">
  Installation
</h4>

```bash theme={null}
dotnet add package ClickHouse.EntityFrameworkCore
```

Nécessite .NET 10.0 et EF Core 10.

<h4 id="ef-core-quick-start">
  Démarrage rapide
</h4>

Définissez votre entité et votre `DbContext`, puis effectuez une requête avec LINQ :

```csharp theme={null}
using Microsoft.EntityFrameworkCore;

public class PageView
{
    public long Id { get; set; }
    public string Path { get; set; }
    public DateOnly Date { get; set; }
    public string UserAgent { get; set; }
}

public class AnalyticsContext : DbContext
{
    public DbSet<PageView> PageViews { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseClickHouse("Host=localhost;Database=analytics");
}

// Query
await using var ctx = new AnalyticsContext();

var topPages = await ctx.PageViews
    .Where(v => v.Date >= new DateOnly(2024, 1, 1))
    .GroupBy(v => v.Path)
    .Select(g => new { Path = g.Key, Views = g.Count() })
    .OrderByDescending(x => x.Views)
    .Take(10)
    .ToListAsync();
```

<h4 id="ef-core-types">
  Types pris en charge
</h4>

| Catégorie | Types ClickHouse | Types CLR |
| - | - | - |
| **Entiers** | `Int8`–`Int64`, `UInt8`–`UInt64` | `sbyte`, `short`, `int`, `long`, `byte`, `ushort`, `uint`, `ulong` |
| **Entiers de grande taille** | `Int128`, `Int256`, `UInt128`, `UInt256` | `BigInteger` |
| **Nombres à virgule flottante** | `Float32`, `Float64`, `BFloat16` | `float`, `double` |
| **Nombres décimaux** | `Decimal(P,S)`, `Decimal32(S)`, `Decimal64(S)`, `Decimal128(S)` | `decimal` ou `ClickHouseDecimal` |
| **Booléens** | `Bool` | `bool` |
| **Chaînes de caractères** | `String`, `FixedString(N)` | `string` |
| **Énumérations** | `Enum8(...)`, `Enum16(...)` | `string` ou `enum` C# |
| **Dates et heures** | `Date`, `Date32`, `DateTime`, `DateTime64(P, 'TZ')` | `DateOnly`, `DateTime` |
| **Heures** | `Time`, `Time64(N)` | `TimeSpan` |
| **UUID** | `UUID` | `Guid` |
| **Adresses réseau** | `IPv4`, `IPv6` | `IPAddress` |
| **Tableaux** | `Array(T)` | `T[]`, `List<T>`, `IList<T>`, `ICollection<T>`, `IReadOnlyList<T>`, `IReadOnlyCollection<T>`, `IEnumerable<T>` |
| **Maps** | `Map(K, V)` | `Dictionary<K,V>` |
| **Tuples** | `Tuple(T1, ...)` | `Tuple<...>` ou `ValueTuple<...>` |
| **Variantes** | `Variant(T1, T2, ...)` | `object` |
| **Types dynamiques** | `Dynamic` | `object` |
| **JSON** | `Json` | `JsonNode` ou `string` |
| **Types géographiques** | `Point`, `Ring`, `LineString`, `Polygon`, `MultiLineString`, `MultiPolygon`, `Geometry` | `Tuple<double,double>` et tableaux de ces tuples ; `object` pour Geometry |
| **Types wrapper** | `Nullable(T)`, `LowCardinality(T)` | Type sous-jacent utilisé automatiquement |

Utilisez `ClickHouseDecimal` (de `ClickHouse.Driver.Numerics`) plutôt que `decimal` si vous avez besoin de la pleine précision des colonnes `Decimal128`/`Decimal256` : le type `decimal` de .NET est limité à 28 ou 29 chiffres significatifs.

<h4 id="ef-core-linq">
  Opérations LINQ prises en charge
</h4>

**Requêtes :** `Where`, `OrderBy`, `Take`, `Skip`, `Select`, `First`, `Single`, `Any`, `All`, `Count`, `Distinct`, `AsNoTracking`

**GROUP BY et agrégations :** `GroupBy` avec `Count`, `LongCount`, `Sum`, `Average`, `Min`, `Max` — y compris `HAVING` (`.Where()` après `.GroupBy()`), plusieurs agrégations dans une même projection et `OrderBy` sur les résultats agrégés.

**Jointures :** `Join` (INNER), combinaisons `GroupJoin`/`SelectMany` (LEFT et CROSS). Une jointure LEFT JOIN renvoie `null` pour les lignes sans correspondance (voir la [sémantique des valeurs nulles de LEFT JOIN](#ef-core-join-nulls) ci-dessous).

**Sous-requêtes :** `Contains` / `IN` corrélés, `Any` / `EXISTS`, `All` et sous-requêtes scalaires dans les projections.

**Opérations sur les ensembles :** `Concat` (→ `UNION ALL`), `Union` (→ `UNION DISTINCT`), `Intersect`, `Except`.

**Collections locales intégrées :** les jointures et les appels à `Contains` portant sur des collections en mémoire (`int[]`, `List<T>`, etc.) sont traduits en une série d’opérations UNION.

**Méthodes sur les chaînes de caractères :** `Contains`, `StartsWith`, `EndsWith`, `IndexOf`, `Replace`, `Substring`, `Trim`/`TrimStart`/`TrimEnd`, `ToLower`, `ToUpper`, `Length`, `IsNullOrEmpty`, `Concat` (et l’opérateur `+`).

**Fonctions mathématiques :** les méthodes standard de `Math` et `MathF` sont traduites en fonctions ClickHouse équivalentes — fonctions arithmétiques, logarithmiques, trigonométriques et utilitaires.

<h5 id="ef-core-join-nulls">
  Sémantique des valeurs NULL dans les LEFT JOIN
</h5>

Le fournisseur ajoute automatiquement `set_join_use_nulls=1` à chaque chemin de connexion afin que le comportement des jointures corresponde aux attentes d’Entity Framework.

Si votre serveur ClickHouse ou votre profil interdit la modification de ce paramètre (par exemple, avec un profil `readonly=1`), désactivez cet ajout avec :

```csharp theme={null}
optionsBuilder.UseClickHouse(connectionString, o => o.DisableJoinNullSemantics());
```

Lorsque l’option de désactivation est activée, LEFT JOIN renvoie les valeurs par défaut des colonnes ClickHouse, et la détection des propriétés de navigation par EF, qui repose sur les valeurs nulles, ne fonctionne plus comme prévu. Effectuez des comparaisons explicites avec `0` / `""` plutôt qu’avec `== null`.

<h4 id="ef-core-insert">
  Insertion de données
</h4>

`SaveChanges` utilise l’API native `InsertBinaryAsync` du driver : un encodage RowBinary avec un corps de requête compressé, bien plus efficace que des requêtes SQL paramétrées :

```csharp theme={null}
await using var ctx = new AnalyticsContext();

ctx.PageViews.Add(new PageView
{
    Id = 1,
    Path = "/home",
    Date = new DateOnly(2024, 6, 15),
    UserAgent = "Mozilla/5.0"
});

await ctx.SaveChangesAsync();
```

Les entités passent de l’état `Added` à l’état `Unchanged` après l’enregistrement, comme avec n’importe quel autre fournisseur EF Core.

**La taille des lots** est configurable (1 000 par défaut) :

```csharp theme={null}
optionsBuilder.UseClickHouse("Host=localhost", o => o.MaxBatchSize(5000));
```

<h4 id="ef-core-bulk-insert">
  Insertion en masse
</h4>

Pour les insertions à haut débit, utilisez `BulkInsertAsync` plutôt que `SaveChanges`. Cette méthode d’extension de `DbContext` évite complètement le suivi des modifications, la résolution des identités et la gestion des états d’EF Core : elle appelle directement la méthode `InsertBinaryAsync` du driver, avec un encodage RowBinary et un corps de requête compressé.

Elle convient donc au chargement de grands jeux de données lorsque vous n’avez pas besoin de suivre les entités après leur insertion :

```csharp theme={null}
var events = Enumerable.Range(0, 100_000)
    .Select(i => new PageView
    {
        Id = i,
        Path = $"/page/{i}",
        Date = DateOnly.FromDateTime(DateTime.Today)
    });

long rowsInserted = await ctx.BulkInsertAsync(events);
```

L’entrée peut être n’importe quel `IEnumerable<T>` : les entités sont parcourues au fil de l’eau, sans être toutes chargées en mémoire. La valeur renvoyée correspond au nombre de lignes insérées. Après l’insertion, les entités ne sont **pas** rattachées au `DbContext` : leur état ne passe donc pas de `Added` à `Unchanged`.

<h4 id="ef-core-enums">
  Énumérations
</h4>

Les colonnes ClickHouse `Enum8`/`Enum16` peuvent être associées à des propriétés de type `string` ou à des types `enum` C#. Avec les énumérations C#, le fournisseur assure automatiquement la conversion entre l’énumération et sa représentation sous forme de chaîne :

```csharp theme={null}
public enum Status { Active, Inactive, Pending }

public class User
{
    public long Id { get; set; }
    public Status Status { get; set; }
}

// Query with enum values
var active = await ctx.Users
    .Where(u => u.Status == Status.Active)
    .ToListAsync();
```

<h4 id="ef-core-value-converters">
  Conversions de types personnalisés
</h4>

Le système `ValueConverter` d’EF Core permet de convertir vos types personnalisés en types déjà pris en charge par le fournisseur. Celui-ci n’a jamais accès à votre type personnalisé : EF Core effectue la conversion avant de lui transmettre la valeur.

**Conversion par propriété :**

```csharp theme={null}
public class Money
{
    public decimal Amount { get; set; }
    public string Currency { get; set; }
}

public class Order
{
    public long Id { get; set; }
    public Money Price { get; set; }
}

// In OnModelCreating:
modelBuilder.Entity<Order>()
    .Property(o => o.Price)
    .HasConversion(
        m => $"{m.Amount}|{m.Currency}",
        s => new Money
        {
            Amount = decimal.Parse(s.Split('|')[0]),
            Currency = s.Split('|')[1]
        })
    .HasColumnType("String");
```

**Classe de convertisseur réutilisable :**

```csharp theme={null}
public class MoneyConverter : ValueConverter<Money, string>
{
    public MoneyConverter() : base(
        m => $"{m.Amount}|{m.Currency}",
        s => Parse(s)) { }

    private static Money Parse(string s)
    {
        var parts = s.Split('|');
        return new Money { Amount = decimal.Parse(parts[0]), Currency = parts[1] };
    }
}

// Apply to a single property:
.HasConversion<MoneyConverter>()

// Or apply to all properties of a type via conventions:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<Money>()
        .HaveConversion<MoneyConverter>();
}
```

<h4 id="ef-core-column-types">
  Annotations de type de colonne
</h4>

Pour les types scalaires comme `string`, `int`, `DateTime`, etc., le fournisseur détermine automatiquement le type ClickHouse. Pour les types paramétrés et les wrappers, vous devez indiquer explicitement le type ClickHouse.

**À l’aide d’annotations de données (attributs) :**

```csharp theme={null}
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

[Table("sensor_readings")]
public class SensorReading
{
    public long Id { get; set; }

    [Column(TypeName = "Array(String)")]
    public string[] Tags { get; set; }

    [Column(TypeName = "Map(String, String)")]
    public Dictionary<string, string> Metadata { get; set; }

    [Column(TypeName = "Nullable(Float64)")]
    public double? Value { get; set; }

    [Column(TypeName = "Decimal128(18)")]
    public decimal HighPrecision { get; set; }
}
```

**Utilisation de l’API fluide dans `OnModelCreating` :**

```csharp theme={null}
modelBuilder.Entity<SensorReading>(e =>
{
    e.ToTable("sensor_readings");
    e.Property(x => x.Tags).HasColumnType("Array(String)");
    e.Property(x => x.Metadata).HasColumnType("Map(String, String)");
    e.Property(x => x.Value).HasColumnType("Nullable(Float64)");
    e.Property(x => x.Category).HasColumnType("LowCardinality(String)");
    e.Property(x => x.HighPrecision).HasColumnType("Decimal128(18)");
});
```

Les wrappers imbriqués tels que `Array(Nullable(Int32))` et `LowCardinality(Nullable(String))` sont pris en charge — le fournisseur retire automatiquement les couches `Nullable` et `LowCardinality` à chaque niveau d’imbrication.

<h4 id="ef-core-variant-dynamic">
  Colonnes Variant et Dynamic
</h4>

Les colonnes ClickHouse `Variant(T1, T2, ...)` et `Dynamic` sont représentées par le type `object` en .NET. Comme `object` est trop générique pour que le type soit déduit automatiquement, vous devez déclarer explicitement le type de stockage à l’aide de `.HasColumnType()` :

```csharp theme={null}
public class Event
{
    public long Id { get; set; }
    public object? Payload { get; set; }
}

// In OnModelCreating:
entity.Property(e => e.Payload).HasColumnType("Variant(String, UInt64, Array(UInt64))");
// or:
entity.Property(e => e.Payload).HasColumnType("Dynamic");
```

À la lecture, la valeur est automatiquement désérialisée en un objet du type .NET correspondant au discriminateur stocké (par exemple, `string`, `ulong`, `ulong[]`).

<h4 id="ef-core-json">
  Colonnes JSON
</h4>

Le fournisseur prend en charge le type de colonne `Json` de ClickHouse, avec une correspondance vers `System.Text.Json.Nodes.JsonNode` (type principal) ou `string` (au moyen d’un `ValueConverter` automatique) :

```csharp theme={null}
using System.Text.Json.Nodes;

public class Event
{
    public long Id { get; set; }
    public JsonNode? Data { get; set; }
}

// In OnModelCreating:
entity.Property(e => e.Data).HasColumnType("Json");
```

La lecture et l’écriture de données JSON sont prises en charge aussi bien par `SaveChanges` que par `BulkInsertAsync` :

```csharp theme={null}
ctx.Events.Add(new Event
{
    Id = 1,
    Data = JsonNode.Parse("""{"action": "click", "x": 100, "y": 200}""")
});
await ctx.SaveChangesAsync();

var ev = await ctx.Events.Where(e => e.Id == 1).SingleAsync();
string action = ev.Data!["action"]!.GetValue<string>(); // "click"
```

Si vous préférez les chaînes JSON brutes, faites correspondre la propriété au type `string` avec un type de colonne `Json` : le fournisseur applique automatiquement un `ValueConverter` :

```csharp theme={null}
public class Event
{
    public long Id { get; set; }
    public string? Data { get; set; }  // raw JSON string
}

entity.Property(e => e.Data).HasColumnType("Json");
```

<Note>
  * **Pas de traduction des chemins JSON** — L’expression `entity.Data["name"]` dans LINQ n’est pas convertie en syntaxe SQL `data.name` de ClickHouse. Appliquez les filtres aux colonnes non JSON et examinez les données JSON en mémoire.
  * **Sémantique des valeurs NULL** — Le type JSON de ClickHouse renvoie `{}` (un objet vide) pour les valeurs NULL, et non un NULL SQL.
  * **Précision des entiers** — Le type JSON de ClickHouse stocke tous les entiers sous forme de `Int64`. Lors de la lecture avec `JsonNode`, utilisez `GetValue<long>()` plutôt que `GetValue<int>()`.
</Note>

<h4 id="ef-core-engines">
  Moteurs de table
</h4>

Configurez les moteurs des tables ClickHouse et les clauses propres à chaque moteur au moyen de l’API fluide `ToTable(name, t => ...)`. Si aucun moteur n’est configuré, le fournisseur utilise `MergeTree` par défaut, avec une clause `ORDER BY` définie à partir de la clé primaire de l’entité.

```csharp theme={null}
modelBuilder.Entity<Event>(e =>
{
    e.ToTable("events", t => t
        .HasMergeTreeEngine()
        .WithOrderBy("UserId", "Timestamp")
        .WithPartitionBy("toYYYYMM(Timestamp)")
        .WithPrimaryKey("UserId")
        .WithSettings("index_granularity = 8192"));
});
```

Familles de moteurs prises en charge :

| Moteur | Méthode fluide | Remarques |
| - | - | - |
| `MergeTree` | `HasMergeTreeEngine()` | Utilisé par défaut si aucun moteur n’est configuré |
| `ReplacingMergeTree` | `HasReplacingMergeTreeEngine("Version", "IsDeleted")` ou `HasReplacingMergeTreeEngine<T>(e => e.Version)` | Colonnes `Version` et `IsDeleted` facultatives |
| `SummingMergeTree` | `HasSummingMergeTreeEngine(…)` ou `HasSummingMergeTreeEngine<T>(e => new { … })` | Colonnes à sommer facultatives |
| `AggregatingMergeTree` | `HasAggregatingMergeTreeEngine()` | — |
| `CollapsingMergeTree` | `HasCollapsingMergeTreeEngine("Sign")` ou `HasCollapsingMergeTreeEngine<T>(e => e.Sign)` | La colonne `Sign` doit être de type `Int8` |
| `VersionedCollapsingMergeTree` | `HasVersionedCollapsingMergeTreeEngine("Sign", "Version")` ou `<T>(e => e.Sign, e => e.Version)` | — |
| `GraphiteMergeTree` | `HasGraphiteMergeTreeEngine("config_section")` | — |
| `Log`, `TinyLog`, `StripeLog`, `Memory` | `HasLogEngine()`, `HasTinyLogEngine()`, `HasStripeLogEngine()`, `HasMemoryEngine()` | Sans ORDER BY ni PARTITION BY |

**Clauses du moteur :** `WithOrderBy`, `WithPartitionBy`, `WithPrimaryKey`, `WithSampleBy`, `WithTtl`, `WithSettings`. Elles s’ajoutent toutes au constructeur de moteur renvoyé par `HasXxxEngine()`.

**Fonctionnalités au niveau des colonnes :** `HasCodec`, `HasTtl`, `HasComment`, `HasDefault` — elles sont toutes prises en compte dans les migrations.

**Index de saut de données** — avec `HasIndex(...).HasSkippingIndexType(...)` :

```csharp theme={null}
modelBuilder.Entity<Event>()
    .HasIndex(e => e.UserId)
    .HasSkippingIndexType("minmax")
    .HasGranularity(4);

// Index with parameters (e.g. bloom_filter, tokenbf_v1):
modelBuilder.Entity<Event>()
    .HasIndex(e => e.Tag)
    .HasSkippingIndexType("bloom_filter")
    .HasSkippingIndexParams("0.01")
    .HasGranularity(1);
```

Les index standard (qui ne permettent pas de sauter des blocs de données) sont ignorés sans avertissement, car ClickHouse ne dispose d’aucun équivalent. Les index uniques lèvent une exception, car ClickHouse n’impose pas l’unicité.

<h4 id="ef-core-migrations">
  Migrations
</h4>

Flux de travail standard des migrations avec EF Core :

```bash theme={null}
dotnet ef migrations add InitialCreate
dotnet ef database update
```

Opérations prises en charge :

| Opération | Éléments générés |
| - | - |
| `CREATE TABLE` | Inclut la clause du moteur, ORDER BY, PARTITION BY, SETTINGS, ainsi que les codecs, TTL, commentaires et valeurs par défaut des colonnes |
| `ALTER TABLE ADD COLUMN` | — |
| `ALTER TABLE DROP COLUMN` | — |
| `ALTER TABLE MODIFY COLUMN` | Gère la modification du type ainsi que l’ajout ou la suppression d’annotations (CODEC, TTL, COMMENT, DEFAULT) |
| `ALTER TABLE RENAME COLUMN` | — |
| `RENAME TABLE` | — |
| `ALTER TABLE ADD INDEX` / `DROP INDEX` | Index de saut de données uniquement |
| `CREATE DATABASE` / `DROP DATABASE` | Via `EnsureCreated` / `EnsureDeleted` et les migrations |

<h4 id="ef-core-limitations">
  Limites des migrations
</h4>

| Fonctionnalité | Raison |
| - | - |
| Clés étrangères | ClickHouse ne fait pas respecter les contraintes de clé étrangère. Les migrations rejettent `AddForeignKey` ; le validateur de modèle émet un avertissement lors de la création du modèle. |
| Contraintes d'unicité / index uniques | ClickHouse ne garantit pas l'unicité. Les index uniques déclenchent une exception lors de la migration. |
| Valeurs générées par le serveur (auto-incrémentation / `IDENTITY`) | ClickHouse ne dispose d'aucun mécanisme équivalent. |
| Colonnes `Nested(…)` | Pas encore prises en charge en tant que type CLR mappé. |
| Entités possédées au format JSON (`.ToJson()`) | Le mappage structurel en JSON des entités possédées n'est pas encore implémenté. Utilisez plutôt `JsonNode` / `string` sur une colonne `Json` (voir [Colonnes JSON](#ef-core-json)). |

Outre ces limites de migration, le fournisseur ne prend pas encore en charge :

* **`UPDATE` / `DELETE`**
* **Transactions** : `BeginTransaction` est une opération sans effet. Les transactions ACID ne sont pas prises en charge dans ClickHouse.
* **Traduction des requêtes sur les chemins JSON** : l'expression LINQ `entity.Data["key"]` n'est pas traduite en syntaxe SQL `data.key` de ClickHouse. Filtrez sur des colonnes non JSON et examinez les données JSON en mémoire.

<h2 id="limitations">
  Limitations
</h2>

<h3 id="valuetuple-caveat">
  Tuples de 8 éléments ou plus avec un tuple imbriqué en dernière position
</h3>

En C#, les types `ValueTuple` de plus de 7 éléments utilisent une imbrication générée par le compilateur : le 8e argument générique (`TRest`) est lui-même un `ValueTuple` qui contient les éléments restants. Par exemple, `(int, int, int, int, int, int, int, string, string)` est compilé en `ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>`.

Une ambiguïté apparaît lorsque la colonne ClickHouse contient un tuple de 8 éléments dont le dernier est lui-même un tuple, par exemple `Tuple(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Tuple(String, String))`. Le driver ne peut pas distinguer :

* Un **tuple plat de 9 éléments** (avec une imbrication de TRest générée par le compilateur)
* Un **tuple de 8 éléments** dont le dernier est un `Tuple(String, String)` imbriqué

Les deux correspondent au même type .NET : `ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>`.

Le driver interprète le 8e argument comme TRest et l’aplatit. Par conséquent, un tuple de 8 éléments dont le dernier est un tuple imbriqué sera sérialisé de manière incorrecte.

Cela concerne aussi bien `System.Tuple` que `ValueTuple`, car tous deux utilisent l’imbrication de TRest au-delà de 7 éléments. Les tuples de 7 éléments ou moins, ainsi que ceux dont le dernier élément n’est pas lui-même un tuple, ne sont pas concernés.

**Solution de contournement :** Placez le tuple interne dans un niveau d’imbrication supplémentaire pour que le driver puisse le distinguer de l’imbrication de TRest :

```csharp theme={null}
// Instead of this (ambiguous — is it 8 elements or 9 flat?):
Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create("a", "b"))

// Do this (unambiguous — inner tuple is wrapped):
Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create(Tuple.Create("a", "b")))
```

***

<h3 id="aggregatefunction-columns">
  Colonnes de type AggregateFunction
</h3>

Les colonnes de type `AggregateFunction(...)` ne peuvent pas être interrogées directement ni recevoir de données par insertion directe.

Pour insérer des données :

```sql theme={null}
INSERT INTO t VALUES (uniqState(1));
```

Pour sélectionner :

```sql theme={null}
SELECT uniqMerge(c) FROM t;
```

***
