EF Core and DbContext

EF Core maps C# classes to database tables, so you read and write rows with LINQ instead of writing SQL by hand.

Syntaxpublic class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)

A relational database keeps data in tables. A table has named columns and holds rows. You normally talk to it in SQL, which is a separate language from C#.

Entity Framework Core, written EF Core, is an object-relational mapper (ORM). It ties one C# class to one table. One object of that class stands for one row. You write a LINQ query over objects, and EF Core turns it into SQL and sends it. This course uses EF Core 10, which ships with .NET 10 and needs the .NET 10 runtime.

The three pieces

First, a provider. This is a NuGet package that knows one database's SQL dialect. Npgsql.EntityFrameworkCore.PostgreSQL for PostgreSQL, Microsoft.EntityFrameworkCore.Sqlite for SQLite.

Second, your own class that inherits DbContext. Each table you want gets a DbSet<T> property on it. The context holds the connection, the model, and a record of every object it has handed you.

Third, options that tell the context which provider and connection string to use: UseNpgsql(...) or UseSqlite(...).

What trips people up

A DbContext is cheap to create and meant to be short-lived. One context does one database operation at a time, and it is not safe to share between threads. In a web application you register it with AddDbContext and get a fresh one per HTTP request.

Example

Example · csharp
// dotnet add package Microsoft.EntityFrameworkCore.Sqlite
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;

// One open connection keeps this in-memory database alive for the whole program.
using var connection = new SqliteConnection("Data Source=:memory:");
connection.Open();

var options = new DbContextOptionsBuilder<BlogContext>()
    .UseSqlite(connection)
    .Options;

using var context = new BlogContext(options);
await context.Database.EnsureCreatedAsync();   // Creates the table. Migrations replace this later.

context.Blogs.Add(new Blog { Name = "Soundscode", Rating = 5 });
await context.SaveChangesAsync();

Blog blog = await context.Blogs.SingleAsync();
Console.WriteLine($"{blog.Id}: {blog.Name} rated {blog.Rating}");

public class Blog
{
    public int Id { get; set; }                // Id is the primary key by convention
    public required string Name { get; set; }
    public int Rating { get; set; }
}

// The primary constructor (C# 12) passes the options to the base class.
public class BlogContext(DbContextOptions<BlogContext> options) : DbContext(options)
{
    public DbSet<Blog> Blogs => Set<Blog>();
}

// Output:
// 1: Soundscode rated 5

When to use it

  • Store and read an application's own data in PostgreSQL without writing SQL for every query.
  • Give an HTTP API a per-request database session that is disposed when the request ends.
  • Point the same entity classes at SQLite in tests and PostgreSQL in production.

More examples

The same context written out longhand

The primary constructor and the written-out constructor produce identical code; pick either, but recognise both. The DbSet property written as => Set<Blog>() needs no null-forgiving operator, which matters because this course has nullable reference types enabled.

Example · csharp
// Shorthand: a primary constructor.
public class BlogContext(DbContextOptions<BlogContext> options) : DbContext(options)
{
    public DbSet<Blog> Blogs => Set<Blog>();
}

// Longhand: exactly the same thing, with the constructor spelled out.
public class BlogContextLonghand : DbContext
{
    public BlogContextLonghand(DbContextOptions<BlogContextLonghand> options)
        : base(options)
    {
    }

    // Longhand for the DbSet property as well.
    public DbSet<Blog> Blogs
    {
        get { return Set<Blog>(); }
    }
}

// A third form you will see in older code. It compiles, but the = null!
// is only there to silence the nullable warning, which is a hint that
// Set<T>() is the better choice.
public class BlogContextOldStyle : DbContext
{
    public DbSet<Blog> Blogs { get; set; } = null!;
}

Registering the context in a web application

This is how a production service wires EF Core up. The connection string comes from configuration, never from a literal in the code, and the context is created and disposed around each request by the framework.

Example · csharp
// Program.cs of a web project.
// dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Dependency injection means the framework creates objects for you and hands
// them to whatever asks. AddDbContext registers AppDbContext with it; the
// default lifetime is scoped, which in a web app means one per HTTP request.
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Shop")));

var app = builder.Build();

// The AppDbContext parameter is filled in by dependency injection.
app.MapGet("/products", async (AppDbContext db) =>
    await db.Products.AsNoTracking().OrderBy(p => p.Name).ToListAsync());

app.Run();

// AppDbContext and Product live in their own files in a real project.
// GET /products returns a JSON array of the rows in the products table.

A context that configures itself, for a console tool

A context can supply its own options instead of receiving them. Reach for this in a throwaway tool. In an application, prefer passing DbContextOptions in, because that lets tests point the same context at a different database.

Example · csharp
using Microsoft.EntityFrameworkCore;

public class ReportContext : DbContext
{
    public DbSet<Product> Products => Set<Product>();

    // OnConfiguring runs when the context needs its options and none were
    // passed in. Use it only where there is no dependency injection container,
    // such as a small console tool or a one-off script.
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        string? fromEnvironment = Environment.GetEnvironmentVariable("SHOP_DB");
        optionsBuilder.UseNpgsql(fromEnvironment ?? "Host=localhost;Database=shop;Username=app");
    }
}

Discussion

  • Be the first to comment on this lesson.