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

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

# Client C# officiel de 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).
Développé à l’origine 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 meilleur choix pour la plupart des applications.

* **ADO.NET** (`ClickHouseDataSource`, `ClickHouseConnection`, `ClickHouseCommand`) : les abstractions standard de base de données de .NET. Indispensable pour l’intégration avec les ORM (Dapper, Linq2db) et lorsque vous avez besoin de la compatibilité ADO.NET. `ClickHouseBulkCopy` est une classe utilitaire qui permet d’insérer efficacement des données à l’aide d’une connexion ADO.NET. `ClickHouseBulkCopy` est obsolète et sera supprimé dans une prochaine version ; utilisez plutôt `ClickHouseClient.InsertBinaryAsync`.

Les deux API partagent le même pool de connexions HTTP sous-jacent et peuvent être utilisées ensemble dans la même application.

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

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

***

<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 avec le gestionnaire de packages NuGet :

```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>

Il existe deux façons de configurer votre connexion à ClickHouse :

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

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

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

| Propriété | Type | Par défaut | Clé de chaîne de connexion | Description |
| - | - | - | - | - |
| Hôte | `string` | `"localhost"` | `Host` | Nom d’hôte ou adresse IP du serveur ClickHouse |
| Port | `ushort` | 8123 (HTTP) / 8443 (HTTPS) | `Port` | Numéro de port ; valeur par défaut selon le protocole |
| Nom d’utilisateur | `string` | `"default"` | `Username` | Nom d’utilisateur pour l’authentification |
| Mot de passe | `string` | `""` | `Password` | Mot de passe pour l’authentification |
| Base de données | `string` | `""` | `Database` | Base de données par défaut ; si vide, utilise celle par défaut du serveur ou de l’utilisateur |
| Protocole | `string` | `"http"` | `Protocol` | Protocole de connexion : `"http"` ou `"https"` |
| Chemin | `string` | `null` | `Path` | Chemin URL pour les scénarios avec reverse proxy (p. ex., `/clickhouse`) |
| Délai d’expiration | `TimeSpan` | 2 minutes | `Timeout` | Délai d’expiration de l’opération (stocké en secondes dans la chaîne de connexion) |

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

| Propriété | Type | Valeur par défaut | Clé de chaîne de connexion | Description |
| - | - | - | - | - |
| UseCompression | `bool` | `true` | `Compression` | Régit la compression de transport dans les deux sens pour une requête ordinaire : elle demande au server de compresser la réponse (`enable_http_compression` ; voir `AcceptEncoding` pour le codec, qu’une valeur explicite peut demander même lorsque cette option est désactivée) **et** compresse en gzip le request corps — sauf avec `UseFormDataParameters`, dont le corps multipart est toujours envoyé uncompressed. Les inserts binaires ne la consultent jamais ; ils utilisent `InsertOptions.Compressor` — voir [Compression des insertions](#insert-compression) |
| AcceptEncoding | `string` | `null` | `AcceptEncoding` | `Accept-Encoding` envoyé avec chaque requête, en remplacement des codecs que le pilote annonce par défaut (`zstd, lz4, gzip, deflate`). Quel que soit le codec renvoyé par le server, il est décodé de manière transparente. Voir [Décompression des réponses](#response-decompression) |
| UseCustomDecimals | `bool` | `true` | `UseCustomDecimals` | Utiliser `ClickHouseDecimal` pour une précision arbitraire ; si false, utilise `decimal` de .NET (limite de 128 bits) |
| ReadStringsAsByteArrays | `bool` | `false` | `ReadStringsAsByteArrays` | Lire les colonnes `String` et `FixedString` sous forme de `byte[]` au lieu de `string` ; utile pour les données binaires |
| UseFormDataParameters | `bool` | `false` | `UseFormDataParameters` | Envoyer les paramètres sous forme de form data au lieu de la requête string de l’URL |
| ReadBufferSize | `int` | `65536` (64 KiB) | `ReadBufferSize` | Taille en octets du buffer qui lit les réponses aux requêtes HTTP. Le pilote emprunte le buffer à un pool partagé et le restitue lorsqu’il libère le lecteur : il ne s’agit donc pas d’une allocation pour chaque requête. Augmentez-la pour réduire les remplissages du buffer avec les grands result sets. Le pilote conserve un buffer par lecteur concurrent : l’utilisation mémoire augmente donc avec la buffer size et le nombre de lecteurs concurrents. Voir [Buffers](#perf-buffers). |
| ParameterTypeResolver | `IParameterTypeResolver` | `null` | — | Résolveur personnalisé pour la mise en correspondance des types de paramètres de style `@` ; voir [Mise en correspondance personnalisée des types de paramètres](#parameter-type-mapping) |
| ParameterFormatter | `IParameterFormatter` | `null` | — | Formateur personnalisé pour la sérialisation des valeurs des paramètres ; voir [Formatage personnalisé des valeurs de paramètres](#parameter-value-formatting) |
| ReadValueConverter | `IReadValueConverter` | `null` | — | Convertisseur personnalisé appliqué 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 `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 des type hints) |
| MapReadMode | `MapReadMode` | `Dictionary` | `MapReadMode` | Mode de renvoi des données `Map(K, V)` : `Dictionary` (renvoie `Dictionary<K, V>` ; une key répétée ne conserve que sa dernière valeur) ou `KeyValuePairs` (renvoie `List<KeyValuePair<K, V>>`, en conservant chaque paire). Voir [Type Map](#type-map-reading-map) |
| AllowDuplicateJsonKeys | `bool` | `false` | `AllowDuplicateJsonKeys` | Manière de lire une row `JSON` dont des paths qui se recouvrent contiennent tous deux une valeur. `false` lève une exception, car conserver l’une des valeurs revient à supprimer l’autre ; `true` conserve celle que la row porte en dernier. Voir [Paths qui se recouvrent](#type-map-reading-json) |

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

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

<Note>
  L’option `UseSession` active la persistance de la session du serveur, ce qui permet d’utiliser des instructions `SET` et des tables temporaires. Les sessions sont réinitialisées après 60 secondes d’inactivité (délai d’expiration par défaut). La durée de vie de la session peut être prolongée en définissant des paramètres de session via des instructions ClickHouse ou 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’option `UseSession` limite cela à une seule requête active par connexion à un instant donné (il s’agit d’une limitation côté serveur).
</Note>

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

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

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

| Propriété | Type | 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 pour créer des instances `HttpClient` |
| HttpClientName | `string` | `null` | — | Nom utilisé par `HttpClientFactory` pour créer un client spécifique |

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

| Propriété | Type | Par défaut | Clé de chaîne de connexion | Description |
| - | - | - | - | - |
| LoggerFactory | `ILoggerFactory` | `null` | — | Fabrique de loggers pour la journalisation de diagnostic |
| EnableDebugMode | `bool` | `false` | — | Active le traçage réseau .NET (nécessite LoggerFactory avec le niveau défini sur Trace) ; **impact significatif sur les performances** |

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

| Propriété | Type | 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 note 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 | — | Tags libres ajoutés à l’en-tête HTTP `User-Agent` pour attribuer les requêtes à chaque application. |

<Note>
  Lorsque vous utilisez une chaîne de connexion pour définir des paramètres personnalisés, utilisez le préfixe `set_`, par ex. : "set\_max\_threads=4". Si vous utilisez un objet ClickHouseClientSettings, n'utilisez pas le préfixe `set_`.

  Pour obtenir 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` vous permet de surcharger les paramètres définis au niveau du client pour chaque requête. Toutes les propriétés sont facultatives et ne remplacent les valeurs par défaut du client que lorsqu’elles sont spécifiées.

| Propriété | Type | Description |
| - | - | - |
| QueryId | `string` | Identifiant de requête personnalisé 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 ex. `max_threads`) |
| CustomHeaders | `IDictionary<string, string>` | En-têtes HTTP supplémentaires pour cette requête |
| UseSession | `bool?` | Remplace le comportement de la 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 la mise en correspondance des types de paramètres de style `@` ; voir [Mise en correspondance personnalisée 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 de paramètres de style `@` ; voir [Formatage personnalisé des valeurs de paramètres](#parameter-value-formatting) |
| ReadValueConverter | `IReadValueConverter` | Remplace le convertisseur défini au niveau du client appliqué 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 côté serveur pour la requête (transmis comme paramètre `max_execution_time`) ; le serveur annule la requête si cette durée est dépassée |
| AcceptEncoding | `string` | Remplace, pour cette requête, `Accept-Encoding` (par ex. `"br"`, `"identity"`), avec priorité sur `ClickHouseClientSettings.AcceptEncoding` ; force également `enable_http_compression=1` dans l’URL. Voir [Compression de 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 spécifiques aux opérations d’insertion en masse via `InsertBinaryAsync`.

| Propriété | Type | Par défaut | Description |
| - | - | - | - |
| BatchSize | `int` | 100,000 | Nombre de lignes par lot |
| MaxDegreeOfParallelism | `int` | 1 | Nombre d’envois de lots en parallèle |
| Format | `RowBinaryFormat` | `RowBinary` | Format binaire : `RowBinary` ou `RowBinaryWithDefaults` |
| Compressor | `IClickHouseCompressor` | `ZstdCompressor.Default` | Codec appliqué au corps de l’insertion (`Content-Encoding`). `null` l’envoie non compressé. Voir [Compression des insertions](#insert-compression) |
| QueryPlacement | `InsertQueryPlacement` | `Body` | Emplacement d’envoi de l’instruction `INSERT INTO ... FORMAT ...` : `Body` (avant les lignes) ou `Url` (comme 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. Ignore la requête de sondage du schéma lorsqu’elle est définie. |
| UseSchemaCache | `bool` | `false` | Met en cache le schéma complet de la table pour chaque paire (base de données, table) pendant 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">
  Ignorer la requête de sondage du schéma
</h4>

Par défaut, `InsertBinaryAsync` envoie une requête `SELECT ... WHERE 1=0` avant chaque insertion afin d’identifier les types de colonnes. Pour les scénarios à haut débit, vous pouvez supprimer cette surcharge de deux façons :

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

Lorsque vous connaissez le schéma de la table à 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 en cache le schéma**

Lorsque vous effectuez plusieurs insertions dans la même table, définissez `UseSchemaCache = true` pour n’interroger le schéma qu’une seule fois et le réutiliser lors des insertions suivantes sur la même instance `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` est prioritaire sur `UseSchemaCache`. Si les deux sont définis, les types explicites sont utilisés.
  * Le cache de schéma ne détecte pas les modifications apportées via `ALTER TABLE`. Si vous modifiez le schéma de la table, créez un nouveau `ClickHouseClient` ou évitez `UseSchemaCache` pour cette table.
  * Le cache est propre à l’instance `ClickHouseClient` et indexé par (database, table). Différents sous-ensembles de colonnes d’une même table partagent un schéma mis en cache unique.
</Note>

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

`ClickHouseClient` est l’API recommandée pour interagir avec ClickHouse. Il est thread-safe, conçu pour être utilisé comme singleton et gère en interne le 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 ClickHouse Cloud console.

Sélectionnez un service, puis cliquez sur **Connect** :

<Image img="https://mintcdn.com/private-7c7dfe99-parallel-read-in-order-multi-part/GTkpPcjoRQ_okrH3/images/_snippets/cloud-connect-button.webp?fit=max&auto=format&n=GTkpPcjoRQ_okrH3&q=85&s=d059c1bbcc7317ff8df85b20189e65f4" size="md" alt="Bouton Connect du service ClickHouse Cloud" border width="998" height="932" data-path="images/_snippets/cloud-connect-button.webp" />

Choisissez **C#**. Les détails de connexion s’affichent ci-dessous.

<Image img="https://mintcdn.com/private-7c7dfe99-parallel-read-in-order-multi-part/GTkpPcjoRQ_okrH3/images/_snippets/connection-details-csharp.webp?fit=max&auto=format&n=GTkpPcjoRQ_okrH3&q=85&s=487b14816a8a8711d46ae022d82d74ef" size="md" alt="Détails de connexion C# de ClickHouse Cloud" border width="851" height="805" data-path="images/_snippets/connection-details-csharp.webp" />

Si vous utilisez ClickHouse autogéré, les détails de connexion sont définis 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 cas 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 conservé sur la durée et partagé dans toute votre application. Créez-le une seule fois (généralement sous forme de singleton) et réutilisez-le pour toutes les opérations sur la base de données. Le client gère en interne le pool de connexions HTTP.
</Note>

***

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

Utilisez `ExecuteNonQueryAsync` pour les instructions qui ne renvoient pas de résultats :

```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 seule valeur :

```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">
  Insérer des données
</h3>

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

Insérez des données à l’aide de requêtes paramétrées avec `ExecuteNonQueryAsync`. Les types des paramètres doivent être spécifiés dans le SQL à l’aide de 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. Il transmet les données en flux à l’aide du format binaire natif de lignes de ClickHouse, prend en charge les envois parallèles par lots 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 l’envoi par lots et le parallélisme avec `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>
  * Le client récupère automatiquement la structure de la table via `SELECT * FROM <table> WHERE 1=0` avant d’insérer les données. Les valeurs fournies doivent correspondre aux types des colonnes cibles. Pour ignorer cette requête, utilisez [`InsertOptions.ColumnTypes` ou `InsertOptions.UseSchemaCache`](#skip-schema-query).
  * Lorsque `MaxDegreeOfParallelism > 1`, les batches sont envoyé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` si vous souhaitez que le serveur applique les valeurs DEFAULT aux colonnes non fournies.
</Note>

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

Au lieu de construire des tableaux `object[]`, vous pouvez insérer directement des objets POCO fortement typés. Enregistrez le type une seule fois, puis passez `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 associées à des colonnes selon une correspondance stricte des noms, sensible à la casse. Vous pouvez personnaliser ce mappage à 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 | Objectif |
| - | - |
| `[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 spécifient un `Type` explicite, la requête de sondage du schéma est entièrement omise. Lorsque seules certaines propriétés ont des types explicites, le pilote revient à la requête de sondage du schéma pour l’ensemble des colonnes.

`InsertBinaryAsync<T>` prend en charge les mêmes `InsertOptions` (batching, 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 contrôler les colonnes insérées, utilisez `[ClickHouseNotMapped]` pour exclure des propriétés ou `[ClickHouseColumn(Name = "...")]` pour les renommer.

  Si `ColumnTypes` est défini dans `InsertOptions`, elles remplaceront les attributs POCO.
</Note>

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

Les insertions de POCO fonctionnent de façon transparente lorsque des colonnes sont ajoutées à la table cible après l’enregistrement du type. Comme le pilote n’insère que les colonnes associées au POCO, toute nouvelle colonne avec `DEFAULT` (ou d’autres expressions par défaut) est automatiquement remplie par le serveur. Aucune modification du code ni aucun nouvel enregistrement ne sont nécessaires.

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

Une insertion binaire écrit son instruction `INSERT INTO ... FORMAT ...` sur la première ligne du corps de la requête, avant les lignes de données. Le corps étant compressé par défaut, les mécanismes de routage et de journalisation qui n'inspectent que l'URL ne voient pas cette instruction. Définissez `InsertOptions.QueryPlacement` sur `InsertQueryPlacement.Url` pour envoyer plutôt l’instruction dans le paramètre d'URL `query`, le corps ne contenant alors que les lignes de données :

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

Utilisez-la lorsqu'un proxy, un load balancer ou une gateway route ou inspecte le paramètre `query`, ou lorsque vous souhaitez que l’instruction apparaisse dans les logs d'accès et les outils d'observability. Cette option est facultative, car l’instruction est alors comptabilisée dans la longueur de l'URL. La limite effective est la plus basse de celles imposées par le runtime .NET, par un intermédiaire et par le server. De .NET 6 à .NET 9, `System.Uri` limite l'URI de requête complet et encodé à 65 519 caractères ; le pilote lève une `InvalidOperationException` qui vous renvoie vers `InsertQueryPlacement.Body` lorsque cette limite est dépassée. Le paramètre `http_max_uri_size` de ClickHouse vaut 1 Mio par défaut, tandis qu'un intermédiaire peut imposer une limite plus basse. En mode corps, l’instruction et les rows ne sont soumis à aucune limite de longueur d'URL ; d'autres options de requête peuvent toujours 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é fournit un accès typé aux colonnes de résultat via des 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 index (à partir de 0) ou par nom de colonne.

```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 de POCO
</h4>

Au lieu de lire les colonnes par index ou par nom, vous pouvez acheminer directement le résultat de la requête vers vos propres classes. Enregistrez le type une 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 à la fois les mappages d’insert et de lecture, et valide les deux en amont. `RegisterBinaryInsertType<T>()` reste inchangé et demeure réservé à l’insert pour des raisons de compatibilité descendante.

Un type enregistré doit avoir :

* Un constructeur public sans paramètre.
* Au moins une propriété publique avec un setter public non-`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 de résultat manquantes laissent les propriétés à leur valeur par défaut ; les colonnes de résultat supplémentaires sont ignorées.

Le pilote n'élargit ni ne restreint les valeurs. En dehors des représentations alternatives listées ci-dessous,
le type framework de la colonne doit être assignable au type de la propriété, et une incompatibilité lève
`InvalidOperationException`. Une propriété `object` accepte donc n'importe quelle colonne.

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

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

| 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 les types natifs `System.Int128`/`System.UInt128` sur .NET 8 et versions ultérieures |
| `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 label) ou `int` (l'ordinal transmis) |
| `String`/`FixedString` | `string` ou `byte[]` |

Chaque ligne accepte également la forme nullable de son type de propriété (`long?`, `DateOnly?`, etc.),
que la colonne soit `Nullable(...)` ou non. Une propriété de type valeur non nullable sur une colonne
`Nullable(T)` est acceptée à l'enregistrement, mais lève une exception à l'arrivée d'un NULL.

Les wrappers tels que `LowCardinality(T)`, `SimpleAggregateFunction(f, T)` et `Object(T)` se mappent exactement comme `T`.

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

Une colonne `Map(K, V)` constitue un cas particulier : une propriété `List<KeyValuePair<K, V>>` ou `KeyValuePair<K, V>[]`
est lue sans boxing et conserve l'ordre de transmission ainsi que les éventuelles 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 de clé et de valeur doivent correspondre exactement : ainsi,
`Map(String, Nullable(Int32))` requiert `KeyValuePair<string, int?>`.

Lorsqu'une colonne propose plusieurs types de propriété (une colonne `DateTime` en tant que `DateTime`,
`DateTimeOffset` ou `DateOnly`, une colonne `String` en tant que `string` ou `byte[]`), le type de propriété déclaré détermine la représentation. Ces représentations alternatives
appartiennent au chemin POCO : `QueryAsync<T>` en dispose, mais pas `MapTo<T>`.

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

Lorsque vous parcourez manuellement un lecteur, utilisez `ClickHouseDataReader.MapTo<T>()` pour matérialiser la ligne en cours dans un POCO enregistré sans faire avancer le lecteur :

```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 piloter vous-même la boucle du lecteur — par exemple pour combiner un accès brut aux colonnes
avec la matérialisation en POCO. Cette méthode lit la ligne via les valeurs boxées du lecteur : elle n'offre donc pas
les types de propriétés alternatifs présentés plus haut, et elle alloue davantage que `QueryAsync<T>`. Préférez
`QueryAsync<T>` lorsque vous n'avez besoin que des lignes ; voir
[choisir le chemin de matérialisation](#perf-read-path) pour les chiffres.

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

Un [convertisseur de valeurs en lecture](#read-value-conversion) défini au niveau du client ou par requête s'applique aux deux chemins et
ne désactive pas la lecture sans boxing. Le pilote convertit chaque colonne via la surcharge correspondant à la manière dont
il l'a lue : le `ConvertValue<T>` typé pour une colonne
sans boxing, et le `ConvertValue` boxé pour une colonne composite. Implémentez ces deux surcharges de façon
cohérente, sans quoi une même colonne donnera des résultats différents selon le chemin emprunté.

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

Si un `LoggerFactory` est configuré, `RegisterPocoType<T>()` et `RegisterBinaryInsertType<T>()` génèrent un journal de niveau `Débogage` (catégorie `ClickHouse.Driver.Client`) indiquant quelles propriétés ont été mappées à quelles colonnes, ainsi que celles qui ont été ignorées et pour quelle raison. Voir [Journalisation et diagnostics](#logging-and-diagnostics).

***

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

Dans ClickHouse, le format standard des paramètres dans les requêtes SQL est `{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. En utiliser un trop grand nombre peut donc entraîner une exception "URL too long". Utilisez `InsertBinaryAsync` pour l’insertion en masse de données afin d’éviter cette limitation.
</Note>

<h4 id="at-style-placeholders">
  Placeholders `@name` de style ADO
</h4>

Le driver accepte également les placeholders `@name`, tels qu'en émettent les ORM comme Dapper. Il s'agit d'une
commodité côté client : avant l'envoi de la requête, chacun d'eux est réécrit en
`{name:ResolvedType}`, si bien que le serveur ne voit jamais de `@`. Consultez la
[résolution de type](#parameter-type-mapping) pour savoir comment le type est choisi. Privilégiez la forme
explicite `{name:Type}` lorsque c'est possible.

Un `@name` sans paramètre correspondant est laissé tel quel, à charge pour le serveur de le rejeter. La correspondance est
case-sensitive : `@ID` ne lie donc pas un paramètre nommé `id`.

<Note>
  Pour désactiver la rewrite, 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, de sorte que
  les requêtes écrites avec la syntaxe native `{name:Type}` continuent de fonctionner.
</Note>

<h4 id="identifier-parameters">
  Paramètres Identifier
</h4>

Le type de paramètre `Identifier` vous permet de lier en toute sécurité un nom de base de données, de table ou de colonne, au lieu d’un littéral de chaîne entre guillemets. Utilisez-le avec la syntaxe `{name:Identifier}` en SQL, ou en définissant `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, et le serveur la substitue comme un identifiant SQL nu, en appliquant lui-même la mise entre accents graves et l’échappement. Les identifiants contenant des caractères spéciaux (y compris des accents graves) sont ainsi préservés sans risque à l’aller-retour.

***

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

Chaque requête se voit attribuer un `query_id` unique, qui peut être utilisé pour extraire des données de la table `system.query_log` ou annuler des requêtes de longue durée. Vous pouvez spécifier un ID de requête personnalisé via `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é, assurez-vous qu'il soit unique à 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 style `@` (par exemple, `WHERE id = @id`), le driver déduit automatiquement le type ClickHouse à partir du type de valeur .NET. Par exemple, `int` correspond à `Int32`.

<Warning>
  **Comportement des paramètres `DateTime` déduits**

  Pour les paramètres de style `@` sans indication `{name:Type}` dans le SQL et sans `ClickHouseType` défini, les valeurs représentant un instant sont déduites comme `DateTime('UTC')` plutôt que comme un simple `DateTime`. Les valeurs `DateTime` dont `Kind` vaut `Utc` ou `Local`, ainsi que toutes les valeurs `DateTimeOffset`, sont envoyées comme `DateTime('UTC')`, ce qui préserve l’instant quel que soit le fuseau horaire du serveur.

  Les indications explicites (`{name:DateTime}`) prévalent sur l’inférence et constituent la manière recommandée de construire les requêtes.
</Warning>

Pour remplacer ces valeurs par défaut, définissez `ParameterTypeResolver` dans `ClickHouseClientSettings`. C'est utile si vous voulez que tous les paramètres `DateTime` utilisent `DateTime64(3)` pour une précision à la milliseconde, ou que toutes les valeurs décimales utilisent une échelle spécifique, sans avoir à définir `ClickHouseType` sur chaque paramètre individuellement.

**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);
```

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

Pour une résolution basée sur le nom ou tenant compte de la valeur, 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 seule requête via `QueryOptions.ParameterTypeResolver`. Lorsqu’il est défini, il prévaut sur le résolveur défini au niveau du client.

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

Le résolveur s’inscrit lui aussi dans une chaîne de priorité. De la priorité la plus élevée à la plus faible :

1. `ClickHouseType` explicite défini sur le paramètre
2. annotation de type SQL issue de la syntaxe `{name:Type}` dans la requête
3. `IParameterTypeResolver` (via `QueryOptions.ParameterTypeResolver`, avec repli sur `ClickHouseClientSettings.ParameterTypeResolver`)
4. Inférence de type intégrée (`TypeConverter.ToClickHouseType`)

Le résolveur fonctionne également avec le chemin `ClickHouseConnection` d’ADO.NET : les paramètres de configuration sont hérités par les connexions créées à partir du client.

***

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

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

Définissez `ParameterFormatter` dans `ClickHouseClientSettings` pour installer un formateur pour toutes les requêtes paramétrées. Le formateur reçoit la valeur, le nom de 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 utiliser le formateur par défaut.

**Utilisation de `DictionaryParameterFormatter` pour un formatage simple par 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 par requête via `QueryOptions.ParameterFormatter`. Lorsqu’il est défini, il prévaut sur le formateur défini au niveau du client.

**Valeurs composites :**

Le formateur s’applique à la fois aux paramètres de collection de premier niveau et à chaque élément à l’intérieur des valeurs composites (`Array`, `Tuple`, `Map`, `Nullable`, `LowCardinality`, `Variant`). Par exemple, un mappage `typeof(int)` formate individuellement chaque élément `Int32` d’un `Array(Int32)`.

**Encadrement par apostrophes simples dans les contextes composites :**

Pour les types ClickHouse de type chaîne (`String`, `FixedString`, `Enum8`, `Enum16`, `IPv4`, `IPv6`, `UUID`) intégrés dans un littéral composite, le driver entoure la sortie du formateur d’apostrophes simples, mais n’en échappe pas le contenu. Si la chaîne renvoyée contient une apostrophe simple ou un antislash non échappé, le littéral composite sera mal formé et le server rejettera la query.

Les paramètres de chaîne de premier niveau (non intégrés dans un composite) sont utilisés tels quels, sans encadrement ; aucun échappement n’est donc nécessaire dans ce cas.

**Priorité du formateur :**

1. `IParameterFormatter` (issu de `QueryOptions.ParameterFormatter`, ou à défaut de `ClickHouseClientSettings.ParameterFormatter`). S’il renvoie une valeur non nulle, c’est cette valeur qui est utilisée.
2. Le formatage intégré, spécifique au type, dans `HttpParameterFormatter`.

Le formateur n’est pas consulté pour les valeurs `null` ou `DBNull`, qui sont toujours sérialisées comme la valeur sentinelle null de ClickHouse (`\N`).

***

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

`IReadValueConverter` vous permet de transformer les valeurs renvoyées par le lecteur de données après la désérialisation, sans modifier leur type CLR. Cas d’usage typiques : définir `DateTime.Kind = Utc` sur une colonne `DateTime` sans fuseau horaire, tronquer ou normaliser des chaînes, ou post-traiter une colonne JSON avant qu’elle n’arrive au code d’application.

Définissez `ReadValueConverter` dans `ClickHouseClientSettings` pour installer un convertisseur pour toutes les opérations de lecture. Le convertisseur est appelé une fois par colonne et par ligne, à la fois via le chemin encapsulé (`GetValue`) et le chemin générique (`GetFieldValue<T>`). Lorsqu’aucun convertisseur n’est défini, il n’y a aucun overhead : 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’est pas enregistré avec `For<T>` sont transmises inchangées. La sélection s’effectue sur le type CLR exact ; enregistrez donc le type réel renvoyé par le lecteur (par ex. `For<JsonObject>` pour une colonne JSON en `JsonReadMode.Binary`).

**`IReadValueConverter` personnalisé pour les scénarios avancés :**

Si vous devez effectuer la sélection en fonction de la chaîne de type côté ClickHouse (par exemple, pour distinguer `DateTime` de `DateTime('UTC')` — tous deux apparaissent comme 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 colonne (`GetFieldType`, `GetSchemaTable`) ne transitent pas par lui et doivent rester cohérentes avec ce qui est renvoyé.

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

**Limite de dispatch :**

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

**Quelle surcharge est exécutée :**

Les deux surcharges doivent être cohérentes, car celle que le driver appelle dépend de la façon dont l’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 sans encapsulation sur le
  [chemin de lecture POCO](#poco-read-converters).
* `ConvertValue` (encapsulé) — `GetValue`, `GetValues`, les indexeurs, `GetChar`, `GetTuple`, et les
  chemins de coercition dans `GetBoolean`, `GetDecimal` et `GetString`.

`IsDBNull` n’exécute aucun convertisseur : il lit directement le drapeau null, de sorte qu’un convertisseur ne peut jamais
modifier le fait qu’une valeur soit considérée comme null. `TryGetEnumOrdinal` le contourne également — voir
[lecture de l’ordinal d’un enum](#ado-net-reader-enum-ordinal).

Le convertisseur fonctionne avec le chemin ADO.NET `ClickHouseConnection` — les paramètres sont hérités par les connexions créées à partir du client.

***

<h3 id="raw-streaming">
  Flux brut
</h3>

Utilisez `ExecuteRawResultAsync` pour transmettre directement le résultat de la requête dans un format spécifique, sans passer par le lecteur de données. Cela est utile pour exporter des données vers des fichiers ou les acheminer vers 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 des formats](/fr/reference/formats/index) pour voir toutes les options.

***

<h3 id="per-query-accept-encoding">
  Compression du transport par requête
</h3>

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

Pour les exports bruts (par ex. Parquet, Arrow, Native), vous pouvez souhaiter négocier un codec différent (par ex. `zstd` ou `lz4`) afin de troquer du CPU contre du débit sans modifier le paramètre défini pour l’ensemble de la connexion. `QueryOptions.AcceptEncoding` et `ClickHouseCommand.AcceptEncoding` définissent l’en-tête HTTP `Accept-Encoding` pour une seule requête, en remplaçant toute valeur par défaut appliquée jusque-là, et forcent `enable_http_compression=1` dans l’URL (condition exigée par ClickHouse 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` construit par le driver laisse `AutomaticDecompression` à `DecompressionMethods.None` et décode lui-même les réponses ; ainsi, `Content-Encoding` n'est jamais retiré à votre insu et le corps brut vous parvient exactement tel que le server l'a envoyé.

<Warning>
  Si vous fournissez votre propre `HttpClient`, laissez également `AutomaticDecompression` désactivé. Ce n'est pas uniquement un paramètre côté réponse : au moment de l'envoi, le handler **ajoute à l'en-tête `Accept-Encoding` sortant tous les algorithmes de son masque qui y sont absents**. 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 le format binaire — et comme ClickHouse résout l'en-tête selon son propre ordre de préférence de codecs (en ignorant l'ordre et les valeurs q), il peut répondre avec un codec que vous n'avez jamais demandé, que le handler décode puis retire, si bien que vous ne pouvez même pas vous en apercevoir. En laissant le masque désactivé, l'offre reste exactement celle que vous avez choisie.
</Warning>

<Warning>
  Si `AcceptEncoding` demande un codec que le driver ne sait pas décoder (`snappy`), seul `ExecuteRawResultAsync` est sûr. `ExecuteReaderAsync`, `ExecuteScalarAsync` et `ExecuteNonQueryAsync` échouent avec une `NotSupportedException` indiquant le codec en cause (auparavant, ils interprétaient les compressed bytes comme s'il s'agissait du format de résultat et produisaient des données inexploitables).
</Warning>

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

Lorsque le serveur renvoie une réponse 4xx/5xx et que `enable_http_compression=1` est défini, il compresse le corps de l’erreur avec le même codec que celui qu’il aurait utilisé pour une réponse réussie. Le driver décode ces corps pour tous les codecs qu’il prend en charge (`lz4`, `zstd`, `gzip`, `deflate`, `br`/`brotli`), afin que le message affiché dans `ClickHouseServerException` soit lisible. Pour tout autre codec (`snappy`, …), il renvoie un message de substitution qui indique le codec et pointe vers `system.query_log` pour le texte d’erreur d’origine.

***

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

`Accept-Encoding` demande uniquement au serveur de compresser la réponse — encore faut-il que quelque chose la décode. Le driver s'en charge lui-même, à partir du `Content-Encoding` de la réponse, si bien que toutes les API de lecture ordinaires (`ExecuteReaderAsync`, `ExecuteScalarAsync`, `ExecuteNonQueryAsync`, `QueryAsync<T>`, Dapper, EF Core, linq2db) fonctionnent sur une réponse compressée sans aucune configuration. Il décode `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 un autre choix, 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 a la préséance :

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

ou dans la connection string, pour les utilisateurs d'ORM qui ne manipulent jamais `ClickHouseClientSettings` :

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

Le définir force également `enable_http_compression=1` dans l'URL, ce que ClickHouse exige avant même de prendre l’en-tête en compte — y compris lorsque `UseCompression` vaut `false`, puisque nommer explicitement un codec revient à en demander un. Sans valeur définie, `UseCompression=false` n'envoie aucun `Accept-Encoding`.

`Accept-Encoding` peut être défini à quatre endroits. Le premier d'entre eux qui nomme un codec l'emporte :

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 keyword de chaîne de connection `AcceptEncoding`

Si aucun ne le fait, le driver envoie sa liste par défaut. Une valeur qui ne nomme aucun codec (null, vide,
espaces, ou uniquement des virgules) est considérée comme non définie et laisse la main à l'endroit suivant. Pour désactiver la compression, utilisez `identity`.

**C'est le server, et non le client, qui choisit le codec.** ClickHouse parcourt `Accept-Encoding` à la recherche de tokens selon son propre ordre de préférence figé — `zstd` > `br` > `lz4` > `snappy` > `gzip` > `deflate` — et ignore aussi bien l'ordre dans lequel vous les listez que les q-values. L’en-tête constitue donc une announcement de capability plutôt qu'une exigence, et le seul moyen d'orienter le choix consiste à omettre certains tokens. La liste par défaut inclut `zstd` : une requête par défaut reçoit donc une réponse en zstd, les tokens restants servant de fallback. `br` est décodable, mais n'est pas annoncé par défaut.

La comparaison des codecs en matière de taille de payload, de CPU côté server et de CPU côté client dépend de vos données, de votre lien réseau et de la valeur de `http_zlib_compression_level` du server (valeur par défaut livrée : 3) — voir [Réglage de la compression](#tuning-compression).

* **`http_zlib_compression_level`.** Ce SETTING s'applique à tous les codecs HTTP, et sa default value est 3. Cette valeur doit être ajustée en fonction de vos données, de la vitesse du lien réseau et de l'utilisation CPU.
* **Un client CPU-bound sur un lien rapide.** Le driver décode le response corps sur le thread appelant : lorsque le réseau n'est pas le goulot d'étranglement, la vitesse de décodage client-side peut devenir le facteur limitant.

Demandez un codec différent par requête, ou pour l'ensemble du client, dès que l'un de ces cas s'applique :

```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 est prise à partir de la réponse, un corps est décodé dès lors que son `Content-Encoding` l'indique, quelle que soit la requête initiale : en l'absence d'en-tête ou avec `identity`, il passe tel quel ; un codec pris en charge est décodé ; toute autre valeur lève une erreur la mentionnant. Il n'y a aucun risque de double décodage — si l'`AutomaticDecompression` d'un handler fourni par l'appelant a déjà décodé un corps, il supprime également `Content-Encoding`, si bien que le driver voit du plaintext et n'y touche pas.

**Les résultats bruts n'annoncent aucun codec.** `ExecuteRawResultAsync` (ainsi que les méthodes publiques `PostStreamAsync` / `InsertRawStreamAsync`) vous transmettent leur corps tel quel : à moins que vous ne spécifiiez vous-même un codec, elles n'en demandent aucun — rien dans le driver ne décode un tel corps, si bien que proposer un codec à cet endroit transformerait silencieusement un export en fichier compressé. La règle est donc simple, et indépendante de la configuration du `HttpClient` : **un corps transmis tel quel arrive exactement comme le server l'a envoyé, et le server envoie du plaintext sauf si vous avez demandé un codec.** En demander un (au niveau du client ou par requête) est le moyen d'exporter délibérément des compressed bytes.

Un `AcceptEncoding` explicite (à l'un ou l'autre niveau) s'applique malgré tout aux requêtes brutes, et `ClickHouseRawResult.ReadDecompressedStreamAsync()` décode le résultat lorsque vous le souhaitez ; `ReadAsStreamAsync`, `ReadAsByteArrayAsync`, `ReadAsStringAsync` et `CopyToAsync` renvoient toujours les octets exactement tels qu'ils sont arrivés.

```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 retourné jusqu'à la fin avant qu'il ne sorte de sa portée, comme ci-dessus. Lorsque la réponse *est* compressée, vous obtenez un decoder créé avec `leaveOpen` : sa libération laisse donc la réponse intacte ; lorsqu'elle n'est **pas** compressée, vous obtenez le flux de contenu HTTP lui-même, et sa libération met fin au corps. Dans les deux cas, c'est le `ClickHouseRawResult` qui est propriétaire de la réponse — n'appelez pas ses autres membres de lecture une fois le flux libéré. Libérer le `ClickHouseRawResult` est toujours nécessaire et suffit à lui seul : cela libère à la fois la réponse et tout decoder inséré ici (les decoders retiennent des buffers issus du pool). Le `await using` ci-dessus est donc facultatif, et peut être conservé sans risque. Des appels séquentiels répétés renvoient le même flux ; le type ne peut pas être utilisé de manière concurrente.

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

<h4 id="insert-compression">
  Compression des insertions (requêtes)
</h4>

Zstd est le codec par défaut pour les insertions : `InsertOptions.Compressor` vaut initialement `ZstdCompressor.Default`,
c'est-à-dire zstd au niveau 3. Affectez-lui un autre compresseur pour changer de codec, ou `null` pour envoyer le
corps non compressé.

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

Quatre codecs sont fournis avec le driver. Chacun dispose d'une instance `Default` ainsi que d'un constructeur qui prend en paramètres un niveau
et la taille du write buffer :

| 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 unique, et les quatre compresseurs
  peuvent être utilisés sans risque depuis plusieurs threads à la fois — ce qui est le cas lorsque
  `InsertOptions.MaxDegreeOfParallelism` est supérieur à 1, puisqu'un insert utilise un compresseur par
  batch. Aucun d'eux n'implémente `IDisposable`. Créez votre propre instance une seule fois et réutilisez-la, de la
  même manière que `Default` est utilisé.
</Note>

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

`IClickHouseCompressor` est public, et une 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 le `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 surchargez donc que celles dont votre codec a besoin.
Implémentez `Decompress` pour décoder les corps de réponse en plus de compresser les requêtes, et levez
`InvalidDataException` depuis le flux qu'il renvoie lorsqu'un corps est corrompu ou dans un format incorrect.

`InsertOptions.Compressor` ne régit que les insertions binaires. Les autres corps de requête du driver sont compressés selon d'autres règles, et aucun d'eux n'y fait appel :

* **Toute requête en texte SQL** (`ExecuteReaderAsync`, `ExecuteScalarAsync`, `ExecuteNonQueryAsync`, `QueryAsync<T>`, `ExecuteRawResultAsync`, la couche ADO.NET) envoie son statement avec `Content-Encoding: gzip` dès lors que `UseCompression` vaut `true` — c'est-à-dire par défaut. Le codec n'est pas configurable : `AcceptEncoding` ne pilote que la réponse, le choix se limite donc à gzip ou rien. Avec `Compression=false`, le statement part en clair. Les statements étant de petite taille, il est rarement utile de s'en préoccuper — mais mieux vaut le savoir lorsque vous observez les requêtes dans un proxy ou une capture de packets.
* **Un corps multipart** — une requête dont les parameters sont envoyés sous forme de form data (`UseFormDataParameters=true`) — est toujours envoyé uncompressed, quelle que soit la valeur de `UseCompression`.
* **Un téléversement raw** (`InsertRawStreamAsync`, `PostStreamAsync`) s'appuie sur son propre flag, propre à chaque appel, et ne consulte ni `UseCompression` ni `InsertOptions.Compressor` : gzip lorsque le flag est positionné, uncompressed sinon. Notez que le parameter `useCompression` de `InsertRawStreamAsync` vaut `true` par défaut : un téléversement raw est donc gzippé sauf si vous passez `false` — même avec `Compression=false` sur le client.

***

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

La compression troque du CPU contre des octets. Le gain réel dépend presque entièrement de la rapidité de votre
lien réseau par rapport à la vitesse d'exécution du codec. Aucun réglage ne convient à
tout le monde.

<h4 id="the-one-number-that-decides-it">
  Le chiffre décisif
</h4>

La compression en vaut la peine tant que le codec est plus rapide que le réseau.

Ce seuil est plus bas qu'on ne l'imagine généralement sur le chemin de lecture, car ClickHouse compresse
les réponses HTTP en mono-thread dans le buffer de sortie. Mesuré sur un service ClickHouse Cloud à 16 vCPU
(`hits`, RowBinary, niveau 3), le serveur produit une sortie compressée à environ 100-200 Mo/s.

Ainsi, pour un résultat volumineux, et en supposant qu'une seule requête soit traitée à la fois, la compression cesse d'être rentable aux alentours de 100 Mo/s. Un flux HTTPS unique
au sein d'une même région cloud dépasse couramment ce débit, tandis que tout trafic traversant l'Internet public, un VPN ou une frontière de région se situe généralement en dessous.

Le chemin d'insertion tolère la compression jusqu'à des débits de liaison plus élevés, car votre client compresse sur un cœur qui lui est propre et se révèle généralement plus rapide que la compression des réponses côté serveur.

<h4 id="rough-guide-by-deployment">
  Guide approximatif par déploiement
</h4>

| Emplacement d'exécution du client | Débit habituel | Lectures | Insertions |
| - | - | - | - |
| Même hôte / boucle locale | > 500 Mo/s | `identity` | `lz4` le plus rapide, ou aucune |
| Même région, même cloud | \~100–500 Mo/s | `identity` ou `lz4` | `zstd:1` |
| Inter-régions, même cloud | \~10–100 Mo/s | `zstd` | `zstd:3` |
| Internet / VPN / cloud différent | \< 25 Mo/s | `zstd` | `zstd:3` |
| Connexion facturée ou très contrainte | \< 5 Mo/s | `zstd` | `zstd:5`+ ou `br` |

Trois éléments que ce tableau ne prend pas en compte :

* **Le coût de l'egress :** si le transfert de données vous est facturé, les octets ont un prix qui dépasse la seule question de la latence, ce qui incite à compresser davantage, quelle que soit la vitesse du lien.
* **Les petits résultats :** tout ce qui précède concerne des payloads volumineux. Pour de petites réponses, le codec n'a quasiment aucune importance et c'est l'overhead par requête qui domine.
* **Les insertions parallèles relèvent les seuils d'insertion.** Chaque valeur de débit ci-dessus vaut pour un *seul* thread. `InsertOptions.MaxDegreeOfParallelism` vaut `1` par défaut, mais l'augmenter permet de compresser les batches en parallèle : le taux d'encodage agrégé du client évolue donc à peu près proportionnellement au nombre de cœurs que vous lui allouez. Ainsi, sur un lien rapide, il reste intéressant de compresser une insertion parallèle bien au-delà de la vitesse à laquelle cela cesse d'être rentable pour une insertion mono-thread. Considérez les lignes d'insertion du tableau comme un *plancher* et, si vous regroupez déjà en batches parallèles, refaites vos tests avant de conclure que votre lien est trop rapide pour la compression.

Le chemin de lecture ne se parallélise qu'entre plusieurs requêtes.

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

| Codec | Ratio | À utiliser quand | Point d'attention |
| - | - | - | - |
| `lz4` | le plus faible | Liaisons rapides ; le CPU est une ressource plus rare que le débit. De loin le moins coûteux à décoder, et le plus rapide sur les petits résultats — ce qui en fait le codec à indiquer lorsque vous voulez vous écarter du zstd par défaut. | Il ne dispose d'**aucun codeur entropique** : sur des données déséquilibrées mais peu répétitives (de longues séries de texte numérique, par exemple), son ratio reste très en deçà de tous les autres. C'est aussi le codec le plus pénalisé par l'augmentation de `http_zlib_compression_level` : passer du niveau 1 au niveau 3 lui coûte environ 2,7× plus de CPU pour environ 29 % d'octets en moins. |
| `zstd` | élevé | Le choix polyvalent dès qu'un vrai réseau entre en jeu. Meilleur ratio par unité de CPU dans la plage qui compte, et au niveau 3 il devance `lz4` en octets *et* en CPU serveur *et* en temps écoulé. | plus coûteux que `lz4` à **décoder** — 1,6× au niveau 3 dans nos mesures, même si au niveau 1 les deux se valent — et le driver décode sur votre thread appelant. Avec `http_zlib_compression_level=1` en particulier, il consomme légèrement *plus* de CPU serveur que `lz4`. |
| `gzip` | moyen | L'interopérabilité — universellement compris par les proxys et les passerelles. | Dominé sur tous les axes à la fois par `lz4` et `zstd` dans nos mesures : plus volumineux que `zstd` tout en coûtant plusieurs fois plus de CPU à l'encodage et 5 à 9× plus au décodage. Choisissez-le pour la compatibilité, pas pour les performances. |
| `br` | le plus élevé aux niveaux bas | Le débit constitue réellement le facteur limitant et vous pouvez y consacrer du CPU. | S'effondre aux niveaux supérieurs — à `http_zlib_compression_level=6`, nous l'avons mesuré à 3–4× le CPU serveur de `zstd`. Non annoncé par défaut, car il l'emporte sur tous les tokens de fallback de la liste par défaut. |

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

La compression des réponses est régie par un unique server setting, `http_zlib_compression_level`, qui s'applique à *tous* les codecs HTTP, et pas seulement à zlib. Sa valeur par défaut est 3.

N'y touchez pas sans avoir mesuré une raison de le faire. Au-dessus de la valeur par défaut, le gain de taille est minime au regard du coût CPU (pour `zstd`, passer de 3 à 6 double environ la charge CPU du server pour \~14 % d'octets en moins), et `br` devient pathologique. En dessous, au niveau 1, la donne change vraiment : `lz4` devient bien moins coûteux et `zstd` perd son avantage CPU sur lui. Définissez-le par requête si nécessaire :

```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 point de bascule
</h4>

Le moyen le plus rapide d'optimiser votre choix de codec et de niveau de compression consiste à chronométrer la même requête avec plusieurs codecs, puis à 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 vision côté serveur du même phénomène, relisez `ProfileEvents` dans `system.query_log` — définissez
`QueryOptions.QueryId` afin de 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';
```

Un piège si vous réalisez ce benchmark vous-même : un simple `LIMIT n` sans `ORDER BY` renvoie *des rows différentes
à chaque exécution*, si bien que chaque répétition compresse des données différentes et que les ratios ne sont plus que du bruit. Comparez
plutôt sur un result set fixe.

***

<h3 id="raw-stream-insert">
  Insertion via raw stream
</h3>

Utilisez `InsertRawStreamAsync` pour insérer des données directement depuis un fichier ou un flux en mémoire, dans des formats tels que CSV, JSON, Parquet ou tout autre [format ClickHouse pris en charge](/fr/reference/formats/index).

**Insertion depuis 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 stream.* `InsertRawStreamAsync` et `PostStreamAsync` libèrent le
  stream que vous leur transmettez dès la fin de la requête, qu'elle ait réussi ou échoué. Ne le libérez pas
  vous-même et ne le réutilisez pas par la suite — d'où le fait que l'exemple ci-dessus n'encapsule pas le
  `FileStream` dans un `using`.

  Un `using` de votre côté s'exécuterait alors que le driver a déjà libéré le stream. Pour un `FileStream` ou
  un `MemoryStream`, ce second appel est sans conséquence, mais pour un stream dont le `Dispose` restitue un buffer
  à un pool ou décrémente un compteur de références, la ressource est libérée deux fois.

  La possession n'est transférée qu'une fois les arguments acceptés : si l'appel lève une exception `ArgumentException` ou
  `ArgumentNullException` parce qu'il manque la table, le stream ou le format, le stream vous appartient toujours.
</Warning>

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

***

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

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

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

La bibliothèque offre une prise en charge complète d’ADO.NET via `ClickHouseConnection`, `ClickHouseCommand` et `ClickHouseDataReader`. Cette API est nécessaire pour l’intégration avec les ORM (Dapper, Linq2db) et lorsque vous avez besoin des abstractions .NET standard pour les bases de données.

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

**Créez toujours des connexions à partir d’un `ClickHouseDataSource`** afin de garantir une gestion correcte du cycle de vie ainsi qu’un pool de connexions. Le `ClickHouseDataSource` gère en interne une unique instance de `ClickHouseClient`, et toutes les connexions partagent son 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();
```

Avec 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 `ClickHouseConnection` directement** dans le code de production. Chaque instanciation directe crée un nouveau client HTTP et un nouveau pool de connexions, ce qui peut entraîner un épuisement des sockets en cas de charge :

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

  Utilisez toujours `ClickHouseDataSource` à la place, ou partagez une unique 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 instructions INSERT, UPDATE, DELETE et 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 un accès typé au résultat de la requête :

```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 l'ordinal d'un enum
</h4>

Une colonne `Enum8` ou `Enum16` se matérialise sous la forme de son label : `GetFieldType` renvoie `string`, et
`GetString`, `GetValue` et `GetFieldValue<string>` retournent tous le label. Les accesseurs numériques
lèvent une `InvalidCastException` sur une colonne enum, car la valeur stockée est une chaîne de caractères.

Utilisez `TryGetEnumOrdinal` pour obtenir le nombre associé au label :

```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`, avec `value` défini à `0`, pour une cellule NULL ou pour
toute colonne qui n'est pas un enum. L'ordinal correspond à la
valeur signée transmise sur le wire : il peut donc être négatif, et un ordinal `Enum16` peut dépasser la taille 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 par endpoint. Par conséquent :

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

**Approches recommandées :**

| Scénario | Approche recommandée |
| - | - |
| Usage général | Utilisez un `ClickHouseClient` singleton |
| ADO.NET / ORMs | Utilisez `ClickHouseDataSource` (crée des connexions partageant le même pool) |
| Environnements DI | Enregistrez `ClickHouseClient` ou `ClickHouseDataSource` comme singleton avec `IHttpClientFactory` |

<Warning>
  Si vous utilisez un `HttpClient` ou un `HttpClientFactory` personnalisé, assurez-vous que `PooledConnectionIdleTimeout` est défini sur une valeur inférieure au `keep_alive_timeout` du serveur, afin d’éviter les erreurs dues à des connexions semi-fermées. La valeur par défaut de `keep_alive_timeout` pour les déploiements Cloud 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 horodatages dans des colonnes `DateTime('UTC')` et utilisez `DateTimeKind.Utc` dans votre code. Cela élimine toute ambiguïté liée au fuseau horaire.

2. **Utilisez `DateTimeOffset` pour gérer explicitement le fuseau horaire.** Il représente toujours un instant précis et inclut les informations de décalage.

3. **Spécifiez le fuseau horaire dans les annotations de type SQL.** Lorsque vous utilisez des paramètres avec des valeurs DateTime `Unspecified` pour des colonnes non UTC, incluez le fuseau horaire dans le 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) transfèrent la responsabilité du batching du client vers le serveur. Au lieu d'exiger un batching côté client, le serveur met les données entrantes en mémoire tampon, puis les écrit dans le stockage en fonction de seuils configurables. Cela est utile dans les scénarios à forte concurrence, comme les workloads d'observability où de nombreux agents envoient de petites charges utiles.

Activez les insertions asynchrones via `CustomSettings` ou 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’usage |
| - | - | - |
| `wait_for_async_insert=1` | L’insertion renvoie une réponse une fois les données écrites sur le 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 lorsque les données sont placées en tampon. Aucune garantie que les données seront persistées. | Uniquement si la perte de données est acceptable |

<Warning>
  Avec `wait_for_async_insert=0`, les erreurs n’apparaissent qu’au moment de l’écriture sur disque et ne peuvent pas être rattachées à l’insertion d’origine. Le client ne fournit pas non plus de mécanisme de régulation, ce qui risque de surcharger le serveur.
</Warning>

**Paramètres clés :**

| Setting | Description |
| - | - |
| `async_insert_max_data_size` | Écrit sur disque lorsque le tampon atteint cette taille (octets) |
| `async_insert_busy_timeout_ms` | Écrit sur disque après ce délai d’expiration (millisecondes) |
| `async_insert_max_query_number` | Écrit sur disque après l’accumulation de ce nombre de requêtes |

***

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

N’activez les sessions que si vous avez besoin de fonctionnalités côté serveur avec état, par exemple :

* Tables temporaires (`CREATE TEMPORARY TABLE`)
* Conservation du contexte de requête d’une instruction à l’autre
* Paramètres de session (`SET max_threads = 4`)

Lorsque les sessions sont activées, les requêtes sont sérialisées afin d’éviter l’utilisation concurrente d’une même session. Cela ajoute un surcoût pour les workload qui ne nécessitent pas d’état de 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 assurer 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 de ClickHouse. Les tableaux ci-dessous présentent les correspondances entre les types ClickHouse et les types .NET natifs lors de la lecture des données depuis la base de données.

<h3 id="clickhouse-native-type-map-reading">
  Correspondance de types : lecture à partir de ClickHouse
</h3>

<h4 id="type-map-reading-integer">
  Types d’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 du type Decimal est gérée par le 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
</h4>

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

<Note>
  Par défaut, les colonnes `String` et `FixedString(N)` sont toutes deux renvoyées sous forme de `string`. Définissez `ReadStringsAsByteArrays=true` dans votre chaîne de connexion pour les lire sous forme de `byte[]`. Cela est utile lorsque vous stockez des données binaires qui peuvent ne pas être en UTF-8 valide.

  Ce paramètre s'applique aussi aux chaînes imbriquées dans d'autres types : ainsi, `Array(String)` est lu comme `byte[][]`
  et `Map(String, String)` comme `Dictionary<byte[], byte[]>` — clés comprises. La seule exception est une
  colonne `JSON`, dont les feuilles de type chaîne sont toujours du 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 de timestamps Unix (secondes ou fractions de seconde écoulées depuis l’époque). Bien que le stockage soit toujours en UTC, les colonnes peuvent avoir un fuseau horaire associé, qui influe sur la manière dont les valeurs sont affichées et interprétées.

Lors de la lecture de valeurs `DateTime`, la propriété `DateTime.Kind` est définie en fonction du 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 d’horloge conservée telle quelle |

Pour les colonnes non UTC, le `DateTime` renvoyé représente l’heure d’horloge dans ce fuseau horaire. Utilisez `ClickHouseDataReader.GetDateTimeOffset()` pour obtenir un `DateTimeOffset` avec le décalage correct pour ce fuseau horaire :

```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.-à-d. `DateTime` au lieu de `DateTime('Europe/Amsterdam')`), le pilote renvoie un `DateTime` avec `Kind=Unspecified`. Cela préserve exactement l’heure telle qu’elle est stockée, sans supposer de fuseau horaire.

Si vous avez besoin d’un comportement tenant compte du fuseau horaire pour des colonnes sans fuseau horaire explicite, vous pouvez :

1. Utiliser des fuseaux horaires explicites dans vos définitions de 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` | Lorsque `JsonReadMode=String` |

Le type de retour des colonnes JSON dépend du paramètre `JsonReadMode` :

* **`Binary` (par défaut)** : renvoie `System.Text.Json.Nodes.JsonObject`. Fournit un accès structuré aux données JSON, mais les types ClickHouse spécialisés (comme les adresses IP, les UUID ou les grands nombres décimaux) sont convertis en représentations sous forme de chaîne dans la structure JSON.

* **`String`** : renvoie le JSON brut sous forme de `string`. Préserve la représentation JSON exacte de ClickHouse, ce qui est utile lorsque vous devez transmettre le JSON sans l’analyser, ou lorsque vous souhaitez 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 effectue la lecture exactement comme `Binary`, mais n'envoie aucun paramètre serveur avec la
requête — utilisez-le sur une connexion qui n'est pas autorisée à en définir un.

<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 **typed path** ; tout autre chemin du document est un
**dynamic path**. Les deux diffèrent lorsqu'une valeur est nulle.

Un typed path apparaît toujours dans le `JsonObject`. Déclaré `Nullable(T)` ou `Dynamic`, il est renvoyé
sous la forme d'un null JSON aussi bien lorsque la valeur stockée est nulle que lorsque le document ne contient pas ce chemin — les deux
cas sont impossibles à distinguer :

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

Déclaré avec un type non nullable, un chemin absent prend la valeur par défaut du type — `JSON(x String)`
donne `{"x":""}` et `JSON(x Int64)` donne `{"x":0}`.

Un chemin dynamique dont la valeur est null est entièrement supprimé de l'objet, si bien que `ContainsKey` renvoie
false pour celui-ci. La lecture de `{"x":null}` depuis une colonne `JSON` simple donne `{}`.

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

<Note>
  C'est ce que le server restitue lui-même, si bien que les modes `Binary` et `String` concordent désormais. Avant la version 1.4.0, un
  chemin typé contenant null était supprimé du `JsonObject`, si bien que `{"x":null}` était 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 feuilles de type chaîne au sein d'une colonne `JSON` sont toujours renvoyées sous forme de texte, quelle que soit la valeur de
`ReadStringsAsByteArrays` — `JsonValue` n'a pas de forme tableau d'octets, un `byte[]` serait donc
rendu en base64. Cela vaut pour `String`, `FixedString`, ainsi que pour ces types encapsulés dans
`LowCardinality`, `Nullable` ou `SimpleAggregateFunction`, et pour les chaînes contenues dans `Array` et `Map`,
clés de map incluses.

<Note>
  Un tableau d'octets dont le lecteur JSON ne peut pas déterminer le type est bel et bien rendu en base64 : un
  chemin typé `Variant` ou `Dynamic` contient une valeur dont le type n'est connu que ligne par ligne ; ainsi, une chaîne
  sous `Variant(Array(UInt8), String)` est renvoyée encodée en base64. Le comportement est identique avec les deux réglages.

  Un type de clé de map JSON autre que `String` exactement — `Map(LowCardinality(String), String)`, par
  exemple — lève une `NotSupportedException`.
</Note>

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

ClickHouse accepte une colonne qui déclare un chemin à 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, si bien que le serveur
restitue la ligne avec une clé dupliquée : `{"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` désignant les deux chemins. Il
en va de même lorsque la valeur est un `Map`, comme dans `JSON(a Map(String, Int64))` lu depuis une ligne qui
possède également un `a.b` dynamique.

Cela ne s'applique que si les deux côtés portent une valeur dans cette ligne. Un côté qui ne contient rien — un null,
un objet vide ou un sous-arbre dont toutes les valeurs sont nulles — cède la place à celui qui porte la donnée,
quel que soit celui des deux chemins que le serveur envoie en premier. Un chevauchement déclaré avec des types `Nullable` ne renseigne donc qu'un seul côté par ligne et se lit sans
erreur : `JSON(a Nullable(Int64), a.b Nullable(Int64))` donne bien `{"a":5}` et `{"a":{"b":7}}`, comme
attendu.

Lisez une telle colonne avec `JsonReadMode.String` pour obtenir le texte JSON du serveur inchangé, clé dupliquée
comprise.

Définissez `AllowDuplicateJsonKeys` pour continuer à lire la colonne en tant que `JsonObject` au lieu de lever une exception. Le
pilote conserve alors celle des deux valeurs qui apparaît en dernier dans la ligne et abandonne l'autre : le
résultat est donc incomplet : `JSON(a Int64, a.b Int64)` contenant `{"a.b":7}` se lit comme `{"a":0}`. Un chemin qui
porte une valeur et dont le parent porte un scalar ou un Array lève malgré tout une exception, car un sous-arbre ne peut être
placé sous ni l'un ni l'autre.

```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">
  Map type
</h4>

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

Un `Map(K, V)` ClickHouse est physiquement un `Array(Tuple(K, V))` et peut contenir plusieurs entrées associées à la même clé. Ce n'est pas le cas d'un `Dictionary` : en mode par défaut, une clé répétée ne conserve donc que sa dernière valeur et les paires précédentes sont abandonnées. Le paramètre `MapReadMode` détermine la représentation utilisée :

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

* **`KeyValuePairs`** : renvoie `List<KeyValuePair<K, V>>` dans l'ordre où le serveur a envoyé les paires, si bien que chaque paire est préservée, y compris les entrées qui répètent une clé.

```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 framework d'une colonne `Map` ; il s'applique donc également à `GetFieldValue<T>`, aux types de schema signalés par le pilote et au mapping des propriétés POCO. Il s'applique partout où une map apparaît dans l'arborescence de types d'une colonne — y compris `Array(Map(...))`, `Map(K, Map(...))`, `Tuple(..., Map(...))` et `Dynamic`.

Les deux représentations sont acceptées à l'écriture, quel que soit le mode — voir [écriture des maps](#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[]` (les `Array(Array(T))` imbriqués sont lus comme des `T[][]` irréguliers ; utilisez `reader.GetFieldValue<T[,]>(ordinal)` pour matérialiser des données rectangulaires sous forme de 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 [Map type](#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>
  Les types Dynamic et Variant sont convertis dans le type correspondant au type sous-jacent réel de chaque ligne.
</Note>

***

<h4 id="type-map-reading-geometry">
  Types de géométrie
</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 de géométrie. Il sera converti en 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 pilote convertit les types .NET vers leurs types ClickHouse correspondants. Les tableaux ci-dessous indiquent quels types .NET sont pris en charge pour chaque type de colonne ClickHouse.

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

| Type ClickHouse | Types .NET acceptés | Remarques |
| - | - | - |
| 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 en virgule flottante
</h4>

| Type ClickHouse | Types .NET acceptés | Remarques |
| - | - | - |
| Float32 | `float`, tout type compatible avec `Convert.ToSingle()` | |
| Float64 | `double`, tout type compatible avec `Convert.ToDouble()` | |
| BFloat16 | `float`, tout type compatible avec `Convert.ToSingle()` | Tronqué au format bfloat16 sur 16 bits |

***

<h4 id="type-map-writing-boolean">
  Type Boolean
</h4>

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

***

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

| Type ClickHouse | Types .NET acceptés | Notes |
| - | - | - |
| String | `string`, `byte[]`, `ReadOnlyMemory<byte>`, `Stream` | Les types binaires sont écrits directement ; les flux peuvent être repositionnables ou non |
| FixedString(N) | `string`, `byte[]`, `ReadOnlyMemory<byte>`, `Stream` | La chaîne est encodée en UTF-8 et complétée ; les types binaires doivent comporter 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 jours Unix sous forme d’UInt16 ; plage prise en charge `[1970-01-01, 2149-06-06]` |
| Date32 | `DateTime`, `DateTimeOffset`, `DateOnly`, types NodaTime | Converti en jours Unix sous forme d’Int32 ; plage prise en charge `[1900-01-01, 2299-12-31]` |
| DateTime | `DateTime`, `DateTimeOffset`, `DateOnly`, types NodaTime | Voir ci-dessous pour plus de détails ; 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 basée sur le paramètre Scale |
| Time | `TimeSpan`, `TimeOnly`, `int` | Borné à ±999:59:59 ; `int` interprété comme un nombre de secondes |
| Time64 | `TimeSpan`, `TimeOnly`, `decimal`, `double`, `float`, `int`, `long`, `string` | Chaîne analysée comme `[-]HHH:MM:SS[.fraction]` ; borné à ±999:59:59.999999999 |

<Note>
  **Valeurs hors plage**

  Lors d’une écriture binaire, les valeurs `Date`, `Date32`, `DateTime` et `DateTime32` en dehors de leur plage prise en charge lèvent une `ArgumentOutOfRangeException` au moment de `Write`, en indiquant le type de colonne et la plage prise en charge. Auparavant, les valeurs hors plage pouvaient être tronquées silencieusement via un entier 32 bits puis réinterprétées par le serveur, produisant des timestamps réels mais erronés.
</Note>

Le pilote respecte `DateTime.Kind` lors de l’écriture des valeurs :

| DateTime.Kind | Paramètres HTTP | Insertion en bloc |
| - | - | - |
| Utc | Instant préservé | Instant préservé |
| Local | Instant préservé | Instant préservé |
| Unspecified | Traité comme une heure d’horloge dans le fuseau horaire du type du paramètre (UTC par défaut) | Traité comme une heure d’horloge dans le fuseau horaire de la colonne |

Les valeurs `DateTimeOffset` préservent 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 d’horloge)**

```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 types DateTime. Cela garantit que votre code fonctionne de manière cohérente, quel que soit le fuseau horaire du serveur, du client ou de la colonne.

<h4 id="datetime-http-param-vs-bulkcopy">
  Paramètres HTTP vs copie en masse
</h4>

Il existe une différence importante entre la liaison de paramètres HTTP et la copie en masse lors de l’écriture de valeurs DateTime `Unspecified` :

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

**Paramètres HTTP** ne connaissent pas automatiquement le fuseau horaire de la colonne. Vous devez le spécifier dans l’indication 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) | Insertion en bloc |
| - | - | - | - | - |
| `Utc` | UTC | Instant préservé | Instant préservé | Instant préservé |
| `Utc` | Europe/Amsterdam | Instant préservé | Instant préservé | Instant préservé |
| `Local` | Quelconque | 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 | Notes |
| - | - | - |
| Decimal(P,S) | `decimal`, `ClickHouseDecimal`, tout type compatible avec `Convert.ToDecimal()` | Lève une `OverflowException` si la précision maximale est dépassée |
| 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 | Remarques |
| - | - | - |
| Json | `string`, `JsonObject`, `JsonNode`, tout objet | Le comportement dépend du paramètre `JsonWriteMode` |

Le comportement lors de l’écriture de JSON est contrôlé par le paramètre `JsonWriteMode` :

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

* **`String` (par défaut)** : Accepte `string`, `JsonObject`, `JsonNode` ou tout objet. Toutes les entrées sont sérialisées via `System.Text.Json.JsonSerializer` et envoyées sous forme de chaînes JSON pour un traitement côté serveur. C’est le mode le plus flexible et il fonctionne sans enregistrement préalable de type.

* **`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. Nécessite d’appeler `connection.RegisterJsonSerializationType<T>()` avant utilisation. L’écriture de valeurs `string` ou `JsonNode` dans ce mode 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 inclut des indications de type (par ex. `JSON(id UInt64, price Decimal128(2))`), le driver utilise ces indications pour sérialiser les valeurs en respectant pleinement les types. Cela préserve la précision de types comme `UInt64`, `Decimal`, `UUID` et `DateTime64`, qui perdraient sinon en précision s'ils étaient sérialisés sous forme de 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 le `JsonWriteMode` :

**Mode String (par défaut)** : les POCO sont sérialisés via `System.Text.Json.JsonSerializer`. Aucun enregistrement de type n’est nécessaire. C’est l’approche la plus simple, et elle fonctionne avec les objets anonymes.

**Mode binaire** : les POCO sont sérialisés à l’aide du format JSON binaire du driver, avec prise en charge complète des indications de type. Les types doivent être enregistrés avec `connection.RegisterJsonSerializationType<T>()` avant utilisation. Ce mode prend en charge des mappages de chemins personnalisés via des 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 binaire.**

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

```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é et les indications de type de colonne est sensible à la casse. Une propriété `UserId` ne correspondra qu’à une indication définie comme `UserId`, et non `userid`. Cela correspond au comportement de ClickHouse, qui permet à des chemins comme `userName` et `UserName` de coexister en tant que champs distincts.

**Limitations (mode binaire uniquement) :**

* Les types POCO doivent être enregistrés sur la connexion avec `connection.RegisterJsonSerializationType<T>()` avant la sérialisation. Toute tentative de sérialiser un type non enregistré lève une `ClickHouseJsonSerializationException`.
* Les propriétés de type Dictionary et array/list nécessitent des indications de type dans la définition de la colonne pour être sérialisées correctement. Sans ces indications, utilisez plutôt String mode.
* Les valeurs nulles des propriétés POCO ne sont écrites que lorsque 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 sont donc ignorées.
* Les attributs `ClickHouseJsonPath` et `ClickHouseJsonIgnore` sont ignorés en String mode (ils ne fonctionnent qu’en mode binaire).

***

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

| Type ClickHouse | Types .NET acceptés | Remarques |
| - | - | - |
| UUID | `Guid`, `string` | Chaîne analysée comme un Guid |
| IPv4 | `IPAddress`, `string` | Doit être une adresse IPv4 ; chaîne analysée via `IPAddress.Parse()` |
| IPv6 | `IPAddress`, `string` | Doit être une adresse IPv6 ; chaîne analysée via `IPAddress.Parse()` |
| Nothing | N’importe quel type | N’écrit rien (no-op) |
| Dynamic | — | **Non pris en charge** (lève `NotImplementedException`) |
| Array(T) | `IList`, `null` | `null` écrit un tableau vide. Pour les types imbriqués (`Array(Array(T))` et au-delà), les formes irrégulières (`T[][]`, `List<List<T>>`) comme les tableaux CLR multidimensionnels rectangulaires (`T[,]`, `T[,,]`, …) sont acceptés ; le rang 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 plus de 7 éléments. |
| Map(K, V) | `IDictionary`, `IEnumerable<KeyValuePair<K, V>>` | Une séquence de paires (par exemple la `List<KeyValuePair<K, V>>` produite par `MapReadMode=KeyValuePairs`) est acceptée dans les deux modes de lecture et peut répéter une clé. S’applique aux insertions binaires ainsi qu’aux query parameters |
| Nullable(T) | `null`, `DBNull` ou types acceptés par T | Écrit l’octet indicateur de null avant la valeur |
| Enum8 | `string`, `sbyte`, types numériques | La chaîne est recherchée dans le dictionnaire de l’enum |
| Enum16 | `string`, `short`, types numériques | La chaîne est recherchée dans le dictionnaire de l’enum |
| 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 de 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 de géométrie
</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 quel type de géométrie ci-dessus | Variante de tous les types de géométrie |

***

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

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

***

<h3 id="nested-type-handling">
  Gestion du type Nested
</h3>

Les types Nested de ClickHouse (`Nested(...)`) peuvent être lus et écrits avec la sémantique 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 diagnostics
</h2>

Le client .NET ClickHouse s’intègre aux abstractions `Microsoft.Extensions.Logging` afin d’offrir une journalisation légère, activée sur demande. Lorsqu’elle est activée, le driver émet des messages structurés pour les événements du cycle de vie de la connexion, l’exécution des commandes, les opérations de transport et les opérations d’insertion en masse. La journalisation est entièrement facultative : les applications qui ne configurent pas de logger continuent de s’exécuter sans surcharge supplémentaire.

<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 du fichier appsettings.json
</h4>

Vous pouvez configurer les niveaux de journalisation à l’aide de la configuration .NET standard :

```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">
  Utilisation d’une configuration en mémoire
</h4>

Vous pouvez également configurer le niveau de verbosité de la journalisation par catégorie dans le code :

```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 dédiées afin de vous permettre d’ajuster finement les niveaux de journalisation par composant :

| Catégorie | Source | Points clés |
| - | - | - |
| `ClickHouse.Driver.Connection` | `ClickHouseConnection` | Cycle de vie de la connexion, sélection de la fabrique de clients HTTP, ouverture/fermeture de la connexion, gestion des sessions. |
| `ClickHouse.Driver.Command` | `ClickHouseCommand` | Début/fin d’exécution des requêtes, durée d’exécution, ID de requête, statistiques du serveur et détails des erreurs. |
| `ClickHouse.Driver.Transport` | `ClickHouseConnection` | Requêtes HTTP streaming de bas niveau, indicateurs de compression, codes d’état des réponses et échecs 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 : diagnostic des problèmes de connexion
</h4>

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

Les éléments suivants seront consignés :

* Sélection de la fabrique de clients HTTP (pool par défaut ou connexion unique)
* Configuration du gestionnaire HTTP (SocketsHttpHandler ou HttpClientHandler)
* Paramètres du pool de connexions (MaxConnectionsPerServer, PooledConnectionLifetime, etc.)
* Paramètres de délai d’expiration (ConnectTimeout, Expect100ContinueTimeout, etc.)
* Configuration SSL/TLS
* Événements d’ouverture/fermeture des connexions
* Suivi des ID de session

<h3 id="logging-debugmode">
  Mode Débogage : tracing réseau et diagnostics
</h3>

Pour faciliter le diagnostic des problèmes réseau, la bibliothèque du driver inclut un utilitaire qui active le tracing de bas niveau des mécanismes réseau internes de .NET. Pour l’activer, vous devez fournir une instance de LoggerFactory avec le niveau défini sur Trace, et définir EnableDebugMode sur true (ou l’activer manuellement via la classe `ClickHouse.Driver.Diagnostic.TraceHelper`). Les événements seront consignés dans la catégorie `ClickHouse.Driver.NetTrace`. Avertissement : cela générera des logs extrêmement verbeux et aura un impact sur les performances. Il n’est pas recommandé d’activer le mode 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 intègre une prise en charge native du tracing distribué avec OpenTelemetry via l’API .NET [`System.Diagnostics.Activity`](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing). Lorsqu’il est activé, le driver émet des spans pour les opérations sur la base de données, qui peuvent être exportés vers des backends d’observabilité 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 tracing
</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 ligne de commande, 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 inclut les attributs de base de données standard d’OpenTelemetry, ainsi que des statistiques de requête 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>

Contrôlez le comportement du tracing à 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` peut exposer des données sensibles dans vos traces. À utiliser avec prudence dans les environnements de production.
</Warning>

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

Lorsque vous vous connectez à ClickHouse via HTTPS, vous pouvez configurer le comportement de TLS/SSL de plusieurs manières.

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

Pour les environnements de production nécessitant une logique de validation des certificats personnalisée, fournissez votre propre `HttpClient` avec un gestionnaire `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>
  Points importants à prendre en compte lors de la fourniture d’un HttpClient personnalisé

  * **Décompression automatique** : laissez `AutomaticDecompression` désactivé. Le driver décode lui-même les réponses compressées, ce n’est donc pas nécessaire — et l’activer joue en votre défaveur côté requête : au moment de l’envoi, le handler *ajoute aussi* chaque algorithme de son masque à l’en-tête `Accept-Encoding` sortant, élargissant ce que le driver avait annoncé, si bien que ClickHouse peut répondre avec un codec que vous n’aviez pas demandé. Voir [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">
  Performance tuning
</h2>

Cette section décrit comment utiliser le client pour obtenir des performances optimales, ainsi que les différentes options que vous pouvez ajuster pour adapter les performances du client à votre cas d'usage.

<h3 id="perf-at-a-glance">
  En un coup d'œil
</h3>

\| Si vous | Faites ceci |
\|---|---|---|
\| Lisez des rows dans des POCO | Utilisez [`QueryAsync<T>`](#perf-read-path), et non `MapTo<T>` |
\| Effectuez des insertions volumineuses | Augmentez [`InsertOptions.BatchSize`](#perf-insert-batching) |
\| Exécutez une application console ou worker à forte charge d'insertion | Activez le [Server GC](#perf-gc) |
\| Lisez de gros résultats via le réseau | Laissez la compression des réponses activée (comportement par défaut) |
\| Insérez via une liaison rapide | Essayez [`InsertOptions.Compressor = null`](#perf-compression) |
\| Insérez de nombreuses fois dans la même table | Utilisez [`UseSchemaCache` ou `ColumnTypes`](#skip-schema-query) |
\| Lisez de très gros résultats | Augmentez [`ReadBufferSize`](#perf-buffers) |

***

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

Il existe trois façons d'extraire une row d'un résultat, et leur coût n'est pas le même. Certains chemins encapsulent les résultats (boxing), ce qui augmente les allocations et dégrade les performances.

| Mode de lecture | Encapsule chaque valeur | Notes |
| - | - | - |
| `QueryAsync<T>` | **No** | Lit le stream directement dans vos propriétés. Le chemin rapide. |
| Accessors typés du reader (`GetInt32`, `GetInt64`, `GetDouble`, `GetGuid`, `GetDateTime`, `GetFieldValue<T>`) | **No** | Lecture sans boxing depuis un stockage de valeurs typées. |
| `MapTo<T>` | Yes | Matérialise d'abord la row, puis en copie les valeurs. |
| `GetValue` et `GetValues` | Yes | Ils renvoient un `object` : la valeur doit donc être encapsulée au moment où vous la demandez. |

Pour une lecture de 1 000 000 de rows sur 105 colonnes du dataset *hits* :

| API | Alloué |
| - | -: |
| `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 bénéficient du chemin rapide lorsqu'ils utilisent des accesseurs typés.* linq2db enregistre `GetInt64`,
  `GetDouble` et `GetDateTime` pour chaque colonne, et lit donc sans boxing. Le code qui lit via
  `GetValue` (y compris un résultat `dynamic` renvoyé par Dapper) applique un boxing à chaque valeur. Si une requête ORM est très sollicitée
  et lit via `GetValue`, utilisez `QueryAsync<T>` pour cette requête précise.
</Note>

***

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

La taille de batch est le principal levier sur le throughput d'insertion. `InsertOptions.BatchSize` vaut par défaut
100 000 rows.

**Utilisez de grands batches.** Pour une insertion de 1 000 000 rows, passer de 10 000 à 100 000 rows par
batch a donné :

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

Si vous ne maîtrisez pas la taille de batch (par exemple lorsque de nombreux petits producteurs envoient des rows indépendamment), utilisez les [async inserts](#async-inserts) et laissez le server se charger du batching.

**Téléversements parallèles.** `InsertOptions.MaxDegreeOfParallelism` vaut `1` par défaut. Augmentez cette valeur pour envoyer
plusieurs batches simultanément. Le gain est surtout net lorsque la compression est activée, car chaque batch est alors compressé
sur son propre thread. Les sessions sont incompatibles avec les inserts parallèles : désactivez les sessions ou conservez
`MaxDegreeOfParallelism = 1`.

**Supprimez la schema probe.** Chaque appel à `InsertBinaryAsync` envoie d'abord une requête `SELECT ... WHERE 1=0`
afin de déterminer les types de colonne. Voir [Ignorer la schema probe requête](#skip-schema-query) pour éliminer cet
aller-retour à l'aide de `ColumnTypes` ou `UseSchemaCache`.

<Note>
  Le chemin d'insertion sans boxing s'applique au format `RowBinary` par défaut. `RowBinaryWithDefaults` doit
  examiner chaque value pour repérer le marker `DBDefault` : il conserve donc le chemin le plus lent.
</Note>

***

<h3 id="perf-compression">
  Compression : les deux directions ne s'accordent pas
</h3>

La compression échange du CPU contre des octets. L'intérêt de cet échange dépend du sens du
transfert, du débit de votre connexion au serveur ClickHouse, de la manière dont vos données réagissent à l'algorithme de compression choisi, et du fait que vous payiez ou non chaque octet transféré.

**Lectures :** laissez la compression activée, sauf si votre serveur s'exécute sur la même machine. C'est le comportement par défaut. Par rapport à l'absence de compression, `zstd` au niveau 1
a donné :

| Client vers serveur | Effet de la compression |
| - | - |
| Même hôte (loopback) | Coûte 8 % |
| Même région cloud | **Économise 16 %** |
| Une région d'écart | **Économise 33 %** |

**Insertions :** mesurez avant de compresser. Les gains ne justifient pas toujours son activation. Gardez également à l'esprit que la décompression ajoute de la charge sur le serveur ; cette charge reste modeste pour Zstd et LZ4, mais peut être élevée pour d'autres algorithmes (par exemple 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 le choix du codec, les niveaux de compression et la manière de déterminer votre propre point de bascule, consultez
[Réglage de la compression](#tuning-compression).

***

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

`ReadBufferSize` définit la taille du buffer qui lit les réponses HTTP. Sa valeur par défaut est de 64 Kio.

Le pilote emprunte ce buffer à un pool partagé et le restitue lorsqu'il libère le reader : il ne s'agit
donc pas d'une allocation pour chaque requête. Augmentez cette valeur pour réduire le nombre de
remplissages du buffer sur des résultats volumineux. Le pilote conserve un buffer par reader
ouvert simultanément : la consommation mémoire augmente donc avec la buffer size et avec le nombre
de readers concurrents.

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

<Warning>
  *Libérez toujours les readers.* Un reader restitue son buffer au pool et libère sa connexion HTTP
  lorsque vous le libérez. Abandonner un reader sans le libérer ne restitue pas le buffer au pool et
  peut rendre la connexion HTTP indisponible ; le garbage collection ordinaire ne remplace pas une
  libération explicite.
</Warning>

***

<h3 id="perf-gc">
  Runtime et GC
</h3>

**Activez le Server GC pour les applications réalisant de nombreuses insertions.** À code identique et à nombre d'octets alloués identique, le Workstation GC s'est révélé jusqu'à 97 % plus lent que le Server GC sur les insertions.

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

Les projets ASP.NET Core définissent 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 cause tient à la taille du budget de la génération 0. Le GC Workstation utilise un budget réduit, si bien que les buffers à courte durée de vie créés par une insertion ne meurent pas en génération 0 : ils passent en génération 1, ce qui augmente la promotion et génère beaucoup plus de travail en génération 2. Dans un cas d'insertion, les collectes de génération 2 pour 1 000 opérations s'élevaient à 4 000 avec le GC Server et à 73 000 avec le GC Workstation.

<Note>
  Le GC Server est un réglage de throughput, pas de latency. Dans ces mêmes mesures, le GC Server a passé moins de la moitié du temps total en pause, mais ses pauses individuelles étaient plus longues (95ᵉ percentile à 114,6 ms contre 61,9 ms). Si votre service est sensible à la tail latency, mesurez les deux modes avant de choisir.
</Note>

***

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

L'établissement d'une nouvelle connexion TCP et la réalisation du handshake TLS prennent un temps considérable.
Réutiliser les connexions réduit nettement la latence de vos requêtes.

* Ne créez pas un client pour chaque requête. Chaque nouveau client, avec son propre `HttpClient`, crée un nouveau
  pool de connexions et repaie le coût du handshake. Utilisez un seul `ClickHouseClient` pendant toute la durée de vie de l'application : il est thread-safe et conçu pour
  un usage en singleton.
* Pour ADO.NET et les ORM, utilisez `ClickHouseDataSource`, afin que toutes les connexions partagent un même pool.

Pour l'ensemble des patterns, consultez
[Durée de vie des connexions et pool de connexions](#best-practices-connection-lifetime).

***

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

Dans bien des cas, les performances dépendront de la shape de vos données, de la vitesse de votre liaison au server,
du fait que vous souhaitiez ou non troquer du CPU client contre du CPU server (ou l'inverse), des limitations de votre matériel, etc.
Il est donc recommandé de mesurer vous-même les performances en fonction de vos données et de votre environnement.

Pour connaître la part de travail effectuée par le server, définissez `QueryOptions.QueryId` et relisez les counters :

```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 pilote convertit automatiquement la syntaxe `@parameter` de Dapper en syntaxe native `{parameter:Type}` de ClickHouse, en déduisant les types à partir des valeurs .NET.

Utilisez `ClickHouseDataSource` pour gérer correctement le cycle de vie de la connexion :

```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>

Tous les modes standard de passage des paramètres de Dapper sont pris en charge :

**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 vers 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 native des paramètres ClickHouse
</h4>

Lorsque vous avez besoin d'un contrôle explicite des types, utilisez directement dans le SQL la syntaxe `{param:Type}` de ClickHouse avec un `Dictionary<string, object>` pour les valeurs de paramètre. N'utilisez pas à la fois la syntaxe `@param` et la syntaxe `{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 IN dans 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 réécrit cela en `WHERE id IN (@Ids1, @Ids2, @Ids3)`, et le driver convertit chaque paramètre étendu.

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

```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">
  Gestionnaires de types personnalisés
</h4>

Certains types ClickHouse, par exemple `ITuple`, `BigInteger` et `ClickHouseDecimal`, nécessitent l’enregistrement de gestionnaires 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());
```

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

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

`GetAll<T>()` et `Get<T>(id)` fonctionnent. En revanche, `Insert<T>()` ne fonctionne pas : il génère une syntaxe SQL Server (`SCOPE_IDENTITY`, `[]`). Il est recommandé d’utiliser à la place 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 de colonnes de ClickHouse (respect de la casse).

<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<>` en tant que valeur de `DbParameter` |
| Types Nested comme paramètre | Non pris en charge | Même raison — Dapper rejette les types complexes comme valeurs de paramètre |
| Types Geo comme paramètre | Non pris en charge | Point, Ring, Polygon, LineString, MultiLineString, MultiPolygon |
| `Dapper.Contrib.Insert<T>()` | Non pris en charge | Génère une syntaxe spécifique à SQL Server |
| Type `Nothing` | Non pris en charge | Aucune représentation .NET pertinente |

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

Ce pilote est compatible avec [linq2db](https://github.com/linq2db/linq2db), un ORM léger et un fournisseur LINQ pour .NET. Consultez le site du projet pour une documentation détaillée.

**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 mappages de tables peuvent être définis à l’aide d’attributs ou de l’API fluide. Si les noms de votre classe et de vos propriétés correspondent exactement aux noms de la table 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();
```

**Bulk Copy :**

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

```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, effectuez des requêtes avec LINQ et insérez des données via `SaveChanges` — le tout avec les conventions EF Core habituelles.

* **NuGet** : [`ClickHouse.EntityFrameworkCore`](https://www.nuget.org/packages/ClickHouse.EntityFrameworkCore)
* **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 JOIN, les sous-requêtes et les opérations ensemblistes), `INSERT` via `SaveChanges` / `BulkInsertAsync`, les migrations avec DDL complet (CREATE / ALTER / DROP), ainsi que la configuration du moteur de table spécifique à 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 le `DbContext`, puis effectuez des requêtes 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` |
| **Grands entiers** | `Int128`, `Int256`, `UInt128`, `UInt256` | `BigInteger` |
| **Flottants** | `Float32`, `Float64`, `BFloat16` | `float`, `double` |
| **Décimaux** | `Decimal(P,S)`, `Decimal32(S)`, `Decimal64(S)`, `Decimal128(S)` | `decimal` ou `ClickHouseDecimal` |
| **Bool** | `Bool` | `bool` |
| **Chaînes** | `String`, `FixedString(N)` | `string` |
| **Énumérations** | `Enum8(...)`, `Enum16(...)` | `string` ou `enum` C# |
| **Date/heure** | `Date`, `Date32`, `DateTime`, `DateTime64(P, 'TZ')` | `DateOnly`, `DateTime` |
| **Heure** | `Time`, `Time64(N)` | `TimeSpan` |
| **UUID** | `UUID` | `Guid` |
| **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<...>` |
| **Variant** | `Variant(T1, T2, ...)` | `object` |
| **Dynamic** | `Dynamic` | `object` |
| **JSON** | `Json` | `JsonNode` ou `string` |
| **Géographiques** | `Point`, `Ring`, `LineString`, `Polygon`, `MultiLineString`, `MultiPolygon`, `Geometry` | `Tuple<double,double>` et les tableaux correspondants ; `object` pour `Geometry` |
| **Wrappers** | `Nullable(T)`, `LowCardinality(T)` | Déballés automatiquement |

Utilisez `ClickHouseDecimal` (de `ClickHouse.Driver.Numerics`) au lieu de `decimal` lorsque vous avez besoin de toute la précision des colonnes `Decimal128`/`Decimal256` — `decimal` en .NET est limité à 28–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égats :** `GroupBy` avec `Count`, `LongCount`, `Sum`, `Average`, `Min`, `Max` — y compris `HAVING` (`.Where()` après `.GroupBy()`), plusieurs agrégats dans une même projection et `OrderBy` sur les résultats agrégés.

**JOINs :** `Join` (INNER), schémas `GroupJoin`/`SelectMany` (LEFT et CROSS). LEFT JOIN renvoie de vraies valeurs `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 ensemblistes :** `Concat` (→ `UNION ALL`), `Union` (→ `UNION DISTINCT`), `Intersect`, `Except`.

**Collections locales en mémoire :** les joins et `Contains` sur des collections en mémoire (`int[]`, `List<T>`, etc.) sont traduits en une série de `UNION`.

**Méthodes de chaîne :** `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 leurs équivalents ClickHouse — fonctions arithmétiques, logarithmiques, trigonométriques et utilitaires.

<h5 id="ef-core-join-nulls">
  Sémantique des valeurs nulles de LEFT JOIN
</h5>

Le fournisseur injecte automatiquement `set_join_use_nulls=1` dans chaque chemin de connexion afin de correspondre aux attentes d'Entity Framework concernant le comportement des JOIN.

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

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

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

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

`SaveChanges` utilise l’API native `InsertBinaryAsync` du pilote — l’encodage RowBinary avec un corps de requête compressé est bien plus efficace que le SQL paramétré :

```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 `Added` à `Unchanged` après la sauvegarde, comme avec tout autre fournisseur EF Core.

La **taille du lot** est configurable (1000 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 chargements à haut débit, utilisez `BulkInsertAsync` au lieu de `SaveChanges`. Il s’agit d’une méthode d’extension sur `DbContext` qui contourne entièrement le suivi des modifications d’EF Core, la résolution des identités et la gestion d’état — elle appelle directement `InsertBinaryAsync` du pilote avec l’encodage RowBinary et un corps de requête compressé.

Cette méthode convient donc au chargement de grands ensembles de données lorsque vous n’avez pas besoin du suivi des entités après l’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 traitées en flux, sans être toutes chargées en mémoire. La valeur de retour est le nombre de lignes insérées. Les entités ne sont **pas** rattachées au `DbContext` après l’insertion, il n’y a donc pas de transition d’état `Added` → `Unchanged`.

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

Les colonnes ClickHouse `Enum8`/`Enum16` peuvent être mappées sur des propriétés `string` ou sur des types C# `enum`. Lorsqu’on utilise des énumérations C#, le fournisseur convertit automatiquement l’énumération depuis et vers 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 type personnalisées
</h4>

Le système `ValueConverter` d’EF Core vous permet d’associer des types personnalisés à des types déjà pris en charge par le fournisseur. Le fournisseur ne voit jamais votre type personnalisé — EF Core effectue la conversion à la limite entre les deux.

**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 spécifier explicitement le type ClickHouse.

**Utilisation des 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 comme `Array(Nullable(Int32))` et `LowCardinality(Nullable(String))` sont pris en charge — le fournisseur retire automatiquement les wrappers `Nullable` et `LowCardinality` à chaque niveau d’imbrication.

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

Dans .NET, les colonnes ClickHouse `Variant(T1, T2, ...)` et `Dynamic` sont mappées à `object`. Comme `object` est trop générique pour une inférence de type automatique, vous devez déclarer explicitement le type de stockage via `.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");
```

Lors de la lecture, la valeur est automatiquement désérialisée dans le type .NET correspondant au discriminateur stocké (par ex. `string`, `ulong`, `ulong[]`).

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

Le fournisseur prend en charge le type de colonne `Json` de ClickHouse, qu’il associe à `System.Text.Json.Nodes.JsonNode` (par défaut) ou à `string` (via 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 du JSON s’effectuent via `SaveChanges` et `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 des chaînes JSON brutes, mappez la propriété en `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** — `entity.Data["name"]` dans LINQ n’est pas converti en syntaxe SQL ClickHouse `data.name`. Filtrez sur des colonnes non JSON et examinez le JSON en mémoire.
  * **Sémantique de NULL** — le type JSON de ClickHouse renvoie `{}` (objet vide) pour les valeurs NULL au lieu de SQL NULL.
  * **Précision des entiers** — le JSON de ClickHouse stocke tous les entiers en `Int64`. Lors de la lecture via `JsonNode`, utilisez `GetValue<long>()` plutôt que `GetValue<int>()`.
</Note>

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

Configurez les moteurs de table ClickHouse et les clauses propres à chaque moteur à l’aide de l’API fluide `ToTable(name, t => ...)`. Si aucun moteur n’est configuré, le fournisseur utilise par défaut `MergeTree`, avec `ORDER BY` déduit 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 Fluent | Remarques |
| - | - | - |
| `MergeTree` | `HasMergeTreeEngine()` | Par défaut si rien n’est configuré |
| `ReplacingMergeTree` | `HasReplacingMergeTreeEngine("Version", "IsDeleted")` ou `HasReplacingMergeTreeEngine<T>(e => e.Version)` | Colonnes Version / IsDeleted facultatives |
| `SummingMergeTree` | `HasSummingMergeTreeEngine(…)` ou `HasSummingMergeTreeEngine<T>(e => new { … })` | Colonnes à additionner 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()` | Pas de ORDER BY / PARTITION BY |

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

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

**Index de saut de données** — via `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 (non-skipping) sont ignorés sans avertissement, car ClickHouse n’a pas d’équivalent. Les index uniques provoquent une exception, car ClickHouse ne garantit pas l’unicité.

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

Processus standard des migrations EF Core :

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

Opérations prises en charge :

| Opération | Génère |
| - | - |
| `CREATE TABLE` | Inclut la clause moteur, ORDER BY, PARTITION BY, SETTINGS, ainsi que les codecs/TTL/commentaires/valeurs par défaut des colonnes |
| `ALTER TABLE ADD COLUMN` | — |
| `ALTER TABLE DROP COLUMN` | — |
| `ALTER TABLE MODIFY COLUMN` | Gère les changements de type ainsi que l’ajout/la suppression d’annotations (CODEC, TTL, COMMENT, DEFAULT) |
| `ALTER TABLE RENAME COLUMN` | — |
| `RENAME TABLE` | — |
| `ALTER TABLE ADD INDEX` / `DROP INDEX` | Uniquement les index de saut de données |
| `CREATE DATABASE` / `DROP DATABASE` | Via `EnsureCreated` / `EnsureDeleted` et les migrations |

<h4 id="ef-core-limitations">
  Limitations des migrations
</h4>

| Fonctionnalité | Raison |
| - | - |
| Clés étrangères | ClickHouse n’applique pas les clés étrangères. Les migrations rejettent `AddForeignKey` ; le validateur du modèle émet un avertissement lors de la génération du modèle. |
| Contraintes uniques / index uniques | ClickHouse n’applique pas l’unicité. Les index uniques génèrent une exception lors de la migration. |
| Valeurs générées par le serveur (auto-incrémentation / `IDENTITY`) | ClickHouse n’a pas d’équivalent. |
| Colonnes `Nested(…)` | Pas encore prises en charge comme type CLR mappé. |
| Entités possédées en JSON (`.ToJson()`) | Le mappage JSON structurel 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)). |

Au-delà des migrations, le fournisseur ne prend pas encore en charge :

* **`UPDATE` / `DELETE`**
* **Transactions** : `BeginTransaction` est sans effet. Les transactions ACID ne sont pas prises en charge dans ClickHouse.
* **Traduction des requêtes avec chemin JSON** : `entity.Data["key"]` dans LINQ ne se traduit pas en syntaxe SQL ClickHouse `data.key`. Filtrez sur des colonnes non JSON et inspectez le JSON en mémoire.

<h2 id="limitations">
  Limites
</h2>

<h3 id="valuetuple-caveat">
  Tuples de 8 éléments ou plus avec un tuple imbriqué en dernière position
</h3>

Les types C# `ValueTuple` de plus de 7 éléments utilisent un schéma d’imbrication généré 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>>`.

Cela crée une ambiguïté lorsque la colonne ClickHouse est 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 pilote ne peut pas faire la distinction entre :

* Un **tuple plat de 9 éléments** (imbrication TRest générée par le compilateur)
* Un **tuple de 8 éléments** dont le dernier élément est un `Tuple(String, String)` imbriqué

Les deux produisent le même type .NET : `ValueTuple<int, int, int, int, int, int, int, ValueTuple<string, string>>`.

Le pilote traite le 8e argument comme TRest (c’est-à-dire qu’il l’aplatit), ce qui signifie que le cas du tuple de 8 éléments avec tuple imbriqué sera sérialisé de manière incorrecte.

Cela affecte à la fois `System.Tuple` et `ValueTuple`, car tous deux utilisent l’imbrication TRest au-delà de 7 éléments. Les tuples de 7 éléments ou moins, ou les tuples dont le dernier élément n’est pas lui-même un tuple, ne sont pas affectés.

**Contournement :** encapsulez le tuple interne dans une couche supplémentaire afin que le pilote puisse le distinguer de l’imbrication 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 ni faire l’objet d’une insertion directe.

Pour insérer :

```sql theme={null}
INSERT INTO t VALUES (uniqState(1));
```

Pour sélectionner :

```sql theme={null}
SELECT uniqMerge(c) FROM t;
```

***
