Code-first view management for Entity Framework Core. Define, version, track, and migrate database views using the same workflow as tables.
- Features
- Packages
- Quick Start
- SQL File Directives
- Materialized Views
- Drift Detection
- Architecture
- Documentation
- Requirements
- Contributing
- License
- SQL file definitions -- Define views in embedded
.sqlfiles with metadata directives - Fluent API definitions -- Define views in C# using
modelBuilder.HasManagedView() - Change detection -- SHA256 hashing with SQL normalization prevents formatting-only changes from triggering migrations
- Snapshot-based tracking -- Design-time snapshot file (similar to EF Core's ModelSnapshot) for CI/CD workflows
- Runtime tracking --
__ManagedViewsHistorytable for production drift detection - Dependency ordering -- Topological sort ensures views are created/dropped in the correct order
- Materialized views -- Full support for PostgreSQL materialized views with index and refresh management
- Migration integration -- Custom operations (
CreateManagedViewOperation,DropManagedViewOperation) integrate into EF Core migrations
| Package | Description | NuGet |
|---|---|---|
EntityFrameworkCore.ManagedViews |
Core abstractions, diffing, hashing, snapshot, discovery | |
EntityFrameworkCore.ManagedViews.PostgreSQL |
PostgreSQL provider with materialized view support |
dotnet add package EntityFrameworkCore.ManagedViews.PostgreSQLAdd .sql files to a Views/ folder and mark them as embedded resources:
<ItemGroup>
<EmbeddedResource Include="Views/**/*.sql" />
</ItemGroup>-- Views/vw_active_products.sql
-- @viewName: vw_active_products
-- @schema: public
-- @type: view
SELECT p.id, p.name, p.price, c.name AS category_name
FROM products p
INNER JOIN categories c ON p.category_id = c.id
WHERE p.is_active = trueprotected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasManagedView("vw_active_products", v => v
.InSchema("public")
.AsSql("SELECT id, name, price FROM products WHERE is_active = true"));
}modelBuilder.Entity<ActiveProductView>(b =>
{
b.HasNoKey();
b.ToManagedView("vw_active_products");
});services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(connectionString)
.UseNpgsqlManagedViews());| Directive | Description | Example |
|---|---|---|
@viewName |
Name of the view (inferred from filename if omitted) | -- @viewName: vw_active_products |
@schema |
Database schema (inferred from filename or defaults to public) |
-- @schema: catalog |
@type |
view or materialized |
-- @type: materialized |
@dependsOn |
Comma-separated dependencies | -- @dependsOn: vw_base, vw_other |
@indexes |
Semicolon-separated index defs (materialized views only) | -- @indexes: idx_name(column1); idx_other(col2 DESC) |
See the Implementation Guide for file naming conventions and schema inference.
-- Views/mv_category_stats.sql
-- @viewName: mv_category_stats
-- @type: materialized
-- @dependsOn: vw_active_products
-- @indexes: idx_cat_stats(category_name)
SELECT category_name, COUNT(*) AS product_count, AVG(price) AS avg_price
FROM vw_active_products
GROUP BY category_nameRefresh at runtime:
await context.RefreshMaterializedViewAsync("mv_category_stats");
await context.RefreshAllMaterializedViewsAsync(concurrently: true);Check for drift between tracked views and current definitions:
var report = await context.CheckViewDriftAsync();
if (report.HasDrift)
{
foreach (var entry in report.Entries)
{
Console.WriteLine($"{entry.ViewName}: {entry.DriftType}");
}
}| Drift Type | Meaning |
|---|---|
Missing |
Tracked in __ManagedViewsHistory but removed from source definitions |
Modified |
SQL hash has changed since last applied |
Untracked |
Exists in source definitions but was never applied |
┌──────────────────────────────────────────────────────────────────┐
│ Your Application │
├──────────────────────────────────────────────────────────────────┤
│ DbContext.UseManagedViews() · ModelBuilder.HasManagedView() │
│ MigrationBuilder.CreateManagedView() · ToManagedView<T>() │
├──────────────────────────────────────────────────────────────────┤
│ EntityFrameworkCore.ManagedViews │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │
│ │Discovery │ │ Hashing │ │ Diffing │ │ Dep. Resolution │ │
│ │(SQL/API) │ │ (SHA256) │ │(Snapshot)│ │(Topological Sort) │ │
│ └──────────┘ └──────────┘ └──────────┘ └───────────────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │Snapshot │ │Tracking │ │Migration │ │
│ │ (JSON) │ │ (DB) │ │ (Ops) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
├──────────────────────────────────────────────────────────────────┤
│ EntityFrameworkCore.ManagedViews.PostgreSQL │
│ ┌───────────────────┐ ┌────────────────────────────────┐ │
│ │ PostgreSQL DDL │ │ Migration SQL Generator │ │
│ │ Provider │ │ (Npgsql) │ │
│ └───────────────────┘ └────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────────┤
│ Entity Framework Core │
└──────────────────────────────────────────────────────────────────┘
For a detailed architecture breakdown, see docs/ARCHITECTURE.md.
| Document | Description |
|---|---|
| Implementation Guide | Setup, configuration, API reference, migration integration, refactoring guide |
| Architecture | Component design, data flow, extension points |
| Provider Guide | How to implement IManagedViewProvider for a new database |
| Project Summary | Design decisions, package layout, developer experience goals |
| Changelog | Release history following Keep a Changelog |
| Contributing | How to contribute, development setup, coding standards |
| Security Policy | Reporting vulnerabilities |
| Code of Conduct | Community standards (Contributor Covenant) |
- .NET 10
- Entity Framework Core 10.x
- PostgreSQL (via Npgsql.EntityFrameworkCore.PostgreSQL 10.x)
Contributions are welcome. Please read the Contributing Guide before submitting a pull request.
This project is licensed under the MIT License. See LICENSE for details.