Use the EF Core Database First approach in ASP.NET Core MVC: scaffold entity classes and a DbContext from an existing SQL Server database with dotnet ef dbcontext scaffold, keep the connection string safe, re-scaffold after schema changes and extend the generated partial classes.

Bishal Biswas
Self Learner
Two ways to start
So far this course has used Code First. You wrote the Product and Category classes, then migrations created the tables from them. The C# code was the source of truth.
Many real projects start the other way round. The database already exists. Another team owns it, an older app uses it, or a DBA designs every table by hand. You cannot let EF Core create or change the schema. You just need C# classes that match it.
That is Database First. EF Core reads the tables, columns, keys and relationships from the database and writes the entity classes and the DbContext for you. This step is called reverse engineering or scaffolding.
| Code First | Database First | |
|---|---|---|
| Source of truth | Your C# classes | The database schema |
| Who changes the schema | You, through migrations | The DBA or another app, with SQL |
| How C# catches up | You edit classes, then add a migration | You run the scaffold command again |
| Good for | New apps you fully own | Existing or shared databases |
Once the classes exist, everything else is the same. Queries, SaveChangesAsync, controllers and views all work exactly like in CRUD Operations with EF Core.
In old ASP.NET MVC 5 with EF 6, Database First meant an
.edmxfile and a visual designer. EF Core has no designer. Database First in EF Core is the scaffold command you will use below, and the result is plain C# files.
The existing database
Say the shop’s database was built by someone else with this SQL:
CREATE TABLE Categories (
CategoryId INT IDENTITY(1,1) CONSTRAINT PK_Categories PRIMARY KEY,
Name NVARCHAR(60) NOT NULL
);
CREATE TABLE Products (
ProductId INT IDENTITY(1,1) CONSTRAINT PK_Products PRIMARY KEY,
Name NVARCHAR(100) NOT NULL,
Price DECIMAL(10, 2) NOT NULL,
InStock BIT NOT NULL,
CategoryId INT NOT NULL
CONSTRAINT FK_Products_Categories REFERENCES Categories (CategoryId)
);
Notice the key names are ProductId and CategoryId, not Id. That is common in existing databases, and scaffolding handles it.
What you need
The same packages and tool as Entity Framework Core Setup:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet tool install --global dotnet-ef
The Design package is what the scaffold command uses. The project must also build, because the tool builds it first.
Put the connection string in appsettings.json
Add the connection string where the app will read it anyway:
{
"ConnectionStrings": {
"Default": "Server=(localdb)\\mssqllocaldb;Database=ShopDb;Trusted_Connection=True;TrustServerCertificate=True"
}
}
Run the scaffold command
From the folder that contains the .csproj file:
dotnet ef dbcontext scaffold Name=ConnectionStrings:Default Microsoft.EntityFrameworkCore.SqlServer --output-dir Models --context-dir Data --context ShopContext --no-onconfiguring
Piece by piece:
Name=ConnectionStrings:Defaulttells the tool to read the connection string from configuration. You could paste the full string here instead, but then it ends up in your shell history.Microsoft.EntityFrameworkCore.SqlServeris the provider to use.--output-dir Modelsis where the entity classes go.--context-dir Dataand--context ShopContextset the folder and name of theDbContext.--no-onconfiguringstops EF Core writing anOnConfiguringmethod into the context. You register the context inProgram.csinstead, like before.
The result:
Data/
ShopContext.cs
Models/
Category.cs
Product.cs
What gets generated
The entity class follows the columns:
public partial class Product
{
public int ProductId { get; set; }
public string Name { get; set; } = null!;
public decimal Price { get; set; }
public bool InStock { get; set; }
public int CategoryId { get; set; }
public virtual Category Category { get; set; } = null!;
}
SQL types become C# types: NVARCHAR becomes string, DECIMAL becomes decimal, BIT becomes bool. The foreign key column becomes CategoryId, and EF Core adds a Category navigation property for it. Category gets a Products collection in return.
The context holds the details that a class cannot show, such as lengths and column types:
public partial class ShopContext : DbContext
{
public ShopContext(DbContextOptions<ShopContext> options)
: base(options)
{
}
public virtual DbSet<Category> Categories { get; set; }
public virtual DbSet<Product> Products { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>(entity =>
{
entity.Property(e => e.Name).HasMaxLength(100);
entity.Property(e => e.Price).HasColumnType("decimal(10, 2)");
entity.HasOne(d => d.Category).WithMany(p => p.Products)
.HasForeignKey(d => d.CategoryId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_Products_Categories");
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
Two things to notice. Every class is partial, and there is an empty OnModelCreatingPartial hook at the end. Both are there so you can add your own code without touching these files. More on that soon.
If you prefer attributes such as [Key] and [StringLength(100)] on the classes, add --data-annotations to the command. Anything that has no attribute still goes in OnModelCreating.
Register the context
This is the same as Code First:
builder.Services.AddDbContext<ShopContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
Now inject ShopContext into a controller and use it:
public class ProductsController : Controller
{
private readonly ShopContext _db;
public ProductsController(ShopContext db) => _db = db;
public async Task<IActionResult> Index()
{
var products = await _db.Products
.Include(p => p.Category)
.OrderBy(p => p.Name)
.AsNoTracking()
.ToListAsync();
return View(products);
}
}
From here, Scaffolding Controllers and Views works on these classes too.
Only some tables
A big database may have hundreds of tables. Pick the ones you need with --table, once per table:
dotnet ef dbcontext scaffold Name=ConnectionStrings:Default Microsoft.EntityFrameworkCore.SqlServer --output-dir Models --context-dir Data --context ShopContext --no-onconfiguring --table Products --table Categories
Use --schema sales to take every table in one schema.
When the database changes
Say the DBA adds a Description column. Your code does not know about it yet. Run the same command again with --force so it can overwrite the files:
dotnet ef dbcontext scaffold Name=ConnectionStrings:Default Microsoft.EntityFrameworkCore.SqlServer --output-dir Models --context-dir Data --context ShopContext --no-onconfiguring --force
This is the Database First loop: the schema changes in the database, then you re-scaffold. You do not add migrations, because you are not the one changing the schema.
--force replaces the generated files completely. Any edit you made inside them is gone. Put long commands in a script file in the repo, such as scaffold.ps1 or scaffold.sh, so everyone runs exactly the same options.
Adding your own code safely
Never edit the generated files. Use the partial keyword instead. Put your extra code in another file with the same class name:
// Models/Partials/Product.cs (yours, never overwritten)
namespace Shop.Models;
public partial class Product
{
public string PriceLabel => Price.ToString("C");
}
A property with only a getter, like PriceLabel, is not mapped to a column, so EF Core ignores it.
For extra model configuration, fill in the hook from your own partial context file:
// Data/ShopContext.Custom.cs (yours, never overwritten)
public partial class ShopContext
{
partial void OnModelCreatingPartial(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>()
.HasQueryFilter(p => p.InStock);
}
}
Both files survive --force, because the scaffold command only overwrites the files it generates. It never deletes your other files.
For validation attributes on generated classes, such as [Required] or [Display], use a separate view model for the form. That keeps the generated entity clean, and it is better practice anyway, as Models and ViewModels explains.
Can I use migrations later?
Yes. Some teams scaffold once to get started and then switch to Code First. To do that, create a first migration and empty its Up and Down methods, because the tables already exist:
dotnet ef migrations add InitialCreate
protected override void Up(MigrationBuilder migrationBuilder)
{
// Baseline: these tables already exist in the database.
}
dotnet ef database update
This only writes a row to __EFMigrationsHistory. From then on, change the classes and add migrations as usual, and stop running the scaffold command. Do not mix the two: if a DBA still changes the schema by hand, stay with Database First.
Common problems
“Build failed.” The tool builds the project before it reads the database. Fix the compile error first. A half-finished partial class is a common cause.
“Login failed” or “server was not found”. The connection string is wrong, or the database server is not running. Test it in a database tool first.
A warning about a sensitive connection string in OnConfiguring. You passed the connection string directly and left out --no-onconfiguring. Delete the generated OnConfiguring method and register the context in Program.cs.
A table is missing. Scaffolding skips tables it cannot map, such as a table with no primary key. The tool prints a warning for each one. Map it as keyless yourself in OnModelCreatingPartial with HasNoKey() if you only read from it.
Your changes vanished. You edited a generated file and then re-scaffolded with --force. Move the change into a partial file.
Quick recap
- Code First makes the database from your classes. Database First makes your classes from the database.
dotnet ef dbcontext scaffoldreads the schema and writes the entity classes and theDbContext.- Use
Name=ConnectionStrings:Defaultand--no-onconfiguringto keep the connection string out of the code. --tableand--schemalimit what is scaffolded, and--data-annotationsuses attributes instead of fluent code.- When the schema changes, re-scaffold with
--force. Never edit generated files. - Add your own code in separate
partialclass files and inOnModelCreatingPartial. - You can switch to migrations later with an empty baseline migration.
// Terminal: scaffold the existing ShopDb database (rerun with --force after schema changes)
//
// dotnet ef dbcontext scaffold Name=ConnectionStrings:Default
// Microsoft.EntityFrameworkCore.SqlServer --output-dir Models --context-dir Data
// --context ShopContext --no-onconfiguring --force
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<ShopContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
var app = builder.Build();
app.UseStaticFiles();
app.UseRouting();
app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
app.Run();
// Models/Product.cs (generated, do not edit)
public partial class Product
{
public int ProductId { get; set; }
public string Name { get; set; } = null!;
public decimal Price { get; set; }
public bool InStock { get; set; }
public int CategoryId { get; set; }
public virtual Category Category { get; set; } = null!;
}
// Models/Category.cs (generated, do not edit)
public partial class Category
{
public int CategoryId { get; set; }
public string Name { get; set; } = null!;
public virtual ICollection<Product> Products { get; set; } = new List<Product>();
}
// Data/ShopContext.cs (generated, do not edit)
public partial class ShopContext : DbContext
{
public ShopContext(DbContextOptions<ShopContext> options) : base(options) { }
public virtual DbSet<Category> Categories { get; set; }
public virtual DbSet<Product> Products { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Category>(entity =>
{
entity.Property(e => e.Name).HasMaxLength(60);
});
modelBuilder.Entity<Product>(entity =>
{
entity.Property(e => e.Name).HasMaxLength(100);
entity.Property(e => e.Price).HasColumnType("decimal(10, 2)");
entity.HasOne(d => d.Category).WithMany(p => p.Products)
.HasForeignKey(d => d.CategoryId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_Products_Categories");
});
OnModelCreatingPartial(modelBuilder);
}
partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
}
// Models/Partials/Product.cs (yours, survives re-scaffolding)
public partial class Product
{
public string PriceLabel => Price.ToString("C");
}
// Data/ShopContext.Custom.cs (yours, survives re-scaffolding)
public partial class ShopContext
{
partial void OnModelCreatingPartial(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>().HasQueryFilter(p => p.InStock);
}
}
// Controllers/ProductsController.cs
public class ProductsController : Controller
{
private readonly ShopContext _db;
public ProductsController(ShopContext db) => _db = db;
public async Task<IActionResult> Index()
{
var products = await _db.Products
.Include(p => p.Category)
.OrderBy(p => p.Name)
.AsNoTracking()
.ToListAsync();
return View(products);
}
}
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready