Skip to content
KeelMatrixPublic

About

Detect EF Core migrations that can break rolling deployments, lose data, or block production traffic, and explain a safer rollout before merge.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

81 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

KeelMatrix.EfGuard

Your migration compiles. EF Core accepts it. Your rolling deployment can still fail. EfGuard compares application/schema generations and provider behavior to find dangerous rollout states before they merge.

EfGuard is a local .NET tool that analyzes EF Core migrations without connecting to or modifying a database. It reports compatibility, data-loss, and provider blocking risks for SQL Server/Azure SQL and PostgreSQL.

Package README relationship

This is the repository and developer-facing README. The standalone NuGet package README is src/KeelMatrix.EfGuard/README.md; it is the file packed as README.md at the package root. Keep package-specific usage and limitations in that project-local document. The pinned public install example is intentionally repeated here so this README is independently useful and remains version-coherent with the package README.

Install

Install the public .NET tool globally:

dotnet tool install --global KeelMatrix.EfGuard --version 0.1.1

The project-local package README contains matching update and uninstall commands.

Quick Start

From a straightforward single-project, single-context repository:

efguard check

For a rolling-deployment comparison with the previous application model:

efguard check --baseline origin/main

Design-time discovery

With --startup-project, EfGuard follows the normal EF Core design-time startup path in a bounded child process: it resolves the startup host through BuildWebHost, CreateWebHostBuilder, CreateHostBuilder, or the startup entry point, then resolves the selected DbContext (or IDbContextFactory<TContext>) from the startup application's scoped services. An IDesignTimeDbContextFactory<TContext> and then a parameterless constructor are fallbacks when startup services do not provide the context. Keep all design-time startup code and factories free of production connections and side effects. The worker response file is limited to 4 MiB (4,194,304 bytes); an oversized response is an untrustworthy extraction failure and returns exit code 2.

