From b19439be2d8d660ac89f8f4fee0144c789997373 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Sat, 5 Sep 2026 18:47:41 +0200 Subject: [PATCH 01/18] Update must reset the ReplacementType --- .../MutateIncomingMessageBehaviorTests.cs | 19 +++++++++++++++++++ .../MutateOutgoingMessageBehaviorTests.cs | 19 +++++++++++++++++++ .../MutateIncomingMessageContext.cs | 2 ++ .../MutateOutgoingMessageContext.cs | 2 ++ 4 files changed, 42 insertions(+) diff --git a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs index f4ca7e73f9..17a60efea4 100644 --- a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs +++ b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs @@ -165,6 +165,25 @@ public async Task When_mutator_uses_the_object_setter_should_use_the_object_over } } + [Test] + public async Task When_typed_replacement_is_followed_by_object_setter_should_use_the_object_overload() + { + var behavior = new MutateIncomingMessageBehavior([]); + + var context = new InterceptUpdateMessageIncomingLogicalMessageContext(); + + context.Services.AddTransient(sp => new MutatorWhichDeclaresAMessageType()); + context.Services.AddTransient(sp => new MutatorWhichMutatesTheBody()); + + await behavior.Invoke(context, ctx => Task.CompletedTask); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.UpdateMessageWithTypeCalled, Is.False); + Assert.That(context.UpdateMessageObjCalled, Is.True); + } + } + class InterceptUpdateMessageIncomingLogicalMessageContext : TestableIncomingLogicalMessageContext { public bool UpdateMessageCalled { get; private set; } diff --git a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehaviorTests.cs b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehaviorTests.cs index 1161d2f18b..5466f00691 100644 --- a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehaviorTests.cs +++ b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehaviorTests.cs @@ -161,6 +161,25 @@ public async Task When_mutator_uses_the_object_setter_should_use_the_object_over } } + [Test] + public async Task When_typed_replacement_is_followed_by_object_setter_should_use_the_object_overload() + { + var behavior = new MutateOutgoingMessageBehavior([]); + + var context = new InterceptUpdateMessageOutgoingLogicalMessageContext(); + + context.Services.AddTransient(sp => new MutatorWhichDeclaresAMessageType()); + context.Services.AddTransient(sp => new MutatorWhichMutatesTheBody()); + + await behavior.Invoke(context, ctx => Task.CompletedTask); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.UpdateMessageWithTypeCalled, Is.False); + Assert.That(context.UpdateMessageObjCalled, Is.True); + } + } + class InterceptUpdateMessageOutgoingLogicalMessageContext : TestableOutgoingLogicalMessageContext { public bool UpdateMessageCalled { get; private set; } diff --git a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs index 3a69dfffb9..e3864da6f7 100644 --- a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs +++ b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs @@ -40,6 +40,8 @@ public object Message { ArgumentNullException.ThrowIfNull(value); MessageInstanceChanged = true; + // The setter declares no message type, so drop any type declared by an earlier replacement. + ReplacementMessageType = null; message = value; } } diff --git a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs index 1804f38e18..e5b40077c6 100644 --- a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs +++ b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs @@ -42,6 +42,8 @@ public object OutgoingMessage { ArgumentNullException.ThrowIfNull(value); MessageInstanceChanged = true; + // The setter declares no message type, so drop any type declared by an earlier replacement. + ReplacementMessageType = null; outgoingMessage = value; } } From 521bf073097502fdad2abb944d031d1bf92af235 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Sat, 5 Sep 2026 18:56:49 +0200 Subject: [PATCH 02/18] Effective dynamic loading diagnostics --- src/NServiceBus.Core/EndpointCreator.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/NServiceBus.Core/EndpointCreator.cs b/src/NServiceBus.Core/EndpointCreator.cs index bdca7d39f4..604f9b1d82 100644 --- a/src/NServiceBus.Core/EndpointCreator.cs +++ b/src/NServiceBus.Core/EndpointCreator.cs @@ -193,13 +193,14 @@ void Configure() void ConfigureMessageTypes(IEnumerable messageTypesHandled) { - var allowDynamicTypeLoading = settings.IsDynamicTypeLoadingEnabled(); + var configuredDynamicTypeLoading = settings.IsDynamicTypeLoadingEnabled(); var strictMode = settings.Get().StrictRegisteredOnlyMode; var messageMetadataRegistry = settings.GetOrCreate(); // Strict mode is the stronger non-overridable policy: it must be in effect before Initialize so // pre-initialization registrations are enforced against it, and it disables dynamic type loading. + var allowDynamicTypeLoading = configuredDynamicTypeLoading && !strictMode; messageMetadataRegistry.StrictRegisteredOnlyMode = strictMode; - messageMetadataRegistry.Initialize(conventions.IsMessageType, allowDynamicTypeLoading && !strictMode); + messageMetadataRegistry.Initialize(conventions.IsMessageType, allowDynamicTypeLoading); messageMetadataRegistry.RegisterMessageTypes(hostingConfiguration.AvailableTypes); messageMetadataRegistry.RegisterMessageTypesBypassingChecks(messageTypesHandled); From 4410820ca4a84b8120ae42dc51634f82c9719556 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Sat, 5 Sep 2026 18:56:59 +0200 Subject: [PATCH 03/18] Add mutator coverage --- src/TrimmedEndpoint/Program.cs | 44 ++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/TrimmedEndpoint/Program.cs b/src/TrimmedEndpoint/Program.cs index 34f5694179..5c7abc7e4e 100644 --- a/src/TrimmedEndpoint/Program.cs +++ b/src/TrimmedEndpoint/Program.cs @@ -2,6 +2,7 @@ using System.Text.Json.Serialization; using NServiceBus.Features; using NServiceBus.Installation; +using NServiceBus.MessageMutator; using NServiceBus.Pipeline; #if INCLUDE_SAGA @@ -42,6 +43,9 @@ configuration.EnableFeature(); configuration.EnableInstallers(); configuration.AddHandler(); +// Exercise the typed replacement APIs so their trimming-sensitive, annotated paths are covered. +configuration.RegisterMessageMutator(new ReplacesIncomingMessageInstance()); +configuration.RegisterMessageMutator(new ReplacesOutgoingMessage()); #if INCLUDE_SAGA configuration.AddMessageType(); configuration.AddMessageType(); @@ -133,6 +137,12 @@ return 5; } +if (!ReplacesIncomingMessageInstance.Replaced || !MyHandler.ReceivedReplacedInstance || !ReplacesOutgoingMessage.Replaced) +{ + Console.Error.WriteLine($"IncomingReplaced={ReplacesIncomingMessageInstance.Replaced} HandlerReceivedReplacedInstance={MyHandler.ReceivedReplacedInstance} OutgoingReplaced={ReplacesOutgoingMessage.Replaced}"); + return 6; +} + Console.WriteLine("TRIM-VALIDATION-SUCCESS"); return 0; @@ -161,10 +171,44 @@ static bool ContainsStrictModeMessage(Exception exception) public class MyHandler : IHandleMessages { public static bool Invoked; + public static bool ReceivedReplacedInstance; public Task Handle(MyCommand message, IMessageHandlerContext context) { Invoked = true; + ReceivedReplacedInstance = message.SomeValue == "replaced"; + return Task.CompletedTask; + } +} + +public sealed class ReplacesIncomingMessageInstance : IMutateIncomingMessages +{ + public static bool Replaced; + + public Task MutateIncoming(MutateIncomingMessageContext context) + { + if (context.Message is MyCommand) + { + context.UpdateMessageInstance(new MyCommand { SomeValue = "replaced" }); + Replaced = true; + } + + return Task.CompletedTask; + } +} + +public sealed class ReplacesOutgoingMessage : IMutateOutgoingMessages +{ + public static bool Replaced; + + public Task MutateOutgoing(MutateOutgoingMessageContext context) + { + if (context.OutgoingMessage is OutgoingCommand) + { + context.UpdateMessage(new OutgoingCommand { SomeValue = "replaced" }, typeof(OutgoingCommand)); + Replaced = true; + } + return Task.CompletedTask; } } From 16b4f9812a06b53c4a2d4b1aeeb9e8574f4c7d32 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Sat, 5 Sep 2026 19:23:39 +0200 Subject: [PATCH 04/18] Simplify registration to facilitate source gen registration under the hood --- src/TrimmedEndpoint/Program.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/TrimmedEndpoint/Program.cs b/src/TrimmedEndpoint/Program.cs index 5c7abc7e4e..7c24eb8479 100644 --- a/src/TrimmedEndpoint/Program.cs +++ b/src/TrimmedEndpoint/Program.cs @@ -32,9 +32,10 @@ }); #endif +// Only send-only message types need explicit registration: types handled by AddHandler/AddSaga are +// registered by the generated code, and duplicating them here would mask failures in that automatic path. // Duplicate registration must be harmless (first registration wins). -configuration.AddMessageType(); -configuration.AddMessageType(); +configuration.AddMessageType(); configuration.AddMessageType(); configuration.AddMessageType(); configuration.Conventions().DefiningCommandsAs(type => @@ -47,9 +48,6 @@ configuration.RegisterMessageMutator(new ReplacesIncomingMessageInstance()); configuration.RegisterMessageMutator(new ReplacesOutgoingMessage()); #if INCLUDE_SAGA -configuration.AddMessageType(); -configuration.AddMessageType(); -configuration.AddMessageType(); configuration.AddSaga(); #endif @@ -205,7 +203,7 @@ public Task MutateOutgoing(MutateOutgoingMessageContext context) { if (context.OutgoingMessage is OutgoingCommand) { - context.UpdateMessage(new OutgoingCommand { SomeValue = "replaced" }, typeof(OutgoingCommand)); + context.UpdateMessage(new OutgoingCommand { SomeValue = "replaced" }); Replaced = true; } From f7301a3edcb54b854297f5eaff3e2ecbb355c5be Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Sat, 5 Sep 2026 19:45:21 +0200 Subject: [PATCH 05/18] All public APIs that use those sources are properly annotated so we can suppress it unconditionally. --- src/NServiceBus.Core/Routing/AssemblyRouteSource.cs | 6 ++++-- .../AssemblyPublisherSource.cs | 9 +++++---- .../NamespacePublisherSource.cs | 10 ++++++---- src/NServiceBus.Core/Routing/NamespaceRouteSource.cs | 6 ++++-- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/NServiceBus.Core/Routing/AssemblyRouteSource.cs b/src/NServiceBus.Core/Routing/AssemblyRouteSource.cs index 8bc47a24d6..36ad78ee2c 100644 --- a/src/NServiceBus.Core/Routing/AssemblyRouteSource.cs +++ b/src/NServiceBus.Core/Routing/AssemblyRouteSource.cs @@ -19,10 +19,12 @@ public AssemblyRouteSource(Assembly messageAssembly, UnicastRoute route) this.route = route; } - [RequiresUnreferencedCode(TrimmingMessage)] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Scanning the configured assembly is intentional; this source can only be constructed through APIs annotated with RequiresUnreferencedCode.")] + static Type[] ScanAssemblyTypes(Assembly assembly) => assembly.GetTypes(); + public IEnumerable GenerateRoutes(Conventions conventions) { - var routes = messageAssembly.GetTypes() + var routes = ScanAssemblyTypes(messageAssembly) .Where(t => conventions.IsMessageType(t)) .Select(t => new RouteTableEntry(t, route)) .ToArray(); diff --git a/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/AssemblyPublisherSource.cs b/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/AssemblyPublisherSource.cs index 0226815f13..5c5b8b7d01 100644 --- a/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/AssemblyPublisherSource.cs +++ b/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/AssemblyPublisherSource.cs @@ -19,10 +19,12 @@ public AssemblyPublisherSource(Assembly messageAssembly, PublisherAddress addres this.address = address; } - [RequiresUnreferencedCode(TrimmingMessage)] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Scanning the configured assembly is intentional; this source can only be constructed through APIs annotated with RequiresUnreferencedCode.")] + static Type[] ScanAssemblyTypes(Assembly assembly) => assembly.GetTypes(); + public IEnumerable GenerateWithBestPracticeEnforcement(Conventions conventions) { - var entries = messageAssembly.GetTypes() + var entries = ScanAssemblyTypes(messageAssembly) .Where(conventions.IsEventType) .Select(t => new PublisherTableEntry(t, address)) .ToArray(); @@ -35,10 +37,9 @@ public IEnumerable GenerateWithBestPracticeEnforcement(Conv return entries; } - [RequiresUnreferencedCode(TrimmingMessage)] public IEnumerable GenerateWithoutBestPracticeEnforcement(Conventions conventions) { - var entries = messageAssembly.GetTypes() + var entries = ScanAssemblyTypes(messageAssembly) .Where(type => conventions.IsMessageType(type) && !conventions.IsCommandType(type)) .Select(t => new PublisherTableEntry(t, address)) .ToArray(); diff --git a/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/NamespacePublisherSource.cs b/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/NamespacePublisherSource.cs index 29c3e80bbe..2b165532a1 100644 --- a/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/NamespacePublisherSource.cs +++ b/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/NamespacePublisherSource.cs @@ -13,6 +13,7 @@ class NamespacePublisherSource : IPublisherSource readonly string messageNamespace; readonly PublisherAddress address; + [RequiresUnreferencedCode(AssemblyPublisherSource.TrimmingMessage)] public NamespacePublisherSource(Assembly messageAssembly, string messageNamespace, PublisherAddress address) { this.messageAssembly = messageAssembly; @@ -20,10 +21,12 @@ public NamespacePublisherSource(Assembly messageAssembly, string messageNamespac this.messageNamespace = messageNamespace; } - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The public namespace publisher API is annotated with RequiresUnreferencedCode because this source intentionally scans the configured assembly.")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Scanning the configured assembly is intentional; this source can only be constructed through APIs annotated with RequiresUnreferencedCode.")] + static Type[] ScanAssemblyTypes(Assembly assembly) => assembly.GetTypes(); + public IEnumerable GenerateWithBestPracticeEnforcement(Conventions conventions) { - var entries = messageAssembly.GetTypes() + var entries = ScanAssemblyTypes(messageAssembly) .Where(t => conventions.IsEventType(t) && string.Equals(t.Namespace, messageNamespace, StringComparison.OrdinalIgnoreCase)) .Select(t => new PublisherTableEntry(t, address)) .ToArray(); @@ -36,10 +39,9 @@ public IEnumerable GenerateWithBestPracticeEnforcement(Conv return entries; } - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The public namespace publisher API is annotated with RequiresUnreferencedCode because this source intentionally scans the configured assembly.")] public IEnumerable GenerateWithoutBestPracticeEnforcement(Conventions conventions) { - var entries = messageAssembly.GetTypes() + var entries = ScanAssemblyTypes(messageAssembly) .Where(t => conventions.IsMessageType(t) && !conventions.IsCommandType(t) && string.Equals(t.Namespace, messageNamespace, StringComparison.OrdinalIgnoreCase)) .Select(t => new PublisherTableEntry(t, address)) .ToArray(); diff --git a/src/NServiceBus.Core/Routing/NamespaceRouteSource.cs b/src/NServiceBus.Core/Routing/NamespaceRouteSource.cs index f77fd23aed..ba4329002e 100644 --- a/src/NServiceBus.Core/Routing/NamespaceRouteSource.cs +++ b/src/NServiceBus.Core/Routing/NamespaceRouteSource.cs @@ -21,10 +21,12 @@ public NamespaceRouteSource(Assembly messageAssembly, string messageNamespace, U this.messageNamespace = messageNamespace; } - [RequiresUnreferencedCode(AssemblyRouteSource.TrimmingMessage)] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Scanning the configured assembly is intentional; this source can only be constructed through APIs annotated with RequiresUnreferencedCode.")] + static Type[] ScanAssemblyTypes(Assembly assembly) => assembly.GetTypes(); + public IEnumerable GenerateRoutes(Conventions conventions) { - var routes = messageAssembly.GetTypes() + var routes = ScanAssemblyTypes(messageAssembly) .Where(t => conventions.IsMessageType(t) && string.Equals(t.Namespace, messageNamespace, StringComparison.OrdinalIgnoreCase)) .Select(t => new RouteTableEntry(t, route)) .ToArray(); From 538a7240a67d0c4c895bda8cd42469d64159ae11 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Sat, 5 Sep 2026 20:04:27 +0200 Subject: [PATCH 06/18] Enable trimming analyzers for now --- .../API/TrimmabilityWarnings.cs | 130 ------------------ ...s.ApproveTrimmabilityWarnings.approved.txt | 13 -- .../TrimmedEndpointTests.cs | 4 +- src/NServiceBus.Core/NServiceBus.Core.csproj | 1 + 4 files changed, 3 insertions(+), 145 deletions(-) delete mode 100644 src/NServiceBus.Core.Tests/API/TrimmabilityWarnings.cs delete mode 100644 src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt diff --git a/src/NServiceBus.Core.Tests/API/TrimmabilityWarnings.cs b/src/NServiceBus.Core.Tests/API/TrimmabilityWarnings.cs deleted file mode 100644 index 7fbd402b32..0000000000 --- a/src/NServiceBus.Core.Tests/API/TrimmabilityWarnings.cs +++ /dev/null @@ -1,130 +0,0 @@ -namespace NServiceBus.Core.Tests.API; - -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using NUnit.Framework; -using Particular.Approvals; - -// As part of the assembly scanning efforts, many code paths that require dynamically -// referenced code have been annotated or restructured to satisfy the trimming analyzer. -// This test captures the current set of trimming warnings so that any new regressions -// are immediately visible. When the test fails because new warnings appeared, revisit -// the changes and consider whether the dynamic access can be avoided. It is acceptable -// to approve new warnings in minor releases when truly necessary, but the goal is to -// keep the list shrinking over time and never grow without deliberate justification. -// Once all warnings are resolved and the approved file is empty, this test can be -// deleted and trimming warnings can be enabled directly in NServiceBus.Core.csproj. -// -// To enable the analyzer to see the warnings in your IDE, add this to the at the -// top of NServiceBus.Core.csproj: -// true -// -// See https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/prepare-libraries-for-trimming for more details. -[TestFixture] -public partial class TrimmabilityWarnings -{ - [Test] - [CancelAfter(30_000)] - public async Task ApproveTrimmabilityWarnings(CancellationToken cancellationToken = default) - { - var projectPath = Path.GetFullPath(Path.Combine( - TestContext.CurrentContext.TestDirectory, - "..", "..", "..", "..", - "NServiceBus.Core", - "NServiceBus.Core.csproj")); - - var warnings = await BuildWithTrimmingAnalyzerEnabled(projectPath, cancellationToken); - - Approver.Verify(warnings); - } - - static async Task BuildWithTrimmingAnalyzerEnabled(string projectPath, CancellationToken cancellationToken = default) - { - var startInfo = new ProcessStartInfo - { - FileName = "dotnet", - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - }; - - startInfo.ArgumentList.Add("build"); - startInfo.ArgumentList.Add(projectPath); - startInfo.ArgumentList.Add("-c:Release"); - startInfo.ArgumentList.Add("-p:EnableTrimAnalyzer=true"); - startInfo.ArgumentList.Add("-p:TreatWarningsAsErrors=false"); - startInfo.ArgumentList.Add("-p:IsPackable=false"); - - using var process = Process.Start(startInfo)!; - - var outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken); - var errorTask = process.StandardError.ReadToEndAsync(cancellationToken); - - await process.WaitForExitAsync(cancellationToken); - - var output = await outputTask; - var error = await errorTask; - - Assert.That(process.ExitCode, Is.Zero, $"Build failed:{Environment.NewLine}{error}{Environment.NewLine}{output}"); - - var warnings = ILWarningRegex().Matches(output) - .Select(m => ScrubLine(m.Value.Trim())) - .Distinct() - .OrderBy(w => w, StringComparer.Ordinal) - .ToList(); - - var grouped = warnings - .GroupBy(w => FileRegex().Match(w).Groups["file"].Value) - .OrderBy(g => g.Key, StringComparer.Ordinal); - - var result = new StringBuilder() - .AppendLine("The following trimming warnings are present in NServiceBus.Core.") - .AppendLine("Changes that make this list longer should not be approved.") - .AppendLine("-----"); - - foreach (var group in grouped) - { - _ = result.AppendLine().AppendLine(group.Key); - foreach (var warning in group) - { - _ = result.AppendLine($" {MessageRegex().Match(warning).Groups["msg"].Value}"); - } - } - - return result.ToString(); - } - - static string ScrubLine(string line) - { - line = PathPrefixRegex().Replace(line, "", 1); - line = line.Replace('\\', '/'); - line = LineNumbersRegex().Replace(line, ""); - line = ProjectPathSuffixRegex().Replace(line, ""); - return line; - } - - [GeneratedRegex(@"^.+?(?=src[\\/])", RegexOptions.IgnoreCase)] - private static partial Regex PathPrefixRegex(); - - [GeneratedRegex(@"\(\d+,\d+\)")] - private static partial Regex LineNumbersRegex(); - - [GeneratedRegex(@"\s*\[[^\]]+[/\\][^\]]+\]$")] - private static partial Regex ProjectPathSuffixRegex(); - - [GeneratedRegex(@".+: warning IL[23]\d{3}.+")] - private static partial Regex ILWarningRegex(); - - [GeneratedRegex(@"^(?src/[^\s:]+)")] - private static partial Regex FileRegex(); - - [GeneratedRegex(@": warning (?IL[23]\d{3}:.+)$")] - private static partial Regex MessageRegex(); -} diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt deleted file mode 100644 index c835831f11..0000000000 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt +++ /dev/null @@ -1,13 +0,0 @@ -The following trimming warnings are present in NServiceBus.Core. -Changes that make this list longer should not be approved. ------ - -src/NServiceBus.Core/Routing/AssemblyRouteSource.cs - IL2046: Member 'NServiceBus.AssemblyRouteSource.GenerateRoutes(Conventions)' with 'RequiresUnreferencedCodeAttribute' implements interface member 'NServiceBus.IRouteSource.GenerateRoutes(Conventions)' without 'RequiresUnreferencedCodeAttribute'. 'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides. - -src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/AssemblyPublisherSource.cs - IL2046: Member 'NServiceBus.AssemblyPublisherSource.GenerateWithBestPracticeEnforcement(Conventions)' with 'RequiresUnreferencedCodeAttribute' implements interface member 'NServiceBus.IPublisherSource.GenerateWithBestPracticeEnforcement(Conventions)' without 'RequiresUnreferencedCodeAttribute'. 'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides. - IL2046: Member 'NServiceBus.AssemblyPublisherSource.GenerateWithoutBestPracticeEnforcement(Conventions)' with 'RequiresUnreferencedCodeAttribute' implements interface member 'NServiceBus.IPublisherSource.GenerateWithoutBestPracticeEnforcement(Conventions)' without 'RequiresUnreferencedCodeAttribute'. 'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides. - -src/NServiceBus.Core/Routing/NamespaceRouteSource.cs - IL2046: Member 'NServiceBus.NamespaceRouteSource.GenerateRoutes(Conventions)' with 'RequiresUnreferencedCodeAttribute' implements interface member 'NServiceBus.IRouteSource.GenerateRoutes(Conventions)' without 'RequiresUnreferencedCodeAttribute'. 'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides. diff --git a/src/NServiceBus.Core.Tests/TrimmedEndpointTests.cs b/src/NServiceBus.Core.Tests/TrimmedEndpointTests.cs index 37f8dcf356..1f2d7ffd9c 100644 --- a/src/NServiceBus.Core.Tests/TrimmedEndpointTests.cs +++ b/src/NServiceBus.Core.Tests/TrimmedEndpointTests.cs @@ -35,8 +35,8 @@ public async Task Scanner_disabled_endpoint_publishes_trimmed_and_processes_a_me // The AddMessageType and AddHandler calls in the sample are intercepted by source generators. If they // were not intercepted, the RequiresUnreferencedCode fallback would surface as IL2026 trim warnings at - // the sample's own call sites. Trim warnings inside NServiceBus.Core itself are tracked separately by - // the TrimmabilityWarnings approval test. + // the sample's own call sites. Trim warnings inside NServiceBus.Core itself fail the Core build via + // EnableTrimAnalyzer. var sampleTrimWarnings = publishResult.Output.Split(Environment.NewLine) .Where(line => line.Contains("Program.cs") && line.Contains("IL2026")) .ToArray(); diff --git a/src/NServiceBus.Core/NServiceBus.Core.csproj b/src/NServiceBus.Core/NServiceBus.Core.csproj index d505a9ea8d..deb0791a4e 100644 --- a/src/NServiceBus.Core/NServiceBus.Core.csproj +++ b/src/NServiceBus.Core/NServiceBus.Core.csproj @@ -6,6 +6,7 @@ true ..\NServiceBus.snk true + true From 9d4ffa1d7ee692590167ad66225d7cde454aebae Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Sat, 5 Sep 2026 20:08:33 +0200 Subject: [PATCH 07/18] Mark as trimmable --- src/NServiceBus.Core/NServiceBus.Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NServiceBus.Core/NServiceBus.Core.csproj b/src/NServiceBus.Core/NServiceBus.Core.csproj index deb0791a4e..8afcc88554 100644 --- a/src/NServiceBus.Core/NServiceBus.Core.csproj +++ b/src/NServiceBus.Core/NServiceBus.Core.csproj @@ -6,7 +6,7 @@ true ..\NServiceBus.snk true - true + true From 1c723f569968aa23939d352fb36751e8fc66562a Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Sat, 5 Sep 2026 21:22:01 +0200 Subject: [PATCH 08/18] XmlSerialization subsystem honest annotations and improved message. Also moved the delegate factory because it should only be used there and no longer be a util. --- ...IApprovals.ApproveNServiceBus.approved.txt | 5 +++- .../XML}/DelegateFactory.cs | 2 ++ .../Serializers/XML/XmlDeserialization.cs | 1 + .../Serializers/XML/XmlMessageSerializer.cs | 1 + .../Serializers/XML/XmlSanitizingStream.cs | 7 +---- .../Serializers/XML/XmlSerialization.cs | 30 +++++-------------- .../Serializers/XML/XmlSerializer.cs | 5 +++- .../Serializers/XML/XmlSerializerCache.cs | 1 + 8 files changed, 21 insertions(+), 31 deletions(-) rename src/NServiceBus.Core/{Utils/Reflection => Serializers/XML}/DelegateFactory.cs (98%) diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index 3b421c88eb..1119e36be4 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -1356,7 +1356,10 @@ namespace NServiceBus public static NServiceBus.Serialization.SerializationExtensions Namespace(this NServiceBus.Serialization.SerializationExtensions config, string namespaceToUse) { } public static NServiceBus.Serialization.SerializationExtensions SanitizeInput(this NServiceBus.Serialization.SerializationExtensions config) { } } - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("XmlSerializer is not supported in trimming scenarios.")] + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("XmlSerializer relies on dynamic code generation which is not available with Ahead" + + " of Time compilation")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("XmlSerializer is not supported in trimming scenarios and members from deserialize" + + "d types may be trimmed if not referenced directly.")] public class XmlSerializer : NServiceBus.Serialization.SerializationDefinition { public XmlSerializer() { } diff --git a/src/NServiceBus.Core/Utils/Reflection/DelegateFactory.cs b/src/NServiceBus.Core/Serializers/XML/DelegateFactory.cs similarity index 98% rename from src/NServiceBus.Core/Utils/Reflection/DelegateFactory.cs rename to src/NServiceBus.Core/Serializers/XML/DelegateFactory.cs index 7ec89c0460..95c2f20623 100644 --- a/src/NServiceBus.Core/Utils/Reflection/DelegateFactory.cs +++ b/src/NServiceBus.Core/Serializers/XML/DelegateFactory.cs @@ -4,10 +4,12 @@ namespace NServiceBus; using System; using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Reflection; using System.Reflection.Emit; +[RequiresDynamicCode(XmlSerializer.DynamicCodeMessage)] static class DelegateFactory { public static Func CreateGet(PropertyInfo property) diff --git a/src/NServiceBus.Core/Serializers/XML/XmlDeserialization.cs b/src/NServiceBus.Core/Serializers/XML/XmlDeserialization.cs index d903bfb0b1..e44533ef83 100644 --- a/src/NServiceBus.Core/Serializers/XML/XmlDeserialization.cs +++ b/src/NServiceBus.Core/Serializers/XML/XmlDeserialization.cs @@ -13,6 +13,7 @@ using Logging; using MessageInterfaces; +[RequiresDynamicCode(XmlSerializer.DynamicCodeMessage)] [RequiresUnreferencedCode(XmlSerializer.TrimmingMessage)] class XmlDeserialization(IMessageMapper mapper, XmlSerializerCache cache, bool skipWrappingRawXml, bool sanitizeInput) { diff --git a/src/NServiceBus.Core/Serializers/XML/XmlMessageSerializer.cs b/src/NServiceBus.Core/Serializers/XML/XmlMessageSerializer.cs index 479675970a..f0da9758bf 100644 --- a/src/NServiceBus.Core/Serializers/XML/XmlMessageSerializer.cs +++ b/src/NServiceBus.Core/Serializers/XML/XmlMessageSerializer.cs @@ -8,6 +8,7 @@ namespace NServiceBus; using MessageInterfaces; using Serialization; +[RequiresDynamicCode(XmlSerializer.DynamicCodeMessage)] [RequiresUnreferencedCode(XmlSerializer.TrimmingMessage)] class XmlMessageSerializer : IMessageSerializer { diff --git a/src/NServiceBus.Core/Serializers/XML/XmlSanitizingStream.cs b/src/NServiceBus.Core/Serializers/XML/XmlSanitizingStream.cs index aaa66d58b1..82405d1022 100644 --- a/src/NServiceBus.Core/Serializers/XML/XmlSanitizingStream.cs +++ b/src/NServiceBus.Core/Serializers/XML/XmlSanitizingStream.cs @@ -7,13 +7,8 @@ namespace NServiceBus; // A StreamReader that excludes XML-illegal characters while reading. [RequiresUnreferencedCode(XmlSerializer.TrimmingMessage)] -class XmlSanitizingStream : StreamReader +class XmlSanitizingStream(Stream streamToSanitize) : StreamReader(streamToSanitize, true) { - public XmlSanitizingStream(Stream streamToSanitize) - : base(streamToSanitize, true) - { - } - public static bool IsLegalXmlChar(string xmlVersion, int character) { switch (xmlVersion) diff --git a/src/NServiceBus.Core/Serializers/XML/XmlSerialization.cs b/src/NServiceBus.Core/Serializers/XML/XmlSerialization.cs index ff5d1fd795..91f914e0b7 100644 --- a/src/NServiceBus.Core/Serializers/XML/XmlSerialization.cs +++ b/src/NServiceBus.Core/Serializers/XML/XmlSerialization.cs @@ -11,23 +11,11 @@ using System.Xml; using System.Xml.Linq; +[RequiresDynamicCode(XmlSerializer.DynamicCodeMessage)] [RequiresUnreferencedCode(XmlSerializer.TrimmingMessage)] -sealed class XmlSerialization : IDisposable +sealed class XmlSerialization(Type messageType, Stream stream, object message, Conventions conventions, XmlSerializerCache cache, bool skipWrappingRawXml, string @namespace = XmlSerialization.DefaultNamespace) + : IDisposable { - public XmlSerialization(Type messageType, Stream stream, object message, Conventions conventions, XmlSerializerCache cache, bool skipWrappingRawXml, string @namespace = DefaultNamespace) - { - this.messageType = messageType; - this.message = message; - this.conventions = conventions; - this.cache = cache; - this.skipWrappingRawXml = skipWrappingRawXml; - this.@namespace = @namespace; - writer = new RawXmlTextWriter(stream, new XmlWriterSettings - { - CloseOutput = false - }); - } - public void Serialize() { var doc = new XDocument(new XDeclaration("1.0", null, null)); @@ -329,14 +317,10 @@ public void Dispose() bool disposed; - readonly XmlSerializerCache cache; - readonly Conventions conventions; - readonly object message; - - readonly Type messageType; - readonly string @namespace; - readonly bool skipWrappingRawXml; - readonly RawXmlTextWriter writer; + readonly RawXmlTextWriter writer = new(stream, new XmlWriterSettings + { + CloseOutput = false + }); const string BaseType = "baseType"; const string DefaultNamespace = "http://tempuri.net"; diff --git a/src/NServiceBus.Core/Serializers/XML/XmlSerializer.cs b/src/NServiceBus.Core/Serializers/XML/XmlSerializer.cs index 218b15c5ed..57489a494b 100644 --- a/src/NServiceBus.Core/Serializers/XML/XmlSerializer.cs +++ b/src/NServiceBus.Core/Serializers/XML/XmlSerializer.cs @@ -11,10 +11,10 @@ /// /// Defines the capabilities of the XML serializer. /// +[RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] public class XmlSerializer : SerializationDefinition { - internal const string TrimmingMessage = "XmlSerializer is not supported in trimming scenarios."; /// /// Provides a factory method for building a message serializer. /// @@ -52,4 +52,7 @@ public override Func Configure(IReadOnlySett internal const string CustomNamespaceConfigurationKey = "XmlSerializer.CustomNamespace"; internal const string SkipWrappingRawXml = "XmlSerializer.SkipWrappingRawXml"; internal const string SanitizeInput = "XmlSerializer.SanitizeInput"; + + internal const string TrimmingMessage = "XmlSerializer is not supported in trimming scenarios and members from deserialized types may be trimmed if not referenced directly."; + internal const string DynamicCodeMessage = "XmlSerializer relies on dynamic code generation which is not available with Ahead of Time compilation"; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Serializers/XML/XmlSerializerCache.cs b/src/NServiceBus.Core/Serializers/XML/XmlSerializerCache.cs index d59e913cda..a652d08b25 100644 --- a/src/NServiceBus.Core/Serializers/XML/XmlSerializerCache.cs +++ b/src/NServiceBus.Core/Serializers/XML/XmlSerializerCache.cs @@ -11,6 +11,7 @@ namespace NServiceBus; using System.Xml.Serialization; using Logging; +[RequiresDynamicCode(XmlSerializer.DynamicCodeMessage)] [RequiresUnreferencedCode(XmlSerializer.TrimmingMessage)] class XmlSerializerCache { From 6ab696cca19e0c7623a0d022bebe192585f6a5dc Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Sat, 5 Sep 2026 21:32:29 +0200 Subject: [PATCH 09/18] Remove unnecessary make generic type --- .../Utils/Reflection/TypeExtensionMethods.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/NServiceBus.Core/Utils/Reflection/TypeExtensionMethods.cs b/src/NServiceBus.Core/Utils/Reflection/TypeExtensionMethods.cs index c4b0a3a26d..5c26300742 100644 --- a/src/NServiceBus.Core/Utils/Reflection/TypeExtensionMethods.cs +++ b/src/NServiceBus.Core/Utils/Reflection/TypeExtensionMethods.cs @@ -69,12 +69,9 @@ public string SerializationFriendlyName() => } } - if (args.Length == 2) + if (args.Length == 2 && t.IsGenericType && t.GetGenericTypeDefinition() == typeof(KeyValuePair<,>)) { - if (typeof(KeyValuePair<,>).MakeGenericType(args[0], args[1]) == t) - { - result = "NServiceBus." + result; - } + result = "NServiceBus." + result; } return result; From 8dcd41bd63aa01e5bf8d00c07111e6e5a25946d7 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Sat, 5 Sep 2026 21:38:29 +0200 Subject: [PATCH 10/18] Refactor JSON serialization logic and update suppression attributes for trimming --- .../HostStartupDiagnosticsWriter.cs | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriter.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriter.cs index a589c63cad..c2229fa3b3 100644 --- a/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriter.cs +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriter.cs @@ -87,7 +87,6 @@ .. deduplicated ]; } - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The legacy reflection-based path is guarded by JsonSerializer.IsReflectionEnabledByDefault check; throws before reaching this call when reflection is disabled.")] static string SerializeToJson(List resolvedEntries, bool forLog) { var buffer = new ArrayBufferWriter(); @@ -115,15 +114,7 @@ static string SerializeToJson(List resolvedEntries, bool forLog) } else { - // Legacy path: use reflection-based serialization with the custom options - if (!JsonSerializer.IsReflectionEnabledByDefault) - { - throw new InvalidOperationException( - $"Startup diagnostics section '{entry.Name}' was registered without JSON type metadata. " + - "Use the overload accepting JsonTypeInfo when reflection serialization is disabled."); - } - - JsonSerializer.Serialize(writer, value, diagnosticsOptions); + SerializeWithReflection(writer, value, entry.Name); } } @@ -131,6 +122,20 @@ static string SerializeToJson(List resolvedEntries, bool forLog) writer.Flush(); return Encoding.UTF8.GetString(buffer.WrittenSpan); + + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Guarded by JsonSerializer.IsReflectionEnabledByDefault check; throws before reaching this call when reflection is disabled.")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Guarded by JsonSerializer.IsReflectionEnabledByDefault check; throws before reaching this call when reflection is disabled.")] + static void SerializeWithReflection(Utf8JsonWriter jsonWriter, object? entryValue, string entryName) + { + if (!JsonSerializer.IsReflectionEnabledByDefault) + { + throw new InvalidOperationException( + $"Startup diagnostics section '{entryName}' was registered without JSON type metadata. " + + "Use the overload accepting JsonTypeInfo when reflection serialization is disabled."); + } + + JsonSerializer.Serialize(jsonWriter, entryValue, diagnosticsOptions); + } } static IEnumerable DeduplicateEntries(List entries) From 1aa469845020f485983616460df5763a9b5b3837 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Sat, 5 Sep 2026 21:43:57 +0200 Subject: [PATCH 11/18] Suppress FileVersionRetriever because the code handles graceful fallback --- src/NServiceBus.Core/Utils/FileVersionRetriever.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/NServiceBus.Core/Utils/FileVersionRetriever.cs b/src/NServiceBus.Core/Utils/FileVersionRetriever.cs index cef30c9f37..cd71d3e884 100644 --- a/src/NServiceBus.Core/Utils/FileVersionRetriever.cs +++ b/src/NServiceBus.Core/Utils/FileVersionRetriever.cs @@ -4,12 +4,14 @@ namespace NServiceBus; using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Reflection; static class FileVersionRetriever { public static string GetFileVersion(Type type) => GetFileVersion(type.Assembly); + [UnconditionalSuppressMessage("SingleFile", "IL3000", Justification = "Location is checked for empty string before use; falls back to AssemblyFileVersionAttribute or assembly name version when running as a single-file app.")] public static string GetFileVersion(Assembly assembly) { if (!string.IsNullOrEmpty(assembly.Location)) From 5966c2d049b8a3bb7ea545203ca05292b281d930 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Sat, 5 Sep 2026 23:20:58 +0200 Subject: [PATCH 12/18] MessageHandlerRegistry path invokes generic methods and therefore also requires dynamic code. The source generated path doesn't require that so suppress --- .../AddHandlerInterceptorSuppressorTests.cs | 67 +++++++++ .../Helpers/MockTrimmingAnalyzer.cs | 25 +++- .../AddSagaInterceptorSuppressorTests.cs | 136 ++++++++++++++++++ .../AddHandlerInterceptor.Suppressor.cs | 16 ++- .../Sagas/AddSagaInterceptor.Suppressor.cs | 18 ++- .../SupressionIds.cs | 2 + ...IApprovals.ApproveNServiceBus.approved.txt | 20 ++- src/NServiceBus.Core/EndpointConfiguration.cs | 2 +- src/NServiceBus.Core/EndpointCreator.cs | 12 +- src/NServiceBus.Core/Sagas/SagaComponent.cs | 1 + src/NServiceBus.Core/Sagas/SagaMetadata.cs | 5 +- .../MessageHandlerRegistrationExtensions.cs | 1 + .../Config/SagaRegistrationExtensions.cs | 1 + .../Unicast/MessageHandlerRegistry.cs | 17 ++- .../Utils/Reflection/MethodInfoExtensions.cs | 6 + 15 files changed, 297 insertions(+), 32 deletions(-) diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerInterceptorSuppressorTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerInterceptorSuppressorTests.cs index cfd120cd55..4df73114cc 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerInterceptorSuppressorTests.cs +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerInterceptorSuppressorTests.cs @@ -42,6 +42,40 @@ public class SampleCommand : ICommand { } Assert.That(diagnostics, Does.Not.Contain("IL2026")); } + [Test] + public void SuppressesIL3050ForAddHandler() + { + var source = """ + using System.Threading.Tasks; + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.AddHandler(); + } + } + + public class SampleHandler : IHandleMessages + { + public Task Handle(SampleCommand cmd, IMessageHandlerContext context) => Task.CompletedTask; + } + + public class SampleCommand : ICommand { } + """; + + var result = SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .WithAnalyzer() + .WithSuppressor() + .Run(); + + var diagnostics = result.GetCompilationOutput(); + + Assert.That(diagnostics, Does.Not.Contain("IL3050")); + } + [Test] public void DoesNotSuppressIL2026ForNonAddHandlerCalls() { @@ -74,4 +108,37 @@ public void SomeOtherMethod() { } Assert.That(diagnostics, Does.Contain("IL2026")); } + + [Test] + public void DoesNotSuppressIL3050ForNonAddHandlerCalls() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + // This call should still produce IL3050 since it's not intercepted + SomeOtherMethod(); + } + + [RequiresDynamicCode("Test method")] + public void SomeOtherMethod() { } + } + """; + + var result = SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .WithAnalyzer() + .WithSuppressor() + .SuppressDiagnosticErrors() + .SuppressCompilationErrors() + .Run(); + + var diagnostics = result.GetCompilationOutput(); + + Assert.That(diagnostics, Does.Contain("IL3050")); + } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Helpers/MockTrimmingAnalyzer.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Helpers/MockTrimmingAnalyzer.cs index afbbfa97f1..9899d57354 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Helpers/MockTrimmingAnalyzer.cs +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Helpers/MockTrimmingAnalyzer.cs @@ -6,7 +6,7 @@ namespace NServiceBus.Core.Analyzer.Tests.Helpers; using Microsoft.CodeAnalysis.Diagnostics; // Currently, this mock analyzer does not support all trimming-related warnings. -// It only supports IL2026 for method invocations for now. +// It only supports IL2026 and IL3050 for method invocations for now. #pragma warning disable RS1001 // Yes we don't want it to be found class MockTrimmingAnalyzer : DiagnosticAnalyzer #pragma warning restore RS1001 @@ -21,7 +21,17 @@ class MockTrimmingAnalyzer : DiagnosticAnalyzer defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public override ImmutableArray SupportedDiagnostics => [IL2026Descriptor]; + static readonly DiagnosticDescriptor IL3050Descriptor = new( +#pragma warning disable RS2008 + id: "IL3050", +#pragma warning restore RS2008 + title: "Using member with RequiresDynamicCodeAttribute", + messageFormat: "Using member '{0}' which has 'RequiresDynamicCodeAttribute'", + category: "AOT", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public override ImmutableArray SupportedDiagnostics => [IL2026Descriptor, IL3050Descriptor]; public override void Initialize(AnalysisContext context) { @@ -36,13 +46,16 @@ public override void Initialize(AnalysisContext context) } var method = invocation.TargetMethod; - if (!method.GetAttributes().Any(attr => attr.AttributeClass?.Name == "RequiresUnreferencedCodeAttribute")) + var attributes = method.GetAttributes(); + if (attributes.Any(attr => attr.AttributeClass?.Name == "RequiresUnreferencedCodeAttribute")) { - return; + operationContext.ReportDiagnostic(Diagnostic.Create(IL2026Descriptor, invocation.Syntax.GetLocation(), method.Name)); } - var diagnostic = Diagnostic.Create(IL2026Descriptor, invocation.Syntax.GetLocation(), method.Name); - operationContext.ReportDiagnostic(diagnostic); + if (attributes.Any(attr => attr.AttributeClass?.Name == "RequiresDynamicCodeAttribute")) + { + operationContext.ReportDiagnostic(Diagnostic.Create(IL3050Descriptor, invocation.Syntax.GetLocation(), method.Name)); + } }, OperationKind.Invocation); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/AddSagaInterceptorSuppressorTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/AddSagaInterceptorSuppressorTests.cs index cbcc9fdfaa..abd3f764e3 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/AddSagaInterceptorSuppressorTests.cs +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/AddSagaInterceptorSuppressorTests.cs @@ -57,6 +57,55 @@ public class SampleCommand : ICommand Assert.That(diagnostics, Does.Not.Contain("IL2026")); } + [Test] + public void SuppressesIL3050ForAddSaga() + { + var source = """ + using System.Threading.Tasks; + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.AddSaga(); + } + } + + public class SampleSaga : Saga, + IAmStartedByMessages + { + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) + { + mapper.MapSaga(saga => saga.CorrelationId) + .ToMessage(msg => msg.CorrelationId); + } + + public Task Handle(SampleCommand cmd, IMessageHandlerContext context) => Task.CompletedTask; + } + + public class SampleSagaData : ContainSagaData + { + public string CorrelationId { get; set; } + } + + public class SampleCommand : ICommand + { + public string CorrelationId { get; set; } + } + """; + + var result = SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .WithAnalyzer() + .WithSuppressor() + .Run(); + + var diagnostics = result.GetCompilationOutput(); + + Assert.That(diagnostics, Does.Not.Contain("IL3050")); + } + [Test] public void SuppressesIL2026ForFinderOnlySaga() { @@ -111,6 +160,60 @@ public class StartSagaMessage : IMessage; Assert.That(diagnostics, Does.Not.Contain("IL2026")); } + [Test] + public void SuppressesIL3050ForFinderOnlySaga() + { + var source = """ + using System.Threading; + using System.Threading.Tasks; + using NServiceBus; + using NServiceBus.Persistence; + using NServiceBus.Extensibility; + using NServiceBus.Sagas; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.AddSaga(); + } + } + + public class FinderOnlySaga : Saga, + IAmStartedByMessages + { + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) + { + mapper.ConfigureFinderMapping(); + } + + public Task Handle(StartSagaMessage message, IMessageHandlerContext context) => Task.CompletedTask; + } + + public class FinderOnlySagaData : ContainSagaData + { + public string Property { get; set; } + } + + public class FinderOnlyFinder : ISagaFinder + { + public Task FindBy(StartSagaMessage message, ISynchronizedStorageSession storageSession, IReadOnlyContextBag context, CancellationToken cancellationToken = default) => Task.FromResult(default(FinderOnlySagaData)); + } + + public class StartSagaMessage : IMessage; + """; + + var result = SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .WithAnalyzer() + .WithSuppressor() + .Run(); + + var diagnostics = result.GetCompilationOutput(); + + Assert.That(diagnostics, Does.Not.Contain("IL3050")); + } + [Test] public void DoesNotSuppressIL2026ForNonAddSagaCalls() { @@ -143,4 +246,37 @@ public void SomeOtherMethod() { } Assert.That(diagnostics, Does.Contain("IL2026")); } + + [Test] + public void DoesNotSuppressIL3050ForNonAddSagaCalls() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + // This call should still produce IL3050 since it's not intercepted + SomeOtherMethod(); + } + + [RequiresDynamicCode("Test method")] + public void SomeOtherMethod() { } + } + """; + + var result = SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .WithAnalyzer() + .WithSuppressor() + .SuppressDiagnosticErrors() + .SuppressCompilationErrors() + .Run(); + + var diagnostics = result.GetCompilationOutput(); + + Assert.That(diagnostics, Does.Contain("IL3050")); + } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer/Handlers/AddHandlerInterceptor.Suppressor.cs b/src/NServiceBus.Core.Analyzer/Handlers/AddHandlerInterceptor.Suppressor.cs index e8cef4cccf..645b4186b0 100644 --- a/src/NServiceBus.Core.Analyzer/Handlers/AddHandlerInterceptor.Suppressor.cs +++ b/src/NServiceBus.Core.Analyzer/Handlers/AddHandlerInterceptor.Suppressor.cs @@ -9,16 +9,23 @@ [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class AddHandlerInterceptorSuppressor : DiagnosticSuppressor { + const string Justification = "The AddHandler method has been intercepted by a statically generated variant."; + static readonly SuppressionDescriptor SuppressRUCDiagnostic = new( SupressionIds.AddHandlerInterceptorSuppression, suppressedDiagnosticId: "IL2026", - justification: "The AddHandler method has been intercepted by a statically generated variant."); + justification: Justification); + + static readonly SuppressionDescriptor SuppressRDCDiagnostic = new( + SupressionIds.AddHandlerInterceptorAotSuppression, + suppressedDiagnosticId: "IL3050", + justification: Justification); public override void ReportSuppressions(SuppressionAnalysisContext context) { foreach (var diagnostic in context.ReportedDiagnostics) { - if (diagnostic.Id != SuppressRUCDiagnostic.SuppressedDiagnosticId) + if (diagnostic.Id != SuppressRUCDiagnostic.SuppressedDiagnosticId && diagnostic.Id != SuppressRDCDiagnostic.SuppressedDiagnosticId) { continue; } @@ -47,10 +54,11 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) var operation = semanticModel.GetOperation(node, context.CancellationToken); if (operation is IInvocationOperation { TargetMethod: { } methodSymbol } && AddHandlerInterceptor.Parser.IsAddHandlerMethod(methodSymbol)) { - context.ReportSuppression(Suppression.Create(SuppressRUCDiagnostic, diagnostic)); + var targetSuppression = diagnostic.Id == SuppressRUCDiagnostic.SuppressedDiagnosticId ? SuppressRUCDiagnostic : SuppressRDCDiagnostic; + context.ReportSuppression(Suppression.Create(targetSuppression, diagnostic)); } } } - public override ImmutableArray SupportedSuppressions => [SuppressRUCDiagnostic]; + public override ImmutableArray SupportedSuppressions => [SuppressRUCDiagnostic, SuppressRDCDiagnostic]; } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer/Sagas/AddSagaInterceptor.Suppressor.cs b/src/NServiceBus.Core.Analyzer/Sagas/AddSagaInterceptor.Suppressor.cs index 76be3d5a09..7b809300b7 100644 --- a/src/NServiceBus.Core.Analyzer/Sagas/AddSagaInterceptor.Suppressor.cs +++ b/src/NServiceBus.Core.Analyzer/Sagas/AddSagaInterceptor.Suppressor.cs @@ -10,16 +10,23 @@ [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class AddSagaInterceptorSuppressor : DiagnosticSuppressor { + const string Justification = "The AddSaga method has been intercepted by a statically generated variant."; + static readonly SuppressionDescriptor SuppressRUCDiagnostic = new( SupressionIds.AddSagaInterceptorSuppression, suppressedDiagnosticId: "IL2026", - justification: "The AddSaga method has been intercepted by a statically generated variant."); + justification: Justification); + + static readonly SuppressionDescriptor SuppressRDCDiagnostic = new( + SupressionIds.AddSagaInterceptorAotSuppression, + suppressedDiagnosticId: "IL3050", + justification: Justification); public override void ReportSuppressions(SuppressionAnalysisContext context) { foreach (var diagnostic in context.ReportedDiagnostics) { - if (diagnostic.Id != SuppressRUCDiagnostic.SuppressedDiagnosticId) + if (diagnostic.Id != SuppressRUCDiagnostic.SuppressedDiagnosticId && diagnostic.Id != SuppressRDCDiagnostic.SuppressedDiagnosticId) { continue; } @@ -53,7 +60,7 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) // Only suppress when an interceptor can actually be emitted for this call site. A saga that cannot be // parsed (no Saga base, abstract, or otherwise unsupported) keeps the RequiresUnreferencedCode - // fallback warning. + // and RequiresDynamicCode fallback warnings. if (methodSymbol.TypeArguments[0] is not INamedTypeSymbol sagaType || !HandlerKnownTypes.TryGet(context.Compilation, out var knownTypes) || Sagas.Parser.Parse(semanticModel, sagaType, knownTypes, context.CancellationToken) is null) @@ -61,9 +68,10 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) continue; } - context.ReportSuppression(Suppression.Create(SuppressRUCDiagnostic, diagnostic)); + var targetSuppression = diagnostic.Id == SuppressRUCDiagnostic.SuppressedDiagnosticId ? SuppressRUCDiagnostic : SuppressRDCDiagnostic; + context.ReportSuppression(Suppression.Create(targetSuppression, diagnostic)); } } - public override ImmutableArray SupportedSuppressions => [SuppressRUCDiagnostic]; + public override ImmutableArray SupportedSuppressions => [SuppressRUCDiagnostic, SuppressRDCDiagnostic]; } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer/SupressionIds.cs b/src/NServiceBus.Core.Analyzer/SupressionIds.cs index eb9fe17499..e0667c0957 100644 --- a/src/NServiceBus.Core.Analyzer/SupressionIds.cs +++ b/src/NServiceBus.Core.Analyzer/SupressionIds.cs @@ -5,4 +5,6 @@ public static class SupressionIds public const string AddHandlerInterceptorSuppression = "NSBS0001"; public const string AddSagaInterceptorSuppression = "NSBS0002"; public const string AddMessageTypeInterceptorSuppression = "NSBS0003"; + public const string AddHandlerInterceptorAotSuppression = "NSBS0004"; + public const string AddSagaInterceptorAotSuppression = "NSBS0005"; } \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index 1119e36be4..b611b6f982 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -751,6 +751,8 @@ namespace NServiceBus } public static class MessageHandlerRegistrationExtensions { + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Registering handlers using assembly scanning relies on dynamic code generation wh" + + "ich is not available with Ahead of Time compilation.")] [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Registering handlers using assembly scanning is not supported in trimming scenari" + "os.")] public static void AddHandler<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] THandler>(this NServiceBus.EndpointConfiguration config) { } @@ -1131,7 +1133,9 @@ namespace NServiceBus } public static class SagaRegistrationExtensions { - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Saga discovery using assembly scanning might require access to unreferenced code")] + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Saga discovery using assembly scanning might relies on dynamic code generation wh" + + "ich is not available with Ahead of Time compilation.")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Saga discovery using assembly scanning might require access to unreferenced code.")] public static void AddSaga<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TSaga>(this NServiceBus.EndpointConfiguration config) where TSaga : NServiceBus.Saga, NServiceBus.IHandleMessages { } } @@ -2513,13 +2517,17 @@ namespace NServiceBus.Sagas [System.Obsolete("Use the overload without available types and conventions. Will be removed in vers" + "ion 11.0.0.", true)] public static NServiceBus.Sagas.SagaMetadata Create(System.Type sagaType, System.Collections.Generic.IEnumerable availableTypes, NServiceBus.Conventions conventions) { } - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Saga discovery using assembly scanning might require access to unreferenced code")] + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Saga discovery using assembly scanning might relies on dynamic code generation wh" + + "ich is not available with Ahead of Time compilation.")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Saga discovery using assembly scanning might require access to unreferenced code.")] public static NServiceBus.Sagas.SagaMetadata Create<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TSaga>() where TSaga : NServiceBus.Saga { } public static NServiceBus.Sagas.SagaMetadata Create<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TSaga, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] TSagaData>(System.Collections.Generic.IReadOnlyCollection associatedMessages, NServiceBus.Sagas.CorrelationPropertyAccessor? correlationPropertyAccessor = null, System.Collections.Generic.IReadOnlyCollection? propertyAccessors = null) where TSaga : NServiceBus.Saga where TSagaData : class, NServiceBus.IContainSagaData, new () { } - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Saga discovery using assembly scanning might require access to unreferenced code")] + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Saga discovery using assembly scanning might relies on dynamic code generation wh" + + "ich is not available with Ahead of Time compilation.")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Saga discovery using assembly scanning might require access to unreferenced code.")] public static System.Collections.Generic.IEnumerable CreateMany(System.Collections.Generic.IEnumerable sagaTypes) { } public class CorrelationPropertyMetadata { @@ -2863,6 +2871,8 @@ namespace NServiceBus.Unicast public class MessageHandlerRegistry { public MessageHandlerRegistry() { } + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Registering handlers using assembly scanning relies on dynamic code generation wh" + + "ich is not available with Ahead of Time compilation.")] [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Registering handlers using assembly scanning is not supported in trimming scenari" + "os.")] public void AddHandler() { } @@ -2870,6 +2880,8 @@ namespace NServiceBus.Unicast where THandler : class, NServiceBus.IHandleMessages { } public void AddMessageHandlerForMessage<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] THandlerAdapter, TMessage, THandler>() where THandlerAdapter : class, NServiceBus.IHandleMessages { } + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Registering handlers using assembly scanning relies on dynamic code generation wh" + + "ich is not available with Ahead of Time compilation.")] [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Registering handlers using assembly scanning is not supported in trimming scenari" + "os.")] public void AddScannedHandlers(System.Collections.Generic.IEnumerable orderedTypes) { } @@ -2878,6 +2890,8 @@ namespace NServiceBus.Unicast public void Clear() { } public System.Collections.Generic.List GetHandlersFor(System.Type messageType) { } public System.Collections.Generic.IEnumerable GetMessageTypes() { } + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Registering handlers using assembly scanning relies on dynamic code generation wh" + + "ich is not available with Ahead of Time compilation.")] [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Registering handlers using assembly scanning is not supported in trimming scenari" + "os.")] [System.Obsolete("Use \'AddHandler()\' instead. Will be treated as an error from version 11" + diff --git a/src/NServiceBus.Core/EndpointConfiguration.cs b/src/NServiceBus.Core/EndpointConfiguration.cs index fa1e0df901..c6de8f9959 100644 --- a/src/NServiceBus.Core/EndpointConfiguration.cs +++ b/src/NServiceBus.Core/EndpointConfiguration.cs @@ -128,7 +128,7 @@ internal void FinalizeConfiguration(IList availableTypes) InvokeDiscoveredInitializers(availableTypes); } - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = EndpointCreator.TrimmingSuppressJustification)] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = EndpointCreator.SuppressJustification)] void InvokeDiscoveredInitializers(IList availableTypes) { ActivateAndInvoke(availableTypes, t => t.Customize(this)); diff --git a/src/NServiceBus.Core/EndpointCreator.cs b/src/NServiceBus.Core/EndpointCreator.cs index 604f9b1d82..8ba13b5234 100644 --- a/src/NServiceBus.Core/EndpointCreator.cs +++ b/src/NServiceBus.Core/EndpointCreator.cs @@ -61,7 +61,7 @@ public static EndpointCreator Create(EndpointConfiguration endpointConfiguration return endpointCreator; - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingSuppressJustification)] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = SuppressJustification)] static void DiscoverInstallers(InstallerComponent.Settings installerSettings, List availableTypes) => installerSettings.AddScannedInstallers(availableTypes); } @@ -181,13 +181,15 @@ void Configure() hostingComponent = HostingComponent.Initialize(hostingConfiguration); MessageSession = new MessageSession(hostingConfiguration.EndpointLogSlot); - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingSuppressJustification)] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = SuppressJustification)] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = SuppressJustification)] static void DiscoverHandlers(ReceiveComponent.Settings receiveSettings, ICollection availableTypes) => receiveSettings.MessageHandlerRegistry.AddScannedHandlers(availableTypes); - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingSuppressJustification)] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = SuppressJustification)] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = SuppressJustification)] static void DiscoverSagas(SagaComponent.Settings sagaSettings, ICollection availableTypes) => sagaSettings.AddDiscoveredSagas(availableTypes); - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingSuppressJustification)] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = SuppressJustification)] static void DiscoverFeatures(ICollection availableTypes, FeatureComponent.Settings featureSettings) => featureSettings.AddScannedTypes(availableTypes); } @@ -255,5 +257,5 @@ internal StartableEndpoint CreateStartableEndpoint(IServiceProvider serviceProvi readonly HostingComponent.Configuration hostingConfiguration; readonly Conventions conventions; - internal const string TrimmingSuppressJustification = "The assembly scanning component has a guard that prevents it from being used when dynamic code is not available so we can safely call this."; + internal const string SuppressJustification = "The assembly scanning component has a guard that prevents it from being used when dynamic code is not available so we can safely call this."; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Sagas/SagaComponent.cs b/src/NServiceBus.Core/Sagas/SagaComponent.cs index 48655bfe30..b66a10a186 100644 --- a/src/NServiceBus.Core/Sagas/SagaComponent.cs +++ b/src/NServiceBus.Core/Sagas/SagaComponent.cs @@ -54,6 +54,7 @@ public Settings(SettingsHolder settings) settings.SetDefault(new SagaMetadataCollection()); } + [RequiresDynamicCode("Saga discovery using assembly scanning might rely on dynamic code generation which is not available with Ahead of Time compilation.")] [RequiresUnreferencedCode("Saga discovery using assembly scanning might require access to unreferenced code")] public void AddDiscoveredSagas(IEnumerable availableTypes) { diff --git a/src/NServiceBus.Core/Sagas/SagaMetadata.cs b/src/NServiceBus.Core/Sagas/SagaMetadata.cs index 35f32a1bc6..caa5c64762 100644 --- a/src/NServiceBus.Core/Sagas/SagaMetadata.cs +++ b/src/NServiceBus.Core/Sagas/SagaMetadata.cs @@ -86,6 +86,7 @@ public bool TryGetFinder(string messageType, [NotNullWhen(true)] out SagaFinderD /// /// Potential saga types. /// Saga metadata for all the found saga types. + [RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] public static IEnumerable CreateMany(IEnumerable sagaTypes) { @@ -108,6 +109,7 @@ public static IEnumerable CreateMany(IEnumerable sagaTypes) /// /// A type representing a Saga. Must be a non-generic type inheriting from . /// An instance of describing the Saga. + [RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] public static SagaMetadata Create<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Saga)] TSaga>() where TSaga : Saga { @@ -256,7 +258,8 @@ static Type GetBaseSagaType(Type t) static readonly MethodInfo CreateSagaOfTSagaTEntityMethod = typeof(SagaMetadata) .GetMethod(nameof(Create), 2, BindingFlags.Public | BindingFlags.Static, [typeof(IReadOnlyCollection), typeof(CorrelationPropertyAccessor), typeof(IReadOnlyCollection)]) ?? throw new MissingMethodException(nameof(Create)); - internal const string TrimmingMessage = "Saga discovery using assembly scanning might require access to unreferenced code"; + internal const string TrimmingMessage = "Saga discovery using assembly scanning might require access to unreferenced code."; + internal const string DynamicCodeMessage = "Saga discovery using assembly scanning might relies on dynamic code generation which is not available with Ahead of Time compilation."; /// /// Details about a saga data property used to correlate messages hitting the saga. diff --git a/src/NServiceBus.Core/Unicast/Config/MessageHandlerRegistrationExtensions.cs b/src/NServiceBus.Core/Unicast/Config/MessageHandlerRegistrationExtensions.cs index 714222917f..3f6d573c54 100644 --- a/src/NServiceBus.Core/Unicast/Config/MessageHandlerRegistrationExtensions.cs +++ b/src/NServiceBus.Core/Unicast/Config/MessageHandlerRegistrationExtensions.cs @@ -14,6 +14,7 @@ public static class MessageHandlerRegistrationExtensions /// /// Registers a message handler. /// + [RequiresDynamicCode(MessageHandlerRegistry.DynamicCodeMessage)] [RequiresUnreferencedCode(MessageHandlerRegistry.TrimmingMessage)] public static void AddHandler<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Handler)] THandler>(this EndpointConfiguration config) { diff --git a/src/NServiceBus.Core/Unicast/Config/SagaRegistrationExtensions.cs b/src/NServiceBus.Core/Unicast/Config/SagaRegistrationExtensions.cs index ece4f59a0c..de3374ca48 100644 --- a/src/NServiceBus.Core/Unicast/Config/SagaRegistrationExtensions.cs +++ b/src/NServiceBus.Core/Unicast/Config/SagaRegistrationExtensions.cs @@ -14,6 +14,7 @@ public static class SagaRegistrationExtensions /// /// Registers a saga. /// + [RequiresDynamicCode(SagaMetadata.DynamicCodeMessage)] [RequiresUnreferencedCode(SagaMetadata.TrimmingMessage)] public static void AddSaga<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Saga)] TSaga>(this EndpointConfiguration config) where TSaga : Saga, IHandleMessages { diff --git a/src/NServiceBus.Core/Unicast/MessageHandlerRegistry.cs b/src/NServiceBus.Core/Unicast/MessageHandlerRegistry.cs index ab7b2d0bba..3e1d3200a0 100644 --- a/src/NServiceBus.Core/Unicast/MessageHandlerRegistry.cs +++ b/src/NServiceBus.Core/Unicast/MessageHandlerRegistry.cs @@ -44,13 +44,11 @@ public List GetHandlersFor(Type messageType) /// Lists all message type for which we have handlers. /// /// This method should not be called on a hot path. - public IEnumerable GetMessageTypes() - { - return (from messagesBeingHandled in messageHandlerFactories.Values - from typeHandled in messagesBeingHandled - let messageType = typeHandled.MessageType - select messageType).Distinct(); - } + public IEnumerable GetMessageTypes() => + (from messagesBeingHandled in messageHandlerFactories.Values + from typeHandled in messagesBeingHandled + let messageType = typeHandled.MessageType + select messageType).Distinct(); /// /// Registers the given potential handler type. @@ -59,12 +57,14 @@ from typeHandled in messagesBeingHandled TreatAsErrorFromVersion = "11", RemoveInVersion = "12")] [Obsolete("Use 'AddHandler()' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] + [RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] public void RegisterHandler(Type handlerType) => AddHandlerWithReflection(handlerType); /// /// Registers the handler type. /// + [RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] public void AddHandler() { @@ -159,6 +159,7 @@ List GetOrCreate() /// Add handlers from types scanned at runtime. /// /// Scanned types, with "load handlers first" types ordered first. + [RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] public void AddScannedHandlers(IEnumerable orderedTypes) { @@ -190,6 +191,7 @@ public void Clear() deduplicationSet.Clear(); } + [RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] void AddHandlerWithReflection(Type handlerType) => AddHandlerWithReflectionMethod.InvokeGeneric(this, [handlerType]); @@ -212,6 +214,7 @@ void AddHandlerWithReflection(Type handlerType) => static readonly Type IHandleMessagesType = typeof(IHandleMessages<>); internal const string TrimmingMessage = "Registering handlers using assembly scanning is not supported in trimming scenarios."; + internal const string DynamicCodeMessage = "Registering handlers using assembly scanning relies on dynamic code generation which is not available with Ahead of Time compilation."; readonly record struct HandlerAndMessage(Type HandlerType, Type MessageType, bool IsTimeoutHandler) { diff --git a/src/NServiceBus.Core/Utils/Reflection/MethodInfoExtensions.cs b/src/NServiceBus.Core/Utils/Reflection/MethodInfoExtensions.cs index 7c7b5ef6c4..94d87c4905 100644 --- a/src/NServiceBus.Core/Utils/Reflection/MethodInfoExtensions.cs +++ b/src/NServiceBus.Core/Utils/Reflection/MethodInfoExtensions.cs @@ -12,18 +12,23 @@ static class MethodInfoExtensions { extension(MethodInfo method) { + [RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] public T? InvokeGeneric(object? target, object?[]? args, Type[] genericTypes) => (T?)method.InvokeGeneric(target, args, genericTypes); + [RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] public T? InvokeGeneric(object?[]? args, Type[] genericTypes) => (T?)method.InvokeGeneric(null, args, genericTypes); + [RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] public T? InvokeGeneric(Type genericType) => (T?)method.InvokeGeneric(null, null, [genericType]); + [RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] public object? InvokeGeneric(object? target, Type[] genericTypes) => method.InvokeGeneric(target, null, genericTypes); + [RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] public object? InvokeGeneric(object? target, object?[]? args, Type[] genericTypes) { @@ -45,4 +50,5 @@ static class MethodInfoExtensions } const string TrimmingMessage = "Generic invocations might require access to unreferenced code"; + const string DynamicCodeMessage = "Generic invocation relies on dynamic code generation which is not available with Ahead of Time compilation"; } \ No newline at end of file From 7da3796f537ff32f0d9ea85f8a85f57cc42bffae Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Sun, 6 Sep 2026 18:20:24 +0200 Subject: [PATCH 13/18] KeyedServiceProviderAdapter suppressions --- .../KeyedServices/KeyedServiceProviderAdapter.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceProviderAdapter.cs b/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceProviderAdapter.cs index 1176f6e78f..9e3df65643 100644 --- a/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceProviderAdapter.cs +++ b/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceProviderAdapter.cs @@ -6,6 +6,7 @@ namespace NServiceBus; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -46,6 +47,7 @@ public bool IsKeyedService(Type serviceType, object? serviceKey) return ContainsLocalEndpointService(serviceType, computedKey) || ContainsRootKeyedService(serviceType, GetBaseKeyOrServiceKey(serviceKey)); } + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = Justification)] public object? GetService(Type serviceType) { ArgumentNullException.ThrowIfNull(serviceType); @@ -80,6 +82,7 @@ public bool IsKeyedService(Type serviceType, object? serviceKey) return ContainsRootEndpointKeyedService(itemType) ? serviceProvider.GetKeyedServices(itemType, serviceKeyedServiceKey.BaseKey) : serviceProvider.GetServices(itemType); } + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = Justification)] public object GetRequiredService(Type serviceType) { ArgumentNullException.ThrowIfNull(serviceType); @@ -114,6 +117,7 @@ public object GetRequiredService(Type serviceType) return ContainsRootEndpointKeyedService(itemType) ? serviceProvider.GetKeyedServices(itemType, serviceKeyedServiceKey.BaseKey) : serviceProvider.GetServices(itemType); } + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = Justification)] public object? GetKeyedService(Type serviceType, object? serviceKey) { ArgumentNullException.ThrowIfNull(serviceType); @@ -148,6 +152,7 @@ public object GetRequiredService(Type serviceType) return GetAllServices(serviceProvider, itemType); } + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = Justification)] public object GetRequiredKeyedService(Type serviceType, object? serviceKey) { ArgumentNullException.ThrowIfNull(serviceType); @@ -281,6 +286,11 @@ KeyedServiceKey GetOrCreateComputedKey(object? serviceKey) return new KeyedServiceKey(serviceKeyedServiceKey, serviceKey); } + // Annotating isn't possible: RequiresDynamicCode on members implementing the unannotated IServiceProvider + // interfaces is an IL3051 mismatch, so the accepted risk is documented here instead. + const string Justification = "Resolving IEnumerable registrations by runtime Type requires the Type-based GetServices/GetKeyedServices overloads which require dynamic code. Mirrors the framework's own IServiceProvider behavior and cannot be made AOT-safe while supporting Type-based resolution."; + + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = Justification)] static object GetAllServices(IServiceProvider serviceProvider, Type itemType) { Type genericEnumerable = typeof(List<>).MakeGenericType(itemType); From 1fed009138572ea57a829ec2e00f30ca1ae94ad7 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 10 Sep 2026 17:30:23 +0200 Subject: [PATCH 14/18] Grammar fix --- src/NServiceBus.Core/Sagas/SagaMetadata.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NServiceBus.Core/Sagas/SagaMetadata.cs b/src/NServiceBus.Core/Sagas/SagaMetadata.cs index caa5c64762..7b348cc523 100644 --- a/src/NServiceBus.Core/Sagas/SagaMetadata.cs +++ b/src/NServiceBus.Core/Sagas/SagaMetadata.cs @@ -259,7 +259,7 @@ static Type GetBaseSagaType(Type t) .GetMethod(nameof(Create), 2, BindingFlags.Public | BindingFlags.Static, [typeof(IReadOnlyCollection), typeof(CorrelationPropertyAccessor), typeof(IReadOnlyCollection)]) ?? throw new MissingMethodException(nameof(Create)); internal const string TrimmingMessage = "Saga discovery using assembly scanning might require access to unreferenced code."; - internal const string DynamicCodeMessage = "Saga discovery using assembly scanning might relies on dynamic code generation which is not available with Ahead of Time compilation."; + internal const string DynamicCodeMessage = "Saga discovery using assembly scanning might rely on dynamic code generation which is not available with Ahead of Time compilation."; /// /// Details about a saga data property used to correlate messages hitting the saga. From 81021a3ea4320c404323193f8de3deae7c92eefa Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 10 Sep 2026 17:31:21 +0200 Subject: [PATCH 15/18] Better explanation (hopefully) --- src/NServiceBus.Core/Routing/AssemblyRouteSource.cs | 2 +- .../MessageDrivenSubscriptions/AssemblyPublisherSource.cs | 2 +- .../MessageDrivenSubscriptions/NamespacePublisherSource.cs | 2 +- src/NServiceBus.Core/Routing/NamespaceRouteSource.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/NServiceBus.Core/Routing/AssemblyRouteSource.cs b/src/NServiceBus.Core/Routing/AssemblyRouteSource.cs index 36ad78ee2c..04c347fbe5 100644 --- a/src/NServiceBus.Core/Routing/AssemblyRouteSource.cs +++ b/src/NServiceBus.Core/Routing/AssemblyRouteSource.cs @@ -19,7 +19,7 @@ public AssemblyRouteSource(Assembly messageAssembly, UnicastRoute route) this.route = route; } - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Scanning the configured assembly is intentional; this source can only be constructed through APIs annotated with RequiresUnreferencedCode.")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Scanning the configured assembly is intentional. Construction is gated by the constructor's RequiresUnreferencedCode annotation, and the scanning members cannot be annotated because they implement an unannotated interface (IL2046).")] static Type[] ScanAssemblyTypes(Assembly assembly) => assembly.GetTypes(); public IEnumerable GenerateRoutes(Conventions conventions) diff --git a/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/AssemblyPublisherSource.cs b/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/AssemblyPublisherSource.cs index 5c5b8b7d01..0756bc9ad8 100644 --- a/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/AssemblyPublisherSource.cs +++ b/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/AssemblyPublisherSource.cs @@ -19,7 +19,7 @@ public AssemblyPublisherSource(Assembly messageAssembly, PublisherAddress addres this.address = address; } - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Scanning the configured assembly is intentional; this source can only be constructed through APIs annotated with RequiresUnreferencedCode.")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Scanning the configured assembly is intentional. Construction is gated by the constructor's RequiresUnreferencedCode annotation, and the scanning members cannot be annotated because they implement an unannotated interface (IL2046).")] static Type[] ScanAssemblyTypes(Assembly assembly) => assembly.GetTypes(); public IEnumerable GenerateWithBestPracticeEnforcement(Conventions conventions) diff --git a/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/NamespacePublisherSource.cs b/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/NamespacePublisherSource.cs index 2b165532a1..cf97082558 100644 --- a/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/NamespacePublisherSource.cs +++ b/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/NamespacePublisherSource.cs @@ -21,7 +21,7 @@ public NamespacePublisherSource(Assembly messageAssembly, string messageNamespac this.messageNamespace = messageNamespace; } - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Scanning the configured assembly is intentional; this source can only be constructed through APIs annotated with RequiresUnreferencedCode.")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Scanning the configured assembly is intentional. Construction is gated by the constructor's RequiresUnreferencedCode annotation, and the scanning members cannot be annotated because they implement an unannotated interface (IL2046).")] static Type[] ScanAssemblyTypes(Assembly assembly) => assembly.GetTypes(); public IEnumerable GenerateWithBestPracticeEnforcement(Conventions conventions) diff --git a/src/NServiceBus.Core/Routing/NamespaceRouteSource.cs b/src/NServiceBus.Core/Routing/NamespaceRouteSource.cs index ba4329002e..aae83c6f2c 100644 --- a/src/NServiceBus.Core/Routing/NamespaceRouteSource.cs +++ b/src/NServiceBus.Core/Routing/NamespaceRouteSource.cs @@ -21,7 +21,7 @@ public NamespaceRouteSource(Assembly messageAssembly, string messageNamespace, U this.messageNamespace = messageNamespace; } - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Scanning the configured assembly is intentional; this source can only be constructed through APIs annotated with RequiresUnreferencedCode.")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Scanning the configured assembly is intentional. Construction is gated by the constructor's RequiresUnreferencedCode annotation, and the scanning members cannot be annotated because they implement an unannotated interface (IL2046).")] static Type[] ScanAssemblyTypes(Assembly assembly) => assembly.GetTypes(); public IEnumerable GenerateRoutes(Conventions conventions) From 90a9a05f9b047e9c4fdf04288290b7b44e5b8da7 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 10 Sep 2026 17:31:57 +0200 Subject: [PATCH 16/18] Reword comment on provider adapter --- .../Hosting/KeyedServices/KeyedServiceProviderAdapter.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceProviderAdapter.cs b/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceProviderAdapter.cs index 9e3df65643..1502a927e6 100644 --- a/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceProviderAdapter.cs +++ b/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceProviderAdapter.cs @@ -286,9 +286,9 @@ KeyedServiceKey GetOrCreateComputedKey(object? serviceKey) return new KeyedServiceKey(serviceKeyedServiceKey, serviceKey); } - // Annotating isn't possible: RequiresDynamicCode on members implementing the unannotated IServiceProvider - // interfaces is an IL3051 mismatch, so the accepted risk is documented here instead. - const string Justification = "Resolving IEnumerable registrations by runtime Type requires the Type-based GetServices/GetKeyedServices overloads which require dynamic code. Mirrors the framework's own IServiceProvider behavior and cannot be made AOT-safe while supporting Type-based resolution."; + // The Microsoft.Extensions.DependencyInjection interfaces implemented here are unannotated, + // so adding RequiresDynamicCode to these members would be an IL3051 mismatch. + const string Justification = "Resolving IEnumerable registrations by runtime Type requires the Type-based GetServices/GetKeyedServices overloads which require dynamic code. Mirrors the framework's own unannotated IServiceProvider behavior and cannot be made AOT-safe while supporting Type-based resolution."; [UnconditionalSuppressMessage("AOT", "IL3050", Justification = Justification)] static object GetAllServices(IServiceProvider serviceProvider, Type itemType) From 373bd5d552541280fc16b4591312772945bf208f Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 10 Sep 2026 17:32:44 +0200 Subject: [PATCH 17/18] IsAotCompatible --- src/NServiceBus.Core/NServiceBus.Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NServiceBus.Core/NServiceBus.Core.csproj b/src/NServiceBus.Core/NServiceBus.Core.csproj index 8afcc88554..254b1a9128 100644 --- a/src/NServiceBus.Core/NServiceBus.Core.csproj +++ b/src/NServiceBus.Core/NServiceBus.Core.csproj @@ -6,7 +6,7 @@ true ..\NServiceBus.snk true - true + true From ecd8833b4d404f971ee0422d839613de7c5d67fa Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 10 Sep 2026 17:41:56 +0200 Subject: [PATCH 18/18] Fix approvals --- .../APIApprovals.ApproveNServiceBus.approved.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index b611b6f982..64e921462e 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -1133,8 +1133,8 @@ namespace NServiceBus } public static class SagaRegistrationExtensions { - [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Saga discovery using assembly scanning might relies on dynamic code generation wh" + - "ich is not available with Ahead of Time compilation.")] + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Saga discovery using assembly scanning might rely on dynamic code generation whic" + + "h is not available with Ahead of Time compilation.")] [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Saga discovery using assembly scanning might require access to unreferenced code.")] public static void AddSaga<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TSaga>(this NServiceBus.EndpointConfiguration config) where TSaga : NServiceBus.Saga, NServiceBus.IHandleMessages { } @@ -2517,16 +2517,16 @@ namespace NServiceBus.Sagas [System.Obsolete("Use the overload without available types and conventions. Will be removed in vers" + "ion 11.0.0.", true)] public static NServiceBus.Sagas.SagaMetadata Create(System.Type sagaType, System.Collections.Generic.IEnumerable availableTypes, NServiceBus.Conventions conventions) { } - [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Saga discovery using assembly scanning might relies on dynamic code generation wh" + - "ich is not available with Ahead of Time compilation.")] + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Saga discovery using assembly scanning might rely on dynamic code generation whic" + + "h is not available with Ahead of Time compilation.")] [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Saga discovery using assembly scanning might require access to unreferenced code.")] public static NServiceBus.Sagas.SagaMetadata Create<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TSaga>() where TSaga : NServiceBus.Saga { } public static NServiceBus.Sagas.SagaMetadata Create<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TSaga, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] TSagaData>(System.Collections.Generic.IReadOnlyCollection associatedMessages, NServiceBus.Sagas.CorrelationPropertyAccessor? correlationPropertyAccessor = null, System.Collections.Generic.IReadOnlyCollection? propertyAccessors = null) where TSaga : NServiceBus.Saga where TSagaData : class, NServiceBus.IContainSagaData, new () { } - [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Saga discovery using assembly scanning might relies on dynamic code generation wh" + - "ich is not available with Ahead of Time compilation.")] + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Saga discovery using assembly scanning might rely on dynamic code generation whic" + + "h is not available with Ahead of Time compilation.")] [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Saga discovery using assembly scanning might require access to unreferenced code.")] public static System.Collections.Generic.IEnumerable CreateMany(System.Collections.Generic.IEnumerable sagaTypes) { } public class CorrelationPropertyMetadata