Entity Framework Core in .NET: The Complete Beginner’s Guide with Best Practices and Examples

Entity Framework Core in .NET: The Complete Beginner’s Guide with Best Practices and Examples

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();

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:


Happy coding with EF Core! If you have questions or feedback, feel free to reach out or comment below.