Backend Development with .NET
Session 09
Entity Framework Core
Intro
Eng. Seif Mansour  ·  Andalusia Academy
Week 5  ·  2.5 hours
Session Goals

By the end of this session, you will be able to:

  • Explain what an ORM is and why Entity Framework Core is the .NET standard for database access
  • Distinguish between the code-first and database-first approaches, and choose the right one for a new project
  • Set up a DbContext with a PostgreSQL or SQL Server connection string
  • Define entity classes and configure their properties and relationships using the Fluent API
  • Register DbContext in Program.cs and verify the application starts cleanly against a real database
Session Agenda
Time Segment Type Duration
0:00What is an ORM?Theory15 min
0:15Code-first vs database-firstTheory10 min
0:25Install EF Core & configure DbContextDemo30 min
0:55Entity configurationDemo20 min
1:15Break10 min
1:25Lab — wire up the Task entityLab45 min
2:10Review & Q&ADiscussion20 min
What is an ORM?
An Object-Relational Mapper bridges the gap between C# objects and relational database tables. Instead of writing raw SQL, you work with strongly-typed C# classes and let the framework handle the translation — queries, inserts, updates, and deletes included.
In this section
  • The impedance mismatch problem between objects and tables
  • What EF Core does at runtime
  • The EF Core pipeline: LINQ to SQL
  • When an ORM fits and when raw SQL is better
What is an ORM?
  • An Object-Relational Mapper (ORM) maps C# classes to database tables and class properties to columns — eliminating manual SQL strings for routine operations
  • Entity Framework Core is Microsoft's official ORM for .NET — it is cross-platform, open-source, and supports PostgreSQL, SQL Server, SQLite, and more through provider packages
  • At runtime, EF Core translates LINQ queries written in C# into provider-specific SQL and executes them against the database
  • Results are returned as tracked C# objects — EF Core watches them for changes, and a single SaveChanges() call flushes all changes to the database in one transaction
  • EF Core also handles schema generation and migrations — C# class definitions become the source of truth for the database schema
Mental model
Think of EF Core as a translator. You speak C# and LINQ; the database speaks SQL. EF Core sits in the middle and handles the conversation so you rarely need to write SQL by hand.
ORM vs Raw SQL — Choosing the Right Tool

EF Core is a strong fit when:

  • You own the schema and want C# classes to drive it
  • CRUD operations dominate the access pattern
  • You want compile-time safety on queries and column names
  • Migrations and schema versioning are part of the workflow
  • You work across multiple database providers and want portability

Raw SQL or Dapper fits better when:

  • Queries are complex aggregations or multi-join reports that the ORM translates poorly
  • Performance is critical and EF Core's generated SQL is too expensive
  • The schema is legacy and cannot be changed to match C# conventions
  • Stored procedures are the mandated data access layer
Best practice
EF Core and raw SQL are not mutually exclusive. Use FromSqlRaw or Dapper for complex queries, and EF Core for everything else — even in the same project.
Code-First vs Database-First
EF Core supports two primary workflows. Code-first treats C# classes as the source of truth and generates the database schema from them. Database-first starts from an existing schema and generates the C# classes. For new projects, code-first is almost always the right choice.
In this section
  • Code-first: C# defines the schema
  • Database-first: scaffolding from an existing database
  • Why code-first is the standard for new .NET projects
  • The role of migrations in the code-first workflow
Code-First vs Database-First
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
Migrations
In code-first, a migration is a C# file that records exactly how to move the database schema from one version to the next — and how to roll it back. Migrations are covered in Session 10.
Why Code-First for New Projects
  • Single source of truth — the C# entity classes define the schema. There is no separate SQL script to keep in sync with the code
  • Version control integration — every schema change produces a migration file checked into Git alongside the code that required the change
  • Reproducible environments — any developer can clone the repo, run dotnet ef database update, and have an identical database in minutes
  • Refactoring safety — renaming a property in C# produces a migration that renames the column; the change is atomic and tracked
  • Team-friendly — pull requests contain both the feature code and its migration, so reviewers see exactly what the schema impact is
