From 3900286244ab95fedf6e73f37d060f270640c637 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Thu, 27 Aug 2026 15:44:59 -0700 Subject: [PATCH 01/11] feat(analyzers): regenerate Orleans contracts --- .../content/docs/diagnostics/orleans0016.md | 4 +- .../content/docs/diagnostics/orleans0017.md | 4 +- .../content/docs/diagnostics/orleans0018.md | 4 +- .../content/docs/diagnostics/orleans0019.md | 4 +- .../content/docs/diagnostics/orleans0020.md | 6 +- .../content/docs/diagnostics/orleans0021.md | 6 +- .../content/docs/diagnostics/orleans0022.md | 4 +- .../content/docs/diagnostics/orleans0023.md | 4 +- .../content/docs/diagnostics/orleans0024.md | 4 +- .../content/docs/diagnostics/orleans0025.md | 6 +- .../contract-compatibility-analyzer.md | 43 +- .../Orleans.Dashboard/OrleansContracts.txt | 7 +- src/Directory.Build.targets | 1 + .../GrainInterfaceVersionAnalyzer.cs | 239 ++++++-- .../GrainInterfaceVersionCodeFix.cs | 511 ++++++++++++++++-- src/Orleans.Analyzers/Resources.resx | 16 +- .../build/Microsoft.Orleans.Analyzers.props | 1 + .../OrleansContracts.txt | 7 +- .../OrleansContracts.txt | 7 +- src/Orleans.Core/OrleansContracts.txt | 7 +- src/Orleans.DurableJobs/OrleansContracts.txt | 7 +- .../OrleansContracts.txt | 7 +- .../OrleansContracts.txt | 7 +- src/Orleans.Reminders/OrleansContracts.txt | 7 +- src/Orleans.Runtime/OrleansContracts.txt | 7 +- src/Orleans.Streaming/OrleansContracts.txt | 7 +- src/Orleans.TestingHost/OrleansContracts.txt | 7 +- .../OrleansContracts.txt | 7 +- src/Orleans.Transactions/OrleansContracts.txt | 7 +- .../GrainInterfaceVersionAnalyzerTest.cs | 437 ++++++++++++++- 30 files changed, 1218 insertions(+), 167 deletions(-) diff --git a/docs/site/src/content/docs/diagnostics/orleans0016.md b/docs/site/src/content/docs/diagnostics/orleans0016.md index 08bc298b164..2f2c75fdebc 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0016.md +++ b/docs/site/src/content/docs/diagnostics/orleans0016.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0016: Grain interface is not active in OrleansContracts.txt" description: Understand and resolve ORLEANS0016 when a grain interface is missing or retired in the contract manifest. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -27,6 +27,8 @@ Verify the interface identity and version, then apply **Add to OrleansContracts. If the interface was restored accidentally, remove it from source or introduce a separately named replacement instead of reusing a retired identity. +Apply **Regenerate OrleansContracts.txt** to rebuild the complete project manifest, or use **Fix all in solution** to update every affected project. Review the generated diff using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). + ## Suppress the diagnostic Deployable RPC contracts should remain in the manifest. If the project intentionally does not maintain a contract manifest, disable the contract analyzer for the project instead of suppressing individual interfaces. diff --git a/docs/site/src/content/docs/diagnostics/orleans0017.md b/docs/site/src/content/docs/diagnostics/orleans0017.md index a96a5e70afa..983451d8b08 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0017.md +++ b/docs/site/src/content/docs/diagnostics/orleans0017.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0017: Grain interface version mismatch" description: Understand and resolve ORLEANS0017 when a grain interface version differs from OrleansContracts.txt. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -25,6 +25,8 @@ The manifest no longer describes the numeric version used by runtime compatibili Determine whether the source or manifest changed unintentionally. Restore the previous source version, or review the rolling-upgrade implications and apply **Update version in OrleansContracts.txt** when the new version is intentional. +Apply **Regenerate OrleansContracts.txt** to rebuild the complete project manifest, or use **Fix all in solution** to update every affected project. Review the generated diff using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). + ## Suppress the diagnostic Use suppression only during a short-lived staged edit. Do not release with a source and manifest version mismatch. diff --git a/docs/site/src/content/docs/diagnostics/orleans0018.md b/docs/site/src/content/docs/diagnostics/orleans0018.md index 312f137789d..6b06d3cad2e 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0018.md +++ b/docs/site/src/content/docs/diagnostics/orleans0018.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0018: Grain interface member not declared" description: Understand and resolve ORLEANS0018 when an RPC method signature is missing from OrleansContracts.txt. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -25,6 +25,8 @@ Older activations can receive an unknown RPC, and changed identities or payload Prefer preserving the existing method and adding a new method for changed behavior. Review payload compatibility, increment the interface version when appropriate, and apply **Add to OrleansContracts.txt**. The code fix records the new signature but does not increment `[Version]`. +Apply **Regenerate OrleansContracts.txt** to rebuild the complete project manifest, or use **Fix all in solution** to update every affected project. Review the generated diff using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). + ## Suppress the diagnostic Suppress only for a demonstrated signature-normalization false positive where the source and manifest represent the same contract. diff --git a/docs/site/src/content/docs/diagnostics/orleans0019.md b/docs/site/src/content/docs/diagnostics/orleans0019.md index dd6b3a83cce..f258d8b0b37 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0019.md +++ b/docs/site/src/content/docs/diagnostics/orleans0019.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0019: Removed grain interface is not retired" description: Understand and resolve ORLEANS0019 when OrleansContracts.txt contains an active interface that source no longer defines. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -25,6 +25,8 @@ The deletion or identity-changing rename is not recorded as intentional, and the Restore the interface if its removal was accidental. Otherwise apply **Mark as *RETIRED* in OrleansContracts.txt**. Preserve retired declarations as contract history. +Apply **Regenerate OrleansContracts.txt** to rebuild the complete project manifest and retire every declaration absent from source, or use **Fix all in solution** to update every affected project. Review the generated diff using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). + ## Suppress the diagnostic Suppression is appropriate only when the manifest intentionally contains contracts owned by another compilation. Prefer one manifest per project so ownership remains explicit. diff --git a/docs/site/src/content/docs/diagnostics/orleans0020.md b/docs/site/src/content/docs/diagnostics/orleans0020.md index 7f181a14e74..aa4568d27fb 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0020.md +++ b/docs/site/src/content/docs/diagnostics/orleans0020.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0020: OrleansContracts.txt is missing" description: Understand and resolve ORLEANS0020 when contract compatibility analysis is enabled without a manifest. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -11,7 +11,7 @@ ms.topic: reference | --- | --- | | Category | Orleans.Versioning | | Severity | Info | -| Code fix | Not available | +| Code fix | Available | ## Cause @@ -23,7 +23,7 @@ The analyzer has no baseline, so it cannot detect RPC identity, signature, versi ## How to fix -Create `OrleansContracts.txt` at `OrleansContractsPath`, add it to source control, and rebuild. Apply the resulting diagnostics' code fixes to populate interface, method, and class declarations. +Apply **Regenerate OrleansContracts.txt** to create and populate the complete project manifest. 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). ## Suppress the diagnostic diff --git a/docs/site/src/content/docs/diagnostics/orleans0021.md b/docs/site/src/content/docs/diagnostics/orleans0021.md index 193859716a8..a8dd3d0d394 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0021.md +++ b/docs/site/src/content/docs/diagnostics/orleans0021.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0021: Duplicate grain interface declaration" description: Understand and resolve ORLEANS0021 when OrleansContracts.txt declares an interface identity more than once. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -15,7 +15,7 @@ ms.topic: reference ## Cause -`OrleansContracts.txt` repeats an interface CLR name or a non-empty `GrainInterfaceType`, including active and retired declarations with the same identity. +`OrleansContracts.txt` repeats an effective interface identity. The effective identity is `GrainInterfaceType` when present and the identity derived from the recorded CLR name using Orleans conventions for a legacy declaration. ## Impact @@ -23,7 +23,7 @@ The manifest is ambiguous. The parser retains the first declaration, so compatib ## How to fix -Merge the declarations into one canonical entry. Keep one active declaration when the interface exists, or one retired declaration when it has been removed. +Merge declarations which have the same effective identity into one canonical entry. Active and retired declarations can share a CLR name when they record different explicit `GrainInterfaceType` values across an identity migration. ## Suppress the diagnostic diff --git a/docs/site/src/content/docs/diagnostics/orleans0022.md b/docs/site/src/content/docs/diagnostics/orleans0022.md index 389b0af22da..e23e1fa69b1 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0022.md +++ b/docs/site/src/content/docs/diagnostics/orleans0022.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0022: Grain class is not active in OrleansContracts.txt" description: Understand and resolve ORLEANS0022 when a concrete grain class is missing or retired in the contract manifest. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -25,6 +25,8 @@ The implementation identity is not protected by contract review. A CLR rename wi Verify the class's durable grain type, add `[GrainType]` when it must remain independent of the CLR name, and apply **Add to OrleansContracts.txt**. The code fix adds or reactivates the class declaration. +Apply **Regenerate OrleansContracts.txt** to rebuild the complete project manifest, or use **Fix all in solution** to update every affected project. Review the generated diff using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). + ## Suppress the diagnostic Suppress only for a grain class intentionally excluded from deployment-contract tracking. diff --git a/docs/site/src/content/docs/diagnostics/orleans0023.md b/docs/site/src/content/docs/diagnostics/orleans0023.md index 4b16080040e..3fffe564cf4 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0023.md +++ b/docs/site/src/content/docs/diagnostics/orleans0023.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0023: Grain class identity mismatch" description: Understand and resolve ORLEANS0023 when a grain class GrainType differs from OrleansContracts.txt. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -27,6 +27,8 @@ Restore the previous `[GrainType]` when the change was accidental. Update the ma The **Update grain class alias in OrleansContracts.txt** code fix accepts the source identity as the new baseline. Review the identity change before applying it. +Apply **Regenerate OrleansContracts.txt** to rebuild the complete project manifest, or use **Fix all in solution** to update every affected project. Review the generated diff using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). + ## Suppress the diagnostic Suppress only for a deliberate, documented identity migration. Updating the reviewed baseline is preferable to retaining a suppression. diff --git a/docs/site/src/content/docs/diagnostics/orleans0024.md b/docs/site/src/content/docs/diagnostics/orleans0024.md index 45f3403e944..ecccb232232 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0024.md +++ b/docs/site/src/content/docs/diagnostics/orleans0024.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0024: Removed grain class is not retired" description: Understand and resolve ORLEANS0024 when OrleansContracts.txt contains an active grain class that source no longer defines. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -25,6 +25,8 @@ The removal or identity-changing rename is not recorded, and the old grain ident Restore the class if its removal was accidental. Otherwise apply **Mark grain class as *RETIRED* in OrleansContracts.txt** and preserve the declaration. +Apply **Regenerate OrleansContracts.txt** to rebuild the complete project manifest and retire every declaration absent from source, or use **Fix all in solution** to update every affected project. Review the generated diff using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). + ## Suppress the diagnostic Suppress only when the manifest intentionally includes classes owned by another compilation. Prefer separate project manifests. diff --git a/docs/site/src/content/docs/diagnostics/orleans0025.md b/docs/site/src/content/docs/diagnostics/orleans0025.md index 642cc3de57b..23fe3dd8eca 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0025.md +++ b/docs/site/src/content/docs/diagnostics/orleans0025.md @@ -1,7 +1,7 @@ --- title: "ORLEANS0025: Duplicate grain class declaration" description: Understand and resolve ORLEANS0025 when OrleansContracts.txt declares a grain identity more than once. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: reference --- @@ -15,7 +15,7 @@ ms.topic: reference ## Cause -`OrleansContracts.txt` repeats a grain class CLR name or a non-empty `GrainType`, including active and retired declarations with the same identity. +`OrleansContracts.txt` repeats an effective grain class identity. The effective identity is `GrainType` when present and the identity derived from the recorded CLR name using Orleans conventions for a legacy declaration. ## Impact @@ -23,7 +23,7 @@ The grain identity history becomes ambiguous, and the parser accepts only the fi ## How to fix -Merge or remove duplicates, retaining one canonical active or retired declaration. +Merge declarations which have the same effective identity into one canonical entry. Active and retired declarations can share a CLR name when they record different explicit `GrainType` values across an identity migration. ## Suppress the diagnostic diff --git a/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md b/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md index af2da8618d4..b3a0fa59bb1 100644 --- a/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md +++ b/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md @@ -1,7 +1,7 @@ --- title: Orleans contract compatibility analyzer description: Track grain RPC contracts during development to identify changes which can break rolling upgrades. -ms.date: 08/25/2026 +ms.date: 08/27/2026 ms.topic: concept-article --- @@ -36,31 +36,38 @@ The path can also be set in `Directory.Build.props` to apply a repository conven ## Create and update the manifest -After opting in, build the project. If no manifest exists, diagnostic `ORLEANS0020` identifies the missing file. Create the file, include it in source control, and apply the Orleans code fixes to add missing interface and class entries. Apply the code fixes again after adding RPC methods. +After opting in, build the project. If no manifest exists, diagnostic `ORLEANS0020` identifies the missing file at the first contract declaration. Apply **Regenerate OrleansContracts.txt** to create and populate the manifest for the project. -Code fixes add the generated-file header, preserve the file's line endings, and write entries in stable ordinal order. The resulting file is deterministic regardless of the order in which fixes are applied. +The regeneration code fix rebuilds every active interface, method, and grain-class entry from the project compilation. It preserves existing `*RETIRED*` entries and marks declarations which are no longer in source as retired. The generated header, line endings, and ordinal entry order are deterministic. ### Regenerate the manifest -Use the analyzer code fixes to regenerate the active contracts: +Apply **Regenerate OrleansContracts.txt** from any Orleans contract diagnostic. One application regenerates the entire project manifest. In an IDE, use **Fix all in project** or **Fix all in solution** to regenerate every affected project. -1. Preserve every `*RETIRED*` declaration from the existing manifest. Retired identities are historical data and cannot be reconstructed from current source. -2. Create an empty `OrleansContracts.txt`, then restore the retired declarations. -3. Build the project. -4. Apply each `ORLEANS0016` and `ORLEANS0022` code fix to add active interfaces and grain classes. -5. Build again and apply each `ORLEANS0018` code fix to add interface methods. -6. Review the resulting identity, version, and signature diff before committing it. +Agents and command-line workflows can apply the same fix to a project or solution: -For routine contract changes, keep the existing manifest and apply the reported update or retirement code fix instead of rebuilding it from scratch. +```dotnetcli +dotnet format MySolution.slnx analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +``` + +Add the generated file to source control and review its diff before committing. Treat every changed contract line as a potential wire-compatibility change: + +- A changed `GrainInterfaceType`, `GrainType`, method identity, parameter type, or return type changes a wire identity or signature. +- A removed source contract becomes `*RETIRED*`, preserving its identity history and preventing accidental reuse. +- A `[Version]` change affects version-aware routing and must align with the rolling-upgrade design. +- A CLR comment-only change records a refactor while the explicit Orleans identity remains stable. + +Coding agents should regenerate the manifest instead of hand-editing active entries, retain retired history, and explain the compatibility impact of each contract diff in the change description. ## Manifest format Interface methods are indented beneath their interface: ```text -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Contoso.Grains.ICartGrain")] Contoso.Grains.ICartGrain [Version(1)] AddAsync(Contoso.Grains.Item) -> Task @@ -69,7 +76,7 @@ interface [GrainInterfaceType("Contoso.Grains.ICartGrain")] Contoso.Grains.ICart class [GrainType("cart")] Contoso.Grains.CartGrain ``` -Each declaration includes both its Orleans identity and CLR type name. A diff which changes both values is a breaking identity change. A diff which changes only the CLR type name preserves the Orleans identity. +Each declaration includes both its Orleans identity and CLR type name. A diff which changes the Orleans identity changes the wire contract. A diff which changes only the CLR type name preserves an explicit Orleans identity. Explicit identities remain visible alongside their CLR names: @@ -93,7 +100,7 @@ Comments record CLR names only when they differ from the stable identity. Commen *RETIRED* class [GrainType("legacy")] Contoso.Grains.LegacyGrain ``` -Don't delete retired entries. They preserve the contract history and prevent a removed identity from being unintentionally reused. +Retired entries preserve contract history and prevent a removed identity from being unintentionally reused. ## Refactor-safe identities @@ -104,7 +111,7 @@ The analyzer uses Orleans identities before CLR names: - or identifies grain methods. - identifies serialized parameter and return types. -When these identities remain unchanged, renaming a CLR class, interface, method, parameter, or aliased data type doesn't require a manifest update. Changing an Orleans identity remains a contract change and produces a diagnostic. +When these identities remain unchanged, a CLR class, interface, method, parameter, or aliased data type can be renamed while preserving the wire identity. Changing an Orleans identity produces a contract diff and diagnostic. Without an explicit stable identity, Orleans derives the identity from the CLR type name. Renaming the CLR type therefore changes the derived identity and the contract. @@ -127,4 +134,4 @@ Standard `.editorconfig` diagnostic configuration can change these severities. P ## Scope -The analyzer tracks RPC interface signatures, numeric interface versions, and concrete grain class identities. It doesn't prove behavioral compatibility or validate persisted state schemas. Continue to follow the [backward compatibility guidelines](backward-compatibility-guidelines.md) and test mixed-version deployments before production rollout. +The analyzer tracks RPC interface signatures, numeric interface versions, and concrete grain class identities. Behavioral compatibility and persisted state schemas require separate review using the [backward compatibility guidelines](backward-compatibility-guidelines.md) and mixed-version deployment tests. diff --git a/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt b/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt index 14bcf5a3150..5b473616583 100644 --- a/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt +++ b/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt @@ -1,6 +1,7 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.Dashboard.Core.IDashboardGrain")] Orleans.Dashboard.Core.IDashboardGrain [Version(0)] GetClusterTracing() -> Task>> diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 721094b23ba..2520dcbd010 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -7,6 +7,7 @@ + diff --git a/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs b/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs index 11d31f780a8..8c0b459f8a2 100644 --- a/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs +++ b/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs @@ -218,6 +218,7 @@ private sealed class Impl private readonly INamedTypeSymbol? _iAddressableType; private readonly INamedTypeSymbol? _aliasAttributeType; private readonly INamedTypeSymbol? _versionAttributeType; + private Location? _firstContractLocation; public Impl( Compilation compilation, @@ -262,13 +263,24 @@ public void AnalyzeNamedType(SymbolAnalysisContext context) if (_data is null) { _visitedInterfaces.TryAdd(interfaceName, true); + RecordContractLocation(namedType); // We'll report file missing at compilation end return; } // Check if interface is declared in the file + var explicitGrainInterfaceType = GetStringAttributeValue( + namedType, + Constants.GrainInterfaceTypeAttributeFullyQualifiedName); var grainInterfaceType = GetGrainInterfaceType(namedType); - var declaredInterface = FindDeclaredInterface(interfaceName, grainInterfaceType); + var declaredInterface = FindDeclaredInterface( + interfaceName, + grainInterfaceType, + explicitGrainInterfaceType is null + || string.Equals( + explicitGrainInterfaceType, + GetDefaultGrainInterfaceType(namedType), + StringComparison.Ordinal)); if (declaredInterface is null) { _visitedInterfaces.TryAdd(interfaceName, true); @@ -288,7 +300,7 @@ public void AnalyzeNamedType(SymbolAnalysisContext context) return; } - _visitedInterfaces.TryAdd(declaredInterface.Name, true); + _visitedInterfaces.TryAdd(GetDeclarationKey(declaredInterface), true); // Check if retired if (declaredInterface.IsRetired) @@ -409,7 +421,7 @@ public void OnCompilationEnd(CompilationAnalysisContext context) { context.ReportDiagnostic(Diagnostic.Create( OrleansContractsFileMissingRule, - Location.None, + _firstContractLocation ?? Location.None, Constants.OrleansContractsFileName)); } @@ -419,45 +431,47 @@ public void OnCompilationEnd(CompilationAnalysisContext context) var sourceText = _grainInterfacesFile.GetText(context.CancellationToken); if (sourceText is not null) { - foreach (var kvp in _data.Interfaces) + foreach (var declaredInterface in _data.Interfaces) { - if (kvp.Value.IsRetired) + if (declaredInterface.IsRetired) { continue; } - if (!_visitedInterfaces.ContainsKey(kvp.Key)) + if (!_visitedInterfaces.ContainsKey(GetDeclarationKey(declaredInterface))) { // Interface in file but not in code - needs to be retired - var location = kvp.Value.GetLocation(sourceText, _grainInterfacesFile.Path); + var location = declaredInterface.GetLocation(sourceText, _grainInterfacesFile.Path); var properties = ImmutableDictionary.Empty - .Add(InterfaceNamePropertyKey, kvp.Key); + .Add(InterfaceNamePropertyKey, declaredInterface.Name) + .Add(GrainInterfaceTypePropertyKey, declaredInterface.GrainInterfaceType); context.ReportDiagnostic(Diagnostic.Create( RemovedInterfaceNotRetiredRule, location, properties, - kvp.Key)); + declaredInterface.Name)); } } - foreach (var kvp in _data.Classes) + foreach (var declaredClass in _data.Classes) { - if (kvp.Value.IsRetired || _visitedClasses.ContainsKey(kvp.Key)) + if (declaredClass.IsRetired || _visitedClasses.ContainsKey(GetDeclarationKey(declaredClass))) { continue; } - var location = kvp.Value.GetLocation(sourceText, _grainInterfacesFile.Path); + var location = declaredClass.GetLocation(sourceText, _grainInterfacesFile.Path); var properties = ImmutableDictionary.Empty - .Add(ClassNamePropertyKey, kvp.Key); + .Add(ClassNamePropertyKey, declaredClass.Name) + .Add(ActualAliasPropertyKey, declaredClass.Alias); context.ReportDiagnostic(Diagnostic.Create( RemovedGrainClassNotRetiredRule, location, properties, - kvp.Key)); + declaredClass.Name)); } } } @@ -470,6 +484,7 @@ private void AnalyzeGrainClass(SymbolAnalysisContext context, INamedTypeSymbol n if (_data is null) { _visitedClasses.TryAdd(className, true); + RecordContractLocation(namedType); return; } @@ -487,7 +502,7 @@ private void AnalyzeGrainClass(SymbolAnalysisContext context, INamedTypeSymbol n return; } - _visitedClasses.TryAdd(declaredClass.Name, true); + _visitedClasses.TryAdd(GetDeclarationKey(declaredClass), true); if (declaredClass.Alias is null || string.Equals(codeAlias, declaredClass.Alias, StringComparison.Ordinal)) { @@ -520,43 +535,68 @@ private bool IsRpcContract(INamedTypeSymbol type) && type.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, _iAddressableType)); } - private DeclaredGrainInterface? FindDeclaredInterface(string interfaceName, string? grainInterfaceType) + private void RecordContractLocation(INamedTypeSymbol type) + { + var location = type.Locations.FirstOrDefault(candidate => candidate.IsInSource); + if (location is not null) + { + Interlocked.CompareExchange(ref _firstContractLocation, location, null); + } + } + + private DeclaredGrainInterface? FindDeclaredInterface( + string interfaceName, + string? grainInterfaceType, + bool allowLegacyNameMatch) { if (grainInterfaceType is not null) { - var stableMatch = _data!.Interfaces.Values.FirstOrDefault(candidate => - string.Equals(candidate.GrainInterfaceType, grainInterfaceType, StringComparison.Ordinal)); + var stableMatch = _data!.Interfaces.FirstOrDefault(candidate => + string.Equals(GetDeclarationKey(candidate), grainInterfaceType, StringComparison.Ordinal)); if (stableMatch is not null) { return stableMatch; } - return _data.Interfaces.TryGetValue(interfaceName, out var legacyMatch) - && legacyMatch.GrainInterfaceType is null - ? legacyMatch - : null; + if (allowLegacyNameMatch) + { + return _data.Interfaces.FirstOrDefault(candidate => + candidate.GrainInterfaceType is null + && string.Equals(candidate.Name, interfaceName, StringComparison.Ordinal)); + } + + return null; } - return _data!.Interfaces.TryGetValue(interfaceName, out var result) ? result : null; + return _data!.Interfaces.FirstOrDefault(candidate => + string.Equals(candidate.Name, interfaceName, StringComparison.Ordinal)); } private DeclaredGrainClass? FindDeclaredClass(string className, string? alias) { if (alias is not null) { - var stableMatch = _data!.Classes.Values.FirstOrDefault(candidate => - string.Equals(candidate.Alias, alias, StringComparison.Ordinal)); + var stableMatch = _data!.Classes.FirstOrDefault(candidate => + string.Equals(GetDeclarationKey(candidate), alias, StringComparison.Ordinal)); if (stableMatch is not null) { return stableMatch; } - return _data.Classes.TryGetValue(className, out var exactNameMatch) ? exactNameMatch : null; + return _data.Classes.FirstOrDefault(candidate => + string.Equals(candidate.Name, className, StringComparison.Ordinal)); } - return _data!.Classes.TryGetValue(className, out var result) ? result : null; + return _data!.Classes.FirstOrDefault(candidate => + string.Equals(candidate.Name, className, StringComparison.Ordinal)); } + private static string GetDeclarationKey(DeclaredGrainInterface declaration) + => declaration.GrainInterfaceType ?? GetDefaultGrainInterfaceType(declaration.Name); + + private static string GetDeclarationKey(DeclaredGrainClass declaration) + => declaration.Alias ?? GetDefaultGrainType(declaration.Name); + private ushort GetVersionFromAttribute(INamedTypeSymbol type) { if (_versionAttributeType is null) @@ -610,21 +650,68 @@ internal static string GetGrainType(INamedTypeSymbol type) return grainType; } - var name = type.MetadataName.ToLowerInvariant(); - var arityIndex = name.IndexOf('`'); - var arity = arityIndex >= 0 ? name.Substring(arityIndex) : string.Empty; - if (arityIndex >= 0) + return GetDefaultGrainType(type); + } + + internal static string GetDefaultGrainType(INamedTypeSymbol type) + { + var name = type.Name.ToLowerInvariant(); + + const string GrainSuffix = "grain"; + if (name.EndsWith(GrainSuffix, StringComparison.Ordinal) && name.Length > GrainSuffix.Length) { - name = name.Substring(0, arityIndex); + name = name.Substring(0, name.Length - GrainSuffix.Length); + } + + var arity = 0; + for (var current = type; current is not null; current = current.ContainingType) + { + arity += current.Arity; + } + + return arity > 0 ? $"{name}`{arity}" : name; + } + + internal static string GetDefaultGrainType(string typeName) + { + var simpleNameStart = typeName.LastIndexOf('.') + 1; + var simpleName = typeName.Substring(simpleNameStart); + var genericStart = simpleName.IndexOf('<'); + if (genericStart >= 0) + { + simpleName = simpleName.Substring(0, genericStart); } + var name = simpleName.ToLowerInvariant(); const string GrainSuffix = "grain"; if (name.EndsWith(GrainSuffix, StringComparison.Ordinal) && name.Length > GrainSuffix.Length) { name = name.Substring(0, name.Length - GrainSuffix.Length); } - return name + arity; + var arity = 0; + var searchIndex = 0; + while ((genericStart = typeName.IndexOf('<', searchIndex)) >= 0) + { + var genericEnd = typeName.IndexOf('>', genericStart + 1); + if (genericEnd < 0) + { + break; + } + + arity++; + for (var index = genericStart + 1; index < genericEnd; index++) + { + if (typeName[index] == ',') + { + arity++; + } + } + + searchIndex = genericEnd + 1; + } + + return arity > 0 ? $"{name}`{arity}" : name; } internal static string GetGrainInterfaceType(INamedTypeSymbol type) @@ -634,14 +721,14 @@ internal static string GetGrainInterfaceType(INamedTypeSymbol type) return grainInterfaceType; } - return GetRuntimeTypeName(type); + return GetDefaultGrainInterfaceType(type); } - private static string GetRuntimeTypeName(INamedTypeSymbol type) + internal static string GetDefaultGrainInterfaceType(INamedTypeSymbol type) { if (type.ContainingType is { } containingType) { - return $"{GetRuntimeTypeName(containingType)}+{type.MetadataName}"; + return $"{GetDefaultGrainInterfaceType(containingType)}+{type.MetadataName}"; } return type.ContainingNamespace.IsGlobalNamespace @@ -649,6 +736,41 @@ private static string GetRuntimeTypeName(INamedTypeSymbol type) : $"{type.ContainingNamespace.ToDisplayString()}.{type.MetadataName}"; } + internal static string GetDefaultGrainInterfaceType(string typeName) + { + var segments = typeName.Split('.'); + var firstGenericSegment = Array.FindIndex(segments, segment => segment.IndexOf('<') >= 0); + if (firstGenericSegment < 0) + { + return typeName; + } + + for (var index = firstGenericSegment; index < segments.Length; index++) + { + var genericStart = segments[index].IndexOf('<'); + if (genericStart < 0) + { + continue; + } + + var genericEnd = segments[index].LastIndexOf('>'); + var arity = 1; + for (var characterIndex = genericStart + 1; characterIndex < genericEnd; characterIndex++) + { + if (segments[index][characterIndex] == ',') + { + arity++; + } + } + + segments[index] = $"{segments[index].Substring(0, genericStart)}`{arity}"; + } + + return string.Join(".", segments.Take(firstGenericSegment)) + + (firstGenericSegment > 0 ? "." : string.Empty) + + string.Join("+", segments.Skip(firstGenericSegment)); + } + internal static string GetMethodSignature(IMethodSymbol method) { var sb = new StringBuilder(); @@ -844,9 +966,9 @@ private static bool HasMeaningfulTypeAlias(ITypeSymbol type) /// internal sealed class GrainInterfaceData { - public Dictionary Interfaces { get; } = new(StringComparer.Ordinal); + public List Interfaces { get; } = new(); - public Dictionary Classes { get; } = new(StringComparer.Ordinal); + public List Classes { get; } = new(); } /// @@ -925,13 +1047,13 @@ internal static class GrainInterfaceFileParser // Or with retired: *RETIRED* interface [GrainInterfaceType("x")] Namespace.IInterface [Version(N)] // The name can include generic type parameters like IMyGrain or IMyGrain private static readonly Regex InterfacePattern = new( - @"^(?\*RETIRED\*\s*)?(?:interface\s+)?(\[GrainInterfaceType\(""(?[^""]+)""\)\]\s*)?(\[Alias\(""(?[^""]+)""\)\]\s*)?(?[\w.]+(?:<[\w,\s]+>)?)\s*\[Version\((?\d+)\)\]$", + @"^(?\*RETIRED\*\s*)?(?:interface\s+)?(\[GrainInterfaceType\(""(?[^""]+)""\)\]\s*)?(\[Alias\(""(?[^""]+)""\)\]\s*)?(?[\w]+(?:<[\w,\s]+>)?(?:\.[\w]+(?:<[\w,\s]+>)?)*)\s*\[Version\((?\d+)\)\]$", RegexOptions.Compiled); // Grain class line: class [GrainType("x")] Namespace.GrainClass // Or with retired: *RETIRED* class [GrainType("x")] Namespace.GrainClass private static readonly Regex GrainClassPattern = new( - @"^(?\*RETIRED\*\s*)?class\s+(\[(?:GrainType|Alias)\(""(?[^""]+)""\)\]\s*)?(?[\w.]+(?:<[\w,\s]+>)?)$", + @"^(?\*RETIRED\*\s*)?class\s+(\[(?:GrainType|Alias)\(""(?[^""]+)""\)\]\s*)?(?[\w]+(?:<[\w,\s]+>)?(?:\.[\w]+(?:<[\w,\s]+>)?)*)$", RegexOptions.Compiled); // Member line: [Alias("x")] Namespace.IInterface.Method(params) -> ReturnType @@ -979,6 +1101,19 @@ internal static bool TryGetGrainClassName(string line, out string name) return false; } + internal static bool TryGetGrainClassType(string line, out string grainType) + { + var match = GrainClassPattern.Match(StripClrComment(line)); + if (match.Success && match.Groups["alias"].Success) + { + grainType = match.Groups["alias"].Value; + return true; + } + + grainType = string.Empty; + return false; + } + internal static bool TryGetContractName(string line, out string name) => TryGetGrainClassName(line, out name) || TryGetInterfaceName(line, out name); @@ -1032,8 +1167,12 @@ public static (GrainInterfaceData Data, List? Errors) Parse(SourceTe currentInterface = null; var name = grainClassMatch.Groups["name"].Value; var alias = grainClassMatch.Groups["alias"].Success ? grainClassMatch.Groups["alias"].Value : null; - if (data.Classes.ContainsKey(name) - || alias is not null && data.Classes.Values.Any(candidate => string.Equals(candidate.Alias, alias, StringComparison.Ordinal))) + var identity = alias ?? GrainInterfaceVersionAnalyzer.GetDefaultGrainType(name); + if (data.Classes.Any(candidate => + string.Equals( + candidate.Alias ?? GrainInterfaceVersionAnalyzer.GetDefaultGrainType(candidate.Name), + identity, + StringComparison.Ordinal))) { errors ??= new List(); var location = Location.Create( @@ -1044,12 +1183,12 @@ public static (GrainInterfaceData Data, List? Errors) Parse(SourceTe continue; } - data.Classes[name] = new DeclaredGrainClass(name) + data.Classes.Add(new DeclaredGrainClass(name) { Alias = alias, IsRetired = grainClassMatch.Groups["retired"].Success, Span = textLine.Span - }; + }); continue; } @@ -1067,9 +1206,13 @@ public static (GrainInterfaceData Data, List? Errors) Parse(SourceTe } var isRetired = interfaceMatch.Groups["retired"].Success; - if (data.Interfaces.ContainsKey(name) - || grainInterfaceType is not null - && data.Interfaces.Values.Any(candidate => string.Equals(candidate.GrainInterfaceType, grainInterfaceType, StringComparison.Ordinal))) + var identity = grainInterfaceType ?? GrainInterfaceVersionAnalyzer.GetDefaultGrainInterfaceType(name); + if (data.Interfaces.Any(candidate => + string.Equals( + candidate.GrainInterfaceType + ?? GrainInterfaceVersionAnalyzer.GetDefaultGrainInterfaceType(candidate.Name), + identity, + StringComparison.Ordinal))) { // Duplicate declaration errors ??= new List(); @@ -1092,7 +1235,7 @@ public static (GrainInterfaceData Data, List? Errors) Parse(SourceTe IsRetired = isRetired, Span = textLine.Span }; - data.Interfaces[name] = currentInterface; + data.Interfaces.Add(currentInterface); continue; } diff --git a/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs b/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs index 4c6203480f2..5783b603549 100644 --- a/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs +++ b/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs @@ -24,11 +24,14 @@ namespace Orleans.Analyzers; public class GrainInterfaceVersionCodeFix : CodeFixProvider { private const string DefaultNewLine = "\n"; + private const string RegenerateCodeActionTitle = "Regenerate OrleansContracts.txt"; + private const string RegenerateCodeActionEquivalenceKey = nameof(RegenerateOrleansContractsFileAsync); private static readonly string[] GeneratedHeader = [ - "# This file is auto-generated by the Orleans contract analyzer.", - "# Update source contracts, then regenerate this file by following:", - "# https://aka.ms/orleans/OrleansContracts.txt" + "# This file is generated by the Orleans contract analyzer.", + "# Regenerate it by applying \"Regenerate OrleansContracts.txt\" at project or solution scope.", + "# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades.", + "# Details: https://aka.ms/orleans/OrleansContracts.txt" ]; public sealed override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create( @@ -36,15 +39,22 @@ public class GrainInterfaceVersionCodeFix : CodeFixProvider GrainInterfaceVersionAnalyzer.RuleId0017, // Version mismatch GrainInterfaceVersionAnalyzer.RuleId0018, // Member not declared GrainInterfaceVersionAnalyzer.RuleId0019, // Removed interface not retired + GrainInterfaceVersionAnalyzer.RuleId0020, // Contracts file missing GrainInterfaceVersionAnalyzer.RuleId0022, // Grain class not declared GrainInterfaceVersionAnalyzer.RuleId0023, // Grain class alias mismatch GrainInterfaceVersionAnalyzer.RuleId0024); // Removed grain class not retired - // Note: We don't use BatchFixer because each fix may need to coordinate updates to the same file - public sealed override FixAllProvider? GetFixAllProvider() => null; + public sealed override FixAllProvider GetFixAllProvider() => OrleansContractsFixAllProvider.Instance; public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) { + context.RegisterCodeFix( + CodeAction.Create( + title: RegenerateCodeActionTitle, + createChangedSolution: cancellationToken => RegenerateOrleansContractsFileAsync(context.Document.Project, cancellationToken), + equivalenceKey: RegenerateCodeActionEquivalenceKey), + context.Diagnostics); + foreach (var diagnostic in context.Diagnostics) { switch (diagnostic.Id) @@ -76,6 +86,432 @@ public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) return Task.CompletedTask; } + private static async Task RegenerateOrleansContractsFileAsync( + Project project, + CancellationToken cancellationToken) + { + var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false); + if (compilation is null) + { + return project.Solution; + } + + var contractsFile = FindContractsDocument(project); + var existingText = contractsFile is null + ? null + : await contractsFile.GetTextAsync(cancellationToken).ConfigureAwait(false); + var newLine = existingText is null ? DefaultNewLine : GetNewLine(existingText); + var lines = new List(); + var activeInterfaceIdentities = new HashSet(StringComparer.Ordinal); + var activeConventionInterfaceNames = new HashSet(StringComparer.Ordinal); + var activeClassIdentities = new HashSet(StringComparer.Ordinal); + var activeConventionClassNames = new HashSet(StringComparer.Ordinal); + var iAddressableType = compilation.GetTypeByMetadataName(Constants.IAddressibleFullyQualifiedName); + var generatedCodeTrees = new Dictionary(); + + foreach (var type in GetAllSourceTypes(compilation.Assembly.GlobalNamespace, generatedCodeTrees) + .OrderBy(GetFullyQualifiedName, StringComparer.Ordinal)) + { + if (type.TypeKind == TypeKind.Interface + && iAddressableType is not null + && !SymbolEqualityComparer.Default.Equals(type, iAddressableType) + && type.AllInterfaces.Any(candidate => SymbolEqualityComparer.Default.Equals(candidate, iAddressableType))) + { + AppendInterface(lines, type, activeInterfaceIdentities, activeConventionInterfaceNames); + } + else if (type.TypeKind == TypeKind.Class && !type.IsAbstract && type.IsGrainClass()) + { + AppendGrainClass(lines, type, activeClassIdentities, activeConventionClassNames); + } + } + + if (existingText is not null) + { + AppendHistoricalContracts( + lines, + existingText.ToString(), + activeInterfaceIdentities, + activeConventionInterfaceNames, + activeClassIdentities, + activeConventionClassNames); + } + + var content = SortContractEntries(string.Join(newLine, lines), newLine); + var newText = SourceText.From(content, Encoding.UTF8); + if (contractsFile is not null) + { + return project.Solution.WithAdditionalDocumentText(contractsFile.Id, newText); + } + + var filePath = GetConfiguredContractsPath(project); + return project.Solution.AddAdditionalDocument( + DocumentId.CreateNewId(project.Id), + Path.GetFileName(filePath), + newText, + filePath: filePath); + } + + private static TextDocument? FindContractsDocument(Project project) + { + foreach (var additionalFile in project.AnalyzerOptions.AdditionalFiles) + { + if (!Path.GetFileName(additionalFile.Path) + .Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase) + && (!project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GetOptions(additionalFile) + .TryGetValue("build_metadata.AdditionalFiles.OrleansContractsFile", out var value) + || !string.Equals(value, "true", StringComparison.OrdinalIgnoreCase))) + { + continue; + } + + var document = project.AdditionalDocuments.FirstOrDefault(candidate => + string.Equals(candidate.FilePath, additionalFile.Path, StringComparison.OrdinalIgnoreCase)); + if (document is not null) + { + return document; + } + } + + return project.AdditionalDocuments.FirstOrDefault(document => + Path.GetFileName(document.FilePath ?? document.Name) + .Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + } + + private static string GetConfiguredContractsPath(Project project) + { + if (project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue( + "build_property.OrleansContractsPath", + out var configuredPath) + && !string.IsNullOrWhiteSpace(configuredPath)) + { + configuredPath = NormalizePathSeparators(configuredPath); + if (Path.IsPathRooted(configuredPath)) + { + return configuredPath; + } + + if (Path.GetDirectoryName(project.FilePath) is { } projectDirectory) + { + return Path.Combine(projectDirectory, configuredPath); + } + + return configuredPath; + } + + return Path.GetDirectoryName(project.FilePath) is { } directory + ? Path.Combine(directory, Constants.OrleansContractsFileName) + : Constants.OrleansContractsFileName; + } + + private static string NormalizePathSeparators(string path) + => path.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar); + + private static IEnumerable GetAllSourceTypes( + INamespaceSymbol @namespace, + Dictionary generatedCodeTrees) + { + foreach (var type in @namespace.GetTypeMembers()) + { + foreach (var result in GetSourceTypeAndNestedTypes(type, generatedCodeTrees)) + { + yield return result; + } + } + + foreach (var childNamespace in @namespace.GetNamespaceMembers()) + { + foreach (var result in GetAllSourceTypes(childNamespace, generatedCodeTrees)) + { + yield return result; + } + } + } + + private static IEnumerable GetSourceTypeAndNestedTypes( + INamedTypeSymbol type, + Dictionary generatedCodeTrees) + { + if (!type.IsImplicitlyDeclared + && !IsGeneratedCode(type, generatedCodeTrees) + && type.Locations.Any(location => location.IsInSource)) + { + yield return type; + } + + foreach (var nestedType in type.GetTypeMembers()) + { + foreach (var result in GetSourceTypeAndNestedTypes(nestedType, generatedCodeTrees)) + { + yield return result; + } + } + } + + private static bool IsGeneratedCode( + INamedTypeSymbol type, + Dictionary generatedCodeTrees) + { + for (ISymbol? symbol = type; symbol is not null; symbol = symbol.ContainingType) + { + if (symbol.GetAttributes().Any(attribute => + attribute.AttributeClass?.ToDisplayString() is + "System.CodeDom.Compiler.GeneratedCodeAttribute" + or "System.Runtime.CompilerServices.CompilerGeneratedAttribute")) + { + return true; + } + } + + return type.Locations + .Where(location => location.IsInSource && location.SourceTree is not null) + .Any(location => IsGeneratedCode(location.SourceTree!, generatedCodeTrees)); + } + + private static bool IsGeneratedCode( + SyntaxTree syntaxTree, + Dictionary generatedCodeTrees) + { + if (generatedCodeTrees.TryGetValue(syntaxTree, out var result)) + { + return result; + } + + var fileName = Path.GetFileName(syntaxTree.FilePath); + result = fileName.EndsWith(".g.cs", StringComparison.OrdinalIgnoreCase) + || fileName.EndsWith(".g.i.cs", StringComparison.OrdinalIgnoreCase) + || fileName.EndsWith(".generated.cs", StringComparison.OrdinalIgnoreCase) + || fileName.EndsWith(".designer.cs", StringComparison.OrdinalIgnoreCase); + if (!result) + { + var text = syntaxTree.GetText(); + var prefixLength = Math.Min(text.Length, 2048); + result = text.ToString(new TextSpan(0, prefixLength)) + .IndexOf("= 0; + } + + generatedCodeTrees[syntaxTree] = result; + return result; + } + + private static void AppendInterface( + List lines, + INamedTypeSymbol type, + HashSet activeInterfaceIdentities, + HashSet activeConventionInterfaceNames) + { + AppendBlockSeparator(lines); + var interfaceName = GetFullyQualifiedName(type); + var explicitInterfaceType = GetGrainInterfaceTypeFromAttributes(type); + var interfaceType = GrainInterfaceVersionAnalyzer.GetGrainInterfaceType(type); + activeInterfaceIdentities.Add(interfaceType); + if (explicitInterfaceType is null + || string.Equals( + explicitInterfaceType, + GrainInterfaceVersionAnalyzer.GetDefaultGrainInterfaceType(type), + StringComparison.Ordinal)) + { + activeConventionInterfaceNames.Add(interfaceName); + } + if (GrainInterfaceVersionAnalyzer.IdentityDiffersFromClrName(explicitInterfaceType, type)) + { + lines.Add($"# {interfaceName}"); + } + + lines.Add($"interface [GrainInterfaceType(\"{interfaceType}\")] {interfaceName} [Version({GetVersionFromAttributes(type)})]"); + foreach (var member in type.GetMembers() + .OfType() + .Where(member => member.MethodKind == MethodKind.Ordinary && !member.IsStatic) + .OrderBy(GrainInterfaceVersionAnalyzer.GetMethodSignature, StringComparer.Ordinal)) + { + if (GrainInterfaceVersionAnalyzer.RequiresClrComment(member)) + { + lines.Add($" # {GrainInterfaceVersionAnalyzer.GetClrMethodSignature(member)}"); + } + + lines.Add($" {GrainInterfaceVersionAnalyzer.GetMethodSignature(member)}"); + } + } + + private static void AppendGrainClass( + List lines, + INamedTypeSymbol type, + HashSet activeClassIdentities, + HashSet activeConventionClassNames) + { + AppendBlockSeparator(lines); + var className = GetFullyQualifiedName(type); + var explicitGrainType = GetGrainTypeFromAttributes(type); + var grainType = GrainInterfaceVersionAnalyzer.GetGrainType(type); + activeClassIdentities.Add(grainType); + if (explicitGrainType is null + || string.Equals( + explicitGrainType, + GrainInterfaceVersionAnalyzer.GetDefaultGrainType(type), + StringComparison.Ordinal)) + { + activeConventionClassNames.Add(className); + } + if (GrainInterfaceVersionAnalyzer.IdentityDiffersFromClrName(explicitGrainType, type)) + { + lines.Add($"# {className}"); + } + + lines.Add($"class [GrainType(\"{grainType}\")] {className}"); + } + + private static void AppendHistoricalContracts( + List result, + string existingContent, + HashSet activeInterfaceIdentities, + HashSet activeConventionInterfaceNames, + HashSet activeClassIdentities, + HashSet activeConventionClassNames) + { + var lines = existingContent.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + for (var index = 0; index < lines.Length; index++) + { + var declaration = lines[index].Trim(); + var isInterface = GrainInterfaceFileParser.TryGetInterfaceName(declaration, out var contractName); + var isClass = !isInterface && GrainInterfaceFileParser.TryGetGrainClassName(declaration, out contractName); + if (!isInterface && !isClass) + { + continue; + } + + string? explicitIdentity = null; + if (isInterface && GrainInterfaceFileParser.TryGetGrainInterfaceType(declaration, out var interfaceType)) + { + explicitIdentity = interfaceType; + } + else if (isClass && GrainInterfaceFileParser.TryGetGrainClassType(declaration, out var grainType)) + { + explicitIdentity = grainType; + } + + var isActive = isInterface + ? explicitIdentity is null + ? activeInterfaceIdentities.Contains( + GrainInterfaceVersionAnalyzer.GetDefaultGrainInterfaceType(contractName)) + || activeConventionInterfaceNames.Contains(contractName) + : activeInterfaceIdentities.Contains(explicitIdentity) + : explicitIdentity is null + ? activeClassIdentities.Contains(GrainInterfaceVersionAnalyzer.GetDefaultGrainType(contractName)) + || activeConventionClassNames.Contains(contractName) + : activeClassIdentities.Contains(explicitIdentity); + var blockEnd = index + 1; + while (blockEnd < lines.Length) + { + if (GrainInterfaceFileParser.TryGetContractName(lines[blockEnd], out _)) + { + break; + } + + if (lines[blockEnd].TrimStart().StartsWith("# ", StringComparison.Ordinal) + && blockEnd + 1 < lines.Length + && GrainInterfaceFileParser.TryGetContractName(lines[blockEnd + 1], out _)) + { + break; + } + + blockEnd++; + } + + if (!isActive) + { + AppendBlockSeparator(result); + if (index > 0 + && lines[index - 1].TrimStart().StartsWith("# ", StringComparison.Ordinal) + && !IsGeneratedHeaderLine(lines[index - 1].TrimStart())) + { + result.Add(lines[index - 1].Trim()); + } + + for (var blockIndex = index; blockIndex < blockEnd; blockIndex++) + { + result.Add(blockIndex == index + && !declaration.StartsWith(GrainInterfaceVersionAnalyzer.RetiredPrefix, StringComparison.Ordinal) + ? $"{GrainInterfaceVersionAnalyzer.RetiredPrefix} {declaration}" + : lines[blockIndex]); + } + } + + index = blockEnd - 1; + } + } + + private static void AppendBlockSeparator(List lines) + { + while (lines.Count > 0 && string.IsNullOrWhiteSpace(lines[lines.Count - 1])) + { + lines.RemoveAt(lines.Count - 1); + } + + if (lines.Count > 0) + { + lines.Add(string.Empty); + } + } + + private static string GetFullyQualifiedName(INamedTypeSymbol type) + => type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", ""); + + private sealed class OrleansContractsFixAllProvider : FixAllProvider + { + public static OrleansContractsFixAllProvider Instance { get; } = new(); + + public override Task GetFixAsync(FixAllContext fixAllContext) + { + if (!string.Equals( + fixAllContext.CodeActionEquivalenceKey, + RegenerateCodeActionEquivalenceKey, + StringComparison.Ordinal)) + { + return Task.FromResult(null); + } + + var title = fixAllContext.Scope == FixAllScope.Solution + ? "Regenerate OrleansContracts.txt in solution" + : "Regenerate OrleansContracts.txt in project"; + return Task.FromResult(CodeAction.Create( + title, + cancellationToken => RegenerateFixAllAsync(fixAllContext, cancellationToken), + RegenerateCodeActionEquivalenceKey)); + } + + private static async Task RegenerateFixAllAsync( + FixAllContext fixAllContext, + CancellationToken cancellationToken) + { + var solution = fixAllContext.Solution; + var projectIds = new List(); + if (fixAllContext.Scope == FixAllScope.Solution) + { + foreach (var project in solution.Projects.Where(project => project.Language == LanguageNames.CSharp)) + { + if (!(await fixAllContext.GetAllDiagnosticsAsync(project).ConfigureAwait(false)).IsEmpty) + { + projectIds.Add(project.Id); + } + } + } + else + { + projectIds.Add(fixAllContext.Project.Id); + } + + foreach (var projectId in projectIds) + { + cancellationToken.ThrowIfCancellationRequested(); + if (solution.GetProject(projectId) is { } project) + { + solution = await RegenerateOrleansContractsFileAsync(project, cancellationToken).ConfigureAwait(false); + } + } + + return solution; + } + } + private static void RegisterAddInterfaceCodeFix(CodeFixContext context, Diagnostic diagnostic) { if (!diagnostic.Properties.TryGetValue(GrainInterfaceVersionAnalyzer.InterfaceNamePropertyKey, out var interfaceName) || @@ -149,10 +585,11 @@ private static void RegisterRetireInterfaceCodeFix(CodeFixContext context, Diagn return; } + diagnostic.Properties.TryGetValue(GrainInterfaceVersionAnalyzer.GrainInterfaceTypePropertyKey, out var grainInterfaceType); context.RegisterCodeFix( CodeAction.Create( title: Resources.RetireGrainInterfaceTitle, - createChangedSolution: ct => RetireInterfaceInFileAsync(context.Document, interfaceName!, ct), + createChangedSolution: ct => RetireInterfaceInFileAsync(context.Document, interfaceName!, grainInterfaceType, ct), equivalenceKey: GrainInterfaceVersionAnalyzer.RuleId0019), diagnostic); } @@ -198,10 +635,11 @@ private static void RegisterRetireGrainClassCodeFix(CodeFixContext context, Diag return; } + diagnostic.Properties.TryGetValue(GrainInterfaceVersionAnalyzer.ActualAliasPropertyKey, out var grainType); context.RegisterCodeFix( CodeAction.Create( title: Resources.RetireGrainClassTitle, - createChangedSolution: ct => RetireGrainClassInFileAsync(context.Document, className!, ct), + createChangedSolution: ct => RetireGrainClassInFileAsync(context.Document, className!, grainType, ct), equivalenceKey: GrainInterfaceVersionAnalyzer.RuleId0024), diagnostic); } @@ -237,8 +675,7 @@ private static async Task AddGrainClassToFileAsync( var grainType = GrainInterfaceVersionAnalyzer.GetGrainType(classSymbol); var classClrComment = GrainInterfaceVersionAnalyzer.IdentityDiffersFromClrName(explicitGrainType, classSymbol) ? className : null; var classLine = $"class [GrainType(\"{grainType}\")] {className}"; - var contractsFile = project.AdditionalDocuments - .FirstOrDefault(d => Path.GetFileName(d.FilePath ?? d.Name).Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + var contractsFile = FindContractsDocument(project); if (contractsFile is null) { return solution; @@ -290,8 +727,7 @@ private static async Task UpdateGrainClassAliasInFileAsync( CancellationToken cancellationToken) { var project = document.Project; - var contractsFile = project.AdditionalDocuments - .FirstOrDefault(d => Path.GetFileName(d.FilePath ?? d.Name).Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + var contractsFile = FindContractsDocument(project); if (contractsFile is null) { return project.Solution; @@ -329,11 +765,11 @@ private static async Task UpdateGrainClassAliasInFileAsync( private static async Task RetireGrainClassInFileAsync( Document document, string className, + string? grainType, CancellationToken cancellationToken) { var project = document.Project; - var contractsFile = project.AdditionalDocuments - .FirstOrDefault(d => Path.GetFileName(d.FilePath ?? d.Name).Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + var contractsFile = FindContractsDocument(project); if (contractsFile is null) { return project.Solution; @@ -350,8 +786,7 @@ private static async Task RetireGrainClassInFileAsync( for (var i = 0; i < lines.Length; i++) { var trimmedLine = lines[i].Trim(); - if (GrainInterfaceFileParser.TryGetGrainClassName(trimmedLine, out var declaredName) - && string.Equals(declaredName, className, StringComparison.Ordinal) + if (IsMatchingGrainClass(trimmedLine, className, grainType) && !trimmedLine.StartsWith(GrainInterfaceVersionAnalyzer.RetiredPrefix, StringComparison.Ordinal)) { lines[i] = $"{GrainInterfaceVersionAnalyzer.RetiredPrefix} {trimmedLine}"; @@ -440,8 +875,7 @@ private static async Task AddInterfaceToFileAsync( } // Find or create the OrleansContracts.txt file - var grainInterfacesFile = project.AdditionalDocuments - .FirstOrDefault(d => Path.GetFileName(d.FilePath ?? d.Name).Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + var grainInterfacesFile = FindContractsDocument(project); if (grainInterfacesFile is not null) { @@ -504,14 +938,11 @@ private static async Task AddInterfaceToFileAsync( } var newText = Microsoft.CodeAnalysis.Text.SourceText.From(SortContractEntries(content.ToString(), DefaultNewLine), Encoding.UTF8); - var projectDir = Path.GetDirectoryName(project.FilePath); - var filePath = projectDir is not null - ? Path.Combine(projectDir, Constants.OrleansContractsFileName) - : Constants.OrleansContractsFileName; + var filePath = GetConfiguredContractsPath(project); solution = solution.AddAdditionalDocument( DocumentId.CreateNewId(project.Id), - Constants.OrleansContractsFileName, + Path.GetFileName(filePath), newText, filePath: filePath); } @@ -529,8 +960,7 @@ private static async Task UpdateVersionInFileAsync( var project = document.Project; var solution = project.Solution; - var grainInterfacesFile = project.AdditionalDocuments - .FirstOrDefault(d => Path.GetFileName(d.FilePath ?? d.Name).Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + var grainInterfacesFile = FindContractsDocument(project); if (grainInterfacesFile is null) { @@ -593,8 +1023,7 @@ private static async Task AddMemberToFileAsync( var project = document.Project; var solution = project.Solution; - var grainInterfacesFile = project.AdditionalDocuments - .FirstOrDefault(d => Path.GetFileName(d.FilePath ?? d.Name).Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + var grainInterfacesFile = FindContractsDocument(project); if (grainInterfacesFile is null) { @@ -658,13 +1087,13 @@ private static async Task AddMemberToFileAsync( private static async Task RetireInterfaceInFileAsync( Document document, string interfaceName, + string? grainInterfaceType, CancellationToken cancellationToken) { var project = document.Project; var solution = project.Solution; - var grainInterfacesFile = project.AdditionalDocuments - .FirstOrDefault(d => Path.GetFileName(d.FilePath ?? d.Name).Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); + var grainInterfacesFile = FindContractsDocument(project); if (grainInterfacesFile is null) { @@ -686,8 +1115,7 @@ private static async Task RetireInterfaceInFileAsync( var trimmedLine = line.Trim(); // Check if this line contains the interface declaration - if (GrainInterfaceFileParser.TryGetInterfaceName(trimmedLine, out var declaredInterfaceName) - && string.Equals(declaredInterfaceName, interfaceName, StringComparison.Ordinal) && + if (IsMatchingInterface(trimmedLine, interfaceName, grainInterfaceType) && !trimmedLine.StartsWith(GrainInterfaceVersionAnalyzer.RetiredPrefix, StringComparison.Ordinal)) { // Add *RETIRED* prefix @@ -734,12 +1162,30 @@ private static bool IsMatchingInterface(string line, string interfaceName, strin { return GrainInterfaceFileParser.TryGetGrainInterfaceType(line, out var declaredGrainInterfaceType) ? string.Equals(declaredGrainInterfaceType, grainInterfaceType, StringComparison.Ordinal) - : string.Equals(declaredInterfaceName, interfaceName, StringComparison.Ordinal); + : string.Equals( + grainInterfaceType, + GrainInterfaceVersionAnalyzer.GetDefaultGrainInterfaceType(interfaceName), + StringComparison.Ordinal) + && string.Equals(declaredInterfaceName, interfaceName, StringComparison.Ordinal); } return string.Equals(declaredInterfaceName, interfaceName, StringComparison.Ordinal); } + private static bool IsMatchingGrainClass(string line, string className, string? grainType) + { + if (!GrainInterfaceFileParser.TryGetGrainClassName(line, out var declaredClassName) + || !string.Equals(declaredClassName, className, StringComparison.Ordinal)) + { + return false; + } + + return grainType is null + ? !GrainInterfaceFileParser.TryGetGrainClassType(line, out _) + : GrainInterfaceFileParser.TryGetGrainClassType(line, out var declaredGrainType) + && string.Equals(declaredGrainType, grainType, StringComparison.Ordinal); + } + private static bool IdentityDiffersFromClrName(string? identity, string fullName) { if (identity is null) @@ -893,6 +1339,9 @@ private static string SortContractEntries(string content, string newLine) private static bool IsGeneratedHeaderLine(string line) => GeneratedHeader.Contains(line, StringComparer.Ordinal) || line is "# OrleansContracts.txt" + or "# This file is auto-generated by the Orleans contract analyzer." + or "# Update source contracts, then regenerate this file by following:" + or "# https://aka.ms/orleans/OrleansContracts.txt" or "# This file tracks grain interface versions for compatibility during rolling upgrades." or "# Format:" or "# # Namespace.GrainClass" diff --git a/src/Orleans.Analyzers/Resources.resx b/src/Orleans.Analyzers/Resources.resx index 4cbbf0c2f81..0dd048a118d 100644 --- a/src/Orleans.Analyzers/Resources.resx +++ b/src/Orleans.Analyzers/Resources.resx @@ -218,7 +218,7 @@ Grain interface is not active in OrleansContracts.txt - Grain interface '{0}' does not have an active declaration in OrleansContracts.txt + Grain interface '{0}' does not have an active declaration in OrleansContracts.txt. Apply 'Regenerate OrleansContracts.txt' at project or solution scope, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. All grain interfaces should have an active declaration in OrleansContracts.txt to ensure version compatibility during rolling upgrades. @@ -227,7 +227,7 @@ Grain interface version mismatch - Grain interface '{0}' has [Version({1})] in OrleansContracts.txt but [Version({2})] in code + Grain interface '{0}' has [Version({1})] in OrleansContracts.txt but [Version({2})] in code. Apply 'Regenerate OrleansContracts.txt' at project or solution scope, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. The [Version] attribute on the grain interface must match the version declared in OrleansContracts.txt. @@ -236,7 +236,7 @@ Grain interface member not declared in OrleansContracts.txt - Grain interface member '{0}' is not declared in OrleansContracts.txt for interface '{1}' + Grain interface member '{0}' is not declared in OrleansContracts.txt for interface '{1}'. Apply 'Regenerate OrleansContracts.txt' at project or solution scope, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. When adding or modifying grain interface members, update OrleansContracts.txt and increment the interface version. @@ -245,7 +245,7 @@ Removed grain interface not marked as retired - Grain interface '{0}' is declared in OrleansContracts.txt but no longer exists in code - mark it as *RETIRED* + Grain interface '{0}' is declared in OrleansContracts.txt but no longer exists in code. Apply 'Regenerate OrleansContracts.txt' at project or solution scope to preserve it as *RETIRED*, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. When removing a grain interface, mark it as *RETIRED* in OrleansContracts.txt to document that it has been intentionally removed. @@ -254,7 +254,7 @@ OrleansContracts.txt file is missing - The project contains grain interfaces but no {0} file to track them + The project contains Orleans contracts but no {0} file. Apply 'Regenerate OrleansContracts.txt' at project or solution scope to create it, then review the generated baseline. See https://aka.ms/orleans/OrleansContracts.txt for details. Add an OrleansContracts.txt file to track Orleans contracts for compatibility during rolling upgrades. @@ -281,7 +281,7 @@ Grain class is not active in OrleansContracts.txt - Grain class '{0}' does not have an active declaration in OrleansContracts.txt + Grain class '{0}' does not have an active declaration in OrleansContracts.txt. Apply 'Regenerate OrleansContracts.txt' at project or solution scope, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. All concrete grain classes should have an active declaration in OrleansContracts.txt to protect grain type identities during renames. @@ -290,7 +290,7 @@ Grain class alias mismatch - Grain class '{0}' has alias '{1}' in OrleansContracts.txt but alias '{2}' in code + Grain class '{0}' has alias '{1}' in OrleansContracts.txt but alias '{2}' in code. Apply 'Regenerate OrleansContracts.txt' at project or solution scope, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. The [GrainType] attribute on a grain class must match the alias declared in OrleansContracts.txt. @@ -299,7 +299,7 @@ Removed grain class not marked as retired - Grain class '{0}' is declared in OrleansContracts.txt but no longer exists in code - mark it as *RETIRED* + Grain class '{0}' is declared in OrleansContracts.txt but no longer exists in code. Apply 'Regenerate OrleansContracts.txt' at project or solution scope to preserve it as *RETIRED*, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. When removing a grain class, mark it as *RETIRED* in OrleansContracts.txt to preserve its contract history. diff --git a/src/Orleans.Analyzers/build/Microsoft.Orleans.Analyzers.props b/src/Orleans.Analyzers/build/Microsoft.Orleans.Analyzers.props index 7d4869b7bd2..bb67bff9c88 100644 --- a/src/Orleans.Analyzers/build/Microsoft.Orleans.Analyzers.props +++ b/src/Orleans.Analyzers/build/Microsoft.Orleans.Analyzers.props @@ -5,5 +5,6 @@ + diff --git a/src/Orleans.BroadcastChannel/OrleansContracts.txt b/src/Orleans.BroadcastChannel/OrleansContracts.txt index b3edafef674..8bfabca4a1d 100644 --- a/src/Orleans.BroadcastChannel/OrleansContracts.txt +++ b/src/Orleans.BroadcastChannel/OrleansContracts.txt @@ -1,6 +1,7 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.BroadcastChannel.IBroadcastChannelConsumerExtension")] Orleans.BroadcastChannel.IBroadcastChannelConsumerExtension [Version(0)] OnError(Orleans.BroadcastChannel.InternalChannelId, System.Exception) -> Task diff --git a/src/Orleans.Core.Abstractions/OrleansContracts.txt b/src/Orleans.Core.Abstractions/OrleansContracts.txt index be4406a54a9..f2b8c4d77b7 100644 --- a/src/Orleans.Core.Abstractions/OrleansContracts.txt +++ b/src/Orleans.Core.Abstractions/OrleansContracts.txt @@ -1,6 +1,7 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.Core.Internal.IGrainManagementExtension")] Orleans.Core.Internal.IGrainManagementExtension [Version(0)] DeactivateOnIdle() -> ValueTask diff --git a/src/Orleans.Core/OrleansContracts.txt b/src/Orleans.Core/OrleansContracts.txt index f90b29fe0a1..495d4ac9f5c 100644 --- a/src/Orleans.Core/OrleansContracts.txt +++ b/src/Orleans.Core/OrleansContracts.txt @@ -1,6 +1,7 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.ClientObservers.IClientGatewayObserver")] Orleans.ClientObservers.IClientGatewayObserver [Version(0)] StopSendingToGateway(Orleans.Runtime.SiloAddress) -> void diff --git a/src/Orleans.DurableJobs/OrleansContracts.txt b/src/Orleans.DurableJobs/OrleansContracts.txt index 756611fbcb2..5116bf1e6f6 100644 --- a/src/Orleans.DurableJobs/OrleansContracts.txt +++ b/src/Orleans.DurableJobs/OrleansContracts.txt @@ -1,6 +1,7 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.DurableJobs.IDurableJobReceiverExtension")] Orleans.DurableJobs.IDurableJobReceiverExtension [Version(0)] HandleDurableJobAsync(Orleans.DurableJobs.IJobRunContext, System.Threading.CancellationToken) -> ValueTask diff --git a/src/Orleans.EventSourcing/OrleansContracts.txt b/src/Orleans.EventSourcing/OrleansContracts.txt index c3cd2d29887..caac3e3d249 100644 --- a/src/Orleans.EventSourcing/OrleansContracts.txt +++ b/src/Orleans.EventSourcing/OrleansContracts.txt @@ -1,6 +1,7 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.EventSourcing.ILogConsistencyProtocolParticipant")] Orleans.EventSourcing.ILogConsistencyProtocolParticipant [Version(0)] DeactivateProtocolParticipant() -> Task diff --git a/src/Orleans.Persistence.Memory/OrleansContracts.txt b/src/Orleans.Persistence.Memory/OrleansContracts.txt index bb17fc7aff6..7595d0d02a9 100644 --- a/src/Orleans.Persistence.Memory/OrleansContracts.txt +++ b/src/Orleans.Persistence.Memory/OrleansContracts.txt @@ -1,5 +1,6 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt class [GrainType("memorystorage")] Orleans.Storage.MemoryStorageGrain diff --git a/src/Orleans.Reminders/OrleansContracts.txt b/src/Orleans.Reminders/OrleansContracts.txt index 2db089842d6..fe3eb45d3b1 100644 --- a/src/Orleans.Reminders/OrleansContracts.txt +++ b/src/Orleans.Reminders/OrleansContracts.txt @@ -1,6 +1,7 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.IRemindable")] Orleans.IRemindable [Version(0)] ReceiveReminder(string, Orleans.Runtime.TickStatus) -> Task diff --git a/src/Orleans.Runtime/OrleansContracts.txt b/src/Orleans.Runtime/OrleansContracts.txt index 75bdb2c2fe4..77b8a9425c0 100644 --- a/src/Orleans.Runtime/OrleansContracts.txt +++ b/src/Orleans.Runtime/OrleansContracts.txt @@ -1,6 +1,7 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt class [GrainType("activationmigrationmanager")] Orleans.Runtime.ActivationMigrationManager diff --git a/src/Orleans.Streaming/OrleansContracts.txt b/src/Orleans.Streaming/OrleansContracts.txt index de3f623bf05..4c25a8903c3 100644 --- a/src/Orleans.Streaming/OrleansContracts.txt +++ b/src/Orleans.Streaming/OrleansContracts.txt @@ -1,6 +1,7 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.Providers.IMemoryStreamQueueGrain")] Orleans.Providers.IMemoryStreamQueueGrain [Version(0)] Dequeue(int) -> Task> diff --git a/src/Orleans.TestingHost/OrleansContracts.txt b/src/Orleans.TestingHost/OrleansContracts.txt index eb810b77708..e755b9e9870 100644 --- a/src/Orleans.TestingHost/OrleansContracts.txt +++ b/src/Orleans.TestingHost/OrleansContracts.txt @@ -1,6 +1,7 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.TestingHost.IStorageFaultGrain")] Orleans.TestingHost.IStorageFaultGrain [Version(0)] AddFaultOnClear(Orleans.Runtime.GrainId, System.Exception) -> Task diff --git a/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt b/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt index cb8e54cb075..73ed8eecb8b 100644 --- a/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt +++ b/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt @@ -1,6 +1,7 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt class [GrainType("consistencytest")] Orleans.Transactions.TestKit.Consistency.ConsistencyTestGrain diff --git a/src/Orleans.Transactions/OrleansContracts.txt b/src/Orleans.Transactions/OrleansContracts.txt index 6521d3b3a11..5ffb83a37d5 100644 --- a/src/Orleans.Transactions/OrleansContracts.txt +++ b/src/Orleans.Transactions/OrleansContracts.txt @@ -1,6 +1,7 @@ -# This file is auto-generated by the Orleans contract analyzer. -# Update source contracts, then regenerate this file by following: -# https://aka.ms/orleans/OrleansContracts.txt +# This file is generated by the Orleans contract analyzer. +# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. +# Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.Transactions.Abstractions.ITransactionManagerExtension")] Orleans.Transactions.Abstractions.ITransactionManagerExtension [Version(0)] Ping(string, System.Guid, System.DateTime, Orleans.Transactions.ParticipantId) -> Task diff --git a/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs b/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs index 9189e7b03fa..fed281bf8dd 100644 --- a/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs +++ b/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs @@ -23,10 +23,13 @@ namespace Analyzers.Tests; public class GrainInterfaceVersionAnalyzerTest { private const string OrleansContractsFileName = "OrleansContracts.txt"; + private const string RegenerateCodeActionTitle = "Regenerate OrleansContracts.txt"; + private const string RegenerateCodeActionEquivalenceKey = "RegenerateOrleansContractsFileAsync"; private const string GeneratedHeader = - "# This file is auto-generated by the Orleans contract analyzer.\n" + - "# Update source contracts, then regenerate this file by following:\n" + - "# https://aka.ms/orleans/OrleansContracts.txt\n\n"; + "# This file is generated by the Orleans contract analyzer.\n" + + "# Regenerate it by applying \"Regenerate OrleansContracts.txt\" at project or solution scope.\n" + + "# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades.\n" + + "# Details: https://aka.ms/orleans/OrleansContracts.txt\n\n"; private static readonly string[] Usings = new[] { @@ -576,6 +579,9 @@ public interface IMyGrain : IGrain var diagnostic = Assert.Single(diagnostics, d => d.Id == GrainInterfaceVersionAnalyzer.RuleId0020); Assert.Contains(OrleansContractsFileName, diagnostic.GetMessage()); + Assert.True(diagnostic.Location.IsInSource); + Assert.Contains(RegenerateCodeActionTitle, diagnostic.GetMessage()); + Assert.Contains("https://aka.ms/orleans/OrleansContracts.txt", diagnostic.GetMessage()); } [Fact] @@ -1017,9 +1023,16 @@ public class MyGrain : Grain, IGrainWithStringKey private async Task<(Solution ChangedSolution, DocumentId? AdditionalDocumentId)> ApplyCodeFixAsync( string source, string? grainInterfacesFileContent, - string expectedDiagnosticId) + string expectedDiagnosticId, + string? codeActionTitle = null, + string? configuredContractsPath = null, + string? generatedSource = null) { - var project = CreateProjectWithAdditionalFilesForCodeFix(source, grainInterfacesFileContent); + var project = CreateProjectWithAdditionalFilesForCodeFix( + source, + grainInterfacesFileContent, + configuredContractsPath, + generatedSource); var document = project.Documents.First(); var compilation = await project.GetCompilationAsync(); @@ -1058,7 +1071,11 @@ public class MyGrain : Grain, IGrainWithStringKey await codeFixer.RegisterCodeFixesAsync(context); Assert.NotEmpty(actions); - var operations = await actions.First().GetOperationsAsync(CancellationToken.None); + var action = codeActionTitle is null + ? actions.FirstOrDefault(candidate => !string.Equals(candidate.Title, RegenerateCodeActionTitle, StringComparison.Ordinal)) + ?? actions.First() + : actions.Single(candidate => string.Equals(candidate.Title, codeActionTitle, StringComparison.Ordinal)); + var operations = await action.GetOperationsAsync(CancellationToken.None); var changedSolution = operations.OfType().Single().ChangedSolution; var additionalDocumentId = changedSolution.GetProject(project.Id)?.AdditionalDocumentIds.FirstOrDefault(); @@ -1080,7 +1097,11 @@ private async Task ApplyCodeFixAndGetContractsAsync( return (await changedDocument!.GetTextAsync(TestContext.Current.CancellationToken)).ToString(); } - private static Project CreateProjectWithAdditionalFilesForCodeFix(string source, string? grainInterfacesFileContent) + private static Project CreateProjectWithAdditionalFilesForCodeFix( + string source, + string? grainInterfacesFileContent, + string? configuredContractsPath = null, + string? generatedSource = null) { const string fileName = "Test.cs"; @@ -1126,6 +1147,15 @@ private static Project CreateProjectWithAdditionalFilesForCodeFix(string source, .AddMetadataReferences(projectId, metadataReferences) .AddDocument(documentId, fileName, SourceText.From(fullSource)); + if (generatedSource is not null) + { + solution = solution.AddDocument( + DocumentId.CreateNewId(projectId, "Generated.g.cs"), + "Generated.g.cs", + SourceText.From(generatedSource), + filePath: "Generated.g.cs"); + } + // Add additional document if content is provided if (grainInterfacesFileContent is not null) { @@ -1133,12 +1163,359 @@ private static Project CreateProjectWithAdditionalFilesForCodeFix(string source, solution = solution.AddAdditionalDocument(additionalDocumentId, OrleansContractsFileName, SourceText.From(grainInterfacesFileContent)); } + if (configuredContractsPath is not null) + { + solution = solution.AddAnalyzerConfigDocument( + DocumentId.CreateNewId(projectId, ".globalconfig"), + ".globalconfig", + SourceText.From( + $"is_global = true{Environment.NewLine}" + + $"build_property.OrleansContractsPath = {configuredContractsPath}{Environment.NewLine}"), + filePath: Path.Combine(Path.GetDirectoryName(configuredContractsPath)!, ".globalconfig")); + } + return solution.GetProject(projectId)! .WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); } #endregion + #region Code Fix Tests - Regenerate + + [Fact] + public async Task CodeFix_RegenerateMissingFile_CreatesCompleteManifest() + { + const string source = @" +[Version(2)] +[GrainInterfaceType(""cart"")] +public interface ICartGrain : IGrain +{ + [Alias(""read"")] + Task GetAsync(int itemId); +} + +[GrainType(""cart"")] +public class CartGrain : Grain, IGrainWithStringKey +{ +} +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle); + + Assert.NotNull(additionalDocumentId); + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("interface [GrainInterfaceType(\"cart\")] ICartGrain [Version(2)]", content); + Assert.Contains(" read(int) -> Task", content); + Assert.Contains("class [GrainType(\"cart\")] CartGrain", content); + } + + [Fact] + public async Task CodeFix_RegenerateMissingFile_UsesConfiguredManifestPath() + { + var configuredPath = Path.Combine(Path.GetTempPath(), "contracts", "rpc-contracts.txt"); + var configuredPathWithAlternateSeparators = configuredPath.Replace('\\', '/'); + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + "public interface IMyGrain : IGrain { Task Ping(); }", + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle, + configuredPathWithAlternateSeparators); + + Assert.NotNull(additionalDocumentId); + Assert.Equal(configuredPath, changedSolution.GetAdditionalDocument(additionalDocumentId!)!.FilePath); + } + + [Fact] + public async Task CodeFix_RegenerateProject_PreservesContractHistory() + { + const string source = @" +public interface ICurrentGrain : IGrain +{ + Task ReadAsync(); +} +"; + const string contractsFile = @" +interface ILegacyGrain [Version(1)] + WriteAsync(int) -> Task + +*RETIRED* class [GrainType(""retired"")] RetiredGrain +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0016, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("interface [GrainInterfaceType(\"ICurrentGrain\")] ICurrentGrain [Version(0)]", content); + Assert.Contains(" ReadAsync() -> Task", content); + Assert.Contains("*RETIRED* interface ILegacyGrain [Version(1)]", content); + Assert.Contains(" WriteAsync(int) -> Task", content); + Assert.Contains("*RETIRED* class [GrainType(\"retired\")] RetiredGrain", content); + } + + [Fact] + public async Task CodeFix_RegenerateProject_DistinguishesClrNamesFromExplicitIdentities() + { + const string source = @" +[GrainInterfaceType(""current-id"")] +public interface LegacyName : IGrain +{ +} +"; + const string contractsFile = @" +interface [GrainInterfaceType(""LegacyName"")] OldContract [Version(0)] +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0016, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("interface [GrainInterfaceType(\"current-id\")] LegacyName [Version(0)]", content); + Assert.Contains( + "*RETIRED* interface [GrainInterfaceType(\"LegacyName\")] OldContract [Version(0)]", + content); + Assert.Empty(await GetDiagnosticsAsync(source, content)); + } + + [Fact] + public async Task CodeFix_RegenerateProject_PreservesLegacyIdentityWhenExplicitIdentityChanges() + { + const string source = @" +[GrainInterfaceType(""new-id"")] +public interface IMyGrain : IGrain +{ +} +"; + const string contractsFile = "interface IMyGrain [Version(0)]"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0016, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("interface [GrainInterfaceType(\"new-id\")] IMyGrain [Version(0)]", content); + Assert.Contains("*RETIRED* interface IMyGrain [Version(0)]", content); + Assert.Empty(await GetDiagnosticsAsync(source, content)); + } + + [Fact] + public async Task CodeFix_RegenerateProject_RecognizesLegacyIdentityAfterClrRename() + { + const string source = @" +[GrainInterfaceType(""OldName"")] +public interface NewName : IGrain +{ +} + +public interface ITriggerGrain : IGrain +{ +} +"; + const string contractsFile = "interface OldName [Version(0)]"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0016, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("interface [GrainInterfaceType(\"OldName\")] NewName [Version(0)]", content); + Assert.DoesNotContain("*RETIRED* interface OldName", content); + Assert.Empty(await GetDiagnosticsAsync(source, content)); + } + + [Fact] + public async Task CodeFix_RegenerateProject_ExcludesGeneratedGrainProxies() + { + const string source = @" +public interface IMyGrain : IGrain +{ + Task Ping(); +} +"; + const string generatedSource = @" +public sealed class GeneratedProxy : IMyGrain +{ + public System.Threading.Tasks.Task Ping() => System.Threading.Tasks.Task.CompletedTask; +} +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle, + generatedSource: generatedSource); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("interface [GrainInterfaceType(\"IMyGrain\")] IMyGrain [Version(0)]", content); + Assert.DoesNotContain("GeneratedProxy", content); + Assert.Empty(await GetDiagnosticsAsync(source, content)); + } + + [Fact] + public async Task CodeFix_RegenerateProject_RecognizesLegacyGrainTypeAfterClrRename() + { + const string source = @" +[GrainType(""old"")] +public class NewGrain : Grain, IGrainWithStringKey +{ +} + +public interface ITriggerGrain : IGrain +{ +} +"; + const string contractsFile = "class OldGrain"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0016, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("class [GrainType(\"old\")] NewGrain", content); + Assert.DoesNotContain("*RETIRED* class OldGrain", content); + Assert.Empty(await GetDiagnosticsAsync(source, content)); + } + + [Fact] + public async Task CodeFix_RegenerateProject_RoundTripsContractsNestedInGenericTypes() + { + const string source = @" +public class Outer +{ + public interface IInnerGrain : IGrain + { + Task Ping(T value); + } + + public class InnerGrain : Grain, IGrainWithStringKey + { + } +} +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains( + "interface [GrainInterfaceType(\"Outer`1+IInnerGrain\")] Outer.IInnerGrain [Version(0)]", + content); + Assert.Contains("class [GrainType(\"inner`1\")] Outer.InnerGrain", content); + Assert.Empty(await GetDiagnosticsAsync(source, content)); + } + + [Fact] + public async Task FixAll_RegenerateSolution_UpdatesEveryProjectWithDiagnostics() + { + var firstProject = CreateProjectWithAdditionalFilesForCodeFix( + "public interface IFirstGrain : IGrain { Task Ping(); }", + "# OrleansContracts.txt\n"); + var solution = firstProject.Solution; + var secondProjectId = ProjectId.CreateNewId("SecondProject"); + var secondDocumentId = DocumentId.CreateNewId(secondProjectId, "Second.cs"); + var secondContractsId = DocumentId.CreateNewId(secondProjectId, OrleansContractsFileName); + var secondSource = string.Join( + Environment.NewLine, + Usings.Select(@using => $"using {@using};").Append( + "public interface ISecondGrain : IGrain { Task Pong(); }")); + solution = solution + .AddProject(secondProjectId, "SecondProject", "SecondProject", LanguageNames.CSharp) + .AddMetadataReferences(secondProjectId, firstProject.MetadataReferences) + .WithProjectCompilationOptions(secondProjectId, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) + .AddDocument(secondDocumentId, "Second.cs", SourceText.From(secondSource)) + .AddAdditionalDocument(secondContractsId, OrleansContractsFileName, SourceText.From("# OrleansContracts.txt\n")); + + firstProject = solution.GetProject(firstProject.Id)!; + var diagnostics = new Dictionary> + { + [firstProject.Id] = new[] { CreateFixAllDiagnostic() }, + [secondProjectId] = new[] { CreateFixAllDiagnostic() } + }; + var codeFixer = new GrainInterfaceVersionCodeFix(); + var context = new FixAllContext( + firstProject.Documents.First(), + codeFixer, + FixAllScope.Solution, + RegenerateCodeActionEquivalenceKey, + codeFixer.FixableDiagnosticIds, + new TestFixAllDiagnosticProvider(diagnostics), + TestContext.Current.CancellationToken); + + var action = await codeFixer.GetFixAllProvider().GetFixAsync(context); + Assert.NotNull(action); + var operations = await action!.GetOperationsAsync(TestContext.Current.CancellationToken); + var changedSolution = operations.OfType().Single().ChangedSolution; + var firstContent = (await changedSolution.GetProject(firstProject.Id)!.AdditionalDocuments.Single() + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + var secondContent = (await changedSolution.GetProject(secondProjectId)!.AdditionalDocuments.Single() + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + + Assert.Contains("interface [GrainInterfaceType(\"IFirstGrain\")] IFirstGrain [Version(0)]", firstContent); + Assert.Contains("interface [GrainInterfaceType(\"ISecondGrain\")] ISecondGrain [Version(0)]", secondContent); + } + + private static Diagnostic CreateFixAllDiagnostic() + => Diagnostic.Create( + new DiagnosticDescriptor( + GrainInterfaceVersionAnalyzer.RuleId0016, + "Contract missing", + "Contract missing", + "Orleans.Versioning", + DiagnosticSeverity.Warning, + isEnabledByDefault: true), + Location.None); + + private sealed class TestFixAllDiagnosticProvider( + IReadOnlyDictionary> diagnostics) : FixAllContext.DiagnosticProvider + { + public override Task> GetDocumentDiagnosticsAsync( + Document document, + CancellationToken cancellationToken) + => Task.FromResult(Enumerable.Empty()); + + public override Task> GetProjectDiagnosticsAsync( + Project project, + CancellationToken cancellationToken) + => Task.FromResult(Enumerable.Empty()); + + public override Task> GetAllDiagnosticsAsync( + Project project, + CancellationToken cancellationToken) + => Task.FromResult( + diagnostics.TryGetValue(project.Id, out var result) + ? result + : Enumerable.Empty()); + } + + #endregion + #region Code Fix Tests - Grain Classes [Fact] @@ -1533,6 +1910,26 @@ public interface INewGrain : IGrain Assert.Equal(1, content.Split(new[] { "stable-interface" }, StringSplitOptions.None).Length - 1); } + [Fact] + public async Task CodeFix_AddInterface_PreservesLegacyDeclarationWhenIdentityChanges() + { + const string source = @" +[GrainInterfaceType(""new-id"")] +public interface IMyGrain : IGrain +{ +} +"; + const string contractsFile = "interface IMyGrain [Version(0)]"; + + var content = await ApplyCodeFixAndGetContractsAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0016); + + Assert.Contains("interface [GrainInterfaceType(\"new-id\")] IMyGrain [Version(0)]", content); + Assert.Contains("interface IMyGrain [Version(0)]", content); + } + #endregion #region Code Fix Tests - ORLEANS0017 Update Version @@ -1908,6 +2305,32 @@ IFoo [Version(1)] Assert.DoesNotContain("*RETIRED* interface IFooBar", content); } + [Fact] + public async Task CodeFix_RetireInterface_UsesStableIdentityWhenClrNamesMatch() + { + const string source = @" +[GrainInterfaceType(""current-id"")] +public interface IMyGrain : IGrain +{ +} +"; + const string grainInterfacesFile = @" +interface [GrainInterfaceType(""current-id"")] IMyGrain [Version(0)] +interface [GrainInterfaceType(""old-id"")] IMyGrain [Version(0)] +"; + + var content = await ApplyCodeFixAndGetContractsAsync( + source, + grainInterfacesFile, + GrainInterfaceVersionAnalyzer.RuleId0019); + + Assert.Contains("interface [GrainInterfaceType(\"current-id\")] IMyGrain [Version(0)]", content); + Assert.DoesNotContain("*RETIRED* interface [GrainInterfaceType(\"current-id\")]", content); + Assert.Contains( + "*RETIRED* interface [GrainInterfaceType(\"old-id\")] IMyGrain [Version(0)]", + content); + } + #endregion #region Inherited Interfaces From c59af54886387e96252179998253326410269791 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Thu, 27 Aug 2026 16:59:16 -0700 Subject: [PATCH 02/11] fix(analyzers): detect removed grain methods --- .../src/content/docs/diagnostics/index.md | 4 + .../content/docs/diagnostics/orleans0027.md | 37 +++ .../contract-compatibility-analyzer.md | 2 + docs/site/src/content/docs/toc.yml | 2 + .../AnalyzerReleases.Unshipped.md | 1 + .../GrainInterfaceVersionAnalyzer.cs | 227 ++++++++++++++-- .../GrainInterfaceVersionCodeFix.cs | 148 ++++++++++- src/Orleans.Analyzers/Resources.Designer.cs | 18 ++ src/Orleans.Analyzers/Resources.resx | 9 + .../GrainInterfaceVersionAnalyzerTest.cs | 249 ++++++++++++++++++ 10 files changed, 667 insertions(+), 30 deletions(-) create mode 100644 docs/site/src/content/docs/diagnostics/orleans0027.md diff --git a/docs/site/src/content/docs/diagnostics/index.md b/docs/site/src/content/docs/diagnostics/index.md index b6ddabca004..f26a81062ae 100644 --- a/docs/site/src/content/docs/diagnostics/index.md +++ b/docs/site/src/content/docs/diagnostics/index.md @@ -110,3 +110,7 @@ Analyzer help links use `https://aka.ms/orleans/diagnostics` with the diagnostic ## ORLEANS0026 [Invalid invokable base type mapping](orleans0026.md) — Error. A custom grain-call return type mapping cannot generate a valid invokable request. + +## ORLEANS0027 + +[Grain interface member removed from source](orleans0027.md) — Warning. The manifest retains an RPC signature which is absent from source. diff --git a/docs/site/src/content/docs/diagnostics/orleans0027.md b/docs/site/src/content/docs/diagnostics/orleans0027.md new file mode 100644 index 00000000000..ad7fcbc036b --- /dev/null +++ b/docs/site/src/content/docs/diagnostics/orleans0027.md @@ -0,0 +1,37 @@ +--- +title: "ORLEANS0027: Grain interface member removed from source" +description: Understand and resolve ORLEANS0027 when OrleansContracts.txt retains an RPC method which is absent from source. +ms.date: 08/27/2026 +ms.topic: reference +--- + +# ORLEANS0027: Grain interface member removed from source + +| Property | Value | +| --- | --- | +| Category | Orleans.Versioning | +| Severity | Warning | +| Code fix | Not available | + +## Cause + +`OrleansContracts.txt` declares an RPC method signature which is absent from the matching source grain interface. + +## Impact + +Removing an RPC method can break calls from older clients or activations during a rolling upgrade. Regeneration retains the historical signature so the wire-contract removal remains visible and requires an explicit decision. + +## How to fix + +Restore the source method when the removal was accidental. When the removal is intentional, review the mixed-version deployment impact, increment the interface version when appropriate, and explicitly remove the retained signature from `OrleansContracts.txt`. + +See [Orleans contract compatibility analyzer](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). + +## Suppress the diagnostic + +Prefer updating the reviewed manifest after accepting the contract removal. Suppress only for a manifest intentionally shared with another compilation. + +```ini +[*.cs] +dotnet_diagnostic.ORLEANS0027.severity = none +``` diff --git a/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md b/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md index b3a0fa59bb1..3f5095a06c3 100644 --- a/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md +++ b/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md @@ -54,6 +54,7 @@ Add the generated file to source control and review its diff before committing. - A changed `GrainInterfaceType`, `GrainType`, method identity, parameter type, or return type changes a wire identity or signature. - A removed source contract becomes `*RETIRED*`, preserving its identity history and preventing accidental reuse. +- A removed RPC method remains in the manifest and reports `ORLEANS0027` until the method is restored or the wire break is explicitly accepted by removing the retained signature. - A `[Version]` change affects version-aware routing and must align with the rolling-upgrade design. - A CLR comment-only change records a refactor while the explicit Orleans identity remains stable. @@ -129,6 +130,7 @@ Without an explicit stable identity, Orleans derives the identity from the CLR t | [`ORLEANS0023`](../../diagnostics/orleans0023.md) | Warning | A grain class identity differs from the manifest. | | [`ORLEANS0024`](../../diagnostics/orleans0024.md) | Warning | A removed grain class isn't marked `*RETIRED*`. | | [`ORLEANS0025`](../../diagnostics/orleans0025.md) | Warning | A grain class is declared more than once. | +| [`ORLEANS0027`](../../diagnostics/orleans0027.md) | Warning | An RPC method remains in the manifest after it is removed from source. | Standard `.editorconfig` diagnostic configuration can change these severities. Prefer fixing contract drift instead of suppressing diagnostics. diff --git a/docs/site/src/content/docs/toc.yml b/docs/site/src/content/docs/toc.yml index 6c7037e1fe1..ac0f03b8883 100644 --- a/docs/site/src/content/docs/toc.yml +++ b/docs/site/src/content/docs/toc.yml @@ -435,6 +435,8 @@ items: href: diagnostics/orleans0025.md - name: ORLEANS0026 href: diagnostics/orleans0026.md + - name: ORLEANS0027 + href: diagnostics/orleans0027.md - name: API context and examples href: resources/api-reference-guide.md - name: Best practices diff --git a/src/Orleans.Analyzers/AnalyzerReleases.Unshipped.md b/src/Orleans.Analyzers/AnalyzerReleases.Unshipped.md index 28ca7fa6bfc..30fabca3063 100644 --- a/src/Orleans.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/Orleans.Analyzers/AnalyzerReleases.Unshipped.md @@ -16,3 +16,4 @@ ORLEANS0022 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Grai ORLEANS0023 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Grain class alias mismatch ORLEANS0024 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Removed grain class not marked as *RETIRED* ORLEANS0025 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Duplicate grain class declaration in file +ORLEANS0027 | Orleans.Versioning | Warning | GrainInterfaceVersionAnalyzer, Removed grain interface member remains in OrleansContracts.txt diff --git a/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs b/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs index 8c0b459f8a2..e8fe95c5d83 100644 --- a/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs +++ b/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs @@ -33,6 +33,7 @@ public sealed partial class GrainInterfaceVersionAnalyzer : DiagnosticAnalyzer public const string RuleId0023 = "ORLEANS0023"; public const string RuleId0024 = "ORLEANS0024"; public const string RuleId0025 = "ORLEANS0025"; + public const string RuleId0027 = "ORLEANS0027"; // Property bag keys for code fixes internal const string InterfaceNamePropertyKey = "InterfaceName"; @@ -79,6 +80,17 @@ public sealed partial class GrainInterfaceVersionAnalyzer : DiagnosticAnalyzer description: new LocalizableResourceString(nameof(Resources.GrainInterfaceMemberNotDeclaredDescription), Resources.ResourceManager, typeof(Resources)), helpLinkUri: Constants.GetDiagnosticHelpLink(RuleId0018)); + private static readonly DiagnosticDescriptor RemovedMemberRule = new( + id: RuleId0027, + title: new LocalizableResourceString(nameof(Resources.GrainInterfaceMemberRemovedTitle), Resources.ResourceManager, typeof(Resources)), + messageFormat: new LocalizableResourceString(nameof(Resources.GrainInterfaceMemberRemovedMessageFormat), Resources.ResourceManager, typeof(Resources)), + category: "Orleans.Versioning", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: new LocalizableResourceString(nameof(Resources.GrainInterfaceMemberRemovedDescription), Resources.ResourceManager, typeof(Resources)), + helpLinkUri: Constants.GetDiagnosticHelpLink(RuleId0027), + customTags: WellKnownDiagnosticTags.CompilationEnd); + private static readonly DiagnosticDescriptor RemovedInterfaceNotRetiredRule = new( id: RuleId0019, title: new LocalizableResourceString(nameof(Resources.GrainInterfaceRemovedNotRetiredTitle), Resources.ResourceManager, typeof(Resources)), @@ -163,7 +175,8 @@ public sealed partial class GrainInterfaceVersionAnalyzer : DiagnosticAnalyzer GrainClassNotDeclaredRule, GrainClassAliasMismatchRule, RemovedGrainClassNotRetiredRule, - DuplicateGrainClassDeclarationRule); + DuplicateGrainClassDeclarationRule, + RemovedMemberRule); public override void Initialize(AnalysisContext context) { @@ -215,6 +228,7 @@ private sealed class Impl private readonly List? _fileParseErrors; private readonly ConcurrentDictionary _visitedInterfaces = new(StringComparer.Ordinal); private readonly ConcurrentDictionary _visitedClasses = new(StringComparer.Ordinal); + private readonly ConcurrentBag _removedMemberDiagnostics = new(); private readonly INamedTypeSymbol? _iAddressableType; private readonly INamedTypeSymbol? _aliasAttributeType; private readonly INamedTypeSymbol? _versionAttributeType; @@ -351,38 +365,23 @@ explicitGrainInterfaceType is null // Alias mismatch - could add a separate diagnostic for this } + var sourceMembers = namedType.GetMembers() + .OfType() + .Where(member => member.MethodKind == MethodKind.Ordinary && !member.IsStatic) + .ToArray(); + // Check members - foreach (var member in namedType.GetMembers().OfType()) + foreach (var member in sourceMembers) { - if (member.MethodKind != MethodKind.Ordinary || member.IsStatic) - { - continue; - } - var memberSignature = GetMethodSignature(member); var memberAlias = GetAliasFromAttribute(member); - if (!declaredInterface.Members.ContainsKey(memberSignature) - && !declaredInterface.Members.Keys.Any(signature => - { - if (string.Equals( - NormalizeStoredMemberSignature(signature, declaredInterface.Name), - memberSignature, - StringComparison.Ordinal)) - { - return true; - } - - var normalized = NormalizeLegacyMethodSignature(signature); - var clrSignature = GetClrMethodSignature(member); - var containingTypePrefix = - $"{member.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "")}."; - return string.Equals(normalized, NormalizeLegacyMethodSignature(clrSignature), StringComparison.Ordinal) - || string.Equals( - normalized, - NormalizeLegacyMethodSignature(clrSignature.Substring(containingTypePrefix.Length)), - StringComparison.Ordinal); - })) + if (!declaredInterface.Members.Values.Any(declaredMember => + GrainInterfaceVersionAnalyzer.IsMatchingMember( + declaredInterface.Name, + declaredMember.Signature, + declaredMember.Alias, + member))) { // Member not found - interface has changed var properties = ImmutableDictionary.Empty @@ -403,6 +402,33 @@ explicitGrainInterfaceType is null } } } + + if (_grainInterfacesFile?.GetText(context.CancellationToken) is { } sourceText) + { + foreach (var declaredMember in declaredInterface.Members.Values) + { + if (sourceMembers.Any(member => + GrainInterfaceVersionAnalyzer.IsMatchingMember( + declaredInterface.Name, + declaredMember.Signature, + declaredMember.Alias, + member))) + { + continue; + } + + var properties = ImmutableDictionary.Empty + .Add(InterfaceNamePropertyKey, interfaceName) + .Add(GrainInterfaceTypePropertyKey, grainInterfaceType) + .Add(MemberNamePropertyKey, declaredMember.Signature); + _removedMemberDiagnostics.Add(Diagnostic.Create( + RemovedMemberRule, + declaredMember.GetLocation(sourceText, _grainInterfacesFile.Path), + properties, + declaredMember.Signature, + interfaceName)); + } + } } public void OnCompilationEnd(CompilationAnalysisContext context) @@ -416,6 +442,11 @@ public void OnCompilationEnd(CompilationAnalysisContext context) } } + foreach (var diagnostic in _removedMemberDiagnostics) + { + context.ReportDiagnostic(diagnostic); + } + // Report file missing if any grain interfaces were found but no file exists if (_data is null && (_visitedInterfaces.Count > 0 || _visitedClasses.Count > 0)) { @@ -866,6 +897,78 @@ internal static string NormalizeStoredMemberSignature(string signature, string i return Regex.Replace(result, @"alias\(""([^""]+)""\)", "$1"); } + internal static bool IsMatchingMember( + string declaredInterfaceName, + string storedSignature, + string? storedAlias, + IMethodSymbol member) + { + var memberSignature = GetMethodSignature(member); + if (storedAlias is not null) + { + if (!string.Equals( + storedAlias, + GetStringAttributeValue(member, Constants.AliasAttributeFullyQualifiedName), + StringComparison.Ordinal)) + { + return false; + } + + var storedIdentity = storedAlias + GrainInterfaceFileParser.GetMethodAritySuffix(storedSignature); + var parameterListStart = memberSignature.IndexOf('('); + if (parameterListStart < 0 + || !string.Equals( + storedIdentity, + memberSignature.Substring(0, parameterListStart), + StringComparison.Ordinal)) + { + return false; + } + + var canonicalStoredSignature = GrainInterfaceFileParser.GetCanonicalMemberSignature( + storedSignature, + storedAlias); + if (string.Equals( + NormalizeStoredMemberSignature(canonicalStoredSignature, declaredInterfaceName), + memberSignature, + StringComparison.Ordinal)) + { + return true; + } + + return string.Equals( + GetNormalizedSignatureSuffix(storedSignature), + GetNormalizedSignatureSuffix(GetClrMethodSignature(member)), + StringComparison.Ordinal); + } + + if (string.Equals( + NormalizeStoredMemberSignature(storedSignature, declaredInterfaceName), + memberSignature, + StringComparison.Ordinal)) + { + return true; + } + + var normalized = NormalizeLegacyMethodSignature(storedSignature); + var clrSignature = GetClrMethodSignature(member); + var containingTypePrefix = + $"{member.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).Replace("global::", "")}."; + return string.Equals(normalized, NormalizeLegacyMethodSignature(clrSignature), StringComparison.Ordinal) + || string.Equals( + normalized, + NormalizeLegacyMethodSignature(clrSignature.Substring(containingTypePrefix.Length)), + StringComparison.Ordinal); + } + + private static string GetNormalizedSignatureSuffix(string signature) + { + var parameterListStart = signature.IndexOf('('); + return parameterListStart < 0 + ? NormalizeLegacyMethodSignature(signature) + : NormalizeLegacyMethodSignature(signature.Substring(parameterListStart)); + } + private static string GetContractTypeName(ITypeSymbol type) { if (type is IArrayTypeSymbol array) @@ -1010,6 +1113,12 @@ public DeclaredGrainMember(string signature) public string Signature { get; } public string? Alias { get; set; } public TextSpan Span { get; set; } + + public Location GetLocation(SourceText sourceText, string filePath) + { + var lineSpan = sourceText.Lines.GetLinePositionSpan(Span); + return Location.Create(filePath, Span, lineSpan); + } } /// @@ -1118,18 +1227,80 @@ internal static bool TryGetContractName(string line, out string name) => TryGetGrainClassName(line, out name) || TryGetInterfaceName(line, out name); internal static bool TryGetMemberSignature(string line, out string signature) + { + if (TryGetMemberDeclaration(line, out signature, out var alias)) + { + signature = GetCanonicalMemberSignature(signature, alias); + return true; + } + + signature = string.Empty; + return false; + } + + internal static bool TryGetMemberDeclaration(string line, out string signature, out string? alias) { var match = MemberPattern.Match(StripClrComment(line)); if (match.Success) { signature = match.Groups["signature"].Value; + alias = match.Groups["alias"].Success ? match.Groups["alias"].Value : null; return true; } signature = string.Empty; + alias = null; return false; } + internal static string GetCanonicalMemberSignature(string signature, string? alias) + { + if (alias is null) + { + return signature; + } + + var parameterListStart = signature.IndexOf('('); + return parameterListStart < 0 + ? signature + : alias + GetMethodAritySuffix(signature) + signature.Substring(parameterListStart); + } + + internal static string GetMethodAritySuffix(string signature) + { + var parameterListStart = signature.IndexOf('('); + if (parameterListStart < 0) + { + return string.Empty; + } + + var methodStart = signature.LastIndexOf('.', parameterListStart - 1) + 1; + var methodName = signature.Substring(methodStart, parameterListStart - methodStart); + var arityMarker = methodName.LastIndexOf('`'); + if (arityMarker >= 0) + { + return methodName.Substring(arityMarker); + } + + var genericStart = methodName.IndexOf('<'); + var genericEnd = methodName.LastIndexOf('>'); + if (genericStart < 0 || genericEnd <= genericStart) + { + return string.Empty; + } + + var arity = 1; + for (var index = genericStart + 1; index < genericEnd; index++) + { + if (methodName[index] == ',') + { + arity++; + } + } + + return $"`{arity}"; + } + internal static string GetClrComment(string line) { const string Prefix = " # CLR: "; diff --git a/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs b/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs index 5783b603549..56d3cbe278b 100644 --- a/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs +++ b/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs @@ -104,6 +104,8 @@ private static async Task RegenerateOrleansContractsFileAsync( var lines = new List(); var activeInterfaceIdentities = new HashSet(StringComparer.Ordinal); var activeConventionInterfaceNames = new HashSet(StringComparer.Ordinal); + var activeInterfaces = new Dictionary(StringComparer.Ordinal); + var activeInterfacesByName = new Dictionary(StringComparer.Ordinal); var activeClassIdentities = new HashSet(StringComparer.Ordinal); var activeConventionClassNames = new HashSet(StringComparer.Ordinal); var iAddressableType = compilation.GetTypeByMetadataName(Constants.IAddressibleFullyQualifiedName); @@ -117,7 +119,13 @@ private static async Task RegenerateOrleansContractsFileAsync( && !SymbolEqualityComparer.Default.Equals(type, iAddressableType) && type.AllInterfaces.Any(candidate => SymbolEqualityComparer.Default.Equals(candidate, iAddressableType))) { - AppendInterface(lines, type, activeInterfaceIdentities, activeConventionInterfaceNames); + AppendInterface( + lines, + type, + activeInterfaceIdentities, + activeConventionInterfaceNames, + activeInterfaces, + activeInterfacesByName); } else if (type.TypeKind == TypeKind.Class && !type.IsAbstract && type.IsGrainClass()) { @@ -132,6 +140,8 @@ private static async Task RegenerateOrleansContractsFileAsync( existingText.ToString(), activeInterfaceIdentities, activeConventionInterfaceNames, + activeInterfaces, + activeInterfacesByName, activeClassIdentities, activeConventionClassNames); } @@ -297,13 +307,17 @@ private static void AppendInterface( List lines, INamedTypeSymbol type, HashSet activeInterfaceIdentities, - HashSet activeConventionInterfaceNames) + HashSet activeConventionInterfaceNames, + Dictionary activeInterfaces, + Dictionary activeInterfacesByName) { AppendBlockSeparator(lines); var interfaceName = GetFullyQualifiedName(type); var explicitInterfaceType = GetGrainInterfaceTypeFromAttributes(type); var interfaceType = GrainInterfaceVersionAnalyzer.GetGrainInterfaceType(type); activeInterfaceIdentities.Add(interfaceType); + activeInterfaces[interfaceType] = type; + activeInterfacesByName[interfaceName] = type; if (explicitInterfaceType is null || string.Equals( explicitInterfaceType, @@ -364,6 +378,8 @@ private static void AppendHistoricalContracts( string existingContent, HashSet activeInterfaceIdentities, HashSet activeConventionInterfaceNames, + Dictionary activeInterfaces, + Dictionary activeInterfacesByName, HashSet activeClassIdentities, HashSet activeConventionClassNames) { @@ -434,11 +450,139 @@ private static void AppendHistoricalContracts( : lines[blockIndex]); } } + else if (isInterface) + { + var interfaceIdentity = explicitIdentity + ?? GrainInterfaceVersionAnalyzer.GetDefaultGrainInterfaceType(contractName); + if (activeInterfaces.TryGetValue(interfaceIdentity, out var activeInterface) + || activeConventionInterfaceNames.Contains(contractName) + && activeInterfacesByName.TryGetValue(contractName, out activeInterface)) + { + MergeHistoricalMembers( + result, + lines, + index, + blockEnd, + contractName, + activeInterface); + } + } index = blockEnd - 1; } } + private static void MergeHistoricalMembers( + List result, + string[] historicalLines, + int historicalDeclarationIndex, + int historicalBlockEnd, + string historicalInterfaceName, + INamedTypeSymbol activeInterface) + { + var generatedDeclarationIndex = -1; + var activeInterfaceIdentity = GrainInterfaceVersionAnalyzer.GetGrainInterfaceType(activeInterface); + var activeInterfaceName = GetFullyQualifiedName(activeInterface); + for (var index = 0; index < result.Count; index++) + { + if (!GrainInterfaceFileParser.TryGetInterfaceName(result[index], out var generatedInterfaceName)) + { + continue; + } + + var generatedIdentity = GrainInterfaceFileParser.TryGetGrainInterfaceType( + result[index], + out var generatedInterfaceType) + ? generatedInterfaceType + : GrainInterfaceVersionAnalyzer.GetDefaultGrainInterfaceType(generatedInterfaceName); + if (string.Equals(generatedIdentity, activeInterfaceIdentity, StringComparison.Ordinal) + || string.Equals(generatedInterfaceName, activeInterfaceName, StringComparison.Ordinal)) + { + generatedDeclarationIndex = index; + break; + } + } + + if (generatedDeclarationIndex < 0) + { + return; + } + + var generatedBlockEnd = generatedDeclarationIndex + 1; + var generatedMembers = new HashSet(StringComparer.Ordinal); + while (generatedBlockEnd < result.Count) + { + if (GrainInterfaceFileParser.TryGetContractName(result[generatedBlockEnd], out _) + || result[generatedBlockEnd].TrimStart().StartsWith("# ", StringComparison.Ordinal) + && generatedBlockEnd + 1 < result.Count + && GrainInterfaceFileParser.TryGetContractName(result[generatedBlockEnd + 1], out _)) + { + break; + } + + if (GrainInterfaceFileParser.TryGetMemberSignature(result[generatedBlockEnd], out var memberSignature)) + { + generatedMembers.Add(GetHistoricalMemberKey(memberSignature, historicalInterfaceName)); + } + + generatedBlockEnd++; + } + + string? pendingComment = null; + for (var index = historicalDeclarationIndex + 1; index < historicalBlockEnd; index++) + { + var line = historicalLines[index]; + if (line.TrimStart().StartsWith("# ", StringComparison.Ordinal)) + { + pendingComment = line.Trim(); + continue; + } + + if (!GrainInterfaceFileParser.TryGetMemberDeclaration( + line, + out var memberSignature, + out var memberAlias)) + { + pendingComment = null; + continue; + } + + if (activeInterface.GetMembers() + .OfType() + .Any(member => member.MethodKind == MethodKind.Ordinary + && !member.IsStatic + && GrainInterfaceVersionAnalyzer.IsMatchingMember( + historicalInterfaceName, + memberSignature, + memberAlias, + member))) + { + pendingComment = null; + continue; + } + + var normalizedSignature = GrainInterfaceVersionAnalyzer.NormalizeStoredMemberSignature( + GrainInterfaceFileParser.GetCanonicalMemberSignature(memberSignature, memberAlias), + historicalInterfaceName); + var historicalMemberKey = GetHistoricalMemberKey(normalizedSignature, historicalInterfaceName); + if (generatedMembers.Add(historicalMemberKey)) + { + if (pendingComment is not null) + { + result.Insert(generatedBlockEnd++, $" {pendingComment}"); + } + + result.Insert(generatedBlockEnd++, $" {historicalMemberKey}"); + } + + pendingComment = null; + } + } + + private static string GetHistoricalMemberKey(string signature, string interfaceName) + => GrainInterfaceVersionAnalyzer.NormalizeLegacyMethodSignature( + GrainInterfaceVersionAnalyzer.NormalizeStoredMemberSignature(signature, interfaceName)); + private static void AppendBlockSeparator(List lines) { while (lines.Count > 0 && string.IsNullOrWhiteSpace(lines[lines.Count - 1])) diff --git a/src/Orleans.Analyzers/Resources.Designer.cs b/src/Orleans.Analyzers/Resources.Designer.cs index c02c461d0fa..5b3f26601ba 100644 --- a/src/Orleans.Analyzers/Resources.Designer.cs +++ b/src/Orleans.Analyzers/Resources.Designer.cs @@ -409,6 +409,24 @@ internal static string GrainInterfaceMemberNotDeclaredTitle { return ResourceManager.GetString("GrainInterfaceMemberNotDeclaredTitle", resourceCulture); } } + + internal static string GrainInterfaceMemberRemovedDescription { + get { + return ResourceManager.GetString("GrainInterfaceMemberRemovedDescription", resourceCulture); + } + } + + internal static string GrainInterfaceMemberRemovedMessageFormat { + get { + return ResourceManager.GetString("GrainInterfaceMemberRemovedMessageFormat", resourceCulture); + } + } + + internal static string GrainInterfaceMemberRemovedTitle { + get { + return ResourceManager.GetString("GrainInterfaceMemberRemovedTitle", resourceCulture); + } + } /// /// Looks up a localized string similar to All grain interfaces should have an active declaration in OrleansContracts.txt to ensure version compatibility during rolling upgrades.. diff --git a/src/Orleans.Analyzers/Resources.resx b/src/Orleans.Analyzers/Resources.resx index 0dd048a118d..cb415639a30 100644 --- a/src/Orleans.Analyzers/Resources.resx +++ b/src/Orleans.Analyzers/Resources.resx @@ -241,6 +241,15 @@ When adding or modifying grain interface members, update OrleansContracts.txt and increment the interface version. + + Grain interface member removed from source + + + Grain interface member '{0}' is declared in OrleansContracts.txt for interface '{1}' but is absent from source. Restore the method or explicitly remove the manifest entry after reviewing the wire break. See https://aka.ms/orleans/OrleansContracts.txt for details. + + + Removing a grain interface method can break calls during a rolling upgrade. OrleansContracts.txt retains the signature until the removal is explicitly accepted. + Removed grain interface not marked as retired diff --git a/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs b/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs index fed281bf8dd..d0fe5332497 100644 --- a/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs +++ b/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs @@ -523,6 +523,118 @@ interface IMyGrain [Version(1)] Assert.Contains(diagnostics, d => d.Id == GrainInterfaceVersionAnalyzer.RuleId0018); } + [Fact] + public async Task RemovedMember_ReportsDiagnostic() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + Task ExistingAsync(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + ExistingAsync() -> Task + RemovedAsync() -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + var diagnostic = Assert.Single( + diagnostics, + diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + Assert.Contains("RemovedAsync() -> Task", diagnostic.GetMessage()); + Assert.Equal(OrleansContractsFileName, Path.GetFileName(diagnostic.Location.GetLineSpan().Path)); + } + + [Fact] + public async Task LegacyAliasedMemberRename_NoDiagnostic() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + [Alias(""stable-method"")] + Task NewName(string renamed); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + [Alias(""stable-method"")] IMyGrain.OldName(string original) -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.DoesNotContain(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0018); + Assert.DoesNotContain(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task AliasedMemberIdentityChange_ReportsAddedAndRemovedSignatures() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + [Alias(""new-method"")] + Task Method(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + [Alias(""old-method"")] IMyGrain.Method() -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0018); + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task RemovedAlias_ReportsAddedAndRemovedSignatures() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + Task Method(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + [Alias(""old-method"")] IMyGrain.Method() -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0018); + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task AliasedMemberGenericArityChange_ReportsAddedAndRemovedSignatures() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + [Alias(""stable-method"")] + Task Method(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + [Alias(""stable-method"")] IMyGrain.Method() -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0018); + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + #endregion #region ORLEANS0019 - Removed Interface Not Retired @@ -1431,6 +1543,143 @@ public class InnerGrain : Grain, IGrainWithStringKey Assert.Empty(await GetDiagnosticsAsync(source, content)); } + [Fact] + public async Task CodeFix_RegenerateProject_PreservesRemovedMemberSignatures() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + Task ExistingAsync(); + Task NewAsync(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + ExistingAsync() -> Task + RemovedAsync() -> Task +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0018, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains(" ExistingAsync() -> Task", content); + Assert.Contains(" NewAsync() -> Task", content); + Assert.Contains(" RemovedAsync() -> Task", content); + + var diagnostics = await GetDiagnosticsAsync(source, content); + Assert.DoesNotContain(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0018); + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task CodeFix_RegenerateProject_PreservesAliasesAndDeduplicatesLegacyMembers() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + Task ExistingAsync(); + Task NewAsync(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + ExistingAsync() -> Task + [Alias(""removed"")] IMyGrain.RemovedAsync(string value) -> Task + removed(string) -> Task +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0018, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Equal(1, content.Split(new[] { "removed(string) -> Task" }, StringSplitOptions.None).Length - 1); + Assert.DoesNotContain("[Alias(\"removed\")]", content); + + var diagnostics = await GetDiagnosticsAsync(source, content); + Assert.Single(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task CodeFix_RegenerateProject_DeduplicatesSemanticallyEquivalentLegacyMembers() + { + const string source = @" +namespace Models +{ + public sealed class Request { } +} + +[Version(1)] +public interface IMyGrain : IGrain +{ + Task Method(Models.Request request); + Task NewAsync(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + Method(Request value) -> Task +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0018, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains(" Method(Models.Request) -> Task", content); + Assert.DoesNotContain(" Method(Request) -> Task", content); + Assert.DoesNotContain( + await GetDiagnosticsAsync(source, content), + diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task CodeFix_RegenerateProject_PreservesRemovedMembersOnNestedLegacyInterfaces() + { + const string source = @" +public class Outer +{ + [Version(1)] + public interface IInnerGrain : IGrain + { + Task ExistingAsync(); + Task NewAsync(); + } +} +"; + const string contractsFile = @" +interface Outer.IInnerGrain [Version(1)] + ExistingAsync() -> Task + RemovedAsync() -> Task +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + contractsFile, + GrainInterfaceVersionAnalyzer.RuleId0018, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains(" RemovedAsync() -> Task", content); + Assert.Contains( + await GetDiagnosticsAsync(source, content), + diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + [Fact] public async Task FixAll_RegenerateSolution_UpdatesEveryProjectWithDiagnostics() { From 012437498bc98cb5f0e604da7f24f4e6583d3fc0 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Thu, 27 Aug 2026 20:39:24 -0700 Subject: [PATCH 03/11] fix(analyzers): address contracts review feedback --- .../GrainInterfaceVersionAnalyzer.cs | 13 +++++++----- .../GrainInterfaceVersionAnalyzerTest.cs | 21 ++++++++++++++++++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs b/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs index e8fe95c5d83..1493be7e35e 100644 --- a/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs +++ b/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs @@ -204,18 +204,19 @@ private void OnCompilationStart(CompilationStartAnalysisContext context) && string.Equals(value, "true", StringComparison.OrdinalIgnoreCase)); GrainInterfaceData? data = null; + SourceText? sourceText = null; List? fileParseErrors = null; if (grainInterfacesFile is not null) { - var sourceText = grainInterfacesFile.GetText(context.CancellationToken); + sourceText = grainInterfacesFile.GetText(context.CancellationToken); if (sourceText is not null) { (data, fileParseErrors) = GrainInterfaceFileParser.Parse(sourceText, grainInterfacesFile.Path); } } - var impl = new Impl(context.Compilation, data, grainInterfacesFile, fileParseErrors); + var impl = new Impl(context.Compilation, data, grainInterfacesFile, sourceText, fileParseErrors); context.RegisterSymbolAction(impl.AnalyzeNamedType, SymbolKind.NamedType); context.RegisterCompilationEndAction(impl.OnCompilationEnd); @@ -225,6 +226,7 @@ private sealed class Impl { private readonly GrainInterfaceData? _data; private readonly AdditionalText? _grainInterfacesFile; + private readonly SourceText? _grainInterfacesFileText; private readonly List? _fileParseErrors; private readonly ConcurrentDictionary _visitedInterfaces = new(StringComparer.Ordinal); private readonly ConcurrentDictionary _visitedClasses = new(StringComparer.Ordinal); @@ -238,10 +240,12 @@ public Impl( Compilation compilation, GrainInterfaceData? data, AdditionalText? grainInterfacesFile, + SourceText? grainInterfacesFileText, List? fileParseErrors) { _data = data; _grainInterfacesFile = grainInterfacesFile; + _grainInterfacesFileText = grainInterfacesFileText; _fileParseErrors = fileParseErrors; _iAddressableType = compilation.GetTypeByMetadataName(Constants.IAddressibleFullyQualifiedName); @@ -403,7 +407,7 @@ explicitGrainInterfaceType is null } } - if (_grainInterfacesFile?.GetText(context.CancellationToken) is { } sourceText) + if (_grainInterfacesFile is not null && _grainInterfacesFileText is { } sourceText) { foreach (var declaredMember in declaredInterface.Members.Values) { @@ -459,8 +463,7 @@ public void OnCompilationEnd(CompilationAnalysisContext context) // Check for removed interfaces (in file but not in code) if (_data is not null && _grainInterfacesFile is not null) { - var sourceText = _grainInterfacesFile.GetText(context.CancellationToken); - if (sourceText is not null) + if (_grainInterfacesFileText is { } sourceText) { foreach (var declaredInterface in _data.Interfaces) { diff --git a/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs b/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs index d0fe5332497..f9646c75c81 100644 --- a/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs +++ b/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs @@ -1277,13 +1277,17 @@ private static Project CreateProjectWithAdditionalFilesForCodeFix( if (configuredContractsPath is not null) { + var analyzerConfigDirectory = Path.GetDirectoryName(configuredContractsPath); + var analyzerConfigPath = string.IsNullOrEmpty(analyzerConfigDirectory) + ? Path.Combine(Path.GetTempPath(), ".globalconfig") + : Path.Combine(analyzerConfigDirectory, ".globalconfig"); solution = solution.AddAnalyzerConfigDocument( DocumentId.CreateNewId(projectId, ".globalconfig"), ".globalconfig", SourceText.From( $"is_global = true{Environment.NewLine}" + $"build_property.OrleansContractsPath = {configuredContractsPath}{Environment.NewLine}"), - filePath: Path.Combine(Path.GetDirectoryName(configuredContractsPath)!, ".globalconfig")); + filePath: analyzerConfigPath); } return solution.GetProject(projectId)! @@ -1342,6 +1346,21 @@ public async Task CodeFix_RegenerateMissingFile_UsesConfiguredManifestPath() Assert.Equal(configuredPath, changedSolution.GetAdditionalDocument(additionalDocumentId!)!.FilePath); } + [Fact] + public async Task CodeFix_RegenerateMissingFile_UsesFilenameOnlyConfiguredManifestPath() + { + const string configuredPath = "rpc-contracts.txt"; + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + "public interface IMyGrain : IGrain { Task Ping(); }", + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle, + configuredPath); + + Assert.NotNull(additionalDocumentId); + Assert.Equal(configuredPath, changedSolution.GetAdditionalDocument(additionalDocumentId!)!.FilePath); + } + [Fact] public async Task CodeFix_RegenerateProject_PreservesContractHistory() { From a40cc889f2afde9da42dd0d515b67bd94e5b9e63 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Thu, 27 Aug 2026 22:08:13 -0700 Subject: [PATCH 04/11] docs(analyzers): make contract regeneration agent-ready --- .../contract-compatibility-analyzer.md | 17 ++++++++++++++--- .../Orleans.Dashboard/OrleansContracts.txt | 5 ++++- .../GrainInterfaceVersionCodeFix.cs | 8 +++++++- src/Orleans.Analyzers/Resources.resx | 16 ++++++++-------- .../OrleansContracts.txt | 5 ++++- .../OrleansContracts.txt | 5 ++++- src/Orleans.Core/OrleansContracts.txt | 5 ++++- src/Orleans.DurableJobs/OrleansContracts.txt | 5 ++++- src/Orleans.EventSourcing/OrleansContracts.txt | 5 ++++- .../OrleansContracts.txt | 5 ++++- src/Orleans.Reminders/OrleansContracts.txt | 5 ++++- src/Orleans.Runtime/OrleansContracts.txt | 5 ++++- src/Orleans.Streaming/OrleansContracts.txt | 5 ++++- src/Orleans.TestingHost/OrleansContracts.txt | 5 ++++- .../OrleansContracts.txt | 5 ++++- src/Orleans.Transactions/OrleansContracts.txt | 5 ++++- .../GrainInterfaceVersionAnalyzerTest.cs | 7 +++++-- 17 files changed, 86 insertions(+), 27 deletions(-) diff --git a/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md b/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md index 3f5095a06c3..50805d91e7c 100644 --- a/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md +++ b/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md @@ -44,12 +44,20 @@ The regeneration code fix rebuilds every active interface, method, and grain-cla Apply **Regenerate OrleansContracts.txt** from any Orleans contract diagnostic. One application regenerates the entire project manifest. In an IDE, use **Fix all in project** or **Fix all in solution** to regenerate every affected project. -Agents and command-line workflows can apply the same fix to a project or solution: +Agents and command-line workflows can regenerate manifests without an IDE: ```dotnetcli -dotnet format MySolution.slnx analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 ``` +Run the command from the repository root. Replace `PATH_TO_PROJECT_OR_SOLUTION` with the path to the owning `.csproj` to regenerate one manifest, or a `.sln`/`.slnx` path to regenerate manifests in every affected project. The `--severity info` option includes `ORLEANS0020`, allowing the command to create a missing manifest. + +After the command completes: + +1. Inspect `git diff -- "*OrleansContracts.txt"` and account for every changed identity, version, and method signature. +2. Preserve all `*RETIRED*` declarations and retained removed-method signatures unless the compatibility break is intentional. +3. Run `dotnet build PATH_TO_PROJECT_OR_SOLUTION` and resolve all Orleans contract diagnostics. `ORLEANS0027` remains until a removed method is restored or its retained signature is explicitly deleted after compatibility review. + Add the generated file to source control and review its diff before committing. Treat every changed contract line as a potential wire-compatibility change: - A changed `GrainInterfaceType`, `GrainType`, method identity, parameter type, or return type changes a wire identity or signature. @@ -66,7 +74,10 @@ Interface methods are indented beneath their interface: ```text # This file is generated by the Orleans contract analyzer. -# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt diff --git a/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt b/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt index 5b473616583..8b3f675387c 100644 --- a/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt +++ b/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt @@ -1,5 +1,8 @@ # This file is generated by the Orleans contract analyzer. -# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt diff --git a/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs b/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs index 56d3cbe278b..5928c633cbc 100644 --- a/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs +++ b/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs @@ -26,10 +26,15 @@ public class GrainInterfaceVersionCodeFix : CodeFixProvider private const string DefaultNewLine = "\n"; private const string RegenerateCodeActionTitle = "Regenerate OrleansContracts.txt"; private const string RegenerateCodeActionEquivalenceKey = nameof(RegenerateOrleansContractsFileAsync); + private const string RegenerationCommand = + "dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024"; private static readonly string[] GeneratedHeader = [ "# This file is generated by the Orleans contract analyzer.", - "# Regenerate it by applying \"Regenerate OrleansContracts.txt\" at project or solution scope.", + "# To regenerate, run this command from the repository root after replacing", + "# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path:", + $"# {RegenerationCommand}", + "# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION", "# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades.", "# Details: https://aka.ms/orleans/OrleansContracts.txt" ]; @@ -1483,6 +1488,7 @@ private static string SortContractEntries(string content, string newLine) private static bool IsGeneratedHeaderLine(string line) => GeneratedHeader.Contains(line, StringComparer.Ordinal) || line is "# OrleansContracts.txt" + or "# Regenerate it by applying \"Regenerate OrleansContracts.txt\" at project or solution scope." or "# This file is auto-generated by the Orleans contract analyzer." or "# Update source contracts, then regenerate this file by following:" or "# https://aka.ms/orleans/OrleansContracts.txt" diff --git a/src/Orleans.Analyzers/Resources.resx b/src/Orleans.Analyzers/Resources.resx index cb415639a30..2ca317faa2b 100644 --- a/src/Orleans.Analyzers/Resources.resx +++ b/src/Orleans.Analyzers/Resources.resx @@ -218,7 +218,7 @@ Grain interface is not active in OrleansContracts.txt - Grain interface '{0}' does not have an active declaration in OrleansContracts.txt. Apply 'Regenerate OrleansContracts.txt' at project or solution scope, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. + Grain interface '{0}' does not have an active declaration in OrleansContracts.txt. Run the regeneration command in the file header for the owning project or solution, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. All grain interfaces should have an active declaration in OrleansContracts.txt to ensure version compatibility during rolling upgrades. @@ -227,7 +227,7 @@ Grain interface version mismatch - Grain interface '{0}' has [Version({1})] in OrleansContracts.txt but [Version({2})] in code. Apply 'Regenerate OrleansContracts.txt' at project or solution scope, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. + Grain interface '{0}' has [Version({1})] in OrleansContracts.txt but [Version({2})] in code. Run the regeneration command in the file header for the owning project or solution, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. The [Version] attribute on the grain interface must match the version declared in OrleansContracts.txt. @@ -236,7 +236,7 @@ Grain interface member not declared in OrleansContracts.txt - Grain interface member '{0}' is not declared in OrleansContracts.txt for interface '{1}'. Apply 'Regenerate OrleansContracts.txt' at project or solution scope, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. + Grain interface member '{0}' is not declared in OrleansContracts.txt for interface '{1}'. Run the regeneration command in the file header for the owning project or solution, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. When adding or modifying grain interface members, update OrleansContracts.txt and increment the interface version. @@ -254,7 +254,7 @@ Removed grain interface not marked as retired - Grain interface '{0}' is declared in OrleansContracts.txt but no longer exists in code. Apply 'Regenerate OrleansContracts.txt' at project or solution scope to preserve it as *RETIRED*, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. + Grain interface '{0}' is declared in OrleansContracts.txt but no longer exists in code. Run the regeneration command in the file header for the owning project or solution to preserve it as *RETIRED*, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. When removing a grain interface, mark it as *RETIRED* in OrleansContracts.txt to document that it has been intentionally removed. @@ -263,7 +263,7 @@ OrleansContracts.txt file is missing - The project contains Orleans contracts but no {0} file. Apply 'Regenerate OrleansContracts.txt' at project or solution scope to create it, then review the generated baseline. See https://aka.ms/orleans/OrleansContracts.txt for details. + The project contains Orleans contracts but no {0} file. From the repository root, replace PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path and run 'dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024'. Review the generated baseline. See https://aka.ms/orleans/OrleansContracts.txt for details. Add an OrleansContracts.txt file to track Orleans contracts for compatibility during rolling upgrades. @@ -290,7 +290,7 @@ Grain class is not active in OrleansContracts.txt - Grain class '{0}' does not have an active declaration in OrleansContracts.txt. Apply 'Regenerate OrleansContracts.txt' at project or solution scope, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. + Grain class '{0}' does not have an active declaration in OrleansContracts.txt. Run the regeneration command in the file header for the owning project or solution, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. All concrete grain classes should have an active declaration in OrleansContracts.txt to protect grain type identities during renames. @@ -299,7 +299,7 @@ Grain class alias mismatch - Grain class '{0}' has alias '{1}' in OrleansContracts.txt but alias '{2}' in code. Apply 'Regenerate OrleansContracts.txt' at project or solution scope, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. + Grain class '{0}' has alias '{1}' in OrleansContracts.txt but alias '{2}' in code. Run the regeneration command in the file header for the owning project or solution, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. The [GrainType] attribute on a grain class must match the alias declared in OrleansContracts.txt. @@ -308,7 +308,7 @@ Removed grain class not marked as retired - Grain class '{0}' is declared in OrleansContracts.txt but no longer exists in code. Apply 'Regenerate OrleansContracts.txt' at project or solution scope to preserve it as *RETIRED*, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. + Grain class '{0}' is declared in OrleansContracts.txt but no longer exists in code. Run the regeneration command in the file header for the owning project or solution to preserve it as *RETIRED*, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. When removing a grain class, mark it as *RETIRED* in OrleansContracts.txt to preserve its contract history. diff --git a/src/Orleans.BroadcastChannel/OrleansContracts.txt b/src/Orleans.BroadcastChannel/OrleansContracts.txt index 8bfabca4a1d..6e13c054a45 100644 --- a/src/Orleans.BroadcastChannel/OrleansContracts.txt +++ b/src/Orleans.BroadcastChannel/OrleansContracts.txt @@ -1,5 +1,8 @@ # This file is generated by the Orleans contract analyzer. -# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt diff --git a/src/Orleans.Core.Abstractions/OrleansContracts.txt b/src/Orleans.Core.Abstractions/OrleansContracts.txt index f2b8c4d77b7..e7de975aca3 100644 --- a/src/Orleans.Core.Abstractions/OrleansContracts.txt +++ b/src/Orleans.Core.Abstractions/OrleansContracts.txt @@ -1,5 +1,8 @@ # This file is generated by the Orleans contract analyzer. -# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt diff --git a/src/Orleans.Core/OrleansContracts.txt b/src/Orleans.Core/OrleansContracts.txt index 495d4ac9f5c..2f2359b38f1 100644 --- a/src/Orleans.Core/OrleansContracts.txt +++ b/src/Orleans.Core/OrleansContracts.txt @@ -1,5 +1,8 @@ # This file is generated by the Orleans contract analyzer. -# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt diff --git a/src/Orleans.DurableJobs/OrleansContracts.txt b/src/Orleans.DurableJobs/OrleansContracts.txt index 5116bf1e6f6..406b1050fcb 100644 --- a/src/Orleans.DurableJobs/OrleansContracts.txt +++ b/src/Orleans.DurableJobs/OrleansContracts.txt @@ -1,5 +1,8 @@ # This file is generated by the Orleans contract analyzer. -# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt diff --git a/src/Orleans.EventSourcing/OrleansContracts.txt b/src/Orleans.EventSourcing/OrleansContracts.txt index caac3e3d249..edc9974d040 100644 --- a/src/Orleans.EventSourcing/OrleansContracts.txt +++ b/src/Orleans.EventSourcing/OrleansContracts.txt @@ -1,5 +1,8 @@ # This file is generated by the Orleans contract analyzer. -# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt diff --git a/src/Orleans.Persistence.Memory/OrleansContracts.txt b/src/Orleans.Persistence.Memory/OrleansContracts.txt index 7595d0d02a9..15107fd95dd 100644 --- a/src/Orleans.Persistence.Memory/OrleansContracts.txt +++ b/src/Orleans.Persistence.Memory/OrleansContracts.txt @@ -1,5 +1,8 @@ # This file is generated by the Orleans contract analyzer. -# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt diff --git a/src/Orleans.Reminders/OrleansContracts.txt b/src/Orleans.Reminders/OrleansContracts.txt index fe3eb45d3b1..65d9e325ed1 100644 --- a/src/Orleans.Reminders/OrleansContracts.txt +++ b/src/Orleans.Reminders/OrleansContracts.txt @@ -1,5 +1,8 @@ # This file is generated by the Orleans contract analyzer. -# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt diff --git a/src/Orleans.Runtime/OrleansContracts.txt b/src/Orleans.Runtime/OrleansContracts.txt index 77b8a9425c0..0d783a0a11d 100644 --- a/src/Orleans.Runtime/OrleansContracts.txt +++ b/src/Orleans.Runtime/OrleansContracts.txt @@ -1,5 +1,8 @@ # This file is generated by the Orleans contract analyzer. -# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt diff --git a/src/Orleans.Streaming/OrleansContracts.txt b/src/Orleans.Streaming/OrleansContracts.txt index 4c25a8903c3..4bd4f23650a 100644 --- a/src/Orleans.Streaming/OrleansContracts.txt +++ b/src/Orleans.Streaming/OrleansContracts.txt @@ -1,5 +1,8 @@ # This file is generated by the Orleans contract analyzer. -# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt diff --git a/src/Orleans.TestingHost/OrleansContracts.txt b/src/Orleans.TestingHost/OrleansContracts.txt index e755b9e9870..53d2dece783 100644 --- a/src/Orleans.TestingHost/OrleansContracts.txt +++ b/src/Orleans.TestingHost/OrleansContracts.txt @@ -1,5 +1,8 @@ # This file is generated by the Orleans contract analyzer. -# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt diff --git a/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt b/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt index 73ed8eecb8b..f1e5f8b767f 100644 --- a/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt +++ b/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt @@ -1,5 +1,8 @@ # This file is generated by the Orleans contract analyzer. -# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt diff --git a/src/Orleans.Transactions/OrleansContracts.txt b/src/Orleans.Transactions/OrleansContracts.txt index 5ffb83a37d5..8dcf7f8ca4d 100644 --- a/src/Orleans.Transactions/OrleansContracts.txt +++ b/src/Orleans.Transactions/OrleansContracts.txt @@ -1,5 +1,8 @@ # This file is generated by the Orleans contract analyzer. -# Regenerate it by applying "Regenerate OrleansContracts.txt" at project or solution scope. +# To regenerate, run this command from the repository root after replacing +# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: +# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 +# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt diff --git a/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs b/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs index f9646c75c81..c11e4cd2cdf 100644 --- a/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs +++ b/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs @@ -27,7 +27,10 @@ public class GrainInterfaceVersionAnalyzerTest private const string RegenerateCodeActionEquivalenceKey = "RegenerateOrleansContractsFileAsync"; private const string GeneratedHeader = "# This file is generated by the Orleans contract analyzer.\n" + - "# Regenerate it by applying \"Regenerate OrleansContracts.txt\" at project or solution scope.\n" + + "# To regenerate, run this command from the repository root after replacing\n" + + "# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path:\n" + + "# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024\n" + + "# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION\n" + "# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades.\n" + "# Details: https://aka.ms/orleans/OrleansContracts.txt\n\n"; @@ -692,7 +695,7 @@ public interface IMyGrain : IGrain var diagnostic = Assert.Single(diagnostics, d => d.Id == GrainInterfaceVersionAnalyzer.RuleId0020); Assert.Contains(OrleansContractsFileName, diagnostic.GetMessage()); Assert.True(diagnostic.Location.IsInSource); - Assert.Contains(RegenerateCodeActionTitle, diagnostic.GetMessage()); + Assert.Contains("dotnet format PATH_TO_PROJECT_OR_SOLUTION", diagnostic.GetMessage()); Assert.Contains("https://aka.ms/orleans/OrleansContracts.txt", diagnostic.GetMessage()); } From 5abd0bd343a2bc10e5cb2b65c0a6c80467bb848a Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Thu, 27 Aug 2026 23:52:36 -0700 Subject: [PATCH 05/11] docs(analyzers): clarify regeneration diagnostics --- .../grain-versioning/contract-compatibility-analyzer.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md b/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md index 50805d91e7c..36ab5948e7b 100644 --- a/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md +++ b/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md @@ -42,7 +42,9 @@ The regeneration code fix rebuilds every active interface, method, and grain-cla ### Regenerate the manifest -Apply **Regenerate OrleansContracts.txt** from any Orleans contract diagnostic. One application regenerates the entire project manifest. In an IDE, use **Fix all in project** or **Fix all in solution** to regenerate every affected project. +Apply **Regenerate OrleansContracts.txt** from `ORLEANS0016`, `ORLEANS0017`, `ORLEANS0018`, `ORLEANS0019`, `ORLEANS0020`, `ORLEANS0022`, `ORLEANS0023`, or `ORLEANS0024`. One application regenerates the entire project manifest. In an IDE, use **Fix all in project** or **Fix all in solution** to regenerate every affected project. + +`ORLEANS0027` intentionally retains a removed method signature, so regeneration isn't offered for that diagnostic. If it is the only remaining diagnostic, restore the source method or explicitly delete the retained signature after reviewing and accepting the wire-compatibility break. Agents and command-line workflows can regenerate manifests without an IDE: From 57fe208f58c3ff694cb916be22f109b58b045b1f Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Fri, 28 Aug 2026 01:06:26 -0700 Subject: [PATCH 06/11] fix(analyzers): validate regeneration diagnostics --- .../GrainInterfaceVersionCodeFix.cs | 41 ++++++++++++++++--- .../GrainInterfaceVersionAnalyzerTest.cs | 10 ++--- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs b/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs index 5928c633cbc..1176226c8c9 100644 --- a/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs +++ b/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs @@ -53,12 +53,16 @@ public class GrainInterfaceVersionCodeFix : CodeFixProvider public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) { - context.RegisterCodeFix( - CodeAction.Create( - title: RegenerateCodeActionTitle, - createChangedSolution: cancellationToken => RegenerateOrleansContractsFileAsync(context.Document.Project, cancellationToken), - equivalenceKey: RegenerateCodeActionEquivalenceKey), - context.Diagnostics); + var validDiagnostics = context.Diagnostics.Where(HasRequiredProperties).ToImmutableArray(); + if (!validDiagnostics.IsEmpty) + { + context.RegisterCodeFix( + CodeAction.Create( + title: RegenerateCodeActionTitle, + createChangedSolution: cancellationToken => RegenerateOrleansContractsFileAsync(context.Document.Project, cancellationToken), + equivalenceKey: RegenerateCodeActionEquivalenceKey), + validDiagnostics); + } foreach (var diagnostic in context.Diagnostics) { @@ -91,6 +95,31 @@ public sealed override Task RegisterCodeFixesAsync(CodeFixContext context) return Task.CompletedTask; } + private static bool HasRequiredProperties(Diagnostic diagnostic) + => diagnostic.Id switch + { + GrainInterfaceVersionAnalyzer.RuleId0016 + => HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.InterfaceNamePropertyKey), + GrainInterfaceVersionAnalyzer.RuleId0017 + => HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.InterfaceNamePropertyKey) + && HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.ActualVersionPropertyKey), + GrainInterfaceVersionAnalyzer.RuleId0018 + => HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.InterfaceNamePropertyKey) + && HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.MemberNamePropertyKey), + GrainInterfaceVersionAnalyzer.RuleId0019 + => HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.InterfaceNamePropertyKey), + GrainInterfaceVersionAnalyzer.RuleId0020 => true, + GrainInterfaceVersionAnalyzer.RuleId0022 + or GrainInterfaceVersionAnalyzer.RuleId0023 + or GrainInterfaceVersionAnalyzer.RuleId0024 + => HasProperty(diagnostic, GrainInterfaceVersionAnalyzer.ClassNamePropertyKey), + _ => false + }; + + private static bool HasProperty(Diagnostic diagnostic, string propertyName) + => diagnostic.Properties.TryGetValue(propertyName, out var value) + && !string.IsNullOrEmpty(value); + private static async Task RegenerateOrleansContractsFileAsync( Project project, CancellationToken cancellationToken) diff --git a/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs b/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs index c11e4cd2cdf..ec2930a01f9 100644 --- a/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs +++ b/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs @@ -2743,10 +2743,8 @@ public interface IMyGrain : IGrain } "; const string contractsFile = "# OrleansContracts.txt\r\nIMyGrain [Version(1)]\r\nIMyGrain.DoSomething() -> Task\r\n"; - const string expectedContractsFile = - "# This file is auto-generated by the Orleans contract analyzer.\r\n" + - "# Update source contracts, then regenerate this file by following:\r\n" + - "# https://aka.ms/orleans/OrleansContracts.txt\r\n\r\n" + + var expectedContractsFile = + GeneratedHeader.Replace("\n", "\r\n", StringComparison.Ordinal) + "interface IMyGrain [Version(2)]\r\n" + " DoSomething() -> Task\r\n"; var properties = ImmutableDictionary.Empty @@ -2760,7 +2758,9 @@ public interface IMyGrain : IGrain var codeFixer = new GrainInterfaceVersionCodeFix(); await codeFixer.RegisterCodeFixesAsync(context); - var action = Assert.Single(actions); + var action = Assert.Single( + actions, + action => !string.Equals(action.Title, RegenerateCodeActionTitle, StringComparison.Ordinal)); var operations = await action.GetOperationsAsync(TestContext.Current.CancellationToken); var changedSolution = Assert.Single(operations.OfType()).ChangedSolution; var changedProject = changedSolution.GetProject(context.Document.Project.Id); From 46b048a12f932708b7b991d06a5912d55a955c04 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Fri, 28 Aug 2026 02:27:32 -0700 Subject: [PATCH 07/11] docs(analyzers): fix ORLEANS0027 suppression scope --- docs/site/src/content/docs/diagnostics/orleans0027.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/site/src/content/docs/diagnostics/orleans0027.md b/docs/site/src/content/docs/diagnostics/orleans0027.md index ad7fcbc036b..928f7c159ac 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0027.md +++ b/docs/site/src/content/docs/diagnostics/orleans0027.md @@ -32,6 +32,6 @@ See [Orleans contract compatibility analyzer](../grains/grain-versioning/contrac Prefer updating the reviewed manifest after accepting the contract removal. Suppress only for a manifest intentionally shared with another compilation. ```ini -[*.cs] +[*] dotnet_diagnostic.ORLEANS0027.severity = none ``` From 7fbd887760061bcd68fb32c3df4dcd014237cc5e Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Fri, 28 Aug 2026 03:21:27 -0700 Subject: [PATCH 08/11] fix(analyzers): find configured contracts manifest --- .../GrainInterfaceVersionCodeFix.cs | 21 ++++++++++++ .../GrainInterfaceVersionAnalyzerTest.cs | 33 +++++++++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs b/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs index 1176226c8c9..75992a4d648 100644 --- a/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs +++ b/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs @@ -197,6 +197,14 @@ private static async Task RegenerateOrleansContractsFileAsync( private static TextDocument? FindContractsDocument(Project project) { + var configuredPath = GetConfiguredContractsPath(project); + var configuredDocument = project.AdditionalDocuments.FirstOrDefault(document => + PathsEqual(document.FilePath ?? document.Name, configuredPath)); + if (configuredDocument is not null) + { + return configuredDocument; + } + foreach (var additionalFile in project.AnalyzerOptions.AdditionalFiles) { if (!Path.GetFileName(additionalFile.Path) @@ -221,6 +229,19 @@ private static async Task RegenerateOrleansContractsFileAsync( .Equals(Constants.OrleansContractsFileName, StringComparison.OrdinalIgnoreCase)); } + private static bool PathsEqual(string left, string right) + { + left = NormalizePathSeparators(left); + right = NormalizePathSeparators(right); + if (Path.IsPathRooted(left) && Path.IsPathRooted(right)) + { + left = Path.GetFullPath(left); + right = Path.GetFullPath(right); + } + + return string.Equals(left, right, StringComparison.OrdinalIgnoreCase); + } + private static string GetConfiguredContractsPath(Project project) { if (project.AnalyzerOptions.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue( diff --git a/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs b/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs index ec2930a01f9..ff2e3ea528c 100644 --- a/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs +++ b/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs @@ -1274,8 +1274,16 @@ private static Project CreateProjectWithAdditionalFilesForCodeFix( // Add additional document if content is provided if (grainInterfacesFileContent is not null) { - var additionalDocumentId = DocumentId.CreateNewId(projectId, OrleansContractsFileName); - solution = solution.AddAdditionalDocument(additionalDocumentId, OrleansContractsFileName, SourceText.From(grainInterfacesFileContent)); + var contractsDocumentPath = configuredContractsPath is null + ? OrleansContractsFileName + : configuredContractsPath.Replace('/', Path.DirectorySeparatorChar); + var contractsDocumentName = Path.GetFileName(contractsDocumentPath); + var additionalDocumentId = DocumentId.CreateNewId(projectId, contractsDocumentName); + solution = solution.AddAdditionalDocument( + additionalDocumentId, + contractsDocumentName, + SourceText.From(grainInterfacesFileContent), + filePath: contractsDocumentPath); } if (configuredContractsPath is not null) @@ -1364,6 +1372,27 @@ public async Task CodeFix_RegenerateMissingFile_UsesFilenameOnlyConfiguredManife Assert.Equal(configuredPath, changedSolution.GetAdditionalDocument(additionalDocumentId!)!.FilePath); } + [Fact] + public async Task CodeFix_RegenerateExistingFile_UsesConfiguredCustomFilename() + { + var configuredPath = Path.Combine(Path.GetTempPath(), "contracts", "rpc-contracts.txt"); + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + "public interface IMyGrain : IGrain { Task Ping(); Task Pong(); }", + "interface IMyGrain [Version(0)]\n Ping() -> Task\n", + GrainInterfaceVersionAnalyzer.RuleId0018, + RegenerateCodeActionTitle, + configuredPath); + + Assert.NotNull(additionalDocumentId); + var project = changedSolution.GetProject(changedSolution.ProjectIds.Single())!; + var document = Assert.Single(project.AdditionalDocuments); + Assert.Equal(configuredPath, document.FilePath); + Assert.Equal("rpc-contracts.txt", document.Name); + Assert.Contains( + "Pong() -> Task", + (await document.GetTextAsync(TestContext.Current.CancellationToken)).ToString()); + } + [Fact] public async Task CodeFix_RegenerateProject_PreservesContractHistory() { From dc1a6c9755f11086e3af7fa15fc9e80ce60e6bdb Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Fri, 28 Aug 2026 08:26:51 -0700 Subject: [PATCH 09/11] fix(analyzers): record runtime method identities --- .../content/docs/diagnostics/orleans0018.md | 4 +- .../content/docs/diagnostics/orleans0027.md | 2 +- .../contract-compatibility-analyzer.md | 16 +- .../Orleans.Dashboard/OrleansContracts.txt | 38 +-- .../GrainInterfaceVersionAnalyzer.cs | 64 ++++- .../GrainInterfaceVersionCodeFix.cs | 150 ++++++++---- .../Orleans.Analyzers.csproj | 5 + src/Orleans.Analyzers/Resources.resx | 2 +- .../OrleansContracts.txt | 8 +- .../MethodIdProvider.cs | 62 +++++ .../GeneratedCodeUtilities.cs | 59 +---- .../Orleans.CodeGenerator.csproj | 1 + .../OrleansContracts.txt | 26 ++- src/Orleans.Core/OrleansContracts.txt | 153 +++++++----- src/Orleans.DurableJobs/OrleansContracts.txt | 8 +- .../OrleansContracts.txt | 14 +- .../OrleansContracts.txt | 2 + src/Orleans.Reminders/OrleansContracts.txt | 41 ++-- src/Orleans.Runtime/OrleansContracts.txt | 83 ++++--- src/Orleans.Streaming/OrleansContracts.txt | 89 ++++--- src/Orleans.TestingHost/OrleansContracts.txt | 20 +- .../OrleansContracts.txt | 122 ++++++---- src/Orleans.Transactions/OrleansContracts.txt | 26 ++- .../GrainInterfaceVersionAnalyzerTest.cs | 218 +++++++++++++++--- 24 files changed, 852 insertions(+), 361 deletions(-) create mode 100644 src/Orleans.CodeGenerator.Shared/MethodIdProvider.cs diff --git a/docs/site/src/content/docs/diagnostics/orleans0018.md b/docs/site/src/content/docs/diagnostics/orleans0018.md index 6b06d3cad2e..0ff2be41704 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0018.md +++ b/docs/site/src/content/docs/diagnostics/orleans0018.md @@ -17,13 +17,15 @@ ms.topic: reference An ordinary grain-interface method has no matching contract signature in `OrleansContracts.txt`. Method identity, generic arity, parameter types and order, and return type are part of the signature. Parameter names are not. +The method identity is the source `[Id]` value, the source `[Alias]` value, or the generated xxHash32 ID used by the Orleans code generator. Recording a generated ID in the manifest does not add an attribute or change the runtime identity. + ## Impact Older activations can receive an unknown RPC, and changed identities or payload types can cause dispatch or serialization failures during a rolling upgrade. ## How to fix -Prefer preserving the existing method and adding a new method for changed behavior. Review payload compatibility, increment the interface version when appropriate, and apply **Add to OrleansContracts.txt**. The code fix records the new signature but does not increment `[Version]`. +Prefer preserving the existing method and adding a new method for changed behavior. Review payload compatibility, increment the interface version when appropriate, and apply **Add to OrleansContracts.txt**. The code fix records the existing effective wire identity and does not increment `[Version]` or modify source attributes. Apply **Regenerate OrleansContracts.txt** to rebuild the complete project manifest, or use **Fix all in solution** to update every affected project. Review the generated diff using the [contract compatibility guidance](../grains/grain-versioning/contract-compatibility-analyzer.md#regenerate-the-manifest). diff --git a/docs/site/src/content/docs/diagnostics/orleans0027.md b/docs/site/src/content/docs/diagnostics/orleans0027.md index 928f7c159ac..94c1a4e8771 100644 --- a/docs/site/src/content/docs/diagnostics/orleans0027.md +++ b/docs/site/src/content/docs/diagnostics/orleans0027.md @@ -15,7 +15,7 @@ ms.topic: reference ## Cause -`OrleansContracts.txt` declares an RPC method signature which is absent from the matching source grain interface. +`OrleansContracts.txt` declares an RPC method signature which is absent from the matching source grain interface. The manifest identity is an explicit `[Id]` or `[Alias]` value when present in source; otherwise, it is the generated method ID already used by Orleans on the wire. ## Impact diff --git a/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md b/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md index 36ab5948e7b..a8844a01b21 100644 --- a/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md +++ b/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md @@ -54,6 +54,10 @@ dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostic Run the command from the repository root. Replace `PATH_TO_PROJECT_OR_SOLUTION` with the path to the owning `.csproj` to regenerate one manifest, or a `.sln`/`.slnx` path to regenerate manifests in every affected project. The `--severity info` option includes `ORLEANS0020`, allowing the command to create a missing manifest. +Regeneration edits `OrleansContracts.txt` files only. It does not add or change `[Alias]`, `[Id]`, `[GrainType]`, or `[GrainInterfaceType]` attributes in source. Attribute-like syntax in the manifest records the effective identity which Orleans already uses at runtime. + +For a method without `[Id]` or `[Alias]`, the manifest records the same generated xxHash32 method ID which the Orleans code generator already uses on the wire. The preceding comment records the CLR signature so reviewers can map the wire ID back to source. A one-time upgrade from an older manifest format can therefore replace a CLR method name with its existing generated ID; this records the current wire contract and does not change it. + After the command completes: 1. Inspect `git diff -- "*OrleansContracts.txt"` and account for every changed identity, version, and method signature. @@ -80,12 +84,16 @@ Interface methods are indented beneath their interface: # PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Contoso.Grains.ICartGrain")] Contoso.Grains.ICartGrain [Version(1)] - AddAsync(Contoso.Grains.Item) -> Task - GetAsync() -> Task + # Contoso.Grains.ICartGrain.AddAsync(Item item) -> Task + 15793847(Contoso.Grains.Item) -> Task + # Contoso.Grains.ICartGrain.GetAsync() -> Task + 857AC6B2() -> Task class [GrainType("cart")] Contoso.Grains.CartGrain ``` @@ -98,13 +106,13 @@ Explicit identities remain visible alongside their CLR names: # Contoso.Grains.ICartGrain interface [GrainInterfaceType("cart")] Contoso.Grains.ICartGrain [Version(1)] # Contoso.Grains.ICartGrain.AddAsync(Item item) -> Task - add(Contoso.Grains.Item) -> Task +[Alias("add")] add(Contoso.Grains.Item) -> Task # Contoso.Grains.CartGrain class [GrainType("cart")] Contoso.Grains.CartGrain ``` -Comments record CLR names only when they differ from the stable identity. Comments are informational and aren't part of contract matching. +`[Alias("...")]` on a manifest method records that the identity comes from a source `[Alias]` attribute. An unmarked eight-digit hexadecimal method identity is the generated wire ID. Comments record CLR names when they improve traceability; comments are informational and aren't part of contract matching. `*RETIRED*` marks an intentionally removed contract: diff --git a/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt b/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt index 8b3f675387c..181ecb811a4 100644 --- a/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt +++ b/src/Dashboard/Orleans.Dashboard/OrleansContracts.txt @@ -3,34 +3,36 @@ # PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.Dashboard.Core.IDashboardGrain")] Orleans.Dashboard.Core.IDashboardGrain [Version(0)] - GetClusterTracing() -> Task>> - GetCounters(string[]) -> Task> - GetGrainState(string?, string?) -> Task> - GetGrainTracing(string) -> Task>>> - GetGrainTypes(string[]) -> Task> - GetSiloTracing(string) -> Task>> - InitializeAsync() -> Task - SubmitTracing(string, Orleans.Concurrency.Immutable) -> Task - TopGrainMethods(int, string[]) -> Task>> + [Alias("GetClusterTracing")] GetClusterTracing() -> Task>> + [Alias("GetCounters")] GetCounters(string[]) -> Task> + [Alias("GetGrainState")] GetGrainState(string?, string?) -> Task> + [Alias("GetGrainTracing")] GetGrainTracing(string) -> Task>>> + [Alias("GetGrainTypes")] GetGrainTypes(string[]) -> Task> + [Alias("GetSiloTracing")] GetSiloTracing(string) -> Task>> + [Alias("InitializeAsync")] InitializeAsync() -> Task + [Alias("SubmitTracing")] SubmitTracing(string, Orleans.Concurrency.Immutable) -> Task + [Alias("TopGrainMethods")] TopGrainMethods(int, string[]) -> Task>> interface [GrainInterfaceType("Orleans.Dashboard.Core.IDashboardRemindersGrain")] Orleans.Dashboard.Core.IDashboardRemindersGrain [Version(0)] - GetReminders(int, int) -> Task> + [Alias("GetReminders")] GetReminders(int, int) -> Task> interface [GrainInterfaceType("Orleans.Dashboard.Core.ISiloGrainProxy")] Orleans.Dashboard.Core.ISiloGrainProxy [Version(0)] - GetMetadata() -> Task>> + [Alias("GetMetadata")] GetMetadata() -> Task>> interface [GrainInterfaceType("Orleans.Dashboard.Core.ISiloGrainService")] Orleans.Dashboard.Core.ISiloGrainService [Version(0)] - Enable(bool) -> Task - GetCounters() -> Task> - GetExtendedProperties() -> Task>> - GetLifecycleStages() -> Task> - GetRuntimeStatistics() -> Task> - ReportCounters(Orleans.Concurrency.Immutable) -> Task - SetVersion(string, string) -> Task + [Alias("Enable")] Enable(bool) -> Task + [Alias("GetCounters")] GetCounters() -> Task> + [Alias("GetExtendedProperties")] GetExtendedProperties() -> Task>> + [Alias("GetLifecycleStages")] GetLifecycleStages() -> Task> + [Alias("GetRuntimeStatistics")] GetRuntimeStatistics() -> Task> + [Alias("ReportCounters")] ReportCounters(Orleans.Concurrency.Immutable) -> Task + [Alias("SetVersion")] SetVersion(string, string) -> Task class [GrainType("dashboard")] Orleans.Dashboard.Implementation.Grains.DashboardGrain diff --git a/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs b/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs index 1493be7e35e..888ed990198 100644 --- a/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs +++ b/src/Orleans.Analyzers/GrainInterfaceVersionAnalyzer.cs @@ -3,6 +3,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Text; +using Orleans.CodeGenerator; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -402,7 +403,8 @@ explicitGrainInterfaceType is null location, properties, memberSignature, - interfaceName)); + interfaceName, + GetClrMethodSignature(member))); } } } @@ -412,7 +414,7 @@ explicitGrainInterfaceType is null foreach (var declaredMember in declaredInterface.Members.Values) { if (sourceMembers.Any(member => - GrainInterfaceVersionAnalyzer.IsMatchingMember( + GrainInterfaceVersionAnalyzer.IsMatchingHistoricalMember( declaredInterface.Name, declaredMember.Signature, declaredMember.Alias, @@ -807,10 +809,15 @@ internal static string GetDefaultGrainInterfaceType(string typeName) internal static string GetMethodSignature(IMethodSymbol method) { - var sb = new StringBuilder(); var methodId = GetAttributeValue(method, Constants.IdAttributeFullyQualifiedName); var methodAlias = GetStringAttributeValue(method, Constants.AliasAttributeFullyQualifiedName); - sb.Append(methodId ?? methodAlias ?? method.Name); + return GetMethodSignature(method, methodId ?? methodAlias ?? MethodIdProvider.Create(method)); + } + + private static string GetMethodSignature(IMethodSymbol method, object methodId) + { + var sb = new StringBuilder(); + sb.Append(methodId); if (method.Arity > 0) { sb.Append('`'); @@ -862,6 +869,11 @@ internal static bool RequiresClrComment(IMethodSymbol method) { var methodId = GetAttributeValue(method, Constants.IdAttributeFullyQualifiedName)?.ToString(); var methodAlias = GetStringAttributeValue(method, Constants.AliasAttributeFullyQualifiedName); + if (methodId is null && methodAlias is null) + { + return true; + } + if (methodId is not null && !string.Equals(methodId, method.Name, StringComparison.Ordinal) || methodAlias is not null && !string.Equals(methodAlias, method.Name, StringComparison.Ordinal)) { @@ -907,11 +919,13 @@ internal static bool IsMatchingMember( IMethodSymbol member) { var memberSignature = GetMethodSignature(member); + var sourceMethodId = GetAttributeValue(member, Constants.IdAttributeFullyQualifiedName); + var sourceMethodAlias = GetStringAttributeValue(member, Constants.AliasAttributeFullyQualifiedName); if (storedAlias is not null) { if (!string.Equals( storedAlias, - GetStringAttributeValue(member, Constants.AliasAttributeFullyQualifiedName), + sourceMethodAlias, StringComparison.Ordinal)) { return false; @@ -945,9 +959,25 @@ internal static bool IsMatchingMember( StringComparison.Ordinal); } + var normalizedStoredSignature = NormalizeStoredMemberSignature(storedSignature, declaredInterfaceName); + if (sourceMethodAlias is not null) + { + return false; + } + + if (sourceMethodId is not null) + { + return string.Equals(normalizedStoredSignature, memberSignature, StringComparison.Ordinal); + } + + if (string.Equals(normalizedStoredSignature, memberSignature, StringComparison.Ordinal)) + { + return true; + } + if (string.Equals( - NormalizeStoredMemberSignature(storedSignature, declaredInterfaceName), - memberSignature, + normalizedStoredSignature, + GetMethodSignature(member, member.Name), StringComparison.Ordinal)) { return true; @@ -964,6 +994,26 @@ internal static bool IsMatchingMember( StringComparison.Ordinal); } + internal static bool IsMatchingHistoricalMember( + string declaredInterfaceName, + string storedSignature, + string? storedAlias, + IMethodSymbol member) + { + if (IsMatchingMember(declaredInterfaceName, storedSignature, storedAlias, member)) + { + return true; + } + + var sourceMethodAlias = GetStringAttributeValue(member, Constants.AliasAttributeFullyQualifiedName); + return storedAlias is null + && sourceMethodAlias is not null + && string.Equals( + NormalizeStoredMemberSignature(storedSignature, declaredInterfaceName), + GetMethodSignature(member), + StringComparison.Ordinal); + } + private static string GetNormalizedSignatureSuffix(string signature) { var parameterListStart = signature.IndexOf('('); diff --git a/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs b/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs index 75992a4d648..e31560066f0 100644 --- a/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs +++ b/src/Orleans.Analyzers/GrainInterfaceVersionCodeFix.cs @@ -26,6 +26,7 @@ public class GrainInterfaceVersionCodeFix : CodeFixProvider private const string DefaultNewLine = "\n"; private const string RegenerateCodeActionTitle = "Regenerate OrleansContracts.txt"; private const string RegenerateCodeActionEquivalenceKey = nameof(RegenerateOrleansContractsFileAsync); + private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); private const string RegenerationCommand = "dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024"; private static readonly string[] GeneratedHeader = @@ -35,6 +36,8 @@ public class GrainInterfaceVersionCodeFix : CodeFixProvider "# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path:", $"# {RegenerationCommand}", "# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION", + "# The regeneration command edits this manifest only; it does not change source attributes.", + "# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash.", "# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades.", "# Details: https://aka.ms/orleans/OrleansContracts.txt" ]; @@ -144,8 +147,13 @@ private static async Task RegenerateOrleansContractsFileAsync( var activeConventionClassNames = new HashSet(StringComparer.Ordinal); var iAddressableType = compilation.GetTypeByMetadataName(Constants.IAddressibleFullyQualifiedName); var generatedCodeTrees = new Dictionary(); + var semanticModels = new Dictionary(); - foreach (var type in GetAllSourceTypes(compilation.Assembly.GlobalNamespace, generatedCodeTrees) + foreach (var type in GetAllSourceTypes( + compilation.Assembly.GlobalNamespace, + compilation, + generatedCodeTrees, + semanticModels) .OrderBy(GetFullyQualifiedName, StringComparer.Ordinal)) { if (type.TypeKind == TypeKind.Interface @@ -181,7 +189,7 @@ private static async Task RegenerateOrleansContractsFileAsync( } var content = SortContractEntries(string.Join(newLine, lines), newLine); - var newText = SourceText.From(content, Encoding.UTF8); + var newText = SourceText.From(content, Utf8NoBom); if (contractsFile is not null) { return project.Solution.WithAdditionalDocumentText(contractsFile.Id, newText); @@ -273,11 +281,17 @@ private static string NormalizePathSeparators(string path) private static IEnumerable GetAllSourceTypes( INamespaceSymbol @namespace, - Dictionary generatedCodeTrees) + Compilation compilation, + Dictionary generatedCodeTrees, + Dictionary semanticModels) { foreach (var type in @namespace.GetTypeMembers()) { - foreach (var result in GetSourceTypeAndNestedTypes(type, generatedCodeTrees)) + foreach (var result in GetSourceTypeAndNestedTypes( + type, + compilation, + generatedCodeTrees, + semanticModels)) { yield return result; } @@ -285,7 +299,11 @@ private static IEnumerable GetAllSourceTypes( foreach (var childNamespace in @namespace.GetNamespaceMembers()) { - foreach (var result in GetAllSourceTypes(childNamespace, generatedCodeTrees)) + foreach (var result in GetAllSourceTypes( + childNamespace, + compilation, + generatedCodeTrees, + semanticModels)) { yield return result; } @@ -294,10 +312,12 @@ private static IEnumerable GetAllSourceTypes( private static IEnumerable GetSourceTypeAndNestedTypes( INamedTypeSymbol type, - Dictionary generatedCodeTrees) + Compilation compilation, + Dictionary generatedCodeTrees, + Dictionary semanticModels) { if (!type.IsImplicitlyDeclared - && !IsGeneratedCode(type, generatedCodeTrees) + && !IsGeneratedCode(type, compilation, generatedCodeTrees, semanticModels) && type.Locations.Any(location => location.IsInSource)) { yield return type; @@ -305,7 +325,11 @@ private static IEnumerable GetSourceTypeAndNestedTypes( foreach (var nestedType in type.GetTypeMembers()) { - foreach (var result in GetSourceTypeAndNestedTypes(nestedType, generatedCodeTrees)) + foreach (var result in GetSourceTypeAndNestedTypes( + nestedType, + compilation, + generatedCodeTrees, + semanticModels)) { yield return result; } @@ -314,22 +338,37 @@ private static IEnumerable GetSourceTypeAndNestedTypes( private static bool IsGeneratedCode( INamedTypeSymbol type, - Dictionary generatedCodeTrees) + Compilation compilation, + Dictionary generatedCodeTrees, + Dictionary semanticModels) { - for (ISymbol? symbol = type; symbol is not null; symbol = symbol.ContainingType) + var declarations = type.DeclaringSyntaxReferences; + return !declarations.IsEmpty + && declarations.All(declaration => + IsGeneratedCode(declaration.SyntaxTree, generatedCodeTrees) + || HasGeneratedCodeAttribute(declaration.GetSyntax(), compilation, semanticModels)); + } + + private static bool HasGeneratedCodeAttribute( + SyntaxNode declaration, + Compilation compilation, + Dictionary semanticModels) + { + if (!semanticModels.TryGetValue(declaration.SyntaxTree, out var semanticModel)) { - if (symbol.GetAttributes().Any(attribute => - attribute.AttributeClass?.ToDisplayString() is - "System.CodeDom.Compiler.GeneratedCodeAttribute" - or "System.Runtime.CompilerServices.CompilerGeneratedAttribute")) - { - return true; - } + semanticModel = compilation.GetSemanticModel(declaration.SyntaxTree); + semanticModels[declaration.SyntaxTree] = semanticModel; } - return type.Locations - .Where(location => location.IsInSource && location.SourceTree is not null) - .Any(location => IsGeneratedCode(location.SourceTree!, generatedCodeTrees)); + return declaration.AncestorsAndSelf() + .OfType() + .SelectMany(type => type.AttributeLists) + .SelectMany(list => list.Attributes) + .Select(attribute => semanticModel.GetSymbolInfo(attribute).Symbol as IMethodSymbol) + .Select(constructor => constructor?.ContainingType.ToDisplayString()) + .Any(attributeType => attributeType is + "System.CodeDom.Compiler.GeneratedCodeAttribute" + or "System.Runtime.CompilerServices.CompilerGeneratedAttribute"); } private static bool IsGeneratedCode( @@ -397,7 +436,9 @@ private static void AppendInterface( lines.Add($" # {GrainInterfaceVersionAnalyzer.GetClrMethodSignature(member)}"); } - lines.Add($" {GrainInterfaceVersionAnalyzer.GetMethodSignature(member)}"); + lines.Add($" {FormatStoredMember( + GrainInterfaceVersionAnalyzer.GetMethodSignature(member), + GetAliasFromAttributes(member))}"); } } @@ -606,7 +647,7 @@ private static void MergeHistoricalMembers( .OfType() .Any(member => member.MethodKind == MethodKind.Ordinary && !member.IsStatic - && GrainInterfaceVersionAnalyzer.IsMatchingMember( + && GrainInterfaceVersionAnalyzer.IsMatchingHistoricalMember( historicalInterfaceName, memberSignature, memberAlias, @@ -627,7 +668,7 @@ private static void MergeHistoricalMembers( result.Insert(generatedBlockEnd++, $" {pendingComment}"); } - result.Insert(generatedBlockEnd++, $" {historicalMemberKey}"); + result.Insert(generatedBlockEnd++, $" {FormatStoredMember(historicalMemberKey, memberAlias)}"); } pendingComment = null; @@ -766,12 +807,20 @@ private static void RegisterAddMemberCodeFix(CodeFixContext context, Diagnostic } diagnostic.Properties.TryGetValue(GrainInterfaceVersionAnalyzer.MemberClrSignaturePropertyKey, out var memberClrSignature); + diagnostic.Properties.TryGetValue(GrainInterfaceVersionAnalyzer.MemberAliasPropertyKey, out var memberAlias); diagnostic.Properties.TryGetValue(GrainInterfaceVersionAnalyzer.GrainInterfaceTypePropertyKey, out var grainInterfaceType); context.RegisterCodeFix( CodeAction.Create( title: Resources.AddToOrleansContractsFileTitle, - createChangedSolution: ct => AddMemberToFileAsync(context.Document, interfaceName!, grainInterfaceType, memberSignature!, memberClrSignature, ct), + createChangedSolution: ct => AddMemberToFileAsync( + context.Document, + interfaceName!, + grainInterfaceType, + memberSignature!, + memberAlias, + memberClrSignature, + ct), equivalenceKey: GrainInterfaceVersionAnalyzer.RuleId0018), diagnostic); } @@ -899,7 +948,7 @@ private static async Task AddGrainClassToFileAsync( var reactivatedContent = SortContractEntries(string.Join(newLine, updatedLines), newLine); return solution.WithAdditionalDocumentText( contractsFile.Id, - Microsoft.CodeAnalysis.Text.SourceText.From(reactivatedContent, Encoding.UTF8)); + Microsoft.CodeAnalysis.Text.SourceText.From(reactivatedContent, Utf8NoBom)); } } @@ -916,7 +965,7 @@ private static async Task AddGrainClassToFileAsync( content = SortContractEntries(content + classLine, newLine); return solution.WithAdditionalDocumentText( contractsFile.Id, - Microsoft.CodeAnalysis.Text.SourceText.From(content, Encoding.UTF8)); + Microsoft.CodeAnalysis.Text.SourceText.From(content, Utf8NoBom)); } private static async Task UpdateGrainClassAliasInFileAsync( @@ -958,7 +1007,7 @@ private static async Task UpdateGrainClassAliasInFileAsync( var content = SortContractEntries(string.Join(newLine, lines), newLine); return project.Solution.WithAdditionalDocumentText( contractsFile.Id, - Microsoft.CodeAnalysis.Text.SourceText.From(content, Encoding.UTF8)); + Microsoft.CodeAnalysis.Text.SourceText.From(content, Utf8NoBom)); } private static async Task RetireGrainClassInFileAsync( @@ -996,7 +1045,7 @@ private static async Task RetireGrainClassInFileAsync( var content = SortContractEntries(string.Join(newLine, lines), newLine); return project.Solution.WithAdditionalDocumentText( contractsFile.Id, - Microsoft.CodeAnalysis.Text.SourceText.From(content, Encoding.UTF8)); + Microsoft.CodeAnalysis.Text.SourceText.From(content, Utf8NoBom)); } private static async Task AddInterfaceToFileAsync( @@ -1057,7 +1106,7 @@ private static async Task AddInterfaceToFileAsync( var interfaceLine = sb.ToString(); // Build member lines - var memberLines = new List<(string Signature, string? ClrSignature)>(); + var memberLines = new List<(string Signature, string? Alias, string? ClrSignature)>(); foreach (var member in symbol.GetMembers().OfType()) { if (member.MethodKind != MethodKind.Ordinary || member.IsStatic) @@ -1068,6 +1117,7 @@ private static async Task AddInterfaceToFileAsync( var memberSignature = GrainInterfaceVersionAnalyzer.GetMethodSignature(member); memberLines.Add(( memberSignature, + GetAliasFromAttributes(member), GrainInterfaceVersionAnalyzer.RequiresClrComment(member) ? GrainInterfaceVersionAnalyzer.GetClrMethodSignature(member) : null)); @@ -1091,7 +1141,7 @@ private static async Task AddInterfaceToFileAsync( i = SetClrCommentBefore(updatedLines, i, interfaceClrComment); updatedLines[i] = interfaceLine; var reactivatedContent = SortContractEntries(string.Join(newLine, updatedLines), newLine); - var reactivatedText = Microsoft.CodeAnalysis.Text.SourceText.From(reactivatedContent, Encoding.UTF8); + var reactivatedText = Microsoft.CodeAnalysis.Text.SourceText.From(reactivatedContent, Utf8NoBom); return solution.WithAdditionalDocumentText(grainInterfacesFile.Id, reactivatedText); } } @@ -1112,11 +1162,11 @@ private static async Task AddInterfaceToFileAsync( { newContent += $"{newLine} # {member.ClrSignature}"; } - newContent += $"{newLine} {member.Signature}"; + newContent += $"{newLine} {FormatStoredMember(member.Signature, member.Alias)}"; } newContent = SortContractEntries(newContent, newLine); - var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Encoding.UTF8); + var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Utf8NoBom); solution = solution.WithAdditionalDocumentText(grainInterfacesFile.Id, newText); } else @@ -1133,10 +1183,10 @@ private static async Task AddInterfaceToFileAsync( { AppendLine(content, $" # {member.ClrSignature}", DefaultNewLine); } - AppendLine(content, $" {member.Signature}", DefaultNewLine); + AppendLine(content, $" {FormatStoredMember(member.Signature, member.Alias)}", DefaultNewLine); } - var newText = Microsoft.CodeAnalysis.Text.SourceText.From(SortContractEntries(content.ToString(), DefaultNewLine), Encoding.UTF8); + var newText = Microsoft.CodeAnalysis.Text.SourceText.From(SortContractEntries(content.ToString(), DefaultNewLine), Utf8NoBom); var filePath = GetConfiguredContractsPath(project); solution = solution.AddAdditionalDocument( @@ -1207,7 +1257,7 @@ private static async Task UpdateVersionInFileAsync( } newContent = SortContractEntries(newContent, newLine); - var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Encoding.UTF8); + var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Utf8NoBom); return solution.WithAdditionalDocumentText(grainInterfacesFile.Id, newText); } @@ -1216,6 +1266,7 @@ private static async Task AddMemberToFileAsync( string interfaceName, string? grainInterfaceType, string memberSignature, + string? memberAlias, string? memberClrSignature, CancellationToken cancellationToken) { @@ -1252,7 +1303,7 @@ private static async Task AddMemberToFileAsync( || trimmedLine.StartsWith("#", StringComparison.Ordinal) || GrainInterfaceFileParser.TryGetContractName(trimmedLine, out _))) { - AppendMember(newLines, memberSignature, memberClrSignature, newLine); + AppendMember(newLines, memberSignature, memberAlias, memberClrSignature, newLine); insertedMember = true; } @@ -1268,7 +1319,7 @@ private static async Task AddMemberToFileAsync( // If we didn't insert the member yet, append it at the end if (foundInterface && !insertedMember) { - AppendMember(newLines, memberSignature, memberClrSignature, newLine); + AppendMember(newLines, memberSignature, memberAlias, memberClrSignature, newLine); } // Remove trailing newline added by AppendLine @@ -1279,7 +1330,7 @@ private static async Task AddMemberToFileAsync( } newContent = SortContractEntries(newContent, newLine); - var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Encoding.UTF8); + var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Utf8NoBom); return solution.WithAdditionalDocumentText(grainInterfacesFile.Id, newText); } @@ -1333,7 +1384,7 @@ private static async Task RetireInterfaceInFileAsync( } newContent = SortContractEntries(newContent, newLine); - var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Encoding.UTF8); + var newText = Microsoft.CodeAnalysis.Text.SourceText.From(newContent, Utf8NoBom); return solution.WithAdditionalDocumentText(grainInterfacesFile.Id, newText); } @@ -1468,13 +1519,20 @@ private static string SortContractEntries(string content, string newLine) } preamble.Add(line); } - else if (GrainInterfaceFileParser.TryGetMemberSignature(line, out var memberSignature)) + else if (GrainInterfaceFileParser.TryGetMemberDeclaration( + line, + out var storedMemberSignature, + out var memberAlias)) { + var memberSignature = GrainInterfaceFileParser.GetCanonicalMemberSignature( + storedMemberSignature, + memberAlias); var inlineComment = GrainInterfaceFileParser.GetClrComment(line); var normalizedMemberLine = NormalizeMemberLine(memberSignature, currentBlock.Name); currentBlock.Members.Add(( normalizedMemberLine, normalizedMemberLine, + memberAlias, pendingComment ?? (inlineComment.Length > 0 ? NormalizeComment(inlineComment) : null))); pendingComment = null; } @@ -1527,7 +1585,7 @@ private static string SortContractEntries(string content, string newLine) { result.Add($" {member.ClrComment}"); } - result.Add($" {member.Line}"); + result.Add($" {FormatStoredMember(member.Line, member.Alias)}"); } result.AddRange(block.OtherLines); } @@ -1590,6 +1648,7 @@ private static void AppendLine(StringBuilder builder, string value, string newLi private static void AppendMember( StringBuilder builder, string memberSignature, + string? memberAlias, string? memberClrSignature, string newLine) { @@ -1598,9 +1657,14 @@ private static void AppendMember( AppendLine(builder, $" # {memberClrSignature}", newLine); } - AppendLine(builder, $" {memberSignature}", newLine); + AppendLine(builder, $" {FormatStoredMember(memberSignature, memberAlias)}", newLine); } + private static string FormatStoredMember(string memberSignature, string? memberAlias) + => memberAlias is null + ? memberSignature + : $"[Alias(\"{memberAlias}\")] {memberSignature}"; + private static ushort GetVersionFromAttributes(ISymbol symbol) { foreach (var attribute in symbol.GetAttributes()) @@ -1680,7 +1744,7 @@ public ContractBlock(string name, string declaration, string? clrComment) public string? ClrComment { get; } - public List<(string Signature, string Line, string? ClrComment)> Members { get; } = new(); + public List<(string Signature, string Line, string? Alias, string? ClrComment)> Members { get; } = new(); public List OtherLines { get; } = new(); } diff --git a/src/Orleans.Analyzers/Orleans.Analyzers.csproj b/src/Orleans.Analyzers/Orleans.Analyzers.csproj index 4675a5de0d5..d68d00de2e5 100644 --- a/src/Orleans.Analyzers/Orleans.Analyzers.csproj +++ b/src/Orleans.Analyzers/Orleans.Analyzers.csproj @@ -19,6 +19,11 @@ + + + + + diff --git a/src/Orleans.Analyzers/Resources.resx b/src/Orleans.Analyzers/Resources.resx index 2ca317faa2b..6cfbcce2090 100644 --- a/src/Orleans.Analyzers/Resources.resx +++ b/src/Orleans.Analyzers/Resources.resx @@ -236,7 +236,7 @@ Grain interface member not declared in OrleansContracts.txt - Grain interface member '{0}' is not declared in OrleansContracts.txt for interface '{1}'. Run the regeneration command in the file header for the owning project or solution, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. + Grain interface source member '{2}' with wire signature '{0}' is not declared in OrleansContracts.txt for interface '{1}'. Run the regeneration command in the file header for the owning project or solution, then review the diff for wire compatibility. See https://aka.ms/orleans/OrleansContracts.txt for details. When adding or modifying grain interface members, update OrleansContracts.txt and increment the interface version. diff --git a/src/Orleans.BroadcastChannel/OrleansContracts.txt b/src/Orleans.BroadcastChannel/OrleansContracts.txt index 6e13c054a45..2df212605c9 100644 --- a/src/Orleans.BroadcastChannel/OrleansContracts.txt +++ b/src/Orleans.BroadcastChannel/OrleansContracts.txt @@ -3,9 +3,13 @@ # PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.BroadcastChannel.IBroadcastChannelConsumerExtension")] Orleans.BroadcastChannel.IBroadcastChannelConsumerExtension [Version(0)] - OnError(Orleans.BroadcastChannel.InternalChannelId, System.Exception) -> Task - OnPublished(Orleans.BroadcastChannel.InternalChannelId, object) -> Task + # Orleans.BroadcastChannel.IBroadcastChannelConsumerExtension.OnError(InternalChannelId streamId, Exception exception) -> Task + 73F72B20(Orleans.BroadcastChannel.InternalChannelId, System.Exception) -> Task + # Orleans.BroadcastChannel.IBroadcastChannelConsumerExtension.OnPublished(InternalChannelId streamId, object item) -> Task + B1E55518(Orleans.BroadcastChannel.InternalChannelId, object) -> Task diff --git a/src/Orleans.CodeGenerator.Shared/MethodIdProvider.cs b/src/Orleans.CodeGenerator.Shared/MethodIdProvider.cs new file mode 100644 index 00000000000..cd74257e6ba --- /dev/null +++ b/src/Orleans.CodeGenerator.Shared/MethodIdProvider.cs @@ -0,0 +1,62 @@ +using System.Text; +using Microsoft.CodeAnalysis; +using Orleans.CodeGenerator.Hashing; + +namespace Orleans.CodeGenerator; + +internal static class MethodIdProvider +{ + public static string Create(IMethodSymbol method) + { + var signature = Format(method); + var hash = XxHash32.Hash(Encoding.UTF8.GetBytes(signature)); + var result = new StringBuilder(hash.Length * 2); + foreach (var value in hash) + { + result.Append(value.ToString("X2")); + } + + return result.ToString(); + } + + private static string Format(IMethodSymbol method) + { + var result = new StringBuilder(); + result.Append(method.ContainingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + result.Append('.'); + result.Append(method.Name); + + if (method.IsGenericMethod) + { + result.Append('<'); + for (var index = 0; index < method.TypeArguments.Length; index++) + { + if (index > 0) + { + result.Append(','); + } + + result.Append(method.TypeArguments[index].Name); + } + + result.Append('>'); + } + + result.Append('('); + for (var index = 0; index < method.Parameters.Length; index++) + { + if (index > 0) + { + result.Append(','); + } + + var parameterType = method.Parameters[index].Type; + result.Append(parameterType is ITypeParameterSymbol + ? parameterType.Name + : parameterType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + } + + result.Append(')'); + return result.ToString(); + } +} diff --git a/src/Orleans.CodeGenerator/GeneratedCodeUtilities.cs b/src/Orleans.CodeGenerator/GeneratedCodeUtilities.cs index a7660c72e5b..6c1ee36f49e 100644 --- a/src/Orleans.CodeGenerator/GeneratedCodeUtilities.cs +++ b/src/Orleans.CodeGenerator/GeneratedCodeUtilities.cs @@ -1,7 +1,5 @@ -using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Orleans.CodeGenerator.Hashing; using Orleans.CodeGenerator.SyntaxGeneration; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; using static Orleans.CodeGenerator.SyntaxGeneration.SymbolExtensions; @@ -26,62 +24,7 @@ internal static class GeneratedCodeUtilities } internal static string CreateHashedMethodId(IMethodSymbol methodSymbol) - { - var methodSignature = Format(methodSymbol); - var hash = XxHash32.Hash(Encoding.UTF8.GetBytes(methodSignature)); - return $"{HexConverter.ToString(hash)}"; - - static string Format(IMethodSymbol methodInfo) - { - var result = new StringBuilder(); - result.Append(methodInfo.ContainingType.ToDisplayName()); - result.Append('.'); - result.Append(methodInfo.Name); - - if (methodInfo.IsGenericMethod) - { - result.Append('<'); - var first = true; - foreach (var typeArgument in methodInfo.TypeArguments) - { - if (!first) result.Append(','); - else first = false; - result.Append(typeArgument.Name); - } - - result.Append('>'); - } - - { - result.Append('('); - var parameters = methodInfo.Parameters; - var first = true; - foreach (var parameter in parameters) - { - if (!first) - { - result.Append(','); - } - - var parameterType = parameter.Type; - switch (parameterType) - { - case ITypeParameterSymbol _: - result.Append(parameterType.Name); - break; - default: - result.Append(parameterType.ToDisplayName()); - break; - } - - first = false; - } - } - - result.Append(')'); - return result.ToString(); - } - } + => MethodIdProvider.Create(methodSymbol); internal static string? GetAlias(LibraryTypes libraryTypes, ISymbol symbol) => (string?)symbol.GetAttribute(libraryTypes.AliasAttribute)?.ConstructorArguments.First().Value; diff --git a/src/Orleans.CodeGenerator/Orleans.CodeGenerator.csproj b/src/Orleans.CodeGenerator/Orleans.CodeGenerator.csproj index 6fb0ee8e325..805e0e1461f 100644 --- a/src/Orleans.CodeGenerator/Orleans.CodeGenerator.csproj +++ b/src/Orleans.CodeGenerator/Orleans.CodeGenerator.csproj @@ -30,6 +30,7 @@ + diff --git a/src/Orleans.Core.Abstractions/OrleansContracts.txt b/src/Orleans.Core.Abstractions/OrleansContracts.txt index e7de975aca3..1f7df80e614 100644 --- a/src/Orleans.Core.Abstractions/OrleansContracts.txt +++ b/src/Orleans.Core.Abstractions/OrleansContracts.txt @@ -3,12 +3,16 @@ # PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.Core.Internal.IGrainManagementExtension")] Orleans.Core.Internal.IGrainManagementExtension [Version(0)] - DeactivateOnIdle() -> ValueTask - MigrateOnIdle() -> ValueTask + # Orleans.Core.Internal.IGrainManagementExtension.DeactivateOnIdle() -> ValueTask + 1B9614D1() -> ValueTask + # Orleans.Core.Internal.IGrainManagementExtension.MigrateOnIdle() -> ValueTask + 4CC93B45() -> ValueTask interface [GrainInterfaceType("Orleans.IGrain")] Orleans.IGrain [Version(0)] @@ -27,14 +31,20 @@ interface [GrainInterfaceType("Orleans.IGrainWithStringKey")] Orleans.IGrainWith interface [GrainInterfaceType("Orleans.ISystemTarget")] Orleans.ISystemTarget [Version(0)] interface [GrainInterfaceType("Orleans.Runtime.IAsyncEnumerableGrainExtension")] Orleans.Runtime.IAsyncEnumerableGrainExtension [Version(0)] - DisposeAsync(System.Guid) -> ValueTask - MoveNext`1(System.Guid) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> - MoveNext`1(System.Guid, System.Threading.CancellationToken) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> - StartEnumeration`1(System.Guid, Orleans.Runtime.IAsyncEnumerableRequest) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> - StartEnumeration`1(System.Guid, Orleans.Runtime.IAsyncEnumerableRequest, System.Threading.CancellationToken) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> + # Orleans.Runtime.IAsyncEnumerableGrainExtension.StartEnumeration(Guid requestId, IAsyncEnumerableRequest request) -> ValueTask<(EnumerationResult Status, object? Value)> + 370CD5AB`1(System.Guid, Orleans.Runtime.IAsyncEnumerableRequest) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> + # Orleans.Runtime.IAsyncEnumerableGrainExtension.DisposeAsync(Guid requestId) -> ValueTask + 3C6D7209(System.Guid) -> ValueTask + # Orleans.Runtime.IAsyncEnumerableGrainExtension.StartEnumeration(Guid requestId, IAsyncEnumerableRequest request, CancellationToken cancellationToken) -> ValueTask<(EnumerationResult Status, object? Value)> + 8678B466`1(System.Guid, Orleans.Runtime.IAsyncEnumerableRequest, System.Threading.CancellationToken) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> + # Orleans.Runtime.IAsyncEnumerableGrainExtension.MoveNext(Guid requestId) -> ValueTask<(EnumerationResult Status, object? Value)> + A7FA7E30`1(System.Guid) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> + # Orleans.Runtime.IAsyncEnumerableGrainExtension.MoveNext(Guid requestId, CancellationToken cancellationToken) -> ValueTask<(EnumerationResult Status, object? Value)> + E60EA75B`1(System.Guid, System.Threading.CancellationToken) -> ValueTask<(Orleans.Runtime.EnumerationResult, object?)> interface [GrainInterfaceType("Orleans.Runtime.ICancellationSourcesExtension")] Orleans.Runtime.ICancellationSourcesExtension [Version(0)] - CancelRemoteToken(System.Guid) -> Task + # Orleans.Runtime.ICancellationSourcesExtension.CancelRemoteToken(Guid tokenId) -> Task + 50F75C16(System.Guid) -> Task interface [GrainInterfaceType("Orleans.Runtime.IGrainExtension")] Orleans.Runtime.IGrainExtension [Version(0)] diff --git a/src/Orleans.Core/OrleansContracts.txt b/src/Orleans.Core/OrleansContracts.txt index 2f2359b38f1..f41e5afc4d7 100644 --- a/src/Orleans.Core/OrleansContracts.txt +++ b/src/Orleans.Core/OrleansContracts.txt @@ -3,82 +3,131 @@ # PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.ClientObservers.IClientGatewayObserver")] Orleans.ClientObservers.IClientGatewayObserver [Version(0)] - StopSendingToGateway(Orleans.Runtime.SiloAddress) -> void + # Orleans.ClientObservers.IClientGatewayObserver.StopSendingToGateway(SiloAddress gateway) -> void + AFB768FD(Orleans.Runtime.SiloAddress) -> void interface [GrainInterfaceType("Orleans.IMembershipTableSystemTarget")] Orleans.IMembershipTableSystemTarget [Version(0)] interface [GrainInterfaceType("Orleans.ISiloControl")] Orleans.ISiloControl [Version(0)] - ForceActivationCollection(System.TimeSpan) -> Task - ForceGarbageCollection() -> Task - ForceRuntimeStatisticsCollection() -> Task - GetActivationCount() -> Task - GetActiveGrains(Orleans.Runtime.GrainType) -> Task> - GetDetailedGrainReport(Orleans.Runtime.GrainId) -> Task - GetDetailedGrainStatistics(string[]) -> Task> - GetGrainStatistics() -> Task>> - GetRuntimeStatistics() -> Task - GetSimpleGrainStatistics() -> Task - MigrateRandomActivations(Orleans.Runtime.SiloAddress, int) -> Task - Ping(string) -> Task - SendControlCommandToProvider`1(string, int, object?) -> Task + # Orleans.ISiloControl.ForceRuntimeStatisticsCollection() -> Task + 0C7DBD0C() -> Task + # Orleans.ISiloControl.Ping(string message) -> Task + 1422B0B7(string) -> Task + # Orleans.ISiloControl.SendControlCommandToProvider(string providerName, int command, object? arg) -> Task + 355CA3FA`1(string, int, object?) -> Task + # Orleans.ISiloControl.GetDetailedGrainReport(GrainId grainId) -> Task + 45172562(Orleans.Runtime.GrainId) -> Task + # Orleans.ISiloControl.ForceActivationCollection(TimeSpan ageLimit) -> Task + 45D07D09(System.TimeSpan) -> Task + # Orleans.ISiloControl.GetSimpleGrainStatistics() -> Task + 6DE16EF7() -> Task + # Orleans.ISiloControl.GetActiveGrains(GrainType grainType) -> Task> + 85797C87(Orleans.Runtime.GrainType) -> Task> + # Orleans.ISiloControl.GetDetailedGrainStatistics(string[]? types) -> Task> + B0F4C24B(string[]) -> Task> + # Orleans.ISiloControl.GetActivationCount() -> Task + C4C370A5() -> Task + # Orleans.ISiloControl.MigrateRandomActivations(SiloAddress target, int count) -> Task + E8327F0B(Orleans.Runtime.SiloAddress, int) -> Task + # Orleans.ISiloControl.GetRuntimeStatistics() -> Task + F18EAF24() -> Task + # Orleans.ISiloControl.ForceGarbageCollection() -> Task + F388CED1() -> Task + # Orleans.ISiloControl.GetGrainStatistics() -> Task>> + FF707A30() -> Task>> interface [GrainInterfaceType("Orleans.Placement.Rebalancing.IActivationRebalancerMonitor")] Orleans.Placement.Rebalancing.IActivationRebalancerMonitor [Version(0)] - Report(RebalancingReport) -> Task + [Alias("Report")] Report(RebalancingReport) -> Task interface [GrainInterfaceType("Orleans.Placement.Rebalancing.IActivationRebalancerWorker")] Orleans.Placement.Rebalancing.IActivationRebalancerWorker [Version(0)] - GetReport() -> ValueTask - ResumeRebalancing() -> Task - SuspendRebalancing(System.TimeSpan?) -> Task + [Alias("GetReport")] GetReport() -> ValueTask + [Alias("ResumeRebalancing")] ResumeRebalancing() -> Task + [Alias("SuspendRebalancing")] SuspendRebalancing(System.TimeSpan?) -> Task interface [GrainInterfaceType("Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget")] Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget [Version(0)] - AcceptExchangeRequest(Orleans.Placement.Repartitioning.AcceptExchangeRequest) -> ValueTask - FlushBuffers() -> ValueTask - GetActivationCount() -> ValueTask - GetGrainCallFrequencies() -> ValueTask> - ResetCounters() -> ValueTask - SetActivationCountOffset(int) -> ValueTask - TriggerExchangeRequest() -> ValueTask + # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.FlushBuffers() -> ValueTask + 11731652() -> ValueTask + # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.SetActivationCountOffset(int activationCountOffset) -> ValueTask + 135356E5(int) -> ValueTask + # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.ResetCounters() -> ValueTask + 21852A09() -> ValueTask + # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.AcceptExchangeRequest(AcceptExchangeRequest request) -> ValueTask + 9D8EDC44(Orleans.Placement.Repartitioning.AcceptExchangeRequest) -> ValueTask + # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.GetActivationCount() -> ValueTask + 9FB525F3() -> ValueTask + # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.TriggerExchangeRequest() -> ValueTask + A6EE4757() -> ValueTask + # Orleans.Placement.Repartitioning.IActivationRepartitionerSystemTarget.GetGrainCallFrequencies() -> ValueTask> + C4497899() -> ValueTask> interface [GrainInterfaceType("Orleans.Runtime.IClusterManifestSystemTarget")] Orleans.Runtime.IClusterManifestSystemTarget [Version(0)] - GetClusterManifest() -> ValueTask - GetClusterManifestUpdate(Orleans.Metadata.MajorMinorVersion) -> ValueTask + # Orleans.Runtime.IClusterManifestSystemTarget.GetClusterManifest() -> ValueTask + 40D39F85() -> ValueTask + # Orleans.Runtime.IClusterManifestSystemTarget.GetClusterManifestUpdate(MajorMinorVersion previousVersion) -> ValueTask + 4EFCA109(Orleans.Metadata.MajorMinorVersion) -> ValueTask interface [GrainInterfaceType("Orleans.Runtime.IDeploymentLoadPublisher")] Orleans.Runtime.IDeploymentLoadPublisher [Version(0)] - UpdateRuntimeStatistics(Orleans.Runtime.SiloAddress, Orleans.Runtime.SiloRuntimeStatistics) -> Task + # Orleans.Runtime.IDeploymentLoadPublisher.UpdateRuntimeStatistics(SiloAddress siloAddress, SiloRuntimeStatistics siloStats) -> Task + C5255F0C(Orleans.Runtime.SiloAddress, Orleans.Runtime.SiloRuntimeStatistics) -> Task interface [GrainInterfaceType("Orleans.Runtime.IGrainCallCancellationExtension")] Orleans.Runtime.IGrainCallCancellationExtension [Version(0)] - CancelRequestAsync(Orleans.Runtime.GrainId, Orleans.Runtime.CorrelationId) -> ValueTask + # Orleans.Runtime.IGrainCallCancellationExtension.CancelRequestAsync(GrainId senderGrainId, CorrelationId messageId) -> ValueTask + FA239824(Orleans.Runtime.GrainId, Orleans.Runtime.CorrelationId) -> ValueTask interface [GrainInterfaceType("Orleans.Runtime.IManagementGrain")] Orleans.Runtime.IManagementGrain [Version(0)] - ForceActivationCollection(Orleans.Runtime.SiloAddress[], System.TimeSpan) -> Task - ForceActivationCollection(System.TimeSpan) -> Task - ForceGarbageCollection(Orleans.Runtime.SiloAddress[]) -> Task - ForceRuntimeStatisticsCollection(Orleans.Runtime.SiloAddress[]) -> Task - GetActivationAddress(Orleans.Runtime.IAddressable) -> ValueTask - GetActiveGrains(Orleans.Runtime.GrainType) -> ValueTask> - GetDetailedGrainStatistics(string[], Orleans.Runtime.SiloAddress[]) -> Task - GetDetailedHosts(bool) -> Task + # Orleans.Runtime.IManagementGrain.GetDetailedGrainStatistics(string[]? types, SiloAddress[]? hostsIds) -> Task + 0A1C0D82(string[], Orleans.Runtime.SiloAddress[]) -> Task + # Orleans.Runtime.IManagementGrain.GetGrainCallFrequencies(SiloAddress[]? hostsIds) -> Task> + 0F06E027(Orleans.Runtime.SiloAddress[]) -> Task> + # Orleans.Runtime.IManagementGrain.GetRuntimeStatistics(SiloAddress[] hostsIds) -> Task + 2D761B36(Orleans.Runtime.SiloAddress[]) -> Task + # Orleans.Runtime.IManagementGrain.GetActivationAddress(IAddressable reference) -> ValueTask + 317D82B6(Orleans.Runtime.IAddressable) -> ValueTask + # Orleans.Runtime.IManagementGrain.ForceActivationCollection(SiloAddress[] hostsIds, TimeSpan ageLimit) -> Task + 329F9A1B(Orleans.Runtime.SiloAddress[], System.TimeSpan) -> Task + # Orleans.Runtime.IManagementGrain.GetSimpleGrainStatistics(SiloAddress[] hostsIds) -> Task + 3CFF788C(Orleans.Runtime.SiloAddress[]) -> Task + # Orleans.Runtime.IManagementGrain.GetActiveGrains(GrainType type) -> ValueTask> + 3DB7923B(Orleans.Runtime.GrainType) -> ValueTask> + # Orleans.Runtime.IManagementGrain.GetHosts(bool onlyActive) -> Task> + 4C0864C2(bool) -> Task> + # Orleans.Runtime.IManagementGrain.ForceActivationCollection(TimeSpan ageLimit) -> Task + 54E6D1D1(System.TimeSpan) -> Task + # Orleans.Runtime.IManagementGrain.ResetGrainCallFrequencies(SiloAddress[]? hostsIds) -> ValueTask + 54FE0FEC(Orleans.Runtime.SiloAddress[]) -> ValueTask + # Orleans.Runtime.IManagementGrain.ForceGarbageCollection(SiloAddress[] hostsIds) -> Task + 5922EB76(Orleans.Runtime.SiloAddress[]) -> Task + # Orleans.Runtime.IManagementGrain.GetSimpleGrainStatistics() -> Task + ACCE9D6A() -> Task # Orleans.Runtime.IManagementGrain.GetGrainActivationCount(GrainReference grainReference) -> Task - GetGrainActivationCount(GrainRef) -> Task - GetGrainCallFrequencies(Orleans.Runtime.SiloAddress[]) -> Task> - GetHosts(bool) -> Task> - GetRuntimeStatistics(Orleans.Runtime.SiloAddress[]) -> Task - GetSimpleGrainStatistics() -> Task - GetSimpleGrainStatistics(Orleans.Runtime.SiloAddress[]) -> Task - GetTotalActivationCount() -> Task - ResetGrainCallFrequencies(Orleans.Runtime.SiloAddress[]) -> ValueTask - SendControlCommandToProvider`1(string, int, object?) -> Task + AEDE93F6(GrainRef) -> Task + # Orleans.Runtime.IManagementGrain.ForceRuntimeStatisticsCollection(SiloAddress[] siloAddresses) -> Task + B761B345(Orleans.Runtime.SiloAddress[]) -> Task + # Orleans.Runtime.IManagementGrain.GetDetailedHosts(bool onlyActive) -> Task + CC6CCBC3(bool) -> Task + # Orleans.Runtime.IManagementGrain.GetTotalActivationCount() -> Task + D7365B43() -> Task + # Orleans.Runtime.IManagementGrain.SendControlCommandToProvider(string providerName, int command, object? arg) -> Task + F67965CC`1(string, int, object?) -> Task interface [GrainInterfaceType("Orleans.Runtime.IMembershipService")] Orleans.Runtime.IMembershipService [Version(0)] - MembershipChangeNotification(Orleans.Runtime.MembershipTableSnapshot) -> Task - Ping(int) -> Task - ProbeIndirectly(Orleans.Runtime.SiloAddress, System.TimeSpan, int) -> Task + # Orleans.Runtime.IMembershipService.ProbeIndirectly(SiloAddress target, TimeSpan probeTimeout, int probeNumber) -> Task + 0F85FAAF(Orleans.Runtime.SiloAddress, System.TimeSpan, int) -> Task + # Orleans.Runtime.IMembershipService.MembershipChangeNotification(MembershipTableSnapshot snapshot) -> Task + 22A02D46(Orleans.Runtime.MembershipTableSnapshot) -> Task + # Orleans.Runtime.IMembershipService.Ping(int pingNumber) -> Task + 39AB7071(int) -> Task interface [GrainInterfaceType("Orleans.Storage.IMemoryStorageGrain")] Orleans.Storage.IMemoryStorageGrain [Version(0)] - DeleteStateAsync`1(string, string?) -> Task - ReadStateAsync`1(string) -> Task> - WriteStateAsync`1(string, Orleans.IGrainState) -> Task + # Orleans.Storage.IMemoryStorageGrain.ReadStateAsync(string grainStoreKey) -> Task?> + 45659318`1(string) -> Task> + # Orleans.Storage.IMemoryStorageGrain.WriteStateAsync(string grainStoreKey, IGrainState grainState) -> Task + 7CC6CA25`1(string, Orleans.IGrainState) -> Task + # Orleans.Storage.IMemoryStorageGrain.DeleteStateAsync(string grainStoreKey, string? eTag) -> Task + B7CADD03`1(string, string?) -> Task diff --git a/src/Orleans.DurableJobs/OrleansContracts.txt b/src/Orleans.DurableJobs/OrleansContracts.txt index 406b1050fcb..49d70fce8d4 100644 --- a/src/Orleans.DurableJobs/OrleansContracts.txt +++ b/src/Orleans.DurableJobs/OrleansContracts.txt @@ -3,13 +3,17 @@ # PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.DurableJobs.IDurableJobReceiverExtension")] Orleans.DurableJobs.IDurableJobReceiverExtension [Version(0)] - HandleDurableJobAsync(Orleans.DurableJobs.IJobRunContext, System.Threading.CancellationToken) -> ValueTask + # Orleans.DurableJobs.IDurableJobReceiverExtension.HandleDurableJobAsync(IJobRunContext context, CancellationToken attemptCancellationToken) -> ValueTask + 703DB2D4(Orleans.DurableJobs.IJobRunContext, System.Threading.CancellationToken) -> ValueTask interface [GrainInterfaceType("Orleans.DurableJobs.ILocalDurableJobManagerSystemTarget")] Orleans.DurableJobs.ILocalDurableJobManagerSystemTarget [Version(0)] - CancelAsync(Orleans.DurableJobs.DurableJob, System.Threading.CancellationToken) -> Task + # Orleans.DurableJobs.ILocalDurableJobManagerSystemTarget.CancelAsync(DurableJob job, CancellationToken requestCancellationToken) -> Task + 4D559F22(Orleans.DurableJobs.DurableJob, System.Threading.CancellationToken) -> Task class [GrainType("localdurablejobmanager")] Orleans.DurableJobs.LocalDurableJobManager diff --git a/src/Orleans.EventSourcing/OrleansContracts.txt b/src/Orleans.EventSourcing/OrleansContracts.txt index edc9974d040..db96365c6b1 100644 --- a/src/Orleans.EventSourcing/OrleansContracts.txt +++ b/src/Orleans.EventSourcing/OrleansContracts.txt @@ -3,13 +3,19 @@ # PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.EventSourcing.ILogConsistencyProtocolParticipant")] Orleans.EventSourcing.ILogConsistencyProtocolParticipant [Version(0)] - DeactivateProtocolParticipant() -> Task - PostActivateProtocolParticipant() -> Task - PreActivateProtocolParticipant() -> Task + # Orleans.EventSourcing.ILogConsistencyProtocolParticipant.PreActivateProtocolParticipant() -> Task + 0DB087C8() -> Task + # Orleans.EventSourcing.ILogConsistencyProtocolParticipant.PostActivateProtocolParticipant() -> Task + 22FD7D72() -> Task + # Orleans.EventSourcing.ILogConsistencyProtocolParticipant.DeactivateProtocolParticipant() -> Task + A36FC884() -> Task interface [GrainInterfaceType("Orleans.SystemTargetInterfaces.ILogConsistencyProtocolGateway")] Orleans.SystemTargetInterfaces.ILogConsistencyProtocolGateway [Version(0)] - RelayMessage(Orleans.Runtime.GrainId, Orleans.EventSourcing.ILogConsistencyProtocolMessage) -> Task + # Orleans.SystemTargetInterfaces.ILogConsistencyProtocolGateway.RelayMessage(GrainId id, ILogConsistencyProtocolMessage payload) -> Task + C86A1066(Orleans.Runtime.GrainId, Orleans.EventSourcing.ILogConsistencyProtocolMessage) -> Task diff --git a/src/Orleans.Persistence.Memory/OrleansContracts.txt b/src/Orleans.Persistence.Memory/OrleansContracts.txt index 15107fd95dd..524150db701 100644 --- a/src/Orleans.Persistence.Memory/OrleansContracts.txt +++ b/src/Orleans.Persistence.Memory/OrleansContracts.txt @@ -3,6 +3,8 @@ # PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt diff --git a/src/Orleans.Reminders/OrleansContracts.txt b/src/Orleans.Reminders/OrleansContracts.txt index 65d9e325ed1..f877ceebf16 100644 --- a/src/Orleans.Reminders/OrleansContracts.txt +++ b/src/Orleans.Reminders/OrleansContracts.txt @@ -3,27 +3,42 @@ # PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.IRemindable")] Orleans.IRemindable [Version(0)] - ReceiveReminder(string, Orleans.Runtime.TickStatus) -> Task + # Orleans.IRemindable.ReceiveReminder(string reminderName, TickStatus status) -> Task + 6461BF2F(string, Orleans.Runtime.TickStatus) -> Task interface [GrainInterfaceType("Orleans.IReminderService")] Orleans.IReminderService [Version(0)] - GetReminder(Orleans.Runtime.GrainId, string) -> Task - GetReminders(Orleans.Runtime.GrainId) -> Task> - RegisterOrUpdateReminder(Orleans.Runtime.GrainId, string, System.TimeSpan, System.TimeSpan) -> Task - Start() -> Task - Stop() -> Task - UnregisterReminder(Orleans.Runtime.IGrainReminder) -> Task + # Orleans.IReminderService.RegisterOrUpdateReminder(GrainId grainId, string reminderName, TimeSpan dueTime, TimeSpan period) -> Task + 1281C86D(Orleans.Runtime.GrainId, string, System.TimeSpan, System.TimeSpan) -> Task + # Orleans.IReminderService.GetReminders(GrainId grainId) -> Task> + 419EB51E(Orleans.Runtime.GrainId) -> Task> + # Orleans.IReminderService.Start() -> Task + 5CF78F8A() -> Task + # Orleans.IReminderService.UnregisterReminder(IGrainReminder reminder) -> Task + A7AF84A8(Orleans.Runtime.IGrainReminder) -> Task + # Orleans.IReminderService.GetReminder(GrainId grainId, string reminderName) -> Task + AC622EEB(Orleans.Runtime.GrainId, string) -> Task + # Orleans.IReminderService.Stop() -> Task + DCFCA00D() -> Task interface [GrainInterfaceType("Orleans.IReminderTableGrain")] Orleans.IReminderTableGrain [Version(0)] - ReadRow(Orleans.Runtime.GrainId, string) -> Task - ReadRows(Orleans.Runtime.GrainId) -> Task - ReadRows(uint, uint) -> Task - RemoveRow(Orleans.Runtime.GrainId, string, string) -> Task - TestOnlyClearTable() -> Task - UpsertRow(Orleans.ReminderEntry) -> Task + # Orleans.IReminderTableGrain.ReadRows(uint begin, uint end) -> Task + 13558B55(uint, uint) -> Task + # Orleans.IReminderTableGrain.UpsertRow(ReminderEntry entry) -> Task + 873299B5(Orleans.ReminderEntry) -> Task + # Orleans.IReminderTableGrain.TestOnlyClearTable() -> Task + 8EBE0523() -> Task + # Orleans.IReminderTableGrain.ReadRow(GrainId grainId, string reminderName) -> Task + ECA791DE(Orleans.Runtime.GrainId, string) -> Task + # Orleans.IReminderTableGrain.ReadRows(GrainId grainId) -> Task + EEEF6FCA(Orleans.Runtime.GrainId) -> Task + # Orleans.IReminderTableGrain.RemoveRow(GrainId grainId, string reminderName, string eTag) -> Task + FF391E0B(Orleans.Runtime.GrainId, string, string) -> Task class [GrainType("localreminderservice")] Orleans.Runtime.ReminderService.LocalReminderService diff --git a/src/Orleans.Runtime/OrleansContracts.txt b/src/Orleans.Runtime/OrleansContracts.txt index 0d783a0a11d..99064daf66b 100644 --- a/src/Orleans.Runtime/OrleansContracts.txt +++ b/src/Orleans.Runtime/OrleansContracts.txt @@ -3,6 +3,8 @@ # PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt @@ -17,7 +19,8 @@ class [GrainType("deploymentloadpublisher")] Orleans.Runtime.DeploymentLoadPubli class [GrainType("developmentleaseprovider")] Orleans.Runtime.Development.DevelopmentLeaseProviderGrain interface [GrainInterfaceType("Orleans.Runtime.Development.IDevelopmentLeaseProviderGrain")] Orleans.Runtime.Development.IDevelopmentLeaseProviderGrain [Version(0)] - Reset() -> Task + # Orleans.Runtime.Development.IDevelopmentLeaseProviderGrain.Reset() -> Task + 847FCE12() -> Task class [GrainType("graincallcancellationmanager")] Orleans.Runtime.GrainCallCancellationManager @@ -30,25 +33,27 @@ class [GrainType("distributedremotegraindirectory")] Orleans.Runtime.GrainDirect class [GrainType("graindirectorypartition")] Orleans.Runtime.GrainDirectory.GrainDirectoryPartition interface [GrainInterfaceType("Orleans.Runtime.GrainDirectory.IGrainDirectoryClient")] Orleans.Runtime.GrainDirectory.IGrainDirectoryClient [Version(0)] - GetRegisteredActivations(Orleans.Runtime.MembershipVersion, RingRange, bool, System.Threading.CancellationToken) -> ValueTask>> - RecoverRegisteredActivations(Orleans.Runtime.MembershipVersion, RingRange, Orleans.Runtime.SiloAddress, int, System.Threading.CancellationToken) -> ValueTask>> + [Alias("GetRegisteredActivations")] GetRegisteredActivations(Orleans.Runtime.MembershipVersion, RingRange, bool, System.Threading.CancellationToken) -> ValueTask>> + [Alias("RecoverRegisteredActivations")] RecoverRegisteredActivations(Orleans.Runtime.MembershipVersion, RingRange, Orleans.Runtime.SiloAddress, int, System.Threading.CancellationToken) -> ValueTask>> interface [GrainInterfaceType("Orleans.Runtime.GrainDirectory.IGrainDirectoryPartition")] Orleans.Runtime.GrainDirectory.IGrainDirectoryPartition [Version(0)] - AcknowledgeSnapshotTransferAsync(Orleans.Runtime.SiloAddress, int, Orleans.Runtime.MembershipVersion, System.Threading.CancellationToken) -> ValueTask - DeregisterAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainAddress, System.Threading.CancellationToken) -> ValueTask> - GetSnapshotAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.MembershipVersion, RingRange, System.Threading.CancellationToken) -> ValueTask - LookupAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainId, System.Threading.CancellationToken) -> ValueTask> - RegisterAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainAddress, Orleans.Runtime.GrainAddress?, System.Threading.CancellationToken) -> ValueTask> + [Alias("AcknowledgeSnapshotTransferAsync")] AcknowledgeSnapshotTransferAsync(Orleans.Runtime.SiloAddress, int, Orleans.Runtime.MembershipVersion, System.Threading.CancellationToken) -> ValueTask + [Alias("DeregisterAsync")] DeregisterAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainAddress, System.Threading.CancellationToken) -> ValueTask> + [Alias("GetSnapshotAsync")] GetSnapshotAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.MembershipVersion, RingRange, System.Threading.CancellationToken) -> ValueTask + [Alias("LookupAsync")] LookupAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainId, System.Threading.CancellationToken) -> ValueTask> + [Alias("RegisterAsync")] RegisterAsync(Orleans.Runtime.MembershipVersion, Orleans.Runtime.GrainAddress, Orleans.Runtime.GrainAddress?, System.Threading.CancellationToken) -> ValueTask> interface [GrainInterfaceType("Orleans.Runtime.GrainDirectory.IGrainDirectoryTestHooks")] Orleans.Runtime.GrainDirectory.IGrainDirectoryTestHooks [Version(0)] - CheckActivationsAsync(Orleans.Concurrency.Immutable>) -> ValueTask>> - CheckIntegrityAsync() -> ValueTask - RecoverAndCheckIntegrityAsync() -> ValueTask - WaitForMembershipVersionAsync(Orleans.Runtime.MembershipVersion) -> ValueTask + [Alias("CheckActivationsAsync")] CheckActivationsAsync(Orleans.Concurrency.Immutable>) -> ValueTask>> + [Alias("CheckIntegrityAsync")] CheckIntegrityAsync() -> ValueTask + [Alias("RecoverAndCheckIntegrityAsync")] RecoverAndCheckIntegrityAsync() -> ValueTask + [Alias("WaitForMembershipVersionAsync")] WaitForMembershipVersionAsync(Orleans.Runtime.MembershipVersion) -> ValueTask interface [GrainInterfaceType("Orleans.Runtime.GrainDirectory.IRemoteClientDirectory")] Orleans.Runtime.GrainDirectory.IRemoteClientDirectory [Version(0)] - GetClientRoutes(System.Collections.Immutable.ImmutableDictionary) -> Task, long)>> - OnUpdateClientRoutes(System.Collections.Immutable.ImmutableDictionary, long)>) -> Task + # Orleans.Runtime.GrainDirectory.IRemoteClientDirectory.OnUpdateClientRoutes(ImmutableDictionary ConnectedClients, long Version)> update) -> Task + 972F9953(System.Collections.Immutable.ImmutableDictionary, long)>) -> Task + # Orleans.Runtime.GrainDirectory.IRemoteClientDirectory.GetClientRoutes(ImmutableDictionary knownRoutes) -> Task ConnectedClients, long Version)>> + A6E49CD1(System.Collections.Immutable.ImmutableDictionary) -> Task, long)>> class [GrainType("localgraindirectoryclientcompatibility")] Orleans.Runtime.GrainDirectory.LocalGrainDirectoryClientCompatibility @@ -57,24 +62,32 @@ class [GrainType("localgraindirectorypartitioncompatibility")] Orleans.Runtime.G class [GrainType("remotegraindirectory")] Orleans.Runtime.GrainDirectory.RemoteGrainDirectory interface [GrainInterfaceType("Orleans.Runtime.IActivationMigrationManagerSystemTarget")] Orleans.Runtime.IActivationMigrationManagerSystemTarget [Version(0)] - AcceptMigratingGrains(System.Collections.Generic.List) -> ValueTask + # Orleans.Runtime.IActivationMigrationManagerSystemTarget.AcceptMigratingGrains(List migratingGrains) -> ValueTask + 29E9E63F(System.Collections.Generic.List) -> ValueTask interface [GrainInterfaceType("Orleans.Runtime.ICatalog")] Orleans.Runtime.ICatalog [Version(0)] - DeleteActivations(System.Collections.Generic.List, Orleans.DeactivationReasonCode, string) -> Task + # Orleans.Runtime.ICatalog.DeleteActivations(List activationAddresses, DeactivationReasonCode reasonCode, string reasonText) -> Task + C4A56D7C(System.Collections.Generic.List, Orleans.DeactivationReasonCode, string) -> Task interface [GrainInterfaceType("Orleans.Runtime.IGrainCallCancellationManagerSystemTarget")] Orleans.Runtime.IGrainCallCancellationManagerSystemTarget [Version(0)] - CancelCallsAsync(System.Collections.Generic.List) -> ValueTask + # Orleans.Runtime.IGrainCallCancellationManagerSystemTarget.CancelCallsAsync(List cancellationRequests) -> ValueTask + AF79F3FA(System.Collections.Generic.List) -> ValueTask interface [GrainInterfaceType("Orleans.Runtime.IGrainTimerInvoker")] Orleans.Runtime.IGrainTimerInvoker [Version(0)] - InvokeCallbackAsync() -> Task + # Orleans.Runtime.IGrainTimerInvoker.InvokeCallbackAsync() -> Task + 3F6C2672() -> Task interface [GrainInterfaceType("Orleans.Runtime.IRemoteGrainDirectory")] Orleans.Runtime.IRemoteGrainDirectory [Version(0)] - AcceptSplitPartition(System.Collections.Generic.List) -> Task - LookUpMany(System.Collections.Generic.List<(Orleans.Runtime.GrainId, int)>) -> Task> - RegisterMany(System.Collections.Generic.List) -> Task + # Orleans.Runtime.IRemoteGrainDirectory.LookUpMany(List<(GrainId GrainId, int Version)> grainAndETagList) -> Task> + 7DF50601(System.Collections.Generic.List<(Orleans.Runtime.GrainId, int)>) -> Task> + # Orleans.Runtime.IRemoteGrainDirectory.AcceptSplitPartition(List singleActivations) -> Task + 9ABE3793(System.Collections.Generic.List) -> Task + # Orleans.Runtime.IRemoteGrainDirectory.RegisterMany(List addresses) -> Task + CD06EAEE(System.Collections.Generic.List) -> Task interface [GrainInterfaceType("Orleans.Runtime.ISiloManifestSystemTarget")] Orleans.Runtime.ISiloManifestSystemTarget [Version(0)] - GetSiloManifest() -> ValueTask + # Orleans.Runtime.ISiloManifestSystemTarget.GetSiloManifest() -> ValueTask + 1857A4C8() -> ValueTask class [GrainType("management")] Orleans.Runtime.Management.ManagementGrain @@ -83,7 +96,7 @@ class [GrainType("membershipsystemtarget")] Orleans.Runtime.MembershipService.Me class [GrainType("membershiptablesystemtarget")] Orleans.Runtime.MembershipService.MembershipTableSystemTarget interface [GrainInterfaceType("Orleans.Runtime.MembershipService.SiloMetadata.ISiloMetadataSystemTarget")] Orleans.Runtime.MembershipService.SiloMetadata.ISiloMetadataSystemTarget [Version(0)] - GetSiloMetadata() -> Task + [Alias("GetSiloMetadata")] GetSiloMetadata() -> Task class [GrainType("silometadatasystemtarget")] Orleans.Runtime.MembershipService.SiloMetadata.SiloMetadataSystemTarget @@ -100,13 +113,21 @@ interface [GrainInterfaceType("Orleans.Runtime.TestHooks.ITestHooksSystemTarget" class [GrainType("testhookssystemtarget")] Orleans.Runtime.TestHooks.TestHooksSystemTarget interface [GrainInterfaceType("Orleans.Runtime.Versions.IVersionStoreGrain")] Orleans.Runtime.Versions.IVersionStoreGrain [Version(0)] - GetCompatibilityStrategies() -> Task> - GetCompatibilityStrategy() -> Task - GetSelectorStrategies() -> Task> - GetSelectorStrategy() -> Task - SetCompatibilityStrategy(Orleans.Runtime.GrainInterfaceType, Orleans.Versions.Compatibility.CompatibilityStrategy) -> Task - SetCompatibilityStrategy(Orleans.Versions.Compatibility.CompatibilityStrategy) -> Task - SetSelectorStrategy(Orleans.Runtime.GrainInterfaceType, Orleans.Versions.Selector.VersionSelectorStrategy) -> Task - SetSelectorStrategy(Orleans.Versions.Selector.VersionSelectorStrategy) -> Task + # Orleans.Runtime.Versions.IVersionStoreGrain.SetCompatibilityStrategy(GrainInterfaceType interfaceType, CompatibilityStrategy strategy) -> Task + 1B7F13C8(Orleans.Runtime.GrainInterfaceType, Orleans.Versions.Compatibility.CompatibilityStrategy) -> Task + # Orleans.Runtime.Versions.IVersionStoreGrain.SetSelectorStrategy(GrainInterfaceType interfaceType, VersionSelectorStrategy strategy) -> Task + 3E6DDE3E(Orleans.Runtime.GrainInterfaceType, Orleans.Versions.Selector.VersionSelectorStrategy) -> Task + # Orleans.Runtime.Versions.IVersionStoreGrain.SetCompatibilityStrategy(CompatibilityStrategy strategy) -> Task + 67A0B5AA(Orleans.Versions.Compatibility.CompatibilityStrategy) -> Task + # Orleans.Runtime.Versions.IVersionStoreGrain.GetCompatibilityStrategy() -> Task + 67EF9A39() -> Task + # Orleans.Runtime.Versions.IVersionStoreGrain.GetCompatibilityStrategies() -> Task> + 7261373F() -> Task> + # Orleans.Runtime.Versions.IVersionStoreGrain.GetSelectorStrategies() -> Task> + 743D88ED() -> Task> + # Orleans.Runtime.Versions.IVersionStoreGrain.GetSelectorStrategy() -> Task + 8A72848A() -> Task + # Orleans.Runtime.Versions.IVersionStoreGrain.SetSelectorStrategy(VersionSelectorStrategy strategy) -> Task + E7532DE3(Orleans.Versions.Selector.VersionSelectorStrategy) -> Task class [GrainType("versionstore")] Orleans.Runtime.Versions.VersionStoreGrain diff --git a/src/Orleans.Streaming/OrleansContracts.txt b/src/Orleans.Streaming/OrleansContracts.txt index 4bd4f23650a..60fe142410c 100644 --- a/src/Orleans.Streaming/OrleansContracts.txt +++ b/src/Orleans.Streaming/OrleansContracts.txt @@ -3,12 +3,16 @@ # PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.Providers.IMemoryStreamQueueGrain")] Orleans.Providers.IMemoryStreamQueueGrain [Version(0)] - Dequeue(int) -> Task> - Enqueue(Orleans.Providers.MemoryMessageData) -> Task + # Orleans.Providers.IMemoryStreamQueueGrain.Enqueue(MemoryMessageData data) -> Task + 74D60341(Orleans.Providers.MemoryMessageData) -> Task + # Orleans.Providers.IMemoryStreamQueueGrain.Dequeue(int maxCount) -> Task> + 7A8F8C1A(int) -> Task> class [GrainType("memorystreamqueue")] Orleans.Providers.MemoryStreamQueueGrain @@ -18,43 +22,70 @@ class [GrainType("stream.checkpoint.configured")] Orleans.Streams.ConfiguredStre interface [GrainInterfaceType("Orleans.Streams.IConfiguredStreamCheckpointerGrain")] Orleans.Streams.IConfiguredStreamCheckpointerGrain [Version(0)] interface [GrainInterfaceType("Orleans.Streams.IPersistentStreamPullingAgent")] Orleans.Streams.IPersistentStreamPullingAgent [Version(0)] - Initialize() -> Task - Shutdown() -> Task + # Orleans.Streams.IPersistentStreamPullingAgent.Initialize() -> Task + 06009D9C() -> Task + # Orleans.Streams.IPersistentStreamPullingAgent.Shutdown() -> Task + 620FF905() -> Task interface [GrainInterfaceType("Orleans.Streams.IPersistentStreamPullingManager")] Orleans.Streams.IPersistentStreamPullingManager [Version(0)] - ExecuteCommand(Orleans.Providers.Streams.Common.PersistentStreamProviderCommand, object?) -> Task - Initialize() -> Task - StartAgents() -> Task - Stop() -> Task - StopAgents() -> Task + # Orleans.Streams.IPersistentStreamPullingManager.Initialize() -> Task + 455AB850() -> Task + # Orleans.Streams.IPersistentStreamPullingManager.StartAgents() -> Task + 54E9E970() -> Task + # Orleans.Streams.IPersistentStreamPullingManager.StopAgents() -> Task + BBD50CFF() -> Task + # Orleans.Streams.IPersistentStreamPullingManager.ExecuteCommand(PersistentStreamProviderCommand command, object? arg) -> Task + DE756D95(Orleans.Providers.Streams.Common.PersistentStreamProviderCommand, object?) -> Task + # Orleans.Streams.IPersistentStreamPullingManager.Stop() -> Task + F4B5B5AA() -> Task interface [GrainInterfaceType("Orleans.Streams.IPubSubRendezvousGrain")] Orleans.Streams.IPubSubRendezvousGrain [Version(0)] - ConsumerCount(Orleans.Runtime.QualifiedStreamId) -> Task - DiagGetConsumers(Orleans.Runtime.QualifiedStreamId) -> Task - FaultSubscription(Orleans.Runtime.GuidId) -> Task - GetAllSubscriptions(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task> - ProducerCount(Orleans.Runtime.QualifiedStreamId) -> Task - RegisterConsumer(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId, string?) -> Task - RegisterProducer(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task> - UnregisterConsumer(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId) -> Task - UnregisterProducer(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task - Validate() -> Task + # Orleans.Streams.IPubSubRendezvousGrain.Validate() -> Task + 20AA72BF() -> Task + # Orleans.Streams.IPubSubRendezvousGrain.FaultSubscription(GuidId subscriptionId) -> Task + 2821FCF5(Orleans.Runtime.GuidId) -> Task + # Orleans.Streams.IPubSubRendezvousGrain.ProducerCount(QualifiedStreamId streamId) -> Task + 29B61035(Orleans.Runtime.QualifiedStreamId) -> Task + # Orleans.Streams.IPubSubRendezvousGrain.RegisterConsumer(GuidId subscriptionId, QualifiedStreamId streamId, GrainId streamConsumer, string? filterData) -> Task + 5E7E20BC(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId, string?) -> Task + # Orleans.Streams.IPubSubRendezvousGrain.ConsumerCount(QualifiedStreamId streamId) -> Task + 5F72C5CF(Orleans.Runtime.QualifiedStreamId) -> Task + # Orleans.Streams.IPubSubRendezvousGrain.GetAllSubscriptions(QualifiedStreamId streamId, GrainId streamConsumer) -> Task> + 7DBE84FA(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task> + # Orleans.Streams.IPubSubRendezvousGrain.DiagGetConsumers(QualifiedStreamId streamId) -> Task + 8A033955(Orleans.Runtime.QualifiedStreamId) -> Task + # Orleans.Streams.IPubSubRendezvousGrain.UnregisterConsumer(GuidId subscriptionId, QualifiedStreamId streamId) -> Task + 974334B6(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId) -> Task + # Orleans.Streams.IPubSubRendezvousGrain.RegisterProducer(QualifiedStreamId streamId, GrainId streamProducer) -> Task> + B5FFB7F3(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task> + # Orleans.Streams.IPubSubRendezvousGrain.UnregisterProducer(QualifiedStreamId streamId, GrainId streamProducer) -> Task + C017B47D(Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId) -> Task interface [GrainInterfaceType("Orleans.Streams.IStreamCheckpointerGrain")] Orleans.Streams.IStreamCheckpointerGrain [Version(0)] - Load(System.Threading.CancellationToken) -> ValueTask - Update(string, string, System.Threading.CancellationToken) -> ValueTask + # Orleans.Streams.IStreamCheckpointerGrain.Update(string offset, string expectedCheckpoint, CancellationToken cancellationToken) -> ValueTask + 7AB50A87(string, string, System.Threading.CancellationToken) -> ValueTask + # Orleans.Streams.IStreamCheckpointerGrain.Load(CancellationToken cancellationToken) -> ValueTask + DE3727A1(System.Threading.CancellationToken) -> ValueTask interface [GrainInterfaceType("Orleans.Streams.IStreamConsumerExtension")] Orleans.Streams.IStreamConsumerExtension [Version(0)] - CompleteStream(Orleans.Runtime.GuidId) -> Task - DeliverBatch(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Streams.IBatchContainer, Orleans.Streams.StreamHandshakeToken?) -> Task - DeliverImmutable(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, object, Orleans.Streams.StreamSequenceToken, Orleans.Streams.StreamHandshakeToken?) -> Task - DeliverMutable(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, object, Orleans.Streams.StreamSequenceToken, Orleans.Streams.StreamHandshakeToken?) -> Task - ErrorInStream(Orleans.Runtime.GuidId, System.Exception) -> Task - GetSequenceToken(Orleans.Runtime.GuidId) -> Task + # Orleans.Streams.IStreamConsumerExtension.DeliverMutable(GuidId subscriptionId, QualifiedStreamId streamId, object item, StreamSequenceToken currentToken, StreamHandshakeToken? handshakeToken) -> Task + 31840DDE(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, object, Orleans.Streams.StreamSequenceToken, Orleans.Streams.StreamHandshakeToken?) -> Task + # Orleans.Streams.IStreamConsumerExtension.CompleteStream(GuidId subscriptionId) -> Task + 49F94A48(Orleans.Runtime.GuidId) -> Task + # Orleans.Streams.IStreamConsumerExtension.ErrorInStream(GuidId subscriptionId, Exception exc) -> Task + 4C676CAF(Orleans.Runtime.GuidId, System.Exception) -> Task + # Orleans.Streams.IStreamConsumerExtension.DeliverImmutable(GuidId subscriptionId, QualifiedStreamId streamId, object item, StreamSequenceToken currentToken, StreamHandshakeToken? handshakeToken) -> Task + 6D8FAEB2(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, object, Orleans.Streams.StreamSequenceToken, Orleans.Streams.StreamHandshakeToken?) -> Task + # Orleans.Streams.IStreamConsumerExtension.DeliverBatch(GuidId subscriptionId, QualifiedStreamId streamId, IBatchContainer item, StreamHandshakeToken? handshakeToken) -> Task + B9CFF2C9(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Streams.IBatchContainer, Orleans.Streams.StreamHandshakeToken?) -> Task + # Orleans.Streams.IStreamConsumerExtension.GetSequenceToken(GuidId subscriptionId) -> Task + C265B3CB(Orleans.Runtime.GuidId) -> Task interface [GrainInterfaceType("Orleans.Streams.IStreamProducerExtension")] Orleans.Streams.IStreamProducerExtension [Version(0)] - AddSubscriber(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId, string?) -> Task - RemoveSubscriber(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId) -> Task + # Orleans.Streams.IStreamProducerExtension.AddSubscriber(GuidId subscriptionId, QualifiedStreamId streamId, GrainId streamConsumer, string? filterData) -> Task + 1341E3D4(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId, Orleans.Runtime.GrainId, string?) -> Task + # Orleans.Streams.IStreamProducerExtension.RemoveSubscriber(GuidId subscriptionId, QualifiedStreamId streamId) -> Task + B98BA876(Orleans.Runtime.GuidId, Orleans.Runtime.QualifiedStreamId) -> Task class [GrainType("persistentstreampullingagent")] Orleans.Streams.PersistentStreamPullingAgent diff --git a/src/Orleans.TestingHost/OrleansContracts.txt b/src/Orleans.TestingHost/OrleansContracts.txt index 53d2dece783..d1d33c7d442 100644 --- a/src/Orleans.TestingHost/OrleansContracts.txt +++ b/src/Orleans.TestingHost/OrleansContracts.txt @@ -3,15 +3,23 @@ # PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.TestingHost.IStorageFaultGrain")] Orleans.TestingHost.IStorageFaultGrain [Version(0)] - AddFaultOnClear(Orleans.Runtime.GrainId, System.Exception) -> Task - AddFaultOnRead(Orleans.Runtime.GrainId, System.Exception) -> Task - AddFaultOnWrite(Orleans.Runtime.GrainId, System.Exception) -> Task - OnClear(Orleans.Runtime.GrainId) -> Task - OnRead(Orleans.Runtime.GrainId) -> Task - OnWrite(Orleans.Runtime.GrainId) -> Task + # Orleans.TestingHost.IStorageFaultGrain.AddFaultOnRead(GrainId grainId, Exception exception) -> Task + 1150D526(Orleans.Runtime.GrainId, System.Exception) -> Task + # Orleans.TestingHost.IStorageFaultGrain.AddFaultOnClear(GrainId grainId, Exception exception) -> Task + 1A607A31(Orleans.Runtime.GrainId, System.Exception) -> Task + # Orleans.TestingHost.IStorageFaultGrain.OnRead(GrainId grainId) -> Task + 5D91E1AF(Orleans.Runtime.GrainId) -> Task + # Orleans.TestingHost.IStorageFaultGrain.AddFaultOnWrite(GrainId grainId, Exception exception) -> Task + B9852E6E(Orleans.Runtime.GrainId, System.Exception) -> Task + # Orleans.TestingHost.IStorageFaultGrain.OnClear(GrainId grainId) -> Task + C94BA77C(Orleans.Runtime.GrainId) -> Task + # Orleans.TestingHost.IStorageFaultGrain.OnWrite(GrainId grainId) -> Task + E8594820(Orleans.Runtime.GrainId) -> Task class [GrainType("storagefault")] Orleans.TestingHost.StorageFaultGrain diff --git a/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt b/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt index f1e5f8b767f..1fcbada73e3 100644 --- a/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt +++ b/src/Orleans.Transactions.TestKit.Base/OrleansContracts.txt @@ -3,21 +3,27 @@ # PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt class [GrainType("consistencytest")] Orleans.Transactions.TestKit.Consistency.ConsistencyTestGrain interface [GrainInterfaceType("Orleans.Transactions.TestKit.Consistency.IConsistencyTestGrain")] Orleans.Transactions.TestKit.Consistency.IConsistencyTestGrain [Version(0)] - Run(Orleans.Transactions.TestKit.Consistency.ConsistencyTestOptions, int, string, int, System.DateTime) -> Task + # Orleans.Transactions.TestKit.Consistency.IConsistencyTestGrain.Run(ConsistencyTestOptions options, int depth, string stack, int max, DateTime stopAfter) -> Task + 2EB318CB(Orleans.Transactions.TestKit.Consistency.ConsistencyTestOptions, int, string, int, System.DateTime) -> Task # Orleans.Transactions.TestKit.Correctnesss.DoubleStateTransactionalGrain class [GrainType("txn-correctness-DoubleStateTransactionalGrain")] Orleans.Transactions.TestKit.Correctnesss.DoubleStateTransactionalGrain interface [GrainInterfaceType("Orleans.Transactions.TestKit.Correctnesss.ITransactionalBitArrayGrain")] Orleans.Transactions.TestKit.Correctnesss.ITransactionalBitArrayGrain [Version(0)] - Get() -> Task> - Ping() -> Task - SetBit(int) -> Task + # Orleans.Transactions.TestKit.Correctnesss.ITransactionalBitArrayGrain.SetBit(int newValue) -> Task + 0183C2F5(int) -> Task + # Orleans.Transactions.TestKit.Correctnesss.ITransactionalBitArrayGrain.Ping() -> Task + 9A5740F1() -> Task + # Orleans.Transactions.TestKit.Correctnesss.ITransactionalBitArrayGrain.Get() -> Task> + B821F3B1() -> Task> # Orleans.Transactions.TestKit.Correctnesss.MaxStateTransactionalGrain class [GrainType("txn-correctness-MaxStateTransactionalGrain")] Orleans.Transactions.TestKit.Correctnesss.MaxStateTransactionalGrain @@ -41,68 +47,104 @@ class [GrainType("exclusivelocktransactiontest")] Orleans.Transactions.TestKit.E class [GrainType("faultinjectiontransactioncoordinator")] Orleans.Transactions.TestKit.FaultInjectionTransactionCoordinatorGrain interface [GrainInterfaceType("Orleans.Transactions.TestKit.ICreateAttributionGrain")] Orleans.Transactions.TestKit.ICreateAttributionGrain [Version(0)] - GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> + # Orleans.Transactions.TestKit.ICreateAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> + 3EFBDD5D(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.ICreateOrJoinAttributionGrain")] Orleans.Transactions.TestKit.ICreateOrJoinAttributionGrain [Version(0)] - GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> + # Orleans.Transactions.TestKit.ICreateOrJoinAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> + C9B8ECB8(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.IExclusiveLockCoordinatorGrain")] Orleans.Transactions.TestKit.IExclusiveLockCoordinatorGrain [Version(0)] - ReadThenWrite(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task - ReadThenWriteWithExclusiveLock(Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain, int) -> Task + # Orleans.Transactions.TestKit.IExclusiveLockCoordinatorGrain.ReadThenWrite(ITransactionTestGrain grain, int value) -> Task + 148E55F3(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task + # Orleans.Transactions.TestKit.IExclusiveLockCoordinatorGrain.ReadThenWriteWithExclusiveLock(IExclusiveLockTransactionTestGrain grain, int value) -> Task + F880C5FF(Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain, int) -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain")] Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain [Version(0)] - Add(int) -> Task - Get() -> Task - Set(int) -> Task + # Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain.Get() -> Task + 16E53FE3() -> Task + # Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain.Add(int numberToAdd) -> Task + 81B05CD8(int) -> Task + # Orleans.Transactions.TestKit.IExclusiveLockTransactionTestGrain.Set(int newValue) -> Task + BD3AA4D0(int) -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.IFaultInjectionTransactionCoordinatorGrain")] Orleans.Transactions.TestKit.IFaultInjectionTransactionCoordinatorGrain [Version(0)] - MultiGrainAddAndFaultInjection(System.Collections.Generic.List, int, Orleans.Transactions.TestKit.FaultInjectionControl?) -> Task - MultiGrainSet(System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.IFaultInjectionTransactionCoordinatorGrain.MultiGrainSet(List grains, int numberToAdd) -> Task + 70FF7C60(System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.IFaultInjectionTransactionCoordinatorGrain.MultiGrainAddAndFaultInjection(List grains, int numberToAdd, FaultInjectionControl? faultInjection) -> Task + E67D54A5(System.Collections.Generic.List, int, Orleans.Transactions.TestKit.FaultInjectionControl?) -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain")] Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain [Version(0)] - Add(int, Orleans.Transactions.TestKit.FaultInjectionControl?) -> Task - Deactivate() -> Task - Get() -> Task - Set(int) -> Task + # Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain.Set(int newValue) -> Task + 8389970A(int) -> Task + # Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain.Add(int numberToAdd, FaultInjectionControl? faultInjectionControl) -> Task + A4CAE05C(int, Orleans.Transactions.TestKit.FaultInjectionControl?) -> Task + # Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain.Deactivate() -> Task + A6C1652E() -> Task + # Orleans.Transactions.TestKit.IFaultInjectionTransactionTestGrain.Get() -> Task + C752DF7D() -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.IJoinAttributionGrain")] Orleans.Transactions.TestKit.IJoinAttributionGrain [Version(0)] - GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> + # Orleans.Transactions.TestKit.IJoinAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> + B1619F67(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.INoAttributionGrain")] Orleans.Transactions.TestKit.INoAttributionGrain [Version(0)] - GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> + # Orleans.Transactions.TestKit.INoAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> + BC7E3A79(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.INotAllowedAttributionGrain")] Orleans.Transactions.TestKit.INotAllowedAttributionGrain [Version(0)] - GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> + # Orleans.Transactions.TestKit.INotAllowedAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> + 891D027E(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.ISupportedAttributionGrain")] Orleans.Transactions.TestKit.ISupportedAttributionGrain [Version(0)] - GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> + # Orleans.Transactions.TestKit.ISupportedAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> + BC7DBC0A(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.ISuppressAttributionGrain")] Orleans.Transactions.TestKit.ISuppressAttributionGrain [Version(0)] - GetNestedTransactionIds(int, System.Collections.Generic.List[]) -> Task[]> + # Orleans.Transactions.TestKit.ISuppressAttributionGrain.GetNestedTransactionIds(int tier, List[] tiers) -> Task?[]> + 5A02311D(int, System.Collections.Generic.List[]) -> Task[]> interface [GrainInterfaceType("Orleans.Transactions.TestKit.ITransactionCommitterTestGrain")] Orleans.Transactions.TestKit.ITransactionCommitterTestGrain [Version(0)] - Commit(Orleans.Transactions.Abstractions.ITransactionCommitOperation) -> Task + # Orleans.Transactions.TestKit.ITransactionCommitterTestGrain.Commit(ITransactionCommitOperation operation) -> Task + C44BE2A4(Orleans.Transactions.Abstractions.ITransactionCommitOperation) -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.ITransactionCoordinatorGrain")] Orleans.Transactions.TestKit.ITransactionCoordinatorGrain [Version(0)] - AddAndThrow(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task - MultiGrainAdd(Orleans.Transactions.TestKit.ITransactionCommitterTestGrain, Orleans.Transactions.Abstractions.ITransactionCommitOperation, System.Collections.Generic.List, int) -> Task - MultiGrainAdd(System.Collections.Generic.List, int) -> Task - MultiGrainAddAndThrow(System.Collections.Generic.List, System.Collections.Generic.List, int) -> Task - MultiGrainDouble(System.Collections.Generic.List) -> Task - MultiGrainDoubleByRWRW(System.Collections.Generic.List, int) -> Task - MultiGrainDoubleByWRWR(System.Collections.Generic.List, int) -> Task - MultiGrainSet(System.Collections.Generic.List, int) -> Task - MultiGrainSetBit(System.Collections.Generic.List, int) -> Task - OrphanCallTransaction() -> Task - UpdateViolated(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainAddAndThrow(List grain, List grains, int numberToAdd) -> Task + 2760260D(System.Collections.Generic.List, System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainAdd(List grains, int numberToAdd) -> Task + 3A6B9237(System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.UpdateViolated(ITransactionTestGrain grains, int numberToAdd) -> Task + 485592B2(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainDouble(List grains) -> Task + 5FC2E7A1(System.Collections.Generic.List) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainSetBit(List grains, int bitIndex) -> Task + 5FF4F216(System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainSet(List grains, int numberToAdd) -> Task + 78D54907(System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainAdd(ITransactionCommitterTestGrain committer, ITransactionCommitOperation operation, List grains, int numberToAdd) -> Task + 8EE5E563(Orleans.Transactions.TestKit.ITransactionCommitterTestGrain, Orleans.Transactions.Abstractions.ITransactionCommitOperation, System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainDoubleByRWRW(List grains, int numberToAdd) -> Task + 9EFEA7F3(System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.MultiGrainDoubleByWRWR(List grains, int numberToAdd) -> Task + B4376B4D(System.Collections.Generic.List, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.AddAndThrow(ITransactionTestGrain grain, int numberToAdd) -> Task + D3EF444F(Orleans.Transactions.TestKit.ITransactionTestGrain, int) -> Task + # Orleans.Transactions.TestKit.ITransactionCoordinatorGrain.OrphanCallTransaction() -> Task + EDCC120B() -> Task interface [GrainInterfaceType("Orleans.Transactions.TestKit.ITransactionTestGrain")] Orleans.Transactions.TestKit.ITransactionTestGrain [Version(0)] - Add(int) -> Task - AddAndThrow(int) -> Task - Deactivate() -> Task - Get() -> Task - Set(int) -> Task - SetAndThrow(int) -> Task + # Orleans.Transactions.TestKit.ITransactionTestGrain.AddAndThrow(int numberToAdd) -> Task + 25B066B5(int) -> Task + # Orleans.Transactions.TestKit.ITransactionTestGrain.SetAndThrow(int numberToSet) -> Task + 35C87F81(int) -> Task + # Orleans.Transactions.TestKit.ITransactionTestGrain.Deactivate() -> Task + 35D6FD32() -> Task + # Orleans.Transactions.TestKit.ITransactionTestGrain.Get() -> Task + 8DAA79AA() -> Task + # Orleans.Transactions.TestKit.ITransactionTestGrain.Set(int newValue) -> Task + CE9EC80B(int) -> Task + # Orleans.Transactions.TestKit.ITransactionTestGrain.Add(int numberToAdd) -> Task + DC07DAEA(int) -> Task class [GrainType("joinattribution")] Orleans.Transactions.TestKit.JoinAttributionGrain diff --git a/src/Orleans.Transactions/OrleansContracts.txt b/src/Orleans.Transactions/OrleansContracts.txt index 8dcf7f8ca4d..02e544c9e58 100644 --- a/src/Orleans.Transactions/OrleansContracts.txt +++ b/src/Orleans.Transactions/OrleansContracts.txt @@ -3,17 +3,27 @@ # PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path: # dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024 # Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION +# The regeneration command edits this manifest only; it does not change source attributes. +# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash. # Review every diff: identity or signature changes can break wire compatibility during rolling upgrades. # Details: https://aka.ms/orleans/OrleansContracts.txt interface [GrainInterfaceType("Orleans.Transactions.Abstractions.ITransactionManagerExtension")] Orleans.Transactions.Abstractions.ITransactionManagerExtension [Version(0)] - Ping(string, System.Guid, System.DateTime, Orleans.Transactions.ParticipantId) -> Task - PrepareAndCommit(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime, System.Collections.Generic.List, int) -> Task - Prepared(string, System.Guid, System.DateTime, Orleans.Transactions.ParticipantId, Orleans.Transactions.TransactionalStatus) -> Task + # Orleans.Transactions.Abstractions.ITransactionManagerExtension.Prepared(string resourceId, Guid transactionId, DateTime timestamp, ParticipantId resource, TransactionalStatus status) -> Task + 12BEFA17(string, System.Guid, System.DateTime, Orleans.Transactions.ParticipantId, Orleans.Transactions.TransactionalStatus) -> Task + # Orleans.Transactions.Abstractions.ITransactionManagerExtension.Ping(string resourceId, Guid transactionId, DateTime timeStamp, ParticipantId resource) -> Task + AC4A9AEB(string, System.Guid, System.DateTime, Orleans.Transactions.ParticipantId) -> Task + # Orleans.Transactions.Abstractions.ITransactionManagerExtension.PrepareAndCommit(string resourceId, Guid transactionId, AccessCounter accessCount, DateTime timeStamp, List writeResources, int totalParticipants) -> Task + B024EFA6(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime, System.Collections.Generic.List, int) -> Task interface [GrainInterfaceType("Orleans.Transactions.Abstractions.ITransactionalResourceExtension")] Orleans.Transactions.Abstractions.ITransactionalResourceExtension [Version(0)] - Abort(string, System.Guid) -> Task - Cancel(string, System.Guid, System.DateTime, Orleans.Transactions.TransactionalStatus) -> Task - CommitReadOnly(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime) -> Task - Confirm(string, System.Guid, System.DateTime) -> Task - Prepare(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime, Orleans.Transactions.ParticipantId) -> Task + # Orleans.Transactions.Abstractions.ITransactionalResourceExtension.CommitReadOnly(string resourceId, Guid transactionId, AccessCounter accessCount, DateTime timeStamp) -> Task + 1BB071FE(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime) -> Task + # Orleans.Transactions.Abstractions.ITransactionalResourceExtension.Prepare(string resourceId, Guid transactionId, AccessCounter accessCount, DateTime timeStamp, ParticipantId transactionManager) -> Task + 2ADCC608(string, System.Guid, Orleans.Transactions.Abstractions.AccessCounter, System.DateTime, Orleans.Transactions.ParticipantId) -> Task + # Orleans.Transactions.Abstractions.ITransactionalResourceExtension.Confirm(string resourceId, Guid transactionId, DateTime timeStamp) -> Task + 5DDDE6F0(string, System.Guid, System.DateTime) -> Task + # Orleans.Transactions.Abstractions.ITransactionalResourceExtension.Cancel(string resourceId, Guid transactionId, DateTime timeStamp, TransactionalStatus status) -> Task + 80028AB9(string, System.Guid, System.DateTime, Orleans.Transactions.TransactionalStatus) -> Task + # Orleans.Transactions.Abstractions.ITransactionalResourceExtension.Abort(string resourceId, Guid transactionId) -> Task + BD051D23(string, System.Guid) -> Task diff --git a/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs b/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs index ff2e3ea528c..2fb7b42106a 100644 --- a/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs +++ b/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.Reflection; using System.Text; +using System.Text.RegularExpressions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; @@ -31,6 +32,8 @@ public class GrainInterfaceVersionAnalyzerTest "# PATH_TO_PROJECT_OR_SOLUTION with the owning .csproj, .sln, or .slnx path:\n" + "# dotnet format PATH_TO_PROJECT_OR_SOLUTION analyzers --severity info --diagnostics ORLEANS0016 ORLEANS0017 ORLEANS0018 ORLEANS0019 ORLEANS0020 ORLEANS0022 ORLEANS0023 ORLEANS0024\n" + "# Verify with: dotnet build PATH_TO_PROJECT_OR_SOLUTION\n" + + "# The regeneration command edits this manifest only; it does not change source attributes.\n" + + "# Methods without [Id] or [Alias] use the Orleans code generator's existing wire ID hash.\n" + "# Review every diff: identity or signature changes can break wire compatibility during rolling upgrades.\n" + "# Details: https://aka.ms/orleans/OrleansContracts.txt\n\n"; @@ -360,6 +363,7 @@ IMyGrain [Version(1)] Assert.Contains(diagnostics, d => d.Id == GrainInterfaceVersionAnalyzer.RuleId0018); var diagnostic = diagnostics.First(d => d.Id == GrainInterfaceVersionAnalyzer.RuleId0018); Assert.Contains("NewMethod", diagnostic.GetMessage()); + Assert.Contains("8E43BF4F() -> Task", diagnostic.GetMessage()); } [Fact] @@ -573,6 +577,28 @@ interface IMyGrain [Version(1)] Assert.DoesNotContain(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); } + [Fact] + public async Task UnmarkedSameNameAlias_ReportsManifestUpgrade() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + [Alias(""Method"")] + Task Method(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + Method() -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0018); + Assert.DoesNotContain(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + [Fact] public async Task AliasedMemberIdentityChange_ReportsAddedAndRemovedSignatures() { @@ -595,6 +621,28 @@ interface IMyGrain [Version(1)] Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); } + [Fact] + public async Task ExplicitId_DoesNotMatchLegacyMethodName() + { + const string source = @" +[Version(1)] +public interface IMyGrain : IGrain +{ + [Id(42)] + Task Ping(); +} +"; + const string contractsFile = @" +interface IMyGrain [Version(1)] + Ping() -> Task +"; + + var diagnostics = await GetDiagnosticsAsync(source, contractsFile); + + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0018); + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); + } + [Fact] public async Task RemovedAlias_ReportsAddedAndRemovedSignatures() { @@ -1091,7 +1139,7 @@ public interface INewGrain : IGrain Assert.Empty(diagnostics); Assert.Contains("# IOldGrain\ninterface [GrainInterfaceType(\"stable-interface\")] IOldGrain [Version(0)]", contractsFile); Assert.Contains( - " stable-method(request) -> Task", + " [Alias(\"stable-method\")] stable-method(request) -> Task", contractsFile); Assert.Contains("# IOldGrain", contractsFile); Assert.Contains("# IOldGrain.OldMethod", contractsFile); @@ -1212,6 +1260,17 @@ private async Task ApplyCodeFixAndGetContractsAsync( return (await changedDocument!.GetTextAsync(TestContext.Current.CancellationToken)).ToString(); } + private static void AssertContainsGeneratedMethod( + string content, + string clrSignature, + string contractSignatureSuffix) + { + var pattern = + $"(?m)^ # {Regex.Escape(clrSignature)}\\r?$\\n" + + $" [0-9A-F]{{8}}{Regex.Escape(contractSignatureSuffix)}\\r?$"; + Assert.Matches(pattern, content); + } + private static Project CreateProjectWithAdditionalFilesForCodeFix( string source, string? grainInterfacesFileContent, @@ -1337,7 +1396,7 @@ public class CartGrain : Grain, IGrainWithStringKey var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); Assert.Contains("interface [GrainInterfaceType(\"cart\")] ICartGrain [Version(2)]", content); - Assert.Contains(" read(int) -> Task", content); + Assert.Contains(" [Alias(\"read\")] read(int) -> Task", content); Assert.Contains("class [GrainType(\"cart\")] CartGrain", content); } @@ -1388,9 +1447,10 @@ public async Task CodeFix_RegenerateExistingFile_UsesConfiguredCustomFilename() var document = Assert.Single(project.AdditionalDocuments); Assert.Equal(configuredPath, document.FilePath); Assert.Equal("rpc-contracts.txt", document.Name); - Assert.Contains( - "Pong() -> Task", - (await document.GetTextAsync(TestContext.Current.CancellationToken)).ToString()); + AssertContainsGeneratedMethod( + (await document.GetTextAsync(TestContext.Current.CancellationToken)).ToString(), + "IMyGrain.Pong() -> Task", + "() -> Task"); } [Fact] @@ -1418,7 +1478,7 @@ interface ILegacyGrain [Version(1)] var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); Assert.Contains("interface [GrainInterfaceType(\"ICurrentGrain\")] ICurrentGrain [Version(0)]", content); - Assert.Contains(" ReadAsync() -> Task", content); + AssertContainsGeneratedMethod(content, "ICurrentGrain.ReadAsync() -> Task", "() -> Task"); Assert.Contains("*RETIRED* interface ILegacyGrain [Version(1)]", content); Assert.Contains(" WriteAsync(int) -> Task", content); Assert.Contains("*RETIRED* class [GrainType(\"retired\")] RetiredGrain", content); @@ -1534,6 +1594,93 @@ public sealed class GeneratedProxy : IMyGrain Assert.Empty(await GetDiagnosticsAsync(source, content)); } + [Fact] + public async Task CodeFix_RegenerateProject_RecognizesAliasedGeneratedCodeAttribute() + { + const string source = @" +using GC = System.CodeDom.Compiler.GeneratedCodeAttribute; + +public interface IMyGrain : IGrain +{ +} + +[GC(""Test"", ""1.0"")] +public sealed class GeneratedProxy : IMyGrain +{ +} +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.DoesNotContain("GeneratedProxy", content); + } + + [Fact] + public async Task CodeFix_RegenerateProject_DoesNotTreatSimilarlyNamedAttributeAsGeneratedCode() + { + const string source = @" +public sealed class CustomGeneratedCodeAttribute : Attribute +{ +} + +[CustomGeneratedCode] +public sealed class MyGrain : Grain, IGrainWithStringKey +{ +} +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("class [GrainType(\"my\")] MyGrain", content); + } + + [Fact] + public async Task CodeFix_RegenerateProject_IncludesSourcePartialGrainWithGeneratedPartial() + { + const string source = @" +public partial class MyGrain : Grain, IGrainWithStringKey +{ +} +"; + const string generatedSource = @" +[System.CodeDom.Compiler.GeneratedCode(""Test"", ""1.0"")] +public partial class MyGrain +{ +} +"; + + var (changedSolution, additionalDocumentId) = await ApplyCodeFixAsync( + source, + grainInterfacesFileContent: null, + GrainInterfaceVersionAnalyzer.RuleId0020, + RegenerateCodeActionTitle, + generatedSource: generatedSource); + + var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.Contains("class [GrainType(\"my\")] MyGrain", content); + + var sourceText = (await changedSolution.Projects.Single().Documents + .Single(document => document.Name == "Test.cs") + .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + Assert.DoesNotContain("[Alias(", sourceText); + Assert.DoesNotContain("[Id(", sourceText); + Assert.DoesNotContain("[GrainType(", sourceText); + Assert.DoesNotContain("[GrainInterfaceType(", sourceText); + } + [Fact] public async Task CodeFix_RegenerateProject_RecognizesLegacyGrainTypeAfterClrRename() { @@ -1619,8 +1766,8 @@ interface IMyGrain [Version(1)] var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); - Assert.Contains(" ExistingAsync() -> Task", content); - Assert.Contains(" NewAsync() -> Task", content); + AssertContainsGeneratedMethod(content, "IMyGrain.ExistingAsync() -> Task", "() -> Task"); + AssertContainsGeneratedMethod(content, "IMyGrain.NewAsync() -> Task", "() -> Task"); Assert.Contains(" RemovedAsync() -> Task", content); var diagnostics = await GetDiagnosticsAsync(source, content); @@ -1655,7 +1802,7 @@ interface IMyGrain [Version(1)] var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); Assert.Equal(1, content.Split(new[] { "removed(string) -> Task" }, StringSplitOptions.None).Length - 1); - Assert.DoesNotContain("[Alias(\"removed\")]", content); + Assert.Contains("[Alias(\"removed\")] removed(string) -> Task", content); var diagnostics = await GetDiagnosticsAsync(source, content); Assert.Single(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0027); @@ -1690,7 +1837,10 @@ interface IMyGrain [Version(1)] var content = (await changedSolution.GetAdditionalDocument(additionalDocumentId!)! .GetTextAsync(TestContext.Current.CancellationToken)).ToString(); - Assert.Contains(" Method(Models.Request) -> Task", content); + AssertContainsGeneratedMethod( + content, + "IMyGrain.Method(Request request) -> Task", + "(Models.Request) -> Task"); Assert.DoesNotContain(" Method(Request) -> Task", content); Assert.DoesNotContain( await GetDiagnosticsAsync(source, content), @@ -1995,7 +2145,7 @@ public interface IMyGrain : IGrain // Should contain the new interface Assert.Contains("IMyGrain [Version(1)]", content); - Assert.Contains("\n DoSomething() -> Task", content); + AssertContainsGeneratedMethod(content, "IMyGrain.DoSomething() -> Task", "() -> Task"); Assert.DoesNotContain("Utility", content); } @@ -2024,7 +2174,10 @@ public interface IMyGrain : IGrain Assert.DoesNotContain("[Alias(", content); Assert.Contains("IMyGrain [Version(2)]", content); - Assert.Contains("\n DoSomething(string) -> Task", content); + AssertContainsGeneratedMethod( + content, + "IMyGrain.DoSomething(string name) -> Task", + "(string) -> Task"); } [Fact] @@ -2102,22 +2255,12 @@ public interface IMiddle : IGrain var alphaInterface = content.IndexOf("IAlpha [Version(1)]", StringComparison.Ordinal); var middleInterface = content.IndexOf("IMiddle [Version(1)]", StringComparison.Ordinal); var zuluInterface = content.IndexOf("IZulu [Version(1)]", StringComparison.Ordinal); - var alphaMember = content.IndexOf(" Alpha() -> Task", StringComparison.Ordinal); - var zetaMember = content.IndexOf(" Zeta() -> Task", StringComparison.Ordinal); - Assert.True(alphaInterface < middleInterface); Assert.True(middleInterface < zuluInterface); - Assert.True(alphaMember < zetaMember); - Assert.Equal( - GeneratedHeader + - "interface IAlpha [Version(1)]\n" + - " Method() -> Task\n\n" + - "interface [GrainInterfaceType(\"IMiddle\")] IMiddle [Version(1)]\n" + - " Alpha() -> Task\n" + - " Zeta() -> Task\n\n" + - "interface IZulu [Version(1)]\n" + - " Method() -> Task\n", - content); + AssertContainsGeneratedMethod(content, "IMiddle.Alpha() -> Task", "() -> Task"); + AssertContainsGeneratedMethod(content, "IMiddle.Zeta() -> Task", "() -> Task"); + Assert.Contains("interface IAlpha [Version(1)]\n Method() -> Task", content); + Assert.Contains("interface IZulu [Version(1)]\n Method() -> Task", content); } [Fact] @@ -2352,7 +2495,10 @@ IMyGrain [Version(1)] var content = changedText.ToString(); // Should contain the new member - Assert.Contains("\n NewMethod(int) -> Task", content); + AssertContainsGeneratedMethod( + content, + "IMyGrain.NewMethod(int value) -> Task", + "(int) -> Task"); } [Fact] @@ -2381,7 +2527,7 @@ IMyGrain [Version(1)] var changedText = await changedDocument!.GetTextAsync(TestContext.Current.CancellationToken); var content = changedText.ToString(); - Assert.Contains("\n new-method(int) -> Task", content); + Assert.Contains("\n [Alias(\"new-method\")] new-method(int) -> Task", content); Assert.Contains(" # IMyGrain.NewMethod(int value) -> Task", content); } @@ -2403,7 +2549,9 @@ public interface IMyGrain : IGrain contractsFile, GrainInterfaceVersionAnalyzer.RuleId0018); - Assert.Equal(GeneratedHeader + "interface IMyGrain [Version(1)]\n NewMethod() -> Task\n", content); + Assert.Equal( + GeneratedHeader + "interface IMyGrain [Version(1)]\n [Alias(\"NewMethod\")] NewMethod() -> Task\n", + content); } [Fact] @@ -2423,9 +2571,11 @@ public interface IMyGrain : IGrain contractsFile, GrainInterfaceVersionAnalyzer.RuleId0018); - Assert.Equal( - GeneratedHeader + "interface IMyGrain [Version(1)]\n ReadStateAsync`1(T) -> Task\n", - content); + Assert.StartsWith(GeneratedHeader + "interface IMyGrain [Version(1)]\n", content); + AssertContainsGeneratedMethod( + content, + "IMyGrain.ReadStateAsync(T value) -> Task", + "`1(T) -> Task"); } [Fact] @@ -2797,10 +2947,12 @@ public interface IMyGrain : IGrain var additionalDocumentId = Assert.Single(changedProject!.AdditionalDocumentIds); var changedDocument = changedSolution.GetAdditionalDocument(additionalDocumentId); Assert.NotNull(changedDocument); - var actualContractsFile = (await changedDocument!.GetTextAsync(TestContext.Current.CancellationToken)).ToString(); + var changedText = await changedDocument!.GetTextAsync(TestContext.Current.CancellationToken); + var actualContractsFile = changedText.ToString(); Assert.Equal(expectedContractsFile, actualContractsFile); Assert.DoesNotContain("\n", actualContractsFile.Replace("\r\n", string.Empty, StringComparison.Ordinal)); + Assert.Empty(changedText.Encoding?.GetPreamble() ?? []); } [Theory] From f231a5ac7fa6dea21732783a79f8e555f435abbc Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Fri, 28 Aug 2026 08:46:24 -0700 Subject: [PATCH 10/11] test(analyzers): cover contract compatibility breaks --- .../GrainInterfaceVersionAnalyzerTest.cs | 247 ++++++++++++++++++ 1 file changed, 247 insertions(+) diff --git a/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs b/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs index 2fb7b42106a..e152c048447 100644 --- a/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs +++ b/test/Orleans.Analyzers.Tests/GrainInterfaceVersionAnalyzerTest.cs @@ -1161,6 +1161,7 @@ public interface IMyGrain : IGrain var diagnostics = await GetDiagnosticsAsync(source, contractsFile); Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0016); + Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0019); } [Fact] @@ -1179,6 +1180,247 @@ public class MyGrain : Grain, IGrainWithStringKey Assert.Contains(diagnostics, diagnostic => diagnostic.Id == GrainInterfaceVersionAnalyzer.RuleId0023); } + [Fact] + public async Task MethodRenameWithoutAlias_ReportsAddedAndRemovedSignatures() + { + const string oldSource = @" +public interface IMyGrain : IGrain +{ + Task OldName(); +} +"; + const string newSource = @" +public interface IMyGrain : IGrain +{ + Task NewName(); +} +"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0018, + GrainInterfaceVersionAnalyzer.RuleId0027); + Assert.Contains(diagnostics, diagnostic => diagnostic.GetMessage().Contains("NewName", StringComparison.Ordinal)); + Assert.Contains("# IMyGrain.OldName() -> Task", contractsFile); + } + + [Fact] + public async Task MethodReturnTypeChange_ReportsAddedAndRemovedSignatures() + { + const string oldSource = @" +public interface IMyGrain : IGrain +{ + Task Read(); +} +"; + const string newSource = @" +public interface IMyGrain : IGrain +{ + Task Read(); +} +"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0018, + GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task MethodParameterTypeChange_ReportsAddedAndRemovedSignatures() + { + const string oldSource = @" +public interface IMyGrain : IGrain +{ + Task Update(string value); +} +"; + const string newSource = @" +public interface IMyGrain : IGrain +{ + Task Update(int value); +} +"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0018, + GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task MethodParameterOrderChange_ReportsAddedAndRemovedSignatures() + { + const string oldSource = @" +public interface IMyGrain : IGrain +{ + Task Update(string name, int count); +} +"; + const string newSource = @" +public interface IMyGrain : IGrain +{ + Task Update(int count, string name); +} +"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0018, + GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task MethodParameterRename_PreservesGeneratedIdentity() + { + const string oldSource = @" +public interface IMyGrain : IGrain +{ + Task Update(string oldName); +} +"; + const string newSource = @" +public interface IMyGrain : IGrain +{ + Task Update(string newName); +} +"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + Assert.Empty(diagnostics); + } + + [Fact] + public async Task MethodIdChange_ReportsAddedAndRemovedSignatures() + { + const string oldSource = @" +public interface IMyGrain : IGrain +{ + [Id(1)] + Task Update(); +} +"; + const string newSource = @" +public interface IMyGrain : IGrain +{ + [Id(2)] + Task Update(); +} +"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0018, + GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task PayloadAliasChange_ReportsAddedAndRemovedSignatures() + { + const string oldSource = @" +[Alias(""request"")] +public sealed class Request { } + +public interface IMyGrain : IGrain +{ + [Alias(""update"")] + Task Update(Request request); +} +"; + const string newSource = @" +[Alias(""request-v2"")] +public sealed class Request { } + +public interface IMyGrain : IGrain +{ + [Alias(""update"")] + Task Update(Request request); +} +"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0018, + GrainInterfaceVersionAnalyzer.RuleId0027); + } + + [Fact] + public async Task InterfaceRenameWithoutStableIdentity_ReportsAddedAndRemovedContracts() + { + const string oldSource = "public interface IOldGrain : IGrain { }"; + const string newSource = "public interface INewGrain : IGrain { }"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0016); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0016, + GrainInterfaceVersionAnalyzer.RuleId0019); + } + + [Fact] + public async Task GrainClassRenameWithoutStableIdentity_ReportsAddedAndRemovedContracts() + { + const string oldSource = "public class OldGrain : Grain, IGrainWithStringKey { }"; + const string newSource = "public class NewGrain : Grain, IGrainWithStringKey { }"; + var contractsFile = await ApplyCodeFixAndGetContractsAsync( + oldSource, + "# OrleansContracts.txt\n", + GrainInterfaceVersionAnalyzer.RuleId0022); + + var diagnostics = await GetDiagnosticsAsync(newSource, contractsFile); + + AssertDiagnosticIds( + diagnostics, + GrainInterfaceVersionAnalyzer.RuleId0022, + GrainInterfaceVersionAnalyzer.RuleId0024); + } + #endregion #region Code Fix Tests Infrastructure @@ -1271,6 +1513,11 @@ private static void AssertContainsGeneratedMethod( Assert.Matches(pattern, content); } + private static void AssertDiagnosticIds(IEnumerable diagnostics, params string[] expected) + => Assert.Equal( + expected.OrderBy(id => id, StringComparer.Ordinal), + diagnostics.Select(diagnostic => diagnostic.Id).OrderBy(id => id, StringComparer.Ordinal)); + private static Project CreateProjectWithAdditionalFilesForCodeFix( string source, string? grainInterfacesFileContent, From c552c07eba33c0fcb416dbc5402342686d7f6988 Mon Sep 17 00:00:00 2001 From: Reuben Bond Date: Fri, 28 Aug 2026 09:09:39 -0700 Subject: [PATCH 11/11] docs(analyzers): fix manifest member indentation --- .../grains/grain-versioning/contract-compatibility-analyzer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md b/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md index a8844a01b21..ba74ed48b00 100644 --- a/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md +++ b/docs/site/src/content/docs/grains/grain-versioning/contract-compatibility-analyzer.md @@ -106,7 +106,7 @@ Explicit identities remain visible alongside their CLR names: # Contoso.Grains.ICartGrain interface [GrainInterfaceType("cart")] Contoso.Grains.ICartGrain [Version(1)] # Contoso.Grains.ICartGrain.AddAsync(Item item) -> Task -[Alias("add")] add(Contoso.Grains.Item) -> Task + [Alias("add")] add(Contoso.Grains.Item) -> Task # Contoso.Grains.CartGrain class [GrainType("cart")] Contoso.Grains.CartGrain