Code First and Database First
By the end of this lesson
Choose an approach based on who owns the schema.
There are two directions you can work in. Code first means your C# classes are the source of truth and migrations push their shape into the database. Database first means the database is the source of truth and your classes are generated from it.
These are often presented as a preference, as though one were modern and the other dated. They are not. The question that decides it is who owns the schema — whose approval is needed to add a column, and what the rest of the organisation expects when the shape of the data changes.
Answer that first, and the technical choice follows without much argument.
The same project, viewed from both directions:
| Code first | Database first | |
|---|---|---|
| Source of truth | Your entity classes and configuration | The existing database schema |
| How the other side is produced | Migrations generate and apply SQL | Scaffolding generates entities and a context |
| Who changes the schema | A developer, in a pull request | Whoever owns the database, by their own process |
| Review of a schema change | Code review of the migration | Whatever change control the database owner uses |
| Shape of the classes | Designed for your domain | A direct reflection of tables and columns |
| Repeating the generation | Not applicable — migrations accumulate | Re-run scaffolding after every schema change |
| Suits | New applications where your team owns the data | Existing or shared databases owned elsewhere |
# Reverse-engineer every table into entity classes plus a DbContext
dotnet ef dbcontext scaffold "Name=ConnectionStrings:AnviDatabase" Microsoft.EntityFrameworkCore.SqlServer --output-dir Models --context-dir Data --context AnviContext --no-onconfiguring
# Limit it to the tables you need
dotnet ef dbcontext scaffold "Name=ConnectionStrings:AnviDatabase" Microsoft.EntityFrameworkCore.SqlServer --table Employees --table Departments --output-dir Models
# After a schema change, regenerate -- overwriting what is there
dotnet ef dbcontext scaffold "Name=ConnectionStrings:AnviDatabase" Microsoft.EntityFrameworkCore.SqlServer --output-dir Models --force- Name=ConnectionStrings:AnviDatabase reads the connection string from configuration rather than putting credentials on your command line and into your shell history.
- The provider is named as the second argument, because reverse engineering has to understand the source database's own types.
- --no-onconfiguring keeps the connection string out of the generated context, so the application can supply it the normal way through AddDbContext.
- --table restricts the output. A database with four hundred tables does not need four hundred entity classes in your project, and scaffolding only what you query keeps the model smaller and faster to build.
- --force overwrites the previously generated files. Read that again before using it: anything you edited by hand in those files is gone.
// Models/Employee.cs -- generated. Do not edit; it will be overwritten.
public partial class Employee
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public int DepartmentId { get; set; }
public decimal AnnualSalary { get; set; }
public virtual Department Department { get; set; } = null!;
public virtual ICollection<Order> Orders { get; set; } = new List<Order>();
}
// Models/Employee.Extensions.cs -- yours. Survives regeneration.
public partial class Employee
{
public string DisplayName => Name + " (" + DepartmentId + ")";
}- Generated entities are partial classes, which is the escape hatch that makes database first workable. Your additions go in a second file with the same class name, and regenerating the first one leaves it alone.
- Column names become property names directly. A legacy table with a column called EMP_SAL_ANN produces a property called EMP_SAL_ANN, which is honest about the database and unpleasant to read. You can rename with configuration, but the generated file will not do it for you.
- The generated classes reflect tables, not your domain. A table with forty nullable columns becomes a class with forty nullable properties. Many teams treat these as a data access layer and map them into their own types.
- There is no migration history in this approach, so the database is not created from your code. A new developer needs a database restored or built by whoever owns it.
Signals that point to one answer or the other:
- A new application, your team owns the data
- Code first. The schema is part of what you are building, so it should be designed alongside the code and reviewed with it.
- A database that already exists and is in use
- Database first, at least to begin with. The schema encodes years of decisions and other systems already depend on it. Generating from it is faster and safer than trying to describe it exactly in C#.
- A DBA or data team controls schema changes
- Database first. If a column needs their approval and their process, migrations from your repository cannot be the source of truth. Your model follows their schema.
- Several applications share the database
- Database first, or code first from exactly one application. What does not work is two applications both generating migrations for the same tables — each one's snapshot ignores the other's changes.
- Change control or audit requirements on the database
- Database first, usually. Where every schema change has to be scripted, approved and recorded outside your repository, the approval process owns the schema, whatever the tooling can do.
- An existing database that your team is taking over
- Scaffold once to get started, then move to code first deliberately: create an initial migration matching the current schema, and evolve from there. Do this as a decision with a date, not by drifting.
Summary
- Code first treats your classes as the source of truth and pushes changes out through migrations
- Database first treats the schema as the source of truth and generates classes from it
- Decide by asking who has to approve a schema change: your team, or someone outside the repository
- Scaffolded files are regenerated, so your own additions belong in a partial class in a separate file
- Never run migrations against a schema another team or another application owns
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Think about it
Who owns this schema?
Three situations. For each, decide code first or database first and say what decided it.
One: a new internal tool for tracking employee training, built and run entirely by your team. Two: a reporting application reading a twelve-year-old orders database that four other systems also write to. Three: a new service whose tables live in a database where every change needs a data team ticket.
Show solution
One is code first. Your team owns the data, so the schema is part of the thing you are building, and having it reviewed in pull requests alongside the code is the benefit you want.
Two is database first. Four other systems depend on that schema, so you are a reader of someone else's structure. Generating your model from it is accurate and carries no risk of your migrations altering something another system relies on. If you only read from it, consider generating no more than the tables you query.
Three is database first, and it is the interesting one, because the service is new. Being new would normally point to code first. What overrides it is the ticket: the data team's process owns schema changes, so your repository cannot be the source of truth without working around the process your organisation deliberately put in place.
In all three cases the technology was identical and the ownership question gave the answer. That is why it is the question to ask first.
Try it yourself
Scaffold and compare
Take a small database you already have — one of the sample databases is fine — and scaffold two or three tables into a throwaway project.
Compare the generated entities with how you would have written those classes yourself. Note every difference, and for each one decide whether the generated version is worse, better, or merely different.
Show solution
The differences you are most likely to find: property names copied from column names rather than chosen; navigation properties marked virtual; nullability driven by the column rather than by whether absence is meaningful; and no separation between what the table stores and what your code would want to work with.
Some of those are better than hand-written code, and it is worth being fair about it. The generated model is exactly right about the database, including details you would have got subtly wrong — a column that is nullable in the schema but that you assumed always had a value, for instance.
The purpose of comparing is to make the trade-off concrete before you commit to it on a real project. Database first buys accuracy about a schema you do not control. It sells you classes shaped by tables. Whether that is a good trade depends entirely on whether you were ever going to be allowed to change those tables.
Saved in this browser only.