ContractGuard 0.0.1-alpha
Prefix ReservedSee the version list below for details.
dotnet tool install --global ContractGuard --version 0.0.1-alpha
dotnet new tool-manifest
dotnet tool install --local ContractGuard --version 0.0.1-alpha
#tool dotnet:?package=ContractGuard&version=0.0.1-alpha&prerelease
nuke :add-package ContractGuard --version 0.0.1-alpha
ContractGuard
A CI gate for .NET API surfaces. An architect prescribes the method signatures a team (or an AI coding agent) must honor; developers implement the bodies however they like; the build fails if any prescribed signature drifts.
Contracts are data, not code: decomposed signature elements in a JSON file that lives in the repo, validated by a schema, reviewed like any other change. Put the contract under CODEOWNERS and the architect-approval workflow comes for free — and because the gate reads the built assembly's metadata (no code execution, no analyzers a developer can switch off), it verifies the artifact that actually ships.
New to it? Start with the owner's manual — quickstart, the settings in plain language, and what the diagnostic IDs mean.
Status
Early scaffold. Core engine, CLI, and MSBuild gate work end to end. See What the gate can and can't see for the current enforcement boundary; the master list of gaps lives as TODOs in code.
How it works
samples/MyCompany.Orders.contract.json what a contract looks like
schema/contractguard.schema.json draft-07 JSON Schema ($schema gives editor red squiggles)
A contract names one assembly, the types it governs, and the members each type must expose:
{
"$schema": "https://contractguard.dev/schema/v1.json",
"assembly": "Shop.Domain",
"types": [
{
"type": "Shop.Calc",
"kind": "class",
"members": [
{ "kind": "method", "name": "Add", "returns": "int",
"params": [["int", "a"], ["int", "b"]] }
]
}
]
}
Policy lives inside the contract (settings: exact-vs-open surface, parameter-name
significance, accessibility scope...) so strictness cannot be weakened from a CI flag —
changing policy means changing the reviewed file.
Use it
CLI (dotnet tool install -g ContractGuard --prerelease):
contractguard extract --assembly Shop.Domain.dll --output Shop.Domain.contract.json
contractguard verify --contract Shop.Domain.contract.json --assembly Shop.Domain.dll
contractguard show --contract Shop.Domain.contract.json
extract takes --scope public,protected,internal,private to pull out more than the
default public+protected surface — prescribed members of any accessibility are enforced
either way; scope governs what the deny sweeps and extraction consider surface.
extract bootstraps a contract from a golden build; verify is the gate (exit 0 pass,
1 violations, 2 errors); show renders the elements back as C# declarations.
MSBuild package (the drop-in):
<PackageReference Include="ContractGuard.MSBuild" Version="0.0.1-alpha" PrivateAssets="all" />
After every build, <project>/<AssemblyName>.contract.json is verified automatically —
violations land in the IDE error list pointing at the contract file. Projects without a
contract file are skipped, so the reference can live solution-wide in Directory.Build.props.
In CI, build with -p:ContractGuardRequireContract=true so a deleted contract file fails
the build instead of silently removing the gate.
What the gate can and can't see
The gate reads assembly metadata, so its enforcement boundary is metadata's boundary. Two kinds of limits apply — ones that are permanent, and ones that are just not built yet.
Identical in metadata — by design, permanent
= defaulton a struct parameter and= nullon a reference parameter compile to the same constant (a nullref). The gate therefore treats the JSON forms"default": nulland{"$special": "default"}as interchangeable, andextractemitsnullfor both. This can never produce a false pass: a given parameter type only admits one of the two meanings.asyncdoes not exist in a binary signature. It is an implementation detail an implementer may freely add or remove, which is why it is deliberately absent from the contract vocabulary. PrescribeTask<T> Submit(...); whether the body isasyncis not the architect's business.
Not decoded yet — accepted by the schema, but not enforced
- Nullable reference annotations. Leave
nullableAnnotationsat its default (ignored): setting it tosignificanttoday produces false violations, because the reader does not yet decodeNullableAttributefrom metadata, so an observedstring?always looks likestring. (int?is a real type,Nullable<int>, and is always enforced.) - Tuple element names. The reader does not yet decode
TupleElementNamesAttribute, sotupleElementNamesdefaults toignoredrather than promise enforcement that doesn't happen. Tuple element types are enforced. - Enum parameter defaults. Write them as the underlying numeric value
(
"default": 0);"default": "OrderStatus.Pending"strings are not resolved yet and will report a mismatch. - Records. Classify as class/struct in metadata;
"kind": "record"is accepted and matched asclass(record-structasstruct). Compiler-synthesized record members are visible to the gate like any other member. ref readonlyreturns and parameters,volatilefields. The modreqs are not decoded;ref readonlycurrently reads as plainref.- Explicit interface implementations are skipped and cannot be governed yet.
significantAttributesis accepted by the schema but attribute comparison is not implemented.
Building
dotnet build
dotnet test
dotnet pack src/ContractGuard.MSBuild -c Release
Requires the .NET 8 SDK or later. Tests compile C# snippets in-memory with Roslyn and read the emitted PE bytes back through the metadata reader — the same harness that will pin metadata-vs-ISymbol front-end consistency when the Roslyn analyzer (phase 2) lands.
License
This project is licensed under the MIT License - see the LICENSE file for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.0.10-alpha | 95 | 6/13/2026 |
| 0.0.9-alpha | 69 | 6/12/2026 |
| 0.0.8-alpha | 70 | 6/12/2026 |
| 0.0.7-alpha | 74 | 6/12/2026 |
| 0.0.6-alpha | 63 | 6/12/2026 |
| 0.0.5-alpha | 64 | 6/12/2026 |
| 0.0.4-alpha | 63 | 6/12/2026 |
| 0.0.3-alpha | 75 | 6/12/2026 |
| 0.0.2-alpha | 69 | 6/12/2026 |
| 0.0.1-alpha | 66 | 6/12/2026 |