ContractGuard.MSBuild
0.0.3-alpha
Prefix Reserved
See the version list below for details.
dotnet add package ContractGuard.MSBuild --version 0.0.3-alpha
NuGet\Install-Package ContractGuard.MSBuild -Version 0.0.3-alpha
<PackageReference Include="ContractGuard.MSBuild" Version="0.0.3-alpha"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="ContractGuard.MSBuild" Version="0.0.3-alpha" />
<PackageReference Include="ContractGuard.MSBuild"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add ContractGuard.MSBuild --version 0.0.3-alpha
#r "nuget: ContractGuard.MSBuild, 0.0.3-alpha"
#:package ContractGuard.MSBuild@0.0.3-alpha
#addin nuget:?package=ContractGuard.MSBuild&version=0.0.3-alpha&prerelease
#tool nuget:?package=ContractGuard.MSBuild&version=0.0.3-alpha&prerelease
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://raw.githubusercontent.com/lxman/ContractGuard/main/schema/contractguard.schema.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
contractguard add --contract Shop.Domain.contract.json --type OrderService "public Task<Result> Submit(Order order)"
contractguard import --contract Shop.Domain.contract.json IOrderContract.cs
contractguard normalize --contract Shop.Domain.contract.json --check
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. The
authoring verbs go the other way: add decomposes a C# declaration string into elements
(the contract file never stores C# text), import decomposes a whole scaffold file - an
interface control document the architect wrote - and normalize rewrites a contract to
canonical form (--check makes it a CI lint).
MSBuild package (the drop-in):
<PackageReference Include="ContractGuard.MSBuild" Version="0.0.3-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.
Decoded from attribute metadata
- Nullable reference annotations (
NullableAttribute/NullableContextAttribute) are decoded, sonullableAnnotations: significantenforcesstringvsstring?for real. The default staysignored— oblivious (pre-nullable) assemblies carry no annotations, and a mixed-context shop turning this on should do so deliberately.int?is a real type,Nullable<int>, and is always enforced regardless. - Tuple element names (
TupleElementNamesAttribute) are decoded and significant by default — renaming(int x, int y)to(int a, int b)breaks consumers using named access. - Record classes are detected (the
EqualityContractcompiler pattern) and compare as"kind": "record"; the synthesized plumbing (EqualityContract,PrintMembers) is not governable surface, while public synthesized members (Equals, operators,Deconstruct) are. Record structs have no metadata marker and staystruct. ref readonlyreturns and parameters andvolatilefields decode from their modreqs/attributes.notnullandclass?constraints decode whennullableAnnotationsis significant.- Enum parameter defaults written as
"OrderStatus.Pending"resolve against enums defined in the scanned assembly. Enums from other assemblies still need the underlying numeric value — the gate never loads foreign assemblies.
Not decoded yet — accepted by the schema, but not enforced
- Nullability on constraint types and inheritance.
where T : IFoo?and annotations on base types and implemented interfaces are not decoded. - 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.
ContractGuard eats its own cooking: ContractGuard.Core's public surface is governed by
its own contract through the
published ContractGuard.MSBuild package, so a PR that drifts the API fails its own gate.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Learn more about Target Frameworks and .NET Standard.
This package has no dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.0.10-alpha | 78 | 6/13/2026 |
| 0.0.9-alpha | 75 | 6/12/2026 |
| 0.0.8-alpha | 73 | 6/12/2026 |
| 0.0.7-alpha | 65 | 6/12/2026 |
| 0.0.6-alpha | 59 | 6/12/2026 |
| 0.0.5-alpha | 63 | 6/12/2026 |
| 0.0.4-alpha | 90 | 6/12/2026 |
| 0.0.3-alpha | 68 | 6/12/2026 |
| 0.0.2-alpha | 83 | 6/12/2026 |
| 0.0.1-alpha | 71 | 6/12/2026 |