Browse pages
Check .NET Code with the Analyzers
The LiteSpec Roslyn analyzers report breaks in LiteSpec .NET rules as compiler warnings on every build, so a team sees each break in the editor instead of finding it in review. This guide installs the analyzer package once for the 5 backend projects, shows how to read and fix the first warnings, and raises enforced rules to build errors that fail the build. Each step names the standard it satisfies, and the guide adds no rule of its own.
Prerequisites#
Start with:
- The .NET 9.0.300 SDK, or Visual Studio 17.14 or later, whose compiler the analyzer package targets.
- A solution with the five application projects that the .NET architecture page requires: Domain, Application.Abstractions, Application, Infrastructure, and WebApi.
- One shared build file that all five projects import, such as
apps/api/Directory.Build.props, with central package management enabled as the .NET configuration page describes.
Procedure#
1. Add the package once for all five projects#
Add one PackageReference in apps/api/Directory.Build.props so every project inherits it:
<ItemGroup>
<PackageReference Include="LiteSpec.Analyzers" Version="2.0.0" PrivateAssets="all" />
</ItemGroup>PrivateAssets="all" keeps the analyzer out of any package you publish. Keep the version in one central place to satisfy litespec/backend/dotnet/packages#pin-every-dependency-centrally: with central package management, put the version in apps/api/Directory.Packages.props instead:
<PackageVersion Include="LiteSpec.Analyzers" Version="2.0.0" />and omit Version from the PackageReference. Never add a second copy in a project file.
2. Confirm each project layer#
Each rule runs only in the layers its provision covers, and the analyzers read the layer from the assembly name suffix. Shop.Domain resolves as Domain, Shop.Application.Abstractions as Application.Abstractions, and the same suffix rule maps Application, Infrastructure, and WebApi. An assembly whose name carries no layer suffix is not analyzed. The resolver checks the two-part Application.Abstractions suffix before Application, so Shop.Application.Abstractions never resolves as Application.
When a project name does not end in a layer name, set the layer in its project file:
<PropertyGroup>
<LiteSpecLayer>Domain</LiteSpecLayer>
</PropertyGroup>The values are Domain, Application.Abstractions, Application, Infrastructure, WebApi, and None. Set None to opt a project out, for example a test project or a Worker. The package ships a props file that exposes LiteSpecLayer to the compiler, so no extra wiring is needed. This step implements litespec/backend/dotnet/architecture#use-five-application-projects.
3. Build and read the first diagnostics#
Build the solution:
dotnet build apps/api/Shop.slnxEach finding is a warning by default and names the provision it enforces. A typical first line reads:
warning LS1002: Enum 'OrderStatus' models a closed set; use an abstract record with sealed cases and a stable Code (https://litespec.dev/2.0.0/backend/dotnet/domain/#model-every-closed-set-of-domain-values-without-enums)The trailing address is the help link: it opens the provision the rule enforces, here litespec/backend/dotnet/domain#model-every-closed-set-of-domain-values-without-enums. In Visual Studio the same warning appears in the Error List with its ID, file, and line, and the tooltip carries the help link. In the terminal, dotnet build prints the line above.
4. Fix a finding#
Work through one finding at a time, starting with the Domain layer. The two examples below cover the most common first warnings. For any other warning, open its rule page from the Analyzer rules overview, which states what the rule reports and how to fix the code.
Replace an enum with a closed record hierarchy#
LS1002 reports any enum in Domain. Before:
public enum OrderStatus
{
Pending,
Succeeded,
Failed,
}After the rewrite, following the shape on the .NET domain page:
public abstract record OrderStatus
{
public abstract string Code { get; }
public static OrderStatus FromCode(string code) => code switch
{
"pending" => new OrderPendingStatus(),
"succeeded" => new OrderSucceededStatus(),
"failed" => new OrderFailedStatus(),
_ => throw new OrderUnsupportedStatusException(code),
};
}
public sealed record OrderPendingStatus : OrderStatus
{
public override string Code => "pending";
}
public sealed record OrderSucceededStatus : OrderStatus
{
public override string Code => "succeeded";
}
public sealed record OrderFailedStatus : OrderStatus
{
public override string Code => "failed";
}Boundary code converts a label through FromCode, and an unrecognized label fails validation instead of falling into a default case. Callers branch on the case type with a switch expression. Define OrderUnsupportedStatusException as a sealed DomainException subclass with a stable code, as litespec/backend/dotnet/domain#reject-business-violations-with-domain-exceptions requires.
Pass time in as an argument#
LS1009 reports any read of a clock, randomness, or new identifier in Domain, such as DateTimeOffset.UtcNow, Guid.NewGuid, or Random.Shared. Before:
public void Publish()
{
State = new PostPublishedState(DateTimeOffset.UtcNow);
}After:
public void Publish(DateTimeOffset publishedAt)
{
State = new PostPublishedState(publishedAt);
}The caller in Application supplies publishedAt from its own clock, and tests pass an explicit value, so Domain stays deterministic. This matches the Publish shape on the .NET domain page and satisfies litespec/backend/domain#pass-nondeterministic-values-into-domain.
5. Make the build fail on findings in CI#
Raise the severities of the rules you enforce in .editorconfig:
[*.cs]
dotnet_diagnostic.LS1002.severity = error
dotnet_diagnostic.LS1009.severity = errorThen fail the build on those warnings. The workspace already treats warnings as errors through apps/api/Directory.Build.props under litespec/backend/dotnet/config#centralize-net-build-settings; to fail only on analyzer diagnostics instead, set:
<PropertyGroup>
<WarningsAsErrors>$(WarningsAsErrors);LS1002;LS1009</WarningsAsErrors>
</PropertyGroup>Extend the list with each further ID the team enforces. The build job compiles the solution with dotnet build <solution>, so a diagnostic raised to an error fails the pull request under litespec/quality/ci#run-applicable-gates-on-every-pull-request. See the continuous integration page for the jobs each change selects.
6. Record a deliberate departure#
When a finding is wrong for the project, record an Exception that cites the provision, with its reason and review date, as the model page describes. Keep the record at docs/exceptions/<name>.md, then suppress narrowly next to the code:
#pragma warning disable LS1011 // Exception: docs/exceptions/domain-docs-deferred.md
public string LegacyLabel { get; }
#pragma warning restore LS1011Or scope an .editorconfig entry to the one file:
[Shop.Domain/Legacy/LegacyLabels.cs]
dotnet_diagnostic.LS1011.severity = suggestionThe example above quiets litespec/backend/dotnet/domain#document-every-public-domain-contract for one legacy file only. Never set a project-wide none without the record: the record carries the departure, and the suppression only quiets the compiler where the record allows.
Verification#
Confirm the setup with these checks:
dotnet build apps/api/Shop.slnxlists noLSwarnings, or lists only ones an Exception record covers.- Add a temporary enum to the Domain project and rebuild: the build reports
LS1002naming the enum. - Remove the enum and rebuild: the warning is gone.