Skip to content

fix(analyzers): scale Orleans contract regeneration - #10915

Open
ReubenBond wants to merge 10 commits into
dotnet:mainfrom
ReubenBond:rb-fix-contract-regeneration-scale
Open

fix(analyzers): scale Orleans contract regeneration#10915
ReubenBond wants to merge 10 commits into
dotnet:mainfrom
ReubenBond:rb-fix-contract-regeneration-scale

Conversation

@ReubenBond

@ReubenBond ReubenBond commented Aug 29, 2026

Copy link
Copy Markdown
Member

Problem

Solution-wide Orleans contract regeneration loaded and analyzed every project which received the Orleans analyzer package. In Sydney, the documented dotnet format solution command exceeded 30 minutes, consumed several gigabytes of memory, and had not created a missing manifest before cancellation.

Missing manifests also needed to remain creatable without a seed OrleansContracts.txt file.

Solution

  • Register configured contract paths during design-time builds before the file exists, while regular builds continue to register existing manifests only.
  • Preserve Orleans.Analyzers.dll and its public implementation types, but move contract analyzer discovery into a small conditional registration assembly.
  • Add the contract registration assembly only when EnableOrleansContractsAnalyzer is true, so repositories can scope contract analysis to manifest-owning projects without removing ordinary Orleans analyzers.
  • Add the Microsoft.Orleans.ContractTool .NET tool. It evaluates a project or solution, selects analyzer-enabled C# projects through MSBuild extension points, creates a temporary filtered .slnx, and runs regeneration against that smaller workspace.
  • Keep project-level dotnet format creation for missing default and custom manifest paths, including parent directories.
  • Retain deterministic bounded Fix All behavior and add package, CLI, workspace, and registration regression coverage.

Rationale

dotnet format performs solution-wide analyzer discovery and diagnostics before invoking Fix All, so Fix All concurrency alone cannot address large workspace loading. Conditional analyzer registration avoids loading contract diagnostics in unrelated projects, and the tool avoids opening unrelated solution projects entirely while preserving one command for repository-wide regeneration.

Fixes #10914
Fixes #10871

Copilot AI lite review requested due to automatic review settings August 29, 2026 05:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Lite
Findings: 1 Medium severity

New issues introduced by this change (1)
Severity Finding
Medium severity test/​Orleans.Analyzers.Tests/​GrainInterfaceVersionAnalyzerTest.csdotnet is executed with WorkingDirectory = tempDirectory, so the repo’s global.json is not…
What changed in this PR

Improves scalability and determinism of Orleans contract manifest regeneration (especially “Fix all in solution” and dotnet format analyzers) by avoiding redundant diagnostics passes, narrowing the project set to regenerate, and bounding concurrency/memory while ensuring missing manifests (and configured paths) can be created.

Changes:

  • Refactors solution-wide regeneration to preselect projects via Orleans-reference reachability + existing manifests, then regenerate with bounded concurrency and apply compact results.
  • Updates analyzer MSBuild targets to always register the configured contracts path as an AdditionalFiles item (with metadata indicating whether the file exists).
  • Expands analyzer/code-fix test coverage, including real dotnet format CLI scenarios and determinism/regression cases; updates docs accordingly.
File Description
test/​Orleans.Analyzers.Tests/​GrainInterfaceVersionAnalyzerTest.cs Adds extensive FixAll/CLI regression coverage and new analyzer-config helper logic.
src/​Orleans.Analyzers/​GrainInterfaceVersionCodeFix.cs Reworks solution FixAll regeneration for scalability (project selection, bounded concurrency, compact apply step).
src/​Orleans.Analyzers/​build/​Microsoft.Orleans.Analyzers.targets Registers contracts path as AdditionalFiles even before it exists; exposes existence metadata.
docs/​site/​src/​content/​docs/​grains/​grain-versioning/​contract-compatibility-analyzer.md Updates guidance for new regeneration behavior and selection/concurrency semantics.
docs/​site/​src/​content/​docs/​diagnostics/​orleans0020.md Updates ORLEANS0020 guidance to reflect creating missing manifests (including parent directories).
Suppressed comments (2)