In repositories with more than one DbContext, --context also scopes migration discovery. EfGuard analyzes exactly the migrations EF Core attributes to the selected context through its migrations metadata (the IMigrationsAssembly service and the context's configured migrations assembly). EF Core treats a migration as part of a context only when the migration carries [DbContext(typeof(ThatContext))], so migrations of another context are never analyzed for the selected context and a context that has no migrations reports zero operations. When no migrations are attributed to the selected context while its migrations assembly still contains migration classes that no [DbContext] attribute claims, EfGuard fails closed with an actionable diagnostic instead of scanning every Migration subclass in that assembly.

Network and Offline Behavior

Core analysis is local and offline. EfGuard never restores packages and never contacts a package feed, so the dependency graph must come from a restore you already ran:

dotnet restore src/Orders/Orders.csproj
efguard check --project src/Orders/Orders.csproj

Extraction builds the selected project with --no-restore against that restored graph. When the graph is missing, EfGuard fails closed with exit code 2 and tells you to run dotnet restore; it never silently falls back to a network restore. --baseline reuses the same restored graph from your working tree inside an isolated temporary checkout, so the active worktree is never modified. The only normal network behavior is telemetry delegated to the shared client.

Exit Codes and Output

  • 0: trustworthy analysis completed with no configured blocking or unverified diagnostic.
  • 1: trustworthy analysis completed with a blocking or unverified diagnostic.
  • 2: analysis could not complete trustworthily because of configuration, extraction, build, provider, baseline, or internal error.

Use --format json for automation. JSON has schemaVersion: 1, provider, providerSql, compatibility, baseline, summary, diagnostics, and errors. Each diagnostic contains ruleId, title, riskDimensions, severity, confidence, optional provider/migration/location, affectedState, explanation, remediation, and uncertainty.

providerSql records provider-generated SQL for the migration operations when the target provider exposes that service. Each statement carries its migration and operation ordinal. EFG301 and EFG302 require exactly one matching operation-level statement with the expected SQL shape; EFG303 uses provider engine evidence independently because it covers foreign-key validation. engineVerified is true only when the repository's real database-engine integration gates have verified the provider behavior behind every provider finding in the report. EfGuard ships that evidence as a versioned provider engine evidence manifest, and it only promotes a provider-locking finding to HIGH/high confidence when the manifest covers the finding's rule and provider. Missing or unverified engine evidence leaves the finding UNVERIFIED instead of asserting a high-confidence provider claim.

Risk Dimensions

Findings identify the affected risk dimensions separately:

  • compatibility: an application generation and schema generation may not coexist.
  • data-loss: values may be removed or rejected by the change.
  • blocking: the operation may hold locks or wait on concurrent work.
  • rollback: the change reduces recovery or rollback options.
  • provider: the result depends on provider-specific behavior.

An UNVERIFIED severity or unknown confidence indicates that the available static evidence is insufficient; it is not a clean result. EfGuard does not prove runtime lock duration or guarantee zero downtime.

Configuration and Suppressions

Create efguard.json at the repository root:

{
  "version": 1,
  "deployment": {
    "strategy": "rolling",
    "minimumCompatibleVersions": 1
  },
  "rules": {
    "EFG302": "error"
  },
  "suppressions": [
    {
      "rule": "EFG101",
      "migration": "20260820153000_RemoveLegacyCode",
      "reason": "The old application version is retired before deployment",
      "expires": "2026-09-30"
    }
  ]
}

Severity values are error/block, warning/high, info/advisory, unverified, and off. Suppressions require a reason. Expired suppressions produce EFG998 and do not hide the original finding. Configuration never accepts connection strings or credentials.

Rules

ID Diagnostic Default Risk
EFG101 Rolling-deployment incompatibility BLOCK compatibility, data loss
EFG102 Required column may reject existing rows BLOCK compatibility, data loss
EFG201 Destructive column/table change HIGH data loss, rollback
EFG202 Unsafe column alteration BLOCK compatibility, data loss
EFG204 Unique index/constraint validation risk HIGH compatibility, blocking
EFG301 Potentially blocking SQL Server index creation HIGH blocking, provider
EFG302 Write-blocking PostgreSQL index creation HIGH blocking, provider
EFG303 Foreign-key validation and locking risk HIGH compatibility, blocking
EFG304 Unbounded data backfill HIGH blocking, data loss
EFG305 Transaction semantics require review HIGH blocking, provider
EFG399 Unverified operation or compatibility history UNVERIFIED compatibility, provider
EFG900 Unsupported database provider UNVERIFIED provider
EFG998 Expired suppression BLOCK configuration

Unknown operations, raw SQL, and custom operations are never silently treated as safe. Transaction suppression is an independent risk dimension: arbitrary SQL retains EFG399 and may also report EFG305, while a classified unbounded backfill may report EFG304 and EFG305 together. SQL is classified locally and is not included in telemetry. Provider lock behavior depends on engine version, capabilities, and workload; EfGuard cannot guarantee zero downtime.

Provider behavior claims (EFG301, EFG302, and EFG303) are promoted to HIGH/high confidence only when the repository's real SQL Server and PostgreSQL integration gates have verified that behavior and the shipped provider engine evidence covers the provider. Those gates execute representative provider SQL against real engines and record the verified claims; EFG301 and EFG302 additionally require matching provider-generated SQL in the report. Unverified claims stay UNVERIFIED and normally produce exit code 1.

Rollout Guidance

Prefer expand, transition, cutover, and contract stages. Add compatible schema first, deploy code that can read both generations, backfill in bounded/resumable batches, switch reads/writes, and remove old schema only after old instances are retired. Provider-specific online or concurrent options still require operational validation.

The baseline matrix is evaluated from extracted EF models and is included in JSON as compatibility: previous application + previous schema, previous application + target schema, current application + previous schema, and current application + target schema. With rolling, both overlap states are blocking when the evidence shows incompatibility. With expand-contract, previous-application + target-schema is advisory because the strategy declares a staged contract order; current-application + previous-schema is not required. With blue-green, neither overlap state is required because the strategy declares isolated application/schema cutover. These strategy verdicts still do not inspect live traffic, queries, or deployment ordering.

For the default rolling policy, a scan without --baseline emits EFG399 because compatibility with the previous application/schema generation has no evidence; it cannot exit cleanly unless EFG399 is explicitly overridden or disabled. When minimumCompatibleVersions is greater than one, one --baseline reference is also insufficient to prove the requested history. With the default one-generation policy, a compatible supplied baseline satisfies the history requirement; operation-level findings still apply. Without a baseline, EfGuard still analyzes only the latest discovered migration, so operation-level diagnostics are not masked by the history finding. With a baseline, it analyzes migrations present in the current extraction but absent from the baseline extraction, treating those as the pending change set.

Supported Matrix

The CLI targets net8.0. Extraction supports EF Core 8, 9, and 10 projects when their own restored dependency graph can be built. Customer projects targeting net8.0, net9.0, or net10.0 use the matching isolated worker. Supported providers are Microsoft SQL Server/Azure SQL and Npgsql PostgreSQL. Unsupported providers return an unverified provider result and exit 2; EfGuard does not guess provider locking behavior.

The supported dependency graph includes the standard design-time package Microsoft.EntityFrameworkCore.Design, which the dotnet ef tooling requires, including when only a separate --startup-project references it. Extraction enumerates metadata per copied assembly, so a design-time, build, or Roslyn assembly that cannot be loaded inside the worker is skipped with a recorded note instead of failing the scan. Fail-closed behavior is preserved where it matters: when the assembly that declares the selected DbContext or its migrations cannot be enumerated at all, extraction stops with the assembly name, the underlying exception, and a corrective next step.

The compatibility fixture matrix exercises each supported EF/provider family:

EF Core SQL Server/Azure SQL PostgreSQL/Npgsql
8 covered covered
9 covered covered
10 covered covered

Troubleshooting

  • Project or DbContext discovery is ambiguous: use --project path/to/App.csproj, --startup-project path/to/App.Api.csproj, and --context AppDbContext. When automatic discovery fails, the error identifies the missing or ambiguous selection and shows the corrective option shape.
  • The startup services, design-time factory, or context constructor fails: run the same design-time path locally, keep it free of production connections and side effects, and ensure the selected startup project is restored and buildable. EfGuard resolves startup services first, then falls back to the design-time factory and parameterless constructor inside a bounded child process; a failure returns exit code 2.
  • The baseline Git reference cannot be read: verify the ref exists locally, the selected project is present at that ref, and the repository has a usable Git checkout. Baseline extraction is isolated and never rewrites the active worktree; failure returns exit code 2.
  • The dependency graph is not restored: run dotnet restore for the selected project and its referenced projects first. EfGuard never restores packages and never contacts a package feed; a missing restored graph is reported as a clear extraction failure with exit code 2. If --baseline reports the same failure, the project's restored graph is missing from your working tree or the baseline dependency set is not available offline.
  • A copied dependency assembly cannot be enumerated: the worker copies the selected project's dependency graph and reads metadata per assembly, so a design-time, build, or Roslyn assembly that cannot be loaded in the worker process is skipped with a recorded note instead of failing the whole scan. If the assembly that declares the selected DbContext or its migrations cannot be enumerated at all, extraction fails closed with exit code 2 and names the assembly and the underlying exception. Rebuild the project (dotnet build) and run EfGuard again; if the failure persists, resolve the reported exception, which usually means the assembly needs a missing dependency or a runtime it was not built for.
  • A migration class is reported as unattributed: EF Core applies and lists a migration only when the migration carries [DbContext(typeof(...))]; EF Core itself writes that attribute into the migration designer files it generates. EfGuard follows the same rule, so a migration class without a matching [DbContext] attribute is never attributed to the selected context and instead produces an actionable extraction failure with exit code 2. Add the attribute, or run the scan for the context that owns those migrations.
  • Extraction times out or exceeds the output limit: reduce the selected project/startup scope and design-time logging. Worker execution is bounded and response output is limited to 4 MiB; either condition is an untrustworthy extraction failure and returns exit code 2.
  • An EF metadata assembly is only partially loadable: a partial type load in the assembly declaring the selected DbContext, or in the migrations assembly EF Core attributes to that context, fails closed with exit code 2 and names the assembly and loader exception. Partial loads in unrelated design-time or other peripheral assemblies are tolerated; EfGuard emits a bounded EfGuard note: diagnostic on standard error for each skipped assembly.
  • The provider is unsupported: SQL Server/Azure SQL and PostgreSQL through Npgsql are supported in v1. Other providers produce EFG900 and exit code 2; no provider-locking claim is inferred.
  • The report contains UNVERIFIED: inspect EFG399 or the provider diagnostic's uncertainty text. Unknown operations and provider findings without generated-SQL evidence fail closed and normally produce exit code 1; review the migration and provider evidence before overriding policy.
  • Configuration is malformed: efguard.json must use version 1, documented property names, known rule IDs, non-empty suppression reasons, and yyyy-MM-dd expiry dates. Unknown properties and rule IDs are rejected with an actionable error and exit code 2.
  • The exit code is unexpected: 0 means a trustworthy clean result, 1 means a trustworthy blocking or unverified result, and 2 means analysis could not complete trustworthily. In CI, treat all non-zero codes as failures unless the job intentionally evaluates the JSON report.

Documentation

CI

Install the package using the pinned command in this README or the project-local package README, then invoke the CLI directly in CI. Both release-facing examples are validated to agree. Local-feed and --add-source commands are validation-only operational details kept in build scripts; they are not release install examples.

On Windows PowerShell, inspect $LASTEXITCODE instead of test. A gating job should fail for either non-zero exit code.

Privacy

After a trustworthy scan completes with exit code 0 or 1, EfGuard requests activation and heartbeat telemetry through KeelMatrix.Telemetry. An incomplete scan (exit code 2) does not request telemetry. EfGuard does not pass source, generated SQL, schema names, analyzed project or repository names, paths, provider/EF versions, findings, or credentials to the shared client. The shared client owns event fields, opt-out handling, cadence, state, queueing, and delivery; see PRIVACY.md and the KeelMatrix.Telemetry privacy policy for the current contract. Set KEELMATRIX_NO_TELEMETRY=1 to disable telemetry during local development and company CI.

For repository validation with telemetry explicitly disabled, run pwsh ./build/validate.ps1 on Windows or ./build/validate.sh on Linux/macOS. This controlled path runs restore, Release build, tests, formatting, package-contract inspection, and an isolated installed-package consumer smoke test.

License

MIT. See LICENSE.

About

Detect EF Core migrations that can break rolling deployments, lose data, or block production traffic, and explain a safer rollout before merge.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages