All articles

Entity Framework Core Database-First Tutorial

Database-first in EF Core means you point a tool at an existing SQL Server database and it generates the entity classes and DbContext for you, instead of you…

0 · log in to like, save & follow Share on LinkedIn Share on X

Database-first in EF Core means you point a tool at an existing SQL Server database and it generates the entity classes and DbContext for you, instead of you writing classes and letting migrations build the schema. You do it with one command — dotnet ef dbcontext scaffold — and re-run it whenever the schema changes. This tutorial shows the full database-first workflow with EF Core 9 and SQL Server 2022: installing the tools, scaffolding a model, the options that matter, keeping your customizations across re-scaffolds, and querying the result with LINQ.

Entity Framework Core Database-First Tutorial

When database-first is the right choice

Database-first fits when the database already exists and you do not own its schema from the application side. Common cases:

  • A legacy database you are wrapping in a new .NET 9 service.
  • A DBA-owned schema where table structure, indexes, and change scripts live in the database team's process, not in your migrations.
  • Multiple applications sharing one database, where the schema is the contract and no single app should own it.
  • Reporting or integration work over a database you only read from.

If instead you are building a greenfield app and want the schema to evolve from your C# classes, choose code-first with migrations. Database-first and code-first are two ends of the same EF Core — pick based on who owns the schema.

The mental model is worth stating plainly: in database-first the SQL Server database is the source of truth and your C# is a generated projection of it. Nothing in the model is authoritative — the moment the database changes, your classes are stale until you re-scaffold. That is exactly the property you want when a DBA controls the schema, and exactly the property you do not want when your application should drive schema changes through version-controlled migrations.

Install the EF Core tools and packages

You need the dotnet-ef CLI plus two NuGet packages in your project: the SQL Server provider and the design-time package that powers scaffolding.

# Install (or update) the global EF Core CLI tool
dotnet tool install --global dotnet-ef
dotnet tool update --global dotnet-ef

# In your project directory
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 9.0.0
dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.0

Microsoft.EntityFrameworkCore.Design must be referenced by the project you scaffold into — it contains the reverse-engineering logic. Confirm the tool is ready with dotnet ef --version, which should report EF Core 9.

Scaffold the model with dotnet ef dbcontext scaffold

The scaffold command reads the database and writes entity classes plus a DbContext. Here is the canonical form:

dotnet ef dbcontext scaffold \
  "Server=localhost;Database=Sales;Trusted_Connection=True;TrustServerCertificate=True" \
  Microsoft.EntityFrameworkCore.SqlServer \
  -o Models --context AppDbContext

The first argument is the connection string, the second is the provider assembly, -o Models puts the generated files in a Models folder, and --context AppDbContext names the context class.

Terminal running dotnet ef dbcontext scaffold and reporting the generated files

The scaffold options that matter

A few options change the output enough to be worth knowing:

  • --table Customers --table Orders scaffolds only the named tables instead of the whole database. Repeat the flag per table.
  • --data-annotations (or -d) puts mapping metadata as attributes like [Key] and [MaxLength] on the entities, rather than only in the fluent API inside OnModelCreating.
  • --use-database-names keeps column and table names exactly as they are in SQL Server instead of re-casing them to .NET conventions.
  • --no-onconfiguring omits the OnConfiguring method so your connection string is never baked into a generated file — you supply it through DbContextOptions in Program.cs instead. Use this one; a hard-coded connection string in source is a leak waiting to happen.
  • --context-dir and --namespace control where the context lands and its namespace.

A production-friendly command usually looks like this:

dotnet ef dbcontext scaffold \
  "Name=ConnectionStrings:Sales" \
  Microsoft.EntityFrameworkCore.SqlServer \
  -o Models --context AppDbContext \
  --data-annotations --no-onconfiguring

What gets generated

Scaffolding produces one plain C# class per table and one DbContext. Each entity has properties for its columns, navigation properties for its foreign keys, and (unless you asked for data annotations) the mapping lives in the context's OnModelCreating. A generated Customer entity looks roughly like this:

public partial class Customer
{
    public int CustomerId { get; set; }
    public string Name { get; set; } = null!;
    public string? Email { get; set; }
    public ICollection<Order> Orders { get; set; } = new List<Order>();
}

Every generated class is partial, and the DbContext exposes a DbSet<T> per table plus an OnModelCreatingPartial partial method hook. Both facts matter for the next step.