One exception
If you must integrate with a database that another team owns and you cannot change its schema, use dotnet 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.
Install EF Core & Configure DbContext
DbContext is the central class in EF Core. It represents a session with the database, exposes DbSet properties for each entity, and coordinates change tracking and transaction management. Getting it wired up correctly is the foundation for everything that follows.
In this section
  • Which NuGet packages to install
  • What DbContext does and why it is a unit of work
  • Writing the AppDbContext class with DbSet properties
  • Registering DbContext in Program.cs with a connection string
  • Keeping connection strings out of source control
Installing EF Core Packages

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
Package roles
Microsoft.EntityFrameworkCore is the runtime — change tracking, LINQ translation, migrations. Design is needed for dotnet ef CLI commands. The provider package (Npgsql or SqlServer) handles the dialect-specific SQL generation and the ADO.NET connection.
DbContext — the Unit of Work

DbContext is the entry point to the database in EF Core. One instance represents one database session:

  • DbSet<T> — one property per entity type, representing the corresponding table
  • Change tracker — monitors loaded entities for modifications; flushes them all on SaveChanges()
  • OnModelCreating — the hook where Fluent API configuration is applied (constraints, indexes, relationships)
  • Scoped lifetime — in ASP.NET Core, each HTTP request gets its own DbContext instance, created by DI and disposed at request end
Controller LINQ query via DbSet DbContext Change tracker + SaveChanges EF Core Provider LINQ → SQL translation Database PostgreSQL / SQL Server Tracked objects SQL statement Result rows
Writing AppDbContext
Data/AppDbContext.cs
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.

Registering DbContext in Program.cs

One call in Program.cs registers AppDbContext in the DI container with the Npgsql provider:

Program.cs
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:

  • Registers AppDbContext as a scoped service — one instance per HTTP request
  • Creates a connection pool shared across all instances so database connections are not opened and closed for every query
  • Configures the provider (Npgsql) and passes it the connection string read from configuration
  • Makes AppDbContext available for constructor injection in controllers, services, and repositories
Connection String Setup

Store the connection string in appsettings.Development.json — not in the committed appsettings.json:

appsettings.Development.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:

.gitignore
appsettings.Development.json
appsettings.*.json
!appsettings.json
Never commit credentials
A connection string with a password pushed to a public repository is a security incident. Add the file to .gitignore before the first commit. Use environment variables or a secrets manager (Azure Key Vault, AWS Secrets Manager) in production.
Key Principle
"Configuration that changes between environments must never be compiled into the binary or committed to source control."
Connection strings, API keys, and passwords are environment-specific secrets. They belong in environment variables, user secrets, or a secrets manager — not in appsettings.json files committed to Git. This rule applies from day one, not just when a project goes to production.
Entity Configuration
Entities are plain C# classes that EF Core maps to tables. Configuration — constraints, default values, max lengths, indexes, and relationships — can be expressed through conventions, data annotations, or the Fluent API. The Fluent API in OnModelCreating is the most expressive and least intrusive option.
In this section
  • Writing a well-structured entity class
  • Fluent API: required, max length, defaults
  • Configuring indexes for frequent query columns
  • Defining a one-to-many relationship
  • Entity configuration best practices
Writing the TaskItem Entity

An entity is a plain C# class. EF Core discovers it through the DbSet<T> property on AppDbContext:

  • By convention, a property named Id or TaskItemId becomes the primary key
  • Non-nullable value types (int, DateTime) map to NOT NULL columns automatically
  • Nullable reference types (string?) map to nullable columns when nullable reference types are enabled
  • Navigation properties represent foreign-key relationships — EF Core uses them to generate JOIN queries
Models/TaskItem.cs
public 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!;
}
Fluent API Configuration
Data/AppDbContext.cs — OnModelCreating
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.

