ClickHouse C# client

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

#### Supported types[​](#ef-core-types "Direct link to Supported types")

|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Category ClickHouse Types CLR Types|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | **Integers** `Int8`–`Int64`, `UInt8`–`UInt64` `sbyte`, `short`, `int`, `long`, `byte`, `ushort`, `uint`, `ulong`| **Big integers** `Int128`, `Int256`, `UInt128`, `UInt256` `BigInteger`| **Floats** `Float32`, `Float64`, `BFloat16` `float`, `double`| **Decimals** `Decimal(P,S)`, `Decimal32(S)`, `Decimal64(S)`, `Decimal128(S)` `decimal` or `ClickHouseDecimal`| **Bool** `Bool` `bool`| **Strings** `String`, `FixedString(N)` `string`| **Enums** `Enum8(...)`, `Enum16(...)` `string` or C# `enum`| **Date/time** `Date`, `Date32`, `DateTime`, `DateTime64(P, 'TZ')` `DateOnly`, `DateTime`| **Time** `Time`, `Time64(N)` `TimeSpan`| **UUID** `UUID` `Guid`| **Network** `IPv4`, `IPv6` `IPAddress`| **Arrays** `Array(T)` `T[]` or `List<T>`| **Maps** `Map(K, V)` `Dictionary<K,V>`| **Tuples** `Tuple(T1, ...)` `Tuple<...>` or `ValueTuple<...>`| **Variant** `Variant(T1, T2, ...)` `object`| **Dynamic** `Dynamic` `object`| **JSON** `Json` `JsonNode` or `string`| **Geographic** `Point`, `Ring`, `LineString`, `Polygon`, `MultiLineString`, `MultiPolygon`, `Geometry` `Tuple<double,double>` and arrays thereof; `object` for Geometry| **Wrappers** `Nullable(T)`, `LowCardinality(T)` Unwrapped automatically | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |

Use `ClickHouseDecimal` (from `ClickHouse.Driver.Numerics`) instead of `decimal` when you need the full precision of `Decimal128`/`Decimal256` columns — .NET `decimal` is limited to 28–29 significant digits.

#### Supported LINQ operations[​](#ef-core-linq "Direct link to Supported LINQ operations")

**Queries:** `Where`, `OrderBy`, `Take`, `Skip`, `Select`, `First`, `Single`, `Any`, `Count`, `Distinct`, `AsNoTracking`

**GROUP BY & Aggregates:** `GroupBy` with `Count`, `LongCount`, `Sum`, `Average`, `Min`, `Max` — including `HAVING` (`.Where()` after `.GroupBy()`), multiple aggregates in a single projection, and `OrderBy` on aggregate results.

**String methods:** `Contains`, `StartsWith`, `EndsWith`, `IndexOf`, `Replace`, `Substring`, `Trim`/`TrimStart`/`TrimEnd`, `ToLower`, `ToUpper`, `Length`, `IsNullOrEmpty`, `Concat` (and `+` operator)

**Math functions:** Standard `Math` and `MathF` methods are translated to their ClickHouse equivalents, including arithmetic, logarithmic, trigonometric, and utility functions.

#### Inserting data[​](#ef-core-insert "Direct link to Inserting data")

`SaveChanges` uses the driver's native `InsertBinaryAsync` API — RowBinary encoding with GZip compression, far more efficient than parameterized SQL:

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

Entities transition from `Added` to `Unchanged` after save, just like any other EF Core provider.

**Batch size** is configurable (default 1000):

```
optionsBuilder.UseClickHouse("Host=localhost", o => o.MaxBatchSize(5000));
```

#### Bulk insert[​](#ef-core-bulk-insert "Direct link to Bulk insert")

For high-throughput loads, use `BulkInsertAsync` instead of `SaveChanges`. This is an extension method on `DbContext` that bypasses EF Core's change tracker, identity resolution, and state management entirely — it calls the driver's `InsertBinaryAsync` directly with RowBinary encoding and GZip compression.

This makes it suitable for loading large datasets where you don't need entity tracking after insert:

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

The input can be any `IEnumerable<T>` — it streams through the entities without loading them all into memory. The return value is the number of rows inserted. Entities are **not** attached to the `DbContext` after insert, so there is no `Added` → `Unchanged` state transition.

#### Enums[​](#ef-core-enums "Direct link to Enums")

ClickHouse `Enum8`/`Enum16` columns can be mapped as `string` properties or as C# `enum` types. When using C# enums, the provider automatically converts between the enum and its string representation:

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

#### Custom type conversions[​](#ef-core-value-converters "Direct link to Custom type conversions")

EF Core's `ValueConverter` system lets you map custom types to types the provider already supports. The provider never sees your custom type — EF Core converts at the boundary.

**Per-property conversion:**

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

**Reusable converter class:**

```
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>();
}
```

#### Column type annotations[​](#ef-core-column-types "Direct link to Column type annotations")