Siemens.AspNet.CodeRules.Engine
7.7.4
Prefix Reserved
dotnet add package Siemens.AspNet.CodeRules.Engine --version 7.7.4
NuGet\Install-Package Siemens.AspNet.CodeRules.Engine -Version 7.7.4
<PackageReference Include="Siemens.AspNet.CodeRules.Engine" Version="7.7.4" />
<PackageVersion Include="Siemens.AspNet.CodeRules.Engine" Version="7.7.4" />
<PackageReference Include="Siemens.AspNet.CodeRules.Engine" />
paket add Siemens.AspNet.CodeRules.Engine --version 7.7.4
#r "nuget: Siemens.AspNet.CodeRules.Engine, 7.7.4"
#:package Siemens.AspNet.CodeRules.Engine@7.7.4
#addin nuget:?package=Siemens.AspNet.CodeRules.Engine&version=7.7.4
#tool nuget:?package=Siemens.AspNet.CodeRules.Engine&version=7.7.4
Siemens.AspNet.CodeRules.Engine
Parse the solution once. Run every rule against it. Report findings a human and an AI agent can act on.
The code rule engine behind all hosts. It registers your rules with the usual IServiceCollection API, creates every
analysis context once (the solution is parsed a single time, also across several projects), runs the rules in parallel
and returns results with findings, skips and errors. It also contains ICodeRuleSetup, the composition root of a code
rule test project, and RuleTester<TRule>, the unit test helper for single rules.
Most consumers never reference it directly: the test adapters (Siemens.AspNet.CodeRules.MSTest, .XUnit, .NUnit) and the
coderules CLI bring it. Reference it yourself when you write rule tests or your own host.
services.AddCodeRuleEngine("My.slnx", o => o.AddRulePack<NamingRulePack>()
.AddRulesFrom(typeof(MyLocalRule).Assembly)
.UseDocumentationUrlTemplate("https://example.com/rules#{Id}"));
Installation
dotnet add package Siemens.AspNet.CodeRules.Engine
Rules themselves only need Siemens.AspNet.CodeRules.Contracts.
ICodeRuleSetup: one setup for every host
A code rule test project has no Program.cs, so it has exactly one class that implements ICodeRuleSetup:
public sealed class CodeRuleSetup : ICodeRuleSetup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddCodeRuleEngine("My.slnx", o => o.AddRulePack<NamingRulePack>());
}
}
It is the single source for all hosts:
- the source generators of the test adapters read it at build time and write one test per registered rule,
- the test adapters call it at test time and run the rule of each test,
- the CLI (
coderules run) loads it from the built project and runs it natively.
So dotnet test and coderules always run the same rules with the same result.
Registering rules
AddCodeRuleEngine(solution, configure) registers the rules, the catalog, the context cache and ICodeRuleEngine as
singletons. solution is a file name or path (e.g. My.slnx), searched from the running assembly upwards.
| Option | Does |
|---|---|
AddRule<TRule>() |
adds one rule explicitly, without reflection |
AddRules(r => r.Add<TRule>()) |
adds several rules explicitly, through the same ICodeRuleRegistry a pack uses |
AddRulePack<TPack>() |
adds the rules an ICodeRulePack contributes |
AddRulesFrom(assembly) |
adds every public, non-abstract rule class of an assembly |
SkipRule<TRule>(reason) |
registers a rule but reports it as skipped, e.g. a rule from a pack you do not own: "Replaced by analyzer X" |
SkipRulePack<TPack>(reason) |
registers a pack's rules but reports them as skipped; meant for implicitly loaded packs, an explicit AddRulePack can simply be left out |
SkipGroup(group, reason) / SkipTag(tag, reason) |
reports every registered rule of a group / with a tag as skipped; an unknown group or tag is a configuration error. Reason precedence: rule > pack > group > tag > IRule.SkipReason |
SkipRule<TRule>(reason, scope => scope.InProject("X").InNamespace("A.B.*").InPath("src/Legacy/**")) (also SkipRulePack, SkipGroup, SkipTag) |
the rule runs, only its findings in project X, namespace A.B (and below) or below src/Legacy are dropped (conditions are ORed) and reported as "N skipped in InProject("X")"; an unscoped skip wins over every scope. See Core contract §6.0.2 |
ExcludeFromAnalysis(reason, scope => scope.InPath("src/Eval")) |
takes C# files out of the CodeContext: no rule sees them (experiments, generated code). Reported once per run with the number of files. See Core contract §6.0.3 |
UseDocumentationUrlTemplate(template) |
links every rule without its own DocumentationUrl; {Id} is replaced by the rule ID |
UseOutputMode(CodeRuleOutputMode.Ai) |
failure text of the test adapters for this project: Human (default) or Ai, see Output mode |
AddContextProvider<TContext, TProvider>() |
a provider for an own context that cannot simply be built from other contexts |
All options combine, in any order; registering the same rule twice is harmless. The generators understand
AddRule<TRule>(), Add<TRule>(), AddRulePack<TPack>() and AddRulesFrom(typeof(X).Assembly).
Own contexts with one public constructor that takes other contexts (e.g. ControllerContext(CodeContext code)) need no
registration: the engine creates them once per run.
Accepting a single finding in the code
A skip in the setup is for whole rules, packs, groups, tags or scopes. A single finding is accepted where it is, the same way as an analyzer finding, always with a reason:
[SuppressMessage("CodeRules", "NAMING_001", Justification = "Name is part of the public contract of v1")]
public sealed class LegacyOrderManager { /* ... */ }
#pragma warning disable NAMING_001 // Generated by the legacy importer, removed with #1234
public sealed class ImportManager { /* ... */ }
#pragma warning restore NAMING_001
[SuppressMessage]counts on every declaration that contains the finding line (member, accessor, parameter, type, outer type). The category is free, the check ID is the rule ID ("NAMING_001:Title"works too).#pragma warning disable <ID>counts from the next line up to the matchingrestore(or the end of the file). A pragma without an ID is ignored.- Without a reason it does not count: no
Justification, or no//comment after the pragma, keeps the finding and says why. The same happens for rules withAllowSuppression => false. - Accepted findings do not fail the rule; they are listed as
Suppressedin the result and in both outputs, so they stay visible. Only findings with a line in a C# file can be accepted this way.
Output mode: human or AI
Every host can write its output for people (Human, the default) or as JSON for AI agents (Ai, the same schema in
every host). How you switch depends on the host:
| Host | Switch |
|---|---|
| Test adapters (MSTest, xUnit, NUnit) | in the setup with UseOutputMode(...) and/or the environment variable |
coderules CLI |
--format ai (setup and environment are ignored) |
RuleTester<TRule> (rule unit tests) |
new RuleTester<TRule>(o => o.OutputMode = CodeRuleOutputMode.Ai), otherwise the environment variable |
In the setup: the default of the project
public sealed class CodeRuleSetup : ICodeRuleSetup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddCodeRuleEngine("My.slnx", o => o.AddRulePack<NamingRulePack>()
.UseOutputMode(CodeRuleOutputMode.Ai));
}
}
Good when the project is mostly run by an AI agent: every dotnet test writes the JSON, without any setup on the machine.
With an environment variable: per machine, CI or agent
$env:CodeRuleSettings__OutputMode = "ai" # PowerShell
export CodeRuleSettings__OutputMode=ai # bash
TestSdkSettings__OutputMode=ai (the variable of AspNetCore.Simple.MsTest.Sdk) also works and switches API tests and
code rules together. Without a shell, put the variable in a .runsettings file of the test project:
<RunSettings>
<RunConfiguration>
<EnvironmentVariables>
<CodeRuleSettings__OutputMode>ai</CodeRuleSettings__OutputMode>
</EnvironmentVariables>
</RunConfiguration>
</RunSettings>
Which one wins
CodeRuleSettings__OutputMode, if set toaiorhuman,- otherwise
TestSdkSettings__OutputMode, if set toaiorhuman, - otherwise
UseOutputMode(...)of the setup, - otherwise
Human.
So the environment always overrides the setup: a project with UseOutputMode(CodeRuleOutputMode.Ai) still shows the human
output with CodeRuleSettings__OutputMode=human. Other values, such as the SDK's hybrid, leave the decision to the setup.
The mode is resolved once per test run, after the setup ran, so Environment.SetEnvironmentVariable inside
ConfigureServices has no effect: use UseOutputMode(...) there.
The CLI has its own --format option because one call can run several code rule projects with different setups.
Your own host
var services = new ServiceCollection();
services.AddCodeRuleEngine("My.slnx", o => o.AddRulePack<NamingRulePack>());
await using var provider = services.BuildServiceProvider();
var engine = provider.GetRequiredService<ICodeRuleEngine>();
var run = await engine.RunAllAsync(cancellationToken); // every rule
var naming = await engine.RunAsync(RuleFilter.Tag("Naming"), cancellationToken); // a selection
var single = await engine.RunAsync("NAMING_001", cancellationToken); // exactly one rule
run.EnsureSuccess(); // throws with a readable report when a rule failed or could not run
| Type | Content |
|---|---|
RunResult |
one RuleResult per rule, Overall outcome, IsSuccess, EnsureSuccess() |
RuleResult |
the rule descriptor, Outcome (Passed, Failed, Skipped, Error), findings, Suppressed (in code), SkippedInScope (scoped skips), FindingProjects, FindingNamespaces, EnsureSuccess() |
RuleFilter |
All, Id("NAMING_*"), Group("Controllers"), Tag("Naming"), Of<TRule>() |
ICodeRuleEngine.Catalog |
the validated rules (IDs, groups, tags) without running anything |
Output
RuleResultFormatter.Format(results): the human report.AiResultFormatter.Format(results, rerun): the JSON schema shared by all hosts (test adapters and CLI), including the instructions for AI agents ("fix the code, not the rule") and, with the optionalreruncallback, the command that checks a fix per result.RepositoryPath: repository relative, clickablepath:line:columnlocations.- The test adapters switch with
UseOutputMode(...)in the setup orCodeRuleSettings__OutputMode=ai, see Output mode;CodeRuleOutput.Resolve(configured)applies the same order in your own host.
Testing a rule: RuleTester<TRule>
Every rule deserves its own unit tests. RuleTester<TRule> (namespace Siemens.AspNet.CodeRules.Engine) runs one rule
against inline code. No solution is parsed and every test gets its own engine, so the tests run in parallel. It works
with any test framework: a failure is a RuleTestException with a readable message.
[TestClass]
public sealed class RecordPropertiesMustBeImmutable_Tests(TestContext testContext)
{
private readonly RuleTester<RecordPropertiesMustBeImmutable> _rule = new();
[TestMethod]
public async Task Should_Report_Finding_When_Record_Property_Has_A_Setter()
{
// The rule must report exactly the code marked with [| ... |].
await _rule.ViolatesAsync("""
public record Order(int Id)
{
[|public string Status { get; set; } = "";|]
}
""", testContext.CancellationToken);
}
[TestMethod]
public async Task Should_Report_No_Finding_When_Record_Property_Is_Immutable()
{
await _rule.CompliesAsync("""
public record Order(int Id)
{
public string Status { get; init; } = "";
}
""", testContext.CancellationToken);
}
[TestMethod]
public async Task Should_Suggest_An_Init_Accessor()
{
var finding = await _rule.ViolatesOnceAsync("""
public record Order(int Id)
{
[|public string Status { get; set; } = "";|]
}
""", testContext.CancellationToken);
Assert.AreEqual("public string Status { get; init; } = \"\";", finding.ShouldBe);
}
}
| Method | Checks |
|---|---|
ViolatesAsync |
the rule reports exactly the marked spans ([|...|], several allowed) |
CompliesAsync |
the rule reports nothing |
ViolatesOnceAsync |
exactly one finding, returned to check What, ShouldBe, Details, ... |
RunAsync |
the raw RuleResult |
Every method also takes several files: TestFile.Production("Order.cs", code) and TestFile.Test("OrderTests.cs", code)
fill ProductionTrees and TestTrees. new RuleTester<TRule>(o => ...) adds own contexts (AddContext,
AddContextProvider) or services (ConfigureServices).
License
Copyright (c) Siemens AG. See LICENSE.md in the package.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. 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. |
-
net10.0
- Microsoft.Extensions.DependencyInjection (>= 10.0.14)
- Siemens.AspNet.CodeRules.Contracts (>= 7.7.4)
- Solution.Parser (>= 6.1.4)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on Siemens.AspNet.CodeRules.Engine:
| Package | Downloads |
|---|---|
|
Siemens.AspNet.CodeRules.MSTest
Runs code rules as MSTest tests: one ICodeRuleSetup in your test project, and a source generator writes one test per registered rule at build time. Test Explorer, categories per group and tag, human or AI output. |
|
|
Siemens.AspNet.CodeRules.XUnit
Runs code rules as xUnit v3 tests: one ICodeRuleSetup in your test project, and a source generator writes one test per registered rule at build time. Test Explorer, category traits per group and tag, human or AI output. |
|
|
Siemens.AspNet.CodeRules.NUnit
Runs code rules as NUnit 4 tests: one ICodeRuleSetup in your test project, and a source generator writes one test per registered rule at build time. Test Explorer, categories per group and tag, human or AI output. |
GitHub repositories
This package is not used by any popular GitHub repositories.
## Next
First release. The engine behind the test adapters and the `coderules` CLI.
- `AddCodeRuleEngine(solution, options)`: register rules with `AddRule`, `AddRules`, `AddRulePack` and `AddRulesFrom`; the solution is parsed once per run, contexts are shared and rules run in parallel.
- Skips with a reason for a rule, pack, group or tag, optionally limited to a scope (`InProject`, `InNamespace`, `InPath`); `ExcludeFromAnalysis` takes files out of the analysis (experiments, generated code).
- Accept single findings in the code with `[SuppressMessage]` or `#pragma warning disable`, always with a reason; rules can forbid it with `AllowSuppression => false`.
- `ICodeRuleSetup`: one composition root per code rule project, read by the source generators, the test adapters and the CLI alike.
- `ICodeRuleEngine` for own hosts: `RunAllAsync`, `RunAsync` with `RuleFilter`, the rule catalog and `EnsureSuccess`.
- Human output and one JSON schema for AI agents (with a `rerun` command per result), switched with `UseOutputMode` or `CodeRuleSettings__OutputMode`.
- `RuleTester<TRule>` for unit tests of single rules against inline code, independent of the test framework.