DEV Community

Ivan Rossouw
Ivan Rossouw

Posted on AI-assisted

When EF Core Sees Only Part of Your Model

Framework warnings are valuable because they turn hidden risk into visible friction. But a warning is still an interpretation of the evidence available to the tool. In a modular application, the command-line process may not load the same assemblies as the running application. When that happens, the design-time model can be a projection rather than the whole truth.

That creates an uncomfortable situation: a migration warning may be both technically correct about the model it sees and dangerously wrong about the database operation it proposes.

The answer is not to ignore warnings casually. It is to identify the boundary, narrow the exception, and replace the lost signal with a stronger executable invariant.

The model can change with the process

Consider a lower-level persistence module that owns a database context. At runtime, higher-level modules can contribute entity configurations through a discovery mechanism. This preserves the dependency direction: the lower-level module does not need references back to every feature that extends it.

Now run command-line migration tooling from the lower-level project. The higher-level contributor may not be loaded, because adding that reference would create a project cycle. The tool builds a valid model, but it is only the portion visible from that process.

The migration snapshot, meanwhile, includes the contributed table. It was created from the full application model and must remain there for every host that uses the shared schema.

The differ compares those two views and concludes that the table exists in the snapshot but not in the current model. Its proposed fix is logical from that narrow perspective: drop the table.

From the system’s perspective, that operation is destructive.

Do not “fix” the snapshot to match a blind spot

Regenerating the snapshot until the warning disappears is tempting. It is also exactly the wrong response when the design-time model is incomplete by construction.

The first diagnostic question should be: what is the entire proposed difference? A probe migration can make the answer concrete. If the only change is removal of a table supplied by a module that the tooling process cannot load, the warning describes a visibility gap—not necessarily real drift.

This distinction matters. “The warning is a false positive” is too broad. A particular warning, for a particular context, under a documented loading boundary may be a false positive. Other differences could still be real.

The safe decision is therefore narrow: allow the legitimate database-update path to proceed despite that one known condition, while keeping a separate full-model comparison as the authoritative drift test.

Suppression spends a safety signal

Ignoring a warning removes friction. It does not remove the underlying ambiguity.

If a design-time factory suppresses a known pending-model warning, the team has spent a safety signal. That signal should be replaced with evidence closer to the failure mode.

In this case, the feared outcome is not an abstract “model mismatch.” It is a migration that drops, renames, or damages a table the design-time process cannot see. That can be tested directly.

An executable migration guard can inspect the operations produced by every committed migration and fail when it finds:

  • a drop of the protected table;
  • a rename that moves it out of the contract;
  • a column removal targeting that table; or
  • raw SQL containing the destructive operation.

This is more specific than the original warning, and that specificity is a strength. The test says exactly which invariant must survive.

Negative guards need positive controls

There is a subtle testing trap here. A test that scans migrations and finds no forbidden operation can pass because the migrations are safe—or because discovery found nothing.

That is a vacuous pass.

Add a positive control that proves the scan sees the migration that originally created the protected table. Now the pair of tests establishes both halves of the claim:

  1. the evidence set is present; and
  2. none of that evidence violates the destructive-operation invariant.

This pattern generalizes well. Whenever a test says “nothing bad exists,” pair it with proof that the search space is populated. It is useful for dependency scans, authorization maps, route inventories, migration checks, and reflection-based conventions.

Keep the full model as the authority

The migration guard protects one known destructive edge. It does not prove that every legitimate model change has a migration.

For that, retain a snapshot comparison that runs in a process where all runtime contributors are loaded. That test compares the snapshot with the full model rather than the design-time projection.

These checks have different jobs:

  • the full-model comparison detects real snapshot drift;
  • the migration guard blocks the known destructive artifact;
  • the positive control proves the guard actually inspected meaningful input; and
  • the narrowly scoped suppression keeps the operational update path usable.

Together, they are stronger than treating one framework warning as universally authoritative.

The trade-off is deliberate maintenance

This approach is not free. The guard knows about a specific architectural boundary. If extension mechanics change, the tests and documentation must change with them. Contributors need to understand why the suppression exists and why generated migration output still requires review.

The alternative is worse: either block legitimate operations forever, or normalize a habit of dismissing warnings and accepting generated drops.

The practical review rule is simple: when tooling sees only a projection, document the blind spot, scope the exception, and encode the real safety property as a failing test. Framework diagnostics are excellent inputs. Your architecture still owns the final claim.

Top comments (0)