JD.Efcpt.Build: Build‑Time EF Core Scaffolding to Keep Database‑First Models in Sync
Source: Dev.to
Introduction
If you were using Entity Framework when EF Core first dropped, you probably remember the moment you went looking for database‑first support and found… nothing.
EF Core launched as a code‑first framework. The Reverse Engineer tooling that EF6 developers relied on (the right‑click, point‑at‑a‑database, generate‑your‑models workflow) wasn’t there. Microsoft’s position was essentially “migrations are the future, figure it out.”
If your team had an existing database, a DBA who actually owned the schema, or compliance requirements that meant the database was the source of truth… well, good luck with that.
The community’s response was immediate and loud. “Where did database first go?” became a recurring theme in GitHub issues, Stack Overflow questions, and the quiet frustration of developers who just wanted to talk to their database without hand‑writing a hundred entity classes.
The (Temporary) Solution
Eventually, tooling caught up. EF Core Power Tools emerged as the community answer: a Visual Studio extension that brought back the reverse‑engineering workflow.
- Point it at a database or a DACPAC.
- Configure some options.
- Generate your models.
Problem solved… mostly.
The Manual Process Problem
Manual processes work fine—right up until they don’t.
I’ve spent enough time in codebases with legacy data layers to recognize a pattern. It goes something like this:
-
Initial setup – Someone installs EF Core Power Tools, generates the initial models, commits everything, and documents the process:
“When the schema changes, regenerate the models using this tool with these settings.”
Clear enough. -
Time passes – The developer who set it up leaves. Documentation gets stale.
- Someone regenerates with slightly different settings and commits the result.
- Someone else forgets to regenerate after a schema change.
-
Drift – The models drift, the configuration drifts, and nobody’s quite sure what the “correct” regeneration process is anymore. People just… stop doing it consistently.
This isn’t a dramatic failure; it’s a slow erosion. The kind of problem that doesn’t announce itself until you’re debugging a production issue and realize the entity class doesn’t have a column that’s been in the database for six months.
If you’ve worked in a codebase long enough, you’ve probably seen some version of this. Maybe you discovered the drift, maybe you caused it (no judgment—we’ve all been there).
The frustrating part is that the fix is always the same:
- Regenerate the models.
- Commit the changes.
- Remind everyone to regenerate after schema changes.
…and six months later you’re having the same conversation again.
Failure Modes (What Goes Wrong)
| Problem | Description |
|---|---|
| Ownership | Who is responsible for regeneration after a schema change? The schema author? The data‑layer owner? The tech lead? No clear answer → sometimes everyone does it (chaos) and sometimes nobody does it (drift). |
| Configuration | EF Core Power Tools stores settings in JSON files (namespaces, nullable reference types, navigation‑property generation, renaming rules, etc.). Dozens of options mean that different developers can produce inconsistent output from the same database. |
| Tooling | Regeneration requires Visual Studio with the extension installed. CI servers don’t have VS. New developers might not have the extension. Remote‑dev setups may not support it. The process that works on one machine doesn’t necessarily work on another. |
| Noise | Regeneration often produces massive diffs: property reordering, whitespace changes, attribute additions—stuff that isn’t a real schema change but clutters the commit. Developers learn to distrust regeneration diffs, become reluctant to run it, and the problem worsens. |
| Timing | Even when everyone knows the process, there’s no enforcement. Code that references a column the models don’t have can still compile if the path isn’t exercised. The error surfaces later, when the connection to the original schema change is long forgotten. |
None of these issues is individually catastrophic, but together they create a process that works in theory and fails in practice.
A Better Idea: Make Generation Part of the Build
If model generation can be invoked from the command line (it can, via EF Core Power Tools CLI), then it can be part of the build.
- Not a separate step you remember to run.
- Not a manual process with unclear ownership.
- Just something that happens when you run
dotnet build.
The build already knows how to:
- Restore packages.
- Run analyzers.
- Produce artifacts.
Adding “generate EF Core models from the schema” to that list isn’t conceptually different from any other build‑time code generation.
- Ownership disappears – the build owns it.
- Configuration drift disappears – the build uses a single, consistent configuration.
- Tooling issues disappear – every machine runs the same MSBuild target; no special extensions required.
Introducing JD.Efcpt.Build
JD.Efcpt.Build is an MSBuild integration that makes EF Core model generation automatic.
How It Works
- Find the schema source – Either a SQL Server Database Project (
.sqlproj) that compiles to a DACPAC, or a connection string pointing to a live database. - Compute a fingerprint – A hash of all inputs (the DACPAC or schema metadata, the configuration file, renaming rules, any custom templates). This fingerprint represents “the current state of everything.”
- Detect changes – If the fingerprint differs from the previous build, the target runs the generation step; otherwise it skips it.
- Generate models – Calls the EF Core Power Tools CLI with the stored configuration, producing the entity classes in a designated folder.
- Add generated files to the compilation – The generated
.csfiles are included automatically in the project, so they’re compiled together with the rest of the code.
Benefits
| Benefit | Explanation |
|---|---|
| Deterministic output | Same inputs → same generated code on every machine. |
| Zero‑manual steps | No need to remember to run a VS extension; the build does it. |
| CI‑friendly | Works on headless agents; no Visual Studio required. |
| Clear ownership | The build owns regeneration; developers just commit the generated files. |
| Reduced noise | Because generation only runs when the fingerprint changes, diffs are limited to actual schema changes. |
| Configurable | All Power‑Tools options are exposed via a simple JSON file that lives in source control. |
Getting Started
# Add the package to your project
dotnet add package JD.Efcpt.Build
Create a efcpt.json (or any name you prefer) in the project root:
{
"ConnectionString": "Server=.;Database=MyDb;Trusted_Connection=True;",
"OutputDirectory": "GeneratedModels",
"Namespace": "MyApp.Data.Models",
"UseNullableReferenceTypes": true,
"GenerateDataAnnotations": true,
"RenamingRules": {
"Tables": { "tbl_": "" },
"Columns": { "col_": "" }
}
}
The package automatically adds an MSBuild target that runs during the build. You can disable it per‑configuration (e.g., only in CI) by setting the EfcptEnabled property.
Summary
- Database‑first support disappeared in early EF Core releases, leading to community frustration.
- EF Core Power Tools filled the gap, but manual regeneration introduced ownership, configuration, tooling, noise, and timing problems.
- Embedding generation in the build eliminates those problems.
JD.Efcpt.Buildprovides a lightweight, deterministic, CI‑friendly solution that makes EF Core model generation a first‑class part of your build pipeline.
Give it a try, and let the build keep your models in sync with the schema—once and for all.
Overview
The package automates EF Core model generation during the build. It:
- Detects changes by comparing a fingerprint of the inputs (using XxHash64).
- Skips generation when the fingerprint matches the previous run – zero overhead for incremental builds.
- Runs generation only when inputs actually change.
What it does
- Generates models with EF Core Power Tools CLI (the same command you would run manually).
- Output:
obj/efcpt/Generated/with a.g.csextension.
- Output:
- Adds generated files to compilation automatically – no need to edit the project file or manage includes.
Modes
Different teams manage database schemas differently, so the package supports two modes.
DACPAC Mode
- For teams using SQL Server Database Projects (
.sqlproj). - The project defines the schema in version‑controlled SQL files.
- The package builds the project, produces a DACPAC, and generates models from it.
..\Database\MyDatabase.sqlproj
Benefits
- Schema lives in source control.
- Changes go through pull requests.
- The DACPAC is a deterministic build artifact.
Connection‑String Mode
- For teams without a database project.
- Useful when you scaffold from a dev database, a cloud database, or simply don’t want DACPACs.
$(DB_CONNECTION_STRING)
- The package connects, queries system tables, and generates models from the schema metadata.
- The fingerprint is computed from that metadata, so incremental builds still work.
Both modes share the same configuration files and produce identical output; they only differ in where the schema comes from.
Minimum Setup
Add the package reference:
<PackageReference Include="JD.Efcpt.Build" Version="x.y.z" />
If you have a .sqlproj in the solution and an efcpt-config.json in the project directory, that’s all you need. Run dotnet build and the models appear.
Configuration (efcpt-config.json)
{
"names": {
"root-namespace": "MyApp.Data",
"dbcontext-name": "ApplicationDbContext"
},
"code-generation": {
"use-nullable-reference-types": true,
"enable-on-configuring": false
}
}
enable-on-configuring: false→ the generatedDbContextdoes not contain a hard‑coded connection string; you configure it in your DI container.
Renaming Rules
If your database naming conventions don’t map cleanly to C#, add renaming rules:
[
{
"SchemaName": "dbo",
"Tables": [
{
"Name": "tbl_Users",
"NewName": "User",
"Columns": [
{ "Name": "user_id", "NewName": "Id" }
]
}
]
}
]
Result: tbl_Users.user_id → User.Id. The database keeps its conventions, your C# code follows its own, and the mapping is version‑controlled.
Partial Classes – Keeping Custom Logic Safe
Generated entity (auto‑generated, regenerated each build):
// obj/efcpt/Generated/User.g.cs
public partial class User
{
public int Id { get; set; }
public string Email { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Your custom logic (never overwritten):
// Models/User.cs
public partial class User
{
public string FullName => $"{FirstName} {LastName}";
public bool HasValidEmail => Email?.Contains("@") ?? false;
}
Both halves compile into a single class. This separation is cleaner than mixing generated and hand‑written code in one file.
CI/CD – Zero‑Touch Integration
Manual regeneration doesn’t work well in pipelines. With build‑time generation, CI just builds:
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Build
run: dotnet build --configuration Release
env:
DB_CONNECTION_STRING: ${{ secrets.DB_CONNECTION_STRING }}
- No extra steps for EF Core generation.
- On .NET 10+, the package uses
dotnet dnxto execute the tool directly from the package feed (no global tool install). - On older versions it falls back to tool manifests or global tools.
Pull requests that modify the schema automatically produce the corresponding model changes, keeping schema and code in sync.
Debugging & Diagnostics
Verbose Logging
<LogLevel>detailed</LogLevel>
Build output now shows which inputs were found, the computed fingerprint, and whether generation ran or was skipped.
Inspect Resolved Inputs
After a build, open obj/efcpt/resolved-inputs.json. It lists exactly what the package considered as inputs for each mode.
Inspect the Fingerprint
obj/efcpt/fingerprint.txt contains the current fingerprint. Compare it with previous runs to understand why generation ran (or didn’t).
Final Note
The package is designed to be practical: fast fingerprint checks keep incremental builds cheap, and the generated code lives in a predictable location (obj/efcpt/Generated). Use partial classes for custom logic, configure the mode that matches your workflow, and let the build handle everything else.
Who This Is For
- Database‑first development where you’ve run into the regeneration coordination problem.
- Projects that change the schema frequently (e.g., weekly releases) and find manual regeneration a source of friction.
- Teams that need builds to behave identically everywhere – local machines, CI servers, new developer laptops – so every developer gets the same generated code from the same inputs.
Who This Probably Isn’t For
- Schemas that are essentially static; rare changes make manual regeneration tolerable.
- Code‑first migrations as the source of truth – a different problem space.
- Projects that don’t use EF Core Power Tools; this package automates that tool, so a different generation approach won’t apply.