YC.EntityFrameworkCore.TigerData.TimescaleDB
1.1.0
dotnet add package YC.EntityFrameworkCore.TigerData.TimescaleDB --version 1.1.0
NuGet\Install-Package YC.EntityFrameworkCore.TigerData.TimescaleDB -Version 1.1.0
<PackageReference Include="YC.EntityFrameworkCore.TigerData.TimescaleDB" Version="1.1.0" />
<PackageVersion Include="YC.EntityFrameworkCore.TigerData.TimescaleDB" Version="1.1.0" />
<PackageReference Include="YC.EntityFrameworkCore.TigerData.TimescaleDB" />
paket add YC.EntityFrameworkCore.TigerData.TimescaleDB --version 1.1.0
#r "nuget: YC.EntityFrameworkCore.TigerData.TimescaleDB, 1.1.0"
#:package YC.EntityFrameworkCore.TigerData.TimescaleDB@1.1.0
#addin nuget:?package=YC.EntityFrameworkCore.TigerData.TimescaleDB&version=1.1.0
#tool nuget:?package=YC.EntityFrameworkCore.TigerData.TimescaleDB&version=1.1.0
YC.EntityFrameworkCore.TigerData.TimescaleDB
First-class TimescaleDB support for Entity Framework Core, layered on top of the Npgsql EF Core provider.
Configure hypertables, columnstore compression, continuous aggregates, retention/reorder policies and background
jobs with the Fluent API or attributes — everything flows through standard EF Core migrations (snapshot-diff,
fully bidirectional, no imperative commands). Query with TimescaleDB hyperfunctions (time_bucket, first/last,
gapfilling, histograms, UUIDv7) directly from LINQ, and reverse-engineer existing schemas with
dotnet ef dbcontext scaffold.
Requirements: EF Core 10,
Npgsql.EntityFrameworkCore.PostgreSQL10, TimescaleDB 2.23+. The modern declarative interface is used (create_hypertable,ALTER TABLE … SET (timescaledb.…)); there is no legacy fallback.
Install
One package — design-time support (migration code generation, scaffolding for dotnet ef) is included:
dotnet add package YC.EntityFrameworkCore.TigerData.TimescaleDB
Setup
Call UseTimescaleDb() after UseNpgsql(...):
services.AddDbContext<MetricsContext>(options => options
.UseNpgsql(connectionString)
.UseTimescaleDb());
| Option | Default | Effect |
|---|---|---|
o.CreateExtension(false) |
true | When true, the first migration emits CREATE EXTENSION IF NOT EXISTS timescaledb;. Disable when the extension is provisioned out of band (e.g. managed cloud). |
o.MigrateData(false) |
true | Model-wide default for migration data operations. Per-entity WithMigrateData(...) overrides it. |
o.RebuildData(false) |
true | Model-wide default — forbid data-copying rebuilds across all entities. Per-entity WithRebuildData(...) overrides it. |
o.AutoDecompress(false) |
true | Model-wide default for chunk decompression on columnstore disable. Per-entity WithAutoDecompress(...) overrides it. |
options.UseNpgsql(cs).UseTimescaleDb(o => o.RebuildData(false)); // disable risky rebuilds everywhere
Intervals
Every duration is expressed type-safely — never a raw string:
| Form | Use it for | Example | Becomes |
|---|---|---|---|
TimeSpan |
Fixed durations (days, hours, minutes, seconds) | TimeSpan.FromDays(7) |
INTERVAL '7 days' |
(int value, Every unit) |
Calendar units a TimeSpan can't express |
(1, Every.Month) |
INTERVAL '1 month' |
long (integer-time hypertables only) |
Raw size in the column's own unit | 86_400 |
86400 |
Every values: Second, Minute, Hour, Day, Week, Month, Year. Use (int, Every) (or its attribute
form) whenever you need Week, Month or Year — those are calendar intervals with no fixed TimeSpan.
Quick start
The same hypertable, configured two ways.
Fluent API:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Reading>(e =>
{
e.HasNoKey(); // raw time-series → keyless
e.ToTable("readings");
e.IsHypertable(x => x.Time, chunkInterval: TimeSpan.FromDays(1));
e.HasSpacePartition(x => x.DeviceId, partitions: 4);
e.HasColumnstore(cs => cs
.SegmentBy(x => x.DeviceId)
.OrderByDescending(x => x.Time));
e.HasColumnstorePolicy(after: TimeSpan.FromDays(7));
e.HasRetentionPolicy(dropAfter: TimeSpan.FromDays(90));
});
}
Attributes (equivalent):
[Columnstore(CompressAfter = 7, CompressAfterUnit = Every.Day)]
[Retention(90, Every.Day)]
public class Reading
{
[PartitionColumn(1, Every.Day)] // declares the hypertable + chunk interval
[OrderBy(0, Sort.Descending)] // columnstore ordering
public DateTimeOffset Time { get; set; }
[SpacePartition(4)]
[SegmentBy] // columnstore segment column
public string DeviceId { get; set; } = null!;
public double Value { get; set; }
}
There is no [Hypertable] attribute — the [PartitionColumn] marker is the hypertable declaration, so
compression / retention / dimensions cannot be written without one.
Features
Every feature is documented in FEATURES.md with its Fluent/attribute syntax, defaults and allowable values. Click through:
| Feature | What it does |
|---|---|
| Hypertable | Partition a table by time into chunks — the core TimescaleDB primitive. |
| Integer-time hypertable | Partition by an integer column (epoch/sequence) with an integer-now function. |
| Space dimensions | Hash (by_hash) or range (by_range) sub-partitioning on extra columns. |
| Tablespaces | Spread chunks across disks/tablespaces, round-robin. |
| Chunk skipping | Min/max range tracking to prune chunks at query time. |
| Columnstore | Native compression with segment-by / order-by layout and sparse indexes. |
| Columnstore policy | Auto-compress chunks older than a data age or creation age. |
| Retention policy | Auto-drop chunks older than a data age or creation age. |
| Reorder policy | Periodically reorder chunks by an index. |
| Continuous aggregates | Incrementally-materialized rollups over a hypertable. |
| Refresh policy | Schedule continuous-aggregate refreshes. |
| Hierarchical caggs | A cagg built on another cagg, ordered automatically. |
| Background jobs | Schedule custom stored procedures via add_job. |
| Migration options | Toggle the automatic data ops (migrate / rebuild / decompress). |
| Hyperfunctions in LINQ | time_bucket, gapfill, first/last, histogram, UUIDv7. |
Migrations — snapshot-driven, no special commands
dotnet ef migrations add records the TimescaleDB configuration as annotations on the table, plus
migrationBuilder.Sql(...) for the objects that aren't tables (the extension, continuous aggregates, jobs,
reorder policies). There are no imperative commands like ConvertToHypertable() or DisableColumnstore().
The provider's SQL generator turns the annotation delta into real DDL when the migration runs. Because the
annotations carry both old and new values, every change is bidirectional automatically — the Down
migration emits the exact reverse with no extra code, and re-running migrations add on an unchanged model
produces an empty migration.
Transition matrix
Changes TimescaleDB cannot apply in place rebuild the table (CREATE TABLE (LIKE … INCLUDING ALL), copy
every row, drop, rename) so data is preserved — nothing is rejected:
| Change in the model | Behavior |
|---|---|
| New table as hypertable | CREATE TABLE … then create_hypertable(...) |
| Existing table → hypertable | create_hypertable(..., migrate_data => true) |
| Hypertable → plain table | rebuild → plain table, data copied back |
| Partition column changed | rebuild → hypertable with the new column |
| Chunk interval changed | set_chunk_time_interval(...) (future chunks) |
| Space dimension added | add_dimension(by_hash(...)) |
| Space dimension removed / changed | rebuild |
| Range dimension added | add_dimension(by_range(...)) |
| Range dimension removed / changed | rebuild |
| Columnstore enabled | ALTER TABLE … SET (timescaledb.enable_columnstore, …) |
| Columnstore disabled | every compressed chunk decompressed, then disabled |
| segmentby / orderby changed | ALTER TABLE … SET (...) (future chunks) |
| Retention / columnstore / reorder policy ± | add_*_policy / remove_*_policy |
| Reorder policy ± | emitted after CREATE INDEX (add) / before drop (remove) |
| Chunk skipping ± | SET timescaledb.enable_chunk_skipping = on; enable/disable_chunk_skipping |
| Cagg query changed | drop + recreate (+ policies re-added, dependents cascaded) |
The automatic, data-heavy steps (migrate_data, the rebuild row-copy, columnstore decompression) can be toggled
per entity or model-wide — see Migration options.
Scaffolding
dotnet ef dbcontext scaffold reverse-engineers hypertables, space dimensions, columnstore settings, retention/
columnstore/reorder policies, chunk skipping, tablespaces and custom jobs. Continuous aggregates are not
reverse-engineered (materialized views are outside the Npgsql scaffolding surface). Columnstore sparse
indexes, creation-time retention/columnstore policies (drop_created_before / created_before) and
range (by_range) dimensions are also not yet reverse-engineered from an existing database — configure
them in the model.
Notes and limitations
- Primary keys on hypertables must include the partition column (TimescaleDB requires every unique constraint to cover the partitioning columns; validated at model-build time).
- Rebuilds are data-heavy and not free of caveats. Un-converting a hypertable, repartitioning, or changing a
space dimension copies every row under lock via
CREATE TABLE (LIKE … INCLUDING ALL). PostgreSQLLIKEdoes not copy FOREIGN KEY constraints (inbound or outbound) and recreates indexes under generated names; the migration includes a warning comment. Re-create FKs and any reorder policy manually afterward. - Chunk interval changes apply to future chunks only (
set_chunk_time_interval); existing chunks keep their interval. Re-chunking historical data is intentionally not performed. - Chunk exclusion is automatic on the partition columns — filtering on the time column or a space
dimension prunes chunks with no configuration. Chunk skipping (
HasChunkSkipping) is the opt-in extension to a non-partition column, and only speeds up compressed chunks (the min/max is gathered when a chunk is converted to the columnstore — enable the columnstore too). Supported types: integer family +date/timestamp/timestamptz. The migration enables thetimescaledb.enable_chunk_skippingGUC for you. - Converting an existing table to a hypertable and disabling the columnstore are data-heavy (row migration / chunk decompression); review the generated migration before running it in production.
- Enabling the columnstore auto-creates a default conversion policy;
HasColumnstorePolicyreplaces it. - Direct Compress (Direct-to-Columnstore) is a session-level ingestion GUC
(
SET timescaledb.enable_direct_compress_insert/copy = on), not a table option — enable it in the session doing the bulkINSERT/COPY. It has no declarative DDL form, so it is outside this package's migration scope (and is a TimescaleDB tech preview).
Building & testing
Prerequisites: the .NET 10 SDK (see global.json) and Docker (the functional tests spin up a
real TimescaleDB via Testcontainers).
dotnet build YC.EntityFrameworkCore.TigerData.TimescaleDB.slnx -c Release
# Fast SQL-generation / model tests (no Docker):
dotnet test test/YC.EntityFrameworkCore.TigerData.TimescaleDB.UnitTests
# End-to-end tests against a real TimescaleDB (Docker required):
dotnet test test/YC.EntityFrameworkCore.TigerData.TimescaleDB.FunctionalTests
The build treats warnings as errors and runs the .NET analyzers — keep it clean.
Contributing
Contributions are welcome. Please read CONTRIBUTING.md first.
- Issues use templates — pick Bug report or Feature request; they apply the right label automatically. Questions go to Discussions.
- Pull requests follow the template and Conventional Commits
in the PR title (
feat:,fix:,docs:, …) — this drives versioning and the changelog. - Releases are automated: Release Please opens a
release PR; merging it tags the version, writes
CHANGELOG.md, and publishes to NuGet via OIDC (no API keys). - Security issues: see SECURITY.md — do not open a public issue.
License
MIT
| 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
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.9)
- Npgsql.EntityFrameworkCore.PostgreSQL (>= 10.0.2)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.