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.

Advanced150–190 minutespreserved customization + T4 labEF Core 10.0.11 · .NET 10.0.11SQLite provider 10.0.11 mandatory baselinedotnet-ef 10.0.11 · SDK 10.0.400Last reviewed: August 2026

Learning outcomes

01

Keep generated entities/context overwriteable by moving behavior into partial classes and stable handwritten files.

02

Use the generated OnModelCreatingPartial hook to layer safe model configuration after scaffolded configuration.

03

Distinguish model customization from domain/application services that do not belong in EF configuration.

04

Install and version EF Core T4 reverse-engineering templates introduced in EF Core 7 and supported in the EF Core 10 toolchain.

05

Update/customize templates intentionally and validate generated model compatibility after template changes.

06

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.

Frozen lab baseline

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.

Generated means reproducible

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.

csharp · generated file — disposable
// 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!;}
csharp · handwritten partial — preserved
// 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.

csharp · representative generated context hook
protected override void OnModelCreating(ModelBuilder modelBuilder){    // ... scaffolded mappings ...    OnModelCreatingPartial(modelBuilder);}partial void OnModelCreatingPartial(ModelBuilder modelBuilder);
csharp · handwritten partial context extension
// 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.

csharp · handwritten extension configuration
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

csharp · bad direct edit
// BAD: added inside Persistence/Generated/Entities/WorkOrder.cspublic bool IsEscalated => Priority >= 4;
shell · overwrite generated output
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.

shell · install EF Core 10 templates
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.

shell · template update checkpoint
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.
Do not copy preview templates into the LTS course

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.

csharp · model compatibility probe
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

  1. Scaffold the Lesson 1 database into Persistence/Generated.
  2. Add DomainExtensions/WorkOrder.Domain.cs and the partial context extension outside the generated folder.
  3. Add a harmless direct edit inside generated WorkOrder.cs.
  4. Run the same scaffold command with --force.
  5. Verify the bad direct edit disappears but the partial extension still compiles and behaves.
  6. Install EF Core Templates 10.0.11 and generate default T4 files.
  7. 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.
  8. Run model/database compatibility probes and delete scratch output.
shell · acceptance checks
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

  1. Why are partial classes a good fit for scaffolded entities?
  2. Where should custom model configuration run relative to generated configuration?
  3. Does marking a SQLite string property as a concurrency token make SQLite generate it?
  4. When were EF reverse-engineering T4 templates introduced?
  5. Why diff customized templates during upgrades?
  6. 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.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.

\n