Introduction
Entity Framework Core (EF Core) is Microsoft’s lightweight, cross-platform version of the popular Entity Framework object-relational mapper (ORM). It enables .NET developers to work with databases using .NET objects, eliminating the need for most data-access code. Instead of writing SQL queries manually, you can use C# or VB.NET to interact with your database.
In this comprehensive guide, we’ll explore EF Core from the ground up, covering everything from basic setup to advanced patterns.
What is Entity Framework Core?
Entity Framework Core is an open-source ORM framework that bridges the gap between your object-oriented application code and relational databases. It allows you to:
- Query databases using LINQ (Language Integrated Query)
- Track changes to objects automatically
- Save data without writing SQL
- Support multiple database providers (SQL Server, PostgreSQL, MySQL, SQLite, and more)
Key Benefits of Using EF Core
- Productivity: Write less code and focus on business logic rather than data access
- Type Safety: Catch errors at compile-time rather than runtime
- Database Independence: Switch database providers with minimal code changes
- Automatic Migrations: Update your database schema as your models evolve
- Performance: Optimized query generation and change tracking
Getting Started with EF Core
Prerequisites
Before diving into EF Core, ensure you have:
- .NET 6.0 or later installed
- Visual Studio 2022, VS Code, or JetBrains Rider
- Basic knowledge of C# and object-oriented programming
- Understanding of relational database concepts
Installation
Install EF Core through NuGet packages. The packages you need depend on your database provider:
# For SQL Server
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
# For SQLite (great for learning)
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
# For PostgreSQL
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
# EF Core Tools (for migrations)
dotnet add package Microsoft.EntityFrameworkCore.Tools
Creating Your First EF Core Application
Step 1: Define Your Model Classes
Models represent the tables in your database. Let’s create a simple blog application:
public class Blog
{
public int BlogId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public List<Post> Posts { get; set; }
}
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; }
public Blog Blog { get; set; }
}
Step 2: Create a DbContext
The DbContext is your gateway to the database. It manages the connection and tracks changes:
using Microsoft.EntityFrameworkCore;
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;");
}
}
Step 3: Create the Database
Use migrations to create your database schema:
# Create a migration
dotnet ef migrations add InitialCreate
# Update the database
dotnet ef database update
Step 4: Perform CRUD Operations
Now you can perform Create, Read, Update, and Delete operations:
using (var context = new BloggingContext())
{
// CREATE
var blog = new Blog
{
Name = "My Tech Blog",
Url = "https://mytechblog.com"
};
context.Blogs.Add(blog);
context.SaveChanges();
// READ
var blogs = context.Blogs.ToList();
var firstBlog = context.Blogs.FirstOrDefault(b => b.Name.Contains("Tech"));
// UPDATE
var blogToUpdate = context.Blogs.Find(1);
blogToUpdate.Name = "Updated Blog Name";
context.SaveChanges();
// DELETE
var blogToDelete = context.Blogs.Find(1);
context.Blogs.Remove(blogToDelete);
context.SaveChanges();
}
Advanced Querying Techniques
Basic LINQ Queries
EF Core translates LINQ queries into SQL:
using (var context = new BloggingContext())
{
// Simple WHERE clause
var techBlogs = context.Blogs
.Where(b => b.Name.Contains("Tech"))
.ToList();
// Ordering results
var sortedBlogs = context.Blogs
.OrderBy(b => b.Name)
.ToList();
// Selecting specific columns
var blogNames = context.Blogs
.Select(b => b.Name)
.ToList();
}
Eager Loading with Include
Load related data to avoid the N+1 query problem:
// Load blogs with their posts
var blogsWithPosts = context.Blogs
.Include(b => b.Posts)
.ToList();
// Multiple levels of includes
var blogsWithPostsAndComments = context.Blogs
.Include(b => b.Posts)
.ThenInclude(p => p.Comments)
.ToList();
Filtering Related Data
// Load only recent posts
var blogsWithRecentPosts = context.Blogs
.Include(b => b.Posts.Where(p => p.PublishedDate > DateTime.Now.AddMonths(-1)))
.ToList();
Explicit Loading
Load related data on demand:
var blog = context.Blogs.Find(1);
// Explicitly load posts
context.Entry(blog)
.Collection(b => b.Posts)
.Load();
Configuring Your Models
Using Data Annotations
Add attributes to your model classes:
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
public class Blog
{
[Key]
public int BlogId { get; set; }
[Required]
[MaxLength(100)]
public string Name { get; set; }
[Column("BlogUrl")]
public string Url { get; set; }
[NotMapped]
public int TempData { get; set; }
}
Using Fluent API
Configure models in OnModelCreating for more flexibility:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>(entity =>
{
entity.HasKey(b => b.BlogId);
entity.Property(b => b.Name)
.IsRequired()
.HasMaxLength(100);
entity.Property(b => b.Url)
.HasColumnName("BlogUrl");
entity.HasMany(b => b.Posts)
.WithOne(p => p.Blog)
.HasForeignKey(p => p.BlogId)
.OnDelete(DeleteBehavior.Cascade);
});
}
Working with Migrations
Creating Migrations
Migrations track changes to your model:
# Add a new migration
dotnet ef migrations add AddPostAuthor
# Remove the last migration (if not applied)
dotnet ef migrations remove
# List all migrations
dotnet ef migrations list
Applying Migrations
# Update to latest migration
dotnet ef database update
# Update to specific migration
dotnet ef database update InitialCreate
# Rollback all migrations
dotnet ef database update 0
Seeding Data
Add initial data in OnModelCreating:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().HasData(
new Blog { BlogId = 1, Name = "Sample Blog", Url = "https://sample.com" },
new Blog { BlogId = 2, Name = "Tech News", Url = "https://technews.com" }
);
}
Performance Optimization
No-Tracking Queries
For read-only scenarios, disable change tracking:
var blogs = context.Blogs
.AsNoTracking()
.ToList();
Compiled Queries
Pre-compile frequently used queries:
private static readonly Func<BloggingContext, int, Blog> GetBlogById =
EF.CompileQuery((BloggingContext context, int id) =>
context.Blogs.FirstOrDefault(b => b.BlogId == id));
// Usage
var blog = GetBlogById(context, 5);
Batch Operations
Update multiple records efficiently:
// Using ExecuteUpdate (EF Core 7+)
context.Blogs
.Where(b => b.Name.Contains("Old"))
.ExecuteUpdate(b => b.SetProperty(x => x.Name, x => x.Name + " - Updated"));
// Using ExecuteDelete (EF Core 7+)
context.Posts
.Where(p => p.PublishedDate < DateTime.Now.AddYears(-5))
.ExecuteDelete();
Projection for Better Performance
Select only the data you need:
var blogSummaries = context.Blogs
.Select(b => new BlogSummary
{
Name = b.Name,
PostCount = b.Posts.Count
})
.ToList();
Common Patterns and Best Practices
Repository Pattern
Encapsulate data access logic:
public interface IRepository<T> where T : class
{
Task<T> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(int id);
}
public class Repository<T> : IRepository<T> where T : class
{
private readonly BloggingContext _context;
private readonly DbSet<T> _dbSet;
public Repository(BloggingContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public async Task<T> GetByIdAsync(int id)
{
return await _dbSet.FindAsync(id);
}
public async Task<IEnumerable<T>> GetAllAsync()
{
return await _dbSet.ToListAsync();
}
public async Task AddAsync(T entity)
{
await _dbSet.AddAsync(entity);
await _context.SaveChangesAsync();
}
public async Task UpdateAsync(T entity)
{
_dbSet.Update(entity);
await _context.SaveChangesAsync();
}
public async Task DeleteAsync(int id)
{
var entity = await _dbSet.FindAsync(id);
if (entity != null)
{
_dbSet.Remove(entity);
await _context.SaveChangesAsync();
}
}
}
Unit of Work Pattern
Manage transactions across multiple repositories:
public interface IUnitOfWork : IDisposable
{
IRepository<Blog> Blogs { get; }
IRepository<Post> Posts { get; }
Task<int> CommitAsync();
}
public class UnitOfWork : IUnitOfWork
{
private readonly BloggingContext _context;
private IRepository<Blog> _blogs;
private IRepository<Post> _posts;
public UnitOfWork(BloggingContext context)
{
_context = context;
}
public IRepository<Blog> Blogs =>
_blogs ??= new Repository<Blog>(_context);
public IRepository<Post> Posts =>
_posts ??= new Repository<Post>(_context);
public async Task<int> CommitAsync()
{
return await _context.SaveChangesAsync();
}
public void Dispose()
{
_context.Dispose();
}
}
Dependency Injection in ASP.NET Core
Register DbContext in your application:
// Program.cs or Startup.cs
builder.Services.AddDbContext<BloggingContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection")));
// In your controller
public class BlogsController : ControllerBase
{
private readonly BloggingContext _context;
public BlogsController(BloggingContext context)
{
_context = context;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<Blog>>> GetBlogs()
{
return await _context.Blogs.ToListAsync();
}
}
Frequently Asked Questions (FAQs)
What is the difference between Entity Framework and Entity Framework Core?
Entity Framework Core is a complete rewrite of the original Entity Framework. It’s lightweight, cross-platform, and designed for modern application development. EF Core supports .NET Core, .NET 5+, and .NET Framework, while the original EF only works with .NET Framework. EF Core has better performance and is actively developed by Microsoft.
Can I use EF Core with existing databases?
Yes, EF Core supports a database-first approach through scaffolding. Use the following command to generate models from an existing database:
dotnet ef dbcontext scaffold "ConnectionString" Microsoft.EntityFrameworkCore.SqlServer -o Models
How do I handle concurrency conflicts in EF Core?
Use concurrency tokens to detect conflicts. Add a RowVersion property to your model:
public class Blog
{
public int BlogId { get; set; }
public string Name { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
}
When a conflict occurs, catch the DbUpdateConcurrencyException and handle it appropriately.
Is EF Core slower than writing raw SQL?
EF Core generates optimized SQL queries that perform well for most scenarios. However, for complex queries or bulk operations, raw SQL might be faster. EF Core allows you to execute raw SQL when needed:
var blogs = context.Blogs
.FromSqlRaw("SELECT * FROM Blogs WHERE Name LIKE '%Tech%'")
.ToList();
How do I implement soft deletes in EF Core?
Add an IsDeleted flag to your entities and use global query filters:
public class Blog
{
public int BlogId { get; set; }
public string Name { get; set; }
public bool IsDeleted { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasQueryFilter(b => !b.IsDeleted);
}
Can I use multiple DbContext classes in one application?
Yes, you can have multiple DbContext classes for different bounded contexts or databases. Register each one separately in your dependency injection container with different lifetimes and connection strings if needed.
How do I handle transactions in EF Core?
SaveChanges automatically wraps changes in a transaction. For manual transaction control:
using (var transaction = context.Database.BeginTransaction())
{
try
{
context.Blogs.Add(new Blog { Name = "New Blog" });
context.SaveChanges();
context.Posts.Add(new Post { Title = "New Post", BlogId = 1 });
context.SaveChanges();
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
What are shadow properties in EF Core?
Shadow properties exist in the EF Core model but not in your entity classes. They’re useful for audit fields:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property<DateTime>("LastUpdated");
}
// Access shadow properties
context.Entry(blog).Property("LastUpdated").CurrentValue = DateTime.Now;
How do I debug EF Core queries?
Enable sensitive data logging and detailed errors during development:
optionsBuilder
.UseSqlServer(connectionString)
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
You can also log SQL queries to the console:
optionsBuilder
.UseSqlServer(connectionString)
.LogTo(Console.WriteLine, LogLevel.Information);
Is EF Core suitable for microservices?
Yes, EF Core works well in microservices architectures. Each microservice can have its own DbContext and database. Use the repository pattern and dependency injection to keep your data access layer clean and testable. Consider using the database per service pattern to maintain loose coupling between services.
Conclusion
Entity Framework Core is a powerful ORM that simplifies data access in .NET applications. By following the patterns and practices outlined in this guide, you can build efficient, maintainable applications with clean data access code. Start with the basics, understand the fundamentals of DbContext and LINQ queries, and gradually explore advanced features like migrations, performance optimization, and design patterns.
Remember that while EF Core handles most data access scenarios elegantly, it’s important to monitor performance and use raw SQL or stored procedures when necessary. Keep learning, experiment with different approaches, and choose the patterns that best fit your application’s needs.