Entity Framework Core - Access Views
Mapping a database View in Entity Framework Core is a powerful pattern for reading complex data projections without cluttering your C# code with massive LINQ queries.
In modern EF Core (versions 5 through 9), Views are treated as Keyless Entity Types.
Here is the complete guide, covering implementation, migrations, corner cases, and best practices.
1. The Core Concept: Keyless Entities
Unlike a standard SQL Table, a View often does not have a Primary Key. Therefore, EF Core treats them differently:
They are Read-Only (you cannot call
.Add(),.Update(), or.Delete()on them).They are Not Tracked by the ChangeTracker (improving performance).
They must be configured with
.HasNoKey().
2. Step-by-Step Implementation
Step A: Define the Class (The Model)
Create a POCO class that matches the columns of your view.
Copy
Copy
public class UserOrderSummary
{
public string CustomerName { get; set; }
public string Email { get; set; }
public int TotalOrders { get; set; }
public decimal TotalSpent { get; set; }
}
Step B: Configure the DbContext
In your DbContext, you must register the entity and tell EF Core it maps to a View.
Copy
Copy
public class AppDbContext : DbContext
{
public DbSet<UserOrderSummary> UserOrderSummaries { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<UserOrderSummary>(eb =>
{
eb.HasNoKey(); // CRITICAL: Tells EF this has no PK
eb.ToView("vw_UserOrderSummary"); // Map to specific DB View name
// Optional: Map columns if C# property names differ from SQL
eb.Property(v => v.CustomerName).HasColumnName("Customer_Name");
});
}
}
Step C: Creating the View (Migrations)
This is the most common point of failure. EF Core does not automatically generate the SQL to create a View. You must write the SQL manually in the migration.
Run
dotnet ef migrations add AddUserOrderViewOpen the generated migration file.
Inject the SQL into the
UpandDownmethods.
Copy
Copy
public partial class AddUserOrderView : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"
CREATE OR ALTER VIEW vw_UserOrderSummary AS
SELECT
u.Name as Customer_Name,
u.Email,
COUNT(o.Id) as TotalOrders,
SUM(o.Amount) as TotalSpent
FROM Users u
JOIN Orders o ON u.Id = o.UserId
GROUP BY u.Name, u.Email
");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("DROP VIEW IF EXISTS vw_UserOrderSummary");
}
}
3. Usage & Querying
You treat the View exactly like a normal DbSet.
Copy
Copy
// Standard LINQ
var heavySpenders = await _context.UserOrderSummaries
.Where(x => x.TotalSpent > 1000)
.OrderByDescending(x => x.TotalSpent)
.ToListAsync();
Note: EF Core will translate this into SELECT ... FROM vw_UserOrderSummary WHERE .... The filtering happens in the database, which is highly efficient.
4. Advanced Corner Cases
Case 1: Views with Relationships (Navigation Properties)
Can a View have a relationship to a Table? Yes. If your view includes a Foreign Key column (e.g., UserId), you can map it back to the User table.
Copy
Copy
public class UserOrderSummary
{
public int UserId { get; set; } // The FK inside the view
public decimal TotalSpent { get; set; }
// Navigation Property
public User User { get; set; }
}
Configuration:
Copy
Copy
modelBuilder.Entity<UserOrderSummary>(eb =>
{
eb.HasNoKey().ToView("vw_UserOrderSummary");
// Config the relationship (One-to-Many logic usually applies)
eb.HasOne(v => v.User)
.WithMany() // Or WithMany(u => u.Summaries)
.HasForeignKey(v => v.UserId);
});
- Caveat: You can generally only read from the View to the Table. Including the View as a child collection of a Table is possible but can get complex with tracking.
Case 2: "Keyed" Views (The Hack)
If your View guarantees a unique row (e.g., it includes the ID of the base table), you can cheat and map it as a normal entity by removing HasNoKey().
Benefit: EF Core can track the entity.
Risk: If the DB View returns duplicate IDs, EF Core will throw a runtime error.
Why do this? If you want to use the view as a strictly defined relationship target for a normal entity.
Case 3: Parameterized Views (Not Supported)
SQL Views do not accept parameters.
Bad Approach: Trying to force parameters into
ToView.Solution: Use Table Valued Functions (TVF) or
FromSqlInterpolated:Copy
Copy
var result = await _context.UserOrderSummaries .FromSqlInterpolated($"SELECT * FROM vw_UserOrderSummary WHERE TotalSpent > {minAmount}") .ToListAsync();
Case 4: Updating a View
By default, HasNoKey entities are read-only. _context.SaveChanges() will do nothing for them.
If you MUST update: You need an INSTEAD OF trigger on the SQL View in the database.
EF Core Side: You would have to map the View as a normal entity (with a Key) for EF to even attempt the
UPDATEcommand. This is generally not recommended; update the underlying tables instead.
5. Best Practices Checklist
Prefix Naming: Always prefix your Views (e.g.,
vw_orView_) in the database to distinguish them from tables, but keep the C# Class name clean (e.g.,UserOrderSummary, notVwUserOrderSummary).Filter Early: Even though Views are efficient, always apply
.Where()in your C# code. Do not pull.ToList()before filtering, or you will pull the entire view into memory.Schema Binding: When creating the View in SQL, consider using
WITH SCHEMABINDING. This prevents the underlying tables from being dropped or altered in a way that breaks the view.Indexed Views: If performance is slow on a View with heavy aggregation (
SUM,COUNT), add a Unique Clustered Index to the View in the database. EF Core doesn't need to know about this; it just benefits from the speed.Nullable Reference Types: Be strict. If a column in the View is result of a
LEFT JOINin SQL, it might be NULL. Ensure your C# property is nullable (int?,string?) to avoid runtime crashes.

