You are viewing a preview of this lesson. Sign in to start learning
Back to ASP.NET with .NET 10

Entity Framework Core

Database access with EF Core and migrations

Entity Framework Core in ASP.NET

Master Entity Framework Core with free flashcards and interactive coding exercises to reinforce your learning. This lesson covers DbContext configuration, LINQ queries, migrations, and relationship mappingβ€”essential skills for building data-driven ASP.NET applications with .NET 10.

Welcome to Entity Framework Core! πŸ’»

Welcome to the world of Entity Framework Core (EF Core)β€”Microsoft's modern, lightweight, and cross-platform object-relational mapper (ORM) for .NET! If you've ever struggled with writing raw SQL queries and manually mapping database results to C# objects, EF Core will feel like a breath of fresh air. It allows you to work with databases using strongly-typed C# code, letting you focus on your business logic rather than database plumbing.

πŸ€” Did you know? Entity Framework Core was completely rewritten from scratch (not just a port of EF6) to be modular, lightweight, and support non-relational data stores. It can work with SQL Server, PostgreSQL, MySQL, SQLite, Cosmos DB, and even in-memory databases for testing!

Core Concepts πŸ”‘

What is Entity Framework Core?

Entity Framework Core is an Object-Relational Mapper (ORM) that bridges the gap between your C# objects (entities) and database tables. Instead of writing SQL queries manually, you write LINQ queries against C# collections, and EF Core translates them into efficient SQL.

🌍 Real-world analogy: Think of EF Core as a skilled translator at an international conference. You speak in C# (your native language), the database speaks SQL (its native language), and EF Core translates between the two seamlessly so you can communicate without learning SQL fluently.

The DbContext: Your Database Gateway πŸšͺ

The DbContext class is the heart of EF Core. It represents a session with the database and provides:

  • DbSet properties that represent tables
  • Change tracking to detect modifications
  • Query translation from LINQ to SQL
  • Transaction management

Here's the structure:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         Your Application            β”‚
β”‚                                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚     Your DbContext           β”‚   β”‚
β”‚  β”‚                              β”‚   β”‚
β”‚  β”‚  DbSet Customers   β”‚   β”‚
β”‚  β”‚  DbSet Orders         β”‚   β”‚
β”‚  β”‚  DbSet Products     β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚             β”‚ EF Core translates    β”‚
β”‚             ↓ LINQ to SQL           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚
              ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         Database (SQL Server)       β”‚
β”‚                                     β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”‚
β”‚  β”‚Customers β”‚  β”‚  Orders  β”‚       β”‚
β”‚  β”‚  Table   β”‚  β”‚   Table  β”‚       β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Entity Classes: Your Data Models πŸ“¦

An entity class is a plain C# class that represents a table in your database. Each property maps to a column:

public class Customer
{
    public int CustomerId { get; set; }  // Primary key
    public string Name { get; set; }
    public string Email { get; set; }
    public DateTime CreatedDate { get; set; }
    
    // Navigation property for relationships
    public List<Order> Orders { get; set; }
}

πŸ’‘ Tip: By convention, EF Core recognizes properties named Id or {ClassName}Id as primary keys automatically!

LINQ Queries: The Power of Strong Typing πŸ”

LINQ (Language Integrated Query) lets you query data using C# syntax. EF Core translates these queries into SQL:

LINQ Query Generated SQL
context.Customers.Where(c => c.Name.StartsWith("A")) SELECT * FROM Customers WHERE Name LIKE 'A%'
context.Orders.OrderBy(o => o.OrderDate) SELECT * FROM Orders ORDER BY OrderDate
context.Products.FirstOrDefault(p => p.Id == 5) SELECT TOP 1 * FROM Products WHERE Id = 5

Migrations: Version Control for Your Database πŸ”„

EF Core migrations track changes to your entity models and apply them to the database:

MIGRATION WORKFLOW

  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ 1. Modify       β”‚
  β”‚    Entity       β”‚ (Add property, change type)
  β”‚    Classes      β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           ↓
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ 2. Create       β”‚  dotnet ef migrations add AddEmail
  β”‚    Migration    β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           ↓
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ 3. Review       β”‚  Check generated Up() and Down() methods
  β”‚    Migration    β”‚
  β”‚    Code         β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           ↓
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ 4. Apply to     β”‚  dotnet ef database update
  β”‚    Database     β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           ↓
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ βœ… Database     β”‚  Schema updated!
  β”‚    Updated      β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Relationship Mapping: Connecting Entities πŸ”—

EF Core supports three types of relationships:

