By the end of this session, you will be able to:
DbContext with a PostgreSQL or SQL Server connection stringDbContext in Program.cs and verify the application starts cleanly against a real database| Time | Segment | Type | Duration |
|---|---|---|---|
| 0:00 | What is an ORM? | Theory | 15 min |
| 0:15 | Code-first vs database-first | Theory | 10 min |
| 0:25 | Install EF Core & configure DbContext | Demo | 30 min |
| 0:55 | Entity configuration | Demo | 20 min |
| 1:15 | Break | — | 10 min |
| 1:25 | Lab — wire up the Task entity | Lab | 45 min |
| 2:10 | Review & Q&A | Discussion | 20 min |
SaveChanges() call flushes all changes to the database in one transactionEF Core is a strong fit when:
Raw SQL or Dapper fits better when:
FromSqlRaw or Dapper for complex queries, and EF Core for everything else — even in the same project.
| Aspect | Code-First | Database-First |
|---|---|---|
| Source of truth | C# entity classes | Existing database schema |
| Schema changes | Edit C# class, run migration | Alter table, re-scaffold classes |
| Version control | Migration files in Git | Schema scripts managed separately |
| Best for | New projects, greenfield development | Integrating with a legacy database |
| This course uses | Yes — always code-first | Not covered |
dotnet ef database update, and have an identical database in minutesdotnet ef dbcontext scaffold to generate classes from the existing tables. This is the database-first path and is the only justified use of it on a new project.
Three packages are needed — the core library, the design-time tooling, and the database provider:
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
Using SQL Server instead? Replace the provider:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet ef CLI commands. The provider package (Npgsql or SqlServer) handles the dialect-specific SQL generation and the ADO.NET connection.
DbContext is the entry point to the database in EF Core. One instance represents one database session:
SaveChanges()public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<TaskItem> Tasks => Set<TaskItem>();
public DbSet<User> Users => Set<User>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<TaskItem>(entity =>
{
entity.HasKey(t => t.Id);
entity.Property(t => t.Title).IsRequired().HasMaxLength(200);
entity.Property(t => t.CreatedAt).HasDefaultValueSql("NOW()");
});
}
}
DbSet<T> properties expose each table as a queryable collection.
OnModelCreating is called once at startup — this is where Fluent API configuration lives.
The constructor receives DbContextOptions from DI, which carries the connection string and provider.
One call in Program.cs registers AppDbContext in the DI container with the Npgsql provider:
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(
builder.Configuration
.GetConnectionString("Default")));
For SQL Server, replace UseNpgsql with UseSqlServer — the rest of the code stays identical.
What AddDbContext does:
AppDbContext as a scoped service — one instance per HTTP requestAppDbContext available for constructor injection in controllers, services, and repositoriesStore the connection string in appsettings.Development.json — not in the committed appsettings.json:
{
"ConnectionStrings": {
"Default": "Host=localhost;Database=taskmanager;Username=postgres;Password=yourpassword"
}
}
The file name appsettings.Development.json is loaded automatically when ASPNETCORE_ENVIRONMENT=Development. It overrides values from appsettings.json.
Add the file to .gitignore immediately:
appsettings.Development.json
appsettings.*.json
!appsettings.json
.gitignore before the first commit. Use environment variables or a secrets manager (Azure Key Vault, AWS Secrets Manager) in production.
An entity is a plain C# class. EF Core discovers it through the DbSet<T> property on AppDbContext:
Id or TaskItemId becomes the primary keystring?) map to nullable columns when nullable reference types are enabledJOIN queriespublic class TaskItem
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string? Description { get; set; }
public bool IsCompleted { get; set; }
public DateTime CreatedAt { get; set; }
// Foreign key
public int UserId { get; set; }
// Navigation property
public User User { get; set; } = null!;
}
modelBuilder.Entity<TaskItem>(entity =>
{
entity.HasKey(t => t.Id);
entity.Property(t => t.Title).IsRequired().HasMaxLength(200);
entity.Property(t => t.Description).HasMaxLength(2000);
entity.Property(t => t.CreatedAt).HasDefaultValueSql("NOW()");
entity.HasOne(t => t.User)
.WithMany(u => u.Tasks)
.HasForeignKey(t => t.UserId)
.OnDelete(DeleteBehavior.Cascade);
});
IsRequired() adds a NOT NULL constraint.
HasMaxLength(200) sets varchar(200) in the database — without it, EF Core defaults to text (Postgres) or nvarchar(max) (SQL Server), which cannot be indexed efficiently.
HasDefaultValueSql("NOW()") lets the database populate the timestamp automatically.
Add indexes for columns you filter on frequently:
entity.HasIndex(t => t.UserId);
entity.HasIndex(t => t.IsCompleted);
entity.HasIndex(t => t.CreatedAt);
Foreign key columns should almost always have an index — EF Core does not create them automatically for non-primary-key foreign keys.
Best practices summary:
HasMaxLength() on string columns — avoid unbounded columnsHasDefaultValueSql() for server-side timestamps so the database clock is authoritativeOnDelete behavior explicitly — do not rely on the provider defaultOnModelCreating or in dedicated IEntityTypeConfiguration<T> classes for large modelsIsRequired() automatically for non-nullable string properties. You still need HasMaxLength() — nullability does not imply a length constraint.
Connect the Task Manager API to a real database. By the end of this lab, the application will start cleanly against a running PostgreSQL or SQL Server instance.
Microsoft.EntityFrameworkCore, Microsoft.EntityFrameworkCore.Design, and Npgsql.EntityFrameworkCore.PostgreSQL (or the SQL Server provider)
TaskItem entity class with Id, Title, Description, IsCompleted, CreatedAt, and a UserId foreign key
AppDbContext with a DbSet<TaskItem> property and configure the entity in OnModelCreating with IsRequired(), HasMaxLength(), and HasDefaultValueSql()
appsettings.Development.json and add that file to .gitignore before committing
AppDbContext in Program.cs with AddDbContext and UseNpgsql (or UseSqlServer)
dotnet run and confirm no startup errors. Migrations and actual database creation are covered in Session 10
DbSet<T> properties per table, manages change tracking, and is registered as a scoped service in DIappsettings.Development.json, add that file to .gitignore, and use environment variables or a secrets manager in productionSession 10 — Migrations, CRUD & LINQ
AppDbContext and entity classes set up in this session are the foundation — Session 10 picks up exactly where this lab endsdotnet ef CLI tool, create your first migration with dotnet ef migrations add, and apply it to generate the real tables with dotnet ef database updateDbSet queries — ToListAsync(), FindAsync(), AddAsync(), and SaveChangesAsync()dotnet run starts the application without errors against a live database instance. The Session 10 demo builds directly on top of this working setup.
Wire up Entity Framework Core for the Task Manager API and connect it to a real database.
What to build:
AppDbContext with DbSet<TaskItem> and DbSet<User>OnModelCreating: required fields, max lengths, default values, and the foreign key relationship between TaskItem and UserAppDbContext in Program.cs and store the connection string in appsettings.Development.json, adding it to .gitignoreAcceptance criteria:
dotnet run starts the application without any EF Core-related startup exceptionsAppDbContext has DbSet properties for both TaskItem and User, configured via the Fluent APIHasMaxLength() calls and no column is left as unbounded textappsettings.Development.json is listed in .gitignore and is not committed to the repositoryTaskItemConfiguration : IEntityTypeConfiguration<TaskItem> class and register it in OnModelCreating using modelBuilder.ApplyConfiguration(new TaskItemConfiguration()). This keeps AppDbContext clean as the model grows.