# Check .NET Code with the Analyzers

## Purpose

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](https://litespec.dev/2.0.0/backend/dotnet/architecture.md#use-five-application-projects) 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](https://litespec.dev/2.0.0/backend/dotnet/config.md#centralize-nuget-versions) 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:

```xml
<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:

```xml
<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:

```xml
<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:

```sh
dotnet build apps/api/Shop.slnx
```

Each finding is a warning by default and names the provision it enforces. A typical first line reads:

```text
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](https://litespec.dev/2.0.0/analyzers/rules/README.md), 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:

```csharp
public enum OrderStatus
{
    Pending,
    Succeeded,
    Failed,
}
```

After the rewrite, following the shape on [the .NET domain page](https://litespec.dev/2.0.0/backend/dotnet/domain.md#model-every-closed-set-of-domain-values-without-enums):

```csharp
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:

```csharp
public void Publish()
{
    State = new PostPublishedState(DateTimeOffset.UtcNow);
}
```

After:

```csharp
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](https://litespec.dev/2.0.0/backend/dotnet/domain.md#model-every-aggregate-lifecycle-with-state-records) 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`:

```ini
[*.cs]
dotnet_diagnostic.LS1002.severity = error
dotnet_diagnostic.LS1009.severity = error
```

Then 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:

```xml
<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](https://litespec.dev/2.0.0/quality/ci.md#run-applicable-gates-on-every-pull-request) 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](https://litespec.dev/2.0.0/core/model.md) describes. Keep the record at `docs/exceptions/<name>.md`, then suppress narrowly next to the code:

```csharp
#pragma warning disable LS1011 // Exception: docs/exceptions/domain-docs-deferred.md
public string LegacyLabel { get; }
#pragma warning restore LS1011
```

Or scope an `.editorconfig` entry to the one file:

```ini
[Shop.Domain/Legacy/LegacyLabels.cs]
dotnet_diagnostic.LS1011.severity = suggestion
```

The 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.slnx` lists no `LS` warnings, or lists only ones an Exception record covers.
- Add a temporary enum to the Domain project and rebuild: the build reports `LS1002` naming the enum.
- Remove the enum and rebuild: the warning is gone.
