diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerInterceptorSuppressorTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerInterceptorSuppressorTests.cs index cfd120cd556..4df73114cc4 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 afbbfa97f13..9899d573541 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 cbcc9fdfaab..abd3f764e32 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 e8cef4cccf6..645b4186b09 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 76be3d5a09e..7b809300b7a 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 eb9fe17499c..e0667c09575 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/API/TrimmabilityWarnings.cs b/src/NServiceBus.Core.Tests/API/TrimmabilityWarnings.cs deleted file mode 100644 index 7fbd402b329..00000000000 --- 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/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index 3b421c88eb9..64e921462ef 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 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 { } } @@ -1356,7 +1360,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() { } @@ -2510,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 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.RequiresUnreferencedCode("Saga discovery using assembly scanning might require access to unreferenced code")] + [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 { @@ -2860,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() { } @@ -2867,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) { } @@ -2875,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.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt deleted file mode 100644 index c835831f11f..00000000000 --- 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/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs index f4ca7e73f90..17a60efea43 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 1161d2f18b1..5466f006912 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.Tests/TrimmedEndpointTests.cs b/src/NServiceBus.Core.Tests/TrimmedEndpointTests.cs index 37f8dcf3566..1f2d7ffd9cd 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/EndpointConfiguration.cs b/src/NServiceBus.Core/EndpointConfiguration.cs index fa1e0df9014..c6de8f9959c 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 bdca7d39f4c..8ba13b52343 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,25 +181,28 @@ 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); } 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); @@ -254,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/Hosting/KeyedServices/KeyedServiceProviderAdapter.cs b/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceProviderAdapter.cs index 1176f6e78f7..1502a927e64 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); } + // 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) { Type genericEnumerable = typeof(List<>).MakeGenericType(itemType); diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriter.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriter.cs index a589c63cad8..c2229fa3b31 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) diff --git a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs index 3a69dfffb95..e3864da6f75 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 1804f38e185..e5b40077c6a 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; } } diff --git a/src/NServiceBus.Core/NServiceBus.Core.csproj b/src/NServiceBus.Core/NServiceBus.Core.csproj index d505a9ea8df..254b1a91285 100644 --- a/src/NServiceBus.Core/NServiceBus.Core.csproj +++ b/src/NServiceBus.Core/NServiceBus.Core.csproj @@ -6,6 +6,7 @@ true ..\NServiceBus.snk true + true diff --git a/src/NServiceBus.Core/Routing/AssemblyRouteSource.cs b/src/NServiceBus.Core/Routing/AssemblyRouteSource.cs index 8bc47a24d67..04c347fbe58 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. 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) { - 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 0226815f131..0756bc9ad87 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. 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) { - 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 29c3e80bbe5..cf970825583 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. 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) { - 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 f77fd23aed0..aae83c6f2c1 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. 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) { - 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(); diff --git a/src/NServiceBus.Core/Sagas/SagaComponent.cs b/src/NServiceBus.Core/Sagas/SagaComponent.cs index 48655bfe305..b66a10a1868 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 35f32a1bc60..7b348cc5234 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 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. 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 7ec89c04603..95c2f206237 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 d903bfb0b13..e44533ef83a 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 479675970ab..f0da9758bf8 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 aaa66d58b1f..82405d1022e 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 ff5d1fd7952..91f914e0b75 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 218b15c5edf..57489a494be 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 d59e913cda5..a652d08b25c 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 { diff --git a/src/NServiceBus.Core/Unicast/Config/MessageHandlerRegistrationExtensions.cs b/src/NServiceBus.Core/Unicast/Config/MessageHandlerRegistrationExtensions.cs index 714222917f6..3f6d573c540 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 ece4f59a0cb..de3374ca489 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 ab7b2d0bba1..3e1d3200a04 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/FileVersionRetriever.cs b/src/NServiceBus.Core/Utils/FileVersionRetriever.cs index cef30c9f37f..cd71d3e8843 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)) diff --git a/src/NServiceBus.Core/Utils/Reflection/MethodInfoExtensions.cs b/src/NServiceBus.Core/Utils/Reflection/MethodInfoExtensions.cs index 7c7b5ef6c40..94d87c4905b 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 diff --git a/src/NServiceBus.Core/Utils/Reflection/TypeExtensionMethods.cs b/src/NServiceBus.Core/Utils/Reflection/TypeExtensionMethods.cs index c4b0a3a26d1..5c26300742c 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; diff --git a/src/TrimmedEndpoint/Program.cs b/src/TrimmedEndpoint/Program.cs index 34f56941799..7c24eb84792 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 @@ -31,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 => @@ -42,10 +44,10 @@ 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(); -configuration.AddMessageType(); configuration.AddSaga(); #endif @@ -133,6 +135,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 +169,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" }); + Replaced = true; + } + return Task.CompletedTask; } }