Entity Framework Core (EF Core) is a modern, open-source, and cross-platform object-relational mapper (ORM) for .NET. It abstracts database interactions, allowing developers to work with databases using .NET objects instead of writing raw SQL. This makes development faster, code cleaner, and applications more maintainable.
This in-depth guide is written for beginners, with practical examples, best practices, and step-by-step explanations. It will help you confidently integrate EF Core into your .NET applications.
What is Entity Framework Core?
Entity Framework Core is Microsoft’s recommended data access technology for .NET. EF Core is a lightweight, extensible, and cross-platform ORM that allows you to interact with your database using .NET objects. It supports Windows, Linux, macOS, and various database providers such as SQL Server, SQLite, PostgreSQL, MySQL, and more.
EF Core enables developers to:
- Map .NET classes to database tables (object-relational mapping).
- Query and manipulate data using LINQ (Language Integrated Query).
- Track changes and persist them to the database.
- Maintain database schema using migrations.
Benefits of Using EF Core
- Productivity: Reduces boilerplate code for database operations.
- Maintainability: Keeps database schema in sync with code using migrations.
- Testability: Makes it easy to mock data access for unit testing.
- Portability: Works with various databases and across platforms.
- Type Safety: Compile-time checking of queries and data structures.
- Rich Features: Supports complex relationships, change tracking, and more.
Setting Up EF Core in a .NET Project
Let’s create a simple blog application using EF Core in a .NET Console Application. The steps are nearly identical for ASP.NET Core or other .NET project types.
1. Create a New .NET Project
dotnet new console -n BloggingApp
cd BloggingApp
2. Add EF Core Packages
Add the core EF Core package and the provider for your database.
For SQL Server:
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
For SQLite:
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
For PostgreSQL:
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
3. Add a Connection String
In a real application, keep your connection string in a configuration file. For a quick demo, we’ll use it directly in code.
Understanding the Data Model and DbContext
1. Define Data Model Classes
Create a file named Post.cs
:
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public DateTime CreatedAt { get; set; }
public List<Comment> Comments { get; set; } = new();
}
public class Comment
{
public int Id { get; set; }
public string Author { get; set; }
public string Content { get; set; }
public int PostId { get; set; }
public Post Post { get; set; }
}
2. Create the DbContext
Create a file named BloggingContext.cs
:
using Microsoft.EntityFrameworkCore;
public class BloggingContext : DbContext
{
public DbSet<Post> Posts { get; set; }
public DbSet<Comment> Comments { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// For SQL Server
optionsBuilder.UseSqlServer(\"Server=(localdb)\\mssqllocaldb;Database=BloggingAppDb;Trusted_Connection=True;\");
// For SQLite, use: optionsBuilder.UseSqlite("Data Source=blogging.db");
}
}
How DbContext Works
- DbContext: Manages entity objects during runtime.
DbSet<T>
: Represents a table in the database.- OnConfiguring: Configures the database connection.
Configuring the Database Provider
Choose the provider matching your database and set the connection string:
- SQL Server:
"Server=(localdb)\\mssqllocaldb;Database=BloggingAppDb;Trusted_Connection=True;"
- SQLite:
"Data Source=blogging.db"
- PostgreSQL:
"Host=localhost;Database=blogging;Username=postgres;Password=yourpassword"
For ASP.NET Core, you typically configure the provider in Program.cs
or Startup.cs
:
builder.Services.AddDbContext<BloggingContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
Working with Migrations
Migrations let you evolve your database schema over time in sync with your data models.
1. Install the EF Core Tools
Already covered above with Microsoft.EntityFrameworkCore.Tools
.
2. Create the Initial Migration
dotnet ef migrations add InitialCreate
This generates code to create the schema based on your models.
3. Apply the Migration
dotnet ef database update
EF Core creates the database and tables.
4. Updating the Schema
When your models change, add a new migration:
dotnet ef migrations add AddSomeField
dotnet ef database update
CRUD Operations in EF Core
Let’s look at how to Create, Read, Update, and Delete data.
1. Create (Insert Data)
using (var context = new BloggingContext())
{
var post = new Post
{
Title = "Hello EF Core",
Content = "EF Core is a powerful ORM for .NET.",
CreatedAt = DateTime.UtcNow
};
context.Posts.Add(post);
context.SaveChanges();
}
2. Read (Fetch Data)
using (var context = new BloggingContext())
{
var posts = context.Posts.ToList();
foreach (var post in posts)
{
Console.WriteLine($"{post.Title} ({post.CreatedAt})");
}
}
Filtering and Ordering
var recentPosts = context.Posts
.Where(p => p.CreatedAt > DateTime.UtcNow.AddDays(-7))
.OrderByDescending(p => p.CreatedAt)
.ToList();
3. Update (Modify Data)
using (var context = new BloggingContext())
{
var post = context.Posts.First();
post.Title = "Updated Title";
context.SaveChanges();
}
4. Delete (Remove Data)
using (var context = new BloggingContext())
{
var post = context.Posts.First();
context.Posts.Remove(post);
context.SaveChanges();
}
Querying Data: LINQ and Relationships
Using LINQ
EF Core allows you to write queries using LINQ, a type-safe, compile-time checked query language.
// All posts with "EF Core" in the title
var efPosts = context.Posts
.Where(p => p.Title.Contains("EF Core"))
.ToList();
Loading Related Data
By default, related data (like Comments for a Post) is not loaded.
There are several ways to load related data:
1. Eager Loading with Include
var postsWithComments = context.Posts
.Include(p => p.Comments)
.ToList();
2. Explicit Loading
var post = context.Posts.First();
context.Entry(post).Collection(p => p.Comments).Load();
3. Lazy Loading
Lazy loading is not enabled by default. To use it, install the proxies package:
dotnet add package Microsoft.EntityFrameworkCore.Proxies
Then enable it in your context:
optionsBuilder
.UseSqlServer("...connection string...")
.UseLazyLoadingProxies();
And mark navigation properties as virtual
:
public virtual List<Comment> Comments { get; set; }
Best Practices When Using EF Core
1. Use Dependency Injection
For ASP.NET Core, always register DbContext
with the dependency injection (DI) container:
builder.Services.AddDbContext<BloggingContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
2. Keep DbContext Lifetime Short
- Use one
DbContext
per unit of work (per web request or operation). - DbContext is not thread-safe.
3. Use AsNoTracking for Read-Only Queries
If you don’t need to update entities, add .AsNoTracking()
for better performance:
var posts = context.Posts.AsNoTracking().ToList();
4. Always Use Migrations
Never modify the database schema manually. Use migrations for all schema changes.
5. Validate Data with Annotations
Use data annotations for validation and schema configuration:
using System.ComponentModel.DataAnnotations;
public class Post
{
public int Id { get; set; }
[Required]
[MaxLength(150)]
public string Title { get; set; }
public string Content { get; set; }
public DateTime CreatedAt { get; set; }
}
6. Monitor Generated SQL
Enable logging to view the SQL generated by EF Core. This helps in debugging and optimizing queries.
optionsBuilder.LogTo(Console.WriteLine, LogLevel.Information);
7. Use Transactions for Multiple Operations
EF Core wraps SaveChanges()
in a transaction by default, but for multiple operations, you can use explicit transactions:
using var transaction = context.Database.BeginTransaction();
try
{
// Multiple operations here
context.SaveChanges();
transaction.Commit();
}
catch
{
transaction.Rollback();
}
8. Prefer Async Methods
Use await context.Posts.ToListAsync()
and await context.SaveChangesAsync()
for scalability in web apps.
Common Pitfalls and How to Avoid Them
1. Long-Lived DbContext
Don’t keep a single DbContext alive for the application’s lifetime. This leads to memory leaks and stale data.
Solution: Use a new DbContext per operation/request.
2. Inefficient Queries (N+1 Problem)
Fetching related data in a loop can cause many queries (N+1 problem).
Solution: Use .Include()
or batch queries.
3. Missing AsNoTracking
Tracking is unnecessary for read-only scenarios and can slow down performance.
Solution: Use .AsNoTracking()
when updates are not required.
4. Ignoring SQL Performance
Blindly using LINQ can lead to inefficient SQL. Always review the SQL generated.
Solution: Use logging and optimize queries as needed.
5. Hard-Coding Connection Strings
Hard-coding connection strings is insecure and makes changing environments difficult.
Solution: Use configuration files or environment variables.
6. Not Using Migrations
Manually editing the database can lead to schema drift.
Solution: Always use migrations.
Performance Tips
- Use No-Tracking queries for read-only scenarios.
- Project only required columns with
.Select()
to reduce data transfer. - Batch updates and deletes where possible.
- Monitor and optimize generated SQL.
- Use indexes for frequently queried columns.
- Be careful with lazy loading; it can cause many small queries.
Example projecting only needed columns:
var postSummaries = context.Posts
.Select(p => new { p.Id, p.Title })
.ToList();
Testing with EF Core
1. Use In-Memory Database for Unit Testing
EF Core provides an in-memory provider for testing:
dotnet add package Microsoft.EntityFrameworkCore.InMemory
Configure your context for tests:
optionsBuilder.UseInMemoryDatabase("TestDb");
Now you can write unit tests without touching a real database.
2. Mocking DbContext
You can also mock DbSet and DbContext for more isolated unit tests using libraries like Moq.
Full Example: Putting It All Together
Here's a minimal example of a .NET Console App using EF Core.
Program.cs:
using System;
using System.Linq;
class Program
{
static void Main()
{
using (var context = new BloggingContext())
{
// Add a new post
var post = new Post { Title = "First Post", Content = "Welcome to my blog!", CreatedAt = DateTime.UtcNow };
context.Posts.Add(post);
context.SaveChanges();
// Read all posts
var posts = context.Posts.ToList();
foreach (var p in posts)
{
Console.WriteLine($"{p.Title} ({p.CreatedAt})");
}
}
}
}
Post.cs and BloggingContext.cs are as described above.
Conclusion and Further Resources
Entity Framework Core is a powerful, flexible ORM for .NET developers. With its simple API, cross-platform support, and rich features, it’s a great choice for modern applications. By following the best practices in this guide, you’ll build maintainable, scalable, and efficient data access layers.
Further Reading:
- Official EF Core Documentation
- EF Core GitHub Repository
- EF Core Migrations Guide
- Best Practices with EF Core
Happy coding with EF Core! If you have questions or feedback, feel free to reach out or comment below.