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 β β β β β β β β DbSetCustomers β β β β 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 ProfileProfile 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
DbContextdefines twoDbSetproperties representing tables OnConfiguringspecifies the database provider (SQL Server) and connection stringOnModelCreatinguses 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 aggregationsFromSqlRaw()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 DbContext | public class MyContext : DbContext |
| Define Entity Set | public 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 Entity | context.Blogs.Add(newBlog) |
| Update Entity | blog.Name = "New Name"; context.SaveChanges(); |
| Delete Entity | context.Blogs.Remove(blog) |
| Save Changes | context.SaveChanges() or await context.SaveChangesAsync() |
| Create Migration | dotnet ef migrations add MigrationName |
| Apply Migration | dotnet ef database update |
| Raw SQL | .FromSqlRaw("SELECT * FROM Blogs WHERE...") |
| Configure via Fluent API | modelBuilder.Entity<Blog>().HasKey(b => b.Id) |
π Further Study
Official Microsoft Documentation: Entity Framework Core Documentation - Comprehensive guide with .NET 10 updates
EF Core Performance Best Practices: Performance in EF Core - Optimization techniques and query analysis
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! π»π