Relationship Example Navigation Properties
One-to-Many Customer β†’ Orders Customer has List<Order>
Order has Customer
Many-to-Many Students ↔ Courses Student has List<Course>
Course has List<Student>
One-to-One User β†’ Profile User has Profile
Profile has User

Foreign keys can be configured explicitly or EF Core will follow conventions:

public class Order
{
    public int OrderId { get; set; }
    public int CustomerId { get; set; }  // Foreign key
    
    public Customer Customer { get; set; }  // Navigation property
}

Loading Strategies: Eager, Lazy, and Explicit πŸ“₯

EF Core offers three ways to load related data:

Strategy Code When Used
Eager Loading .Include(c => c.Orders) Load related data upfront with JOIN
Lazy Loading Automatic (requires proxies) Load related data on first access
Explicit Loading .Collection(c => c.Orders).Load() Manually control when to load

πŸ’‘ Tip: Eager loading with Include() is usually the most efficientβ€”it generates a single query with JOIN rather than multiple round trips!

Tracking vs. No-Tracking Queries πŸ“Š

By default, EF Core tracks entities retrieved from the database to detect changes:

// Tracking (default) - EF Core watches for changes
var customer = context.Customers.FirstOrDefault(c => c.Id == 1);
customer.Email = "newemail@example.com";
context.SaveChanges();  // UPDATE executed

// No-tracking - Better performance for read-only scenarios
var customers = context.Customers
    .AsNoTracking()
    .ToList();  // No change tracking overhead

🧠 Mnemonic: "Tracking = Transformations" (use when you'll modify data)

Detailed Examples 🎯

Example 1: Setting Up DbContext and Entities

Let's build a simple blog system:

using Microsoft.EntityFrameworkCore;

// Entity classes
public class Blog
{
    public int BlogId { get; set; }
    public string Name { get; set; }
    public string Url { get; set; }
    
    public List<Post> Posts { get; set; }  // Navigation property
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public DateTime PublishedDate { get; set; }
    
    public int BlogId { get; set; }  // Foreign key
    public Blog Blog { get; set; }    // Navigation property
}

// DbContext configuration
public class BloggingContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
    
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(
            "Server=(localdb)\\mssqllocaldb;Database=BloggingDb;Trusted_Connection=True;"
        );
    }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Configure relationships and constraints
        modelBuilder.Entity<Blog>()
            .HasMany(b => b.Posts)
            .WithOne(p => p.Blog)
            .HasForeignKey(p => p.BlogId)
            .OnDelete(DeleteBehavior.Cascade);
            
        modelBuilder.Entity<Post>()
            .Property(p => p.Title)
            .IsRequired()
            .HasMaxLength(200);
    }
}

Explanation:

  • The DbContext defines two DbSet properties representing tables
  • OnConfiguring specifies the database provider (SQL Server) and connection string
  • OnModelCreating uses the Fluent API to configure relationships and validation rules
  • The one-to-many relationship is configured with cascade delete (deleting a blog deletes its posts)

Example 2: CRUD Operations with EF Core

using (var context = new BloggingContext())
{
    // CREATE - Add new entities
    var blog = new Blog
    {
        Name = "Tech Blog",
        Url = "https://techblog.example.com",
        Posts = new List<Post>
        {
            new Post
            {
                Title = "Getting Started with EF Core",
                Content = "Entity Framework Core is awesome!",
                PublishedDate = DateTime.Now
            }
        }
    };
    context.Blogs.Add(blog);
    context.SaveChanges();  // INSERT statements executed
    
    // READ - Query with filtering and sorting
    var recentPosts = context.Posts
        .Where(p => p.PublishedDate > DateTime.Now.AddDays(-7))
        .OrderByDescending(p => p.PublishedDate)
        .Include(p => p.Blog)  // Eager load the related blog
        .ToList();
    
    foreach (var post in recentPosts)
    {
        Console.WriteLine($"{post.Title} on {post.Blog.Name}");
    }
    
    // UPDATE - Modify tracked entities
    var blogToUpdate = context.Blogs.FirstOrDefault(b => b.Name == "Tech Blog");
    if (blogToUpdate != null)
    {
        blogToUpdate.Url = "https://newtechblog.example.com";
        context.SaveChanges();  // UPDATE statement executed
    }
    
    // DELETE - Remove entities
    var postToDelete = context.Posts.FirstOrDefault(p => p.PostId == 1);
    if (postToDelete != null)
    {
        context.Posts.Remove(postToDelete);
        context.SaveChanges();  // DELETE statement executed
    }
}

Explanation:

  • Add() marks entities for insertion; SaveChanges() executes the SQL
  • LINQ queries are translated to SQLβ€”the Where() becomes a WHERE clause
  • Include() performs eager loading with a JOIN to avoid the N+1 query problem
  • Changes to tracked entities are automatically detected and saved
  • Remove() marks entities for deletion

