EF.CH
0.0.12
See the version list below for details.
dotnet add package EF.CH --version 0.0.12
NuGet\Install-Package EF.CH -Version 0.0.12
<PackageReference Include="EF.CH" Version="0.0.12" />
<PackageVersion Include="EF.CH" Version="0.0.12" />
<PackageReference Include="EF.CH" />
paket add EF.CH --version 0.0.12
#r "nuget: EF.CH, 0.0.12"
#:package EF.CH@0.0.12
#addin nuget:?package=EF.CH&version=0.0.12
#tool nuget:?package=EF.CH&version=0.0.12
EF.CH - Entity Framework Core Provider for ClickHouse
An Entity Framework Core provider for ClickHouse, built on the ClickHouse.Driver ADO.NET driver.
Features
- LINQ to ClickHouse SQL - Full query translation with ClickHouse-specific optimizations
- MergeTree Engine Family - MergeTree, ReplacingMergeTree, SummingMergeTree, AggregatingMergeTree, CollapsingMergeTree
- Rich Type System - Arrays, Maps, Tuples, Nested types, Enums, IPv4/IPv6, DateTime64
- Materialized Views - LINQ-based and raw SQL definitions
- Projections - Pre-sorted and pre-aggregated table-level optimizations
- EF Core Migrations - DDL generation with ClickHouse-specific clauses
- DELETE Support - Lightweight and mutation-based strategies
- Dictionaries - In-memory key-value stores with dictGet translation
- External Entities - Query PostgreSQL, MySQL, Redis, and ODBC sources via table functions
- Scaffolding - Reverse engineering with C# enum generation
- Compression Codecs - Per-column compression via fluent API and attributes
- Window Functions - Row numbering, ranking, lag/lead, running totals with fluent API
- Data Skipping Indices - Minmax, bloom filter, token/ngram bloom filters, and set indices
Quick Start
// 1. Install the package
// dotnet add package EF.CH
// 2. Create your entity
public class Order
{
public Guid Id { get; set; }
public DateTime OrderDate { get; set; }
public string CustomerId { get; set; } = string.Empty;
public decimal Total { get; set; }
}
// 3. Create your DbContext
public class MyDbContext : DbContext
{
public DbSet<Order> Orders => Set<Order>();
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseClickHouse("Host=localhost;Database=mydb");
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>(entity =>
{
entity.HasKey(e => e.Id);
entity.UseMergeTree(x => new { x.OrderDate, x.Id }); // Required: ORDER BY
entity.HasPartitionByMonth(x => x.OrderDate); // Optional: partitioning
});
}
}
// 4. Use it
await using var context = new MyDbContext();
await context.Database.EnsureCreatedAsync();
context.Orders.Add(new Order
{
Id = Guid.NewGuid(),
OrderDate = DateTime.UtcNow,
CustomerId = "customer-123",
Total = 99.99m
});
await context.SaveChangesAsync();
var recentOrders = await context.Orders
.Where(o => o.OrderDate > DateTime.UtcNow.AddDays(-7))
.ToListAsync();
Installation
dotnet add package EF.CH
Requirements:
- .NET 10.0+
- ClickHouse 22.0+
- EF Core 10.0
ClickHouse Concepts for EF Core Developers
If you're coming from SQL Server or PostgreSQL, ClickHouse works differently. Understanding these differences is essential:
Every Table Needs an ENGINE
Unlike SQL Server where tables "just work", ClickHouse requires you to specify a table engine. The MergeTree family is most common:
// This is REQUIRED - there's no default engine
entity.UseMergeTree(x => new { x.OrderDate, x.Id });
No ACID Transactions
ClickHouse uses eventual consistency. SaveChanges() batches INSERTs but there's no rollback on failure. Design your application accordingly.
No Row-Level UPDATE
ClickHouse doesn't support efficient UPDATE statements. Attempting to update an entity throws NotSupportedException. Instead:
- Use
ReplacingMergeTreewith a version column for "last write wins" semantics - Use delete-and-reinsert patterns for infrequent updates
Batch-Oriented, Not Row-at-a-Time
ClickHouse is optimized for bulk inserts (thousands of rows). Single-row inserts work but aren't efficient. Batch your writes when possible.
No Auto-Increment
There's no IDENTITY or auto-increment. Use Guid or application-generated IDs:
public Guid Id { get; set; } = Guid.NewGuid();
No Foreign Key Enforcement
ClickHouse doesn't enforce referential integrity. Foreign keys are your application's responsibility.
Table Engines
Choose the right engine for your use case:
| Engine | Use Case | Configuration |
|---|---|---|
| MergeTree | General purpose, append-only | entity.UseMergeTree(x => x.Id) |
| ReplacingMergeTree | Deduplication by key with version | entity.UseReplacingMergeTree(x => x.Version, x => x.Id) |
| SummingMergeTree | Auto-sum numeric columns | entity.UseSummingMergeTree(x => new { x.Date, x.ProductId }) |
| AggregatingMergeTree | Pre-aggregated state | entity.UseAggregatingMergeTree(x => x.Key) |
| CollapsingMergeTree | Row cancellation with sign | entity.UseCollapsingMergeTree(x => x.Sign, x => x.Key) |
| VersionedCollapsingMergeTree | Out-of-order row cancellation | entity.UseVersionedCollapsingMergeTree(x => x.Sign, x => x.Version, x => x.Key) |
See docs/engines/ for detailed documentation on each engine.
Type Mappings
| .NET Type | ClickHouse Type |
|---|---|
int, long, short, sbyte |
Int32, Int64, Int16, Int8 |
uint, ulong, ushort, byte |
UInt32, UInt64, UInt16, UInt8 |
float, double |
Float32, Float64 |
decimal |
Decimal(18, 4) |
string |
String |
bool |
Bool |
Guid |
UUID |
DateTime |
DateTime64(3) |
DateTimeOffset |
DateTime64(3) with timezone |
DateOnly |
Date |
TimeOnly |
Time |
T[], List<T> |
Array(T) |
Dictionary<K,V> |
Map(K, V) |
enum |
Enum8 or Enum16 (auto-selected) |
See docs/types/ for the complete type mapping reference.
Key Differences from SQL Server/PostgreSQL
| Feature | SQL Server/PostgreSQL | ClickHouse |
|---|---|---|
| Transactions | Full ACID | Eventual consistency |
| UPDATE | Efficient row updates | Not supported - use ReplacingMergeTree |
| DELETE | Immediate | Lightweight (marks) or mutation (async rewrite) |
| Auto-increment | IDENTITY, SERIAL |
Not available - use UUID |
| Foreign Keys | Enforced constraints | Application-level only |
| Indexes | B-tree, hash, etc. | Primary key (ORDER BY) + skip indices |
| Insert Pattern | Row-at-a-time OK | Batch thousands of rows |
| Use Case | OLTP | OLAP/Analytics |
Table Options
// Partitioning - improves query performance and data management
entity.HasPartitionByMonth(x => x.CreatedAt); // PARTITION BY toYYYYMM()
entity.HasPartitionByDay(x => x.EventDate); // PARTITION BY toYYYYMMDD()
// TTL - automatic data expiration
entity.HasTtl("CreatedAt + INTERVAL 90 DAY");
// Sampling - for approximate queries on large datasets
entity.HasSampleBy("intHash32(UserId)");
DELETE Operations
// Via change tracker (lightweight delete by default)
var entity = await context.Orders.FindAsync(id);
context.Orders.Remove(entity);
await context.SaveChangesAsync();
// Bulk delete
await context.Orders
.Where(o => o.OrderDate < cutoffDate)
.ExecuteDeleteAsync();
// Configure mutation-based delete
options.UseClickHouse("...", o => o.UseDeleteStrategy(ClickHouseDeleteStrategy.Mutation));
Window Functions
using EF.CH.Extensions;
var analytics = context.Orders.Select(o => new
{
o.Id,
// Lambda style (recommended) - no .Value needed
RowNum = Window.RowNumber(w => w
.PartitionBy(o.Region)
.OrderBy(o.OrderDate)),
PrevAmount = Window.Lag(o.Amount, 1, w => w
.OrderBy(o.OrderDate)),
RunningTotal = Window.Sum(o.Amount, w => w
.PartitionBy(o.Region)
.OrderBy(o.OrderDate)
.Rows().UnboundedPreceding().CurrentRow())
});
Available Functions: RowNumber, Rank, DenseRank, PercentRank, NTile, Lag, Lead, FirstValue, LastValue, NthValue, Sum, Avg, Count, Min, Max
See docs/features/window-functions.md for full documentation including fluent API style.
Data Skipping Indices
Skip indices allow ClickHouse to skip reading granules that don't match query predicates, dramatically improving query performance for selective filters.
modelBuilder.Entity<LogEvent>(entity =>
{
entity.UseMergeTree(x => new { x.Timestamp, x.Id });
// Minmax for datetime range queries
entity.HasIndex(x => x.Timestamp)
.UseMinmax()
.HasGranularity(4);
// Bloom filter for array membership (has(Tags, 'error'))
entity.HasIndex(x => x.Tags)
.UseBloomFilter(falsePositive: 0.025)
.HasGranularity(3);
// Token bloom filter for log search (LIKE '%exception%')
entity.HasIndex(x => x.Message)
.UseTokenBF(size: 10240, hashes: 3, seed: 0)
.HasGranularity(4);
// Set index for low-cardinality columns
entity.HasIndex(x => x.Status)
.UseSet(maxRows: 100)
.HasGranularity(2);
});
Or use attributes:
public class LogEvent
{
[MinMaxIndex(Granularity = 4)]
public DateTime Timestamp { get; set; }
[BloomFilterIndex(FalsePositive = 0.025, Granularity = 3)]
public string[] Tags { get; set; } = [];
[TokenBFIndex(Granularity = 4)]
public string Message { get; set; } = string.Empty;
[SetIndex(MaxRows = 100, Granularity = 2)]
public string Status { get; set; } = string.Empty;
}
Index Types:
| Type | Use Case |
|---|---|
UseMinmax() |
Numeric/datetime range queries |
UseBloomFilter(fpp) |
Exact matching, array membership |
UseTokenBF(...) |
Tokenized text search (logs, URLs) |
UseNgramBF(...) |
Fuzzy/substring text matching |
UseSet(maxRows) |
Low-cardinality exact matching |
See docs/features/skip-indices.md for full documentation.
Materialized Views
// LINQ-based (type-safe)
modelBuilder.Entity<HourlySummary>(entity =>
{
entity.UseSummingMergeTree(x => new { x.Hour, x.ProductId });
entity.AsMaterializedView<HourlySummary, Order>(
query: orders => orders
.GroupBy(o => new { Hour = o.OrderDate.Date, o.ProductId })
.Select(g => new HourlySummary
{
Hour = g.Key.Hour,
ProductId = g.Key.ProductId,
OrderCount = g.Count(),
TotalRevenue = g.Sum(o => o.Total)
}),
populate: false);
});
Projections
Projections are table-level optimizations stored alongside the main table data. Unlike materialized views, projections are not separately queryable - the query optimizer automatically uses them when beneficial.
// Sort-order projection - auto-named: orders__prj_ord__customer_id__order_date
entity.HasProjection()
.OrderBy(x => x.CustomerId)
.ThenBy(x => x.OrderDate)
.Build();
// Aggregation projection - explicit name, anonymous type
entity.HasProjection("daily_stats")
.GroupBy(x => x.OrderDate.Date)
.Select(g => new {
Date = g.Key,
TotalAmount = g.Sum(o => o.Amount),
OrderCount = g.Count()
})
.Build();
// ClickHouse-specific aggregates (uniq, argMax, quantile, etc.)
entity.HasProjection("advanced_stats")
.GroupBy(x => x.Date)
.Select(g => new {
Date = g.Key,
UniqueUsers = ClickHouseAggregates.Uniq(g, o => o.UserId),
TopProduct = ClickHouseAggregates.ArgMax(g, o => o.ProductId, o => o.Revenue)
})
.Build();
See docs/features/projections.md for full documentation including all ClickHouse aggregate functions.
Dictionaries
ClickHouse dictionaries are in-memory key-value stores for fast lookups:
// Define dictionary entity with marker interface
public class CountryLookup : IClickHouseDictionary
{
public ulong Id { get; set; }
public string Name { get; set; } = string.Empty;
}
// Configure in OnModelCreating
entity.AsDictionary<CountryLookup, Country>(cfg => cfg
.HasKey(x => x.Id)
.FromTable()
.UseHashedLayout()
.HasLifetime(300));
// Use in LINQ queries - translates to dictGet()
var orders = db.Orders
.Select(o => new {
o.Id,
CountryName = db.CountryDict.Get(o.CountryId, c => c.Name)
});
Layouts: Flat, Hashed, ComplexKeyHashed, Cache, Direct
External Entities
Query remote databases directly through ClickHouse table functions:
// Define external entity (keyless - uses table function, not a ClickHouse table)
public class ExternalCustomer
{
public int id { get; set; }
public string name { get; set; } = string.Empty;
public string email { get; set; } = string.Empty;
}
// Configure in OnModelCreating
modelBuilder.ExternalPostgresEntity<ExternalCustomer>(ext => ext
.FromTable("customers", schema: "public")
.Connection(c => c
.HostPort(env: "PG_HOST")
.Database(env: "PG_DATABASE")
.Credentials("PG_USER", "PG_PASSWORD")));
// Query like any other entity - generates postgresql() table function
var customers = await context.ExternalCustomers
.Where(c => c.name.StartsWith("A"))
.ToListAsync();
// JOIN with native ClickHouse tables
var orderSummary = await context.Orders
.Join(context.ExternalCustomers, o => o.CustomerId, c => c.id,
(o, c) => new { c.name, o.Amount })
.ToListAsync();
Supported Providers:
| Provider | Extension Method | Use Case |
|---|---|---|
| PostgreSQL | ExternalPostgresEntity<T>() |
Direct credentials |
| MySQL | ExternalMySqlEntity<T>() |
REPLACE INTO, ON DUPLICATE KEY |
| ODBC | ExternalOdbcEntity<T>() |
SQL Server, Oracle via DSN |
| Redis | ExternalRedisEntity<T>() |
Key-value with auto-generated schema |
See docs/features/external-entities.md for detailed configuration.
Documentation
| Topic | Description |
|---|---|
| Getting Started | Installation and first project |
| ClickHouse Concepts | Key differences from RDBMS |
| Table Engines | MergeTree family guide |
| Type Mappings | Complete type reference |
| Features | Materialized views, partitioning, TTL, etc. |
| Projections | Table-level sort and aggregation optimizations |
| Compression Codecs | Per-column compression configuration |
| Window Functions | Ranking, lead/lag, running totals |
| Data Skipping Indices | Bloom filter, minmax, set, and token indices |
| External Entities | Query remote PostgreSQL, MySQL, Redis, ODBC |
| Migrations | EF Core migrations with ClickHouse |
| Scaffolding | Reverse engineering |
| Limitations | What doesn't work |
Samples
| Sample | Description |
|---|---|
| QuickStartSample | Minimal working example |
| MigrationSample | EF Core migrations |
| KeylessSample | Keyless entities for append-only data |
| ReplacingMergeTreeSample | Deduplication patterns |
| SummingMergeTreeSample | Auto-aggregation with SummingMergeTree |
| CollapsingMergeTreeSample | Row cancellation with sign column |
| MaterializedViewSample | Real-time aggregation |
| ArrayTypeSample | Working with arrays |
| MapTypeSample | Working with Map(K, V) dictionaries |
| EnumTypeSample | ClickHouse enum type mapping |
| PartitioningSample | Table partitioning strategies |
| QueryModifiersSample | Final(), Sample(), WithSettings() |
| DeleteStrategiesSample | Lightweight vs mutation deletes |
| OptimizeTableSample | Programmatic OPTIMIZE TABLE |
| DictionarySample | In-memory dictionary lookups |
| DictionaryJoinSample | Dictionaries as JOIN replacement |
| ExternalPostgresSample | Query PostgreSQL from ClickHouse |
| ExternalRedisSample | Redis key-value integration |
License
MIT License - see LICENSE for details.
Acknowledgments
- ClickHouse.Driver - The ADO.NET driver this provider builds on
- EntityFrameworkCore.ClickHouse - Reference implementation
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- ClickHouse.Driver (>= 0.9.0)
- Microsoft.EntityFrameworkCore.Design (>= 10.0.0)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 10.0.4 | 102 | 3/29/2026 |
| 9.0.5 | 95 | 3/26/2026 |
| 8.1.1 | 89 | 3/26/2026 |
| 8.1.0 | 97 | 2/24/2026 |
| 8.0.9 | 96 | 2/17/2026 |
| 8.0.8 | 113 | 1/30/2026 |
| 8.0.7 | 114 | 1/8/2026 |
| 8.0.6 | 110 | 1/8/2026 |
| 8.0.5 | 123 | 1/7/2026 |
| 8.0.4 | 112 | 1/7/2026 |
| 8.0.3 | 108 | 1/6/2026 |
| 0.0.25 | 109 | 1/8/2026 |
| 0.0.24 | 111 | 1/7/2026 |
| 0.0.23 | 106 | 1/7/2026 |
| 0.0.22 | 114 | 1/6/2026 |
| 0.0.21 | 103 | 1/6/2026 |
| 0.0.12 | 132 | 12/20/2025 |