KirillMaster.QualityGates.Build 0.3.1

dotnet add package KirillMaster.QualityGates.Build --version 0.3.1
                    
NuGet\Install-Package KirillMaster.QualityGates.Build -Version 0.3.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="KirillMaster.QualityGates.Build" Version="0.3.1">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="KirillMaster.QualityGates.Build" Version="0.3.1" />
                    
Directory.Packages.props
<PackageReference Include="KirillMaster.QualityGates.Build">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add KirillMaster.QualityGates.Build --version 0.3.1
                    
#r "nuget: KirillMaster.QualityGates.Build, 0.3.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package KirillMaster.QualityGates.Build@0.3.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=KirillMaster.QualityGates.Build&version=0.3.1
                    
Install as a Cake Addin
#tool nuget:?package=KirillMaster.QualityGates.Build&version=0.3.1
                    
Install as a Cake Tool

quality-gates

Reusable quality gates for modern .NET (net8 / net10). MIT licensed.

Package Purpose
KirillMaster.QualityGates.Build Build-only package: warnings-as-errors, AnalysisLevel 10.0-all (pinned), nullable, curated third-party analyzers (Sonar, Meziantou, Roslynator, VS.Threading, IDisposableAnalyzers, BannedApiAnalyzers), own Roslyn analyzers QG0xxx, quality.globalconfig, BannedSymbols.txt, .editorconfig template, grandfathered suppressions.
KirillMaster.QualityGates.Architecture ArchUnitNET-based test base for hexagonal layering, config-driven layers, namespace-cycle and type-cycle (baseline) checks, repository meta-tests (locked settings, central package versions, suppression ratchet, every project covered).
KirillMaster.QualityGates.Results Minimal Result, Result<T>, Error, ErrorType (netstandard2.0 / net10): implicit conversions, MemberNotNullWhen, no fluent API. Companion of the QG0100-QG0111 rules.
KirillMaster.QualityGates.Results.AspNetCore ToHttpResult() ProblemDetails mapping and a ProblemExceptionHandler base that never exposes exception details.

No StyleCop (Roslynator covers it). Legacy .NET Framework (net48) is not supported.

Result API caveats

  • Result<T> converts implicitly from T and from Error; Result<Error> is therefore ambiguous, do not use Error as T.
  • ErrorType is closed by convention: add values only together with the ToHttpResult mapping (the exhaustive switch fails the build on a missing member).
  • A Result or Result<T> must be used; QG0106 reports discarded and never-read results.

Usage


<GlobalPackageReference Include="KirillMaster.QualityGates.Build" Version="0.1.0" />
<PackageVersion Include="KirillMaster.QualityGates.Architecture" Version="0.1.0" />

Without central package management add <PackageReference Include="KirillMaster.QualityGates.Build" Version="0.1.0" PrivateAssets="all" /> to each project.

Copy templates/editorconfig.template from the package (or src/KirillMaster.QualityGates.Build/templates/) to .editorconfig in the repository root.

MSBuild contract

Property names are shared with the company package so one tool can drive both; the report format is described in docs/contract.md.

