Chapter 16 · Reverse Engineering and Database-First Workflows
Control Tables, Schemas, Naming, Data Annotations, Connection Strings, and Generated Output
Control the reverse-engineering boundary with table/schema filters, naming choices, annotations, namespaces, generated paths, and secret-safe connection configuration instead of accepting an uncontrolled code dump.
Learning outcomes
Limit scaffolding to the tables/views/schemas actually owned by the application.
Predict the difference between normalized .NET naming and
--use-database-names.
Choose --data-annotations knowing Fluent API
remains necessary for unsupported configuration.
Control context/entity output directories and namespaces so generated files have an explicit lifecycle.
Keep connection strings and production credentials out of generated source and command history where practical.
Compare provider-specific scaffold options without pretending schemas or naming rules are portable.
1. The practical problem: “scaffold everything” creates an accidental application boundary
The legacy database now contains 180 objects across operational,
audit, staging, billing, and vendor-maintained areas. ServiceHub
needs six. If the team scaffolds the entire database, every
generated DbSet looks available to the application
and every re-scaffold amplifies unrelated churn. Reverse
engineering should define a
bounded database contract, not import whatever
happens to exist.
Course baseline: .NET 10 runtime 10.0.11, SDK 10.0.400, EF Core/dotnet-ef/Microsoft.EntityFrameworkCore.Design/Microsoft.EntityFrameworkCore.Sqlite 10.0.11. SQLite is the mandatory free/local provider. EF Core 11 preview APIs are out of scope unless clearly labeled.
2. Table and schema filters are part of architecture
The .NET CLI accepts repeatable --table and
--schema filters. On providers with schemas,
--schema ops includes all tables/views in that
schema; explicit table names can be schema-qualified. SQLite
does not expose SQL Server/PostgreSQL-style schemas, so the
mandatory SQLite lab uses table/view filters and labels schema
examples as provider-specific.
dotnet tool run dotnet-ef dbcontext scaffold "Data Source=servicehub-legacy.db" Microsoft.EntityFrameworkCore.Sqlite --table work_orders --table technicians --table open_work_order_summary --context ServiceHubLegacyContext --output-dir Persistence/Generated/Entities --context-dir Persistence/Generated --no-onconfiguring
dotnet ef dbcontext scaffold "$SERVICEHUB_SQLSERVER" Microsoft.EntityFrameworkCore.SqlServer --schema ops --table audit.LegacyImportLog --context ServiceHubLegacyContext --no-onconfiguring
Do not copy the SQL Server schema example to SQLite and assume it means the same thing. Filtering semantics are provider/database metadata semantics exposed through EF tooling.
3. Naming: database fidelity vs application readability
By default EF fixes database identifiers toward normal .NET
naming conventions. --use-database-names preserves
database names as much as valid C# permits; invalid identifiers
still require synthesis, and navigation names are still
generated. --no-pluralize controls pluralization
separately.
| Database object | Default scaffold tendency | With --use-database-names |
|---|---|---|
work_orders |
WorkOrder entity /
WorkOrders DbSet
|
closer to work_orders, subject to valid C#
identifiers
|
customer_name |
CustomerName |
closer to customer_name |
1st-contact |
synthesized valid C# name | still must be made into a valid C# identifier |
| relationship names | generated from FK/entity context | still generated; database names cannot fully determine domain navigation names |
dotnet ef dbcontext scaffold "Data Source=servicehub-legacy.db" Microsoft.EntityFrameworkCore.Sqlite --table work_orders --output-dir Scratch/Normalized --context NormalizedContext --no-onconfiguringdotnet ef dbcontext scaffold "Data Source=servicehub-legacy.db" Microsoft.EntityFrameworkCore.Sqlite --table work_orders --output-dir Scratch/DatabaseNames --context DatabaseNamesContext --use-database-names --no-pluralize --no-onconfiguring
Choose a convention and keep it stable. Repeatedly switching naming switches creates source churn unrelated to schema drift.
4. Data annotations do not eliminate Fluent API
--data-annotations asks the scaffolder to prefer
attributes when a mapping can be represented that way. It does
not mean every relational detail has a suitable attribute.
Provider-specific facets, default SQL, indexes, delete behavior,
view mapping, composite details, or other configuration may
still appear in OnModelCreating.
dotnet ef dbcontext scaffold "Data Source=servicehub-legacy.db" Microsoft.EntityFrameworkCore.Sqlite --table work_orders --table technicians --data-annotations --context AnnotatedLegacyContext --output-dir Scratch/Annotated --no-onconfiguring
Attributes generated from database metadata are still generated code. Do not add hand-written validation/domain attributes directly to those files if re-scaffolding can overwrite them; Lesson 3 moves custom behavior to partials and stable extension files.
5. Output directories and namespaces are lifecycle controls
Generated code should have an obvious ownership boundary.
ServiceHub places the context and entities under
Persistence/Generated, while handwritten code lives
elsewhere. --context-dir,
--output-dir, --namespace, and
--context-namespace make that boundary explicit.
dotnet ef dbcontext scaffold "Data Source=servicehub-legacy.db" Microsoft.EntityFrameworkCore.Sqlite --context ServiceHubLegacyContext --context-dir Persistence/Generated --output-dir Persistence/Generated/Entities --context-namespace ServiceHub.LegacyScaffold.Persistence.Generated --namespace ServiceHub.LegacyScaffold.Persistence.Generated.Entities --no-onconfiguring
A generated directory is easy to delete, regenerate, diff, and exclude from handwritten code review patterns. Mixing generated and custom files in the same folder makes accidental overwrite harder to spot.
6. Connection strings: values are required at design time, secrets are not source code
The tool must connect to the source database. That does not justify committing a production password. For the local SQLite lab the connection string contains only a disposable file path. For authenticated databases, prefer secure local/environment configuration and the provider's recommended authentication flow. Avoid shell history leakage where credentials are sensitive.
A command copied into README, CI logs, shell history,
generated OnConfiguring, or source control can
expose credentials long after scaffolding completes. Treat
connection strings as secrets whenever they carry
authentication material.
$env:SERVICEHUB_DB = 'Data Source=servicehub-legacy.db'dotnet ef dbcontext scaffold $env:SERVICEHUB_DB Microsoft.EntityFrameworkCore.Sqlite ` --context ServiceHubLegacyContext ` --no-onconfiguringRemove-Item Env:SERVICEHUB_DB
EF also supports configuration-name patterns in appropriate design-time setups, but whether the tool can resolve them depends on how the project creates configuration at design time. Verify with a disposable credential rather than assuming runtime DI magically exists during scaffolding.
7. Provider-specific options and schema meaning
| Concern | SQLite mandatory path | SQL Server/PostgreSQL-style path |
|---|---|---|
| Schemas | No normal multi-schema namespace; filter tables/views | --schema can select database schemas |
| Identifier case/collation | SQLite-specific rules | Engine/provider-specific; can affect generated names and queries |
| Store types | SQLite affinity-oriented | Richer provider type metadata may be preserved |
| Concurrency inference | Application token usually not inferable from generic columns |
SQL Server rowversion can be recognized;
other mechanisms are provider-specific
|
| Comments/defaults/computed metadata | Only what provider exposes and EF models | Provider may expose richer metadata; verify generated mapping |
The same scaffold command syntax does not imply identical metadata fidelity. Keep provider-specific reverse-engineering tests near the provider integration layer.
8. Failure case: filters silently omit a required relationship target
Suppose you scaffold work_orders but omit
technicians. The tool cannot generate a normal
relationship to an entity type that does not exist in the
selected model. The foreign-key scalar may remain, while
navigation shape changes or disappears. That is not necessarily
a tooling bug; you intentionally changed the model boundary.
dotnet ef dbcontext scaffold "Data Source=servicehub-legacy.db" Microsoft.EntityFrameworkCore.Sqlite --table work_orders --context WorkOrdersOnlyContext --output-dir Scratch/TooNarrow --no-onconfiguring
Repair by including relationship targets that the application needs, or consciously keep the FK scalar-only boundary. Verify the generated model and query shape instead of assuming a navigation must exist.
9. Lab: generate three controlled variants and diff them
- Create the same disposable SQLite database from Lesson 1.
-
Generate
Normalized,DatabaseNames, andAnnotatedoutputs into separate scratch folders. -
Diff entity/property names, attributes, and
OnModelCreating. - Run a metadata probe and ensure all three variants map the same selected tables/keys/relationships.
-
Confirm no generated file contains the connection string when
--no-onconfiguringis used. - Delete the scratch folders; keep only the documented scaffold command and stable handwritten extensions.
grep -R "Data Source=" Scratch || truegit diff --no-index Scratch/Normalized Scratch/DatabaseNames || truegit diff --no-index Scratch/Normalized Scratch/Annotated || true
On Windows without grep, use
Select-String -Path Scratch\**\*.cs -Pattern 'Data
Source='. The goal is observable secret hygiene, not a specific shell.
10. Production judgment and bridge
Choose filters based on application ownership, not convenience. Choose naming based on long-term code readability and regeneration stability. Choose annotations only when they improve the team's generated-code policy, not because they are “more EF.” Keep design-time credentials external, and review provider-specific scaffold output whenever the database engine changes.
The next lesson solves the core maintainability problem: how to
add domain behavior, model overrides, services, and
generated-code customization without creating files that
--force will erase.
Check your understanding
- What does
--schemamean on SQLite? -
Does
--use-database-namesguarantee byte-for-byte database identifiers become C# names? -
Can
--data-annotationsremove all Fluent API? - Why separate generated output directories?
- Why might omitting a related table change navigation output?
- Where should authenticated database credentials live?
Review the answers
1. SQLite does not expose the same multi-schema model as SQL Server/PostgreSQL; use table/view filters for the mandatory SQLite path.
2. No. Invalid C# identifiers and generated navigation names still require synthesis.
3. No. Some relational/provider configuration cannot be represented with attributes and remains in OnModelCreating.
4. It makes ownership, overwrite policy, diffing, and re-scaffolding explicit.
5. The target entity is outside the selected reverse-engineered model, so the relationship cannot be represented normally.
6. In secure external/design-time configuration or provider-recommended authentication flows, not committed generated source.
Authoritative references
Reverse engineering is provider- and version-sensitive. Re-check these primary sources when regenerating the chapter or adapting it to another database engine.
- Reverse Engineering command-line options — table/schema filters, naming, annotations and connection configuration
- EF Core CLI reference — current command and option reference
- Connection strings - EF Core — external configuration patterns
- SQLite provider limitations — SQLite-specific relational limitations
- Microsoft.EntityFrameworkCore.Design 10.0.11 — pinned design-time package