A few things scaffolding does automatically that are easy to miss: it infers primary keys from the database's key constraints, wires up navigation properties on both sides of each foreign key, maps computed and identity columns as ValueGeneratedOnAdd, and reads default constraints into the fluent configuration. What it cannot infer, it skips — many-to-many join tables without their own key, stored procedures, and views without a discoverable key may need manual attention or an explicit --table include list.

Re-scaffold when the schema changes, without losing customizations

When the database changes, re-run the same command with --force to overwrite the generated files:

dotnet ef dbcontext scaffold "Name=ConnectionStrings:Sales" \
  Microsoft.EntityFrameworkCore.SqlServer \
  -o Models --context AppDbContext --data-annotations --no-onconfiguring --force

--force regenerates everything, so never hand-edit the generated files — your changes would be wiped. Instead, use the fact that the classes are partial. Put your additions in separate files:

// Customer.Custom.cs — survives re-scaffolding
public partial class Customer
{
    public string DisplayName => $"{Name} <{Email}>";
}

For extra model configuration, implement the OnModelCreatingPartial partial method in your own file rather than editing OnModelCreating. This keeps a clean line between generated code and your code, so --force stays safe to run.

Query the scaffolded context with LINQ

Once the context is generated, it behaves like any EF Core context. Register it in Program.cs and query with LINQ:

var options = new DbContextOptionsBuilder<AppDbContext>()
    .UseSqlServer("Name=ConnectionStrings:Sales")
    .Options;

await using var db = new AppDbContext(options);

var recentOrders = await db.Orders
    .Where(o => o.Total > 100m)
    .Include(o => o.Customer)
    .OrderByDescending(o => o.OrderDate)
    .Take(10)
    .ToListAsync();

foreach (var o in recentOrders)
    Console.WriteLine($"{o.Customer.Name}: {o.Total:C}");

EF Core 9 translates this into a single parameterized SQL query with a join, streaming results into your entities. In a real ASP.NET Core app you would register the context once with builder.Services.AddDbContext<AppDbContext>(o => o.UseSqlServer(connectionString)) and inject it into your services, but the scaffolded context works identically either way. The scaffolded navigation properties — like o.Customer and the Customer.Orders collection — let you traverse foreign keys with Include and ThenInclude without writing any join SQL yourself.

Because scaffolding maps SQL Server types faithfully, watch the nullability: a nullable column becomes a nullable C# property (string?), and a NOT NULL column becomes a non-nullable one initialized to null!. EF Core 9 respects nullable reference types here, so the generated model gives you accurate null hints at compile time.

Database-first vs code-first trade-offs

Both approaches use the same runtime; they differ in who is the source of truth.

  • Database-first: the schema leads. Great for existing or DBA-owned databases. Downside — you re-scaffold to sync, and your model reflects the database's shape, not your domain's ideal shape.
  • Code-first: your C# classes lead, and migrations generate SQL. Great for greenfield apps and version-controlled schema evolution. Downside — awkward when someone else owns the database.

You are not locked in. Many teams scaffold once from a legacy database, then switch to code-first migrations going forward, treating the initial scaffold as a starting point.

Key takeaways

  • Database-first generates entities and a DbContext from an existing schema using dotnet ef dbcontext scaffold.
  • Install dotnet-ef, plus the SqlServer and Design packages, before scaffolding.
  • Use --data-annotations, --no-onconfiguring, --use-database-names, and --table to shape the output.
  • Re-scaffold with --force; keep customizations in separate files using the partial classes and OnModelCreatingPartial.
  • Choose database-first when the schema is owned elsewhere; choose code-first when your app owns the schema.
  • The scaffolded context queries with LINQ exactly like any EF Core 9 context.

Frequently asked questions

Does database-first still exist in EF Core 9?

Yes. EF Core never had a designer-based database-first like EF6, but reverse engineering through dotnet ef dbcontext scaffold is fully supported in EF Core 9 and is the standard way to generate a model from an existing SQL Server 2022 database.

How do I update my model when the database schema changes?

Re-run the same scaffold command with --force to regenerate the files. Because generated classes are partial, keep any hand-written code in separate files so --force does not overwrite it.

Should I hard-code the connection string in the scaffolded context?

No. Pass --no-onconfiguring so the connection string is not baked into a generated file, and supply it at runtime through DbContextOptions, configuration, or dependency injection instead.

Can I switch from database-first to code-first later?

Yes. A common pattern is to scaffold once from a legacy database, then adopt code-first migrations for all future changes, using the initial scaffold as your baseline model.

Enjoyed this article? Get the best GeeksArray articles in your inbox — once a week, no spam, unsubscribe anytime.

Comments (0)

Log in to join the conversation.

No comments yet — be the first to share your thoughts.