Skip to main content

Command Palette

Search for a command to run...

Entity Framework Core - Access Views

Published
•5 min read•View as Markdown

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.

  1. Run dotnet ef migrations add AddUserOrderView

  2. Open the generated migration file.

  3. Inject the SQL into the Up and Down methods.

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 UPDATE command. This is generally not recommended; update the underlying tables instead.


5. Best Practices Checklist

  1. Prefix Naming: Always prefix your Views (e.g., vw_ or View_) in the database to distinguish them from tables, but keep the C# Class name clean (e.g., UserOrderSummary, not VwUserOrderSummary).

  2. 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.

  3. 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.

  4. 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.

  5. Nullable Reference Types: Be strict. If a column in the View is result of a LEFT JOIN in SQL, it might be NULL. Ensure your C# property is nullable (int?, string?) to avoid runtime crashes.

More from this blog

E

EF Core

31 posts