test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs:2847

  • Same as the restore step: using tempDirectory as the working directory bypasses global.json SDK selection. Using repositoryRoot makes the integration test deterministic w.r.t. SDK selection.
            var format = await RunDotNetAsync(
                tempDirectory,
                "format",

test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs:2874

  • Same as above: consider running dotnet build with WorkingDirectory = repositoryRoot so SDK selection honors global.json.
            var build = await RunDotNetAsync(
                tempDirectory,
                "build",

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs Outdated
Copilot AI review requested due to automatic review settings August 29, 2026 10:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Lite
Findings: 1 Medium severity

New issues introduced by this change (1)
Severity Finding
Medium severity test/​Orleans.Analyzers.Tests/​GrainInterfaceVersionAnalyzerTest.csconfiguredPathProperty hard-codes a Windows path separator (\) inside the generated MSBuild…
Issues resolved since last review (1)
Severity Finding
Medium severity test/​Orleans.Analyzers.Tests/​GrainInterfaceVersionAnalyzerTest.csdotnet is executed with WorkingDirectory = tempDirectory, so the repo’s global.json is not… View resolved comment

Comment thread test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs Outdated
Copilot AI review requested due to automatic review settings August 29, 2026 10:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Review tier: Lite
Findings: None

Issues resolved since last review (1)
Severity Finding
Medium severity test/​Orleans.Analyzers.Tests/​GrainInterfaceVersionAnalyzerTest.csconfiguredPathProperty hard-codes a Windows path separator (\) inside the generated MSBuild… View resolved comment
Suppressed comments (3)

Previously missed (3) — in code that hasn't changed since the last review.

src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs:212

  • hasExistingManifest is based on existingText.Length > 0, so an existing-but-empty manifest (or an existing manifest whose text is not loaded yet) can be treated as non-existent. When the project also has no active contracts, regeneration returns null and the existing manifest is left unchanged. Since this PR already introduces OrleansContractsFileExists metadata to distinguish placeholders vs existing manifests, use HasExistingContractsManifest(project) when deciding whether regeneration should proceed.
        var hasExistingManifest = existingText is { Length: > 0 };
        if (!hasActiveContracts && !hasExistingManifest)
        {
            return null;
        }

docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md:33

  • This documentation implies the configured contracts path is always registered as an AdditionalFile before it exists. In the shipped MSBuild targets, the placeholder AdditionalFiles item is only included during design-time builds (or when the file exists). Consider clarifying this so readers understand the behavior is specifically for IDE/dotnet format workflows.
By default, the analyzer tracks `OrleansContracts.txt` beside the project file. The analyzer package registers the configured path as a compiler `AdditionalFile`, including before the file exists, so no explicit `AdditionalFiles` item or seed file is required.

docs/site/src/content/docs/diagnostics/orleans0020.md:26

  • The code fix itself only updates the workspace; directory creation is performed by the host when it writes changes to disk (for example, dotnet format). To avoid over-promising behavior across different hosts, consider rephrasing this sentence to attribute directory creation to tooling/hosts rather than the code fix implementation itself.
Apply **Regenerate OrleansContracts.txt** to create and populate the complete project manifest. The configured path can be absent; the code fix creates the file and its parent directory. Use **Fix all in solution** to create manifests for every affected project, then add the generated files to source control and review the baseline using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Lite
Findings: 1 Medium severity

New issues introduced by this change (1)
Severity Finding
Medium severity src/​Orleans.Analyzers/​GrainInterfaceVersionCodeFix.cs — HasExistingContractsManifest relies on document.TryGetText(out var text) to detect an existing…

Comment thread src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs Outdated
Copilot AI review requested due to automatic review settings August 29, 2026 14:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Review tier: Lite
Findings: 1 Low severity

New issues introduced by this change (1)
Severity Finding
Low severity docs/​site/​src/​content/​docs/​grains/​grain-versioning/​contract-compatibility-analyzer.md — The docs say the analyzer package registers the contracts path as an AdditionalFile "including…
Issues resolved since last review (1)
Severity Finding
Medium severity src/​Orleans.Analyzers/​GrainInterfaceVersionCodeFix.cs — HasExistingContractsManifest relies on document.TryGetText(out var text) to detect an existing… View resolved comment

Copilot AI review requested due to automatic review settings August 29, 2026 14:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Review tier: Lite
Findings: None

Issues resolved since last review (1)
Severity Finding
Low severity docs/​site/​src/​content/​docs/​grains/​grain-versioning/​contract-compatibility-analyzer.md — The docs say the analyzer package registers the contracts path as an AdditionalFile "including… View resolved comment
Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs:296

  • PathsEqual only normalizes to full paths when both inputs are rooted. When one side is rooted (e.g., configured absolute path) and the other is not (e.g., an existing AdditionalDocument with null FilePath, so the caller passes document.Name), this will never match and can cause regeneration to create a duplicate AdditionalDocument instead of updating the existing one (especially for custom manifest filenames). Consider adding a fallback for the mixed rooted/unrooted case (eg compare file names) so existing workspace documents without FilePath can still be recognized.
        if (Path.IsPathRooted(left) && Path.IsPathRooted(right))
        {
            left = Path.GetFullPath(left);
            right = Path.GetFullPath(right);
        }

Copilot AI review requested due to automatic review settings August 30, 2026 02:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Lite
Findings: 1 High severity

New issues introduced by this change (1)
Severity Finding
High severity src/​Orleans.Analyzers.Contracts/​Orleans.Analyzers.Contracts.csproj — The PackagePath values here include the filename (e.g. build\Microsoft.Orleans.Analyzers.props).…

Comment thread src/Orleans.Analyzers.Contracts/Orleans.Analyzers.Contracts.csproj
Copilot AI review requested due to automatic review settings August 30, 2026 02:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Lite
Findings: 2 High severity

New issues introduced by this change (1)
Severity Finding
High severity src/​Orleans.Analyzers.Contracts/​Orleans.Analyzers.Contracts.csprojOrleans.Analyzers.Contracts.csproj packs Orleans.Analyzers.dll from…
Pre-existing issues (1)
Severity Finding
High severity src/​Orleans.Analyzers.Contracts/​Orleans.Analyzers.Contracts.csproj — The PackagePath values here include the filename (e.g. build\Microsoft.Orleans.Analyzers.props).… View comment

Comment thread src/Orleans.Analyzers.Contracts/Orleans.Analyzers.Contracts.csproj
Copilot AI review requested due to automatic review settings August 30, 2026 02:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Review tier: Lite
Findings: 1 High severity

Pre-existing issues (1)
Severity Finding
High severity src/​Orleans.Analyzers.Contracts/​Orleans.Analyzers.Contracts.csproj — The PackagePath values here include the filename (e.g. build\Microsoft.Orleans.Analyzers.props).… View comment
Issues resolved since last review (1)
Severity Finding
High severity src/​Orleans.Analyzers.Contracts/​Orleans.Analyzers.Contracts.csprojOrleans.Analyzers.Contracts.csproj packs Orleans.Analyzers.dll from… View resolved comment
Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

src/Orleans.ContractTool/Program.cs:145

  • The finally block unconditionally deletes the temporary directory. If deletion fails (common on Windows due to file locks/AV), the tool will throw and potentially report failure even after successfully regenerating contracts. Cleanup should be best-effort and must not mask the real exit code.
finally
{
    Directory.Delete(temporaryDirectory, recursive: true);
}

test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs:2906

  • Test cleanup deletes the temp directory unconditionally. This can cause flaky failures if dotnet leaves files temporarily locked (especially on Windows). Consider making cleanup best-effort (check existence and swallow IO/unauthorized exceptions) so the test result reflects the actual assertions, not cleanup timing.
        finally
        {
            Directory.Delete(tempDirectory, recursive: true);
        }

Copilot AI review requested due to automatic review settings August 30, 2026 03:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Review tier: Lite
Findings: 1 High severity

New issues introduced by this change (1)
Severity Finding
High severity src/​Orleans.Analyzers/​build/​Microsoft.Orleans.Analyzers.targets<Analyzer Include="$(OrleansContractsAnalyzerPath)" /> is unconditional when…
Issues resolved since last review (1)
Severity Finding
High severity src/​Orleans.Analyzers.Contracts/​Orleans.Analyzers.Contracts.csproj — The PackagePath values here include the filename (e.g. build\Microsoft.Orleans.Analyzers.props).… View resolved comment

Comment thread src/Orleans.Analyzers/build/Microsoft.Orleans.Analyzers.targets
Copilot AI review requested due to automatic review settings August 30, 2026 04:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Review tier: Lite
Findings: None

Issues resolved since last review (1)
Severity Finding
High severity src/​Orleans.Analyzers/​build/​Microsoft.Orleans.Analyzers.targets<Analyzer Include="$(OrleansContractsAnalyzerPath)" /> is unconditional when… View resolved comment
Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/Orleans.ContractTool/Program.cs:109

  • The solution-mode project filter can miss analyzer-enabled projects whose Orleans contract declarations come from linked/externally-included .cs files (outside the project directory). ContainsContractDeclarations only scans *.cs under Path.GetDirectoryName(projectPath), so such projects may be excluded when the manifest is missing, preventing the tool from creating the initial manifest for them.
        .Where(project =>
            File.Exists(project.ProjectPath)
            && string.Equals(Path.GetExtension(project.ProjectPath), ".csproj", StringComparison.OrdinalIgnoreCase)
            && (File.Exists(project.ContractsPath) || ContainsContractDeclarations(project.ProjectPath)))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Solution-wide OrleansContracts regeneration scales poorly on large solutions ORLEANS0020 should offer a code fix to create OrleansContracts.txt

2 participants