Property Default Meaning
QualityGatesEnabled true false turns everything off (set in Directory.Build.props or /p:)
QualityGatesProfile strict strict = AnalysisLevel 10.0-all; legacy-ratchet = 10.0-recommended plus grandfathered suppressions
QualityGatesMode adopt (env QUALITYGATES_MODE when empty) adopt compiles grandfathered suppressions; refactor does not compile them, forces TreatWarningsAsErrors=true and raises every QG* rule to error (see Refactor mode); the mode can also be set in the project body
QualityGatesRefactorScope empty (all projects) ; list of project-name prefixes (Shop.Orders*;Shop.Billing, blanks and spaces ignored, trailing * optional); projects outside the scope stay in adopt
EnforceCodeStyleInBuild, AnalysisLevel true, pinned by profile defaults only; values set by the consumer win. AnalysisLevel is never latest so SDK upgrades do not change the rules
QualityGatesLayer derived core (*.Domain, *.Application, *.Core, *.Ports.Input, *.Ports.Output), test (*.Tests, relaxed test config), adapter
QualityGatesBannedSymbols true false drops BannedSymbols.txt
QualityGatesGlossary empty path to a word list that must be named glossary.txt (one word per line or comma-separated, # comments); it is added as an AdditionalFile and activates QG0174 (unknown word). Without it QG0174 is silent
QualityGatesGrandfatheredDir <repo>/quality/grandfathered/ folder with <Project>.GlobalSuppressions.cs files compiled into the project

NuGet audit is not part of the package props (restore does not see them). Set it in Directory.Build.props:

<NuGetAuditMode>all</NuGetAuditMode>
<WarningsNotAsErrors>$(WarningsNotAsErrors);NU1900;NU1901;NU1902;NU1903;NU1904;NU1905</WarningsNotAsErrors>

Analyzers

See docs/analyzers.md. Defaults are warnings, so TreatWarningsAsErrors makes them blocking. Thresholds are tunable in .editorconfig (qg_max_method_lines = 80). QG0031/QG0032 are opt-in.

Architecture tests

public sealed class Layering : HexagonalArchitectureTests
{
    protected override IEnumerable<Assembly> Assemblies => [typeof(Shop.Domain.Order).Assembly, typeof(Shop.Application.IOrders).Assembly];
    protected override ArchitectureConfig Config => ArchitectureConfig.Hexagonal("Shop");
}

public sealed class RepoRules : RepositoryRulesTests { }

ArchitectureConfig.FromFile("architecture.json") loads custom layers: { "layers": [ { "name": "Core", "pattern": "^X\\.Core(\\..*)?$", "mayDependOn": [], "forbiddenNamespaces": ["System.IO"] } ] }. Tests carry Category=Architecture for dotnet test --filter Category=Architecture.

Module and hexagonal rules (opt in)

Both are off until the base class overrides ModuleRulesConfig / HexagonalRulesConfig (tests Modules_follow_isolation_rules, Hexagonal_roles_follow_profile_rules). Violations carry a key and a measure; a violation is tolerated while a baseline entry with the same key has a measure at least as large ({"rules": {"FeatureIsolation": {"Bill->Fulfillment": 3}}}, RuleBaseline.From(violations).ToJson() generates it). Override FailOnStaleBaseline to also fail on entries that vanished or shrank, so the baseline only shrinks.

Generic module engine (ModulesConfig, baseline quality/modules-baseline.json). Modules are ordered regexes on the type namespace, then on the assembly name; kinds are Feature, Shared, Model, Composition.

Rule Checks Default
FeatureIsolation a feature depends on no other feature and no Composition module soft (baseline); StrictFeatureIsolation ignores the baseline
SharedPurity Shared depends on no Feature or Composition on
SharedCap types per Shared module off (SharedMaxTypes)
CompositionCap types per Composition module (root ratchet) off (CompositionMaxTypes)
ModelIsLeaf Model depends on no other module on
ModuleFanOut distinct modules a feature depends on 6 (ModuleFanOutMax)
OrchestrationThin orchestrator (OrchestratorPattern, default (Orchestrator|Coordinator)$) methods and distinct dependencies 10 / 12

Hexagonal profile (HexagonalProfile, baseline quality/hexagonal-baseline.json). Roles come from regexes (namespace first, then assembly): *.Core*, *.Ports.Input, *.Ports.Output, *.Adapters.*, *.Runners.*, *.Host*; every pattern can be overridden.

Rule Checks Default
AdapterReferencesAdapter Adapters.A types use Adapters.B types on
AdapterDirection Core and ports never use adapters, runners or host; adapters only the roles in AdaptersMayDependOn adapters: Core, both ports
RunnerHostReferences runners use only RunnersMayDependOn (no level skipping) input ports
CoreNoContainer DI container namespaces and IServiceProvider in Core and ports MS DI, Autofac, Ninject, StructureMap, Lamar, SimpleInjector, Windsor, Unity
PortWidth members per port interface 10
NoMirrorTypes Ports.Input and Ports.Output types sharing at least 90% of member name and type pairs (min 2 members) 0.9
PortNoTechnologyTypes IQueryable, HTTP, JSON, Stream, System.Data, EF, Amazon, Npgsql types in port signatures on
Use-case slice rules (opt in per rule)

List rule names in HexagonalProfile.ExtraRules (JSON: "extraRules": ["FlowDirection", "HandlerIsolation"]); they use the same baseline as the rules above, and unknown names throw. A handler is a non-abstract class implementing a Ports.Input interface that matches HandlerInterfacePattern (default ^I[A-Z]\w*Handler$); the suffix of the class does not matter. Flow levels are Runner, Handler, Domain (other Core), Port (Ports.Input and Ports.Output) and Adapter.

Rule Checks Config
FlowDirection nothing points back up the flow: Domain, ports and adapters never use a concrete handler; handlers, Domain, ports and adapters never use runners roles
NoLevelSkipping (L5) runners never use output ports or adapters; handlers, Domain and ports never use an adapter (only through an output port) roles
HandlerIsolation a handler uses no other handler class and no other use case's input port (its own port and decorators of it are fine) HandlerInterfacePattern
ResultAtBoundary a Result is only returned: no property, field or parameter of Result type outside ResultAllowedRoles ResultTypePattern, ResultAllowedRoles (Runners, Host)
OnePortPerUseCase handler port has exactly one member (:members), one implementation, decorators of the same port excepted (:implementations); a handler implements one port (:ports) and has no public method beyond it (:public-methods)
FolderAllowlist the feature folder under Core.Features, Ports.Input/Output, Adapters.X, Runners.X is not in ForbiddenFolders (Services, Helpers, Utils, Common, Shared, Mappers, Dtos, Contracts, Interfaces, Internal, Infrastructure, Base, ...) and, when Features is set, is a registered feature or in FolderVocabulary; works on namespaces FeatureFolderPattern, Features, FolderVocabulary, ForbiddenFolders
FeatureMirror every Ports.Input and Ports.Output feature has a Core feature (Ports:X) and every Core feature has an input port feature (Core:X); does nothing without Core features CoreFeaturePattern, PortFeaturePattern
HandlerReferenced (Z2) every handler class or its port is referenced from test code; include the test assemblies in Assemblies TestPattern

StableDependencies (module engine, ModulesConfig) reports A->B when I(B) > I(A) + StableDependenciesEpsilon (0.15) with I = Ce / (Ca + Ce); modules with fewer than StableDependenciesMinCoupling (4) couplings are skipped. It is a warning (written to stderr by Modules_follow_isolation_rules, override ReportWarnings) until EnforceStableDependencies is true; then it fails on new entries like the other rules.

Convention and contract helpers (test classes)

Call these from your own tests; findings are RuleViolations, so RuleBaseline works on them.

Helper Gate Checks
ErrorConventions.SingleExceptionHandler(assemblies) X1 exactly one non-abstract IExceptionHandler implementation in the host and composition assemblies
ErrorConventions.UniqueErrorCodes(assemblies) X9 codes (string constants or the Code of static Error values) are unique across all static *Errors classes
EfConcurrency.MissingConcurrencyToken(dbContext.Model) Z13 entities marked by markerPattern (default IAggregateRoot/AggregateRoot base, interface or attribute) have a property with IsConcurrencyToken; reflection over the model, no EF reference
OpenApiSnapshot.Assert(json, path) H7, Y7 generated OpenAPI JSON equals the committed snapshot (sorted keys, info.version and servers dropped); BreakingChanges(old, new) lists removed paths, operations, success responses, removed or retyped schema properties, removed enum values, new required parameters or properties
MessageContractSnapshot.Assert(types, path) Y8 same for JSON schemas of message types (Discover(assembly, regex) picks them); property names camelCase, enums as names

Test types (TestPattern) have no role and are only the reference source of HandlerReferenced. DI registration classes (CompositionTypePattern, e.g. ServiceCollectionExtensions) are outside the flow levels; runners may use output port DTOs (the rule covers interfaces). Selected rules whose roles match no type throw instead of passing silently. Runner and Runners segments both mean runners; Core features live under Core.Features.X or Core.Internal.X. UniqueErrorCodes counts only members typed Error (errorTypePattern) and string members named *Code (codeMemberPattern); an unreadable member is a violation.

OpenAPI breaking changes are direction-aware (request: removed enum values, new required properties, narrowed nullability; response: added enum values, dropped required, removed properties, widened nullability; also format changes, oneOf/anyOf as sets, path-level parameters, renamed path templates). Relative snapshot paths resolve against the repository root (QG_REPO_ROOT, else .git or solution above the binaries). QG_UPDATE_SNAPSHOTS is ignored when CI is set.

Snapshot files are never rewritten silently: a missing or different file fails and lists the breaking changes; set QG_UPDATE_SNAPSHOTS=1 to write it, plus QG_ALLOW_BREAKING_CHANGES=1 when the diff contains breaking changes. The OpenAPI document comes from a WebApplicationFactory test (class T) or the build-time generator.

Optional package KirillMaster.QualityGates.Testing.AspNetCore

HostSmoke.AssertStartsAsync<Program>(configure) starts the host with test configuration (ValidateOnStart and DI validation run) and probes one path for non-5xx (Z14). RouteRecorder records the routes the tests hit (factory.WithWebHostBuilder(b => b.ConfigureServices(recorder.Install))) and AssertAllRoutesHit(factory.Services, known) fails for every route in the endpoint table that no test hit (infrastructure routes openapi, swagger, health ignored; known is the ratchet list). HostSmoke.AssertSingleExceptionHandler(factory.Services) checks X1 against the registered services. Targets net10.0 and requires Microsoft.AspNetCore.Mvc.Testing 10.x (the package depends on it; do not pin another major). Fallback endpoints (MapFallback) are not routes, and a 404 answered by a catch-all route or matching no endpoint is not a hit.

Refactoring and baseline keys

Keys name modules and types (Bill->Fulfillment, Shop.Core.IFat), so a move or rename makes the old entry stale and the same violation new. Remap the baseline in the same change: RuleBaseline.FromFile(path).Remap(new Dictionary<string, string> { ["Old.Type"] = "New.Type" }).ToJson() renames exact keys, keeps the measure (the larger one if two keys collapse) and leaves unmapped keys alone. Relative baseline paths need RepositoryRoot; without it the test throws instead of using an empty baseline. FailOnStaleBaseline only checks the rules the test ran, so one baseline file can be shared.

Legacy projects (ratchet)

Set QualityGatesProfile=legacy-ratchet, put existing violations into quality/grandfathered/<Project>.GlobalSuppressions.cs and record the count in quality/baseline.json ({"maxGrandfatheredSuppressions": 42}). Suppressions_stay_under_baseline fails if the count grows or if the baseline is stale, so it can only go down.

Type cycles

HexagonalArchitectureTests.Types_have_no_new_cycles finds cycles between types of the given assemblies (strongly connected components of the type dependency graph; compiler-generated types and nested-to-containing edges are ignored). Known cycles are tolerated through quality/type-cycles.json (ArchitectureConfig.TypeCycleBaselinePath): {"cycles": [["Shop.A", "Shop.B"]]}, one member set per cycle; a cycle that gains a member counts as new. TypeCycleBaseline.From(TypeCycles.Find(...)).ToJson() generates the file. Disable with CheckTypeCycles = false.

Refactor mode

QualityGatesMode=refactor (property in Directory.Build.props or the project, /p:, or env QUALITYGATES_MODE) does three things for in-scope projects:

  1. Stops compiling quality/grandfathered/*.GlobalSuppressions.cs, so every grandfathered violation is an error again.
  2. Sets TreatWarningsAsErrors=true, even when the project sets false.
  3. Adds quality.refactor.globalconfig (global_level = 100), which sets all QualityGates.* analyzer categories (Abstraction, Design, Errors, Hygiene, Naming, Size, Testing) to error. This also enables the opt-in QG0031/QG0032. In every profile and mode QG rules are at least warnings, so the CI new-code gate sees them on changed lines; legacy code is covered by grandfathered suppressions. Curated third-party analyzers and AnalysisLevel are not changed (they already follow the profile); an .editorconfig entry still wins over the globalconfig.

Use QualityGatesRefactorScope to work on a few projects at a time; the rest keep adopt behaviour. Mode and scope are resolved in the package .targets, and TreatWarningsAsErrors is applied by the QualityGatesResolveMode target before compilation.

Gates and where they run

Gate Mechanism Blocking
Build warnings as errors, analyzers Build package yes, every build
Format dotnet format --verify-no-changes yes (CI job format)
Architecture Architecture package tests yes
Tests dotnet test yes
Diff coverage >= 80 diff-cover on cobertura, PR only (merge gate) yes, merge gate only
Mutation Stryker.NET never blocks (continue-on-error)

See .github/workflows/ci.yml and samples/HexSample.

Develop

dotnet build -c Release
dotnet test -c Release
dotnet pack -c Release        # writes artifacts/*.nupkg

Sample against the local feed: dotnet test samples/HexSample/HexSample.sln (restores from artifacts/).

Releasing

Publishing runs in .github/workflows/release.yml and authenticates to nuget.org with Trusted Publishing (OIDC, no API key stored anywhere). The nuget.org trusted publishing policy must match owner KirillMaster, repository quality-gates, workflow file release.yml.

One-time setup: add the repository variable NUGET_USER (Settings → Secrets and variables → Actions → Variables) set to your nuget.org profile name.

Per release: bump Version in Directory.Build.props, update CHANGELOG.md, then:

git tag v0.1.0 && git push origin v0.1.0

The workflow derives the package version from the tag (v0.1.0 → 0.1.0) and pushes with --skip-duplicate. Manual run: Actions → release → Run workflow, enter version.

There are no supported framework assets in this package.

Learn more about Target Frameworks and .NET Standard.

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.3.1 0 10/30/2026
0.3.0 0 10/30/2026
0.2.0 5 10/29/2026
0.1.0 84 10/23/2026