diff --git a/.gitignore b/.gitignore
index e6d35f75..7bade528 100644
--- a/.gitignore
+++ b/.gitignore
@@ -486,3 +486,6 @@ $RECYCLE.BIN/
**/Container
**/Publish
+
+# Claude Code local configuration
+.claude/
diff --git a/creedengo-csharp.slnx b/creedengo-csharp.slnx
index bac26c87..5db9630e 100644
--- a/creedengo-csharp.slnx
+++ b/creedengo-csharp.slnx
@@ -32,6 +32,7 @@
+
diff --git a/src/Creedengo.Core/Analyzers/GCI98.UseThenByInsteadOfOrderBy.Fixer.cs b/src/Creedengo.Core/Analyzers/GCI98.UseThenByInsteadOfOrderBy.Fixer.cs
new file mode 100644
index 00000000..62d276e0
--- /dev/null
+++ b/src/Creedengo.Core/Analyzers/GCI98.UseThenByInsteadOfOrderBy.Fixer.cs
@@ -0,0 +1,49 @@
+namespace Creedengo.Core.Analyzers;
+
+/// GCI98 fixer: Use 'ThenBy' instead of 'OrderBy'.
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UseThenByInsteadOfOrderByFixer)), Shared]
+public sealed class UseThenByInsteadOfOrderByFixer : CodeFixProvider
+{
+ ///
+ public override ImmutableArray FixableDiagnosticIds => _fixableDiagnosticIds;
+ private static readonly ImmutableArray _fixableDiagnosticIds =
+ ImmutableArray.Create(UseThenByInsteadOfOrderBy.Descriptor.Id);
+
+ ///
+ public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ ///
+ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ if (await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false) is not { } root)
+ return;
+
+ foreach (var diagnostic in context.Diagnostics)
+ {
+ if (root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true) is not SimpleNameSyntax nameSyntax)
+ continue;
+ if (nameSyntax.Parent is not MemberAccessExpressionSyntax memberAccess)
+ continue;
+
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ title: "Use 'ThenBy' instead of 'OrderBy'",
+ createChangedDocument: _ => FixAsync(context.Document, memberAccess, nameSyntax),
+ equivalenceKey: "UseThenByInsteadOfOrderBy"),
+ diagnostic);
+ }
+ }
+
+ private static Task FixAsync(
+ Document document,
+ MemberAccessExpressionSyntax memberAccess,
+ SimpleNameSyntax nameSyntax)
+ {
+ var newIdentifier = nameSyntax.Identifier.Text == "OrderBy" ? "ThenBy" : "ThenByDescending";
+ SimpleNameSyntax newNameSyntax = nameSyntax is GenericNameSyntax generic
+ ? generic.WithIdentifier(SyntaxFactory.Identifier(newIdentifier))
+ : SyntaxFactory.IdentifierName(newIdentifier);
+ var newMemberAccess = memberAccess.WithName(newNameSyntax.WithTriviaFrom(nameSyntax));
+ return document.WithUpdatedRoot(memberAccess, newMemberAccess);
+ }
+}
diff --git a/src/Creedengo.Core/Analyzers/GCI98.UseThenByInsteadOfOrderBy.cs b/src/Creedengo.Core/Analyzers/GCI98.UseThenByInsteadOfOrderBy.cs
new file mode 100644
index 00000000..1c0a67a5
--- /dev/null
+++ b/src/Creedengo.Core/Analyzers/GCI98.UseThenByInsteadOfOrderBy.cs
@@ -0,0 +1,51 @@
+namespace Creedengo.Core.Analyzers;
+
+/// GCI98: Use 'ThenBy' instead of 'OrderBy' in a LINQ sort chain.
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class UseThenByInsteadOfOrderBy : DiagnosticAnalyzer
+{
+ private static readonly ImmutableArray InvocationExpressions =
+ ImmutableArray.Create(SyntaxKind.InvocationExpression);
+
+ /// The diagnostic descriptor.
+ public static DiagnosticDescriptor Descriptor { get; } = Rule.CreateDescriptor(
+ id: Rule.Ids.GCI98_UseThenByInsteadOfOrderBy,
+ title: "Use 'ThenBy' instead of 'OrderBy'",
+ message: "Call 'ThenBy' or 'ThenByDescending' instead of 'OrderBy' or 'OrderByDescending' to preserve the primary sort order",
+ category: Rule.Categories.Usage,
+ severity: DiagnosticSeverity.Warning,
+ description: "Chaining 'OrderBy' or 'OrderByDescending' after another sort operation discards all previous sort keys. Use 'ThenBy' or 'ThenByDescending' to add a secondary sort key while preserving the primary sort.");
+
+ ///
+ public override ImmutableArray SupportedDiagnostics => _supportedDiagnostics;
+ private static readonly ImmutableArray _supportedDiagnostics =
+ ImmutableArray.Create(Descriptor);
+
+ ///
+ public override void Initialize(AnalysisContext context)
+ {
+ context.EnableConcurrentExecution();
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.RegisterSyntaxNodeAction(static context => AnalyzeNode(context), InvocationExpressions);
+ }
+
+ private static void AnalyzeNode(SyntaxNodeAnalysisContext context)
+ {
+ var invocation = (InvocationExpressionSyntax)context.Node;
+
+ if (invocation.Expression is not MemberAccessExpressionSyntax
+ { Name.Identifier.Text: "OrderBy" or "OrderByDescending" } memberAccess)
+ return;
+
+ if (memberAccess.Expression is not InvocationExpressionSyntax
+ {
+ Expression: MemberAccessExpressionSyntax
+ {
+ Name.Identifier.Text: "OrderBy" or "OrderByDescending" or "ThenBy" or "ThenByDescending"
+ }
+ })
+ return;
+
+ context.ReportDiagnostic(Diagnostic.Create(Descriptor, memberAccess.Name.GetLocation()));
+ }
+}
diff --git a/src/Creedengo.Core/Models/Rule.cs b/src/Creedengo.Core/Models/Rule.cs
index 943d3e6a..e55b3792 100644
--- a/src/Creedengo.Core/Models/Rule.cs
+++ b/src/Creedengo.Core/Models/Rule.cs
@@ -1,4 +1,4 @@
-namespace Creedengo.Core.Models;
+namespace Creedengo.Core.Models;
internal static class Rule
{
@@ -32,6 +32,7 @@ public static class Ids
public const string GCIXX_UnnecessaryAssignment = "GCIXX";
public const string GCI96_UseEventArgsDotEmpty = "GCI96";
public const string GCI2333_RemoveRedundantToCharArrayCall = "GCI2333";
+ public const string GCI98_UseThenByInsteadOfOrderBy = "GCI98";
}
/// Creates a diagnostic descriptor.
diff --git a/src/Creedengo.Sandbox/Creedengo.Sandbox.csproj b/src/Creedengo.Sandbox/Creedengo.Sandbox.csproj
new file mode 100644
index 00000000..5588302d
--- /dev/null
+++ b/src/Creedengo.Sandbox/Creedengo.Sandbox.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
diff --git a/src/Creedengo.Sandbox/Program.cs b/src/Creedengo.Sandbox/Program.cs
new file mode 100644
index 00000000..dfb8f564
--- /dev/null
+++ b/src/Creedengo.Sandbox/Program.cs
@@ -0,0 +1 @@
+Console.WriteLine("Write your own class sandbox");
diff --git a/src/Creedengo.Sandbox/RCS1200Sandbox.cs b/src/Creedengo.Sandbox/RCS1200Sandbox.cs
new file mode 100644
index 00000000..b5ff3169
--- /dev/null
+++ b/src/Creedengo.Sandbox/RCS1200Sandbox.cs
@@ -0,0 +1,19 @@
+namespace Creedengo.Sandbox;
+
+internal class RCS1200Sandbox
+{
+ public static void Sort()
+ {
+
+ var items = new (int a, int b)[] {
+ (1, 2),
+ (3, 4),
+ (5, 6)
+ };
+
+ var sorted = items.OrderBy(item => item.Item1)
+ .OrderBy(item => item.Item2);
+
+ }
+
+}
diff --git a/src/Creedengo.Tests/Tests/GCI98.UseThenByInsteadOfOrderBy.Tests.cs b/src/Creedengo.Tests/Tests/GCI98.UseThenByInsteadOfOrderBy.Tests.cs
new file mode 100644
index 00000000..ece34a7a
--- /dev/null
+++ b/src/Creedengo.Tests/Tests/GCI98.UseThenByInsteadOfOrderBy.Tests.cs
@@ -0,0 +1,262 @@
+namespace Creedengo.Tests.Tests;
+
+[TestClass]
+public sealed class UseThenByInsteadOfOrderByTests
+{
+ private static readonly CodeFixerDlg VerifyAsync =
+ TestRunner.VerifyAsync;
+
+ [TestMethod]
+ public Task EmptyCodeAsync() => VerifyAsync("");
+
+ #region No diagnostic
+
+ [TestMethod]
+ public Task DontWarnOnSingleOrderByAsync() => VerifyAsync("""
+ using System.Linq;
+ using System.Collections.Generic;
+
+ public static class Test
+ {
+ public static void Run()
+ {
+ var items = new List<(int A, int B)>();
+ var result = items.OrderBy(x => x.A);
+ }
+ }
+ """);
+
+ [TestMethod]
+ public Task DontWarnOnSingleOrderByDescendingAsync() => VerifyAsync("""
+ using System.Linq;
+ using System.Collections.Generic;
+
+ public static class Test
+ {
+ public static void Run()
+ {
+ var items = new List<(int A, int B)>();
+ var result = items.OrderByDescending(x => x.A);
+ }
+ }
+ """);
+
+ [TestMethod]
+ public Task DontWarnOnOrderByThenByAsync() => VerifyAsync("""
+ using System.Linq;
+ using System.Collections.Generic;
+
+ public static class Test
+ {
+ public static void Run()
+ {
+ var items = new List<(int A, int B)>();
+ var result = items.OrderBy(x => x.A).ThenBy(x => x.B);
+ }
+ }
+ """);
+
+ [TestMethod]
+ public Task DontWarnOnOrderByDescendingThenByDescendingAsync() => VerifyAsync("""
+ using System.Linq;
+ using System.Collections.Generic;
+
+ public static class Test
+ {
+ public static void Run()
+ {
+ var items = new List<(int A, int B)>();
+ var result = items.OrderByDescending(x => x.A).ThenByDescending(x => x.B);
+ }
+ }
+ """);
+
+ [TestMethod]
+ public Task DontWarnOnOrderByFollowedBySelectAsync() => VerifyAsync("""
+ using System.Linq;
+ using System.Collections.Generic;
+
+ public static class Test
+ {
+ public static void Run()
+ {
+ var items = new List<(int A, int B)>();
+ var result = items.OrderBy(x => x.A).Select(x => x.B);
+ }
+ }
+ """);
+
+ #endregion
+
+ #region Diagnostic + fix
+
+ [TestMethod]
+ public Task WarnOnOrderByAfterOrderByAsync() => VerifyAsync("""
+ using System.Linq;
+ using System.Collections.Generic;
+
+ public static class Test
+ {
+ public static void Run()
+ {
+ var items = new List<(int A, int B)>();
+ var query1 = items.OrderBy(x => x.A).[|OrderBy|](x => x.B);
+ var query2 = items
+ .OrderBy(x => x.A)
+ .[|OrderBy|](x => x.B);
+ }
+ }
+ """, """
+ using System.Linq;
+ using System.Collections.Generic;
+
+ public static class Test
+ {
+ public static void Run()
+ {
+ var items = new List<(int A, int B)>();
+ var query1 = items.OrderBy(x => x.A).ThenBy(x => x.B);
+ var query2 = items
+ .OrderBy(x => x.A)
+ .ThenBy(x => x.B);
+ }
+ }
+ """);
+
+ [TestMethod]
+ public Task WarnOnOrderByDescendingAfterOrderByAsync() => VerifyAsync("""
+ using System.Linq;
+ using System.Collections.Generic;
+
+ public static class Test
+ {
+ public static void Run()
+ {
+ var items = new List<(int A, int B)>();
+ var result = items.OrderBy(x => x.A).[|OrderByDescending|](x => x.B);
+ }
+ }
+ """, """
+ using System.Linq;
+ using System.Collections.Generic;
+
+ public static class Test
+ {
+ public static void Run()
+ {
+ var items = new List<(int A, int B)>();
+ var result = items.OrderBy(x => x.A).ThenByDescending(x => x.B);
+ }
+ }
+ """);
+
+ [TestMethod]
+ public Task WarnOnOrderByAfterOrderByDescendingAsync() => VerifyAsync("""
+ using System.Linq;
+ using System.Collections.Generic;
+
+ public static class Test
+ {
+ public static void Run()
+ {
+ var items = new List<(int A, int B)>();
+ var result = items.OrderByDescending(x => x.A).[|OrderBy|](x => x.B);
+ }
+ }
+ """, """
+ using System.Linq;
+ using System.Collections.Generic;
+
+ public static class Test
+ {
+ public static void Run()
+ {
+ var items = new List<(int A, int B)>();
+ var result = items.OrderByDescending(x => x.A).ThenBy(x => x.B);
+ }
+ }
+ """);
+
+ [TestMethod]
+ public Task WarnOnOrderByDescendingAfterOrderByDescendingAsync() => VerifyAsync("""
+ using System.Linq;
+ using System.Collections.Generic;
+
+ public static class Test
+ {
+ public static void Run()
+ {
+ var items = new List<(int A, int B)>();
+ var result = items.OrderByDescending(x => x.A).[|OrderByDescending|](x => x.B);
+ }
+ }
+ """, """
+ using System.Linq;
+ using System.Collections.Generic;
+
+ public static class Test
+ {
+ public static void Run()
+ {
+ var items = new List<(int A, int B)>();
+ var result = items.OrderByDescending(x => x.A).ThenByDescending(x => x.B);
+ }
+ }
+ """);
+
+ [TestMethod]
+ public Task WarnOnOrderByAfterThenByAsync() => VerifyAsync("""
+ using System.Linq;
+ using System.Collections.Generic;
+
+ public static class Test
+ {
+ public static void Run()
+ {
+ var items = new List<(int A, int B, int C)>();
+ var result = items.OrderBy(x => x.A).ThenBy(x => x.B).[|OrderBy|](x => x.C);
+ }
+ }
+ """, """
+ using System.Linq;
+ using System.Collections.Generic;
+
+ public static class Test
+ {
+ public static void Run()
+ {
+ var items = new List<(int A, int B, int C)>();
+ var result = items.OrderBy(x => x.A).ThenBy(x => x.B).ThenBy(x => x.C);
+ }
+ }
+ """);
+
+ [TestMethod]
+ public Task WarnOnChainedOrderByAsync() => VerifyAsync("""
+ using System.Linq;
+ using System.Collections.Generic;
+
+ public static class Test
+ {
+ public static void Run()
+ {
+ var items = new List<(int A, int B, int C)>();
+ var result = items.OrderBy(x => x.A).[|OrderBy|](x => x.B).[|OrderBy|](x => x.C);
+ }
+ }
+ """, """
+ using System.Linq;
+ using System.Collections.Generic;
+
+ public static class Test
+ {
+ public static void Run()
+ {
+ var items = new List<(int A, int B, int C)>();
+ var result = items.OrderBy(x => x.A).ThenBy(x => x.B).ThenBy(x => x.C);
+ }
+ }
+ """);
+
+ #endregion
+}