fix(analyzers): scale Orleans contract regeneration - #10915
Conversation
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review tier: Lite
Findings: 1
New issues introduced by this change (1)
| Severity | Finding |
|---|---|
test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs — dotnet 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
AdditionalFilesitem (with metadata indicating whether the file exists). - Expands analyzer/code-fix test coverage, including real
dotnet formatCLI 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
tempDirectoryas the working directory bypassesglobal.jsonSDK selection. UsingrepositoryRootmakes 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 buildwithWorkingDirectory = repositoryRootso SDK selection honorsglobal.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.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review tier: Lite
Findings: 1
New issues introduced by this change (1)
| Severity | Finding |
|---|---|
test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs — configuredPathProperty hard-codes a Windows path separator (\) inside the generated MSBuild… |
Issues resolved since last review (1)
| Severity | Finding |
|---|---|
test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs — dotnet is executed with WorkingDirectory = tempDirectory, so the repo’s global.json is not… View resolved comment |
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
Review tier: Lite
Findings: None
Issues resolved since last review (1)
| Severity | Finding |
|---|---|
test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs — configuredPathProperty 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
hasExistingManifestis based onexistingText.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 returnsnulland the existing manifest is left unchanged. Since this PR already introducesOrleansContractsFileExistsmetadata to distinguish placeholders vs existing manifests, useHasExistingContractsManifest(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
AdditionalFilebefore it exists. In the shipped MSBuild targets, the placeholderAdditionalFilesitem 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 formatworkflows.
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).
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review tier: Lite
Findings: 1
New issues introduced by this change (1)
| Severity | Finding |
|---|---|
src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs — HasExistingContractsManifest relies on document.TryGetText(out var text) to detect an existing… |
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
Review tier: Lite
Findings: 1
New issues introduced by this change (1)
| Severity | Finding |
|---|---|
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 |
|---|---|
src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs — HasExistingContractsManifest relies on document.TryGetText(out var text) to detect an existing… View resolved comment |
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
Review tier: Lite
Findings: None
Issues resolved since last review (1)
| Severity | Finding |
|---|---|
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);
}
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review tier: Lite
Findings: 1
New issues introduced by this change (1)
| Severity | Finding |
|---|---|
src/Orleans.Analyzers.Contracts/Orleans.Analyzers.Contracts.csproj — The PackagePath values here include the filename (e.g. build\Microsoft.Orleans.Analyzers.props).… |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review tier: Lite
Findings: 2
New issues introduced by this change (1)
| Severity | Finding |
|---|---|
src/Orleans.Analyzers.Contracts/Orleans.Analyzers.Contracts.csproj — Orleans.Analyzers.Contracts.csproj packs Orleans.Analyzers.dll from… |
Pre-existing issues (1)
| Severity | Finding |
|---|---|
src/Orleans.Analyzers.Contracts/Orleans.Analyzers.Contracts.csproj — The PackagePath values here include the filename (e.g. build\Microsoft.Orleans.Analyzers.props).… View comment |
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
Review tier: Lite
Findings: 1
Pre-existing issues (1)
| Severity | Finding |
|---|---|
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 |
|---|---|
src/Orleans.Analyzers.Contracts/Orleans.Analyzers.Contracts.csproj — Orleans.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
finallyblock 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
dotnetleaves 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);
}
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
Review tier: Lite
Findings: 1
New issues introduced by this change (1)
| Severity | Finding |
|---|---|
src/Orleans.Analyzers/build/Microsoft.Orleans.Analyzers.targets — <Analyzer Include="$(OrleansContractsAnalyzerPath)" /> is unconditional when… |
Issues resolved since last review (1)
| Severity | Finding |
|---|---|
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 |
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
Review tier: Lite
Findings: None
Issues resolved since last review (1)
| Severity | Finding |
|---|---|
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).
ContainsContractDeclarationsonly scans*.csunderPath.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)))



Problem
Solution-wide Orleans contract regeneration loaded and analyzed every project which received the Orleans analyzer package. In Sydney, the documented
dotnet formatsolution 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.txtfile.Solution
Orleans.Analyzers.dlland its public implementation types, but move contract analyzer discovery into a small conditional registration assembly.EnableOrleansContractsAnalyzeristrue, so repositories can scope contract analysis to manifest-owning projects without removing ordinary Orleans analyzers.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.dotnet formatcreation for missing default and custom manifest paths, including parent directories.Rationale
dotnet formatperforms 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