Chapter 16 · Reverse Engineering and Database-First Workflows
Preserve Custom Code with Partial Classes, Partial Methods, Separate Configurations, and Templates
Keep generated files disposable by moving domain behavior and model extensions into partial classes, the generated OnModelCreatingPartial hook, separate services/configuration, and version-pinned EF Core T4 templates.
Learning outcomes
Keep generated entities/context overwriteable by moving behavior into partial classes and stable handwritten files.
Use the generated OnModelCreatingPartial hook
to layer safe model configuration after scaffolded
configuration.
Distinguish model customization from domain/application services that do not belong in EF configuration.
Install and version EF Core T4 reverse-engineering templates introduced in EF Core 7 and supported in the EF Core 10 toolchain.
Update/customize templates intentionally and validate generated model compatibility after template changes.
Demonstrate a force re-scaffold that erases a bad direct edit while preserved partial code survives.
1. The practical problem: generated code is useful precisely because it is replaceable
The team adds IsEscalated() directly to scaffolded
WorkOrder.cs, changes a generated navigation name,
and patches OnModelCreating. Three months later a
schema change requires
dotnet ef dbcontext scaffold --force; the edits
disappear. The mistake was not re-scaffolding—the mistake was
treating generated files as the home for custom behavior.
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.
A sustainable database-first project should be able to delete and regenerate the generated folder from a known schema/toolchain. If that destroys irreplaceable application behavior, the ownership boundary is wrong.
2. Partial entity classes preserve domain-friendly behavior
EF's default reverse-engineered entity classes are
partial. C# combines multiple partial declarations
into one CLR type at compile time. Put stable application
behavior in a separate non-generated file with the same
namespace and type name.
// Persistence/Generated/Entities/WorkOrder.cspublic partial class WorkOrder{ public long WorkOrderId { get; set; } public string WorkOrderNumber { get; set; } = null!; public long Priority { get; set; } public string Summary { get; set; } = null!;}
// DomainExtensions/WorkOrder.Domain.csnamespace ServiceHub.LegacyScaffold.Persistence.Generated.Entities;public partial class WorkOrder{ public bool RequiresSupervisorReview() => Priority >= 4; public string DisplayLabel => $"{WorkOrderNumber} · {Summary}";}
The second file survives re-scaffolding because the tool only overwrites its generated output path. Keep the derived property unmapped unless the generated model discovers it by convention; if necessary, ignore it in the partial model hook shown next.
3. Use OnModelCreatingPartial as the generated
model extension seam
The generated context normally ends
OnModelCreating by calling a partial method.
Implement that method in another partial file. Because it runs
after scaffolded configuration, later configuration can augment
or override compatible facets. Do not use it to lie about the
physical database.
protected override void OnModelCreating(ModelBuilder modelBuilder){ // ... scaffolded mappings ... OnModelCreatingPartial(modelBuilder);}partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
// Persistence/ServiceHubLegacyContext.Custom.cspublic partial class ServiceHubLegacyContext{ partial void OnModelCreatingPartial(ModelBuilder modelBuilder) { modelBuilder.Entity<WorkOrder>() .Ignore(w => w.DisplayLabel); // Restore application intent the SQLite schema cannot infer. modelBuilder.Entity<WorkOrder>() .Property(w => w.Revision) .IsConcurrencyToken(); }}
The concurrency configuration is a deliberate application
policy. It is valid only if the application also advances
Revision consistently, just as Chapters 04 and 13
established. Adding IsConcurrencyToken() does not
make SQLite auto-generate a token.
4. Separate configuration and services when they improve ownership
For larger customizations, an
IEntityTypeConfiguration<WorkOrder> keeps the
partial context small. Apply it from
OnModelCreatingPartial. Domain workflows,
authorization, HTTP ETags, and business services should remain
outside EF configuration entirely.
public sealed class WorkOrderLegacyExtensionConfiguration : IEntityTypeConfiguration<WorkOrder>{ public void Configure(EntityTypeBuilder<WorkOrder> builder) { builder.Ignore(w => w.DisplayLabel); builder.Property(w => w.Revision).IsConcurrencyToken(); }}public partial class ServiceHubLegacyContext{ partial void OnModelCreatingPartial(ModelBuilder modelBuilder) => modelBuilder.ApplyConfiguration(new WorkOrderLegacyExtensionConfiguration());}
If a custom configuration contradicts the physical database—for example marking a nullable database column as required—EF may generate incorrect materialization/write behavior. “Custom” does not mean “free to invent a different schema contract.”
5. Deliberate failure: edit the generated file, then force re-scaffold
// BAD: added inside Persistence/Generated/Entities/WorkOrder.cspublic bool IsEscalated => Priority >= 4;
dotnet ef dbcontext scaffold "Data Source=servicehub-legacy.db" Microsoft.EntityFrameworkCore.Sqlite --context ServiceHubLegacyContext --context-dir Persistence/Generated --output-dir Persistence/Generated/Entities --no-onconfiguring --force
Expected result: the direct edit disappears. That is the
intended behavior of --force. Repair by moving the
property/method to the partial file outside the generated
directory, repeat the scaffold, and verify the partial still
compiles.
6. T4 templates customize generation itself
Since EF Core 7, reverse-engineering C# generation can be
customized with T4 text templates. In the current stable
baseline, pin
Microsoft.EntityFrameworkCore.Templates to 10.0.11,
install the template package, then add the default
DbContext.t4 and EntityType.t4 files
into CodeTemplates/EFCore.
dotnet new install Microsoft.EntityFrameworkCore.Templates@10.0.11dotnet new ef-templates# Generated in the project:# CodeTemplates/EFCore/DbContext.t4# CodeTemplates/EFCore/EntityType.t4
T4 executes at scaffolding time. It can change namespace imports, interfaces, class shape, comments, or emitted configuration. This is powerful because every re-scaffold uses the template; it is dangerous for exactly the same reason. Keep template changes small, version controlled, reviewed, and tested against a representative schema.
7. Template changes have their own upgrade lifecycle
Once copied into the project, templates are code you own. Updating EF Core packages does not guarantee your customized copies automatically acquire improvements from new defaults. Before an EF upgrade, compare your templates against the new package version, port intentional changes, and regenerate into a scratch directory.
dotnet new update# Review the installed EF template package version.# Recreate default templates in a scratch project at 10.0.11 (or the target stable version).# Diff your CodeTemplates/EFCore/*.t4 against the new defaults before adopting changes.
EF Core 11 preview templates can emit code or conventions not present in EF Core 10. Chapter 16 keeps T4 package/tool/provider versions on the stable 10.0.11 line.
8. Validate template output against database compatibility
A custom template can remove provider facets that are irrelevant
to your app—or accidentally remove something essential.
Microsoft recommends validating the resulting model remains
compatible with the database. One practical signal is
Database.GenerateCreateScript(): it shows the
database objects EF believes its model would require. It is not
a live-database diff, but differences can reveal a damaged
generated model.
await using var db = new ServiceHubLegacyContext(options);var script = db.Database.GenerateCreateScript();Console.WriteLine(script);foreach (var entity in db.Model.GetEntityTypes()){ Console.WriteLine($"{entity.DisplayName()} -> {entity.GetTableName() ?? entity.GetViewName()}");}
Also execute representative reads/writes against a disposable copy of the real provider. A pretty generated class is not evidence that type mapping, defaults, concurrency, or relationship behavior stayed correct.
9. Lab: prove custom code survives regeneration
-
Scaffold the Lesson 1 database into
Persistence/Generated. -
Add
DomainExtensions/WorkOrder.Domain.csand the partial context extension outside the generated folder. -
Add a harmless direct edit inside generated
WorkOrder.cs. -
Run the same scaffold command with
--force. - Verify the bad direct edit disappears but the partial extension still compiles and behaves.
- Install EF Core Templates 10.0.11 and generate default T4 files.
- Make one small, reviewable template change (for example, a generated-code comment or interface that has no database semantics), re-scaffold into scratch, and diff output.
- Run model/database compatibility probes and delete scratch output.
git diff -- Persistence/Generated CodeTemplates/EFCore# Build the project.# Run the metadata/GenerateCreateScript probe.# Run at least one query and one safe write on the disposable database.
10. Production judgment and bridge
Use partials and extension hooks for application behavior that should survive re-scaffolding. Use T4 only when the desired change is genuinely a generation policy shared across many generated types. Keep security policy, orchestration, external I/O, and business workflows outside generated EF code. Review template upgrades just like source generators or compiler tooling.
The next lesson turns this into an operational workflow: regenerate into scratch, diff generated output, classify database drift, and decide whether the database remains the authoritative schema or the team is deliberately transitioning toward migrations.
Check your understanding
- Why are partial classes a good fit for scaffolded entities?
- Where should custom model configuration run relative to generated configuration?
- Does marking a SQLite string property as a concurrency token make SQLite generate it?
- When were EF reverse-engineering T4 templates introduced?
- Why diff customized templates during upgrades?
-
What should happen to a handwritten edit inside generated
code after
--force?
Review the answers
1. They let handwritten behavior compile into the same CLR type while generated files remain safe to overwrite.
2. In the generated
OnModelCreatingPartial seam, after scaffolded
configuration, with compatibility verified.
3. No. The application must still manage token values.
4. EF Core 7; the chapter pins the current EF Core 10 template package.
5. Copied templates are owned source and do not automatically inherit future default-template fixes.
6. It may be overwritten; move the behavior to preserved partial/separate code instead.
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.
- Custom Reverse Engineering Templates — official T4 workflow, upgrade considerations and advanced customization
- Reverse Engineering - EF Core — generated partial classes/context behavior
- Microsoft.EntityFrameworkCore.Templates 10.0.11 — pinned stable T4 template package
- Entity type configuration — model configuration fundamentals
- GenerateCreateScript API guidance — schema/model tooling context