Entity Framework Core: Complete Guide to .NET ORM
Entity Framework Core is a modern object-relational mapper (ORM) for .NET. It enables developers to work with databases using .NET objects, eliminating the need for most data-access code.
Why Entity Framework Core?
- 🎯 Type-Safe - LINQ queries with compile-time checking
- 🔄 Cross-Platform - Works on Windows, Linux, and macOS
- 📊 Multiple Databases - SQL Server, PostgreSQL, MySQL, SQLite, and more
- 🚀 Performance - Optimized query generation and tracking
- 🛠️ Migrations - Database schema versioning
Installation
# Install EF Core packages
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
# Install EF Core CLI tools
dotnet tool install --global dotnet-ef
Creating Your First DbContext
using Microsoft.EntityFrameworkCore;
public class ApplicationDbContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Post> Posts { get; set; }
public DbSet<Comment> Comments { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(
"Server=(localdb)\\mssqllocaldb;Database=MyApp;Trusted_Connection=True;"
);
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Configure entity relationships and constraints
modelBuilder.Entity<Post>()
.HasOne(p => p.User)
.WithMany(u => u.Posts)
.HasForeignKey(p => p.UserId);
modelBuilder.Entity<Comment>()
.HasOne(c => c.Post)
.WithMany(p => p.Comments)
.HasForeignKey(c => c.PostId);
}
}
Defining Entities
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public DateTime CreatedAt { get; set; }
// Navigation property
public ICollection<Post> Posts { get; set; }
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public DateTime PublishedAt { get; set; }
// Foreign key
public int UserId { get; set; }
// Navigation properties
public User User { get; set; }
public ICollection<Comment> Comments { get; set; }
}
public class Comment
{
public int Id { get; set; }
public string Text { get; set; }
public DateTime CreatedAt { get; set; }
public int PostId { get; set; }
public Post Post { get; set; }
}
Migrations
# Create initial migration
dotnet ef migrations add InitialCreate
# Update database
dotnet ef database update
# Add new migration after model changes
dotnet ef migrations add AddUserProfile
# Rollback to previous migration
dotnet ef database update PreviousMigrationName
# Remove last migration (if not applied)
dotnet ef migrations remove
# Generate SQL script
dotnet ef migrations script
CRUD Operations
Create
using (var context = new ApplicationDbContext())
{
var user = new User
{
Name = "John Doe",
Email = "john@example.com",
CreatedAt = DateTime.UtcNow
};
context.Users.Add(user);
await context.SaveChangesAsync();
Console.WriteLine($"Created user with ID: {user.Id}");
}
Read
using (var context = new ApplicationDbContext())
{
// Get all users
var allUsers = await context.Users.ToListAsync();
// Get single user
var user = await context.Users
.FirstOrDefaultAsync(u => u.Email == "john@example.com");
// Get user with posts
var userWithPosts = await context.Users
.Include(u => u.Posts)
.FirstOrDefaultAsync(u => u.Id == 1);
// Get user with posts and comments
var userWithDetails = await context.Users
.Include(u => u.Posts)
.ThenInclude(p => p.Comments)
.FirstOrDefaultAsync(u => u.Id == 1);
}
Update
using (var context = new ApplicationDbContext())
{
var user = await context.Users.FindAsync(1);
if (user != null)
{
user.Name = "Jane Doe";
user.Email = "jane@example.com";
await context.SaveChangesAsync();
}
}
Delete
using (var context = new ApplicationDbContext())
{
var user = await context.Users.FindAsync(1);
if (user != null)
{
context.Users.Remove(user);
await context.SaveChangesAsync();
}
}
LINQ Queries
Filtering
// Where clause
var activeUsers = await context.Users
.Where(u => u.CreatedAt > DateTime.UtcNow.AddDays(-30))
.ToListAsync();
// Multiple conditions
var results = await context.Posts
.Where(p => p.UserId == 1 && p.PublishedAt.Year == 2024)
.ToListAsync();
Sorting
// Order by
var sortedPosts = await context.Posts
.OrderByDescending(p => p.PublishedAt)
.ThenBy(p => p.Title)
.ToListAsync();
Projection
// Select specific fields
var userNames = await context.Users
.Select(u => new { u.Id, u.Name, u.Email })
.ToListAsync();
// Complex projection
var postSummaries = await context.Posts
.Select(p => new PostSummary
{
Title = p.Title,
AuthorName = p.User.Name,
CommentCount = p.Comments.Count
})
.ToListAsync();
Grouping
var postsByUser = await context.Posts
.GroupBy(p => p.UserId)
.Select(g => new
{
UserId = g.Key,
PostCount = g.Count(),
LatestPost = g.OrderByDescending(p => p.PublishedAt).First()
})
.ToListAsync();
Joins
var query = from user in context.Users
join post in context.Posts on user.Id equals post.UserId
where post.PublishedAt.Year == 2024
select new
{
UserName = user.Name,
PostTitle = post.Title
};
var results = await query.ToListAsync();
Relationships
One-to-Many
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Post> Posts { get; set; }
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
// Configuration
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.HasForeignKey(p => p.BlogId);
Many-to-Many
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Course> Courses { get; set; }
}
public class Course
{
public int Id { get; set; }
public string Title { get; set; }
public ICollection<Student> Students { get; set; }
}
// EF Core 5.0+ automatically creates join table
modelBuilder.Entity<Student>()
.HasMany(s => s.Courses)
.WithMany(c => c.Students);
One-to-One
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public UserProfile Profile { get; set; }
}
public class UserProfile
{
public int Id { get; set; }
public string Bio { get; set; }
public int UserId { get; set; }
public User User { get; set; }
}
// Configuration
modelBuilder.Entity<User>()
.HasOne(u => u.Profile)
.WithOne(p => p.User)
.HasForeignKey<UserProfile>(p => p.UserId);
Advanced Features
Raw SQL Queries
// Execute raw SQL
var users = await context.Users
.FromSqlRaw("SELECT * FROM Users WHERE CreatedAt > {0}", DateTime.UtcNow.AddDays(-30))
.ToListAsync();
// Execute stored procedure
var results = await context.Users
.FromSqlRaw("EXEC GetActiveUsers")
.ToListAsync();
// Execute non-query
await context.Database.ExecuteSqlRawAsync(
"UPDATE Users SET IsActive = 1 WHERE CreatedAt > {0}",
DateTime.UtcNow.AddDays(-30)
);
Global Query Filters
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Soft delete filter
modelBuilder.Entity<Post>()
.HasQueryFilter(p => !p.IsDeleted);
// Multi-tenant filter
modelBuilder.Entity<Order>()
.HasQueryFilter(o => o.TenantId == _currentTenantId);
}
Value Conversions
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Convert enum to string
modelBuilder.Entity<User>()
.Property(u => u.Status)
.HasConversion<string>();
// Custom conversion
modelBuilder.Entity<User>()
.Property(u => u.DateOfBirth)
.HasConversion(
v => v.ToUniversalTime(),
v => DateTime.SpecifyKind(v, DateTimeKind.Utc)
);
}
Shadow Properties
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.Property<DateTime>("LastModified");
}
// Set shadow property
context.Entry(post).Property("LastModified").CurrentValue = DateTime.UtcNow;
Performance Optimization
1. AsNoTracking for Read-Only Queries
// Faster for read-only scenarios
var users = await context.Users
.AsNoTracking()
.ToListAsync();
2. Explicit Loading
var user = await context.Users.FindAsync(1);
// Load related data on demand
await context.Entry(user)
.Collection(u => u.Posts)
.LoadAsync();
3. Split Queries
// Avoid cartesian explosion
var users = await context.Users
.Include(u => u.Posts)
.Include(u => u.Comments)
.AsSplitQuery()
.ToListAsync();
4. Compiled Queries
private static readonly Func<ApplicationDbContext, int, Task<User>> _getUserById =
EF.CompileAsyncQuery((ApplicationDbContext context, int id) =>
context.Users.FirstOrDefault(u => u.Id == id));
// Use compiled query
var user = await _getUserById(context, 1);
5. Batch Operations
// Bulk insert
var users = new List<User>();
for (int i = 0; i < 1000; i++)
{
users.Add(new User { Name = $"User {i}" });
}
context.Users.AddRange(users);
await context.SaveChangesAsync();
Best Practices
1. Use Dependency Injection
// Startup.cs
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
// Controller
public class UsersController : ControllerBase
{
private readonly ApplicationDbContext _context;
public UsersController(ApplicationDbContext context)
{
_context = context;
}
}
2. Use DTOs for API Responses
public class UserDto
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
var users = await context.Users
.Select(u => new UserDto
{
Id = u.Id,
Name = u.Name,
Email = u.Email
})
.ToListAsync();
3. Handle Concurrency
public class User
{
public int Id { get; set; }
public string Name { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
}
try
{
await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
// Handle concurrency conflict
}
Conclusion
Entity Framework Core is a powerful ORM that simplifies data access in .NET applications. Master these concepts to build efficient, maintainable database applications.
Resources
Enjoyed this article?
Explore more deep dives into architecture, performance, and modern .NET.