From 2addc11938b01812599a4c7dca12b53957d623f3 Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Wed, 21 Jan 2026 12:12:42 +0100 Subject: [PATCH 1/5] Make order of generated logger items stable. --- .../gen/LoggerMessageGenerator.Emitter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs index a230e2f0354b80..52fc0c1bd730a5 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs @@ -47,7 +47,7 @@ public string Emit(IReadOnlyList logClasses, CancellationToken canc _builder.AppendLine("// "); _builder.AppendLine("#nullable enable"); - foreach (LoggerClass lc in logClasses) + foreach (LoggerClass lc in logClasses.OrderBy(x => x.Namespace).ThenBy(x => x.Name)) { cancellationToken.ThrowIfCancellationRequested(); GenType(lc); From 43ea879d01c9955bd77059a53fd3ca5397b45c2b Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Wed, 21 Jan 2026 12:25:34 +0100 Subject: [PATCH 2/5] Copilot feedback. --- .../gen/LoggerMessageGenerator.Emitter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs index 52fc0c1bd730a5..6b64d081ded4a6 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs @@ -47,7 +47,7 @@ public string Emit(IReadOnlyList logClasses, CancellationToken canc _builder.AppendLine("// "); _builder.AppendLine("#nullable enable"); - foreach (LoggerClass lc in logClasses.OrderBy(x => x.Namespace).ThenBy(x => x.Name)) + foreach (LoggerClass lc in logClasses.OrderBy(x => x.Namespace, StringComparer.Ordinal).ThenBy(x => x.Name, StringComparer.Ordinal)) { cancellationToken.ThrowIfCancellationRequested(); GenType(lc); From d89c029fd74432df457e3499606bebe1c586dc8c Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Thu, 22 Jan 2026 11:24:16 +0100 Subject: [PATCH 3/5] Move sorting to parser. --- .../gen/LoggerMessageGenerator.Emitter.cs | 2 +- .../gen/LoggerMessageGenerator.Parser.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs index 6b64d081ded4a6..a230e2f0354b80 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Emitter.cs @@ -47,7 +47,7 @@ public string Emit(IReadOnlyList logClasses, CancellationToken canc _builder.AppendLine("// "); _builder.AppendLine("#nullable enable"); - foreach (LoggerClass lc in logClasses.OrderBy(x => x.Namespace, StringComparer.Ordinal).ThenBy(x => x.Name, StringComparer.Ordinal)) + foreach (LoggerClass lc in logClasses) { cancellationToken.ThrowIfCancellationRequested(); GenType(lc); diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Parser.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Parser.cs index 75a63ad8131ce5..03c1bb2026e732 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Parser.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Parser.cs @@ -598,7 +598,7 @@ static bool IsAllowedKind(SyntaxKind kind) => return Array.Empty(); } - return results; + return results.OrderBy(x => x.Namespace, StringComparer.Ordinal).ThenBy(x => x.Name, StringComparer.Ordinal).ToList(); } private static string GenerateClassName(TypeDeclarationSyntax typeDeclaration) From fe8c377148251b227dc7a8a101b3c1a8425a2c1a Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Thu, 22 Jan 2026 13:22:55 +0100 Subject: [PATCH 4/5] Add test. --- ...thMultipleClassesStableOrder.generated.txt | 57 +++++++++++++++++++ .../LoggerMessageGeneratorEmitterTests.cs | 25 ++++++++ 2 files changed, 82 insertions(+) create mode 100644 src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithMultipleClassesStableOrder.generated.txt diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithMultipleClassesStableOrder.generated.txt b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithMultipleClassesStableOrder.generated.txt new file mode 100644 index 00000000000000..0b8ab636f8430d --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/Baselines/TestWithMultipleClassesStableOrder.generated.txt @@ -0,0 +1,57 @@ +// +#nullable enable + +namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses +{ + partial class ClassA + { + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")] + private static readonly global::System.Action __LogACallback = + global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Debug, new global::Microsoft.Extensions.Logging.EventId(1, nameof(LogA)), "Message from ClassA", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true }); + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")] + static partial void LogA(global::Microsoft.Extensions.Logging.ILogger logger) + { + if (logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Debug)) + { + __LogACallback(logger, null); + } + } + } +} +namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses +{ + partial class ClassB + { + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")] + private static readonly global::System.Action __LogBCallback = + global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Information, new global::Microsoft.Extensions.Logging.EventId(2, nameof(LogB)), "Message from ClassB", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true }); + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")] + static partial void LogB(global::Microsoft.Extensions.Logging.ILogger logger) + { + if (logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Information)) + { + __LogBCallback(logger, null); + } + } + } +} +namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses +{ + partial class ClassC + { + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")] + private static readonly global::System.Action __LogCCallback = + global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Warning, new global::Microsoft.Extensions.Logging.EventId(3, nameof(LogC)), "Message from ClassC", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true }); + + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "%VERSION%")] + static partial void LogC(global::Microsoft.Extensions.Logging.ILogger logger) + { + if (logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Warning)) + { + __LogCCallback(logger, null); + } + } + } +} \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorEmitterTests.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorEmitterTests.cs index 96f163ff80f4e2..4fcc33163d6380 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorEmitterTests.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/tests/Microsoft.Extensions.Logging.Generators.Tests/LoggerMessageGeneratorEmitterTests.cs @@ -148,6 +148,31 @@ internal class Class2 { } await VerifyAgainstBaselineUsingFile("TestWithNestedClass.generated.txt", testSourceCode); } + [Fact] + public async Task TestBaseline_TestWithMultipleClassesStableOrder_Success() + { + string testSourceCode = @" +namespace Microsoft.Extensions.Logging.Generators.Tests.TestClasses +{ + internal static partial class ClassC + { + [LoggerMessage(EventId = 3, Level = LogLevel.Warning, Message = ""Message from ClassC"")] + static partial void LogC(ILogger logger); + } + internal static partial class ClassA + { + [LoggerMessage(EventId = 1, Level = LogLevel.Debug, Message = ""Message from ClassA"")] + static partial void LogA(ILogger logger); + } + internal static partial class ClassB + { + [LoggerMessage(EventId = 2, Level = LogLevel.Information, Message = ""Message from ClassB"")] + static partial void LogB(ILogger logger); + } +}"; + await VerifyAgainstBaselineUsingFile("TestWithMultipleClassesStableOrder.generated.txt", testSourceCode); + } + #if ROSLYN4_0_OR_GREATER [Fact] public async Task TestBaseline_TestWithFileScopedNamespace_Success() From d33e0cc803dc784a9b6b5eddb2a62ee544b19715 Mon Sep 17 00:00:00 2001 From: Jiri Cincura Date: Fri, 23 Jan 2026 09:51:01 +0100 Subject: [PATCH 5/5] In-place sort. --- .../gen/LoggerMessageGenerator.Parser.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Parser.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Parser.cs index 03c1bb2026e732..769ba89cc3697a 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Parser.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/gen/LoggerMessageGenerator.Parser.cs @@ -598,7 +598,12 @@ static bool IsAllowedKind(SyntaxKind kind) => return Array.Empty(); } - return results.OrderBy(x => x.Namespace, StringComparer.Ordinal).ThenBy(x => x.Name, StringComparer.Ordinal).ToList(); + results.Sort((lhs, rhs) => + { + int c = StringComparer.Ordinal.Compare(lhs.Namespace, rhs.Namespace); + return c != 0 ? c : StringComparer.Ordinal.Compare(lhs.Name, rhs.Name); + }); + return results; } private static string GenerateClassName(TypeDeclarationSyntax typeDeclaration)