Entity Framework Core (EF Core) is an ORM that maps C# classes to database tables, letting you work with data as objects.

Setup

  dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
  

Define Models and DbContext

  public class Blog
{
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public List<Post> Posts { get; set; } = new();
}

public class Post
{
    public int Id { get; set; }
    public string Content { get; set; } = string.Empty;
    public int BlogId { get; set; }
    public Blog Blog { get; set; } = null!;
}

public class AppDbContext : DbContext
{
    public DbSet<Blog> Blogs => Set<Blog>();
    public DbSet<Post> Posts => Set<Post>();

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        options.UseSqlite("Data Source=app.db");
    }
}
  

CRUD Operations

  using var db = new AppDbContext();
await db.Database.EnsureCreatedAsync();

// Create
var blog = new Blog { Title = "My Blog" };
blog.Posts.Add(new Post { Content = "First post" });
db.Blogs.Add(blog);
await db.SaveChangesAsync();

// Read
var blogs = await db.Blogs
    .Include(b => b.Posts)
    .Where(b => b.Title.Contains("Blog"))
    .ToListAsync();

// Update
blog.Title = "Updated Title";
await db.SaveChangesAsync();

// Delete
db.Blogs.Remove(blog);
await db.SaveChangesAsync();
  

Migrations

  dotnet ef migrations add InitialCreate
dotnet ef database update
  

Migrations version-control your schema changes and apply them consistently across environments.

Register in ASP.NET Core

  builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
  

Inject AppDbContext into controllers or services via constructor injection.

Fluent API Configuration

  protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>(entity =>
    {
        entity.HasKey(b => b.Id);
        entity.Property(b => b.Title).HasMaxLength(200).IsRequired();
        entity.HasIndex(b => b.Title);
    });

    modelBuilder.Entity<Post>()
        .HasOne(p => p.Blog)
        .WithMany(b => b.Posts)
        .HasForeignKey(p => p.BlogId)
        .OnDelete(DeleteBehavior.Cascade);
}
  

Raw SQL When Needed

  var blogs = await db.Blogs
    .FromSqlRaw("SELECT * FROM Blogs WHERE Title LIKE {0}", "%dotnet%")
    .ToListAsync();
  

Prefer LINQ; use raw SQL for complex reports or performance-critical queries.

Change Tracking

  var blog = await db.Blogs.FindAsync(1);
blog.Title = "Updated";  // tracked automatically
await db.SaveChangesAsync();

// Read-only query — no tracking overhead
var titles = await db.Blogs
    .AsNoTracking()
    .Select(b => b.Title)
    .ToListAsync();
  

Seeding Data

  public static async Task SeedAsync(AppDbContext db)
{
    if (!await db.Blogs.AnyAsync())
    {
        db.Blogs.Add(new Blog { Title = "Default Blog" });
        await db.SaveChangesAsync();
    }
}
  

Call from Program.cs at startup in development.

Common Pitfalls

  • Forgetting Include() — related entities load as null without eager loading.
  • Calling SaveChanges() in a loop — batch changes and save once.
  • Not disposing DbContext — use await using or DI scoped lifetime.