Two ways to go from schema to entity
There are two legitimate starting points for an EF Core entity, and they solve different problems. If you already have a live, reachable database with real tables, dotnet ef dbcontext scaffold reverse-engineers the actual current schema — including relationships it can detect from foreign keys — into entity classes and a DbContext. If what you have is a CREATE TABLE script — from a design doc, a colleague's migration, or a schema you're about to create and want to preview the C# for first — there's no live database to point a scaffolding tool at, and hand-writing or using a script-based generator like the SQL Table to C# Entity Generator on this site is the faster path.
The real scaffolding command
Against a live database, the actual command looks like this:
dotnet tool install --global dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet ef dbcontext scaffold ^
"Server=localhost;Database=AppDb;Trusted_Connection=True;Encrypt=True;" ^
Microsoft.EntityFrameworkCore.SqlServer ^
-o Models ^
-c AppDbContext ^
--data-annotations
Two prerequisites people miss the first time: the dotnet-ef tool has to be installed separately (it isn't bundled with the SDK), and the project needs both the provider package (Microsoft.EntityFrameworkCore.SqlServer here) and Microsoft.EntityFrameworkCore.Design referenced, or the command fails immediately with a missing-package error. Getting that connection string right first is worth doing carefully — see Connection Strings in .NET 8 for what each provider actually expects. Re-running the same scaffold command after the schema changes refuses to overwrite existing files unless you add --force — that's a safety net, not a bug, since scaffolding overwrites the entire generated file rather than merging changes into anything you've hand-edited.
What scaffolding puts where: Fluent API by default
Left to its defaults, dotnet ef dbcontext scaffold puts every piece of configuration — column types, required/optional, keys, indexes, relationships — into the generated DbContext's OnModelCreating method using the Fluent API, and leaves the entity classes themselves as plain, attribute-free POCOs. The --data-annotations flag changes that, pushing configuration onto the entity classes as attributes ([Key], [Required], [MaxLength]) instead. Neither is more "correct" — it's the same decision the SQL Table to C# Entity Generator exposes as its own config-style toggle, because both real EF Core tooling and a from-scratch generator have to make the same call.
Data annotations vs Fluent API: the actual tradeoff
Data annotations read well right next to the property they describe, but they couple your domain model to System.ComponentModel.DataAnnotations and EF Core-specific attributes — inconvenient if that entity class needs to stay a plain, framework-agnostic POCO for other reasons. Fluent API keeps the entity class completely clean and puts all mapping decisions in one place per entity via a separate IEntityTypeConfiguration<T> class, registered with modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly) — and it can express things data annotations simply can't, like composite keys via HasKey(x => new { x.ColA, x.ColB }), since System.ComponentModel.DataAnnotations's own [Key] attribute has no composite-key syntax at all.
Nullable reference types: what actually changes
Nullable reference types (NRT) only affect reference-type properties — string and byte[] in a typical scaffolded entity. A NOT NULL integer column always generates a plain int; a nullable integer column always generates int? — that's ordinary nullable-value-type syntax and has nothing to do with the NRT feature or your project's <Nullable> setting. Where NRT actually matters is a nullable NVARCHAR column: with NRT disabled, it generates as a plain string regardless of nullability, since a pre-NRT compiler has no way to express "this string might be null" in the type system; with NRT enabled, the same column generates as string?, making the possible-null state visible to the compiler and to anyone calling .Length on it without a null check.
Worth remembering: NRT annotations are a compile-time signal for the compiler's null-flow analysis, not a runtime guarantee. Nothing stops a string property from actually holding null at runtime if it comes from deserialized JSON, reflection, or any other path that bypasses the constructor — NRT catches accidental null dereferences in code you write against the entity, it doesn't validate the database's actual contents.
The gap both approaches share: foreign keys
Real dotnet ef dbcontext scaffold, reading a live database, does detect foreign key constraints and generates navigation properties for them automatically — one of its biggest advantages over any script-based generator. A generator working only from a CREATE TABLE script — like the one on this site — can only see what's in that single script; if the foreign key is declared with a separate ALTER TABLE ... ADD CONSTRAINT statement, common when generating schema through a migration tool, rather than inline in the same CREATE TABLE, neither a script-based generator nor a human skimming the script would catch it without also checking for that separate statement. That's exactly why the SQL Table to C# Entity Generator explicitly recognizes and skips FOREIGN KEY, UNIQUE, and INDEX clauses rather than trying to guess navigation properties from them — an incorrect guess is worse than an honest gap you fill in by hand.
Re-scaffolding vs Migrations: don't run both against the same model
dotnet ef dbcontext scaffold and dotnet ef migrations add solve opposite directions of the same problem, and mixing them on the same entities causes real confusion. Scaffolding reads an existing database and writes C# to match it — database-first. Migrations read your C# model and generate SQL to evolve the database to match it — code-first. Once a project has started using Migrations to manage schema changes, re-running scaffold against that same database regenerates entity files and can silently overwrite hand-added properties, custom validation attributes, or navigation properties you added after the initial scaffold, none of which get merged back — the command replaces the file, it doesn't diff it. Scaffolding is the right tool for the initial reverse-engineering step, or for a one-off reporting model against a database you don't own the schema of; once your team owns the schema and evolves it through Migrations, treat further scaffold runs as a "regenerate from scratch and manually re-apply my customizations" operation, not a routine sync command.
A smaller but common surprise on a first scaffold: table names don't automatically get pluralized or singularized to match C# naming conventions — a table literally named tbl_customer_orders scaffolds as a class named exactly TblCustomerOrder after only the underscore-to-PascalCase conversion, with no attempt to guess a "nicer" singular or plural form. If your database's table-naming convention doesn't already read naturally as a C# class name, expect to rename the generated classes by hand — the tool converts casing, not vocabulary.
When to hand-write instead of scaffold
Reach for real dotnet ef dbcontext scaffold when a live database is the source of truth and you want relationships detected automatically. Reach for a script-based generator, or hand-writing, when you're designing a new table and want to see the C# shape before the table exists anywhere — previewing what a connection string will eventually point to, before the database behind it has been built. Once the schema is real and connected, that's the point where LINQ queries against these entities start generating actual SQL, worth checking with the same care described in how LINQ translates to SQL — try a query against your freshly scaffolded entity in the LINQ to SQL Converter before running it for real, since a wrong assumption about a nullable column here shows up as a wrong WHERE clause there.