Example 3: Advanced Querying with Projections

using (var context = new BloggingContext())
{
    // Projection - Select only needed columns (more efficient)
    var blogSummaries = context.Blogs
        .Select(b => new
        {
            BlogName = b.Name,
            PostCount = b.Posts.Count,
            LatestPost = b.Posts
                .OrderByDescending(p => p.PublishedDate)
                .Select(p => p.Title)
                .FirstOrDefault()
        })
        .ToList();
    
    // Grouping and aggregation
    var postsByMonth = context.Posts
        .GroupBy(p => new
        {
            Year = p.PublishedDate.Year,
            Month = p.PublishedDate.Month
        })
        .Select(g => new
        {
            Period = $"{g.Key.Year}-{g.Key.Month:00}",
            Count = g.Count(),
            Titles = g.Select(p => p.Title).ToList()
        })
        .ToList();
    
    // Raw SQL for complex queries
    var popularBlogs = context.Blogs
        .FromSqlRaw(@"
            SELECT b.* 
            FROM Blogs b
            INNER JOIN Posts p ON b.BlogId = p.BlogId
            GROUP BY b.BlogId, b.Name, b.Url
            HAVING COUNT(p.PostId) > 10
        ")
        .ToList();
    
    // Async operations for scalability
    var allBlogs = await context.Blogs
        .Include(b => b.Posts)
        .ToListAsync();
}

Explanation:

  • Projections with Select() retrieve only necessary columns, reducing data transfer
  • GroupBy() generates SQL GROUP BY clauses for aggregations
  • FromSqlRaw() allows raw SQL when LINQ isn't sufficient (still returns tracked entities)
  • Async methods (ToListAsync(), FirstOrDefaultAsync()) prevent thread blocking in web apps

Example 4: Handling Migrations in .NET 10

# Install EF Core tools (if not already installed)
dotnet tool install --global dotnet-ef

# Create initial migration
dotnet ef migrations add InitialCreate

# Apply migration to database
dotnet ef database update

# Add a new property to Post entity
# (e.g., public int ViewCount { get; set; })

# Create migration for the change
dotnet ef migrations add AddViewCountToPost

# Review the generated migration file
# Migrations/20240315_AddViewCountToPost.cs

# Apply the new migration
dotnet ef database update

# Rollback to previous migration if needed
dotnet ef database update InitialCreate

# Remove last migration (if not applied to database)
dotnet ef migrations remove

Generated migration code looks like:

public partial class AddViewCountToPost : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AddColumn<int>(
            name: "ViewCount",
            table: "Posts",
            type: "int",
            nullable: false,
            defaultValue: 0);
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropColumn(
            name: "ViewCount",
            table: "Posts");
    }
}

Explanation:

  • Migrations create a version history for your database schema
  • Up() method applies changes; Down() method reverts them
  • Each migration is timestamped and applied in order
  • You can rollback by specifying a target migration name

πŸ”§ Try this: Create a simple console app with a single entity, generate a migration, inspect the generated SQL, then add a property and create another migration to see how EF Core tracks schema changes!

Common Mistakes to Avoid ⚠️

1. The N+1 Query Problem

❌ Wrong:

var blogs = context.Blogs.ToList();
foreach (var blog in blogs)
{
    // This executes a separate query for EACH blog!
    Console.WriteLine($"{blog.Name} has {blog.Posts.Count} posts");
}
// Result: 1 query for blogs + N queries for posts = N+1 queries

βœ… Right:

var blogs = context.Blogs
    .Include(b => b.Posts)  // Single query with JOIN
    .ToList();
foreach (var blog in blogs)
{
    Console.WriteLine($"{blog.Name} has {blog.Posts.Count} posts");
}
// Result: 1 query with JOIN

2. Forgetting to Call SaveChanges()

❌ Wrong:

var blog = context.Blogs.FirstOrDefault(b => b.BlogId == 1);
blog.Name = "Updated Name";
// No SaveChanges() - change is lost!

βœ… Right:

var blog = context.Blogs.FirstOrDefault(b => b.BlogId == 1);
blog.Name = "Updated Name";
context.SaveChanges();  // Persists changes to database

3. Not Disposing DbContext Properly

❌ Wrong:

var context = new BloggingContext();
var blogs = context.Blogs.ToList();
// Context never disposed - memory leak!

βœ… Right:

using (var context = new BloggingContext())
{
    var blogs = context.Blogs.ToList();
}  // Context automatically disposed

