So far you have written SQL by hand. For an application like the Kladde that would be a lot of work: every contact would have to be assembled from columns when reading and taken apart again when saving, for every table, for every change. That work is done by an O/R mapper (object-relational mapper). It maps classes to tables, translates queries written in LINQ into SQL, and remembers what you changed so that it can save it with one call.

The O/R mapper of .NET is Entity Framework Core, EF Core for short. The course uses EF Core 10, a long-term support version like .NET 10 itself. In this lesson the Kladde gets a new project, Kladde.Data, that maps its classes to the tables from your data model. The tables themselves are created by the first migration in the next lesson; today is about how EF Core sees your model.

EF Core writes SQL for you, but it doesn’t relieve you of thinking in SQL. What you learned in the last four lessons you still need: to check what EF Core generates.

DbContext and DbSet

Two classes form the core:

  • DbContext stands for a session with the database. It knows the model, opens the connection when needed, tracks loaded objects and saves changes with SaveChangesAsync.
  • DbSet<T> stands for a table. Through context.Contacts you query contacts and add new ones.

The fastest way to understand EF Core is to look at which tables it would make from a class. That needs no database: GenerateCreateScript() writes the SQL without opening a connection. In k2-exercises, create a folder ef-playground and in it conventions.cs:

C#
#:package Microsoft.EntityFrameworkCore.SqlServer@10.0.14
#:package System.Security.Cryptography.Pkcs@10.0.14
#:property PublishAot=false
using Microsoft.EntityFrameworkCore;

using var context = new NotebookContext();
Console.WriteLine(context.Database.GenerateCreateScript());

public sealed class Note
{
    public int Id { get; set; }
    public required string Text { get; set; }
    public string? Category { get; set; }
    public DateOnly Day { get; set; }
    public bool Done { get; set; }
}

public sealed class NotebookContext : DbContext
{
    public DbSet<Note> Notes => Set<Note>();

    protected override void OnConfiguring(DbContextOptionsBuilder options) =>
        options.UseSqlServer("Server=localhost,1433;Database=notebook");
}
PowerShell
dotnet run conventions.cs
CREATE TABLE [Notes] (
    [Id] int NOT NULL IDENTITY,
    [Text] nvarchar(max) NOT NULL,
    [Category] nvarchar(max) NULL,
    [Day] date NOT NULL,
    [Done] bit NOT NULL,
    CONSTRAINT [PK_Notes] PRIMARY KEY ([Id])
);
GO

Without a single line of configuration, EF Core has decided a lot. These are its conventions:

  • The table name is the name of the DbSet property: Notes.
  • A property Id (or NoteId) becomes the primary key. If it is an int, the server hands out the numbers with IDENTITY.
  • Every property with get and set becomes a column of the same name. The type follows the table from lesson 2.4.3: DateOnly becomes date, bool becomes bit.
  • A string without a length becomes nvarchar(max). That is almost never what you want; you set a length yourself.
  • NULL or NOT NULL is read by EF Core from the nullable reference types: a string? may be missing, a string may not. Your house rule from K1, nullable on, pays off here.

You know the line #:package System.Security.Cryptography.Pkcs from check.cs. Here is the explanation: through Microsoft.Data.SqlClient, the SQL Server provider of EF Core 10.0.14 brings along the package System.Security.Cryptography.Pkcs in version 9.0.11, and NuGet knows security vulnerabilities for that version. Since .NET 10, NuGet also checks such packages that come along, and reports the warnings NU1902 and NU1903. In the Kladde, warnings are errors, and the build stops. A direct reference to the fixed version 10.0.14 solves it: NuGet then takes the higher one. How to handle such messages in general is shown in lesson 2.6.6.