Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@ samples/Eftdb.Samples.DatabaseFirst/**/*.cs
# AI
CLAUDE.md
.claude
tmpclaude-*

# Code coverage report
coverage
Expand Down
38 changes: 19 additions & 19 deletions samples/Eftdb.Samples.DatabaseFirst/README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
# EF Core Database-First Example with TimescaleDB
# EF Core Database-First Example with TimescaleDB

This project demonstrates how to use the **Database-First** approach with [TimescaleDB](https://www.timescale.com/) using the `CmdScale.EntityFrameworkCore.TimescaleDB` package.

---

## 📦 Required NuGet Packages
## Required NuGet Packages

Ensure the following package is installed in your project:

- `CmdScale.EntityFrameworkCore.TimescaleDB.Design`

---

## 🛠️ Scaffold DbContext and Models
## Scaffold DbContext and Models

Use the following command to scaffold the `DbContext` and entity classes from an existing TimescaleDB database:

```bash
dotnet ef dbcontext scaffold
"Host=localhost;Database=cmdscale-ef-timescaledb;Username=timescale_admin;Password=R#!kro#GP43ra8Ae"
CmdScale.EntityFrameworkCore.TimescaleDB.Design
--output-dir Models
--schema public
--context-dir .
--context MyTimescaleDbContext
--project CmdScale.EntityFrameworkCore.TimescaleDB.Example.DataAccess.DbFirst
dotnet ef dbcontext scaffold \
"Host=localhost;Database=cmdscale-ef-timescaledb;Username=timescale_admin;Password=R#!kro#GP43ra8Ae" \
CmdScale.EntityFrameworkCore.TimescaleDB.Design \
--output-dir Models \
--schema public \
--context-dir . \
--context MyTimescaleDbContext \
--project samples/Eftdb.Samples.DatabaseFirst
```

This command will:
Expand All @@ -37,20 +37,20 @@ This command will:

---

## 📁 Project Structure
## Project Structure

```text
CmdScale.EntityFrameworkCore.TimescaleDB.Example.DataAccess.DbFirst/
├── Models/ # Auto-generated entity models
└── MyTimescaleDbContext.cs # Auto-generated DbContext
samples/Eftdb.Samples.DatabaseFirst/
|
+-- Models/ # Auto-generated entity models
+-- MyTimescaleDbContext.cs # Auto-generated DbContext
```

---

## 🐳 Docker
## Docker

- A `docker-compose.yml` file is available in the **Solution Items** to spin up a TimescaleDB container for local development:
- A `docker-compose.yml` file is available at the repository root to spin up a TimescaleDB container for local development:

```bash
docker-compose up -d
Expand All @@ -60,7 +60,7 @@ CmdScale.EntityFrameworkCore.TimescaleDB.Example.DataAccess.DbFirst/

---

## 📚 Resources
## Resources

- [Entity Framework Core Documentation](https://learn.microsoft.com/en-us/ef/core/)
- [TimescaleDB Documentation](https://docs.timescale.com/)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using CmdScale.EntityFrameworkCore.TimescaleDB.Abstractions;
using CmdScale.EntityFrameworkCore.TimescaleDB.Configuration.ContinuousAggregate;
using CmdScale.EntityFrameworkCore.TimescaleDB.Configuration.ContinuousAggregatePolicy;
using CmdScale.EntityFrameworkCore.TimescaleDB.Example.DataAccess.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
Expand All @@ -18,7 +19,9 @@ public void Configure(EntityTypeBuilder<TradeAggregate> builder)
.AddGroupByColumn(x => x.Exchange)
.AddGroupByColumn("1, 2")
.Where("\"ticker\" = 'MCRS'")
.MaterializedOnly();
.MaterializedOnly()
.WithRefreshPolicy(startOffset: "7 days", endOffset: "1 hour", scheduleInterval: "1 hour")
.WithRefreshNewestFirst(true);
}
}
}
6 changes: 6 additions & 0 deletions samples/Eftdb.Samples.Shared/Models/WeatherAggregate.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using CmdScale.EntityFrameworkCore.TimescaleDB.Abstractions;
using CmdScale.EntityFrameworkCore.TimescaleDB.Configuration.ContinuousAggregate;
using CmdScale.EntityFrameworkCore.TimescaleDB.Configuration.ContinuousAggregatePolicy;
using Microsoft.EntityFrameworkCore;

namespace CmdScale.EntityFrameworkCore.TimescaleDB.Example.DataAccess.Models
Expand All @@ -18,6 +19,11 @@ namespace CmdScale.EntityFrameworkCore.TimescaleDB.Example.DataAccess.Models
MaterializedOnly = false,
Where = "\"temperature\" > -50 AND \"humidity\" >= 0")]
[TimeBucket("1 day", nameof(WeatherData.Time), GroupBy = true)]
[ContinuousAggregatePolicy(
StartOffset = "30 days",
EndOffset = "1 day",
ScheduleInterval = "1 hour",
RefreshNewestFirst = true)]
public class WeatherAggregate
{
// Avg aggregate function
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using CmdScale.EntityFrameworkCore.TimescaleDB.Configuration.ContinuousAggregatePolicy;
using Microsoft.EntityFrameworkCore.Scaffolding.Metadata;
using static CmdScale.EntityFrameworkCore.TimescaleDB.Design.Scaffolding.ContinuousAggregatePolicyScaffoldingExtractor;

namespace CmdScale.EntityFrameworkCore.TimescaleDB.Design.Scaffolding
{
/// <summary>
/// Applies continuous aggregate policy annotations to scaffolded database views.
/// </summary>
public sealed class ContinuousAggregatePolicyAnnotationApplier : IAnnotationApplier
{
public void ApplyAnnotations(DatabaseTable table, object featureInfo)
{
if (featureInfo is not ContinuousAggregatePolicyInfo info)
{
throw new ArgumentException($"Expected {nameof(ContinuousAggregatePolicyInfo)}, got {featureInfo.GetType().Name}", nameof(featureInfo));
}

// Mark that this continuous aggregate has a refresh policy
table[ContinuousAggregatePolicyAnnotations.HasRefreshPolicy] = true;

// Apply start_offset and end_offset
if (!string.IsNullOrWhiteSpace(info.StartOffset))
{
table[ContinuousAggregatePolicyAnnotations.StartOffset] = info.StartOffset;
}

if (!string.IsNullOrWhiteSpace(info.EndOffset))
{
table[ContinuousAggregatePolicyAnnotations.EndOffset] = info.EndOffset;
}

// Apply schedule_interval
if (!string.IsNullOrWhiteSpace(info.ScheduleInterval))
{
table[ContinuousAggregatePolicyAnnotations.ScheduleInterval] = info.ScheduleInterval;
}

// Apply initial_start
if (info.InitialStart.HasValue)
{
table[ContinuousAggregatePolicyAnnotations.InitialStart] = info.InitialStart.Value;
}

// Apply include_tiered_data (only if not null - it's an optional parameter)
if (info.IncludeTieredData.HasValue)
{
table[ContinuousAggregatePolicyAnnotations.IncludeTieredData] = info.IncludeTieredData.Value;
}

// Apply buckets_per_batch (only if different from default value of 1)
if (info.BucketsPerBatch.HasValue && info.BucketsPerBatch.Value != 1)
{
table[ContinuousAggregatePolicyAnnotations.BucketsPerBatch] = info.BucketsPerBatch.Value;
}

// Apply max_batches_per_execution (only if different from default value of 0)
if (info.MaxBatchesPerExecution.HasValue && info.MaxBatchesPerExecution.Value != 0)
{
table[ContinuousAggregatePolicyAnnotations.MaxBatchesPerExecution] = info.MaxBatchesPerExecution.Value;
}

// Apply refresh_newest_first (only if different from default value of true)
if (info.RefreshNewestFirst.HasValue && !info.RefreshNewestFirst.Value)
{
table[ContinuousAggregatePolicyAnnotations.RefreshNewestFirst] = info.RefreshNewestFirst.Value;
}
}
}
}
Loading