// Or in .NET 10 with minimal APIs:
builder.Services.AddDbContext<BloggingContext>();  // DI handles lifecycle

4. Modifying Collections During Enumeration

❌ Wrong:

foreach (var post in blog.Posts)
{
    if (post.ViewCount < 10)
        blog.Posts.Remove(post);  // Throws InvalidOperationException!
}

βœ… Right:

var postsToRemove = blog.Posts.Where(p => p.ViewCount < 10).ToList();
foreach (var post in postsToRemove)
{
    blog.Posts.Remove(post);
}
context.SaveChanges();

5. Using Synchronous Methods in Async Controllers

❌ Wrong (ASP.NET Controller):

public IActionResult GetBlogs()
{
    var blogs = context.Blogs.ToList();  // Blocks thread!
    return Ok(blogs);
}

βœ… Right:

public async Task<IActionResult> GetBlogs()
{
    var blogs = await context.Blogs.ToListAsync();  // Non-blocking
    return Ok(blogs);
}

6. Not Configuring Cascade Behavior

❌ Wrong (Default behavior may surprise you):

// Deleting a blog might fail if posts exist!
var blog = context.Blogs.FirstOrDefault(b => b.BlogId == 1);
context.Blogs.Remove(blog);
context.SaveChanges();  // Might throw foreign key constraint error

βœ… Right:

// In OnModelCreating:
modelBuilder.Entity<Blog>()
    .HasMany(b => b.Posts)
    .WithOne(p => p.Blog)
    .OnDelete(DeleteBehavior.Cascade);  // Posts deleted automatically

Key Takeaways πŸŽ“

βœ… DbContext is your gateway to the databaseβ€”it manages entities and translates LINQ to SQL

βœ… Entity classes map to database tables; follow naming conventions or use Fluent API

βœ… LINQ queries provide type-safe, readable data access that compiles to efficient SQL

βœ… Migrations version-control your database schemaβ€”treat them like code!

βœ… Always use Include() for related data to avoid the N+1 query problem

βœ… Use async methods (ToListAsync(), SaveChangesAsync()) in web applications

βœ… Track only when neededβ€”use AsNoTracking() for read-only queries

βœ… Call SaveChanges() to persist modifications to the database

βœ… Dispose DbContext properly with using statements or dependency injection

βœ… Configure relationships explicitly using Fluent API for clarity and control

πŸ“‹ Quick Reference Card

Create DbContextpublic class MyContext : DbContext
Define Entity Setpublic DbSet<Blog> Blogs { get; set; }
Query (LINQ)context.Blogs.Where(b => b.Name.Contains("Tech"))
Eager Load.Include(b => b.Posts)
No Tracking.AsNoTracking()
Add Entitycontext.Blogs.Add(newBlog)
Update Entityblog.Name = "New Name"; context.SaveChanges();
Delete Entitycontext.Blogs.Remove(blog)
Save Changescontext.SaveChanges() or await context.SaveChangesAsync()
Create Migrationdotnet ef migrations add MigrationName
Apply Migrationdotnet ef database update
Raw SQL.FromSqlRaw("SELECT * FROM Blogs WHERE...")
Configure via Fluent APImodelBuilder.Entity<Blog>().HasKey(b => b.Id)

πŸ“š Further Study

  1. Official Microsoft Documentation: Entity Framework Core Documentation - Comprehensive guide with .NET 10 updates

  2. EF Core Performance Best Practices: Performance in EF Core - Optimization techniques and query analysis

  3. EF Core GitHub Repository: dotnet/efcore on GitHub - Source code, issues, and design discussions for the latest features

Congratulations! You now have a solid foundation in Entity Framework Core. Practice by building a small projectβ€”maybe a blog, task manager, or inventory systemβ€”to reinforce these concepts. Happy coding! πŸ’»πŸš€

Practice Questions

Test your understanding with these questions:

Q1: What is the base class for database context in EF Core?
A: DbContext
Q2: Complete the code to define an entity set: ```csharp public class BloggingContext : DbContext { public {{1}}<Blog> Blogs { get; set; } } ```
A: ["DbSet"]
Q3: What does this LINQ query retrieve? ```csharp var result = context.Posts .Where(p => p.PublishedDate > DateTime.Now.AddDays(-7)) .OrderByDescending(p => p.PublishedDate) .ToList(); ``` A. All posts ordered by date B. Posts from the last 7 days, newest first C. Posts older than 7 days D. Only the most recent post E. Posts with future dates
A: B
Q4: Which method must be called to persist changes to the database?
A: SaveChanges
Q5: Complete the eager loading code: ```csharp var blogs = context.Blogs .{{1}}(b => b.Posts) .ToList(); ```
A: ["Include"]