Indexes and Entity Configuration Best Practices

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:

  • Always call HasMaxLength() on string columns — avoid unbounded columns
  • Use HasDefaultValueSql() for server-side timestamps so the database clock is authoritative
  • Define OnDelete behavior explicitly — do not rely on the provider default
  • Keep all Fluent API configuration in OnModelCreating or in dedicated IEntityTypeConfiguration<T> classes for large models
EF Core 8
With nullable reference types enabled, EF Core 8 infers IsRequired() automatically for non-nullable string properties. You still need HasMaxLength() — nullability does not imply a length constraint.
Lab — Wire Up the Task Entity

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.

1 Install Microsoft.EntityFrameworkCore, Microsoft.EntityFrameworkCore.Design, and Npgsql.EntityFrameworkCore.PostgreSQL (or the SQL Server provider)
2 Create a TaskItem entity class with Id, Title, Description, IsCompleted, CreatedAt, and a UserId foreign key
3 Create AppDbContext with a DbSet<TaskItem> property and configure the entity in OnModelCreating with IsRequired(), HasMaxLength(), and HasDefaultValueSql()
4 Add the connection string to appsettings.Development.json and add that file to .gitignore before committing
5 Register AppDbContext in Program.cs with AddDbContext and UseNpgsql (or UseSqlServer)
6 Run the application with dotnet run and confirm no startup errors. Migrations and actual database creation are covered in Session 10
Summary
  • EF Core is an ORM — it maps C# classes to database tables and translates LINQ queries into provider-specific SQL, eliminating manual SQL strings for routine operations
  • Code-first is the standard approach — C# entity classes are the source of truth; migrations translate class changes into schema changes tracked in version control
  • DbContext is the central class — it exposes DbSet<T> properties per table, manages change tracking, and is registered as a scoped service in DI
  • Fluent API in OnModelCreating is the recommended way to configure constraints, indexes, default values, and relationships without polluting entity classes with attributes
  • Connection strings are secrets — store them in appsettings.Development.json, add that file to .gitignore, and use environment variables or a secrets manager in production
  • Migrations come next — Session 10 covers creating and applying migrations to generate and evolve the real database schema from these entity definitions
What's Next

Session 10 — Migrations, CRUD & LINQ

  • The AppDbContext and entity classes set up in this session are the foundation — Session 10 picks up exactly where this lab ends
  • You will install the dotnet ef CLI tool, create your first migration with dotnet ef migrations add, and apply it to generate the real tables with dotnet ef database update
  • The in-memory lists currently backing the Task API endpoints will be replaced with real DbSet queries — ToListAsync(), FindAsync(), AddAsync(), and SaveChangesAsync()
  • LINQ filtering, ordering, and pagination will be pushed to the database layer so the SQL is efficient from the start
Before next session
Complete the assignment and make sure dotnet run starts the application without errors against a live database instance. The Session 10 demo builds directly on top of this working setup.
Assignment

Wire up Entity Framework Core for the Task Manager API and connect it to a real database.

What to build:

  • Install the EF Core packages and the appropriate database provider, then create AppDbContext with DbSet<TaskItem> and DbSet<User>
  • Configure both entities using the Fluent API in OnModelCreating: required fields, max lengths, default values, and the foreign key relationship between TaskItem and User
  • Register AppDbContext in Program.cs and store the connection string in appsettings.Development.json, adding it to .gitignore

Acceptance criteria:

  • dotnet run starts the application without any EF Core-related startup exceptions
  • AppDbContext has DbSet properties for both TaskItem and User, configured via the Fluent API
  • All string columns have explicit HasMaxLength() calls and no column is left as unbounded text
  • appsettings.Development.json is listed in .gitignore and is not committed to the repository
Bonus
Extract the entity configuration into a dedicated TaskItemConfiguration : IEntityTypeConfiguration<TaskItem> class and register it in OnModelCreating using modelBuilder.ApplyConfiguration(new TaskItemConfiguration()). This keeps AppDbContext clean as the model grows.
Questions?
Session 09 — Entity Framework Core Intro