From 12b61f90d9c459530ea0ce37255f4d3cdcfdcabd Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 4 Jun 2026 21:24:34 +0000 Subject: [PATCH 01/79] Trimming warning approval (#7800) * Trimming warning approval * Minimal * Binglog * Remove verbosity * Simplify * Cancellation * Fix a regex to not be so greedy, so that messages with array types don't get cut off New regex uses [^\]] instead of . for characters inside the path to make sure it doesn't escape the bounds of one [PATH] block, and adds a [/\\] in the middle, so that essentially a path has to have the following between [ and ]: 1. One or more not ] characters 2. At least one path separator char, with both / and \ as options to account for platform 3. One or more not ] characters * Update src/NServiceBus.Core.Tests/API/TrimmabilityWarnings.cs Co-authored-by: David Boike --------- Co-authored-by: Daniel Marbach Co-authored-by: David Boike --- .../API/TrimmabilityWarnings.cs | 142 ++++++++++++++++++ ...s.ApproveTrimmabilityWarnings.approved.txt | 81 ++++++++++ 2 files changed, 223 insertions(+) create mode 100644 src/NServiceBus.Core.Tests/API/TrimmabilityWarnings.cs create 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 new file mode 100644 index 00000000000..cfe2a14b3be --- /dev/null +++ b/src/NServiceBus.Core.Tests/API/TrimmabilityWarnings.cs @@ -0,0 +1,142 @@ +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 binlogPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "out.binlog"); + + try + { + var warnings = await BuildWithTrimmingAnalyzerEnabled(projectPath, binlogPath, cancellationToken); + + Approver.Verify(warnings); + } + finally + { + if (File.Exists(binlogPath)) + { + File.Delete(binlogPath); + } + } + } + + static async Task BuildWithTrimmingAnalyzerEnabled(string projectPath, string binlogPath, 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("-p:EnableTrimAnalyzer=true"); + startInfo.ArgumentList.Add("-p:TreatWarningsAsErrors=false"); + startInfo.ArgumentList.Add("-p:IsPackable=false"); + startInfo.ArgumentList.Add($"-bl:{binlogPath}"); + + 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(); +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt new file mode 100644 index 00000000000..de8fe1c5cca --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt @@ -0,0 +1,81 @@ +The following trimming warnings are present in NServiceBus.Core. +Changes that make this list longer should not be approved. +----- + +src/NServiceBus.Core/Envelopes/EnvelopeComponent.cs + IL2091: 'T' generic argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicConstructors' in 'Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateInstance(IServiceProvider, params Object[])'. The generic parameter 'THandler' of 'NServiceBus.EnvelopeComponent.Settings.AddEnvelopeHandler()' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. + +src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceCollectionAdapter.cs + IL2067: 'instanceType' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicConstructors' in call to 'Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateFactory(Type, Type[])'. The parameter 'type' of method 'lambda expression' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. + +src/NServiceBus.Core/Hosting/StartupDiagnostics/Host.cs + IL2026: Using member 'System.Reflection.Assembly.GetType(String)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Types might be removed by trimming. If the type name is a string literal, consider using Type.GetType instead. + IL2075: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods' in call to 'System.Type.GetMethod(String, BindingFlags)'. The return value of method 'System.Reflection.Assembly.GetType(String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. + IL2075: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperty(String, BindingFlags)'. The return value of method 'System.Reflection.Assembly.GetType(String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. + +src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriter.cs + IL2026: Using member 'System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. + +src/NServiceBus.Core/Hosting/StartupDiagnostics/JsonPrettyPrinter.cs + IL2026: Using member 'System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. + +src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/ConcreteProxyCreator.cs + IL2067: 'interfaceType' argument does not satisfy 'DynamicallyAccessedMemberTypes.All' in call to 'System.Reflection.Emit.TypeBuilder.AddInterfaceImplementation(Type)'. The parameter 'type' of method 'NServiceBus.ConcreteProxyCreator.CreateTypeFrom(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. + IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 'type' of method 'NServiceBus.ConcreteProxyCreator.GetAllProperties(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. + IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperties()'. The parameter 'type' of method 'NServiceBus.ConcreteProxyCreator.GetAllProperties(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. + +src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/MessageMapper.cs + IL2067: 'type' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicConstructors', 'DynamicallyAccessedMemberTypes.NonPublicConstructors' in call to 'System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(Type)'. The parameter 't' of method 'NServiceBus.MessageInterfaces.MessageMapper.Reflection.MessageMapper.CreateInstance(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. + IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 't' of method 'NServiceBus.MessageInterfaces.MessageMapper.Reflection.MessageMapper.InnerInitialize(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. + IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicFields' in call to 'System.Type.GetFields(BindingFlags)'. The parameter 't' of method 'NServiceBus.MessageInterfaces.MessageMapper.Reflection.MessageMapper.InnerInitialize(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. + IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods' in call to 'System.Type.GetMethods()'. The parameter 'interfaceType' of method 'NServiceBus.MessageInterfaces.MessageMapper.Reflection.MessageMapper.GenerateImplementationFor(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. + IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicParameterlessConstructor' in call to 'System.Type.GetConstructor(Type[])'. The parameter 't' of method 'NServiceBus.MessageInterfaces.MessageMapper.Reflection.MessageMapper.InnerInitialize(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. + IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperties()'. The parameter 't' of method 'NServiceBus.MessageInterfaces.MessageMapper.Reflection.MessageMapper.InnerInitialize(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. + IL2072: 'type' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicConstructors', 'DynamicallyAccessedMemberTypes.NonPublicConstructors' in call to 'System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(Type)'. The return value of method 'NServiceBus.MessageInterfaces.MessageMapper.Reflection.MessageMapper.GetMappedTypeFor(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. + IL2075: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicParameterlessConstructor' in call to 'System.Type.GetConstructor(Type[])'. The return value of method 'NServiceBus.ConcreteProxyCreator.CreateTypeFrom(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. + +src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaStorageFile.cs + IL2026: Using member 'System.Text.Json.JsonSerializer.DeserializeAsync(Stream, JsonSerializerOptions, CancellationToken)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. + IL2026: Using member 'System.Text.Json.JsonSerializer.SerializeAsync(Stream, Object, Type, JsonSerializerOptions, CancellationToken)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. + +src/NServiceBus.Core/Pipeline/RegisterStep.cs + IL2072: 'instanceType' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicConstructors' in call to 'Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateInstance(IServiceProvider, Type, params Object[])'. The return value of method 'NServiceBus.Pipeline.RegisterStep.BehaviorType.get' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. + +src/NServiceBus.Core/Pipeline/RegisterStepExtensions.cs + IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 'behaviorType' of method 'NServiceBus.RegisterStepExtensions.GetBehaviorInterface(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. + IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 'behaviorType' of method 'NServiceBus.RegisterStepExtensions.IsBehavior(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. + +src/NServiceBus.Core/Receiving/ReceiveComponent.Configuration.cs + IL2075: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperties()'. The return value of method '.MessageType.get' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. + +src/NServiceBus.Core/Routing/AssemblyRouteSource.cs + IL2026: Using member 'System.Reflection.Assembly.GetTypes()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Types might be removed. + +src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/AssemblyPublisherSource.cs + IL2026: Using member 'System.Reflection.Assembly.GetTypes()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Types might be removed. + +src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/NamespacePublisherSource.cs + IL2026: Using member 'System.Reflection.Assembly.GetTypes()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Types might be removed. + +src/NServiceBus.Core/Routing/NamespaceRouteSource.cs + IL2026: Using member 'System.Reflection.Assembly.GetTypes()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Types might be removed. + +src/NServiceBus.Core/Serializers/SystemJson/JsonMessageSerializer.cs + IL2026: Using member 'System.Text.Json.JsonSerializer.Deserialize(Stream, Type, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. + IL2026: Using member 'System.Text.Json.JsonSerializer.Serialize(Stream, TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. + +src/NServiceBus.Core/Transports/Learning/LearningTransportDispatcher.cs + IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 'messageType' of method 'NServiceBus.LearningTransportDispatcher.GetPotentialEventTypes(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. + +src/NServiceBus.Core/Transports/TransportSeam.cs + IL2026: Using member 'System.Transactions.TransactionManager.ImplicitDistributedTransactions.set' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Distributed transactions support may not be compatible with trimming. If your program creates a distributed transaction via System.Transactions, the correctness of the application cannot be guaranteed after trimming. + +src/NServiceBus.Core/Unicast/MessageHandlerRegistry.cs + IL2026: Using member 'NServiceBus.Unicast.MessageHandlerRegistry.AddHandler()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Registering handlers using assembly scanning is not supported in trimming scenarios. + IL2026: Using member 'NServiceBus.Unicast.MessageHandlerRegistry.AddScannedHandlers(IEnumerable)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Registering handlers using assembly scanning is not supported in trimming scenarios. + IL2026: Using member 'NServiceBus.Unicast.MessageHandlerRegistry.RegisterHandler(Type)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Registering handlers using assembly scanning is not supported in trimming scenarios. + +src/NServiceBus.Core/Unicast/Messages/MessageMetadataRegistry.cs + IL2057: Unrecognized value passed to the parameter 'typeName' of method 'System.Type.GetType(String)'. It's not possible to guarantee the availability of the target type. + IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 'type' of method 'NServiceBus.Unicast.Messages.MessageMetadataRegistry.GetParentTypes(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. + IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 'type' of method 'NServiceBus.Unicast.Messages.MessageMetadataRegistry.PlaceInMessageHierarchy(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. From 4bfea9c16169c928916ac660562cda24131877e5 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Mon, 8 Jun 2026 08:19:25 +0000 Subject: [PATCH 02/79] Remove the container tests since they are no longer shipped and effectively exercise the MS DI seam which doesn't make any sense anymore. (#7801) Co-authored-by: Daniel Marbach --- src/NServiceBus.ContainerTests/.editorconfig | 4 - .../NServiceBus.ContainerTests.csproj | 21 -- .../When_building_components.cs | 196 -------------- .../When_disposing_the_builder.cs | 53 ---- ...When_querying_for_registered_components.cs | 58 ----- .../When_registering_components.cs | 194 -------------- .../When_using_nested_containers.cs | 244 ------------------ ...IApprovals.ApproveNServiceBus.approved.txt | 1 - src/NServiceBus.Core/NServiceBus.Core.csproj | 1 - src/NServiceBus.slnx | 1 - 10 files changed, 773 deletions(-) delete mode 100644 src/NServiceBus.ContainerTests/.editorconfig delete mode 100644 src/NServiceBus.ContainerTests/NServiceBus.ContainerTests.csproj delete mode 100644 src/NServiceBus.ContainerTests/When_building_components.cs delete mode 100644 src/NServiceBus.ContainerTests/When_disposing_the_builder.cs delete mode 100644 src/NServiceBus.ContainerTests/When_querying_for_registered_components.cs delete mode 100644 src/NServiceBus.ContainerTests/When_registering_components.cs delete mode 100644 src/NServiceBus.ContainerTests/When_using_nested_containers.cs diff --git a/src/NServiceBus.ContainerTests/.editorconfig b/src/NServiceBus.ContainerTests/.editorconfig deleted file mode 100644 index 6ba33f0efeb..00000000000 --- a/src/NServiceBus.ContainerTests/.editorconfig +++ /dev/null @@ -1,4 +0,0 @@ -[*.cs] - -# Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken -dotnet_diagnostic.NSB0002.severity = suggestion diff --git a/src/NServiceBus.ContainerTests/NServiceBus.ContainerTests.csproj b/src/NServiceBus.ContainerTests/NServiceBus.ContainerTests.csproj deleted file mode 100644 index 4fcc13665f1..00000000000 --- a/src/NServiceBus.ContainerTests/NServiceBus.ContainerTests.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - net10.0 - true - ..\NServiceBusTests.snk - - - - - - - - - - - - - - - diff --git a/src/NServiceBus.ContainerTests/When_building_components.cs b/src/NServiceBus.ContainerTests/When_building_components.cs deleted file mode 100644 index 440de2b6237..00000000000 --- a/src/NServiceBus.ContainerTests/When_building_components.cs +++ /dev/null @@ -1,196 +0,0 @@ -namespace NServiceBus.ContainerTests; - -using System; -using Microsoft.Extensions.DependencyInjection; -using NUnit.Framework; - -public class When_building_components -{ - [Test] - public void Singleton_components_should_yield_the_same_instance() - { - var serviceCollection = new ServiceCollection(); - InitializeServices(serviceCollection); - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - var instance1 = serviceProvider.GetService(typeof(SingletonComponent)); - var instance2 = serviceProvider.GetService(typeof(SingletonComponent)); - - Assert.That(instance1, Is.EqualTo(instance2)); - } - - [Test] - public void Transient_components_should_yield_unique_instances() - { - var serviceCollection = new ServiceCollection(); - InitializeServices(serviceCollection); - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - var instance1 = serviceProvider.GetService(); - var instance2 = serviceProvider.GetService(); - - Assert.That(instance1, Is.Not.EqualTo(instance2)); - } - - [Test] - public void Scoped_components_should_yield_the_same_instance() - { - var serviceCollection = new ServiceCollection(); - InitializeServices(serviceCollection); - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - var instance1 = serviceProvider.GetService(typeof(ScopedComponent)); - var instance2 = serviceProvider.GetService(typeof(ScopedComponent)); - - Assert.That(instance2, Is.SameAs(instance1)); - } - - [Test] - public void Lambda_scoped_components_should_yield_the_same_instance() - { - var serviceCollection = new ServiceCollection(); - InitializeServices(serviceCollection); - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - var instance1 = serviceProvider.GetService(typeof(ScopedLambdaComponent)); - var instance2 = serviceProvider.GetService(typeof(ScopedLambdaComponent)); - - Assert.That(instance2, Is.SameAs(instance1)); - } - - [Test] - public void Lambda_transient_components_should_yield_unique_instances() - { - var serviceCollection = new ServiceCollection(); - InitializeServices(serviceCollection); - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - var instance1 = serviceProvider.GetService(typeof(TransientLambdaComponent)); - var instance2 = serviceProvider.GetService(typeof(TransientLambdaComponent)); - - Assert.That(instance1, Is.Not.EqualTo(instance2)); - } - - [Test] - public void Lambda_singleton_components_should_yield_the_same_instance() - { - var serviceCollection = new ServiceCollection(); - InitializeServices(serviceCollection); - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - var instance1 = serviceProvider.GetService(typeof(SingletonLambdaComponent)); - var instance2 = serviceProvider.GetService(typeof(SingletonLambdaComponent)); - - Assert.That(instance1, Is.EqualTo(instance2)); - } - - [Test] - public void Resolving_all_components_of_unregistered_types_should_give_empty_list() - { - var serviceCollection = new ServiceCollection(); - InitializeServices(serviceCollection); - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - Assert.That(serviceProvider.GetServices(), Is.Empty); - } - - [Test] - public void Resolving_recursive_types_does_not_stack_overflow() - { - try - { - var serviceCollection = new ServiceCollection(); - InitializeServices(serviceCollection); - using var serviceProvider = serviceCollection.BuildServiceProvider(); - serviceProvider.GetService(typeof(RecursiveComponent)); - } - catch (Exception) - { - // this can't be a StackOverflowException as they can't be caught - } - } - - static void InitializeServices(IServiceCollection serviceCollection) - { - serviceCollection.AddSingleton(); - serviceCollection.AddTransient(); - serviceCollection.AddScoped(); - serviceCollection.AddSingleton(_ => new SingletonLambdaComponent()); - serviceCollection.AddTransient(_ => new TransientLambdaComponent()); - serviceCollection.AddScoped(_ => new ScopedLambdaComponent()); - serviceCollection.AddSingleton(_ => new RecursiveComponent()); - } - - public class RecursiveComponent - { - public RecursiveComponent Instance { get; set; } - } - - public class SingletonComponent - { - } - - public interface ISingletonComponentWithPropertyDependency - { - } - - public class SingletonComponentWithPropertyDependency : ISingletonComponentWithPropertyDependency - { - public SingletonComponent Dependency { get; set; } - } - - public class TransientComponent - { - } - - public class UnregisteredComponent - { - public SingletonComponent SingletonComponent { get; set; } - } - - public class SingletonLambdaComponent - { - } - - public class ScopedLambdaComponent - { - } - - public class TransientLambdaComponent - { - } -} - -public class StaticFactory -{ -#pragma warning disable CA1822 // Mark members as static - public ComponentCreatedByFactory Create() -#pragma warning restore CA1822 // Mark members as static - { - return new ComponentCreatedByFactory(); - } -} - -public class ComponentCreatedByFactory -{ -} - -public class ComponentWithBothConstructorAndSetterInjection -{ - public ComponentWithBothConstructorAndSetterInjection(ConstructorDependency constructorDependency) - { - ConstructorDependency = constructorDependency; - } - - public ConstructorDependency ConstructorDependency { get; } - - public SetterDependency SetterDependency { get; set; } -} - -public class ConstructorDependency -{ -} - -public class SetterDependency -{ -} \ No newline at end of file diff --git a/src/NServiceBus.ContainerTests/When_disposing_the_builder.cs b/src/NServiceBus.ContainerTests/When_disposing_the_builder.cs deleted file mode 100644 index 617c9ae9506..00000000000 --- a/src/NServiceBus.ContainerTests/When_disposing_the_builder.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace NServiceBus.ContainerTests; - -using System; -using Microsoft.Extensions.DependencyInjection; -using NUnit.Framework; - -public class When_disposing_the_builder -{ - [Test] - public void Should_dispose_all_IDisposable_components() - { - var serviceCollection = new ServiceCollection(); - - DisposableComponent.DisposeCalled = false; - AnotherSingletonComponent.DisposeCalled = false; - - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(new AnotherSingletonComponent()); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - serviceProvider.GetService(typeof(DisposableComponent)); - serviceProvider.GetService(typeof(AnotherSingletonComponent)); - (serviceProvider as IDisposable)?.Dispose(); - - using (Assert.EnterMultipleScope()) - { - Assert.That(DisposableComponent.DisposeCalled, Is.True, "Dispose should be called on DisposableComponent"); - Assert.That(AnotherSingletonComponent.DisposeCalled, Is.False, "Dispose should not be called on AnotherSingletonComponent"); - } - } - - public class DisposableComponent : IDisposable - { - public static bool DisposeCalled { get; set; } - - public void Dispose() - { - DisposeCalled = true; - GC.SuppressFinalize(this); - } - } - - public class AnotherSingletonComponent : IDisposable - { - public static bool DisposeCalled { get; set; } - - public void Dispose() - { - DisposeCalled = true; - GC.SuppressFinalize(this); - } - } -} \ No newline at end of file diff --git a/src/NServiceBus.ContainerTests/When_querying_for_registered_components.cs b/src/NServiceBus.ContainerTests/When_querying_for_registered_components.cs deleted file mode 100644 index 4858e2e4757..00000000000 --- a/src/NServiceBus.ContainerTests/When_querying_for_registered_components.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace NServiceBus.ContainerTests; - -using System.Linq; -using Microsoft.Extensions.DependencyInjection; -using NUnit.Framework; - -[TestFixture] -public class When_querying_for_registered_components -{ - [Test] - public void Existing_components_should_return_true() - { - var serviceCollection = new ServiceCollection(); - InitializeBuilder(serviceCollection); - - Assert.That(serviceCollection.Any(sd => sd.ServiceType == typeof(ExistingComponent)), Is.True); - } - - [Test] - public void Non_existing_components_should_return_false() - { - var serviceCollection = new ServiceCollection(); - InitializeBuilder(serviceCollection); - - Assert.That(serviceCollection.Any(sd => sd.ServiceType == typeof(NonExistingComponent)), Is.False); - } - - [Test] - public void Builders_should_not_determine_existence_by_building_components() - { - var serviceCollection = new ServiceCollection(); - InitializeBuilder(serviceCollection); - - Assert.That(serviceCollection.Any(sd => sd.ServiceType == typeof(ExistingComponentWithUnsatisfiedDependency)), Is.True); - } - - static void InitializeBuilder(IServiceCollection c) - { - c.AddTransient(); - c.AddTransient(); - } - - public class NonExistingComponent - { - } - - public class ExistingComponent - { - } - - public class ExistingComponentWithUnsatisfiedDependency - { - public ExistingComponentWithUnsatisfiedDependency(NonExistingComponent dependency) - { - - } - } -} \ No newline at end of file diff --git a/src/NServiceBus.ContainerTests/When_registering_components.cs b/src/NServiceBus.ContainerTests/When_registering_components.cs deleted file mode 100644 index 1a8ed1dbb30..00000000000 --- a/src/NServiceBus.ContainerTests/When_registering_components.cs +++ /dev/null @@ -1,194 +0,0 @@ -namespace NServiceBus.ContainerTests; - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.DependencyInjection; -using NUnit.Framework; - - -public class When_registering_components -{ - [Test] - public void Multiple_registrations_of_the_same_component_should_be_allowed() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - Assert.That(serviceProvider.GetServices().Count(), Is.EqualTo(2)); - } - - [Test] - public void Should_support_lambdas_that_uses_other_components_registered_later() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddTransient(s => ((StaticFactory)s.GetService(typeof(StaticFactory))).Create()); - serviceCollection.AddSingleton(_ => new StaticFactory()); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - Assert.That(serviceProvider.GetService(typeof(ComponentCreatedByFactory)), Is.Not.Null); - } - - [Test] - public void A_registration_should_be_allowed_to_be_updated() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(new SingletonComponent()); - serviceCollection.AddSingleton(new AnotherSingletonComponent()); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - Assert.That(serviceProvider.GetService(typeof(ISingletonComponent)), Is.InstanceOf()); - } - - [Test] - public void Register_singleton_should_be_supported() - { - var singleton = new SingletonComponent(); - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(singleton); - serviceCollection.AddSingleton(singleton); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - Assert.That(singleton, Is.EqualTo(serviceProvider.GetService(typeof(SingletonComponent)))); - Assert.That(singleton, Is.EqualTo(serviceProvider.GetService(typeof(ISingletonComponent)))); - } - - [Test] - public void Registering_the_same_singleton_for_different_interfaces_should_be_supported() - { - var serviceCollection = new ServiceCollection(); - var singleton = new SingletonThatImplementsToInterfaces(); - serviceCollection.AddSingleton(singleton); - serviceCollection.AddSingleton(singleton); - serviceCollection.AddTransient(); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - var dependency = (ComponentThatDependsOnMultiSingletons)serviceProvider.GetService(typeof(ComponentThatDependsOnMultiSingletons)); - - using (Assert.EnterMultipleScope()) - { - Assert.That(dependency.Singleton1, Is.Not.Null); - Assert.That(dependency.Singleton2, Is.Not.Null); - - Assert.That(singleton, Is.EqualTo(serviceProvider.GetService(typeof(ISingleton1)))); - } - Assert.That(singleton, Is.EqualTo(serviceProvider.GetService(typeof(ISingleton2)))); - } - - [Test] - public void Given_lookupType_should_be_used_as_service_in_the_registration_when_RegisterSingleton() - { - var serviceCollection = new ServiceCollection(); - var expected = new InheritedFromSomeClass(); - serviceCollection.AddSingleton(expected); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - Assert.That(serviceProvider.GetService(typeof(SomeClass)), Is.EqualTo(expected)); - - using (var scope = serviceProvider.CreateScope()) - { - Assert.That(scope.ServiceProvider.GetService(typeof(SomeClass)), Is.EqualTo(expected)); - } - } -} - - -public class ComponentThatDependsOnMultiSingletons -{ - public ComponentThatDependsOnMultiSingletons(ISingleton1 singleton1, ISingleton2 singleton2) - { - Singleton1 = singleton1; - Singleton2 = singleton2; - } - - public ISingleton1 Singleton1 { get; private set; } - public ISingleton2 Singleton2 { get; private set; } -} - -public class SingletonThatImplementsToInterfaces : ISingleton2 -{ -} - -public interface ISingleton2 : ISingleton1 -{ -} - -public interface ISingleton1 -{ -} - -public class ComponentWithMultipleInterfaces : ISomeInterface, ISomeOtherInterface -{ -} - -public class ComponentWithGenericInterface : ISomeGenericInterface -{ -} - -public class ComponentWithSystemInterface : IGrouping, IDisposable -{ - public IEnumerator GetEnumerator() - { - throw new NotImplementedException(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - public string Key => throw new NotImplementedException(); - - public void Dispose() - { - GC.SuppressFinalize(this); - } -} - -public interface ISomeGenericInterface -{ -} - -public interface ISomeOtherInterface : IYetAnotherInterface -{ -} - -public interface IYetAnotherInterface -{ -} - -public class DuplicateClass -{ - public bool SomeProperty { get; set; } - public bool AnotherProperty { get; set; } -} - -public interface IWithSetterDependencies -{ -} - -public class SomeClass : ISomeInterface -{ -} - -public class InheritedFromSomeClass : SomeClass -{ -} - -public class SomeOtherClass : ISomeInterface -{ -} - -public interface ISomeInterface -{ -} - -#pragma warning disable CA1711 // Identifiers should not have incorrect suffix -public enum SomeEnum -#pragma warning restore CA1711 // Identifiers should not have incorrect suffix -{ - X -} \ No newline at end of file diff --git a/src/NServiceBus.ContainerTests/When_using_nested_containers.cs b/src/NServiceBus.ContainerTests/When_using_nested_containers.cs deleted file mode 100644 index 8e01e2abd11..00000000000 --- a/src/NServiceBus.ContainerTests/When_using_nested_containers.cs +++ /dev/null @@ -1,244 +0,0 @@ -namespace NServiceBus.ContainerTests; - -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using NUnit.Framework; - -public class When_using_nested_containers -{ - [Test] - public async Task Scoped__components_should_be_disposed_when_the_child_container_is_disposed() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddScoped(); - - var serviceProvider = serviceCollection.BuildServiceProvider(); - await using (serviceProvider.ConfigureAwait(false)) - { - var scope = serviceProvider.CreateAsyncScope(); - await using (scope.ConfigureAwait(false)) - { - scope.ServiceProvider.GetService(typeof(ScopedComponent)); - } - } - - Assert.That(ScopedComponent.DisposeCalled, Is.True); - } - - [Test] - public void Scoped_components_should_yield_different_instances_between_parent_and_child_containers() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddScoped(); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - var parentInstance = serviceProvider.GetService(typeof(ScopedComponent)); - using (var scope = serviceProvider.CreateScope()) - { - var childInstance = scope.ServiceProvider.GetService(typeof(ScopedComponent)); - - Assert.That(childInstance, Is.Not.SameAs(parentInstance)); - } - } - - [Test] - public void Scoped_components_should_yield_different_instances_between_different_instances_of_child_containers() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddScoped(); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - object instance1; - using (var scope = serviceProvider.CreateScope()) - { - instance1 = scope.ServiceProvider.GetService(typeof(ScopedComponent)); - } - - object instance2; - using (var scope = serviceProvider.CreateScope()) - { - instance2 = scope.ServiceProvider.GetService(typeof(ScopedComponent)); - } - Assert.That(instance2, Is.Not.SameAs(instance1)); - } - - [Test] - public void Transient_components_should_not_be_shared_across_child_containers() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddTransient(); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - object instance1; - using (var scope = serviceProvider.CreateScope()) - { - instance1 = scope.ServiceProvider.GetService(typeof(TransientComponent)); - } - - object instance2; - using (var scope = serviceProvider.CreateScope()) - { - instance2 = scope.ServiceProvider.GetService(typeof(TransientComponent)); - } - - Assert.That(instance2, Is.Not.SameAs(instance1)); - } - - [Test] - public void Scoped_components_in_the_parent_container_should_be_singletons_in_the_same_child_container() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddScoped(); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - using (var scope = serviceProvider.CreateScope()) - { - var instance1 = scope.ServiceProvider.GetService(typeof(ScopedComponent)); - var instance2 = scope.ServiceProvider.GetService(typeof(ScopedComponent)); - - Assert.That(instance2, Is.SameAs(instance1), "UoW's should be singleton in child container"); - } - } - - [Test] - public void Scoped_components_built_on_root_container_should_be_singletons_even_with_child_builder_present() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddScoped(); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - using (serviceProvider.CreateScope()) - { - } - - var instance1 = serviceProvider.GetService(typeof(ScopedComponent)); - var instance2 = serviceProvider.GetService(typeof(ScopedComponent)); - - Assert.That(instance2, Is.SameAs(instance1), "UoW's should be singletons in the root container"); - } - - [Test] - public void Should_not_dispose_singletons_when_container_goes_out_of_scope() - { - var serviceCollection = new ServiceCollection(); - var singletonInMainContainer = new SingletonComponent(); - serviceCollection.AddSingleton(singletonInMainContainer); - serviceCollection.AddScoped(); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - using (var scope = serviceProvider.CreateScope()) - { - scope.ServiceProvider.GetService(typeof(ComponentThatDependsOfSingleton)); - } - Assert.That(SingletonComponent.DisposeCalled, Is.False); - } - - [Test] - public void Should_dispose_all_non_singleton_IDisposable_components_in_child_container() - { - var serviceCollection = new ServiceCollection(); - DisposableComponent.DisposeCalled = false; - AnotherDisposableComponent.DisposeCalled = false; - serviceCollection.AddSingleton(new AnotherDisposableComponent()); - serviceCollection.AddScoped(); - - - using (var serviceProvider = serviceCollection.BuildServiceProvider()) - using (var scope = serviceProvider.CreateScope()) - { - scope.ServiceProvider.GetService(typeof(DisposableComponent)); - } - - using (Assert.EnterMultipleScope()) - { - Assert.That(AnotherDisposableComponent.DisposeCalled, Is.False, "Dispose should not be called on AnotherSingletonComponent because it belongs to main container"); - Assert.That(DisposableComponent.DisposeCalled, Is.True, "Dispose should be called on DisposableComponent"); - } - } - - public interface IInstanceToReplaceInNested - { - } - - public class InstanceToReplaceInNested_Parent : IInstanceToReplaceInNested - { - } - - public class InstanceToReplaceInNested_Child : IInstanceToReplaceInNested - { - } - - class SingletonComponent : ISingletonComponent, IDisposable - { - public void Dispose() - { - DisposeCalled = true; - } - - public static bool DisposeCalled; - } - - class ComponentThatDependsOfSingleton - { - } -} - -public class TransientComponent : IDisposable -{ - public void Dispose() - { - GC.SuppressFinalize(this); - } -} - -public class ScopedComponent : IDisposable -{ - public void Dispose() - { - DisposeCalled = true; - GC.SuppressFinalize(this); - } - - public static bool DisposeCalled { get; private set; } -} - -public class SingletonComponent : ISingletonComponent -{ -} - -public class AnotherSingletonComponent : ISingletonComponent -{ -} - -public interface ISingletonComponent -{ -} - -public class DisposableComponent : IDisposable -{ - public static bool DisposeCalled { get; set; } - - public void Dispose() - { - DisposeCalled = true; - GC.SuppressFinalize(this); - } -} - -public class AnotherDisposableComponent : IDisposable -{ - public static bool DisposeCalled { get; set; } - - public void Dispose() - { - DisposeCalled = true; - GC.SuppressFinalize(this); - } -} \ 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 acbacb5d5ae..f269fab38a3 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -1,6 +1,5 @@ [assembly: System.CLSCompliant(false)] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"NServiceBus.AcceptanceTesting, PublicKey=0024000004800000940000000602000000240000525341310004000001000100dde965e6172e019ac82c2639ffe494dd2e7dd16347c34762a05732b492e110f2e4e2e1b5ef2d85c848ccfb671ee20a47c8d1376276708dc30a90ff1121b647ba3b7259a6bc383b2034938ef0e275b58b920375ac605076178123693c6c4f1331661a62eba28c249386855637780e3ff5f23a6d854700eaa6803ef48907513b92")] -[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"NServiceBus.ContainerTests, PublicKey=00240000048000009400000006020000002400005253413100040000010001007f16e21368ff041183fab592d9e8ed37e7be355e93323147a1d29983d6e591b04282e4da0c9e18bd901e112c0033925eb7d7872c2f1706655891c5c9d57297994f707d16ee9a8f40d978f064ee1ffc73c0db3f4712691b23bf596f75130f4ec978cf78757ec034625a5f27e6bb50c618931ea49f6f628fd74271c32959efb1c5")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"NServiceBus.Core.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001007f16e21368ff041183fab592d9e8ed37e7be355e93323147a1d29983d6e591b04282e4da0c9e18bd901e112c0033925eb7d7872c2f1706655891c5c9d57297994f707d16ee9a8f40d978f064ee1ffc73c0db3f4712691b23bf596f75130f4ec978cf78757ec034625a5f27e6bb50c618931ea49f6f628fd74271c32959efb1c5")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"NServiceBus.PersistenceTests, PublicKey=00240000048000009400000006020000002400005253413100040000010001007f16e21368ff041183fab592d9e8ed37e7be355e93323147a1d29983d6e591b04282e4da0c9e18bd901e112c0033925eb7d7872c2f1706655891c5c9d57297994f707d16ee9a8f40d978f064ee1ffc73c0db3f4712691b23bf596f75130f4ec978cf78757ec034625a5f27e6bb50c618931ea49f6f628fd74271c32959efb1c5")] [assembly: System.Runtime.InteropServices.ComVisible(false)] diff --git a/src/NServiceBus.Core/NServiceBus.Core.csproj b/src/NServiceBus.Core/NServiceBus.Core.csproj index 1aa0b8f960e..f660bd6476a 100644 --- a/src/NServiceBus.Core/NServiceBus.Core.csproj +++ b/src/NServiceBus.Core/NServiceBus.Core.csproj @@ -48,7 +48,6 @@ - diff --git a/src/NServiceBus.slnx b/src/NServiceBus.slnx index 2921f61f2ae..63b3860b326 100644 --- a/src/NServiceBus.slnx +++ b/src/NServiceBus.slnx @@ -12,7 +12,6 @@ - From 46a4cbf17b969496276a5339b14bdff9d8161c3b Mon Sep 17 00:00:00 2001 From: Adam McCoy Date: Mon, 8 Jun 2026 18:53:34 +1000 Subject: [PATCH 03/79] Support structured logging in downstream providers (#7804) * Pass predictably shaped state objects to underlying logging providers * Add test to verify structured state preservation in slot scope logging --------- Co-authored-by: Daniel Marbach --- .../Logging/EndpointLoggingScopeTests.cs | 22 +++++++++++++++++++ .../Logging/MicrosoftLoggerAdapter.cs | 9 ++++---- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/NServiceBus.Core.Tests/Logging/EndpointLoggingScopeTests.cs b/src/NServiceBus.Core.Tests/Logging/EndpointLoggingScopeTests.cs index 11b1d04f188..bc411c18680 100644 --- a/src/NServiceBus.Core.Tests/Logging/EndpointLoggingScopeTests.cs +++ b/src/NServiceBus.Core.Tests/Logging/EndpointLoggingScopeTests.cs @@ -33,6 +33,26 @@ public void Should_include_endpoint_name_and_identifier_for_multi_hosted_endpoin new KeyValuePair("EndpointIdentifier", "blue")); } + [Test] + public void Should_preserve_structured_state_when_logging_in_slot_scope() + { + var loggerFactory = new FakeLoggerLoggerFactory(); + var slot = new EndpointLogSlot("Sales", "blue"); + LogManager.RegisterSlotFactory(slot, new MicrosoftLoggerFactoryAdapter(loggerFactory)); + + var logger = LogManager.GetLogger($"{nameof(EndpointLoggingScopeTests)}-{Guid.NewGuid():N}"); + + using (LogManager.BeginSlotScope(slot)) + { + logger.InfoFormat("message {0}", "value"); + } + + Assert.That(loggerFactory.CapturedStructuredState, Has.Some.Matches>?>(state => + state is not null && + state.Any(kv => kv is { Key: "{OriginalFormat}", Value: "message {0}" }) && + state.Any(kv => kv is { Key: "0", Value: "value" }))); + } + [Test] public void Should_include_only_endpoint_name_when_identifier_is_not_provided() { @@ -314,6 +334,8 @@ sealed class FakeLoggerLoggerFactory : Microsoft.Extensions.Logging.ILoggerFacto public List CapturedMessages => [.. provider.Collector.GetSnapshot().Select(r => r.Message)]; + public List>?> CapturedStructuredState => [.. provider.Collector.GetSnapshot().Select(r => r.StructuredState)]; + public List>> CapturedLogScopes => [ .. provider.Collector.GetSnapshot() diff --git a/src/NServiceBus.Core/Logging/MicrosoftLoggerAdapter.cs b/src/NServiceBus.Core/Logging/MicrosoftLoggerAdapter.cs index 8ec267e7642..829ce9154fb 100644 --- a/src/NServiceBus.Core/Logging/MicrosoftLoggerAdapter.cs +++ b/src/NServiceBus.Core/Logging/MicrosoftLoggerAdapter.cs @@ -8,9 +8,6 @@ namespace NServiceBus; sealed class MicrosoftLoggerAdapter(ILogger logger) : ILog { - static readonly Func MessageFormatter = static (state, _) => state ?? string.Empty; - static readonly Func<(string format, object?[] args), Exception?, string> FormatMessageFormatter = static (state, _) => string.Format(state.format, state.args); - public bool IsDebugEnabled => logger.IsEnabled(LogLevel.Debug); public bool IsInfoEnabled => logger.IsEnabled(LogLevel.Information); public bool IsWarnEnabled => logger.IsEnabled(LogLevel.Warning); @@ -47,6 +44,7 @@ sealed class MicrosoftLoggerAdapter(ILogger logger) : ILog public void FatalFormat(string format, params object?[] args) => WriteFormat(LogLevel.Critical, format, args); +#pragma warning disable CA2254 // Template should be a static expression -- this is infrastructure, not application code void Write(LogLevel level, string? message, Exception? exception = null) { if (!logger.IsEnabled(level)) @@ -54,7 +52,7 @@ void Write(LogLevel level, string? message, Exception? exception = null) return; } - logger.Log(level, default, message, exception, MessageFormatter); + logger.Log(level, default, exception, message); } void WriteFormat(LogLevel level, string format, object?[] args) @@ -64,6 +62,7 @@ void WriteFormat(LogLevel level, string format, object?[] args) return; } - logger.Log(level, default, (format, args), null, FormatMessageFormatter); + logger.Log(level, default, null, format, args); } +#pragma warning restore CA2254 } \ No newline at end of file From 6ecfa7fa3bd9d0d476e8d7fd55cb21154aa27b35 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:41:43 +0200 Subject: [PATCH 04/79] Update dependency Particular.AnalyzerTesting to 2.1.0 (#7808) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- .../NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj index 0e21071087f..7a8e0c540d3 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj @@ -20,7 +20,7 @@ - + From 09a753a965d4ecefe87df7a15d62859dddcdba18 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Thu, 11 Jun 2026 18:57:44 +1000 Subject: [PATCH 05/79] Fix NSB0033 incorrectly reporting mixed handler style when helper overloads are called from interface Handle (#7807) * Fix NSB0033 false positive for Handle overloads with a non-message first parameter A public method named `Handle` whose second parameter is `IMessageHandlerContext` and that returns `Task` was classified as a convention-based handler regardless of its first parameter type. A helper overload such as `Handle(string text, IMessageHandlerContext context, ...)` therefore looked like a convention-based handler for messages of type `string`, and on a class that also implements `IHandleMessages` this raised NSB0033 (mixed interface/convention style). `IsValidConventionBasedHandleMethod` now requires the first parameter to be a plausible message type: it rejects framework types (any non-`None` SpecialType such as `string`, primitives, `object`), value types, and types in the `System` namespace. NServiceBus messages are user-defined reference types, so none of these can be a message. This is complementary to #7770, which only excluded methods implementing a handler-derived interface member; a standalone helper overload implements no interface and slipped through. * Enhance convention-based handler detection by incorporating compilation context * More efficient * It is fine to use the symbols directly since we are not caching them --------- Co-authored-by: Daniel Marbach --- .../Handlers/HandlerAttributeFixer.cs | 2 +- .../Handlers/HandlerAttributeAnalyzerTests.cs | 134 ++++++++++++++++ .../Handlers/ConventionBasedHandlerHelper.cs | 145 +++++++++++++++--- .../Handlers/HandlerAttributeAnalyzer.cs | 4 +- .../Handlers/Handlers.Parser.cs | 6 +- 5 files changed, 264 insertions(+), 27 deletions(-) diff --git a/src/NServiceBus.Core.Analyzer.Fixes/Handlers/HandlerAttributeFixer.cs b/src/NServiceBus.Core.Analyzer.Fixes/Handlers/HandlerAttributeFixer.cs index 5e3b568fbc7..cae91242bb1 100644 --- a/src/NServiceBus.Core.Analyzer.Fixes/Handlers/HandlerAttributeFixer.cs +++ b/src/NServiceBus.Core.Analyzer.Fixes/Handlers/HandlerAttributeFixer.cs @@ -164,7 +164,7 @@ static async Task MoveHandlerAttribute( } var isInterfaceBasedHandler = type.ImplementsGenericInterface(knownTypes.IHandleMessages); - var isConventionBasedHandler = !isInterfaceBasedHandler && ConventionBasedHandlerHelper.HasValidConventionBasedHandleMethods(type, knownTypes); + var isConventionBasedHandler = !isInterfaceBasedHandler && ConventionBasedHandlerHelper.HasValidConventionBasedHandleMethods(type, knownTypes, cancellationToken: cancellationToken); if (!isInterfaceBasedHandler && !isConventionBasedHandler) { diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/HandlerAttributeAnalyzerTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/HandlerAttributeAnalyzerTests.cs index 7285e9ac380..3bbb5edbaef 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/HandlerAttributeAnalyzerTests.cs +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/HandlerAttributeAnalyzerTests.cs @@ -659,6 +659,140 @@ class MyMessage : IMessage {} return Assert(source); } + [Test] + public Task DoesNotReportMixedStyleForHelperMethodNamedHandleWithNonMessageFirstParameter() + { + // A pure interface-based handler with an unrelated public helper overload that happens to be + // named Handle and takes IMessageHandlerContext. The helper is called from the interface Handle + // method, so it is not a convention-based handler — this is not a mixed style. + var source = + """ + using System.Threading; + using System.Threading.Tasks; + using NServiceBus; + + [Handler] + class MyHandler : IHandleMessages + { + public Task Handle(MyMessage message, IMessageHandlerContext context) => Handle("text", context); + + public Task Handle(string text, IMessageHandlerContext context, CancellationToken cancellation = default) => + Task.CompletedTask; + } + + class MyMessage : IMessage {} + """; + + return Assert(source); + } + + [Test] + public Task DoesNotReportMixedStyleForHelperWithUserDefinedTypeCalledFromInterfaceHandle() + { + // A helper with a user-defined DTO as first param that is called from the interface Handle method. + // The call-site analysis detects the invocation, so this is not a mixed style. + var source = + """ + using System.Threading.Tasks; + using NServiceBus; + + class MyDto {} + + [Handler] + class MyHandler : IHandleMessages + { + public Task Handle(MyMessage message, IMessageHandlerContext context) => Handle(new MyDto(), context); + + public Task Handle(MyDto dto, IMessageHandlerContext context) => Task.CompletedTask; + } + + class MyMessage : IMessage {} + """; + + return Assert(source); + } + + [Test] + public Task DoesNotReportMixedStyleForHelperCalledFromBlockBodiedInterfaceHandle() + { + // Same pattern but with a block-bodied interface Handle method. + var source = + """ + using System.Threading.Tasks; + using NServiceBus; + + [Handler] + class MyHandler : IHandleMessages + { + public async Task Handle(MyMessage message, IMessageHandlerContext context) + { + await Handle("text", context); + } + + public Task Handle(string text, IMessageHandlerContext context) => Task.CompletedTask; + } + + class MyMessage : IMessage {} + """; + + return Assert(source); + } + + [Test] + public Task ReportsMixedStyleWhenConventionBasedMethodNotCalledFromInterfaceHandle() + { + // A convention-based Handle method that is NOT called from the interface Handle method + // should still be reported as mixed style. + var source = + """ + using System.Threading.Tasks; + using NServiceBus; + + [Handler] + class [|MyHandler|] : IHandleMessages + { + public Task Handle(MyMessage message, IMessageHandlerContext context) => Task.CompletedTask; + + public Task Handle(OtherMessage message, IMessageHandlerContext context) => Task.CompletedTask; + } + + class MyMessage : IMessage {} + class OtherMessage : IMessage {} + """; + + return Assert(source, DiagnosticIds.ConventionBasedHandlerMixedStyle); + } + + [Test] + public Task DoesNotReportMixedStyleForHelperPassingMessageProperties() + { + // A helper that receives individual properties extracted from the message. + // The call-site analysis detects it is called from the interface Handle method. + var source = + """ + using System.Threading.Tasks; + using NServiceBus; + + [Handler] + class MyHandler : IHandleMessages + { + public Task Handle(MyMessage message, IMessageHandlerContext context) => + Handle(message.Id, message.Name, context); + + public Task Handle(string id, string name, IMessageHandlerContext context) => + Task.CompletedTask; + } + + class MyMessage : IMessage + { + public string Id { get; set; } + public string Name { get; set; } + } + """; + + return Assert(source); + } + [Test] public Task DoesNotReportConventionBasedHandlerImplementingUnrelatedInterfaceWithHandleMethod() { diff --git a/src/NServiceBus.Core.Analyzer/Handlers/ConventionBasedHandlerHelper.cs b/src/NServiceBus.Core.Analyzer/Handlers/ConventionBasedHandlerHelper.cs index fc3d10dc9fb..48185b1d13f 100644 --- a/src/NServiceBus.Core.Analyzer/Handlers/ConventionBasedHandlerHelper.cs +++ b/src/NServiceBus.Core.Analyzer/Handlers/ConventionBasedHandlerHelper.cs @@ -4,17 +4,24 @@ namespace NServiceBus.Core.Analyzer.Handlers; using System.Collections.Generic; using System.Linq; +using System.Threading; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; static class ConventionBasedHandlerHelper { public const string HandleMethodName = "Handle"; - public static bool IsConventionBasedHandlerType(INamedTypeSymbol classType, HandlerKnownTypes knownTypes) + public static bool IsConventionBasedHandlerType(INamedTypeSymbol classType, HandlerKnownTypes knownTypes, Compilation? compilation = null, CancellationToken cancellationToken = default) { + var interfaceMessageTypes = CollectInterfaceMessageTypes(classType, knownTypes); + var interfaceHandleCallees = compilation is null + ? null + : CollectInterfaceHandleCallees(classType, knownTypes, compilation, cancellationToken); + for (var current = classType; current is not null; current = current.BaseType) { - if (HasValidConventionBasedHandleMethods(current, knownTypes, classType)) + if (HasValidConventionBasedHandleMethods(current, knownTypes, interfaceMessageTypes, interfaceHandleCallees)) { return true; } @@ -23,38 +30,39 @@ public static bool IsConventionBasedHandlerType(INamedTypeSymbol classType, Hand return false; } - public static bool HasValidConventionBasedHandleMethods(INamedTypeSymbol classType, HandlerKnownTypes knownTypes) => - HasValidConventionBasedHandleMethods(classType, knownTypes, classType); + public static bool HasValidConventionBasedHandleMethods(INamedTypeSymbol classType, HandlerKnownTypes knownTypes, Compilation? compilation = null, CancellationToken cancellationToken = default) => + HasValidConventionBasedHandleMethods(classType, knownTypes, + CollectInterfaceMessageTypes(classType, knownTypes), + compilation is null ? null : CollectInterfaceHandleCallees(classType, knownTypes, compilation, cancellationToken)); - static bool HasValidConventionBasedHandleMethods(INamedTypeSymbol classType, HandlerKnownTypes knownTypes, INamedTypeSymbol interfaceImplementationType) + static bool HasValidConventionBasedHandleMethods(INamedTypeSymbol classType, HandlerKnownTypes knownTypes, HashSet interfaceMessageTypes, HashSet? interfaceHandleCallees) { - var interfaceMessageTypes = new HashSet(System.StringComparer.Ordinal); - foreach (var iface in interfaceImplementationType.AllInterfaces) + foreach (var member in classType.GetMembers()) { - if (iface.IsGenericType && HandlerConventions.IsHandlerInterface(iface.OriginalDefinition, knownTypes) && - iface.TypeArguments[0] is INamedTypeSymbol msgType) + if (member is not IMethodSymbol method) { - interfaceMessageTypes.Add(msgType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + continue; } - } - foreach (var member in classType.GetMembers()) - { - if (member is not IMethodSymbol method) + if (!IsValidConventionBasedHandleMethod(method, knownTypes, interfaceMessageTypes)) { continue; } - if (IsValidConventionBasedHandleMethod(method, knownTypes, interfaceMessageTypes, interfaceImplementationType)) + // Exclude methods that are helpers called from within an interface Handle implementation + if (interfaceHandleCallees is not null && + (interfaceHandleCallees.Contains(method) || interfaceHandleCallees.Contains(method.OriginalDefinition))) { - return true; + continue; } + + return true; } return false; } - public static bool IsValidConventionBasedHandleMethod(IMethodSymbol method, HandlerKnownTypes knownTypes, HashSet interfaceMessageTypes, INamedTypeSymbol? interfaceImplementationType = null) + public static bool IsValidConventionBasedHandleMethod(IMethodSymbol method, HandlerKnownTypes knownTypes, HashSet interfaceMessageTypes, INamedTypeSymbol? interfaceImplementationType = null) { if (method.Name != HandleMethodName || method.DeclaredAccessibility != Accessibility.Public || @@ -88,13 +96,13 @@ public static bool IsValidConventionBasedHandleMethod(IMethodSymbol method, Hand } // If the class implements IHandleMessages for this exact message type with exactly 2 params, - // this Handle method is the interface implementation — not a convention-based method. - var messageTypeFqn = messageType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - if (method.Parameters.Length == 2 && interfaceMessageTypes.Contains(messageTypeFqn)) + // this Handle method is the interface implementation, not a convention-based method. + if (method.Parameters.Length == 2 && interfaceMessageTypes.Contains(messageType)) { return false; } + // If this Handle method is the implementation of an interface member that belongs to a handler // interface hierarchy (IHandleMessages, IHandleTimeouts, IAmStartedByMessages, or an // interface deriving from them), it is interface-based, not convention-based. @@ -107,6 +115,96 @@ public static bool IsValidConventionBasedHandleMethod(IMethodSymbol method, Hand return true; } + static HashSet CollectInterfaceMessageTypes(INamedTypeSymbol classType, HandlerKnownTypes knownTypes) + { + var messageTypes = new HashSet(SymbolEqualityComparer.Default); + foreach (var iface in classType.AllInterfaces) + { + if (iface.IsGenericType && HandlerConventions.IsHandlerInterface(iface.OriginalDefinition, knownTypes) && + iface.TypeArguments[0] is INamedTypeSymbol msgType) + { + messageTypes.Add(msgType); + } + } + + return messageTypes; + } + + // Collects all methods that are called from within interface Handle method implementations. + // These are helper methods, not convention-based handlers. + static HashSet CollectInterfaceHandleCallees( + INamedTypeSymbol classType, + HandlerKnownTypes knownTypes, + Compilation compilation, + CancellationToken cancellationToken) + { + var visitedImplementations = new HashSet(SymbolEqualityComparer.Default); + var callees = new HashSet(SymbolEqualityComparer.Default); + SyntaxTree? cachedTree = null; + SemanticModel? cachedModel = null; + + foreach (var iface in classType.AllInterfaces) + { + if (!iface.IsGenericType) + { + continue; + } + + if (!HandlerConventions.IsHandlerInterface(iface.OriginalDefinition, knownTypes)) + { + continue; + } + + foreach (var member in iface.GetMembers(HandleMethodName)) + { + if (member is not IMethodSymbol ifaceMethod) + { + continue; + } + + if (classType.FindImplementationForInterfaceMember(ifaceMethod) is not IMethodSymbol impl) + { + continue; + } + + if (!visitedImplementations.Add(impl)) + { + continue; + } + + foreach (var syntaxRef in impl.DeclaringSyntaxReferences) + { + var syntax = syntaxRef.GetSyntax(cancellationToken); + + if (!ReferenceEquals(syntax.SyntaxTree, cachedTree)) + { + cachedTree = syntax.SyntaxTree; + cachedModel = compilation.GetSemanticModel(cachedTree); + } + + foreach (var invocation in syntax.DescendantNodes().OfType()) + { + if (cachedModel!.GetSymbolInfo(invocation, cancellationToken).Symbol is not IMethodSymbol invokedMethod) + { + continue; + } + + callees.Add(invokedMethod); + callees.Add(invokedMethod.OriginalDefinition); + + if (invokedMethod.ReducedFrom is { } reduced) + { + callees.Add(reduced); + callees.Add(reduced.OriginalDefinition); + } + } + } + } + } + + return callees; + } + static bool ImplementsHandlerInterfaceMember(IMethodSymbol method, HandlerKnownTypes knownTypes, INamedTypeSymbol? interfaceImplementationType) { // Fast path: check explicit interface implementations first (O(1)) @@ -138,8 +236,13 @@ static bool ImplementsHandlerInterfaceMember(IMethodSymbol method, HandlerKnownT continue; } - foreach (var interfaceMethod in iface.GetMembers(method.Name).OfType()) + foreach (var member in iface.GetMembers(method.Name)) { + if (member is not IMethodSymbol interfaceMethod) + { + continue; + } + if (interfaceMethod.Parameters.Length != method.Parameters.Length) { continue; diff --git a/src/NServiceBus.Core.Analyzer/Handlers/HandlerAttributeAnalyzer.cs b/src/NServiceBus.Core.Analyzer/Handlers/HandlerAttributeAnalyzer.cs index 6b591b57926..cb0e8ac8b15 100644 --- a/src/NServiceBus.Core.Analyzer/Handlers/HandlerAttributeAnalyzer.cs +++ b/src/NServiceBus.Core.Analyzer/Handlers/HandlerAttributeAnalyzer.cs @@ -37,7 +37,7 @@ public override void Initialize(AnalysisContext context) var isInterfaceBasedHandler = classType.ImplementsGenericInterface(knownTypes.IHandleMessages); var isSaga = classType.ImplementsGenericType(knownTypes.SagaBase); - var isConventionBasedHandler = !isSaga && ConventionBasedHandlerHelper.IsConventionBasedHandlerType(classType, knownTypes); + var isConventionBasedHandler = !isSaga && ConventionBasedHandlerHelper.IsConventionBasedHandlerType(classType, knownTypes, context.Compilation, context.CancellationToken); if (!isInterfaceBasedHandler || isSaga) { @@ -150,7 +150,7 @@ public override void Initialize(AnalysisContext context) } // Interface-based handler: check for mixed-style (also has convention-based Handle methods) - if (ConventionBasedHandlerHelper.HasValidConventionBasedHandleMethods(classType, knownTypes)) + if (ConventionBasedHandlerHelper.HasValidConventionBasedHandleMethods(classType, knownTypes, context.Compilation, context.CancellationToken)) { var classLocation = classType.GetClassIdentifierLocation(context.CancellationToken); if (classLocation is not null) diff --git a/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Parser.cs b/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Parser.cs index ecde2e0cd10..fd63a0c2757 100644 --- a/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Parser.cs +++ b/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Parser.cs @@ -109,12 +109,12 @@ public static HandlerSpec Parse(INamedTypeSymbol handlerType, .ToList(); // Collect message types already handled by IHandleMessages interface implementations - var interfaceMessageTypes = new HashSet(StringComparer.Ordinal); + var interfaceMessageTypes = new HashSet(SymbolEqualityComparer.Default); foreach (var @interface in handlerType.AllInterfaces.Where(IsHandlerInterface)) { if (@interface.TypeArguments[0] is INamedTypeSymbol msgType) { - interfaceMessageTypes.Add(msgType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + interfaceMessageTypes.Add(msgType); } } @@ -137,7 +137,7 @@ public static HandlerSpec Parse(INamedTypeSymbol handlerType, static List ParseConventionBasedMethods( INamedTypeSymbol handlerType, HandlerKnownTypes knownTypes, - HashSet interfaceMessageTypes, + HashSet interfaceMessageTypes, bool includeInheritedMethods, CancellationToken cancellationToken) { From 69650ff0df37ec9793d6265df52f08ac0cf5035d Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 11:01:40 +0200 Subject: [PATCH 06/79] Update .NET Packages (#7809) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj | 2 +- src/NServiceBus.Core/NServiceBus.Core.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj index 52ccab6f296..48d5617553e 100644 --- a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj +++ b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/NServiceBus.Core/NServiceBus.Core.csproj b/src/NServiceBus.Core/NServiceBus.Core.csproj index f660bd6476a..73896eb238c 100644 --- a/src/NServiceBus.Core/NServiceBus.Core.csproj +++ b/src/NServiceBus.Core/NServiceBus.Core.csproj @@ -13,9 +13,9 @@ - + - + From e02c64668a1935f9c306c076b65a99bc0e5ebe26 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 11:02:10 +0200 Subject: [PATCH 07/79] Update dependency Particular.Licensing.Sources to 7.1.1 (#7810) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- 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 73896eb238c..1ae3e46db14 100644 --- a/src/NServiceBus.Core/NServiceBus.Core.csproj +++ b/src/NServiceBus.Core/NServiceBus.Core.csproj @@ -20,7 +20,7 @@ - + From 42c08d49e53e3d785abd34aeba032906ee758951 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Fri, 12 Jun 2026 02:07:11 +1000 Subject: [PATCH 08/79] Ensure RunningEndpointInstance.DisposeAsync completes when host is disposed without StopAsync (#7806) * DisposeAsync should pass a bounded cancellation token to transport Shutdown * . * Update RunningEndpointInstance.cs * . * , * Update StartableEndpoint.cs * Update RunningEndpointInstance.cs * Remove shutdown token timeout since it would create akward split between dispose and shutdown. Instead cancel the token immediately on dispose. * Core acceptance test * Ignore exceptions during DisposeAsync to prevent throwing from disposal process --------- Co-authored-by: Daniel Marbach --- .../Support/EndpointBehavior.cs | 8 ++ .../When_disposing_without_stopping.cs | 58 +++++++++++++ .../Unicast/RunningEndpointInstanceTest.cs | 86 ++++++++++++++++++- .../Receiving/ReceiveComponent.cs | 2 +- .../Unicast/RunningEndpointInstance.cs | 25 +++++- 5 files changed, 172 insertions(+), 7 deletions(-) create mode 100644 src/NServiceBus.AcceptanceTests/Core/Stopping/When_disposing_without_stopping.cs diff --git a/src/NServiceBus.AcceptanceTesting/Support/EndpointBehavior.cs b/src/NServiceBus.AcceptanceTesting/Support/EndpointBehavior.cs index 2aba4d1cf05..92ad45ee49d 100644 --- a/src/NServiceBus.AcceptanceTesting/Support/EndpointBehavior.cs +++ b/src/NServiceBus.AcceptanceTesting/Support/EndpointBehavior.cs @@ -44,6 +44,14 @@ public EndpointBehavior(IEndpointConfigurationFactory endpointBuilder, int insta return async token => await endpointLifecycle.Stop(token).ConfigureAwait(false); }); + // For some super advanced scenarios require + // disposing the endpoint, and this backdoor allows that without having to expose the lifecycle in Core + collectionAdapter.AddKeyedSingleton>("Disposer", (provider, key) => + { + var endpointLifecycle = provider.GetRequiredKeyedService(serviceKey); + return async () => await endpointLifecycle.DisposeAsync().ConfigureAwait(false); + }); + return Task.FromResult(new StartableEndpointInstance(serviceKey)); }, static (startableEndpoint, provider, cancellationToken) => startableEndpoint.Start(provider, cancellationToken)); } diff --git a/src/NServiceBus.AcceptanceTests/Core/Stopping/When_disposing_without_stopping.cs b/src/NServiceBus.AcceptanceTests/Core/Stopping/When_disposing_without_stopping.cs new file mode 100644 index 00000000000..b9bbca9058d --- /dev/null +++ b/src/NServiceBus.AcceptanceTests/Core/Stopping/When_disposing_without_stopping.cs @@ -0,0 +1,58 @@ +namespace NServiceBus.AcceptanceTests.Core.Stopping; + +using System; +using System.Threading; +using System.Threading.Tasks; +using AcceptanceTesting; +using EndpointTemplates; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; + +public class When_disposing_without_stopping : NServiceBusAcceptanceTest +{ + [Test] + [CancelAfter(30000)] + public async Task Should_initiate_immediate_handler_cancellation(CancellationToken cancellationToken = default) => + await Scenario.Define() + .WithEndpoint(b => + b.ServiceResolve(static async (provider, context, token) => + { + var session = provider.GetRequiredService(); + await session.SendLocal(new MessageThatTakesALongTime(), token); + + await context.MessageReceived.Task.WaitAsync(token); + + var disposer = provider.GetRequiredKeyedService>("Disposer"); + await disposer(); + }, true)) + .Run(cancellationToken); + + public class Context : ScenarioContext + { + public TaskCompletionSource MessageReceived { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + } + + public class EndpointThatGetsRugPulled : EndpointConfigurationBuilder + { + public EndpointThatGetsRugPulled() => EndpointSetup(); + + [Handler] + public class InfiniteHandler(Context testContext) : IHandleMessages + { + public async Task Handle(MessageThatTakesALongTime message, IMessageHandlerContext context) + { + try + { + testContext.MessageReceived.SetResult(); + await Task.Delay(Timeout.InfiniteTimeSpan, context.CancellationToken); + } + catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested) + { + testContext.MarkAsCompleted(); + } + } + } + } + + public class MessageThatTakesALongTime : IMessage; +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/Unicast/RunningEndpointInstanceTest.cs b/src/NServiceBus.Core.Tests/Unicast/RunningEndpointInstanceTest.cs index 2ea4d274604..bd70effd3a6 100644 --- a/src/NServiceBus.Core.Tests/Unicast/RunningEndpointInstanceTest.cs +++ b/src/NServiceBus.Core.Tests/Unicast/RunningEndpointInstanceTest.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using Settings; using Testing; +using NServiceBus.Transport; [TestFixture] public class RunningEndpointInstanceTest @@ -17,10 +18,10 @@ static RunningEndpointInstance Create() var testInstance = new RunningEndpointInstance( settings, - null, + null!, new FeatureComponent(new FeatureComponent.Settings()), new TestableMessageSession(), - null, + null!, new CancellationTokenSource(), NoOpAsyncDisposable.Instance, new EndpointLogSlot("RunningEndpointInstanceTest", endpointIdentifier: null)); @@ -47,6 +48,59 @@ public async Task ShouldAllowMultipleDispose() Assert.That(async () => await testInstance.DisposeAsync(), Throws.Nothing); } + [Test] + public async Task DisposeAsync_should_pass_a_bounded_cancellation_token_to_transport_Shutdown() + { + var capturingTransport = new TokenCapturingTransportInfrastructure(); + + var testInstance = new RunningEndpointInstance( + new SettingsHolder(), + CreateEmptyReceiveComponent(), + new FeatureComponent(new FeatureComponent.Settings()), + new TestableMessageSession(), + capturingTransport, + new CancellationTokenSource(), + NoOpAsyncDisposable.Instance, + new EndpointLogSlot("RunningEndpointInstanceTest", endpointIdentifier: null)); + + await testInstance.DisposeAsync(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(capturingTransport.ShutdownWasCalled, Is.True, + "transport.Shutdown was not called during DisposeAsync."); + Assert.That(capturingTransport.ObservedToken.CanBeCanceled, Is.True, + "DisposeAsync passed CancellationToken.None to StopCore (which then passes it to " + + "transport.Shutdown). Disposal must be bounded by an internal cancellation token so " + + "a stuck transport cannot hang shutdown indefinitely. " + + "See RunningEndpointInstance.DisposeAsync — `await StopCore()` is missing a token."); + } + } + + [Test] + public async Task DisposeAsync_should_complete_even_when_transport_Shutdown_hangs() + { + var hangingTransport = new HangingTransportInfrastructure(); + + var testInstance = new RunningEndpointInstance( + new SettingsHolder(), + CreateEmptyReceiveComponent(), + new FeatureComponent(new FeatureComponent.Settings()), + new TestableMessageSession(), + hangingTransport, + new CancellationTokenSource(), + NoOpAsyncDisposable.Instance, + new EndpointLogSlot("RunningEndpointInstanceTest", endpointIdentifier: null)); + + var dispose = testInstance.DisposeAsync().AsTask(); + var winner = await Task.WhenAny(dispose, Task.Delay(TimeSpan.FromSeconds(5))); + + Assert.That(winner, Is.SameAs(dispose), + "DisposeAsync did not complete within 5s of a 250ms internal timeout — " + + "the disposeShutdownTimeout must fire and let disposal proceed past a stuck transport.Shutdown."); + Assert.DoesNotThrowAsync(() => dispose, "DisposeAsync threw an exception when transport.Shutdown hung. DisposeAsync should complete successfully even if transport.Shutdown does not."); + } + [Test] public async Task ShouldThrowExceptionAfterInvokingStop() { @@ -61,4 +115,32 @@ public async Task ShouldThrowExceptionAfterInvokingStop() Assert.Throws(() => testInstance.Subscribe(typeof(object), new SubscribeOptions()), "Invoking messaging operations on the endpoint instance after it has been triggered to stop is not supported."); Assert.Throws(() => testInstance.Unsubscribe(typeof(object), new UnsubscribeOptions()), "Invoking messaging operations on the endpoint instance after it has been triggered to stop is not supported."); } + + // Empty ReceiveComponent (no receivers) so Stop is a no-op and StopCore reaches + // transport.Shutdown without us having to stand up the full receive pipeline. + static ReceiveComponent CreateEmptyReceiveComponent() => + new(configuration: null!, activityFactory: null!, endpointLogSlot: null!); + + sealed class TokenCapturingTransportInfrastructure : TransportInfrastructure + { + public bool ShutdownWasCalled { get; private set; } + public CancellationToken ObservedToken { get; private set; } + + public override Task Shutdown(CancellationToken cancellationToken = default) + { + ShutdownWasCalled = true; + ObservedToken = cancellationToken; + return Task.CompletedTask; + } + + public override string ToTransportAddress(QueueAddress address) => address.BaseAddress; + } + + sealed class HangingTransportInfrastructure : TransportInfrastructure + { + public override Task Shutdown(CancellationToken cancellationToken = default) => + Task.Delay(Timeout.Infinite, cancellationToken); + + public override string ToTransportAddress(QueueAddress address) => address.BaseAddress; + } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Receiving/ReceiveComponent.cs b/src/NServiceBus.Core/Receiving/ReceiveComponent.cs index f9b6236be20..12ffdc5b7d2 100644 --- a/src/NServiceBus.Core/Receiving/ReceiveComponent.cs +++ b/src/NServiceBus.Core/Receiving/ReceiveComponent.cs @@ -17,7 +17,7 @@ namespace NServiceBus; partial class ReceiveComponent { - ReceiveComponent(Configuration configuration, IActivityFactory activityFactory, EndpointLogSlot endpointLogSlot) + internal ReceiveComponent(Configuration configuration, IActivityFactory activityFactory, EndpointLogSlot endpointLogSlot) { this.configuration = configuration; this.activityFactory = activityFactory; diff --git a/src/NServiceBus.Core/Unicast/RunningEndpointInstance.cs b/src/NServiceBus.Core/Unicast/RunningEndpointInstance.cs index cdf8fc65777..d084ceacf14 100644 --- a/src/NServiceBus.Core/Unicast/RunningEndpointInstance.cs +++ b/src/NServiceBus.Core/Unicast/RunningEndpointInstance.cs @@ -99,11 +99,28 @@ public async ValueTask DisposeAsync() return; } - await StopCore().ConfigureAwait(false); + // In case Stop was not called, we need to trigger shutdown before cleaning up resources. + // Since we're already disposing, we want to bypass any waits and just trigger shutdown with a canceled token. + // We are effectively indicating the graceful shutdown period has already elapsed and any ongoing operations should + // be aborted immediately if they participate in the cooperative cancellation. + var cancellationToken = new CancellationToken(true); - settings.Clear(); - stoppingTokenSource.Dispose(); - await serviceProviderLease.DisposeAsync().ConfigureAwait(false); + try + { + await StopCore(cancellationToken).ConfigureAwait(false); + } +#pragma warning disable PS0019 + catch (Exception) +#pragma warning restore PS0019 + { + // ignored because we're already disposing and we don't want to throw from DisposeAsync. Any exceptions from StopCore are already logged, so we can safely ignore them here. + } + finally + { + settings.Clear(); + stoppingTokenSource.Dispose(); + await serviceProviderLease.DisposeAsync().ConfigureAwait(false); + } } public Task Send(object message, SendOptions sendOptions, CancellationToken cancellationToken = default) From d6b43fa28498156fe9a288e091209b5478cb82a5 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 17 Jun 2026 19:14:27 +0000 Subject: [PATCH 09/79] Support FromKeyedServicesAttribute (#7822) Co-authored-by: Daniel Marbach --- ...BasedHandlerWithKeyedServices.approved.txt | 140 ++++++++++++++++++ ...yedServicesInAnotherNamespace.approved.txt | 126 ++++++++++++++++ ...BasedHandlerWithKeyedServices.approved.txt | 77 ++++++++++ ...yedServicesInAnotherNamespace.approved.txt | 63 ++++++++ .../Handlers/AddHandlerGeneratorTests.cs | 74 +++++++++ .../Handlers/AddHandlerInterceptorTests.cs | 72 +++++++++ .../Handlers/HandlerKnownTypes.cs | 7 +- .../Handlers/Handlers.Emitter.cs | 22 ++- .../Handlers/Handlers.Parser.cs | 79 +++++++++- 9 files changed, 648 insertions(+), 12 deletions(-) create mode 100644 src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerGeneratorTests.ConventionBasedHandlerWithKeyedServices.approved.txt create mode 100644 src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerGeneratorTests.ConventionBasedHandlerWithKeyedServicesInAnotherNamespace.approved.txt create mode 100644 src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerInterceptorTests.ConventionBasedHandlerWithKeyedServices.approved.txt create mode 100644 src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerInterceptorTests.ConventionBasedHandlerWithKeyedServicesInAnotherNamespace.approved.txt diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerGeneratorTests.ConventionBasedHandlerWithKeyedServices.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerGeneratorTests.ConventionBasedHandlerWithKeyedServices.approved.txt new file mode 100644 index 00000000000..5ac5b7d5fff --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerGeneratorTests.ConventionBasedHandlerWithKeyedServices.approved.txt @@ -0,0 +1,140 @@ +// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.Handlers.AddHandlerGenerator/HandlerRegistrations.Handlers.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace NServiceBus +{ + public static partial class ConventionBasedHandlerWithKeyedServicesHandlerRegistryExtensions + { + public sealed partial class ConventionBasedHandlerWithKeyedServicesRootRegistry + { + partial void AddAllHandlersCore() + { + AddOrderShippedHandler(); + } + + /// + /// Registers the handler with the endpoint configuration. + /// + public void AddOrderShippedHandler() + { + var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); + var messageHandlerRegistry = settings.GetOrCreate(); + var messageMetadataRegistry = settings.GetOrCreate(); + messageHandlerRegistry.AddMessageHandlerForMessage(); + messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof(global::Cmd1), []); + } + } + } +} + +[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] +[global::System.Diagnostics.StackTraceHiddenAttribute] +[global::System.Diagnostics.DebuggerNonUserCodeAttribute] +sealed file class OrderShippedHandler__Handle__Cmd1_9d5effcace7aa020 : global::NServiceBus.IHandleMessages +{ + readonly global::IMyService _ctorServiceFromCtor; + readonly global::IMyService _methodServiceFromMethod; + readonly global::IMyService _methodService2FromMethod; + readonly global::IMyService _methodService3FromMethod; + readonly global::IMyService _methodService4FromMethod; + readonly global::IMyService _methodService5FromMethod; + readonly global::IMyService _methodService6FromMethod; + readonly global::IMyService _methodService7FromMethod; + + public OrderShippedHandler__Handle__Cmd1_9d5effcace7aa020([global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("CtorKey")] global::IMyService ctorServiceFromCtorFromCtor, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("ConstKey")] global::IMyService methodServiceFromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("MethodKey")] global::IMyService methodService2FromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("NamedKey")] global::IMyService methodService3FromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("Cmd1")] global::IMyService methodService4FromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices(typeof(global::Cmd1))] global::IMyService methodService5FromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices(null)] global::IMyService methodService6FromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices((global::ServiceKey)1)] global::IMyService methodService7FromMethodFromMethod) + { + _ctorServiceFromCtor = ctorServiceFromCtorFromCtor; + _methodServiceFromMethod = methodServiceFromMethodFromMethod; + _methodService2FromMethod = methodService2FromMethodFromMethod; + _methodService3FromMethod = methodService3FromMethodFromMethod; + _methodService4FromMethod = methodService4FromMethodFromMethod; + _methodService5FromMethod = methodService5FromMethodFromMethod; + _methodService6FromMethod = methodService6FromMethodFromMethod; + _methodService7FromMethod = methodService7FromMethodFromMethod; + } + + public global::System.Threading.Tasks.Task Handle(global::Cmd1 message, global::NServiceBus.IMessageHandlerContext context) + { + var handler = new global::OrderShippedHandler(_ctorServiceFromCtor); + return handler.Handle(message, context, _methodServiceFromMethod, _methodService2FromMethod, _methodService3FromMethod, _methodService4FromMethod, _methodService5FromMethod, _methodService6FromMethod, _methodService7FromMethod, context.CancellationToken); + } +} + +// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.AddHandlerAndSagasRegistrationGenerator/HandlerRegistrations.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace NServiceBus +{ + /// + /// Provides access to handler and saga registries discovered in the ConventionBasedHandlerWithKeyedServices assembly. + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + public static partial class ConventionBasedHandlerWithKeyedServicesHandlerRegistryExtensions + { + extension (global::NServiceBus.HandlerRegistry registry) + { + /// + /// Gets the root registry for handler and saga types in the ConventionBasedHandlerWithKeyedServices assembly. + /// + /// + /// Use the returned registry to access namespace-specific registries and add-all methods for this assembly. + /// + public ConventionBasedHandlerWithKeyedServicesRootRegistry ConventionBasedHandlerWithKeyedServicesAssembly => new(registry.Configuration); + } + + /// + /// Root registry to add handlers and sagas for the entire assembly. + /// + public sealed partial class ConventionBasedHandlerWithKeyedServicesRootRegistry(global::NServiceBus.EndpointConfiguration configuration) + { + readonly global::NServiceBus.EndpointConfiguration _configuration = configuration ?? throw new System.ArgumentNullException(nameof(configuration)); + + /// + /// Registers all handlers and sagas for this namespace segment and its child namespaces. + /// + /// + /// Includes handlers in this namespace: . + /// + public void AddAll() + { + AddAllHandlers(); + AddAllSagas(); + } + + /// + /// Registers all handlers for this namespace segment and its child namespaces. + /// + /// + /// Includes handlers in this namespace: . + /// + public void AddAllHandlers() + { + AddAllHandlersCore(); + } + + /// + /// Registers all sagas for this namespace segment and its child namespaces. + /// + public void AddAllSagas() + { + AddAllSagasCore(); + } + + partial void AddAllHandlersCore(); + partial void AddAllSagasCore(); + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerGeneratorTests.ConventionBasedHandlerWithKeyedServicesInAnotherNamespace.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerGeneratorTests.ConventionBasedHandlerWithKeyedServicesInAnotherNamespace.approved.txt new file mode 100644 index 00000000000..345dc7ad05f --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerGeneratorTests.ConventionBasedHandlerWithKeyedServicesInAnotherNamespace.approved.txt @@ -0,0 +1,126 @@ +// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.Handlers.AddHandlerGenerator/HandlerRegistrations.Handlers.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace NServiceBus +{ + public static partial class ConventionBasedHandlerWithKeyedServicesInAnotherNamespaceHandlerRegistryExtensions + { + public sealed partial class ConventionBasedHandlerWithKeyedServicesInAnotherNamespaceRootRegistry + { + partial void AddAllHandlersCore() + { + AddOrderShippedHandler(); + } + + /// + /// Registers the handler with the endpoint configuration. + /// + public void AddOrderShippedHandler() + { + var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); + var messageHandlerRegistry = settings.GetOrCreate(); + var messageMetadataRegistry = settings.GetOrCreate(); + messageHandlerRegistry.AddMessageHandlerForMessage(); + messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof(global::Cmd1), []); + } + } + } +} + +[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] +[global::System.Diagnostics.StackTraceHiddenAttribute] +[global::System.Diagnostics.DebuggerNonUserCodeAttribute] +sealed file class OrderShippedHandler__Handle__Cmd1_8cbe5cd78af1d63b : global::NServiceBus.IHandleMessages +{ + readonly global::IMyService _serviceFromMethod; + + public OrderShippedHandler__Handle__Cmd1_8cbe5cd78af1d63b([global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("SecretKey")] global::IMyService serviceFromMethodFromMethod) + { + _serviceFromMethod = serviceFromMethodFromMethod; + } + + public global::System.Threading.Tasks.Task Handle(global::Cmd1 message, global::NServiceBus.IMessageHandlerContext context) + { + var handler = new global::OrderShippedHandler(); + return handler.Handle(message, context, _serviceFromMethod); + } +} + +// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.AddHandlerAndSagasRegistrationGenerator/HandlerRegistrations.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace NServiceBus +{ + /// + /// Provides access to handler and saga registries discovered in the ConventionBasedHandlerWithKeyedServicesInAnotherNamespace assembly. + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + public static partial class ConventionBasedHandlerWithKeyedServicesInAnotherNamespaceHandlerRegistryExtensions + { + extension (global::NServiceBus.HandlerRegistry registry) + { + /// + /// Gets the root registry for handler and saga types in the ConventionBasedHandlerWithKeyedServicesInAnotherNamespace assembly. + /// + /// + /// Use the returned registry to access namespace-specific registries and add-all methods for this assembly. + /// + public ConventionBasedHandlerWithKeyedServicesInAnotherNamespaceRootRegistry ConventionBasedHandlerWithKeyedServicesInAnotherNamespaceAssembly => new(registry.Configuration); + } + + /// + /// Root registry to add handlers and sagas for the entire assembly. + /// + public sealed partial class ConventionBasedHandlerWithKeyedServicesInAnotherNamespaceRootRegistry(global::NServiceBus.EndpointConfiguration configuration) + { + readonly global::NServiceBus.EndpointConfiguration _configuration = configuration ?? throw new System.ArgumentNullException(nameof(configuration)); + + /// + /// Registers all handlers and sagas for this namespace segment and its child namespaces. + /// + /// + /// Includes handlers in this namespace: . + /// + public void AddAll() + { + AddAllHandlers(); + AddAllSagas(); + } + + /// + /// Registers all handlers for this namespace segment and its child namespaces. + /// + /// + /// Includes handlers in this namespace: . + /// + public void AddAllHandlers() + { + AddAllHandlersCore(); + } + + /// + /// Registers all sagas for this namespace segment and its child namespaces. + /// + public void AddAllSagas() + { + AddAllSagasCore(); + } + + partial void AddAllHandlersCore(); + partial void AddAllSagasCore(); + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerInterceptorTests.ConventionBasedHandlerWithKeyedServices.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerInterceptorTests.ConventionBasedHandlerWithKeyedServices.approved.txt new file mode 100644 index 00000000000..fc85c750ec2 --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerInterceptorTests.ConventionBasedHandlerWithKeyedServices.approved.txt @@ -0,0 +1,77 @@ +// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.Handlers.AddHandlerInterceptor/InterceptionsOfAddHandlerMethod.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace System.Runtime.CompilerServices +{ + [global::System.Diagnostics.Conditional("DEBUG")] + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] + sealed file class InterceptsLocationAttribute : global::System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + _ = version; + _ = data; + } + } +} + +namespace NServiceBus +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + static file class InterceptionsOfAddHandlerMethod + { + extension (NServiceBus.EndpointConfiguration endpointConfiguration) + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "{PLATFORM-SPECIFIC-BASE64-DATA}")] // test.cs(10,13) + public void AddHandler_OrderShippedHandler_5f4446a2a6b04987() + { + System.ArgumentNullException.ThrowIfNull(endpointConfiguration); + var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); + var messageHandlerRegistry = settings.GetOrCreate(); + var messageMetadataRegistry = settings.GetOrCreate(); + messageHandlerRegistry.AddMessageHandlerForMessage(); + messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof(global::Cmd1), []); + } + } + } +} + +[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] +[global::System.Diagnostics.StackTraceHiddenAttribute] +[global::System.Diagnostics.DebuggerNonUserCodeAttribute] +sealed file class OrderShippedHandler__Handle__Cmd1_9d5effcace7aa020 : global::NServiceBus.IHandleMessages +{ + readonly global::IMyService _ctorServiceFromCtor; + readonly global::IMyService _methodServiceFromMethod; + readonly global::IMyService _methodService2FromMethod; + readonly global::IMyService _methodService3FromMethod; + readonly global::IMyService _methodService4FromMethod; + readonly global::IMyService _methodService5FromMethod; + readonly global::IMyService _methodService6FromMethod; + readonly global::IMyService _methodService7FromMethod; + + public OrderShippedHandler__Handle__Cmd1_9d5effcace7aa020([global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("CtorKey")] global::IMyService ctorServiceFromCtorFromCtor, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("ConstKey")] global::IMyService methodServiceFromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("MethodKey")] global::IMyService methodService2FromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("NamedKey")] global::IMyService methodService3FromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("Cmd1")] global::IMyService methodService4FromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices(typeof(global::Cmd1))] global::IMyService methodService5FromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices(null)] global::IMyService methodService6FromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices((global::ServiceKey)1)] global::IMyService methodService7FromMethodFromMethod) + { + _ctorServiceFromCtor = ctorServiceFromCtorFromCtor; + _methodServiceFromMethod = methodServiceFromMethodFromMethod; + _methodService2FromMethod = methodService2FromMethodFromMethod; + _methodService3FromMethod = methodService3FromMethodFromMethod; + _methodService4FromMethod = methodService4FromMethodFromMethod; + _methodService5FromMethod = methodService5FromMethodFromMethod; + _methodService6FromMethod = methodService6FromMethodFromMethod; + _methodService7FromMethod = methodService7FromMethodFromMethod; + } + + public global::System.Threading.Tasks.Task Handle(global::Cmd1 message, global::NServiceBus.IMessageHandlerContext context) + { + var handler = new global::OrderShippedHandler(_ctorServiceFromCtor); + return handler.Handle(message, context, _methodServiceFromMethod, _methodService2FromMethod, _methodService3FromMethod, _methodService4FromMethod, _methodService5FromMethod, _methodService6FromMethod, _methodService7FromMethod, context.CancellationToken); + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerInterceptorTests.ConventionBasedHandlerWithKeyedServicesInAnotherNamespace.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerInterceptorTests.ConventionBasedHandlerWithKeyedServicesInAnotherNamespace.approved.txt new file mode 100644 index 00000000000..4c19be2c5ac --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerInterceptorTests.ConventionBasedHandlerWithKeyedServicesInAnotherNamespace.approved.txt @@ -0,0 +1,63 @@ +// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.Handlers.AddHandlerInterceptor/InterceptionsOfAddHandlerMethod.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace System.Runtime.CompilerServices +{ + [global::System.Diagnostics.Conditional("DEBUG")] + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] + sealed file class InterceptsLocationAttribute : global::System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + _ = version; + _ = data; + } + } +} + +namespace NServiceBus +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + static file class InterceptionsOfAddHandlerMethod + { + extension (NServiceBus.EndpointConfiguration endpointConfiguration) + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "{PLATFORM-SPECIFIC-BASE64-DATA}")] // test.cs(9,13) + public void AddHandler_OrderShippedHandler_5f4446a2a6b04987() + { + System.ArgumentNullException.ThrowIfNull(endpointConfiguration); + var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); + var messageHandlerRegistry = settings.GetOrCreate(); + var messageMetadataRegistry = settings.GetOrCreate(); + messageHandlerRegistry.AddMessageHandlerForMessage(); + messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof(global::Cmd1), []); + } + } + } +} + +[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] +[global::System.Diagnostics.StackTraceHiddenAttribute] +[global::System.Diagnostics.DebuggerNonUserCodeAttribute] +sealed file class OrderShippedHandler__Handle__Cmd1_8cbe5cd78af1d63b : global::NServiceBus.IHandleMessages +{ + readonly global::IMyService _serviceFromMethod; + + public OrderShippedHandler__Handle__Cmd1_8cbe5cd78af1d63b([global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("SecretKey")] global::IMyService serviceFromMethodFromMethod) + { + _serviceFromMethod = serviceFromMethodFromMethod; + } + + public global::System.Threading.Tasks.Task Handle(global::Cmd1 message, global::NServiceBus.IMessageHandlerContext context) + { + var handler = new global::OrderShippedHandler(); + return handler.Handle(message, context, _serviceFromMethod); + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerGeneratorTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerGeneratorTests.cs index e2ef8af53f7..263ea41a1a3 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerGeneratorTests.cs +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerGeneratorTests.cs @@ -458,6 +458,80 @@ public class Cmd1 : ICommand {} .AssertRunsAreEqual(); } + [Test] + public void ConventionBasedHandlerWithKeyedServices() + { + var source = """ + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.Handlers.ConventionBasedHandlerWithKeyedServicesAssembly.AddAll(); + } + } + + [Handler] + public class OrderShippedHandler + { + public OrderShippedHandler([FromKeyedServices("CtorKey")] IMyService ctorService) {} + + public Task Handle(Cmd1 message, IMessageHandlerContext context, [FromKeyedServices(Keys.HandlerKey)] IMyService methodService, [FromKeyedServices("MethodKey")] IMyService methodService2, [FromKeyedServices(key: "NamedKey")] IMyService methodService3, [FromKeyedServices(nameof(Cmd1))] IMyService methodService4, [FromKeyedServices(typeof(Cmd1))] IMyService methodService5, [FromKeyedServices(null)] IMyService methodService6, [FromKeyedServices(ServiceKey.First)] IMyService methodService7, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + public interface IMyService {} + public class Cmd1 : ICommand {} + public static class Keys { public const string HandlerKey = "ConstKey"; } + public enum ServiceKey { None = 0, First = 1, Second = 2 } + """; + + SourceGeneratorTest.ForIncrementalGenerator() + .WithIncrementalGenerator() + .WithSource(source, "test.cs") + .Run() + .Approve() + .AssertRunsAreEqual(); + } + + [Test] + public void ConventionBasedHandlerWithKeyedServicesInAnotherNamespace() + { + var source = """ + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.Handlers.ConventionBasedHandlerWithKeyedServicesInAnotherNamespaceAssembly.AddAll(); + } + } + + [Handler] + public class OrderShippedHandler + { + public Task Handle(Cmd1 message, IMessageHandlerContext context, [FromKeyedServices(Security.KeyVault.HandlerKey)] IMyService service) => Task.CompletedTask; + } + + public interface IMyService {} + public class Cmd1 : ICommand {} + namespace Security { public static class KeyVault { public const string HandlerKey = "SecretKey"; } } + """; + + SourceGeneratorTest.ForIncrementalGenerator() + .WithIncrementalGenerator() + .WithSource(source, "test.cs") + .Run() + .Approve() + .AssertRunsAreEqual(); + } + [Test] public void ConventionBasedHandlerCtorAndMethodInjectionWithSameParameterName() { diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerInterceptorTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerInterceptorTests.cs index 6fb222ae10b..211d60bbf98 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerInterceptorTests.cs +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerInterceptorTests.cs @@ -262,6 +262,78 @@ public class Cmd1 : ICommand {} .AssertRunsAreEqual(); } + [Test] + public void ConventionBasedHandlerWithKeyedServices() + { + var source = """ + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.AddHandler(); + } + } + + [Handler] + public class OrderShippedHandler + { + public OrderShippedHandler([FromKeyedServices("CtorKey")] IMyService ctorService) {} + + public Task Handle(Cmd1 message, IMessageHandlerContext context, [FromKeyedServices(Keys.HandlerKey)] IMyService methodService, [FromKeyedServices("MethodKey")] IMyService methodService2, [FromKeyedServices(key: "NamedKey")] IMyService methodService3, [FromKeyedServices(nameof(Cmd1))] IMyService methodService4, [FromKeyedServices(typeof(Cmd1))] IMyService methodService5, [FromKeyedServices(null)] IMyService methodService6, [FromKeyedServices(ServiceKey.First)] IMyService methodService7, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + public interface IMyService {} + public class Cmd1 : ICommand {} + public static class Keys { public const string HandlerKey = "ConstKey"; } + public enum ServiceKey { None = 0, First = 1, Second = 2 } + """; + + SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .Run() + .Approve() + .AssertRunsAreEqual(); + } + + [Test] + public void ConventionBasedHandlerWithKeyedServicesInAnotherNamespace() + { + var source = """ + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.AddHandler(); + } + } + + [Handler] + public class OrderShippedHandler + { + public Task Handle(Cmd1 message, IMessageHandlerContext context, [FromKeyedServices(Security.KeyVault.HandlerKey)] IMyService service) => Task.CompletedTask; + } + + public interface IMyService {} + public class Cmd1 : ICommand {} + namespace Security { public static class KeyVault { public const string HandlerKey = "SecretKey"; } } + """; + + SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .Run() + .Approve() + .AssertRunsAreEqual(); + } + [Test] public void ConventionBasedHandlerWithMultipleConstructorsUsesMostGreedyConstructor() { diff --git a/src/NServiceBus.Core.Analyzer/Handlers/HandlerKnownTypes.cs b/src/NServiceBus.Core.Analyzer/Handlers/HandlerKnownTypes.cs index bf81129583d..f78843af37d 100644 --- a/src/NServiceBus.Core.Analyzer/Handlers/HandlerKnownTypes.cs +++ b/src/NServiceBus.Core.Analyzer/Handlers/HandlerKnownTypes.cs @@ -14,7 +14,8 @@ public sealed record HandlerKnownTypes( INamedTypeSymbol SagaBase, INamedTypeSymbol IMessageHandlerContext, INamedTypeSymbol? CancellationTokenType, - INamedTypeSymbol? ActivatorUtilitiesConstructorAttributeType) + INamedTypeSymbol? ActivatorUtilitiesConstructorAttributeType, + INamedTypeSymbol? FromKeyedServicesAttributeType) { public static bool TryGet(Compilation compilation, [NotNullWhen(true)] out HandlerKnownTypes? knownTypes) { @@ -27,6 +28,7 @@ public static bool TryGet(Compilation compilation, [NotNullWhen(true)] out Handl var iMessageHandlerContext = compilation.GetTypeByMetadataName("NServiceBus.IMessageHandlerContext"); var cancellationTokenType = compilation.GetTypeByMetadataName("System.Threading.CancellationToken"); var activatorUtilitiesConstructorAttributeType = compilation.GetTypeByMetadataName("Microsoft.Extensions.DependencyInjection.ActivatorUtilitiesConstructorAttribute"); + var fromKeyedServicesAttributeType = compilation.GetTypeByMetadataName("Microsoft.Extensions.DependencyInjection.FromKeyedServicesAttribute"); if (iHandleMessages is null || iHandleTimeouts is null || iAmStartedByMessages is null || handlerAttribute is null || sagaBase is null || iMessageHandlerContext is null) @@ -44,7 +46,8 @@ public static bool TryGet(Compilation compilation, [NotNullWhen(true)] out Handl sagaBase, iMessageHandlerContext, cancellationTokenType, - activatorUtilitiesConstructorAttributeType); + activatorUtilitiesConstructorAttributeType, + fromKeyedServicesAttributeType); return true; } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Emitter.cs b/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Emitter.cs index be487e3cc69..952165429b9 100644 --- a/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Emitter.cs +++ b/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Emitter.cs @@ -1,4 +1,5 @@ -namespace NServiceBus.Core.Analyzer.Handlers; +#nullable enable +namespace NServiceBus.Core.Analyzer.Handlers; using System.Collections.Generic; using System.Linq; @@ -76,7 +77,7 @@ static void EmitAdapterType(SourceWriter sourceWriter, ConventionBasedMethodSpec if (adapterParams.AllParams.Count > 0) { sourceWriter.WriteLine(); - var ctorArgs = string.Join(", ", adapterParams.AllParams.Select(p => $"{p.FullyQualifiedType} {p.ConstructorParameterName}")); + var ctorArgs = string.Join(", ", adapterParams.AllParams.Select(p => $"{FormatKeyedServiceAttribute(p.KeyedServiceKey)}{p.FullyQualifiedType} {p.ConstructorParameterName}")); sourceWriter.WriteLine($"public {method.AdapterName}({ctorArgs})"); sourceWriter.WriteLine("{"); sourceWriter.Indentation++; @@ -137,7 +138,7 @@ static AdapterParamSpecs BuildAdapterParams(ConventionBasedMethodSpec method) { var memberName = CreateUniqueName(p.ParameterName, "FromCtor", usedMemberNames); var constructorParameterName = CreateUniqueName(memberName, "FromCtor", usedCtorParameterNames); - all.Add(new AdapterParamSpec(memberName, constructorParameterName, p.FullyQualifiedType)); + all.Add(new AdapterParamSpec(memberName, constructorParameterName, p.FullyQualifiedType, p.KeyedServiceKey)); ctorFieldReferences.Add($"_{memberName}"); } // Method params (excluding CancellationToken — those come from context) @@ -150,7 +151,7 @@ static AdapterParamSpecs BuildAdapterParams(ConventionBasedMethodSpec method) var memberName = CreateUniqueName(p.ParameterName, "FromMethod", usedMemberNames); var constructorParameterName = CreateUniqueName(memberName, "FromMethod", usedCtorParameterNames); - all.Add(new AdapterParamSpec(memberName, constructorParameterName, p.FullyQualifiedType)); + all.Add(new AdapterParamSpec(memberName, constructorParameterName, p.FullyQualifiedType, p.KeyedServiceKey)); methodFieldReferences.Add($"_{memberName}"); } @@ -174,7 +175,18 @@ static string CreateUniqueName(string baseName, string suffix, HashSet u return candidate; } - readonly record struct AdapterParamSpec(string MemberName, string ConstructorParameterName, string FullyQualifiedType); + readonly record struct AdapterParamSpec(string MemberName, string ConstructorParameterName, string FullyQualifiedType, string? KeyedServiceKey); + + static string FormatKeyedServiceAttribute(string? keyedServiceKey) => + keyedServiceKey switch + { + // null => no [FromKeyedServices] attribute on the original parameter + null => string.Empty, + // "" => bare [FromKeyedServices] with no argument + "" => "[global::Microsoft.Extensions.DependencyInjection.FromKeyedServices] ", + // verbatim expression text from the original source + _ => $"[global::Microsoft.Extensions.DependencyInjection.FromKeyedServices({keyedServiceKey})] " + }; readonly record struct AdapterParamSpecs(List AllParams, List CtorFieldReferences, List MethodFieldReferences); } diff --git a/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Parser.cs b/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Parser.cs index fd63a0c2757..abdb9fe17b6 100644 --- a/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Parser.cs +++ b/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Parser.cs @@ -8,6 +8,7 @@ namespace NServiceBus.Core.Analyzer.Handlers; using System.Text; using System.Threading; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Utility; using BaseParser = AddHandlerAndSagasRegistrationGenerator.Parser; @@ -41,7 +42,13 @@ public enum RegistrationType public readonly record struct RegistrationSpec(RegistrationType RegistrationType, string MessageType, ImmutableEquatableArray MessageHierarchy, string HandlerType); - public readonly record struct InjectedParamSpec(string ParameterName, string FullyQualifiedType, bool IsCancellationToken); + // KeyedServiceKey carries the [FromKeyedServices(...)] key folded to its compile-time constant + // value, rendered as a valid C# expression for re-application in the generated adapter ctor. + // Sentinel semantics: + // - null : no [FromKeyedServices] attribute present + // - "" (empty) : attribute present with no argument (bare [FromKeyedServices]) + // - any other value: the folded key (e.g. "\"MyKey\"", "null", "typeof(global::T)", "(global::E)1") + public readonly record struct InjectedParamSpec(string ParameterName, string FullyQualifiedType, bool IsCancellationToken, string? KeyedServiceKey); public readonly record struct ConventionBasedMethodSpec( string MessageType, @@ -146,7 +153,7 @@ static List ParseConventionBasedMethods( // Ctor params of the handler type (for instance methods) var selectedConstructor = SelectConstructor(handlerType, knownTypes.ActivatorUtilitiesConstructorAttributeType); - var ctorParams = GetCtorParams(selectedConstructor, knownTypes.CancellationTokenType); + var ctorParams = GetCtorParams(selectedConstructor, knownTypes.CancellationTokenType, knownTypes.FromKeyedServicesAttributeType); foreach (var method in GetHandleMethods(handlerType, includeInheritedMethods)) { @@ -174,10 +181,12 @@ static List ParseConventionBasedMethods( var param = method.Parameters[i]; bool isCt = knownTypes.CancellationTokenType is not null && SymbolEqualityComparer.Default.Equals(param.Type, knownTypes.CancellationTokenType); + var keyedServiceKey = TryGetKeyedServiceKey(param, knownTypes.FromKeyedServicesAttributeType); methodParams.Add(new InjectedParamSpec( param.Name, param.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - isCt)); + isCt, + keyedServiceKey)); } var hierarchy = new ImmutableEquatableArray( @@ -234,7 +243,8 @@ readonly record struct ParameterSignatureKey( static ImmutableEquatableArray GetCtorParams( IMethodSymbol? constructor, - INamedTypeSymbol? cancellationTokenType) + INamedTypeSymbol? cancellationTokenType, + INamedTypeSymbol? fromKeyedServicesAttributeType) { if (constructor is null || constructor.Parameters.Length == 0) { @@ -247,12 +257,71 @@ static ImmutableEquatableArray GetCtorParams( var p = constructor.Parameters[i]; bool isCt = cancellationTokenType is not null && SymbolEqualityComparer.Default.Equals(p.Type, cancellationTokenType); - specs[i] = new InjectedParamSpec(p.Name, p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), isCt); + var keyedServiceKey = TryGetKeyedServiceKey(p, fromKeyedServicesAttributeType); + specs[i] = new InjectedParamSpec(p.Name, p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), isCt, keyedServiceKey); } return specs.ToImmutableEquatableArray(); } + // Extracts the [FromKeyedServices(...)] key for a parameter, folded to its compile-time + // constant value. Folding guarantees the value resolves in the generated adapter, + // which lives at global scope. Covers every valid attribute-argument + // form uniformly: string literals, const fields, nameof, typeof, named arguments, and null. + // Sentinel: null = no FromKeyedServices attribute; "" (empty) = bare attribute with no key; + // otherwise the folded key rendered as a valid C# expression. + static string? TryGetKeyedServiceKey(IParameterSymbol param, INamedTypeSymbol? fromKeyedServicesAttributeType) + { + if (fromKeyedServicesAttributeType is null) + { + return null; + } + + foreach (var attribute in param.GetAttributes()) + { + if (attribute.AttributeClass is null) + { + continue; + } + + if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, fromKeyedServicesAttributeType) && + !SymbolEqualityComparer.Default.Equals(attribute.AttributeClass.OriginalDefinition, fromKeyedServicesAttributeType)) + { + continue; + } + + return FormatKeyedServiceKey(attribute); + } + + return null; + } + + static string FormatKeyedServiceKey(AttributeData attribute) + { + if (attribute.ConstructorArguments.Length == 0) + { + // Bare attribute with no key argument. + return string.Empty; + } + + var arg = attribute.ConstructorArguments[0]; + + // typeof(T) -> typeof(global::T) so it resolves at the adapter's global scope. + if (arg is { Kind: TypedConstantKind.Type, Value: ITypeSymbol typeSymbol }) + { + return $"typeof({typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)})"; + } + + // Enum constant -> cast the underlying integral value to the fully-qualified enum type. + if (arg is { Kind: TypedConstantKind.Enum, Type: INamedTypeSymbol enumType }) + { + return $"({enumType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}){arg.Value}"; + } + + // Primitives (string/int/bool/...) and null -> C# literal form via the shared formatter. + return SymbolDisplay.FormatPrimitive(arg.Value, quoteStrings: true, useHexadecimalNumbers: false) ?? "null"; + } + static IMethodSymbol? SelectConstructor(INamedTypeSymbol handlerType, INamedTypeSymbol? activatorUtilitiesConstructorAttributeType) { var candidates = handlerType.Constructors From 2ddfd86acfe65d0749cce105101642b4c13ca5c1 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 18 Jun 2026 09:45:07 +0000 Subject: [PATCH 10/79] Optimize ContextBag with InlineArray-based slot storage (#7823) * Enhance ContextBag with inline storage and retrieval optimizations - Implemented inline storage for up to 8 entries to improve performance. - Added methods for setting, getting, and removing inline entries. - Updated merge logic to handle inline entries and stash more efficiently. - Introduced tests to validate inline entry functionality and behavior. * Update struct conventions tests to reflect immutability rules for ContextBag+Slot * Length constant * Comments Co-authored-by: David Boike * Length constant for inline array --------- Co-authored-by: Daniel Marbach Co-authored-by: David Boike --- ...ichDontFollowStructGuidelines.approved.txt | 11 +- src/NServiceBus.Core.Tests/ContextBagTests.cs | 503 +++++++++++++++++- .../StructConventionsTests.cs | 6 + .../Extensibility/ContextBag.cs | 165 +++++- 4 files changed, 675 insertions(+), 10 deletions(-) diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/StructConventionsTests.ApproveStructsWhichDontFollowStructGuidelines.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/StructConventionsTests.ApproveStructsWhichDontFollowStructGuidelines.approved.txt index 631d90e5f42..1d23aafe370 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/StructConventionsTests.ApproveStructsWhichDontFollowStructGuidelines.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/StructConventionsTests.ApproveStructsWhichDontFollowStructGuidelines.approved.txt @@ -1,4 +1,4 @@ --------------------------------------------------- REMEMBER -------------------------------------------------- +-------------------------------------------------- REMEMBER -------------------------------------------------- CONSIDER defining a struct instead of a class if instances of the type are small and commonly short-lived or are commonly embedded in other objects. AVOID defining a struct unless the type has all of the following characteristics: @@ -10,3 +10,12 @@ AVOID defining a struct unless the type has all of the following characteristics In all other cases, you should define your types as classes. -------------------------------------------------- REMEMBER -------------------------------------------------- +NServiceBus.Extensibility.ContextBag+Slot violates the following rules: + - The following fields are public, so the type is not immutable: + - Field Key of type System.String is public. + - Field Value of type System.Object is public. + - The following fields are reference types, which are potentially mutable: + - Field Key of type System.String is a reference type. + - Field Value of type System.Object is a reference type. + - The size cannot be determined because there are fields that are reference types. + diff --git a/src/NServiceBus.Core.Tests/ContextBagTests.cs b/src/NServiceBus.Core.Tests/ContextBagTests.cs index d41b7b67413..73072b3b201 100644 --- a/src/NServiceBus.Core.Tests/ContextBagTests.cs +++ b/src/NServiceBus.Core.Tests/ContextBagTests.cs @@ -1,9 +1,10 @@ -#nullable enable +#nullable enable namespace NServiceBus.Core.Tests; using Extensibility; using NUnit.Framework; +using System.Collections.Generic; [TestFixture] public class ContextBagTests @@ -62,4 +63,504 @@ public void SetOnRoot_should_set_value_on_root_context() Assert.That(fork.Get(key), Is.EqualTo(42), "stored value should be visible to a forked context"); } } + + [Test] + public void Should_set_and_get_a_single_inline_entry() + { + var bag = new ContextBag(); + + bag.Set("key1", "value1"); + + Assert.That(bag.Get("key1"), Is.EqualTo("value1")); + } + + [Test] + public void Should_set_and_get_four_inline_entries() + { + var bag = new ContextBag(); + + for (var i = 1; i <= 4; i++) + { + bag.Set($"key{i}", $"value{i}"); + } + + using (Assert.EnterMultipleScope()) + { + for (var i = 1; i <= 4; i++) + { + Assert.That(bag.Get($"key{i}"), Is.EqualTo($"value{i}")); + } + } + } + + [Test] + public void Should_set_and_get_eight_inline_entries() + { + var bag = new ContextBag(); + + for (var i = 1; i <= 8; i++) + { + bag.Set($"key{i}", $"value{i}"); + } + + using (Assert.EnterMultipleScope()) + { + for (var i = 1; i <= 8; i++) + { + Assert.That(bag.Get($"key{i}"), Is.EqualTo($"value{i}")); + } + } + } + + [Test] + public void Should_set_and_get_more_than_eight_entries() + { + var bag = new ContextBag(); + + for (var i = 1; i <= 12; i++) + { + bag.Set($"key{i}", $"value{i}"); + } + + using (Assert.EnterMultipleScope()) + { + for (var i = 1; i <= 12; i++) + { + Assert.That(bag.Get($"key{i}"), Is.EqualTo($"value{i}")); + } + } + } + + [Test] + public void Should_update_an_existing_inline_key() + { + var bag = new ContextBag(); + + bag.Set("key1", "original"); + bag.Set("key1", "updated"); + + Assert.That(bag.Get("key1"), Is.EqualTo("updated")); + } + + [Test] + public void Should_update_an_existing_stash_key() + { + var bag = new ContextBag(); + + for (var i = 1; i <= 9; i++) + { + bag.Set($"key{i}", $"value{i}"); + } + + bag.Set("key9", "updated"); + + Assert.That(bag.Get("key9"), Is.EqualTo("updated")); + } + + [Test] + public void Should_remove_the_first_inline_key() + { + var bag = new ContextBag(); + + bag.Set("key1", "value1"); + bag.Set("key2", "value2"); + bag.Set("key3", "value3"); + + bag.Remove("key1"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(() => bag.Get("key1"), Throws.TypeOf()); + Assert.That(bag.Get("key2"), Is.EqualTo("value2")); + Assert.That(bag.Get("key3"), Is.EqualTo("value3")); + } + } + + [Test] + public void Should_remove_a_middle_inline_key() + { + var bag = new ContextBag(); + + bag.Set("key1", "value1"); + bag.Set("key2", "value2"); + bag.Set("key3", "value3"); + + bag.Remove("key2"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(() => bag.Get("key2"), Throws.TypeOf()); + Assert.That(bag.Get("key1"), Is.EqualTo("value1")); + Assert.That(bag.Get("key3"), Is.EqualTo("value3")); + } + } + + [Test] + public void Should_remove_the_last_inline_key() + { + var bag = new ContextBag(); + + bag.Set("key1", "value1"); + bag.Set("key2", "value2"); + bag.Set("key3", "value3"); + + bag.Remove("key3"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(() => bag.Get("key3"), Throws.TypeOf()); + Assert.That(bag.Get("key1"), Is.EqualTo("value1")); + Assert.That(bag.Get("key2"), Is.EqualTo("value2")); + } + } + + [Test] + public void Should_remove_a_stash_key() + { + var bag = new ContextBag(); + + for (var i = 1; i <= 9; i++) + { + bag.Set($"key{i}", $"value{i}"); + } + + bag.Remove("key9"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(() => bag.Get("key9"), Throws.TypeOf()); + for (var i = 1; i <= 8; i++) + { + Assert.That(bag.Get($"key{i}"), Is.EqualTo($"value{i}")); + } + } + } + + [Test] + public void Should_keep_remaining_inline_keys_after_swap_removal() + { + var bag = new ContextBag(); + + bag.Set("key1", "value1"); + bag.Set("key2", "value2"); + bag.Set("key3", "value3"); + bag.Set("key4", "value4"); + + bag.Remove("key1"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(() => bag.Get("key1"), Throws.TypeOf()); + Assert.That(bag.Get("key2"), Is.EqualTo("value2")); + Assert.That(bag.Get("key3"), Is.EqualTo("value3")); + Assert.That(bag.Get("key4"), Is.EqualTo("value4")); + } + + bag.Remove("key2"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(() => bag.Get("key1"), Throws.TypeOf()); + Assert.That(() => bag.Get("key2"), Throws.TypeOf()); + Assert.That(bag.Get("key3"), Is.EqualTo("value3")); + Assert.That(bag.Get("key4"), Is.EqualTo("value4")); + } + } + + [Test] + public void Should_clear_the_bag() + { + var bag = new ContextBag(); + + for (var i = 1; i <= 12; i++) + { + bag.Set($"key{i}", $"value{i}"); + } + + bag.Clear(); + + using (Assert.EnterMultipleScope()) + { + for (var i = 1; i <= 12; i++) + { + Assert.That(() => bag.Get($"key{i}"), Throws.TypeOf()); + } + } + } + + [Test] + public void Should_throw_when_getting_a_missing_key() + { + var bag = new ContextBag(); + + Assert.That(() => bag.Get("nonexistent"), Throws.TypeOf()); + } + + [Test] + public void Should_return_false_when_try_getting_a_missing_key() + { + var bag = new ContextBag(); + + var found = bag.TryGet("nonexistent", out var result); + + using (Assert.EnterMultipleScope()) + { + Assert.That(found, Is.False); + Assert.That(result, Is.Null); + } + } + + [Test] + public void Should_find_a_key_in_the_parent_bag() + { + var root = new ContextBag(); + var child = new ContextBag(root); + + root.Set("parentKey", "parentValue"); + + Assert.That(child.Get("parentKey"), Is.EqualTo("parentValue")); + } + + [Test] + public void Should_fallback_to_parent_after_removing_local_override() + { + var root = new ContextBag(); + var child = new ContextBag(root); + + root.Set("sharedKey", "rootValue"); + child.Set("sharedKey", "childValue"); + + child.Remove("sharedKey"); + + Assert.That(child.Get("sharedKey"), Is.EqualTo("rootValue")); + } + + [Test] + public void Should_allow_removing_a_nonexistent_key() + { + var bag = new ContextBag(); + + Assert.That(() => bag.Remove("nonexistent"), Throws.Nothing); + } + + [Test] + public void Should_keep_inline_entries_reachable_after_overflowing_to_stash() + { + var bag = new ContextBag(); + + for (var i = 1; i <= 8; i++) + { + bag.Set($"key{i}", $"inline{i}"); + } + + bag.Set("key9", "stashValue"); + + using (Assert.EnterMultipleScope()) + { + for (var i = 1; i <= 8; i++) + { + Assert.That(bag.Get($"key{i}"), Is.EqualTo($"inline{i}")); + } + + Assert.That(bag.Get("key9"), Is.EqualTo("stashValue")); + } + } + + [Test] + public void Should_reuse_inline_slots_after_removing_a_key() + { + var bag = new ContextBag(); + + bag.Set("key1", "value1"); + bag.Set("key2", "value2"); + bag.Set("key3", "value3"); + + bag.Remove("key2"); + + bag.Set("key4", "value4"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(bag.Get("key1"), Is.EqualTo("value1")); + Assert.That(bag.Get("key3"), Is.EqualTo("value3")); + Assert.That(bag.Get("key4"), Is.EqualTo("value4")); + } + } + + [Test] + public void Should_merge_inline_entries_into_empty_target() + { + var target = new ContextBag(); + var source = new ContextBag(); + + source.Set("key1", "value1"); + source.Set("key2", "value2"); + source.Set("key3", "value3"); + + target.Merge(source); + + using (Assert.EnterMultipleScope()) + { + Assert.That(target.Get("key1"), Is.EqualTo("value1")); + Assert.That(target.Get("key2"), Is.EqualTo("value2")); + Assert.That(target.Get("key3"), Is.EqualTo("value3")); + } + } + + [Test] + public void Should_merge_inline_entries_into_non_empty_target() + { + var target = new ContextBag(); + var source = new ContextBag(); + + target.Set("existing", "existingValue"); + source.Set("key1", "value1"); + + target.Merge(source); + + using (Assert.EnterMultipleScope()) + { + Assert.That(target.Get("existing"), Is.EqualTo("existingValue")); + Assert.That(target.Get("key1"), Is.EqualTo("value1")); + } + } + + [Test] + public void Should_merge_overwrites_existing_key() + { + var target = new ContextBag(); + var source = new ContextBag(); + + target.Set("sharedKey", "originalValue"); + source.Set("sharedKey", "newValue"); + + target.Merge(source); + + Assert.That(target.Get("sharedKey"), Is.EqualTo("newValue")); + } + + [Test] + public void Should_merge_stash_entries_into_empty_target() + { + var target = new ContextBag(); + var source = new ContextBag(); + + for (var i = 1; i <= 9; i++) + { + source.Set($"key{i}", $"value{i}"); + } + + target.Merge(source); + + using (Assert.EnterMultipleScope()) + { + for (var i = 1; i <= 9; i++) + { + Assert.That(target.Get($"key{i}"), Is.EqualTo($"value{i}")); + } + } + } + + [Test] + public void Should_merge_stash_entries_into_target_with_full_inline() + { + var target = new ContextBag(); + var source = new ContextBag(); + + for (var i = 1; i <= 8; i++) + { + target.Set($"existing{i}", $"existingValue{i}"); + } + + for (var i = 1; i <= 9; i++) + { + source.Set($"key{i}", $"value{i}"); + } + + target.Merge(source); + + using (Assert.EnterMultipleScope()) + { + for (var i = 1; i <= 8; i++) + { + Assert.That(target.Get($"existing{i}"), Is.EqualTo($"existingValue{i}")); + } + + for (var i = 1; i <= 9; i++) + { + Assert.That(target.Get($"key{i}"), Is.EqualTo($"value{i}")); + } + } + } + + [Test] + public void Should_merge_stash_entries_fill_remaining_inline_slots() + { + var target = new ContextBag(); + var source = new ContextBag(); + + target.Set("existing", "existingValue"); + + for (var i = 1; i <= 9; i++) + { + source.Set($"key{i}", $"value{i}"); + } + + target.Merge(source); + + using (Assert.EnterMultipleScope()) + { + Assert.That(target.Get("existing"), Is.EqualTo("existingValue")); + + for (var i = 1; i <= 9; i++) + { + Assert.That(target.Get($"key{i}"), Is.EqualTo($"value{i}")); + } + } + } + + [Test] + public void Should_merge_when_both_have_stash_entries() + { + var target = new ContextBag(); + var source = new ContextBag(); + + for (var i = 1; i <= 9; i++) + { + target.Set($"existing{i}", $"existingValue{i}"); + } + + for (var i = 1; i <= 9; i++) + { + source.Set($"key{i}", $"value{i}"); + } + + target.Merge(source); + + using (Assert.EnterMultipleScope()) + { + for (var i = 1; i <= 9; i++) + { + Assert.That(target.Get($"existing{i}"), Is.EqualTo($"existingValue{i}")); + } + + for (var i = 1; i <= 9; i++) + { + Assert.That(target.Get($"key{i}"), Is.EqualTo($"value{i}")); + } + } + } + + [Test] + public void Should_merge_from_empty_source() + { + var target = new ContextBag(); + var source = new ContextBag(); + + target.Set("key1", "value1"); + + target.Merge(source); + + Assert.That(target.Get("key1"), Is.EqualTo("value1")); + } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/StructConventionsTests.cs b/src/NServiceBus.Core.Tests/StructConventionsTests.cs index 82b12cfa338..ce73bc4a131 100644 --- a/src/NServiceBus.Core.Tests/StructConventionsTests.cs +++ b/src/NServiceBus.Core.Tests/StructConventionsTests.cs @@ -39,6 +39,12 @@ public void ApproveStructsWhichDontFollowStructGuidelines() continue; } + // InlineArray types are compiler-generated and don't follow normal struct conventions + if (type.GetCustomAttribute() != null) + { + continue; + } + // readonly structs can probably be ignored if (type.GetCustomAttribute() != null) { diff --git a/src/NServiceBus.Core/Extensibility/ContextBag.cs b/src/NServiceBus.Core/Extensibility/ContextBag.cs index 968e9b4a87d..2a43718a02b 100644 --- a/src/NServiceBus.Core/Extensibility/ContextBag.cs +++ b/src/NServiceBus.Core/Extensibility/ContextBag.cs @@ -5,6 +5,7 @@ namespace NServiceBus.Extensibility; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Pipeline; @@ -48,6 +49,18 @@ public ContextBag(ContextBag? parentBag = null) public bool TryGet(string key, [NotNullWhen(true)] out T? result) { ArgumentException.ThrowIfNullOrWhiteSpace(key); + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + result = (T)slot.Value!; + return true; + } + } + if (stash?.TryGetValue(key, out var value) == true) { result = (T)value; @@ -116,6 +129,29 @@ public T Get(string key) public void Remove(string key) { ArgumentException.ThrowIfNullOrWhiteSpace(key); + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + // The number of slots in use decreases by one + count--; + + // If we're abandoning the last slot, in-use slots are still contiguous (order doesn't matter) + if (i != count) + { + // If removing from the begining/middle, swap the last slot into the hole + slots[i] = slots[count]; + } + + // Then clear out the (previously) last slot + slots[count] = default; + return; + } + } + _ = stash?.Remove(key); } @@ -127,7 +163,32 @@ public void Set(string key, T t) ArgumentException.ThrowIfNullOrWhiteSpace(key); ArgumentNullException.ThrowIfNull(t); - GetOrCreateStash()[key] = t; + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + slot.Value = t; + return; + } + } + + if (count < InlineArrayLength) + { + slots[count] = new Slot { Key = key, Value = t }; + count++; + return; + } + + var s = stash; + if (s is null) + { + s = []; + stash = s; + } + + s[key] = t; } /// @@ -151,23 +212,90 @@ internal void SetOnRoot(string key, T t) /// The source context. internal void Merge(ContextBag context) { - if (context.stash == null) + if (count == 0 && stash is null && context.stash is null) + { + var sourceCount = context.count; + + for (int i = 0; i < sourceCount; i++) + { + slots[i] = context.slots[i]; + } + + count = sourceCount; + return; + } + + for (int i = 0; i < context.count; i++) + { + ref var sourceSlot = ref context.slots[i]; + SetInlineOrStash(sourceSlot.Key!, sourceSlot.Value!); + } + + var sourceStash = context.stash; + if (sourceStash is null) + { + return; + } + + if (count == InlineArrayLength) { + var targetStash = stash ??= []; + + foreach (var kvp in sourceStash) + { + targetStash[kvp.Key] = kvp.Value; + } + return; } - var targetStash = GetOrCreateStash(); - foreach (var kvp in context.stash) + foreach (var kvp in sourceStash) + { + SetInlineOrStash(kvp.Key, kvp.Value); + } + + return; + + void SetInlineOrStash(string key, object value) { - targetStash[kvp.Key] = kvp.Value; + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + slot.Value = value; + return; + } + } + + if (count < InlineArrayLength) + { + slots[count] = new Slot + { + Key = key, + Value = value + }; + count++; + return; + } + + (stash ??= [])[key] = value; } } - Dictionary GetOrCreateStash() + /// + /// Removes all entries from the context. + /// + internal void Clear() { - stash ??= []; + for (int i = 0; i < count; i++) + { + slots[i] = default; + } - return stash; + count = 0; + stash?.Clear(); } internal Func Invoker { get; set; } @@ -176,5 +304,26 @@ Dictionary GetOrCreateStash() private protected ContextBag root; + SlotArray slots; + int count; Dictionary? stash; + + // The number of slots that can be stored inline in the struct before falling back to a dictionary. + // This number was carefully chosen after benchmarking and analyzing typical usage patterns of the + // context bag in the main and recoverability pipelines. + // The goal was to minimize allocations while keeping lookups efficient for the most common cases. + // Do not blindly change this. + const int InlineArrayLength = 8; + + struct Slot + { + public string? Key; + public object? Value; + } + + [InlineArray(InlineArrayLength)] + struct SlotArray + { + Slot _element0; + } } \ No newline at end of file From 5f3aac98fd6656845fc8a27aacfefa7804fad89f Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 18 Jun 2026 11:13:21 +0000 Subject: [PATCH 11/79] Trimming safe mapping by using guarding against IsDynamicCodeSupported (#7821) * Trimming safe mapping by using guarding against IsDynamicCodeSupported Also adds trimming annotations up to the entry points for this path * Move the defaults to the other defaults * Enhance TrimmingSafeMessageMapper to throw NotSupportedException for interface and abstract type mappings; add tests for concrete event publishing with trimming-safe mapper * Fix usings * Proper xml doc * Approve new test for downstreams * Small twist to the doc --------- Co-authored-by: Daniel Marbach --- ...val.ApproveShippedSourceFiles.approved.txt | 1 + ...rface_message_with_trimming_safe_mapper.cs | 95 +++++++++++ ...oncrete_event_with_trimming_safe_mapper.cs | 154 ++++++++++++++++++ ...IApprovals.ApproveNServiceBus.approved.txt | 60 ++++--- ...s.ApproveTrimmabilityWarnings.approved.txt | 15 -- .../When_mapping_with_trimming_safe_mapper.cs | 125 ++++++++++++++ src/NServiceBus.Core/EndpointConfiguration.cs | 11 ++ src/NServiceBus.Core/EndpointCreator.cs | 6 +- src/NServiceBus.Core/IMessageCreator.cs | 9 +- .../IMessageProcessingContext.cs | 3 +- src/NServiceBus.Core/IMessageSession.cs | 5 +- src/NServiceBus.Core/IPipelineContext.cs | 5 +- .../Reflection/ConcreteProxyCreator.cs | 5 + .../MessageMapper/Reflection/MessageMapper.cs | 11 +- .../TrimmingSafeMessageMapper.cs | 90 ++++++++++ .../MessageProcessingContextExtensions.cs | 3 +- src/NServiceBus.Core/MessageSession.cs | 4 +- .../MessageSessionExtensions.cs | 11 +- .../Pipeline/Incoming/IncomingContext.cs | 7 +- .../Pipeline/Outgoing/OutgoingContext.cs | 5 +- .../PipelineContextExtensions.cs | 11 +- .../Unicast/MessageOperations.cs | 7 +- .../Unicast/RunningEndpointInstance.cs | 5 +- 23 files changed, 571 insertions(+), 77 deletions(-) create mode 100644 src/NServiceBus.AcceptanceTests/Core/Recoverability/When_deserializing_interface_message_with_trimming_safe_mapper.cs create mode 100644 src/NServiceBus.AcceptanceTests/Routing/When_publishing_a_concrete_event_with_trimming_safe_mapper.cs create mode 100644 src/NServiceBus.Core.Tests/MessageMapper/When_mapping_with_trimming_safe_mapper.cs create mode 100644 src/NServiceBus.Core/MessageInterfaces/MessageMapper/TrimmingSafeMessageMapper.cs diff --git a/src/NServiceBus.AcceptanceTests/ApprovalFiles/AcceptanceTestsShippedSourceFilesApproval.ApproveShippedSourceFiles.approved.txt b/src/NServiceBus.AcceptanceTests/ApprovalFiles/AcceptanceTestsShippedSourceFilesApproval.ApproveShippedSourceFiles.approved.txt index 44a4fa4a2dd..aa3251ab29b 100644 --- a/src/NServiceBus.AcceptanceTests/ApprovalFiles/AcceptanceTestsShippedSourceFilesApproval.ApproveShippedSourceFiles.approved.txt +++ b/src/NServiceBus.AcceptanceTests/ApprovalFiles/AcceptanceTestsShippedSourceFilesApproval.ApproveShippedSourceFiles.approved.txt @@ -89,6 +89,7 @@ Routing/When_making_endpoint_uniquely_addressable.cs Routing/When_multiple_mappings_exists.cs Routing/When_overriding_local_address.cs Routing/When_publishing.cs +Routing/When_publishing_a_concrete_event_with_trimming_safe_mapper.cs Routing/When_publishing_an_event_implementing_two_unrelated_interfaces.cs Routing/When_publishing_an_interface.cs Routing/When_publishing_an_interface_with_unobtrusive.cs diff --git a/src/NServiceBus.AcceptanceTests/Core/Recoverability/When_deserializing_interface_message_with_trimming_safe_mapper.cs b/src/NServiceBus.AcceptanceTests/Core/Recoverability/When_deserializing_interface_message_with_trimming_safe_mapper.cs new file mode 100644 index 00000000000..75e1f25a241 --- /dev/null +++ b/src/NServiceBus.AcceptanceTests/Core/Recoverability/When_deserializing_interface_message_with_trimming_safe_mapper.cs @@ -0,0 +1,95 @@ +namespace NServiceBus.AcceptanceTests.Core.Recoverability; + +using System; +using System.Threading.Tasks; +using AcceptanceTesting; +using AcceptanceTesting.Support; +using Configuration.AdvancedExtensibility; +using EndpointTemplates; +using Features; +using MessageInterfaces.MessageMapper.Reflection; +using NServiceBus.MessageInterfaces; +using NUnit.Framework; +using Conventions = AcceptanceTesting.Customization.Conventions; + +public class When_deserializing_interface_message_with_trimming_safe_mapper : NServiceBusAcceptanceTest +{ + [Test] + public void Should_move_to_error_queue_with_actionable_exception() + { + Context context = null; + + var exception = Assert.ThrowsAsync(async () => + { + await Scenario.Define(ctx => context = ctx) + .WithEndpoint(b => + b.When(c => c.Subscribed, (session, ctx) => session.Publish())) + .WithEndpoint(b => b.When(async (session, ctx) => + { + await session.Subscribe(); + if (ctx.HasNativePubSubSupport) + { + ctx.Subscribed = true; + } + })) + .Run(); + }); + + using (Assert.EnterMultipleScope()) + { + Assert.That(exception!.FailedMessage.Exception, Is.TypeOf()); + Assert.That(exception.FailedMessage.Exception.InnerException, Is.TypeOf()); + Assert.That(exception.FailedMessage.Exception.InnerException!.Message, Does.Contain("dynamic code")); + Assert.That(exception.ScenarioContext.FailedMessages, Has.Count.EqualTo(1)); + Assert.That(context!.HandlerInvoked, Is.False); + } + } + + public class Context : ScenarioContext + { + public bool Subscribed { get; set; } + public bool HandlerInvoked { get; set; } + } + + public class Publisher : EndpointConfigurationBuilder + { + public Publisher() => + EndpointSetup(c => + { + c.UseSerialization(); + c.OnEndpointSubscribed((s, context) => + { + if (s.SubscriberEndpoint.Contains(Conventions.EndpointNamingConvention(typeof(Subscriber)))) + { + context.Subscribed = true; + } + }); + }, + metadata => metadata.RegisterSelfAsPublisherFor(this)); + } + + public class Subscriber : EndpointConfigurationBuilder + { + public Subscriber() => + EndpointSetup(c => + { + c.UseSerialization(); + c.DisableFeature(); + // Force the trimming-safe mapper, the path NativeAOT/trimmed endpoints take. + c.GetSettings().Set(new TrimmingSafeMessageMapper()); + }, + metadata => metadata.RegisterPublisherFor()); + + [Handler] + public class MyHandler(Context testContext) : IHandleMessages + { + public Task Handle(IMyEvent @event, IMessageHandlerContext context) + { + testContext.HandlerInvoked = true; + return Task.CompletedTask; + } + } + } + + public interface IMyEvent : IEvent; +} \ No newline at end of file diff --git a/src/NServiceBus.AcceptanceTests/Routing/When_publishing_a_concrete_event_with_trimming_safe_mapper.cs b/src/NServiceBus.AcceptanceTests/Routing/When_publishing_a_concrete_event_with_trimming_safe_mapper.cs new file mode 100644 index 00000000000..01f6b7abf9a --- /dev/null +++ b/src/NServiceBus.AcceptanceTests/Routing/When_publishing_a_concrete_event_with_trimming_safe_mapper.cs @@ -0,0 +1,154 @@ +namespace NServiceBus.AcceptanceTests.Routing; + +using System; +using System.Threading; +using System.Threading.Tasks; +using AcceptanceTesting; +using Configuration.AdvancedExtensibility; +using EndpointTemplates; +using Features; +using MessageInterfaces.MessageMapper.Reflection; +using NServiceBus.MessageInterfaces; +using NUnit.Framework; +using Conventions = AcceptanceTesting.Customization.Conventions; + +public class When_publishing_a_concrete_event_with_trimming_safe_mapper : NServiceBusAcceptanceTest +{ + [Test, CancelAfter(20_000)] + public async Task Should_dispatch_to_interface_handlers_without_generating_a_proxy(CancellationToken cancellationToken = default) + { + var context = await Scenario.Define(c => c.Id = Guid.NewGuid()) + .WithEndpoint(b => + b.When(c => c is { EventASubscribed: true, EventBSubscribed: true }, (session, ctx) => + { + var message = new CompositeEvent + { + ContextId = ctx.Id + }; + return session.Publish(message); + })) + .WithEndpoint(b => b.When(async (session, ctx) => + { + await session.Subscribe(); + await session.Subscribe(); + + if (ctx.HasNativePubSubSupport) + { + ctx.EventASubscribed = true; + ctx.EventBSubscribed = true; + } + })) + .Run(cancellationToken); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.GotEventA, Is.True); + Assert.That(context.GotEventB, Is.True); + Assert.That(context.HandlerReceivedConcreteInstance, Is.True); + } + } + + public class Context : ScenarioContext + { + public Guid Id { get; set; } + public bool EventASubscribed { get; set; } + public bool EventBSubscribed { get; set; } + public bool GotEventA { get; set; } + public bool GotEventB { get; set; } + public bool HandlerReceivedConcreteInstance { get; set; } + + public void MaybeCompleted() => MarkAsCompleted(GotEventA, GotEventB); + } + + public class Publisher : EndpointConfigurationBuilder + { + public Publisher() => + EndpointSetup(c => + { + // Force the trimming-safe mapper even though the test runs on a JIT runtime, + // to prove the path that NativeAOT/trimmed endpoints take. + c.GetSettings().Set(new TrimmingSafeMessageMapper()); + c.OnEndpointSubscribed((s, context) => + { + if (s.SubscriberEndpoint.Contains(Conventions.EndpointNamingConvention(typeof(Subscriber)))) + { + if (s.MessageType == typeof(IEventA).AssemblyQualifiedName) + { + context.EventASubscribed = true; + } + if (s.MessageType == typeof(IEventB).AssemblyQualifiedName) + { + context.EventBSubscribed = true; + } + } + }); + }, + metadata => metadata.RegisterSelfAsPublisherFor(this)); + } + + public class Subscriber : EndpointConfigurationBuilder + { + public Subscriber() => + EndpointSetup(c => + { + c.DisableFeature(); + c.GetSettings().Set(new TrimmingSafeMessageMapper()); + }, + metadata => + { + metadata.RegisterPublisherFor(); + metadata.RegisterPublisherFor(); + }); + + [Handler] + public class EventAHandler(Context testContext) : IHandleMessages + { + public Task Handle(IEventA @event, IMessageHandlerContext context) + { + if (@event.ContextId != testContext.Id) + { + return Task.CompletedTask; + } + testContext.GotEventA = true; + testContext.HandlerReceivedConcreteInstance = @event is CompositeEvent; + testContext.MaybeCompleted(); + + return Task.CompletedTask; + } + } + + [Handler] + public class EventBHandler(Context testContext) : IHandleMessages + { + public Task Handle(IEventB @event, IMessageHandlerContext context) + { + if (@event.ContextId != testContext.Id) + { + return Task.CompletedTask; + } + + testContext.GotEventB = true; + testContext.MaybeCompleted(); + + return Task.CompletedTask; + } + } + } + + public class CompositeEvent : IEventA, IEventB + { + public Guid ContextId { get; set; } + public string StringProperty { get; set; } + public int IntProperty { get; set; } + } + + public interface IEventA : IEvent + { + Guid ContextId { get; set; } + } + + public interface IEventB : IEvent + { + Guid ContextId { get; set; } + } +} diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index f269fab38a3..7b94f65396c 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -515,9 +515,9 @@ namespace NServiceBus } public interface IMessageCreator { - object CreateInstance(System.Type messageType); - T CreateInstance(); - T CreateInstance(System.Action action); + object CreateInstance([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.Type messageType); + T CreateInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(); + T CreateInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(System.Action action); } public interface IMessageHandlerContext : NServiceBus.Extensibility.IExtendable, NServiceBus.ICancellableContext, NServiceBus.IMessageProcessingContext, NServiceBus.IPipelineContext { @@ -531,14 +531,14 @@ namespace NServiceBus string ReplyToAddress { get; } System.Threading.Tasks.Task ForwardCurrentMessageTo(string destination); System.Threading.Tasks.Task Reply(object message, NServiceBus.ReplyOptions options); - System.Threading.Tasks.Task Reply(System.Action messageConstructor, NServiceBus.ReplyOptions options); + System.Threading.Tasks.Task Reply<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(System.Action messageConstructor, NServiceBus.ReplyOptions options); } public interface IMessageSession { System.Threading.Tasks.Task Publish(object message, NServiceBus.PublishOptions publishOptions, System.Threading.CancellationToken cancellationToken = default); - System.Threading.Tasks.Task Publish(System.Action messageConstructor, NServiceBus.PublishOptions publishOptions, System.Threading.CancellationToken cancellationToken = default); + System.Threading.Tasks.Task Publish<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(System.Action messageConstructor, NServiceBus.PublishOptions publishOptions, System.Threading.CancellationToken cancellationToken = default); System.Threading.Tasks.Task Send(object message, NServiceBus.SendOptions sendOptions, System.Threading.CancellationToken cancellationToken = default); - System.Threading.Tasks.Task Send(System.Action messageConstructor, NServiceBus.SendOptions sendOptions, System.Threading.CancellationToken cancellationToken = default); + System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(System.Action messageConstructor, NServiceBus.SendOptions sendOptions, System.Threading.CancellationToken cancellationToken = default); System.Threading.Tasks.Task Subscribe(System.Type eventType, NServiceBus.SubscribeOptions subscribeOptions, System.Threading.CancellationToken cancellationToken = default); System.Threading.Tasks.Task Unsubscribe(System.Type eventType, NServiceBus.UnsubscribeOptions unsubscribeOptions, System.Threading.CancellationToken cancellationToken = default); } @@ -549,9 +549,9 @@ namespace NServiceBus public interface IPipelineContext : NServiceBus.Extensibility.IExtendable, NServiceBus.ICancellableContext { System.Threading.Tasks.Task Publish(object message, NServiceBus.PublishOptions options); - System.Threading.Tasks.Task Publish(System.Action messageConstructor, NServiceBus.PublishOptions publishOptions); + System.Threading.Tasks.Task Publish<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(System.Action messageConstructor, NServiceBus.PublishOptions publishOptions); System.Threading.Tasks.Task Send(object message, NServiceBus.SendOptions options); - System.Threading.Tasks.Task Send(System.Action messageConstructor, NServiceBus.SendOptions options); + System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(System.Action messageConstructor, NServiceBus.SendOptions options); } public interface ISagaNotFoundHandler { @@ -720,7 +720,7 @@ namespace NServiceBus public static class MessageProcessingContextExtensions { public static System.Threading.Tasks.Task Reply(this NServiceBus.IMessageProcessingContext context, object message) { } - public static System.Threading.Tasks.Task Reply(this NServiceBus.IMessageProcessingContext context, System.Action messageConstructor) { } + public static System.Threading.Tasks.Task Reply<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this NServiceBus.IMessageProcessingContext context, System.Action messageConstructor) { } } public static class MessageProcessingOptimizationExtensions { @@ -732,14 +732,14 @@ namespace NServiceBus public static class MessageSessionExtensions { public static System.Threading.Tasks.Task Publish(this NServiceBus.IMessageSession session, object message, System.Threading.CancellationToken cancellationToken = default) { } - public static System.Threading.Tasks.Task Publish(this NServiceBus.IMessageSession session, System.Threading.CancellationToken cancellationToken = default) { } - public static System.Threading.Tasks.Task Publish(this NServiceBus.IMessageSession session, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } + public static System.Threading.Tasks.Task Publish<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this NServiceBus.IMessageSession session, System.Threading.CancellationToken cancellationToken = default) { } + public static System.Threading.Tasks.Task Publish<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this NServiceBus.IMessageSession session, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } public static System.Threading.Tasks.Task Send(this NServiceBus.IMessageSession session, object message, System.Threading.CancellationToken cancellationToken = default) { } public static System.Threading.Tasks.Task Send(this NServiceBus.IMessageSession session, string destination, object message, System.Threading.CancellationToken cancellationToken = default) { } - public static System.Threading.Tasks.Task Send(this NServiceBus.IMessageSession session, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } - public static System.Threading.Tasks.Task Send(this NServiceBus.IMessageSession session, string destination, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } + public static System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this NServiceBus.IMessageSession session, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } + public static System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this NServiceBus.IMessageSession session, string destination, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } public static System.Threading.Tasks.Task SendLocal(this NServiceBus.IMessageSession session, object message, System.Threading.CancellationToken cancellationToken = default) { } - public static System.Threading.Tasks.Task SendLocal(this NServiceBus.IMessageSession session, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } + public static System.Threading.Tasks.Task SendLocal<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this NServiceBus.IMessageSession session, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } public static System.Threading.Tasks.Task Subscribe(this NServiceBus.IMessageSession session, System.Type messageType, System.Threading.CancellationToken cancellationToken = default) { } public static System.Threading.Tasks.Task Subscribe(this NServiceBus.IMessageSession session, System.Threading.CancellationToken cancellationToken = default) { } public static System.Threading.Tasks.Task Unsubscribe(this NServiceBus.IMessageSession session, System.Type messageType, System.Threading.CancellationToken cancellationToken = default) { } @@ -816,14 +816,14 @@ namespace NServiceBus public static class PipelineContextExtensions { public static System.Threading.Tasks.Task Publish(this NServiceBus.IPipelineContext context, object message) { } - public static System.Threading.Tasks.Task Publish(this NServiceBus.IPipelineContext context) { } - public static System.Threading.Tasks.Task Publish(this NServiceBus.IPipelineContext context, System.Action messageConstructor) { } + public static System.Threading.Tasks.Task Publish<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this NServiceBus.IPipelineContext context) { } + public static System.Threading.Tasks.Task Publish<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this NServiceBus.IPipelineContext context, System.Action messageConstructor) { } public static System.Threading.Tasks.Task Send(this NServiceBus.IPipelineContext context, object message) { } public static System.Threading.Tasks.Task Send(this NServiceBus.IPipelineContext context, string destination, object message) { } - public static System.Threading.Tasks.Task Send(this NServiceBus.IPipelineContext context, System.Action messageConstructor) { } - public static System.Threading.Tasks.Task Send(this NServiceBus.IPipelineContext context, string destination, System.Action messageConstructor) { } + public static System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this NServiceBus.IPipelineContext context, System.Action messageConstructor) { } + public static System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this NServiceBus.IPipelineContext context, string destination, System.Action messageConstructor) { } public static System.Threading.Tasks.Task SendLocal(this NServiceBus.IPipelineContext context, object message) { } - public static System.Threading.Tasks.Task SendLocal(this NServiceBus.IPipelineContext context, System.Action messageConstructor) { } + public static System.Threading.Tasks.Task SendLocal<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this NServiceBus.IPipelineContext context, System.Action messageConstructor) { } } public class PublishOptions : NServiceBus.Extensibility.ExtendableOptions { @@ -1699,12 +1699,28 @@ namespace NServiceBus.MessageInterfaces } namespace NServiceBus.MessageInterfaces.MessageMapper.Reflection { + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("The message mapper generates concrete proxies for interface-based messages using " + + "System.Reflection.Emit, which is not supported when dynamic code generation is u" + + "navailable (e.g. trimming or NativeAOT).")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The message mapper reflects over all fields, properties, constructors and custom " + + "attributes of the provided message types to generate concrete proxies, which can" + + "not be statically analyzed by the trimmer.")] public class MessageMapper : NServiceBus.IMessageCreator, NServiceBus.MessageInterfaces.IMessageMapper { public MessageMapper() { } - public object CreateInstance(System.Type t) { } - public T CreateInstance() { } - public T CreateInstance(System.Action action) { } + public object CreateInstance([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.Type t) { } + public T CreateInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>() { } + public T CreateInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(System.Action action) { } + public System.Type? GetMappedTypeFor(System.Type t) { } + public System.Type? GetMappedTypeFor(string typeName) { } + public void Initialize(System.Collections.Generic.IEnumerable? types) { } + } + public sealed class TrimmingSafeMessageMapper : NServiceBus.IMessageCreator, NServiceBus.MessageInterfaces.IMessageMapper + { + public TrimmingSafeMessageMapper() { } + public object CreateInstance([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.Type t) { } + public T CreateInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>() { } + public T CreateInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(System.Action action) { } public System.Type? GetMappedTypeFor(System.Type t) { } public System.Type? GetMappedTypeFor(string typeName) { } public void Initialize(System.Collections.Generic.IEnumerable? types) { } diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt index de8fe1c5cca..68fef70644c 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt @@ -19,21 +19,6 @@ src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriter.cs src/NServiceBus.Core/Hosting/StartupDiagnostics/JsonPrettyPrinter.cs IL2026: Using member 'System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. -src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/ConcreteProxyCreator.cs - IL2067: 'interfaceType' argument does not satisfy 'DynamicallyAccessedMemberTypes.All' in call to 'System.Reflection.Emit.TypeBuilder.AddInterfaceImplementation(Type)'. The parameter 'type' of method 'NServiceBus.ConcreteProxyCreator.CreateTypeFrom(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. - IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 'type' of method 'NServiceBus.ConcreteProxyCreator.GetAllProperties(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. - IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperties()'. The parameter 'type' of method 'NServiceBus.ConcreteProxyCreator.GetAllProperties(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. - -src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/MessageMapper.cs - IL2067: 'type' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicConstructors', 'DynamicallyAccessedMemberTypes.NonPublicConstructors' in call to 'System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(Type)'. The parameter 't' of method 'NServiceBus.MessageInterfaces.MessageMapper.Reflection.MessageMapper.CreateInstance(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. - IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 't' of method 'NServiceBus.MessageInterfaces.MessageMapper.Reflection.MessageMapper.InnerInitialize(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. - IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicFields' in call to 'System.Type.GetFields(BindingFlags)'. The parameter 't' of method 'NServiceBus.MessageInterfaces.MessageMapper.Reflection.MessageMapper.InnerInitialize(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. - IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods' in call to 'System.Type.GetMethods()'. The parameter 'interfaceType' of method 'NServiceBus.MessageInterfaces.MessageMapper.Reflection.MessageMapper.GenerateImplementationFor(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. - IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicParameterlessConstructor' in call to 'System.Type.GetConstructor(Type[])'. The parameter 't' of method 'NServiceBus.MessageInterfaces.MessageMapper.Reflection.MessageMapper.InnerInitialize(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. - IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperties()'. The parameter 't' of method 'NServiceBus.MessageInterfaces.MessageMapper.Reflection.MessageMapper.InnerInitialize(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. - IL2072: 'type' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicConstructors', 'DynamicallyAccessedMemberTypes.NonPublicConstructors' in call to 'System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(Type)'. The return value of method 'NServiceBus.MessageInterfaces.MessageMapper.Reflection.MessageMapper.GetMappedTypeFor(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. - IL2075: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicParameterlessConstructor' in call to 'System.Type.GetConstructor(Type[])'. The return value of method 'NServiceBus.ConcreteProxyCreator.CreateTypeFrom(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. - src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaStorageFile.cs IL2026: Using member 'System.Text.Json.JsonSerializer.DeserializeAsync(Stream, JsonSerializerOptions, CancellationToken)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. IL2026: Using member 'System.Text.Json.JsonSerializer.SerializeAsync(Stream, Object, Type, JsonSerializerOptions, CancellationToken)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. diff --git a/src/NServiceBus.Core.Tests/MessageMapper/When_mapping_with_trimming_safe_mapper.cs b/src/NServiceBus.Core.Tests/MessageMapper/When_mapping_with_trimming_safe_mapper.cs new file mode 100644 index 00000000000..000127a2484 --- /dev/null +++ b/src/NServiceBus.Core.Tests/MessageMapper/When_mapping_with_trimming_safe_mapper.cs @@ -0,0 +1,125 @@ +#nullable enable + +namespace MessageMapperTests; + +using System; +using NServiceBus; +using NServiceBus.MessageInterfaces.MessageMapper.Reflection; +using NUnit.Framework; + +[TestFixture] +public class When_mapping_with_trimming_safe_mapper +{ + [Test] + public void Initialize_is_a_noop_and_does_not_throw() + { + var mapper = new TrimmingSafeMessageMapper(); + + Assert.DoesNotThrow(() => mapper.Initialize(null)); + Assert.DoesNotThrow(() => mapper.Initialize([])); + Assert.DoesNotThrow(() => mapper.Initialize([typeof(IInterfaceWithOnlyProperties)])); + } + + [Test] + public void GetMappedTypeFor_returns_the_same_concrete_type() + { + var mapper = new TrimmingSafeMessageMapper(); + + Assert.That(mapper.GetMappedTypeFor(typeof(ConcreteMessage)), Is.SameAs(typeof(ConcreteMessage))); + } + + [Test] + public void GetMappedTypeFor_for_interface_throws_not_supported() + { + var mapper = new TrimmingSafeMessageMapper(); + + // Failing here keeps the deserialization error actionable instead of letting the serializer fail opaquely + // later on an interface it cannot target. + var ex = Assert.Throws(() => mapper.GetMappedTypeFor(typeof(IInterfaceWithOnlyProperties))); + + Assert.That(ex!.Message, Does.Contain("not supported").And.Contain("dynamic code")); + } + + [Test] + public void GetMappedTypeFor_by_name_returns_null() + { + var mapper = new TrimmingSafeMessageMapper(); + + Assert.That(mapper.GetMappedTypeFor(typeof(ConcreteMessage).FullName!), Is.Null); + } + + [Test] + public void CreateInstance_for_concrete_type_returns_an_instance() + { + var mapper = new TrimmingSafeMessageMapper(); + + var instance = mapper.CreateInstance(typeof(ConcreteMessage)); + + Assert.That(instance, Is.InstanceOf()); + } + + [Test] + public void CreateInstance_generic_for_concrete_type_returns_an_instance() + { + var mapper = new TrimmingSafeMessageMapper(); + + var instance = mapper.CreateInstance(); + + Assert.That(instance, Is.InstanceOf()); + } + + [Test] + public void CreateInstance_generic_with_action_applies_the_action() + { + var mapper = new TrimmingSafeMessageMapper(); + + var instance = mapper.CreateInstance(m => m.SomeProperty = "hello"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(instance, Is.InstanceOf()); + Assert.That(instance.SomeProperty, Is.EqualTo("hello")); + } + } + + [Test] + public void CreateInstance_for_interface_throws_not_supported() + { + var mapper = new TrimmingSafeMessageMapper(); + + var ex = Assert.Throws(() => mapper.CreateInstance(typeof(IInterfaceWithOnlyProperties))); + + Assert.That(ex!.Message, Does.Contain("not supported").And.Contain("dynamic code")); + } + + [Test] + public void CreateInstance_generic_for_interface_throws_not_supported() + { + var mapper = new TrimmingSafeMessageMapper(); + + Assert.Throws(() => mapper.CreateInstance()); + } + + [Test] + public void CreateInstance_for_abstract_type_throws_not_supported() + { + var mapper = new TrimmingSafeMessageMapper(); + + Assert.Throws(() => mapper.CreateInstance(typeof(AbstractMessage))); + } + + public interface IInterfaceWithOnlyProperties : IMessage + { + string SomeProperty { get; set; } + } + + public class ConcreteMessage : IMessage + { + public string? SomeProperty { get; set; } + } + + public abstract class AbstractMessage + { + public string? SomeProperty { get; set; } + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core/EndpointConfiguration.cs b/src/NServiceBus.Core/EndpointConfiguration.cs index b19c25d4948..fa1e0df9014 100644 --- a/src/NServiceBus.Core/EndpointConfiguration.cs +++ b/src/NServiceBus.Core/EndpointConfiguration.cs @@ -4,9 +4,12 @@ namespace NServiceBus; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.CompilerServices; using System.Transactions; using Configuration.AdvancedExtensibility; using Features; +using MessageInterfaces; +using MessageInterfaces.MessageMapper.Reflection; using Microsoft.Extensions.DependencyInjection; using Particular.Obsoletes; using Pipeline; @@ -33,6 +36,8 @@ public EndpointConfiguration(string endpointName) Settings.SetDefault("Transactions.IsolationLevel", IsolationLevel.ReadCommitted); Settings.SetDefault("Transactions.DefaultTimeout", TransactionManager.DefaultTimeout); + Settings.SetDefault(RuntimeFeature.IsDynamicCodeSupported ? CreateDynamicCodeMessageMapper() : new TrimmingSafeMessageMapper()); + Settings.Set(new AssemblyScanningComponent.Configuration(Settings)); Settings.Set(new HostingComponent.Settings(Settings)); Settings.Set(new InstallerComponent.Settings(Settings)); @@ -168,4 +173,10 @@ static void ActivateAndInvoke(IList types, Action action) where T : static bool HasDefaultConstructor([DynamicallyAccessedMembers(DynamicMemberTypeAccess.InitializationExtension)] Type type) => type.GetConstructor(Type.EmptyTypes) != null; } + + const string DynamicCodeMessageMapperSuppressJustification = "The MessageMapper relies on System.Reflection.Emit to generate interface proxies, which is only constructed when RuntimeFeature.IsDynamicCodeSupported is true, so it is unreachable when the application is trimmed or published as NativeAOT."; + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = DynamicCodeMessageMapperSuppressJustification)] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = DynamicCodeMessageMapperSuppressJustification)] + static MessageMapper CreateDynamicCodeMessageMapper() => new(); } \ No newline at end of file diff --git a/src/NServiceBus.Core/EndpointCreator.cs b/src/NServiceBus.Core/EndpointCreator.cs index 71c893904df..5839247f92d 100644 --- a/src/NServiceBus.Core/EndpointCreator.cs +++ b/src/NServiceBus.Core/EndpointCreator.cs @@ -7,7 +7,6 @@ namespace NServiceBus; using Features; using Logging; using MessageInterfaces; -using MessageInterfaces.MessageMapper.Reflection; using Microsoft.Extensions.DependencyInjection; using Pipeline; using Settings; @@ -116,9 +115,6 @@ void Configure() var routingConfiguration = RoutingComponent.Configure(settings.Get()); - var messageMapper = new MessageMapper(); - settings.Set(messageMapper); - recoverabilityComponent = new RecoverabilityComponent(settings); var sagaSettings = settings.Get(); @@ -146,7 +142,7 @@ void Configure() pipelineSettings, hostingConfiguration); - sendComponent = SendComponent.Initialize(pipelineSettings, hostingConfiguration, routingComponent, messageMapper); + sendComponent = SendComponent.Initialize(pipelineSettings, hostingConfiguration, routingComponent, settings.Get()); envelopeComponent = new EnvelopeComponent(settings.Get()); diff --git a/src/NServiceBus.Core/IMessageCreator.cs b/src/NServiceBus.Core/IMessageCreator.cs index a662c7e7347..b68a1421d4d 100644 --- a/src/NServiceBus.Core/IMessageCreator.cs +++ b/src/NServiceBus.Core/IMessageCreator.cs @@ -3,18 +3,21 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; /// /// The abstraction for creating interface-based messages. /// public interface IMessageCreator { + internal const DynamicallyAccessedMemberTypes CreatorMembersRequired = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + /// /// Creates an instance of the message type T. /// /// The type of message interface to instantiate. /// A message object that implements the interface T. - T CreateInstance(); + T CreateInstance<[DynamicallyAccessedMembers(CreatorMembersRequired)] T>(); /// /// Creates an instance of the message type T and fills it with data. @@ -22,12 +25,12 @@ public interface IMessageCreator /// The type of message interface to instantiate. /// An action to set various properties of the instantiated object. /// A message object that implements the interface T. - T CreateInstance(Action action); + T CreateInstance<[DynamicallyAccessedMembers(CreatorMembersRequired)] T>(Action action); /// /// Creates an instance of the given message type. /// /// The type of message to instantiate. /// A message object that implements the given interface. - object CreateInstance(Type messageType); + object CreateInstance([DynamicallyAccessedMembers(CreatorMembersRequired)] Type messageType); } \ No newline at end of file diff --git a/src/NServiceBus.Core/IMessageProcessingContext.cs b/src/NServiceBus.Core/IMessageProcessingContext.cs index 23844af9e19..4da04485752 100644 --- a/src/NServiceBus.Core/IMessageProcessingContext.cs +++ b/src/NServiceBus.Core/IMessageProcessingContext.cs @@ -2,6 +2,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; /// @@ -37,7 +38,7 @@ public interface IMessageProcessingContext : IPipelineContext /// The type of message, usually an interface. /// An action which initializes properties of the message. /// Options for this reply. - Task Reply(Action messageConstructor, ReplyOptions options); + Task Reply<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, ReplyOptions options); /// /// Forwards the current message being handled to the destination maintaining diff --git a/src/NServiceBus.Core/IMessageSession.cs b/src/NServiceBus.Core/IMessageSession.cs index a7fdd7b6c33..2852e020b47 100644 --- a/src/NServiceBus.Core/IMessageSession.cs +++ b/src/NServiceBus.Core/IMessageSession.cs @@ -3,6 +3,7 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; @@ -26,7 +27,7 @@ public interface IMessageSession /// An action which initializes properties of the message. /// The options for the send. /// A to observe. - Task Send(Action messageConstructor, SendOptions sendOptions, CancellationToken cancellationToken = default); + Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, SendOptions sendOptions, CancellationToken cancellationToken = default); /// /// Publish the message to subscribers. @@ -43,7 +44,7 @@ public interface IMessageSession /// An action which initializes properties of the message. /// Specific options for this event. /// A to observe. - Task Publish(Action messageConstructor, PublishOptions publishOptions, CancellationToken cancellationToken = default); + Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, PublishOptions publishOptions, CancellationToken cancellationToken = default); /// /// Subscribes to receive published messages of the specified type. diff --git a/src/NServiceBus.Core/IPipelineContext.cs b/src/NServiceBus.Core/IPipelineContext.cs index a52eaf2e1dc..69a46eb7789 100644 --- a/src/NServiceBus.Core/IPipelineContext.cs +++ b/src/NServiceBus.Core/IPipelineContext.cs @@ -1,6 +1,7 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Extensibility; @@ -22,7 +23,7 @@ public interface IPipelineContext : ICancellableContext, IExtendable /// The type of message, usually an interface. /// An action which initializes properties of the message. /// The options for the send. - Task Send(Action messageConstructor, SendOptions options); + Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, SendOptions options); /// /// Publish the message to subscribers. @@ -37,5 +38,5 @@ public interface IPipelineContext : ICancellableContext, IExtendable /// The type of message, usually an interface. /// An action which initializes properties of the message. /// Specific options for this event. - Task Publish(Action messageConstructor, PublishOptions publishOptions); + Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, PublishOptions publishOptions); } \ No newline at end of file diff --git a/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/ConcreteProxyCreator.cs b/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/ConcreteProxyCreator.cs index b8142771726..ddf7d80d4dd 100644 --- a/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/ConcreteProxyCreator.cs +++ b/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/ConcreteProxyCreator.cs @@ -4,12 +4,14 @@ namespace NServiceBus; using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Reflection.Emit; class ConcreteProxyCreator { + [RequiresDynamicCode("Generating message proxies uses System.Reflection.Emit to emit concrete types at runtime, which is not supported when dynamic code generation is unavailable (e.g. trimming or NativeAOT).")] public ConcreteProxyCreator() { var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("NServiceBusMessageProxies"), AssemblyBuilderAccess.Run); @@ -20,6 +22,8 @@ public ConcreteProxyCreator() /// Generates the concrete implementation of the given type. /// Only properties on the given type are generated in the concrete implementation. /// + [RequiresDynamicCode("Generating message proxies uses System.Reflection.Emit to emit concrete types at runtime, which is not supported when dynamic code generation is unavailable (e.g. trimming or NativeAOT).")] + [RequiresUnreferencedCode("Generating message proxies reflects over all properties and custom attributes of the source interface, which cannot be statically analyzed by the trimmer.")] public Type CreateTypeFrom(Type type) { var typeBuilder = moduleBuilder.DefineType($"{type.FullName}{SUFFIX}", @@ -136,6 +140,7 @@ static void AddCustomAttributeToProperty(CustomAttributeData attributeData, Prop /// /// Returns all properties on the given type, going up the inheritance hierarchy. /// + [RequiresUnreferencedCode("Reflects over all properties and implemented interfaces of the given type, which cannot be statically analyzed by the trimmer.")] static List GetAllProperties(Type type) { var props = new List(type.GetProperties()); diff --git a/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/MessageMapper.cs b/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/MessageMapper.cs index b3640a3e3b4..0a6343a3c68 100644 --- a/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/MessageMapper.cs +++ b/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/MessageMapper.cs @@ -5,6 +5,7 @@ namespace NServiceBus.MessageInterfaces.MessageMapper.Reflection; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; @@ -13,6 +14,8 @@ namespace NServiceBus.MessageInterfaces.MessageMapper.Reflection; /// /// Uses reflection to map between interfaces and their generated concrete implementations. /// +[RequiresDynamicCode("The message mapper generates concrete proxies for interface-based messages using System.Reflection.Emit, which is not supported when dynamic code generation is unavailable (e.g. trimming or NativeAOT).")] +[RequiresUnreferencedCode("The message mapper reflects over all fields, properties, constructors and custom attributes of the provided message types to generate concrete proxies, which cannot be statically analyzed by the trimmer.")] public class MessageMapper : IMessageMapper { /// @@ -77,12 +80,12 @@ public void Initialize(IEnumerable? types) /// /// Calls the and returns its result cast to . /// - public T CreateInstance() => (T)CreateInstance(typeof(T)); + public T CreateInstance<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>() => (T)CreateInstance(typeof(T)); /// /// Calls the generic CreateInstance and performs the given action on the result. /// - public T CreateInstance(Action action) + public T CreateInstance<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action action) { var result = CreateInstance(); @@ -95,13 +98,13 @@ public T CreateInstance(Action action) /// If the given type is an interface, finds its generated concrete implementation, instantiates it, and returns the /// result. /// - public object CreateInstance(Type t) + public object CreateInstance([DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] Type t) { ArgumentNullException.ThrowIfNull(t); InitType(t); - if ((t.IsInterface || t.IsAbstract) && GetMappedTypeFor(t) is Type mapped) + if ((t.IsInterface || t.IsAbstract) && GetMappedTypeFor(t) is { } mapped) { return RuntimeHelpers.GetUninitializedObject(mapped); } diff --git a/src/NServiceBus.Core/MessageInterfaces/MessageMapper/TrimmingSafeMessageMapper.cs b/src/NServiceBus.Core/MessageInterfaces/MessageMapper/TrimmingSafeMessageMapper.cs new file mode 100644 index 00000000000..46210b62a21 --- /dev/null +++ b/src/NServiceBus.Core/MessageInterfaces/MessageMapper/TrimmingSafeMessageMapper.cs @@ -0,0 +1,90 @@ +#nullable enable + +namespace NServiceBus.MessageInterfaces.MessageMapper.Reflection; + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +/// +/// A trimming/AOT-safe that does not rely on dynamic code generation. +/// +/// +/// Interface-based messages require a generated concrete proxy and therefore cannot be supported when +/// dynamic code generation is unavailable (trimming / NativeAOT). This mapper maps types as identity +/// (concrete types stay as-is) and instantiates concrete types directly. Any attempt to create or +/// resolve a proxy for an interface or an abstract class throws at the point of use, +/// rather than at startup. +/// +public sealed class TrimmingSafeMessageMapper : IMessageMapper +{ + /// + /// Does nothing. Proxy generation is not supported without dynamic code. + /// + public void Initialize(IEnumerable? types) + { + // Intentionally a no-op. Interface mapping is not available without dynamic code generation, + // and concrete types do not require initialization. + } + + /// + /// Returns the given concrete type unchanged. Interface or abstract types cannot be mapped to a generated + /// concrete implementation without dynamic code generation. Failing here (rather than returning the + /// interface and letting the serializer fail opaquely later) keeps the deserialization error actionable. + /// + /// Thrown when is an interface or abstract type. Interface-based messages require a proxy that can only be generated with dynamic code; publish a concrete type that implements the interface instead. + public Type? GetMappedTypeFor(Type t) + { + ArgumentNullException.ThrowIfNull(t); + + if (t.IsInterface || t.IsAbstract) + { + throw new NotSupportedException( + $"Mapping interface or abstract type '{t.FullName}' to a concrete implementation is not supported when dynamic code generation is unavailable (e.g. under trimming or NativeAOT), because no proxy can be generated. " + + "Publish a concrete type that implements the interface instead of the interface itself so it can be deserialized directly."); + } + + return t; + } + + /// + /// Returns null. No name-to-type mapping is available without dynamic code generation. + /// + public Type? GetMappedTypeFor(string typeName) => null; + + /// + /// Instantiates a concrete message type. + /// + /// Thrown when is an interface or abstract type. Interface-based messages require a proxy that can only be generated with dynamic code; use a concrete type instead. + public T CreateInstance<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>() => (T)CreateInstance(typeof(T)); + + /// + /// Instantiates a concrete message type, then applies the given action. + /// + /// Thrown when is an interface or abstract type. Interface-based messages require a proxy that can only be generated with dynamic code; use a concrete type instead. + public T CreateInstance<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action action) + { + var result = CreateInstance(); + action(result); + return result; + } + + /// + /// Instantiates a concrete message type. + /// + /// Thrown when is an interface or abstract type. Interface-based messages require a proxy that can only be generated with dynamic code; use a concrete type instead. + public object CreateInstance([DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] Type t) + { + ArgumentNullException.ThrowIfNull(t); + + if (t.IsInterface || t.IsAbstract) + { + throw new NotSupportedException( + $"Creating an instance of interface or abstract type '{t.FullName}' is not supported when dynamic code generation is unavailable (e.g. under trimming or NativeAOT). " + + "Use concrete message types instead of interface-based messages in these scenarios."); + } + + return RuntimeHelpers.GetUninitializedObject(t); + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core/MessageProcessingContextExtensions.cs b/src/NServiceBus.Core/MessageProcessingContextExtensions.cs index 3e9aca6f2bb..36d6d6951e1 100644 --- a/src/NServiceBus.Core/MessageProcessingContextExtensions.cs +++ b/src/NServiceBus.Core/MessageProcessingContextExtensions.cs @@ -1,6 +1,7 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; /// @@ -27,7 +28,7 @@ public static Task Reply(this IMessageProcessingContext context, object message) /// The type of message, usually an interface. /// Object being extended. /// An action which initializes properties of the message. - public static Task Reply(this IMessageProcessingContext context, Action messageConstructor) + public static Task Reply<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IMessageProcessingContext context, Action messageConstructor) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(messageConstructor); diff --git a/src/NServiceBus.Core/MessageSession.cs b/src/NServiceBus.Core/MessageSession.cs index 5eb95fdab40..935825aa707 100644 --- a/src/NServiceBus.Core/MessageSession.cs +++ b/src/NServiceBus.Core/MessageSession.cs @@ -90,7 +90,7 @@ public async Task Send(object message, SendOptions sendOptions, CancellationToke await messageOperations.Send(CreateContext(linkedTokenSource.Token), message, sendOptions).ConfigureAwait(false); } - public async Task Send(Action messageConstructor, SendOptions sendOptions, CancellationToken cancellationToken = default) + public async Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, SendOptions sendOptions, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(messageConstructor); ArgumentNullException.ThrowIfNull(sendOptions); @@ -112,7 +112,7 @@ public async Task Publish(object message, PublishOptions publishOptions, Cancell await messageOperations.Publish(CreateContext(linkedTokenSource.Token), message, publishOptions).ConfigureAwait(false); } - public async Task Publish(Action messageConstructor, PublishOptions publishOptions, CancellationToken cancellationToken = default) + public async Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, PublishOptions publishOptions, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(messageConstructor); ArgumentNullException.ThrowIfNull(publishOptions); diff --git a/src/NServiceBus.Core/MessageSessionExtensions.cs b/src/NServiceBus.Core/MessageSessionExtensions.cs index 6dc2c7a88f6..c115ecb96fe 100644 --- a/src/NServiceBus.Core/MessageSessionExtensions.cs +++ b/src/NServiceBus.Core/MessageSessionExtensions.cs @@ -1,6 +1,7 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; @@ -33,7 +34,7 @@ public static Task Send(this IMessageSession session, object message, Cancellati /// /// The message will be sent to the destination configured for . /// - public static Task Send(this IMessageSession session, Action messageConstructor, CancellationToken cancellationToken = default) + public static Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IMessageSession session, Action messageConstructor, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(session); ArgumentNullException.ThrowIfNull(messageConstructor); @@ -69,7 +70,7 @@ public static Task Send(this IMessageSession session, string destination, object /// The destination to which the message will be sent. /// An action which initializes properties of the message. /// A to observe. - public static Task Send(this IMessageSession session, string destination, Action messageConstructor, CancellationToken cancellationToken = default) + public static Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IMessageSession session, string destination, Action messageConstructor, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(session); ArgumentException.ThrowIfNullOrWhiteSpace(destination); @@ -107,7 +108,7 @@ public static Task SendLocal(this IMessageSession session, object message, Cance /// Object being extended. /// An action which initializes properties of the message. /// A to observe. - public static Task SendLocal(this IMessageSession session, Action messageConstructor, CancellationToken cancellationToken = default) + public static Task SendLocal<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IMessageSession session, Action messageConstructor, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(session); ArgumentNullException.ThrowIfNull(messageConstructor); @@ -139,7 +140,7 @@ public static Task Publish(this IMessageSession session, object message, Cancell /// The instance of to use for the action. /// A to observe. /// The message type. - public static Task Publish(this IMessageSession session, CancellationToken cancellationToken = default) + public static Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IMessageSession session, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(session); @@ -153,7 +154,7 @@ public static Task Publish(this IMessageSession session, CancellationToken ca /// The instance of to use for the action. /// An action which initializes properties of the message. /// A to observe. - public static Task Publish(this IMessageSession session, Action messageConstructor, CancellationToken cancellationToken = default) + public static Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IMessageSession session, Action messageConstructor, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(session); ArgumentNullException.ThrowIfNull(messageConstructor); diff --git a/src/NServiceBus.Core/Pipeline/Incoming/IncomingContext.cs b/src/NServiceBus.Core/Pipeline/Incoming/IncomingContext.cs index 02fed74a535..094e99626ee 100644 --- a/src/NServiceBus.Core/Pipeline/Incoming/IncomingContext.cs +++ b/src/NServiceBus.Core/Pipeline/Incoming/IncomingContext.cs @@ -4,6 +4,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Pipeline; @@ -30,7 +31,7 @@ public Task Send(object message, SendOptions options) return MessageOperations.Send(this, message, options); } - public Task Send(Action messageConstructor, SendOptions options) + public Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, SendOptions options) { return MessageOperations.Send(this, messageConstructor, options); } @@ -40,7 +41,7 @@ public Task Publish(object message, PublishOptions options) return MessageOperations.Publish(this, message, options); } - public Task Publish(Action messageConstructor, PublishOptions publishOptions) + public Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, PublishOptions publishOptions) { return MessageOperations.Publish(this, messageConstructor, publishOptions); } @@ -50,7 +51,7 @@ public Task Reply(object message, ReplyOptions options) return MessageOperations.Reply(this, message, options); } - public Task Reply(Action messageConstructor, ReplyOptions options) + public Task Reply<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, ReplyOptions options) { return MessageOperations.Reply(this, messageConstructor, options); } diff --git a/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingContext.cs b/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingContext.cs index 1a5c401f0c3..61c9b275f7f 100644 --- a/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingContext.cs +++ b/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingContext.cs @@ -4,6 +4,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Pipeline; @@ -27,7 +28,7 @@ public Task Send(object message, SendOptions options) return MessageOperations.Send(this, message, options); } - public Task Send(Action messageConstructor, SendOptions options) + public Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, SendOptions options) { return MessageOperations.Send(this, messageConstructor, options); } @@ -37,7 +38,7 @@ public Task Publish(object message, PublishOptions options) return MessageOperations.Publish(this, message, options); } - public Task Publish(Action messageConstructor, PublishOptions publishOptions) + public Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, PublishOptions publishOptions) { return MessageOperations.Publish(this, messageConstructor, publishOptions); } diff --git a/src/NServiceBus.Core/PipelineContextExtensions.cs b/src/NServiceBus.Core/PipelineContextExtensions.cs index d7d54aac15b..a8540fcfee0 100644 --- a/src/NServiceBus.Core/PipelineContextExtensions.cs +++ b/src/NServiceBus.Core/PipelineContextExtensions.cs @@ -3,6 +3,7 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; /// @@ -32,7 +33,7 @@ public static Task Send(this IPipelineContext context, object message) /// /// The message will be sent to the destination configured for . /// - public static Task Send(this IPipelineContext context, Action messageConstructor) + public static Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IPipelineContext context, Action messageConstructor) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(messageConstructor); @@ -66,7 +67,7 @@ public static Task Send(this IPipelineContext context, string destination, objec /// The instance of to use for the action. /// The destination to which the message will be sent. /// An action which initializes properties of the message. - public static Task Send(this IPipelineContext context, string destination, Action messageConstructor) + public static Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IPipelineContext context, string destination, Action messageConstructor) { ArgumentNullException.ThrowIfNull(context); ArgumentException.ThrowIfNullOrWhiteSpace(destination); @@ -102,7 +103,7 @@ public static Task SendLocal(this IPipelineContext context, object message) /// The type of message, usually an interface. /// Object being extended. /// An action which initializes properties of the message. - public static Task SendLocal(this IPipelineContext context, Action messageConstructor) + public static Task SendLocal<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IPipelineContext context, Action messageConstructor) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(messageConstructor); @@ -132,7 +133,7 @@ public static Task Publish(this IPipelineContext context, object message) /// /// The instance of to use for the action. /// The message type. - public static Task Publish(this IPipelineContext context) + public static Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IPipelineContext context) { ArgumentNullException.ThrowIfNull(context); @@ -145,7 +146,7 @@ public static Task Publish(this IPipelineContext context) /// The type of message, usually an interface. /// The instance of to use for the action. /// An action which initializes properties of the message. - public static Task Publish(this IPipelineContext context, Action messageConstructor) + public static Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IPipelineContext context, Action messageConstructor) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(messageConstructor); diff --git a/src/NServiceBus.Core/Unicast/MessageOperations.cs b/src/NServiceBus.Core/Unicast/MessageOperations.cs index 34c60f3cda1..99e770a0883 100644 --- a/src/NServiceBus.Core/Unicast/MessageOperations.cs +++ b/src/NServiceBus.Core/Unicast/MessageOperations.cs @@ -2,6 +2,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Extensibility; using MessageInterfaces; @@ -36,7 +37,7 @@ public MessageOperations( this.activityFactory = activityFactory; } - public Task Publish(IBehaviorContext context, Action messageConstructor, PublishOptions options) + public Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(IBehaviorContext context, Action messageConstructor, PublishOptions options) { return Publish(context, typeof(T), messageMapper.CreateInstance(messageConstructor), options); } @@ -103,7 +104,7 @@ public async Task Unsubscribe(IBehaviorContext context, Type eventType, Unsubscr await unsubscribePipeline.Invoke(unsubscribeContext, activity).ConfigureAwait(false); } - public Task Send(IBehaviorContext context, Action messageConstructor, SendOptions options) + public Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(IBehaviorContext context, Action messageConstructor, SendOptions options) { return SendMessage(context, typeof(T), messageMapper.CreateInstance(messageConstructor), options); } @@ -144,7 +145,7 @@ public Task Reply(IBehaviorContext context, object message, ReplyOptions options return ReplyMessage(context, messageType, message, options); } - public Task Reply(IBehaviorContext context, Action messageConstructor, ReplyOptions options) + public Task Reply<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(IBehaviorContext context, Action messageConstructor, ReplyOptions options) { return ReplyMessage(context, typeof(T), messageMapper.CreateInstance(messageConstructor), options); } diff --git a/src/NServiceBus.Core/Unicast/RunningEndpointInstance.cs b/src/NServiceBus.Core/Unicast/RunningEndpointInstance.cs index d084ceacf14..34f23d82d7e 100644 --- a/src/NServiceBus.Core/Unicast/RunningEndpointInstance.cs +++ b/src/NServiceBus.Core/Unicast/RunningEndpointInstance.cs @@ -3,6 +3,7 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Features; @@ -132,7 +133,7 @@ public Task Send(object message, SendOptions sendOptions, CancellationToken canc return messageSession.Send(message, sendOptions, cancellationToken); } - public Task Send(Action messageConstructor, SendOptions sendOptions, CancellationToken cancellationToken = default) + public Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, SendOptions sendOptions, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(messageConstructor); ArgumentNullException.ThrowIfNull(sendOptions); @@ -150,7 +151,7 @@ public Task Publish(object message, PublishOptions publishOptions, CancellationT return messageSession.Publish(message, publishOptions, cancellationToken); } - public Task Publish(Action messageConstructor, PublishOptions publishOptions, CancellationToken cancellationToken = default) + public Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, PublishOptions publishOptions, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(messageConstructor); ArgumentNullException.ThrowIfNull(publishOptions); From 09b7b0734686554c9749e7fe4aa8d1675f88b089 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 16:42:01 +0000 Subject: [PATCH 12/79] Update actions/checkout action to v7 (#7828) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f80272361ae..7206d74ec6a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: fail-fast: false steps: - name: Checkout - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 with: fetch-depth: 0 - name: Setup .NET SDK diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ff7e527831d..aedcdb86fed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.0 with: fetch-depth: 0 - name: Setup .NET SDK From 7b295cc9d6c3a39f2392bde208f662b8c8805244 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Sun, 21 Jun 2026 18:07:57 +0000 Subject: [PATCH 13/79] Prepare tests for header pooling (#7826) * Taking a defensive copy of the contexts in the transport tests to enable pooling * Copy headers in acceptance tests --------- Co-authored-by: Daniel Marbach --- .../Diagnostics/When_message_is_faulted.cs | 2 +- ...n_incoming_message_moved_to_error_queue.cs | 2 +- ...When_requesting_message_to_be_forwarded.cs | 2 +- .../When_retrying_message_from_error_queue.cs | 3 ++- .../When_message_is_available.cs | 21 ++++++++++--------- .../When_modifying_headers_before_on_error.cs | 12 +++++++++-- .../When_modifying_headers_before_retry.cs | 10 +++++++-- .../When_modifying_headers_in_on_error.cs | 10 +++++++-- .../When_on_message_throws.cs | 12 +++++++++-- ...n_on_message_throws_after_delayed_retry.cs | 13 ++++++++++-- ...on_message_throws_after_immediate_retry.cs | 13 ++++++++++-- .../When_receiving_message.cs | 13 ++++++++++-- .../When_scope_complete_throws.cs | 13 ++++++++++-- ...ows_after_successful_message_processing.cs | 11 +++++++++- .../When_setting_context_items.cs | 13 ++++++++++-- ...hen_using_unicode_characters_in_headers.cs | 10 +++++++-- 16 files changed, 125 insertions(+), 35 deletions(-) diff --git a/src/NServiceBus.AcceptanceTests/Core/Diagnostics/When_message_is_faulted.cs b/src/NServiceBus.AcceptanceTests/Core/Diagnostics/When_message_is_faulted.cs index dd87b9ecba6..a6b37e59fc7 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Diagnostics/When_message_is_faulted.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Diagnostics/When_message_is_faulted.cs @@ -87,7 +87,7 @@ public class AuditMessageHandler(Context testContext) : IHandleMessages x.Key, x => x.Value); testContext.IsMessageHandledByTheAuditEndpoint = true; testContext.MaybeCompleted(); return Task.CompletedTask; diff --git a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_incoming_message_moved_to_error_queue.cs b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_incoming_message_moved_to_error_queue.cs index 0c2383481c6..89cd80b1f7e 100644 --- a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_incoming_message_moved_to_error_queue.cs +++ b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_incoming_message_moved_to_error_queue.cs @@ -52,7 +52,7 @@ class ErrorMessageDetector(Context testContext) : Behavior next) { - testContext.ErrorMessageHeaders = context.Message.Headers; + testContext.ErrorMessageHeaders = context.Message.Headers.ToDictionary(x => x.Key, x => x.Value); testContext.MarkAsCompleted(); return next(); } diff --git a/src/NServiceBus.AcceptanceTests/Forwarding/When_requesting_message_to_be_forwarded.cs b/src/NServiceBus.AcceptanceTests/Forwarding/When_requesting_message_to_be_forwarded.cs index edc1b7e0963..61b5aa44c0b 100644 --- a/src/NServiceBus.AcceptanceTests/Forwarding/When_requesting_message_to_be_forwarded.cs +++ b/src/NServiceBus.AcceptanceTests/Forwarding/When_requesting_message_to_be_forwarded.cs @@ -39,7 +39,7 @@ public class MessageToForwardHandler(Context testContext) : IHandleMessages x.Key, x => x.Value); testContext.GotForwardedMessage = true; testContext.MarkAsCompleted(); return Task.CompletedTask; diff --git a/src/NServiceBus.AcceptanceTests/Recoverability/When_retrying_message_from_error_queue.cs b/src/NServiceBus.AcceptanceTests/Recoverability/When_retrying_message_from_error_queue.cs index acceb2f1f6e..47b54e80505 100644 --- a/src/NServiceBus.AcceptanceTests/Recoverability/When_retrying_message_from_error_queue.cs +++ b/src/NServiceBus.AcceptanceTests/Recoverability/When_retrying_message_from_error_queue.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using AcceptanceTesting; using AcceptanceTesting.Customization; @@ -100,7 +101,7 @@ public class FailedMessageHandler(Context testContext) : IHandleMessages x.Key, x => x.Value); testContext.MaybeCompleted(); return Task.CompletedTask; } diff --git a/src/NServiceBus.TransportTests/When_message_is_available.cs b/src/NServiceBus.TransportTests/When_message_is_available.cs index 25188a0bed9..f7298fa98d7 100644 --- a/src/NServiceBus.TransportTests/When_message_is_available.cs +++ b/src/NServiceBus.TransportTests/When_message_is_available.cs @@ -1,5 +1,6 @@ namespace NServiceBus.TransportTests; +using System; using System.Collections.Generic; using System.Threading.Tasks; using NUnit.Framework; @@ -15,18 +16,18 @@ public async Task Should_invoke_on_message(TransportTransactionMode transactionM { var onMessageInvoked = CreateTaskCompletionSource(); - byte[] messageBody = null; - await StartPump( - (context, _) => - { - messageBody = context.Body.ToArray(); - return onMessageInvoked.SetCompleted(context); - }, + (context, _) => onMessageInvoked.SetCompleted(new MessageContext( + context.NativeMessageId, + new Dictionary(context.Headers), + context.Body.ToArray(), + context.TransportTransaction, + context.ReceiveAddress, + context.Extensions)), (_, __) => Task.FromResult(ErrorHandleResult.Handled), transactionMode); - await SendMessage(InputQueueName, new Dictionary { { "MyHeader", "MyValue" } }, body: new byte[] { 1, 2, 3 }); + await SendMessage(InputQueueName, new Dictionary { { "MyHeader", "MyValue" } }, body: [1, 2, 3]); var messageContext = await onMessageInvoked.Task; @@ -34,7 +35,7 @@ await StartPump( { Assert.That(string.IsNullOrEmpty(messageContext.NativeMessageId), Is.False, "Should pass the native message id"); Assert.That(messageContext.Headers["MyHeader"], Is.EqualTo("MyValue"), "Should pass the message headers"); - Assert.That(messageBody, Is.EqualTo(new byte[] { 1, 2, 3 }), "Should pass the body"); + Assert.That(messageContext.Body.Span.SequenceEqual(new byte[] { 1, 2, 3 }), Is.True, "Should pass the body"); } } -} +} \ No newline at end of file diff --git a/src/NServiceBus.TransportTests/When_modifying_headers_before_on_error.cs b/src/NServiceBus.TransportTests/When_modifying_headers_before_on_error.cs index c6dc9e2f512..084aa296d39 100644 --- a/src/NServiceBus.TransportTests/When_modifying_headers_before_on_error.cs +++ b/src/NServiceBus.TransportTests/When_modifying_headers_before_on_error.cs @@ -24,7 +24,15 @@ await StartPump( }, (context, __) => { - errorHandled.SetResult(context); + errorHandled.SetResult(new ErrorContext( + context.Exception, + new Dictionary(context.Headers), + context.NativeMessageId, + context.Body.ToArray(), + context.TransportTransaction, + context.ImmediateProcessingFailures, + context.ReceiveAddress, + context.Extensions)); return Task.FromResult(ErrorHandleResult.Handled); }, transactionMode); @@ -35,4 +43,4 @@ await StartPump( Assert.That(errorContext.Headers["test-header"], Is.EqualTo("original")); } -} +} \ No newline at end of file diff --git a/src/NServiceBus.TransportTests/When_modifying_headers_before_retry.cs b/src/NServiceBus.TransportTests/When_modifying_headers_before_retry.cs index d861b612f64..4b9d7eabf4d 100644 --- a/src/NServiceBus.TransportTests/When_modifying_headers_before_retry.cs +++ b/src/NServiceBus.TransportTests/When_modifying_headers_before_retry.cs @@ -22,7 +22,13 @@ await StartPump( { if (retrying) { - return retried.SetCompleted(context); + return retried.SetCompleted(new MessageContext( + context.NativeMessageId, + new Dictionary(context.Headers), + context.Body.ToArray(), + context.TransportTransaction, + context.ReceiveAddress, + context.Extensions)); } context.Headers["test-header"] = "modified"; @@ -41,4 +47,4 @@ await StartPump( Assert.That(retryMessageContext.Headers["test-header"], Is.EqualTo("original")); } -} +} \ No newline at end of file diff --git a/src/NServiceBus.TransportTests/When_modifying_headers_in_on_error.cs b/src/NServiceBus.TransportTests/When_modifying_headers_in_on_error.cs index c5df1706a3f..0751e7c2b5a 100644 --- a/src/NServiceBus.TransportTests/When_modifying_headers_in_on_error.cs +++ b/src/NServiceBus.TransportTests/When_modifying_headers_in_on_error.cs @@ -18,7 +18,13 @@ public async Task Should_roll_back(TransportTransactionMode transactionMode) var retrying = false; await StartPump( - (context, _) => retrying ? retried.SetCompleted(context) : throw new Exception(), + (context, _) => retrying ? retried.SetCompleted(new MessageContext( + context.NativeMessageId, + new Dictionary(context.Headers), + context.Body.ToArray(), + context.TransportTransaction, + context.ReceiveAddress, + context.Extensions)) : throw new Exception(), (context, _) => { retrying = true; @@ -33,4 +39,4 @@ await StartPump( Assert.That(retryMessageContext.Headers["test-header"], Is.EqualTo("original")); } -} +} \ No newline at end of file diff --git a/src/NServiceBus.TransportTests/When_on_message_throws.cs b/src/NServiceBus.TransportTests/When_on_message_throws.cs index 2dc25b8513c..5dcac8caa74 100644 --- a/src/NServiceBus.TransportTests/When_on_message_throws.cs +++ b/src/NServiceBus.TransportTests/When_on_message_throws.cs @@ -20,7 +20,15 @@ await StartPump( (_, __) => throw new Exception("Simulated exception"), (context, _) => { - onErrorCalled.SetResult(context); + onErrorCalled.SetResult(new ErrorContext( + context.Exception, + new Dictionary(context.Headers), + context.NativeMessageId, + context.Body.ToArray(), + context.TransportTransaction, + context.ImmediateProcessingFailures, + context.ReceiveAddress, + context.Extensions)); return Task.FromResult(ErrorHandleResult.Handled); }, transactionMode); @@ -36,4 +44,4 @@ await StartPump( Assert.That(errorContext.Headers["MyHeader"], Is.EqualTo("MyValue"), "Should pass the message headers"); } } -} +} \ No newline at end of file diff --git a/src/NServiceBus.TransportTests/When_on_message_throws_after_delayed_retry.cs b/src/NServiceBus.TransportTests/When_on_message_throws_after_delayed_retry.cs index 079dda0ea84..02a13646848 100644 --- a/src/NServiceBus.TransportTests/When_on_message_throws_after_delayed_retry.cs +++ b/src/NServiceBus.TransportTests/When_on_message_throws_after_delayed_retry.cs @@ -1,6 +1,7 @@ namespace NServiceBus.TransportTests; using System; +using System.Collections.Generic; using System.Threading.Tasks; using NUnit.Framework; using Transport; @@ -27,7 +28,15 @@ await StartPump( } else { - sentDelayedMessage.SetResult(context); + sentDelayedMessage.SetResult(new ErrorContext( + context.Exception, + new Dictionary(context.Headers), + context.NativeMessageId, + context.Body.ToArray(), + context.TransportTransaction, + context.ImmediateProcessingFailures, + context.ReceiveAddress, + context.Extensions)); } return ErrorHandleResult.Handled; @@ -40,4 +49,4 @@ await StartPump( Assert.That(errorContext.ImmediateProcessingFailures, Is.EqualTo(1), "Should track delivery attempts between immediate retries"); } -} +} \ No newline at end of file diff --git a/src/NServiceBus.TransportTests/When_on_message_throws_after_immediate_retry.cs b/src/NServiceBus.TransportTests/When_on_message_throws_after_immediate_retry.cs index 68fd71df2fc..5dece053e0b 100644 --- a/src/NServiceBus.TransportTests/When_on_message_throws_after_immediate_retry.cs +++ b/src/NServiceBus.TransportTests/When_on_message_throws_after_immediate_retry.cs @@ -1,6 +1,7 @@ namespace NServiceBus.TransportTests; using System; +using System.Collections.Generic; using System.Threading.Tasks; using NUnit.Framework; using Transport; @@ -23,7 +24,15 @@ await StartPump( { if (attempts == maxAttempts) { - maxAttemptsReached.SetResult(context); + maxAttemptsReached.SetResult(new ErrorContext( + context.Exception, + new Dictionary(context.Headers), + context.NativeMessageId, + context.Body.ToArray(), + context.TransportTransaction, + context.ImmediateProcessingFailures, + context.ReceiveAddress, + context.Extensions)); return Task.FromResult(ErrorHandleResult.Handled); } @@ -37,4 +46,4 @@ await StartPump( Assert.That(errorContext.ImmediateProcessingFailures, Is.EqualTo(attempts), "Should track delivery attempts between immediate retries"); } -} +} \ No newline at end of file diff --git a/src/NServiceBus.TransportTests/When_receiving_message.cs b/src/NServiceBus.TransportTests/When_receiving_message.cs index 28a6fdcb503..b99a74ccf70 100644 --- a/src/NServiceBus.TransportTests/When_receiving_message.cs +++ b/src/NServiceBus.TransportTests/When_receiving_message.cs @@ -1,6 +1,7 @@ namespace NServiceBus.TransportTests; using System; +using System.Collections.Generic; using System.Threading.Tasks; using NUnit.Framework; using Transport; @@ -23,7 +24,15 @@ await StartPump( }, (context, _) => { - onError.SetResult(context); + onError.SetResult(new ErrorContext( + context.Exception, + new Dictionary(context.Headers), + context.NativeMessageId, + context.Body.ToArray(), + context.TransportTransaction, + context.ImmediateProcessingFailures, + context.ReceiveAddress, + context.Extensions)); return Task.FromResult(ErrorHandleResult.Handled); }, transactionMode); @@ -33,4 +42,4 @@ await StartPump( var errorContext = await onError.Task; Assert.That(errorContext.ReceiveAddress, Is.EqualTo(receiver.ReceiveAddress)); } -} +} \ No newline at end of file diff --git a/src/NServiceBus.TransportTests/When_scope_complete_throws.cs b/src/NServiceBus.TransportTests/When_scope_complete_throws.cs index c25340d9ce5..84b5fc3c4ce 100644 --- a/src/NServiceBus.TransportTests/When_scope_complete_throws.cs +++ b/src/NServiceBus.TransportTests/When_scope_complete_throws.cs @@ -1,6 +1,7 @@ namespace NServiceBus.TransportTests; using System; +using System.Collections.Generic; using System.Threading.Tasks; using System.Transactions; using NUnit.Framework; @@ -25,7 +26,15 @@ await StartPump( }, (context, _) => { - onErrorCalled.SetResult(context); + onErrorCalled.SetResult(new ErrorContext( + context.Exception, + new Dictionary(context.Headers), + context.NativeMessageId, + context.Body.ToArray(), + context.TransportTransaction, + context.ImmediateProcessingFailures, + context.ReceiveAddress, + context.Extensions)); return Task.FromResult(ErrorHandleResult.Handled); }, transactionMode); @@ -57,4 +66,4 @@ class EnlistmentWhichFailsDuringPrepare : IEnlistmentNotification public void InDoubt(Enlistment enlistment) => enlistment.Done(); } -} +} \ No newline at end of file diff --git a/src/NServiceBus.TransportTests/When_scope_throws_after_successful_message_processing.cs b/src/NServiceBus.TransportTests/When_scope_throws_after_successful_message_processing.cs index f6e97d2fa03..7e609f63e21 100644 --- a/src/NServiceBus.TransportTests/When_scope_throws_after_successful_message_processing.cs +++ b/src/NServiceBus.TransportTests/When_scope_throws_after_successful_message_processing.cs @@ -1,6 +1,7 @@ namespace NServiceBus.TransportTests; using System; +using System.Collections.Generic; using System.Threading.Tasks; using System.Transactions; using NUnit.Framework; @@ -30,7 +31,15 @@ await StartPump( return Task.FromResult(ErrorHandleResult.RetryRequired); } - secondFailure.SetResult(context); + secondFailure.SetResult(new ErrorContext( + context.Exception, + new Dictionary(context.Headers), + context.NativeMessageId, + context.Body.ToArray(), + context.TransportTransaction, + context.ImmediateProcessingFailures, + context.ReceiveAddress, + context.Extensions)); return Task.FromResult(ErrorHandleResult.Handled); }, diff --git a/src/NServiceBus.TransportTests/When_setting_context_items.cs b/src/NServiceBus.TransportTests/When_setting_context_items.cs index 9d038633172..9088f0be4ff 100644 --- a/src/NServiceBus.TransportTests/When_setting_context_items.cs +++ b/src/NServiceBus.TransportTests/When_setting_context_items.cs @@ -1,6 +1,7 @@ namespace NServiceBus.TransportTests; using System; +using System.Collections.Generic; using System.Threading.Tasks; using NUnit.Framework; using Transport; @@ -23,7 +24,15 @@ await StartPump( }, (context, _) => { - onError.SetResult(context); + onError.SetResult(new ErrorContext( + context.Exception, + new Dictionary(context.Headers), + context.NativeMessageId, + context.Body.ToArray(), + context.TransportTransaction, + context.ImmediateProcessingFailures, + context.ReceiveAddress, + context.Extensions)); return Task.FromResult(ErrorHandleResult.Handled); }, transactionMode); @@ -34,4 +43,4 @@ await StartPump( Assert.That(errorContext.Extensions.Get("MyKey"), Is.EqualTo("MyValue")); } -} +} \ No newline at end of file diff --git a/src/NServiceBus.TransportTests/When_using_unicode_characters_in_headers.cs b/src/NServiceBus.TransportTests/When_using_unicode_characters_in_headers.cs index 3bdf0f532c9..101648e1b5b 100644 --- a/src/NServiceBus.TransportTests/When_using_unicode_characters_in_headers.cs +++ b/src/NServiceBus.TransportTests/When_using_unicode_characters_in_headers.cs @@ -13,7 +13,13 @@ public async Task Should_support_unicode_characters() var messageProcessed = CreateTaskCompletionSource(); await StartPump( - (context, _) => messageProcessed.SetCompleted(context), + (context, _) => messageProcessed.SetCompleted(new MessageContext( + context.NativeMessageId, + new Dictionary(context.Headers), + context.Body.ToArray(), + context.TransportTransaction, + context.ReceiveAddress, + context.Extensions)), (_, __) => Task.FromResult(ErrorHandleResult.Handled), TransportTransactionMode.None); @@ -35,4 +41,4 @@ await StartPump( Assert.That(messageContext.Headers, Is.Not.Empty); Assert.That(messageContext.Headers, Is.SupersetOf(headers)); } -} +} \ No newline at end of file From 5d3b4839997b13b806dd485fd282a978c6b0fb59 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Mon, 22 Jun 2026 19:51:29 +0000 Subject: [PATCH 14/79] Support AOT and trimming compliance in SystemJson serializer and Learning Saga Persister (#7829) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Make SystemJson serializer AOT and trimming friendly Apply the same pattern used in NServiceBus.Persistence.NonDurable's SagaEntry: resolve JsonTypeInfo via TypeInfoResolver and use the typed overloads when available, falling back to reflection-based serialization only when JsonSerializer.IsReflectionEnabledByDefault is true. Suppress trimming/AOT warnings on the reflection fallback methods. * Approve removal of SystemJson serializer trimming warnings The JsonMessageSerializer trimming warnings (IL2026 for both Serialize and Deserialize) are no longer present after making the serializer AOT and trimming friendly. * Add SerializerOptions property to SagaManifest * Pass SerializerOptions through SagaManifestCollection * Make Learning Saga Persister AOT and trimming friendly Apply the same pattern used in the SystemJson serializer: resolve JsonTypeInfo via TypeInfoResolver and use the typed overloads when available, falling back to reflection-based serialization only when JsonSerializer.IsReflectionEnabledByDefault is true. Suppress trimming/AOT warnings on the reflection fallback methods. * Add SagaSerializerOptions extension method for Learning Saga Persister Allows users to configure custom JsonSerializerOptions for the learning saga persister via the persistence configuration API. * Approve removal of Learning Saga Persister trimming warnings and new public API The SagaStorageFile trimming warnings (IL2026 for both SerializeAsync and DeserializeAsync) are no longer present. The new SagaSerializerOptions extension method is added to the public API. * Guard JsonStringEnumConverter behind IsReflectionEnabledByDefault Only include the converter in non-AOT mode. Suppress the IL3050 AOT warning on the factory method since the converter is only created when reflection serialization is available. * Extract ResolveTypeInfo into reusable extension method Consolidates the duplicated ResolveTypeInfo logic from both JsonMessageSerializer and SagaStorageFile into a single extension method on JsonSerializerOptions? in the Serializers folder. * Pass default options for persistence tests * Nullable options * Fix flaky test that fails in Debug mode * Fix warning * Update src/NServiceBus.PersistenceTests/PersistenceTestsConfiguration.cs Co-authored-by: Andreas Öhlund --------- Co-authored-by: Daniel Marbach Co-authored-by: Andreas Öhlund --- ...IApprovals.ApproveNServiceBus.approved.txt | 1 + ...s.ApproveTrimmabilityWarnings.approved.txt | 8 -- src/NServiceBus.Core.Tests/StandardsTests.cs | 26 +++---- .../SagaPersister/LearningSagaPersistence.cs | 23 +++++- ...ingSagaPersisterConfigurationExtensions.cs | 14 ++++ .../Learning/SagaPersister/SagaManifest.cs | 2 + .../SagaPersister/SagaManifestCollection.cs | 7 +- .../Learning/SagaPersister/SagaStorageFile.cs | 73 +++++++++++++++---- .../JsonSerializerOptionsExtensions.cs | 24 ++++++ .../SystemJson/JsonMessageSerializer.cs | 49 ++++++++++++- 10 files changed, 182 insertions(+), 45 deletions(-) create mode 100644 src/NServiceBus.Core/Serializers/JsonSerializerOptionsExtensions.cs diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index 7b94f65396c..6a55b5f799b 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -645,6 +645,7 @@ namespace NServiceBus public class LearningPersistence : NServiceBus.Persistence.PersistenceDefinition, NServiceBus.Persistence.IPersistenceDefinitionFactory { } public static class LearningSagaPersisterConfigurationExtensions { + public static void SagaSerializerOptions(this NServiceBus.PersistenceExtensions persistenceExtensions, System.Text.Json.JsonSerializerOptions options) { } public static void SagaStorageDirectory(this NServiceBus.PersistenceExtensions persistenceExtensions, string path) { } } public class LearningTransport : NServiceBus.Transport.TransportDefinition diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt index 68fef70644c..1fb90478f9d 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt @@ -19,10 +19,6 @@ src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriter.cs src/NServiceBus.Core/Hosting/StartupDiagnostics/JsonPrettyPrinter.cs IL2026: Using member 'System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. -src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaStorageFile.cs - IL2026: Using member 'System.Text.Json.JsonSerializer.DeserializeAsync(Stream, JsonSerializerOptions, CancellationToken)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. - IL2026: Using member 'System.Text.Json.JsonSerializer.SerializeAsync(Stream, Object, Type, JsonSerializerOptions, CancellationToken)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. - src/NServiceBus.Core/Pipeline/RegisterStep.cs IL2072: 'instanceType' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicConstructors' in call to 'Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateInstance(IServiceProvider, Type, params Object[])'. The return value of method 'NServiceBus.Pipeline.RegisterStep.BehaviorType.get' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. @@ -45,10 +41,6 @@ src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/NamespacePublisherSource src/NServiceBus.Core/Routing/NamespaceRouteSource.cs IL2026: Using member 'System.Reflection.Assembly.GetTypes()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Types might be removed. -src/NServiceBus.Core/Serializers/SystemJson/JsonMessageSerializer.cs - IL2026: Using member 'System.Text.Json.JsonSerializer.Deserialize(Stream, Type, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. - IL2026: Using member 'System.Text.Json.JsonSerializer.Serialize(Stream, TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. - src/NServiceBus.Core/Transports/Learning/LearningTransportDispatcher.cs IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 'messageType' of method 'NServiceBus.LearningTransportDispatcher.GetPotentialEventTypes(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. diff --git a/src/NServiceBus.Core.Tests/StandardsTests.cs b/src/NServiceBus.Core.Tests/StandardsTests.cs index 192b967e1f9..1147b05df0e 100644 --- a/src/NServiceBus.Core.Tests/StandardsTests.cs +++ b/src/NServiceBus.Core.Tests/StandardsTests.cs @@ -51,15 +51,12 @@ public void NonPublicShouldHaveSimpleNamespace() } } - static bool IsCompilerGenerated(Type x) - { - return Attribute.IsDefined(x, typeof(CompilerGeneratedAttribute), false); - } + static bool IsCompilerGenerated(Type x) => Attribute.IsDefined(x, typeof(CompilerGeneratedAttribute), false); [Test] public void LoggersShouldBeStaticField() { - foreach (var type in typeof(EndpointCreator).Assembly.GetTypes()) + foreach (var type in typeof(EndpointCreator).Assembly.GetTypes().Where(t => !IsCompilerGenerated(t))) { // Logging namespace contains special adapter types that are not expected to have static loggers if (type.Namespace == "NServiceBus.Logging") @@ -100,20 +97,15 @@ public void VerifyAttributesAreSealed() } } - static IEnumerable GetBehaviors() - { - return typeof(EndpointCreator).Assembly.GetTypes() + static IEnumerable GetBehaviors() => + typeof(EndpointCreator).Assembly.GetTypes() .Where(type => type.GetInterfaces().Any(face => face.Name == nameof(NServiceBus.Pipeline.IBehavior)) && !type.IsAbstract && !type.IsGenericType); - } - static IEnumerable GetFeatures() - { - return typeof(EndpointCreator).Assembly.GetTypes() + + static IEnumerable GetFeatures() => + typeof(EndpointCreator).Assembly.GetTypes() .Where(type => typeof(Feature).IsAssignableFrom(type) && type.IsPublic && !type.IsAbstract); - } - static IEnumerable GetAttributeTypes() - { - return typeof(EndpointCreator).Assembly.GetTypes() + static IEnumerable GetAttributeTypes() => + typeof(EndpointCreator).Assembly.GetTypes() .Where(type => typeof(Attribute).IsAssignableFrom(type)); - } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/LearningSagaPersistence.cs b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/LearningSagaPersistence.cs index 2b1f974cb14..5a164e15345 100644 --- a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/LearningSagaPersistence.cs +++ b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/LearningSagaPersistence.cs @@ -3,7 +3,10 @@ namespace NServiceBus.Features; using System; +using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.Extensions.DependencyInjection; using NServiceBus.Sagas; @@ -26,12 +29,30 @@ public LearningSagaPersistence() protected override void Setup(FeatureConfigurationContext context) { var storageLocation = context.Settings.Get(StorageLocationKey); + var serializerOptions = context.Settings.GetOrDefault(SerializerOptionsKey) ?? GetDefaultOptions(); var allSagas = context.Settings.Get(); - context.Services.AddSingleton(new SagaManifestCollection(allSagas, storageLocation, sagaName => sagaName.Replace("+", ""))); + context.Services.AddSingleton(new SagaManifestCollection(allSagas, storageLocation, sagaName => sagaName.Replace("+", ""), serializerOptions)); context.Services.AddSingleton(); } + static JsonSerializerOptions GetDefaultOptions() + { + var options = new JsonSerializerOptions(); + if (JsonSerializer.IsReflectionEnabledByDefault) + { + options.Converters.Add(CreateDefaultConverter()); + } + return options; + } + + [UnconditionalSuppressMessage( + "AOT", + "IL3050", + Justification = "Only used when System.Text.Json reflection is enabled.")] + static JsonStringEnumConverter CreateDefaultConverter() => new(); + internal static readonly string StorageLocationKey = "LearningSagaPersistence.StorageLocation"; + internal static readonly string SerializerOptionsKey = "LearningSagaPersistence.SerializerOptions"; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/LearningSagaPersisterConfigurationExtensions.cs b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/LearningSagaPersisterConfigurationExtensions.cs index dd0c9579d31..1628ae77f84 100644 --- a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/LearningSagaPersisterConfigurationExtensions.cs +++ b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/LearningSagaPersisterConfigurationExtensions.cs @@ -3,6 +3,7 @@ namespace NServiceBus; using System; +using System.Text.Json; using Features; /// @@ -22,4 +23,17 @@ public static void SagaStorageDirectory(this PersistenceExtensions + /// Configures the to use for serializing saga data. + /// + /// The persistence extensions to extend. + /// The to use. + public static void SagaSerializerOptions(this PersistenceExtensions persistenceExtensions, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(persistenceExtensions); + ArgumentNullException.ThrowIfNull(options); + + persistenceExtensions.Settings.Set(LearningSagaPersistence.SerializerOptionsKey, options); + } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaManifest.cs b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaManifest.cs index 30dc6474a77..996665ca547 100644 --- a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaManifest.cs +++ b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaManifest.cs @@ -4,11 +4,13 @@ namespace NServiceBus; using System; using System.IO; +using System.Text.Json; class SagaManifest { public required string StorageDirectory { get; init; } public required Type SagaEntityType { get; init; } + public required JsonSerializerOptions SerializerOptions { get; init; } public string GetFilePath(Guid sagaId) => Path.Combine(StorageDirectory, sagaId + ".json"); } \ No newline at end of file diff --git a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaManifestCollection.cs b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaManifestCollection.cs index beccb2f38db..93d14296801 100644 --- a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaManifestCollection.cs +++ b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaManifestCollection.cs @@ -5,12 +5,14 @@ namespace NServiceBus; using System; using System.Collections.Generic; using System.IO; +using System.Text.Json; using Sagas; class SagaManifestCollection { - public SagaManifestCollection(SagaMetadataCollection sagas, string storageLocation, Func sagaNameConverter) + public SagaManifestCollection(SagaMetadataCollection sagas, string storageLocation, Func sagaNameConverter, JsonSerializerOptions? serializerOptions = null) { + serializerOptions ??= new JsonSerializerOptions(); foreach (var metadata in sagas) { var sagaStorageDir = Path.Combine(storageLocation, sagaNameConverter(metadata.SagaType.FullName!)); @@ -23,7 +25,8 @@ public SagaManifestCollection(SagaMetadataCollection sagas, string storageLocati var manifest = new SagaManifest { StorageDirectory = sagaStorageDir, - SagaEntityType = metadata.SagaEntityType + SagaEntityType = metadata.SagaEntityType, + SerializerOptions = serializerOptions }; sagaManifests[metadata.SagaEntityType] = manifest; diff --git a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaStorageFile.cs b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaStorageFile.cs index 253df6a8213..f4ae89986f9 100644 --- a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaStorageFile.cs +++ b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaStorageFile.cs @@ -3,15 +3,20 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text.Json; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using System.Threading; using System.Threading.Tasks; class SagaStorageFile : IDisposable, IAsyncDisposable { - SagaStorageFile(FileStream fileStream) => this.fileStream = fileStream; + SagaStorageFile(FileStream fileStream, JsonSerializerOptions options) + { + this.fileStream = fileStream; + this.options = options; + } public void Dispose() { @@ -58,17 +63,17 @@ public async ValueTask DisposeAsync() return noSagaFoundResult; } - return OpenWithRetryOnConcurrency(filePath, FileMode.Open, cancellationToken)!; + return OpenWithRetryOnConcurrency(filePath, FileMode.Open, manifest.SerializerOptions, cancellationToken)!; } public static Task Create(Guid sagaId, SagaManifest manifest, CancellationToken cancellationToken = default) { var filePath = manifest.GetFilePath(sagaId); - return OpenWithRetryOnConcurrency(filePath, FileMode.CreateNew, cancellationToken); + return OpenWithRetryOnConcurrency(filePath, FileMode.CreateNew, manifest.SerializerOptions, cancellationToken); } - static async Task OpenWithRetryOnConcurrency(string filePath, FileMode fileAccess, CancellationToken cancellationToken) + static async Task OpenWithRetryOnConcurrency(string filePath, FileMode fileAccess, JsonSerializerOptions options, CancellationToken cancellationToken) { var numRetries = 0; @@ -78,7 +83,7 @@ static async Task OpenWithRetryOnConcurrency(string filePath, F try { - return new SagaStorageFile(new FileStream(filePath, fileAccess, FileAccess.ReadWrite, FileShare.None, DefaultBufferSize, FileOptions.Asynchronous)); + return new SagaStorageFile(new FileStream(filePath, fileAccess, FileAccess.ReadWrite, FileShare.None, DefaultBufferSize, FileOptions.Asynchronous), options); } catch (IOException) { @@ -101,8 +106,19 @@ public async Task Write(IContainSagaData sagaData, CancellationToken cancellatio ObjectDisposedException.ThrowIf(fileStream is null, this); fileStream.Position = 0; - await JsonSerializer.SerializeAsync(fileStream, sagaData, sagaData.GetType(), Options, cancellationToken) - .ConfigureAwait(false); + + var sagaDataType = sagaData.GetType(); + var typeInfo = options.ResolveTypeInfo(sagaDataType); + if (typeInfo is not null) + { + await JsonSerializer.SerializeAsync(fileStream, sagaData, typeInfo, cancellationToken) + .ConfigureAwait(false); + } + else + { + await SerializeWithReflectionAsync(fileStream, sagaData, sagaDataType, options, cancellationToken) + .ConfigureAwait(false); + } // Because the file is opened in ReadWrite mode, leftover content from last write // could be left behind if the new content is shorter. @@ -115,17 +131,46 @@ await JsonSerializer.SerializeAsync(fileStream, sagaData, sagaData.GetType(), Op { ObjectDisposedException.ThrowIf(fileStream is null, this); - return JsonSerializer.DeserializeAsync(fileStream, Options, cancellationToken); + var typeInfo = options.ResolveTypeInfo(typeof(TSagaData)); + if (typeInfo is not null) + { + return ReadWithTypeInfoAsync(fileStream, typeInfo, cancellationToken); + } + else + { + return DeserializeWithReflectionAsync(fileStream, options, cancellationToken); + } } + static async ValueTask ReadWithTypeInfoAsync(Stream stream, JsonTypeInfo typeInfo, CancellationToken cancellationToken) where TSagaData : class + => (TSagaData?)await JsonSerializer.DeserializeAsync(stream, typeInfo, cancellationToken).ConfigureAwait(false); + + [UnconditionalSuppressMessage( + "Trimming", + "IL2026", + Justification = "Only called when System.Text.Json reflection serialization is enabled.")] + [UnconditionalSuppressMessage( + "AOT", + "IL3050", + Justification = "Only called when System.Text.Json reflection serialization is enabled.")] + static Task SerializeWithReflectionAsync(Stream stream, object value, Type inputType, JsonSerializerOptions options, CancellationToken cancellationToken) + => JsonSerializer.SerializeAsync(stream, value, inputType, options, cancellationToken); + + [UnconditionalSuppressMessage( + "Trimming", + "IL2026", + Justification = "Only called when System.Text.Json reflection serialization is enabled.")] + [UnconditionalSuppressMessage( + "AOT", + "IL3050", + Justification = "Only called when System.Text.Json reflection serialization is enabled.")] + static async ValueTask DeserializeWithReflectionAsync(Stream stream, JsonSerializerOptions options, CancellationToken cancellationToken) where TSagaData : class + => (TSagaData?)await JsonSerializer.DeserializeAsync(stream, typeof(TSagaData), options, cancellationToken).ConfigureAwait(false); + FileStream? fileStream; + readonly JsonSerializerOptions options; bool isCompleted; const int DefaultBufferSize = 4096; static readonly Task noSagaFoundResult = Task.FromResult(null); - - static readonly JsonSerializerOptions Options = new() - { - Converters = { new JsonStringEnumConverter() } - }; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Serializers/JsonSerializerOptionsExtensions.cs b/src/NServiceBus.Core/Serializers/JsonSerializerOptionsExtensions.cs new file mode 100644 index 00000000000..9df21b619ee --- /dev/null +++ b/src/NServiceBus.Core/Serializers/JsonSerializerOptionsExtensions.cs @@ -0,0 +1,24 @@ +#nullable enable + +namespace NServiceBus; + +using System; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; + +static class JsonSerializerOptionsExtensions +{ + extension(JsonSerializerOptions? options) + { + public JsonTypeInfo? ResolveTypeInfo(Type runtimeType) + { + var typeInfo = options?.TypeInfoResolver?.GetTypeInfo(runtimeType, options); + if (typeInfo is not null) + { + return typeInfo; + } + + return JsonSerializer.IsReflectionEnabledByDefault ? null : throw new InvalidOperationException($"No JSON metadata was found for '{runtimeType.FullName}'."); + } + } +} diff --git a/src/NServiceBus.Core/Serializers/SystemJson/JsonMessageSerializer.cs b/src/NServiceBus.Core/Serializers/SystemJson/JsonMessageSerializer.cs index b4c998de5ce..6d66c49504b 100644 --- a/src/NServiceBus.Core/Serializers/SystemJson/JsonMessageSerializer.cs +++ b/src/NServiceBus.Core/Serializers/SystemJson/JsonMessageSerializer.cs @@ -3,6 +3,7 @@ namespace NServiceBus.Serializers.SystemJson; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text.Json; @@ -27,7 +28,18 @@ public JsonMessageSerializer(JsonSerializerOptions? serializerOptions, string co public string ContentType { get; } public void Serialize(object message, Stream stream) - => JsonSerializer.Serialize(stream, message, serializerOptions); + { + var messageType = message.GetType(); + var typeInfo = serializerOptions.ResolveTypeInfo(messageType); + if (typeInfo is not null) + { + JsonSerializer.Serialize(stream, message, typeInfo); + } + else + { + SerializeWithReflection(message, stream, messageType, serializerOptions); + } + } public object[] Deserialize(ReadOnlyMemory body, IList? messageTypes = null) { @@ -49,10 +61,41 @@ public object[] Deserialize(ReadOnlyMemory body, IList? messageTypes object Deserialize(ReadOnlyMemory body, Type type) { var actualType = GetMappedType(type); - using var stream = new ReadOnlyStream(body); - return JsonSerializer.Deserialize(stream, actualType, serializerOptions)!; + var typeInfo = serializerOptions.ResolveTypeInfo(actualType); + if (typeInfo is not null) + { + using var stream = new ReadOnlyStream(body); + return JsonSerializer.Deserialize(stream, typeInfo)!; + } + else + { + using var stream = new ReadOnlyStream(body); + return DeserializeWithReflection(stream, actualType, serializerOptions)!; + } } + [UnconditionalSuppressMessage( + "Trimming", + "IL2026", + Justification = "Only called when System.Text.Json reflection serialization is enabled.")] + [UnconditionalSuppressMessage( + "AOT", + "IL3050", + Justification = "Only called when System.Text.Json reflection serialization is enabled.")] + static void SerializeWithReflection(object message, Stream stream, Type messageType, JsonSerializerOptions? options) + => JsonSerializer.Serialize(stream, message, messageType, options); + + [UnconditionalSuppressMessage( + "Trimming", + "IL2026", + Justification = "Only called when System.Text.Json reflection serialization is enabled.")] + [UnconditionalSuppressMessage( + "AOT", + "IL3050", + Justification = "Only called when System.Text.Json reflection serialization is enabled.")] + static object DeserializeWithReflection(Stream stream, Type type, JsonSerializerOptions? options) + => JsonSerializer.Deserialize(stream, type, options)!; + static IEnumerable FindRootTypes(IEnumerable messageTypesToDeserialize) { Type? currentRoot = null; From 11d7707424ff158de437cbdfcee232d435180e09 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 10:36:45 +0000 Subject: [PATCH 15/79] Update dependency Microsoft.NET.Test.Sdk to 18.7.0 (#7830) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- .../NServiceBus.AcceptanceTests.csproj | 2 +- .../NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj | 2 +- src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj | 2 +- .../NServiceBus.Learning.AcceptanceTests.csproj | 2 +- .../NServiceBus.PersistenceTests.csproj | 2 +- .../NServiceBus.TransportTests.csproj | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj b/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj index ea546b1c257..6dc98b74939 100644 --- a/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj +++ b/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj index 7a8e0c540d3..55d8e78f6cc 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj index 48d5617553e..35949fc9c13 100644 --- a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj +++ b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj b/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj index 585c481b424..80072fa2d28 100644 --- a/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj +++ b/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj b/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj index 7e091be3153..ad99551c205 100644 --- a/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj +++ b/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj b/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj index 99bd27b1320..aaaa392d21e 100644 --- a/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj +++ b/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj @@ -10,7 +10,7 @@ - + From 1104eeb9407bc6726d5484fe1955dbdbfd284a72 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Fri, 26 Jun 2026 13:46:09 +0000 Subject: [PATCH 16/79] Resolve globally registered endpoint-keyed services inside keyed endpoints (#7833) * Cleanup usings * Add acceptance test to reproduce the most complex scenario * Properly fallback to global keyed services where necessary and update test cases * Use "is" * Refactor service lookup methods to use ConcurrentDictionary for improved performance --------- Co-authored-by: Daniel Marbach --- ...ed_async_disposables_externally_managed.cs | 1 - ...solving_all_services_within_an_endpoint.cs | 1 - ...olving_endpoint_specific_keyed_services.cs | 97 +++++ .../KeyedServiceCollectionAdapterTests.cs | 175 +++------ .../KeyedServiceProviderAdapterTests.cs | 368 ++++++++++++++++++ .../KeyedServiceCollectionAdapter.cs | 50 +-- .../KeyedServiceProviderAdapter.cs | 123 ++++-- 7 files changed, 634 insertions(+), 181 deletions(-) create mode 100644 src/NServiceBus.AcceptanceTests/Core/DependencyInjection/When_resolving_endpoint_specific_keyed_services.cs diff --git a/src/NServiceBus.AcceptanceTests/Core/DependencyInjection/When_registering_keyed_async_disposables_externally_managed.cs b/src/NServiceBus.AcceptanceTests/Core/DependencyInjection/When_registering_keyed_async_disposables_externally_managed.cs index a97e5e75b47..7c089037166 100644 --- a/src/NServiceBus.AcceptanceTests/Core/DependencyInjection/When_registering_keyed_async_disposables_externally_managed.cs +++ b/src/NServiceBus.AcceptanceTests/Core/DependencyInjection/When_registering_keyed_async_disposables_externally_managed.cs @@ -1,7 +1,6 @@ namespace NServiceBus.AcceptanceTests.Core.DependencyInjection; using System; -using System.Threading; using System.Threading.Tasks; using AcceptanceTesting; using EndpointTemplates; diff --git a/src/NServiceBus.AcceptanceTests/Core/DependencyInjection/When_resolving_all_services_within_an_endpoint.cs b/src/NServiceBus.AcceptanceTests/Core/DependencyInjection/When_resolving_all_services_within_an_endpoint.cs index aca72d3467d..d33c33a7b10 100644 --- a/src/NServiceBus.AcceptanceTests/Core/DependencyInjection/When_resolving_all_services_within_an_endpoint.cs +++ b/src/NServiceBus.AcceptanceTests/Core/DependencyInjection/When_resolving_all_services_within_an_endpoint.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Threading.Tasks; using AcceptanceTesting; -using AcceptanceTesting.Support; using EndpointTemplates; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; diff --git a/src/NServiceBus.AcceptanceTests/Core/DependencyInjection/When_resolving_endpoint_specific_keyed_services.cs b/src/NServiceBus.AcceptanceTests/Core/DependencyInjection/When_resolving_endpoint_specific_keyed_services.cs new file mode 100644 index 00000000000..4289897d0bd --- /dev/null +++ b/src/NServiceBus.AcceptanceTests/Core/DependencyInjection/When_resolving_endpoint_specific_keyed_services.cs @@ -0,0 +1,97 @@ +namespace NServiceBus.AcceptanceTests.Core.DependencyInjection; + +using System.Threading.Tasks; +using AcceptanceTesting; +using EndpointTemplates; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; + +public class When_resolving_endpoint_specific_keyed_services : NServiceBusAcceptanceTest +{ + // When registering keyed services globally that match endpoint-specific keys, each endpoint should resolve its own + // global keyed service without having to use FromKeyedServices. Local keyed services using the same logical key + // should still be isolated per endpoint. + [Test] + public async Task Should_be_possible() + { + var result = await Scenario.Define() + .WithServices(static services => + { + var firstEndpointName = AcceptanceTesting.Customization.Conventions.EndpointNamingConvention(typeof(FirstEndpointUsingGlobalKeyedService)); + var secondEndpointName = AcceptanceTesting.Customization.Conventions.EndpointNamingConvention(typeof(SecondEndpointUsingGlobalKeyedService)); + + services.AddKeyedSingleton($"{firstEndpointName}1"); + services.AddKeyedSingleton($"{secondEndpointName}2"); + }) + .WithEndpoint(b => b + .Services(static services => services.AddKeyedSingleton("local")) + .When((session, _) => session.SendLocal(new SomeMessage()))) + .WithEndpoint(b => b + .Services(static services => services.AddKeyedSingleton("local")) + .When((session, _) => session.SendLocal(new SomeMessage()))) + .Run(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result.FirstComponent, Is.InstanceOf()); + Assert.That(result.FirstLocalComponent, Is.InstanceOf()); + Assert.That(result.SecondComponent, Is.InstanceOf()); + Assert.That(result.SecondLocalComponent, Is.InstanceOf()); + } + } + + public class Context : ScenarioContext + { + public IMyComponent FirstComponent { get; set; } + public ILocalComponent FirstLocalComponent { get; set; } + public IMyComponent SecondComponent { get; set; } + public ILocalComponent SecondLocalComponent { get; set; } + + public void MaybeCompleted() => MarkAsCompleted(FirstComponent is not null && FirstLocalComponent is not null && SecondComponent is not null && SecondLocalComponent is not null); + } + + public interface IMyComponent; + public interface ILocalComponent; + + public class FirstEndpointComponent : IMyComponent; + public class FirstLocalComponent : ILocalComponent; + + public class SecondEndpointComponent : IMyComponent; + public class SecondLocalComponent : ILocalComponent; + + public class FirstEndpointUsingGlobalKeyedService : EndpointConfigurationBuilder + { + public FirstEndpointUsingGlobalKeyedService() => EndpointSetup(); + + [Handler] + public class SomeMessageHandler(Context testContext, IMyComponent component, [FromKeyedServices("local")] ILocalComponent localComponent) : IHandleMessages + { + public Task Handle(SomeMessage message, IMessageHandlerContext context) + { + testContext.FirstComponent = component; + testContext.FirstLocalComponent = localComponent; + testContext.MaybeCompleted(); + return Task.CompletedTask; + } + } + } + + public class SecondEndpointUsingGlobalKeyedService : EndpointConfigurationBuilder + { + public SecondEndpointUsingGlobalKeyedService() => EndpointSetup(); + + [Handler] + public class SomeMessageHandler(Context testContext, IMyComponent component, [FromKeyedServices("local")] ILocalComponent localComponent) : IHandleMessages + { + public Task Handle(SomeMessage message, IMessageHandlerContext context) + { + testContext.SecondComponent = component; + testContext.SecondLocalComponent = localComponent; + testContext.MaybeCompleted(); + return Task.CompletedTask; + } + } + } + + public class SomeMessage : ICommand; +} diff --git a/src/NServiceBus.Core.Tests/Hosting/KeyedServiceCollectionAdapterTests.cs b/src/NServiceBus.Core.Tests/Hosting/KeyedServiceCollectionAdapterTests.cs index 6d5f7952488..19c63815ba4 100644 --- a/src/NServiceBus.Core.Tests/Hosting/KeyedServiceCollectionAdapterTests.cs +++ b/src/NServiceBus.Core.Tests/Hosting/KeyedServiceCollectionAdapterTests.cs @@ -9,8 +9,6 @@ namespace NServiceBus.Core.Tests.Host; using Microsoft.Extensions.DependencyInjection.Extensions; using NUnit.Framework; -#pragma warning disable IDE0028 // Collection initialization can be simplified — test clarity requires explicit Add calls - [TestFixture] public class KeyedServiceCollectionAdapterTests { @@ -413,20 +411,6 @@ public void Remove_should_throw_on_null() Assert.Throws(() => adapter.Remove(null!)); } - [Test] - public void Remove_should_decrement_service_type_count() - { - var inner = new ServiceCollection(); - var adapter = new KeyedServiceCollectionAdapter(inner, "ep"); - - var descriptor = new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Singleton); - adapter.Add(descriptor); - - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.True); - adapter.Remove(descriptor); - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.False); - } - [Test] public void RemoveAt_should_remove_descriptor_at_index() { @@ -449,28 +433,15 @@ public void RemoveAt_should_remove_descriptor_at_index() Assert.That(inner[0].ServiceType, Is.EqualTo(typeof(IBar))); } - [Test] - public void RemoveAt_should_decrement_service_type_count() - { - var inner = new ServiceCollection(); - var adapter = new KeyedServiceCollectionAdapter(inner, "ep"); - - var d1 = new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Transient); - adapter.Add(d1); - - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.True); - adapter.RemoveAt(0); - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.False); - } - [Test] public void Clear_should_remove_all_from_adapter_and_inner() { var inner = new ServiceCollection(); - var adapter = new KeyedServiceCollectionAdapter(inner, "ep"); - - adapter.Add(new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Transient)); - adapter.Add(new ServiceDescriptor(typeof(IBar), typeof(Bar), ServiceLifetime.Scoped)); + var adapter = new KeyedServiceCollectionAdapter(inner, "ep") + { + new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Transient), + new ServiceDescriptor(typeof(IBar), typeof(Bar), ServiceLifetime.Scoped) + }; adapter.Clear(); @@ -481,18 +452,6 @@ public void Clear_should_remove_all_from_adapter_and_inner() } } - [Test] - public void Clear_should_reset_service_type_counts() - { - var adapter = CreateAdapter("ep"); - - adapter.Add(new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Transient)); - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.True); - - adapter.Clear(); - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.False); - } - [Test] public void CopyTo_should_copy_original_descriptors() { @@ -515,78 +474,79 @@ public void CopyTo_should_copy_original_descriptors() } [Test] - public void ContainsService_should_return_true_for_registered_type() + public void ContainsLocalService_should_distinguish_keyed_and_non_keyed_registrations() { var adapter = CreateAdapter(); adapter.Add(new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Transient)); + adapter.Add(new ServiceDescriptor(typeof(IFoo), "sub", typeof(Foo2), ServiceLifetime.Transient)); - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.True); + using (Assert.EnterMultipleScope()) + { + Assert.That(adapter.ContainsLocalService(typeof(IFoo), null), Is.True); + Assert.That(adapter.ContainsLocalService(typeof(IFoo), "sub"), Is.True); + Assert.That(adapter.ContainsLocalService(typeof(IFoo), "other"), Is.False); + } } [Test] - public void ContainsService_should_return_false_for_unregistered_type() + public void ContainsLocalService_should_match_closed_generic_request_for_open_generic_registration() { var adapter = CreateAdapter(); - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.False); + adapter.Add(new ServiceDescriptor(typeof(IGeneric<>), "sub", typeof(GenericFoo<>), ServiceLifetime.Transient)); + + Assert.That(adapter.ContainsLocalService(typeof(IGeneric), "sub"), Is.True); } [Test] - public void ContainsService_should_return_true_for_open_generic_when_closed_registered() + public void ContainsLocalService_should_return_false_after_remove() { var adapter = CreateAdapter(); - adapter.Add(new ServiceDescriptor(typeof(IGeneric<>), typeof(GenericFoo<>), ServiceLifetime.Transient)); + var descriptor = new ServiceDescriptor(typeof(IFoo), "sub", typeof(Foo), ServiceLifetime.Transient); + adapter.Add(descriptor); + + adapter.Remove(descriptor); - Assert.That(adapter.ContainsService(typeof(IGeneric)), Is.True); + Assert.That(adapter.ContainsLocalService(typeof(IFoo), "sub"), Is.False); } [Test] - public void ContainsService_should_return_false_for_closed_when_only_other_closed_registered() + public void ContainsLocalService_should_return_false_after_remove_at() { var adapter = CreateAdapter(); - adapter.Add(new ServiceDescriptor(typeof(IGeneric), typeof(GenericFoo), ServiceLifetime.Transient)); + adapter.Add(new ServiceDescriptor(typeof(IFoo), "sub", typeof(Foo), ServiceLifetime.Transient)); - Assert.That(adapter.ContainsService(typeof(IGeneric)), Is.False); - } + adapter.RemoveAt(0); - [Test] - public void ContainsService_should_throw_on_null() - { - var adapter = CreateAdapter(); - Assert.Throws(() => adapter.ContainsService(null!)); + Assert.That(adapter.ContainsLocalService(typeof(IFoo), "sub"), Is.False); } [Test] - public void Multiple_same_type_should_increment_count() + public void ContainsLocalService_should_return_false_after_clear() { var adapter = CreateAdapter(); - adapter.Add(new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Transient)); - adapter.Add(new ServiceDescriptor(typeof(IFoo), typeof(Foo2), ServiceLifetime.Transient)); + adapter.Add(new ServiceDescriptor(typeof(IFoo), "sub", typeof(Foo2), ServiceLifetime.Transient)); + + adapter.Clear(); - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.True); + using (Assert.EnterMultipleScope()) + { + Assert.That(adapter.ContainsLocalService(typeof(IFoo), null), Is.False); + Assert.That(adapter.ContainsLocalService(typeof(IFoo), "sub"), Is.False); + } } [Test] - public void Removing_all_of_same_type_should_remove_service_type() + public void GetLocalServiceKey_should_project_to_endpoint_composite_key() { - var inner = new ServiceCollection(); - var adapter = new KeyedServiceCollectionAdapter(inner, "ep"); - - var d1 = new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Transient); - var d2 = new ServiceDescriptor(typeof(IFoo), typeof(Foo2), ServiceLifetime.Transient); - adapter.Add(d1); - adapter.Add(d2); - - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.True); + var adapter = CreateAdapter("ep"); - adapter.Remove(d1); - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.True); + var key = adapter.GetLocalServiceKey("sub"); - adapter.Remove(d2); using (Assert.EnterMultipleScope()) { - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.False); - Assert.That(inner, Is.Empty); + Assert.That(key.BaseKey, Is.EqualTo("ep")); + Assert.That(key.ServiceKey, Is.EqualTo("sub")); } } @@ -594,9 +554,11 @@ public void Removing_all_of_same_type_should_remove_service_type() public void Add_should_augment_non_keyed_descriptor_to_keyed_in_inner() { var inner = new ServiceCollection(); - var adapter = new KeyedServiceCollectionAdapter(inner, "ep"); - adapter.Add(new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Singleton)); + _ = new KeyedServiceCollectionAdapter(inner, "ep") + { + new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Singleton) + }; Assert.That(inner[0].ServiceKey, Is.InstanceOf()); var key = (KeyedServiceKey)inner[0].ServiceKey!; @@ -611,9 +573,11 @@ public void Add_should_augment_non_keyed_descriptor_to_keyed_in_inner() public void Add_should_augment_keyed_descriptor_with_composite_key_in_inner() { var inner = new ServiceCollection(); - var adapter = new KeyedServiceCollectionAdapter(inner, "ep"); - adapter.Add(new ServiceDescriptor(typeof(IFoo), "sub", typeof(Foo), ServiceLifetime.Singleton)); + _ = new KeyedServiceCollectionAdapter(inner, "ep") + { + new ServiceDescriptor(typeof(IFoo), "sub", typeof(Foo), ServiceLifetime.Singleton) + }; Assert.That(inner[0].ServiceKey, Is.InstanceOf()); var key = (KeyedServiceKey)inner[0].ServiceKey!; @@ -667,10 +631,11 @@ public void Remove_should_sync_with_inner_collection() public void Clear_should_remove_all_from_inner() { var inner = new ServiceCollection(); - var adapter = new KeyedServiceCollectionAdapter(inner, "ep"); - - adapter.Add(new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Singleton)); - adapter.Add(new ServiceDescriptor(typeof(IBar), typeof(Bar), ServiceLifetime.Scoped)); + var adapter = new KeyedServiceCollectionAdapter(inner, "ep") + { + new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Singleton), + new ServiceDescriptor(typeof(IBar), typeof(Bar), ServiceLifetime.Scoped) + }; adapter.Clear(); @@ -823,36 +788,6 @@ public void Add_instance_descriptor_should_keep_instance_in_original() Assert.That(adapter[0].ImplementationInstance, Is.SameAs(instance)); } - [Test] - public void Service_type_counts_should_track_multiple_registrations() - { - var inner = new ServiceCollection(); - var adapter = new KeyedServiceCollectionAdapter(inner, "ep"); - - adapter.Add(new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Transient)); - adapter.Add(new ServiceDescriptor(typeof(IFoo), typeof(Foo2), ServiceLifetime.Transient)); - adapter.Add(new ServiceDescriptor(typeof(IFoo), typeof(Foo3), ServiceLifetime.Transient)); - - using (Assert.EnterMultipleScope()) - { - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.True); - Assert.That(inner, Has.Count.EqualTo(3)); - } - - adapter.Remove(adapter[0]); - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.True); - - adapter.Remove(adapter[0]); - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.True); - - adapter.Remove(adapter[0]); - using (Assert.EnterMultipleScope()) - { - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.False); - Assert.That(inner, Is.Empty); - } - } - interface IFoo; interface IBar; @@ -865,8 +800,6 @@ class Foo : IFoo; class Foo2 : IFoo; - class Foo3 : IFoo; - class Bar : IBar; class Baz : IBaz; diff --git a/src/NServiceBus.Core.Tests/Hosting/KeyedServiceProviderAdapterTests.cs b/src/NServiceBus.Core.Tests/Hosting/KeyedServiceProviderAdapterTests.cs index dbde1062a24..db850530c36 100644 --- a/src/NServiceBus.Core.Tests/Hosting/KeyedServiceProviderAdapterTests.cs +++ b/src/NServiceBus.Core.Tests/Hosting/KeyedServiceProviderAdapterTests.cs @@ -2,6 +2,9 @@ namespace NServiceBus.Core.Tests.Host; +using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; @@ -36,4 +39,369 @@ public async Task Async_dispose_should_not_dispose_underlying_service_provider() var scopeFactory = adapter.GetRequiredService(); Assert.DoesNotThrow(() => scopeFactory.CreateScope().Dispose()); } + + [Test] + public void GetRequiredService_should_resolve_root_service_registered_with_endpoint_key() + { + var services = new ServiceCollection(); + services.AddKeyedSingleton("endpoint"); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var component = adapter.GetRequiredService(typeof(IComponent)); + + Assert.That(component, Is.InstanceOf()); + } + + [Test] + public void GetService_should_resolve_root_service_registered_with_endpoint_key() + { + var services = new ServiceCollection(); + services.AddKeyedSingleton("endpoint"); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var component = adapter.GetService(typeof(IComponent)); + + Assert.That(component, Is.InstanceOf()); + } + + [Test] + public void IsKeyedService_should_return_true_for_local_keyed_service() + { + var services = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + keyedServices.AddKeyedSingleton("component"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var result = adapter.IsKeyedService(typeof(IComponent), "component"); + + Assert.That(result, Is.True); + } + + [Test] + public void IsKeyedService_should_return_true_for_root_keyed_service() + { + var services = new ServiceCollection(); + services.AddKeyedSingleton("component"); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var result = adapter.IsKeyedService(typeof(IComponent), "component"); + + Assert.That(result, Is.True); + } + + [Test] + public void IsKeyedService_should_use_root_keyed_service_probe() + { + var providerServices = new ServiceCollection(); + providerServices.AddKeyedSingleton("component"); + var adapterServices = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(adapterServices, "endpoint"); + using var rootProvider = providerServices.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var result = adapter.IsKeyedService(typeof(IComponent), "component"); + + Assert.That(result, Is.True); + } + + [Test] + public void IsKeyedService_should_return_false_when_service_is_not_available() + { + var services = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var result = adapter.IsKeyedService(typeof(IComponent), "component"); + + Assert.That(result, Is.False); + } + + [Test] + public void IsKeyedService_should_return_false_for_local_service_with_different_endpoint_key() + { + var services = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + keyedServices.AddKeyedSingleton("component"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var result = adapter.IsKeyedService(typeof(IComponent), new KeyedServiceKey("other-endpoint", "component")); + + Assert.That(result, Is.False); + } + + [Test] + public void IsService_should_return_true_for_local_endpoint_service() + { + var services = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + keyedServices.AddSingleton(); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var result = adapter.IsService(typeof(IComponent)); + + Assert.That(result, Is.True); + } + + [Test] + public void IsService_should_return_true_for_root_service_registered_with_endpoint_key() + { + var services = new ServiceCollection(); + services.AddKeyedSingleton("endpoint"); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var result = adapter.IsService(typeof(IComponent)); + + Assert.That(result, Is.True); + } + + [Test] + public void IsService_should_return_true_for_root_unkeyed_service() + { + var services = new ServiceCollection(); + services.AddSingleton(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var result = adapter.IsService(typeof(IComponent)); + + Assert.That(result, Is.True); + } + + [Test] + public void IsService_should_use_root_service_probe() + { + var providerServices = new ServiceCollection(); + providerServices.AddSingleton(); + var adapterServices = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(adapterServices, "endpoint"); + using var rootProvider = providerServices.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var result = adapter.IsService(typeof(IComponent)); + + Assert.That(result, Is.True); + } + + [Test] + public void IsService_should_return_false_when_service_is_not_available() + { + var services = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var result = adapter.IsService(typeof(IComponent)); + + Assert.That(result, Is.False); + } + + [Test] + public void IsService_should_return_true_for_adapter_services() + { + var services = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + using (Assert.EnterMultipleScope()) + { + Assert.That(adapter.IsService(typeof(IServiceProvider)), Is.True); + Assert.That(adapter.IsService(typeof(ISupportRequiredService)), Is.True); + Assert.That(adapter.IsService(typeof(IServiceProviderIsService)), Is.True); + Assert.That(adapter.IsService(typeof(IServiceProviderIsKeyedService)), Is.True); + Assert.That(adapter.IsService(typeof(IServiceScopeFactory)), Is.True); + Assert.That(adapter.IsService(typeof(IEnumerable)), Is.True); + } + } + + [Test] + public void GetService_and_GetRequiredService_should_return_adapter_services() + { + var services = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + using (Assert.EnterMultipleScope()) + { + Assert.That(adapter.GetService(typeof(IServiceProvider)), Is.SameAs(adapter)); + Assert.That(adapter.GetRequiredService(typeof(ISupportRequiredService)), Is.SameAs(adapter)); + Assert.That(adapter.GetService(typeof(IServiceProviderIsService)), Is.SameAs(adapter)); + Assert.That(adapter.GetRequiredService(typeof(IServiceProviderIsKeyedService)), Is.SameAs(adapter)); + Assert.That(adapter.GetService(typeof(IServiceScopeFactory)), Is.Not.Null); + Assert.That(adapter.GetRequiredService(typeof(IServiceScopeFactory)), Is.Not.Null); + } + } + + [Test] + public void GetRequiredKeyedService_should_prefer_local_keyed_service_for_matching_key() + { + var services = new ServiceCollection(); + services.AddKeyedSingleton("component"); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + keyedServices.AddKeyedSingleton("component"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var component = adapter.GetRequiredKeyedService(typeof(IComponent), "component"); + + Assert.That(component, Is.InstanceOf()); + } + + [Test] + public void GetKeyedService_should_prefer_local_keyed_service_for_matching_key() + { + var services = new ServiceCollection(); + services.AddKeyedSingleton("component"); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + keyedServices.AddKeyedSingleton("component"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var component = adapter.GetKeyedService(typeof(IComponent), "component"); + + Assert.That(component, Is.InstanceOf()); + } + + [Test] + public void GetRequiredKeyedService_should_fallback_to_root_keyed_service_when_local_key_does_not_match() + { + var services = new ServiceCollection(); + services.AddKeyedSingleton("root-component"); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + keyedServices.AddKeyedSingleton("local-component"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var component = adapter.GetRequiredKeyedService(typeof(IComponent), "root-component"); + + Assert.That(component, Is.InstanceOf()); + } + + [Test] + public void GetKeyedService_should_return_null_when_keyed_service_is_not_available() + { + var services = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var component = adapter.GetKeyedService(typeof(IComponent), "component"); + + Assert.That(component, Is.Null); + } + + [Test] + public void GetService_and_GetRequiredService_should_resolve_local_endpoint_enumerables() + { + var services = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + keyedServices.AddSingleton(); + keyedServices.AddSingleton(); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var components = ((IEnumerable)adapter.GetService(typeof(IEnumerable))!).ToList(); + var requiredComponents = ((IEnumerable)adapter.GetRequiredService(typeof(IEnumerable))).ToList(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(components, Has.Count.EqualTo(2)); + Assert.That(requiredComponents, Has.Count.EqualTo(2)); + Assert.That(requiredComponents, Is.EquivalentTo(components)); + } + } + + [Test] + public void GetService_and_GetRequiredService_should_resolve_root_endpoint_keyed_enumerables() + { + var services = new ServiceCollection(); + services.AddKeyedSingleton("endpoint"); + services.AddKeyedSingleton("endpoint"); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var components = ((IEnumerable)adapter.GetService(typeof(IEnumerable))!).ToList(); + var requiredComponents = ((IEnumerable)adapter.GetRequiredService(typeof(IEnumerable))).ToList(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(components, Has.Count.EqualTo(2)); + Assert.That(requiredComponents, Has.Count.EqualTo(2)); + Assert.That(requiredComponents, Is.EquivalentTo(components)); + } + } + + [Test] + public void GetKeyedService_and_GetRequiredKeyedService_should_resolve_local_keyed_enumerables() + { + var services = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + keyedServices.AddKeyedSingleton("component"); + keyedServices.AddKeyedSingleton("component"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var components = ((IEnumerable)adapter.GetKeyedService(typeof(IEnumerable), "component")!).ToList(); + var requiredComponents = ((IEnumerable)adapter.GetRequiredKeyedService(typeof(IEnumerable), "component")).ToList(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(components, Has.Count.EqualTo(2)); + Assert.That(requiredComponents, Has.Count.EqualTo(2)); + Assert.That(requiredComponents, Is.EquivalentTo(components)); + } + } + + [Test] + public void GetKeyedService_and_GetRequiredKeyedService_should_resolve_all_services_for_any_key() + { + var services = new ServiceCollection(); + services.AddSingleton(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + keyedServices.AddSingleton(); + keyedServices.AddKeyedSingleton("component"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var components = ((IEnumerable)adapter.GetKeyedService(typeof(IEnumerable), KeyedServiceKey.Any)!).ToList(); + var requiredComponents = ((IEnumerable)adapter.GetRequiredKeyedService(typeof(IEnumerable), KeyedServiceKey.Any)).ToList(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(components, Has.Count.EqualTo(3)); + Assert.That(requiredComponents, Has.Count.EqualTo(3)); + Assert.That(requiredComponents, Is.EquivalentTo(components)); + } + } + + interface IComponent; + + class LocalComponent : IComponent; + + class LocalKeyedComponent : IComponent; + + class SecondLocalKeyedComponent : IComponent; + + class RootEndpointComponent : IComponent; + + class SecondRootEndpointComponent : IComponent; + + class RootComponent : IComponent; + + class RootKeyedComponent : IComponent; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceCollectionAdapter.cs b/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceCollectionAdapter.cs index 1caf73a2f80..108618713be 100644 --- a/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceCollectionAdapter.cs +++ b/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceCollectionAdapter.cs @@ -53,7 +53,6 @@ public void Clear() keyedDescriptors.Clear(); originalDescriptors.Clear(); - serviceTypeCounts.Clear(); } public bool Contains(ServiceDescriptor item) @@ -90,7 +89,6 @@ public bool Remove(ServiceDescriptor item) originalDescriptors.RemoveAt(index); keyedDescriptors.RemoveAt(index); _ = Inner.Remove(keyedDescriptor); - DecrementServiceTypeCount(keyedDescriptor.ServiceType); return true; } @@ -100,29 +98,39 @@ public void RemoveAt(int index) keyedDescriptors.RemoveAt(index); originalDescriptors.RemoveAt(index); _ = Inner.Remove(keyedDescriptor); - DecrementServiceTypeCount(keyedDescriptor.ServiceType); } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public bool ContainsService(Type serviceType) + public bool ContainsLocalService(Type serviceType, object? serviceKey) { ArgumentNullException.ThrowIfNull(serviceType); - if (serviceTypeCounts.ContainsKey(serviceType)) + foreach (var descriptor in originalDescriptors) { - return true; + if (ServiceTypeMatches(descriptor.ServiceType, serviceType) && Equals(GetServiceKey(descriptor), serviceKey)) + { + return true; + } } - if (!serviceType.IsGenericType) + return false; + } + + public KeyedServiceKey GetLocalServiceKey(object? serviceKey) => new(ServiceKey, serviceKey); + + static bool ServiceTypeMatches(Type registeredServiceType, Type requestedServiceType) + { + if (registeredServiceType == requestedServiceType) { - return false; + return true; } - var definition = serviceType.GetGenericTypeDefinition(); - return serviceTypeCounts.ContainsKey(definition); + return requestedServiceType.IsGenericType && registeredServiceType == requestedServiceType.GetGenericTypeDefinition(); } + static object? GetServiceKey(ServiceDescriptor descriptor) => descriptor.IsKeyedService ? descriptor.ServiceKey : null; + ServiceDescriptor EnsureKeyedDescriptor(ServiceDescriptor descriptor) { ServiceDescriptor keyedDescriptor; @@ -191,30 +199,9 @@ ServiceDescriptor EnsureKeyedDescriptor(ServiceDescriptor descriptor) } } - if (!serviceTypeCounts.TryAdd(keyedDescriptor.ServiceType, 1)) - { - serviceTypeCounts[keyedDescriptor.ServiceType]++; - } - return keyedDescriptor; } - void DecrementServiceTypeCount(Type serviceType) - { - if (!serviceTypeCounts.TryGetValue(serviceType, out var count)) - { - return; - } - - if (count <= 1) - { - _ = serviceTypeCounts.Remove(serviceType); - return; - } - - serviceTypeCounts[serviceType] = count - 1; - } - static class UnsafeAccessor { [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_implementationType")] @@ -223,6 +210,5 @@ static class UnsafeAccessor readonly List originalDescriptors = []; readonly List keyedDescriptors = []; - readonly Dictionary serviceTypeCounts = []; readonly ConcurrentDictionary factories = new(); } \ 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 14f8372b62c..1176f6e78f7 100644 --- a/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceProviderAdapter.cs +++ b/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceProviderAdapter.cs @@ -4,6 +4,7 @@ namespace NServiceBus; using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -27,21 +28,22 @@ public KeyedServiceProviderAdapter(IServiceProvider serviceProvider, object serv keyedScopeFactory = new KeyedServiceScopeFactory(serviceProvider.GetRequiredService(), serviceKeyedServiceKey, serviceCollection); } - public bool IsService(Type serviceType) => serviceCollection.ContainsService(serviceType); - - public bool IsKeyedService(Type serviceType, object? serviceKey) + public bool IsService(Type serviceType) { - if (!serviceCollection.ContainsService(serviceType)) - { - return false; - } + ArgumentNullException.ThrowIfNull(serviceType); - if (serviceKey is KeyedServiceKey key) + if (IsServiceProvider(serviceType) || IsScopeFactory(serviceType) || IsServicesRequest(serviceType)) { - return Equals(serviceKeyedServiceKey.BaseKey, key.BaseKey); + return true; } - return false; + return ContainsLocalEndpointService(serviceType, serviceKeyedServiceKey) || ContainsRootEndpointKeyedService(serviceType) || ContainsRootService(serviceType); + } + + public bool IsKeyedService(Type serviceType, object? serviceKey) + { + var computedKey = GetOrCreateComputedKey(serviceKey); + return ContainsLocalEndpointService(serviceType, computedKey) || ContainsRootKeyedService(serviceType, GetBaseKeyOrServiceKey(serviceKey)); } public object? GetService(Type serviceType) @@ -61,13 +63,21 @@ public bool IsKeyedService(Type serviceType, object? serviceKey) if (!IsServicesRequest(serviceType)) { - return IsKeyedService(serviceType, serviceKeyedServiceKey) - ? serviceProvider.GetKeyedService(serviceType, serviceKeyedServiceKey) - : serviceProvider.GetService(serviceType); + if (ContainsLocalEndpointService(serviceType, serviceKeyedServiceKey)) + { + return serviceProvider.GetKeyedService(serviceType, GetLocalEndpointServiceKey(serviceKeyedServiceKey)); + } + + return ContainsRootEndpointKeyedService(serviceType) ? serviceProvider.GetKeyedService(serviceType, serviceKeyedServiceKey.BaseKey) : serviceProvider.GetService(serviceType); } var itemType = serviceType.GetGenericArguments()[0]; - return IsKeyedService(itemType, serviceKeyedServiceKey) ? serviceProvider.GetKeyedServices(itemType, serviceKeyedServiceKey) : serviceProvider.GetServices(itemType); + if (ContainsLocalEndpointService(itemType, serviceKeyedServiceKey)) + { + return serviceProvider.GetKeyedServices(itemType, GetLocalEndpointServiceKey(serviceKeyedServiceKey)); + } + + return ContainsRootEndpointKeyedService(itemType) ? serviceProvider.GetKeyedServices(itemType, serviceKeyedServiceKey.BaseKey) : serviceProvider.GetServices(itemType); } public object GetRequiredService(Type serviceType) @@ -87,13 +97,21 @@ public object GetRequiredService(Type serviceType) if (!IsServicesRequest(serviceType)) { - return IsKeyedService(serviceType, serviceKeyedServiceKey) - ? serviceProvider.GetRequiredKeyedService(serviceType, serviceKeyedServiceKey) - : serviceProvider.GetRequiredService(serviceType); + if (ContainsLocalEndpointService(serviceType, serviceKeyedServiceKey)) + { + return serviceProvider.GetRequiredKeyedService(serviceType, GetLocalEndpointServiceKey(serviceKeyedServiceKey)); + } + + return ContainsRootEndpointKeyedService(serviceType) ? serviceProvider.GetRequiredKeyedService(serviceType, serviceKeyedServiceKey.BaseKey) : serviceProvider.GetRequiredService(serviceType); } var itemType = serviceType.GetGenericArguments()[0]; - return IsKeyedService(itemType, serviceKeyedServiceKey) ? serviceProvider.GetKeyedServices(itemType, serviceKeyedServiceKey) : serviceProvider.GetServices(itemType); + if (ContainsLocalEndpointService(itemType, serviceKeyedServiceKey)) + { + return serviceProvider.GetKeyedServices(itemType, GetLocalEndpointServiceKey(serviceKeyedServiceKey)); + } + + return ContainsRootEndpointKeyedService(itemType) ? serviceProvider.GetKeyedServices(itemType, serviceKeyedServiceKey.BaseKey) : serviceProvider.GetServices(itemType); } public object? GetKeyedService(Type serviceType, object? serviceKey) @@ -114,16 +132,16 @@ public object GetRequiredService(Type serviceType) var computedKey = GetOrCreateComputedKey(serviceKey); if (!IsServicesRequest(serviceType)) { - return IsKeyedService(serviceType, computedKey) - ? serviceProvider.GetKeyedService(serviceType, computedKey) + return ContainsLocalEndpointService(serviceType, computedKey) + ? serviceProvider.GetKeyedService(serviceType, GetLocalEndpointServiceKey(computedKey)) : serviceProvider.GetKeyedService(serviceType, GetBaseKeyOrServiceKey(serviceKey)); } var itemType = serviceType.GetGenericArguments()[0]; if (!Equals(computedKey, anyKey)) { - return IsKeyedService(itemType, computedKey) - ? serviceProvider.GetKeyedServices(itemType, computedKey) + return ContainsLocalEndpointService(itemType, computedKey) + ? serviceProvider.GetKeyedServices(itemType, GetLocalEndpointServiceKey(computedKey)) : serviceProvider.GetKeyedServices(itemType, GetBaseKeyOrServiceKey(serviceKey)); } @@ -148,16 +166,16 @@ public object GetRequiredKeyedService(Type serviceType, object? serviceKey) var computedKey = GetOrCreateComputedKey(serviceKey); if (!IsServicesRequest(serviceType)) { - return IsKeyedService(serviceType, computedKey) - ? serviceProvider.GetRequiredKeyedService(serviceType, computedKey) + return ContainsLocalEndpointService(serviceType, computedKey) + ? serviceProvider.GetRequiredKeyedService(serviceType, GetLocalEndpointServiceKey(computedKey)) : serviceProvider.GetRequiredKeyedService(serviceType, GetBaseKeyOrServiceKey(serviceKey)); } var itemType = serviceType.GetGenericArguments()[0]; if (!Equals(computedKey, anyKey)) { - return IsKeyedService(itemType, computedKey) - ? serviceProvider.GetKeyedServices(itemType, computedKey) + return ContainsLocalEndpointService(itemType, computedKey) + ? serviceProvider.GetKeyedServices(itemType, GetLocalEndpointServiceKey(computedKey)) : serviceProvider.GetKeyedServices(itemType, GetBaseKeyOrServiceKey(serviceKey)); } @@ -204,6 +222,55 @@ public void Dispose() // this allows resolving services by either the KeyedServiceKey or the base key and makes the experience consistent static object? GetBaseKeyOrServiceKey(object? serviceKey) => serviceKey is KeyedServiceKey key ? key.BaseKey : serviceKey; + bool ContainsLocalEndpointService(Type serviceType, KeyedServiceKey key) => + Equals(serviceKeyedServiceKey.BaseKey, key.BaseKey) && + localEndpointServiceLookup.GetOrAdd((serviceType, key.ServiceKey), static (lookupKey, collection) => + collection.ContainsLocalService(lookupKey.ServiceType, lookupKey.ServiceKey), serviceCollection); + + KeyedServiceKey GetLocalEndpointServiceKey(KeyedServiceKey key) => serviceCollection.GetLocalServiceKey(key.ServiceKey); + + bool ContainsRootEndpointKeyedService(Type serviceType) => ContainsRootKeyedService(serviceType, serviceKeyedServiceKey.BaseKey); + + bool ContainsRootService(Type serviceType) => + rootServiceLookup.GetOrAdd(serviceType, static (lookupServiceType, state) => + { + var rootServiceProbe = state.ServiceProvider?.GetService(); + if (rootServiceProbe?.IsService(lookupServiceType) is true) + { + return true; + } + + foreach (var descriptor in state.ServiceCollection.Inner) + { + if (!descriptor.IsKeyedService && ServiceTypeMatches(descriptor.ServiceType, lookupServiceType)) + { + return true; + } + } + + return false; + }, (ServiceProvider: serviceProvider, ServiceCollection: serviceCollection)); + + bool ContainsRootKeyedService(Type serviceType, object? serviceKey) => + rootKeyedServiceLookup.GetOrAdd((serviceType, serviceKey), static (lookupKey, state) => + { + var rootKeyedServiceProbe = state.ServiceProvider?.GetService(); + if (rootKeyedServiceProbe?.IsKeyedService(lookupKey.ServiceType, lookupKey.ServiceKey) is true) + { + return true; + } + + foreach (var descriptor in state.ServiceCollection.Inner) + { + if (descriptor.IsKeyedService && ServiceTypeMatches(descriptor.ServiceType, lookupKey.ServiceType) && Equals(lookupKey.ServiceKey, descriptor.ServiceKey)) + { + return true; + } + } + + return false; + }, (ServiceProvider: serviceProvider, ServiceCollection: serviceCollection)); + KeyedServiceKey GetOrCreateComputedKey(object? serviceKey) { if (serviceKey is KeyedServiceKey key && Equals(serviceKeyedServiceKey.BaseKey, key.BaseKey)) @@ -226,6 +293,7 @@ static object GetAllServices(IServiceProvider serviceProvider, Type itemType) } static bool IsServicesRequest(Type serviceType) => serviceType.IsGenericType && serviceType.GetGenericTypeDefinition() == typeof(IEnumerable<>); + static bool ServiceTypeMatches(Type registeredServiceType, Type requestedServiceType) => registeredServiceType == requestedServiceType || (requestedServiceType.IsGenericType && registeredServiceType == requestedServiceType.GetGenericTypeDefinition()); static bool IsServiceProvider(Type serviceType) => serviceType == typeof(IServiceProvider) || serviceType == typeof(ISupportRequiredService) || serviceType == typeof(IServiceProviderIsKeyedService) || serviceType == typeof(IServiceProviderIsService); static bool IsScopeFactory(Type serviceType) => serviceType == typeof(IServiceScopeFactory); @@ -236,4 +304,7 @@ static object GetAllServices(IServiceProvider serviceProvider, Type itemType) readonly bool ownsProvider; readonly KeyedServiceKey anyKey; readonly KeyedServiceScopeFactory keyedScopeFactory; + readonly ConcurrentDictionary<(Type ServiceType, object? ServiceKey), bool> localEndpointServiceLookup = new(); + readonly ConcurrentDictionary rootServiceLookup = new(); + readonly ConcurrentDictionary<(Type ServiceType, object? ServiceKey), bool> rootKeyedServiceLookup = new(); } \ No newline at end of file From d072c26c597940e94f489c0ab2ec67448578cd26 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 17:54:01 +0000 Subject: [PATCH 17/79] Update actions/setup-dotnet action to v5.4.0 (#7832) * Update actions/setup-dotnet action to v5.4.0 * Change invalid global.json sdk version --------- Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> Co-authored-by: David Boike --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 2 +- global.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7206d74ec6a..1cd2b1b53eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: with: fetch-depth: 0 - name: Setup .NET SDK - uses: actions/setup-dotnet@v5.3.0 + uses: actions/setup-dotnet@v5.4.0 with: global-json-file: global.json - name: Build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aedcdb86fed..3361226c0d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: with: fetch-depth: 0 - name: Setup .NET SDK - uses: actions/setup-dotnet@v5.3.0 + uses: actions/setup-dotnet@v5.4.0 with: global-json-file: global.json - name: Build diff --git a/global.json b/global.json index 2b3567e3704..7ca72cbbf19 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.0", + "version": "10.0.100", "allowPrerelease": false, "rollForward": "latestFeature" } From 57ee8bd2d71b8cbea0dbd50db840f354d0aab5b3 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:09:58 -0400 Subject: [PATCH 18/79] Update dependency Particular.Licensing.Sources to 7.2.0 (#7853) * Update dependency Particular.Licensing.Sources to 7.2.0 * Update for nullability and upgrade protection removal --------- Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> Co-authored-by: Brandon Ording --- .../Licensing/LicenseManagerTests.cs | 51 ++----------------- .../ActiveLicenseFindResultExtensions.cs | 2 +- .../Licensing/LicenseManager.cs | 23 +++------ .../Licensing/LicenseReminder.cs | 17 +++---- src/NServiceBus.Core/NServiceBus.Core.csproj | 2 +- 5 files changed, 21 insertions(+), 74 deletions(-) diff --git a/src/NServiceBus.Core.Tests/Licensing/LicenseManagerTests.cs b/src/NServiceBus.Core.Tests/Licensing/LicenseManagerTests.cs index b71eaedfc92..0dcb3a4ce78 100644 --- a/src/NServiceBus.Core.Tests/Licensing/LicenseManagerTests.cs +++ b/src/NServiceBus.Core.Tests/Licensing/LicenseManagerTests.cs @@ -35,17 +35,17 @@ public void WhenSubscriptionLicenseExpired() } [Test] - public void WhenUpgradeProtectionExpiredForThisRelease() + public void WhenUpgradeProtectionNoLongerBeingSupported() { var logger = new TestableLogger(); - LicenseManager.LogLicenseStatus(LicenseStatus.InvalidDueToExpiredUpgradeProtection, logger, new License(), "fake-url"); + LicenseManager.LogLicenseStatus(LicenseStatus.InvalidDueToUpgradeProtectionNoLongerBeingSupported, logger, new License(), "fake-url"); Assert.That(logger.Logs, Has.Count.EqualTo(1)); using (Assert.EnterMultipleScope()) { Assert.That(logger.Logs[0].level, Is.EqualTo(LogLevel.Error)); - Assert.That(logger.Logs[0].message, Is.EqualTo("Upgrade protection expired. In order for us to continue to provide you with support and new versions of the Particular Service Platform, contact us to renew your license: contact@particular.net")); + Assert.That(logger.Logs[0].message, Is.EqualTo("Licenses with ugprade protection are no longer supported. In order for us to continue to provide you with support and new versions of the Particular Service Platform, contact us to renew your license: contact@particular.net")); } } @@ -116,51 +116,6 @@ public void WhenSubscriptionAboutToExpire(int daysRemaining, string expectedMess } } - [TestCase(3, "Upgrade protection expiring in 3 days. Contact us to renew your license: contact@particular.net")] - [TestCase(1, "Upgrade protection expiring in 1 day. Contact us to renew your license: contact@particular.net")] - [TestCase(0, "Upgrade protection expiring today. Contact us to renew your license: contact@particular.net")] - public void WhenUpgradeProtectionAboutToExpire(int daysRemaining, string expectedMessage) - { - var logger = new TestableLogger(); - var today = new DateTime(2012, 12, 12); - var license = new License - { - utcDateTimeProvider = () => today, - UpgradeProtectionExpiration = today.AddDays(daysRemaining) - }; - - LicenseManager.LogLicenseStatus(LicenseStatus.ValidWithExpiringUpgradeProtection, logger, license, "fake-url"); - - Assert.That(logger.Logs, Has.Count.EqualTo(1)); - using (Assert.EnterMultipleScope()) - { - Assert.That(logger.Logs[0].level, Is.EqualTo(LogLevel.Warn)); - Assert.That(logger.Logs[0].message, Is.EqualTo(expectedMessage)); - } - } - - [Test] - public void WhenUpgradeProtectionExpiredForFutureVersions() - { - var logger = new TestableLogger(); - var today = new DateTime(2012, 12, 12); - var license = new License - { - utcDateTimeProvider = () => today, - releaseDateProvider = () => today.AddDays(-20), - UpgradeProtectionExpiration = today.AddDays(-10) - }; - - LicenseManager.LogLicenseStatus(LicenseStatus.ValidWithExpiredUpgradeProtection, logger, license, "fake-url"); - - Assert.That(logger.Logs, Has.Count.EqualTo(1)); - using (Assert.EnterMultipleScope()) - { - Assert.That(logger.Logs[0].level, Is.EqualTo(LogLevel.Warn)); - Assert.That(logger.Logs[0].message, Is.EqualTo("Upgrade protection expired. In order for us to continue to provide you with support and new versions of the Particular Service Platform, contact us to renew your license: contact@particular.net")); - } - } - class TestableLogger : ILog { public bool IsDebugEnabled => true; diff --git a/src/NServiceBus.Core/Licensing/ActiveLicenseFindResultExtensions.cs b/src/NServiceBus.Core/Licensing/ActiveLicenseFindResultExtensions.cs index 6a0c9e9ab95..e91181b10c0 100644 --- a/src/NServiceBus.Core/Licensing/ActiveLicenseFindResultExtensions.cs +++ b/src/NServiceBus.Core/Licensing/ActiveLicenseFindResultExtensions.cs @@ -6,5 +6,5 @@ namespace NServiceBus; static class ActiveLicenseFindResultExtensions { - public static bool HasLicenseExpired(this ActiveLicenseFindResult result) => result.License.HasExpired(); + public static bool HasLicenseExpired(this ActiveLicenseFindResult result) => result.License?.HasExpired() ?? true; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Licensing/LicenseManager.cs b/src/NServiceBus.Core/Licensing/LicenseManager.cs index 7e8f616cc61..7241894eee7 100644 --- a/src/NServiceBus.Core/Licensing/LicenseManager.cs +++ b/src/NServiceBus.Core/Licensing/LicenseManager.cs @@ -22,7 +22,7 @@ public static ActiveLicenseFindResult InitializeLicense(string? licenseText, str LogFindResults(result); - var licenseStatus = result.License.GetLicenseStatus(); + var licenseStatus = result.License?.GetLicenseStatus(); LogLicenseStatus(licenseStatus, Logger, result.License, developerLicenseUrl); if (licenseStatus == LicenseStatus.InvalidDueToExpiredTrial) @@ -33,39 +33,32 @@ public static ActiveLicenseFindResult InitializeLicense(string? licenseText, str return result; } - public static void LogLicenseStatus(LicenseStatus licenseStatus, ILog logger, License license, string developerLicenseUrl) + public static void LogLicenseStatus(LicenseStatus? licenseStatus, ILog logger, License? license, string developerLicenseUrl) { - var whenLicenseExpiresPhrase = GetRemainingDaysString(license.GetDaysUntilLicenseExpires()); - var whenUpgradeProtectedExpiresPhrase = GetRemainingDaysString(license.GetDaysUntilUpgradeProtectionExpires()); + var whenLicenseExpiresPhrase = GetRemainingDaysString(license?.GetDaysUntilLicenseExpires()); switch (licenseStatus) { case LicenseStatus.Valid: break; - case LicenseStatus.ValidWithExpiredUpgradeProtection: - logger.Warn("Upgrade protection expired. In order for us to continue to provide you with support and new versions of the Particular Service Platform, contact us to renew your license: contact@particular.net"); - break; case LicenseStatus.ValidWithExpiringTrial: - logger.Warn(license.IsExtendedTrial + logger.Warn(license?.IsExtendedTrial ?? false ? $"Development license expiring {whenLicenseExpiresPhrase}. If you’re still in development, renew your license for free at {developerLicenseUrl} otherwise email contact@particular.net" : $"Trial license expiring {whenLicenseExpiresPhrase}. Get your free development license at {developerLicenseUrl}"); break; case LicenseStatus.ValidWithExpiringSubscription: logger.Warn($"License expiring {whenLicenseExpiresPhrase}. Contact us to renew your license: contact@particular.net"); break; - case LicenseStatus.ValidWithExpiringUpgradeProtection: - logger.Warn($"Upgrade protection expiring {whenUpgradeProtectedExpiresPhrase}. Contact us to renew your license: contact@particular.net"); - break; case LicenseStatus.InvalidDueToExpiredTrial: - logger.Error(license.IsExtendedTrial + logger.Error(license?.IsExtendedTrial ?? false ? $"Development license expired. If you’re still in development, renew your license for free at {developerLicenseUrl} otherwise email contact@particular.net" : $"Trial license expired. Get your free development license at {developerLicenseUrl}"); break; case LicenseStatus.InvalidDueToExpiredSubscription: logger.Error("License expired. Contact us to renew your license: contact@particular.net"); break; - case LicenseStatus.InvalidDueToExpiredUpgradeProtection: - logger.Error("Upgrade protection expired. In order for us to continue to provide you with support and new versions of the Particular Service Platform, contact us to renew your license: contact@particular.net"); + case LicenseStatus.InvalidDueToUpgradeProtectionNoLongerBeingSupported: + logger.Error("Licenses with ugprade protection are no longer supported. In order for us to continue to provide you with support and new versions of the Particular Service Platform, contact us to renew your license: contact@particular.net"); break; default: break; @@ -137,7 +130,7 @@ static void OpenDeveloperLicensePage(string developerLicenseUrl) static string CreateDeveloperLicenseUrl(ActiveLicenseFindResult result) { var version = VersionInformation.MajorMinorPatch; - var isRenewal = result.License.IsExtendedTrial ? "1" : "0"; + var isRenewal = result.License?.IsExtendedTrial ?? false ? "1" : "0"; var platform = GetPlatformCode(); var frameworkVersion = GetFrameworkVersion(); return $"https://particular.net/license/nservicebus?v={version}&t={isRenewal}&p={platform}&f={frameworkVersion}"; diff --git a/src/NServiceBus.Core/Licensing/LicenseReminder.cs b/src/NServiceBus.Core/Licensing/LicenseReminder.cs index dcc6fa40ae6..2b17221d5c2 100644 --- a/src/NServiceBus.Core/Licensing/LicenseReminder.cs +++ b/src/NServiceBus.Core/Licensing/LicenseReminder.cs @@ -45,17 +45,16 @@ protected override void Setup(FeatureConfigurationContext context) static object GenerateLicenseDiagnostics(ActiveLicenseFindResult result) => new { - result.License.RegisteredTo, - result.License.LicenseType, - result.License.Edition, - Tier = result.License.Edition, - LicenseStatus = result.License.GetLicenseStatus(), + result.License?.RegisteredTo, + result.License?.LicenseType, + result.License?.Edition, + Tier = result.License?.Edition, + LicenseStatus = result.License?.GetLicenseStatus(), LicenseLocation = result.Location, - ValidApplications = string.Join(",", result.License.ValidApplications), - CommercialLicense = result.License.IsCommercialLicense, + ValidApplications = string.Join(",", result.License?.ValidApplications ?? []), + CommercialLicense = result.License?.IsCommercialLicense, IsExpired = result.HasLicenseExpired(), - result.License.ExpirationDate, - UpgradeProtectionExpirationDate = result.License.UpgradeProtectionExpiration + result.License?.ExpirationDate, }; public const string LicenseTextSettingsKey = "LicenseText"; diff --git a/src/NServiceBus.Core/NServiceBus.Core.csproj b/src/NServiceBus.Core/NServiceBus.Core.csproj index 1ae3e46db14..6eac6997fd5 100644 --- a/src/NServiceBus.Core/NServiceBus.Core.csproj +++ b/src/NServiceBus.Core/NServiceBus.Core.csproj @@ -20,7 +20,7 @@ - + From 951482be40606641c357ea7576dbd53d1f509286 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 15:15:54 +0000 Subject: [PATCH 19/79] Update dependency Microsoft.NET.Test.Sdk to 18.8.0 (#7854) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- .../NServiceBus.AcceptanceTests.csproj | 2 +- .../NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj | 2 +- src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj | 2 +- .../NServiceBus.Learning.AcceptanceTests.csproj | 2 +- .../NServiceBus.PersistenceTests.csproj | 2 +- .../NServiceBus.TransportTests.csproj | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj b/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj index 6dc98b74939..6ae319dd777 100644 --- a/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj +++ b/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj index 55d8e78f6cc..a154da9cade 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj index 35949fc9c13..7866421d0d9 100644 --- a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj +++ b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj b/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj index 80072fa2d28..b4b89f5cd00 100644 --- a/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj +++ b/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj b/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj index ad99551c205..3fb3d82f667 100644 --- a/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj +++ b/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj b/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj index aaaa392d21e..893980c67de 100644 --- a/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj +++ b/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj @@ -10,7 +10,7 @@ - + From fd790f619ab4c3ab2e165ccae8af3a74d999947e Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 13:56:26 -0400 Subject: [PATCH 20/79] Update dependency Particular.Licensing.Sources to 7.3.0 (#7858) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- 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 6eac6997fd5..e181f3c8185 100644 --- a/src/NServiceBus.Core/NServiceBus.Core.csproj +++ b/src/NServiceBus.Core/NServiceBus.Core.csproj @@ -20,7 +20,7 @@ - + From c747104d6b83a155809fa5a4c444b4e3a7bb9383 Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Wed, 15 Jul 2026 12:16:11 -0700 Subject: [PATCH 21/79] Add support for nullable reference types - Pipeline folder (#7847) * Introduce support for nullable reference types #6257 * Fix spacing --------- Co-authored-by: Brandon Ording --- src/NServiceBus.Core/Pipeline/IPipelineTerminator.cs | 4 +++- .../Pipeline/Incoming/MessageHandlerInvoker.cs | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/NServiceBus.Core/Pipeline/IPipelineTerminator.cs b/src/NServiceBus.Core/Pipeline/IPipelineTerminator.cs index e21f90b406e..566ffd3929f 100644 --- a/src/NServiceBus.Core/Pipeline/IPipelineTerminator.cs +++ b/src/NServiceBus.Core/Pipeline/IPipelineTerminator.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; /// /// Marker interface for pipeline terminators. diff --git a/src/NServiceBus.Core/Pipeline/Incoming/MessageHandlerInvoker.cs b/src/NServiceBus.Core/Pipeline/Incoming/MessageHandlerInvoker.cs index 3f83eb301ea..38cc34cef82 100644 --- a/src/NServiceBus.Core/Pipeline/Incoming/MessageHandlerInvoker.cs +++ b/src/NServiceBus.Core/Pipeline/Incoming/MessageHandlerInvoker.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; From 7d5d476031ff0f0ab8092c26b05fbbdb36aa28bb Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Wed, 15 Jul 2026 15:09:43 -0700 Subject: [PATCH 22/79] Add support for nullable reference types - Hosting folder (#7848) * enable , Nullable in the remaining file in Core Hosting folder * fix empty space * fix empty space * fall back to a default value ("Unknown assembly") instead of propagating null * Formatting * Make nullable enable spacing consistent in Hosting folder --------- Co-authored-by: Brandon Ording --- .../Hosting/AddHostInfoHeadersBehavior.cs | 1 + .../Hosting/AuditHostInformationBehavior.cs | 1 + .../Hosting/ExternallyManagedContainerHost.cs | 1 + .../Hosting/Helpers/AssemblyScanner.cs | 1 + .../Hosting/Helpers/AssemblyScannerResults.cs | 1 + .../Helpers/AssemblyScanningComponent.cs | 5 +++-- .../Helpers/AssemblyScanningDiagnostics.cs | 19 +++++++------------ .../Hosting/Helpers/AssemblyValidator.cs | 1 + .../Hosting/Helpers/SkippedFile.cs | 1 + .../HostInfoConfigurationExtensions.cs | 1 + .../Hosting/HostInfoSettings.cs | 1 + .../Hosting/HostInformation.cs | 1 + .../Hosting/HostingComponent.cs | 1 + .../Hosting/InternallyManagedContainerHost.cs | 1 + 14 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/NServiceBus.Core/Hosting/AddHostInfoHeadersBehavior.cs b/src/NServiceBus.Core/Hosting/AddHostInfoHeadersBehavior.cs index be777e9dfa7..4b706cdcea2 100644 --- a/src/NServiceBus.Core/Hosting/AddHostInfoHeadersBehavior.cs +++ b/src/NServiceBus.Core/Hosting/AddHostInfoHeadersBehavior.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Hosting/AuditHostInformationBehavior.cs b/src/NServiceBus.Core/Hosting/AuditHostInformationBehavior.cs index f4a1d3545fb..7d6ba19794c 100644 --- a/src/NServiceBus.Core/Hosting/AuditHostInformationBehavior.cs +++ b/src/NServiceBus.Core/Hosting/AuditHostInformationBehavior.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Hosting/ExternallyManagedContainerHost.cs b/src/NServiceBus.Core/Hosting/ExternallyManagedContainerHost.cs index f31b2035fe2..13e42160e77 100644 --- a/src/NServiceBus.Core/Hosting/ExternallyManagedContainerHost.cs +++ b/src/NServiceBus.Core/Hosting/ExternallyManagedContainerHost.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanner.cs b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanner.cs index 56dd3c0c41a..cd90b3dfbad 100644 --- a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanner.cs +++ b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanner.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus.Hosting.Helpers; using System; diff --git a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScannerResults.cs b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScannerResults.cs index 0300179bbb8..588f041b5c8 100644 --- a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScannerResults.cs +++ b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScannerResults.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus.Hosting.Helpers; using System; diff --git a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningComponent.cs b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningComponent.cs index 5d78ac01362..328483cb8ab 100644 --- a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningComponent.cs +++ b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningComponent.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; @@ -39,7 +40,7 @@ public static AssemblyScanningComponent Initialize(Configuration configuration, // Deliberately strongly typed because we need to truncate this super large section when writing to the logs var assemblyScanningDiagnostics = new AssemblyScanningDiagnostics( - scannableAssemblies.Assemblies.Select(a => new AssemblyDetails(a.FullName, FileVersionRetriever.GetFileVersion(a))), + scannableAssemblies.Assemblies.Select(a => new AssemblyDetails(a.FullName ?? "Unknown assembly", FileVersionRetriever.GetFileVersion(a))), scannableAssemblies.SkippedFiles.Select(f => new SkippedFile(f.FilePath, f.SkipReason)), scannableAssemblies.ErrorsThrownDuringScanning, assemblyScannerSettings @@ -75,7 +76,7 @@ static AssemblyScannerResults ScanAssemblies(AssemblyScannerConfiguration assemb public class Configuration(SettingsHolder settings) { - // This is only used for testability until we have a dedicated AOT test project. Not called on the hot path so has no performance implications. + // This is only used for testability until we have a dedicated AOT test project. Not called on the hot path so has no performance implications. public bool DynamicCodeSupported { get; set; } = System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported; public List? UserProvidedTypes { get; set; } diff --git a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningDiagnostics.cs b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningDiagnostics.cs index fc04c2e4caf..223233690f7 100644 --- a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningDiagnostics.cs +++ b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningDiagnostics.cs @@ -1,28 +1,23 @@ +#nullable enable + namespace NServiceBus; using System.Collections.Generic; using Hosting.Helpers; -sealed class AssemblyScanningDiagnostics( - IEnumerable assemblies, - IEnumerable skippedFiles, - bool errorsThrownDuringScanning, - AssemblyScannerConfiguration settings) +sealed class AssemblyScanningDiagnostics(IEnumerable assemblies, IEnumerable skippedFiles, bool errorsThrownDuringScanning, AssemblyScannerConfiguration settings) { public const string SectionName = "AssemblyScanning"; public IEnumerable Assemblies { get; } = assemblies; + public IEnumerable SkippedFiles { get; } = skippedFiles; + public bool ErrorsThrownDuringScanning { get; } = errorsThrownDuringScanning; + public AssemblyScannerConfiguration Settings { get; } = settings; - public AssemblyScanningDiagnostics CreateCompactedVersion() => - new( - [], - [], - ErrorsThrownDuringScanning, - Settings - ); + public AssemblyScanningDiagnostics CreateCompactedVersion() => new([], [], ErrorsThrownDuringScanning, Settings); } record AssemblyDetails(string FullName, string FileVersion); \ No newline at end of file diff --git a/src/NServiceBus.Core/Hosting/Helpers/AssemblyValidator.cs b/src/NServiceBus.Core/Hosting/Helpers/AssemblyValidator.cs index a65598f11f2..611532597c2 100644 --- a/src/NServiceBus.Core/Hosting/Helpers/AssemblyValidator.cs +++ b/src/NServiceBus.Core/Hosting/Helpers/AssemblyValidator.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Hosting/Helpers/SkippedFile.cs b/src/NServiceBus.Core/Hosting/Helpers/SkippedFile.cs index 96e70d0c18c..409276d13de 100644 --- a/src/NServiceBus.Core/Hosting/Helpers/SkippedFile.cs +++ b/src/NServiceBus.Core/Hosting/Helpers/SkippedFile.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus.Hosting.Helpers; /// diff --git a/src/NServiceBus.Core/Hosting/HostInfoConfigurationExtensions.cs b/src/NServiceBus.Core/Hosting/HostInfoConfigurationExtensions.cs index 81666aff691..9461a6b7b72 100644 --- a/src/NServiceBus.Core/Hosting/HostInfoConfigurationExtensions.cs +++ b/src/NServiceBus.Core/Hosting/HostInfoConfigurationExtensions.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Hosting/HostInfoSettings.cs b/src/NServiceBus.Core/Hosting/HostInfoSettings.cs index e1e2f9a3aea..6740e4204d3 100644 --- a/src/NServiceBus.Core/Hosting/HostInfoSettings.cs +++ b/src/NServiceBus.Core/Hosting/HostInfoSettings.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Hosting/HostInformation.cs b/src/NServiceBus.Core/Hosting/HostInformation.cs index a290d8b5fa2..a63f52afc2a 100644 --- a/src/NServiceBus.Core/Hosting/HostInformation.cs +++ b/src/NServiceBus.Core/Hosting/HostInformation.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus.Hosting; using System; diff --git a/src/NServiceBus.Core/Hosting/HostingComponent.cs b/src/NServiceBus.Core/Hosting/HostingComponent.cs index f79dafb60cc..3c079166ab3 100644 --- a/src/NServiceBus.Core/Hosting/HostingComponent.cs +++ b/src/NServiceBus.Core/Hosting/HostingComponent.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Hosting/InternallyManagedContainerHost.cs b/src/NServiceBus.Core/Hosting/InternallyManagedContainerHost.cs index 161261be5c8..3cba9b26f88 100644 --- a/src/NServiceBus.Core/Hosting/InternallyManagedContainerHost.cs +++ b/src/NServiceBus.Core/Hosting/InternallyManagedContainerHost.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; From 10e35b0026284859055a6c44e218fe3c531e381d Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Wed, 15 Jul 2026 15:18:13 -0700 Subject: [PATCH 23/79] Add support for nullable reference types - Logging (#7849) * add #nullable enable to missed file * Make nullable enable spacing consistent in the Logging folder --------- Co-authored-by: Brandon Ording --- src/NServiceBus.Core/Logging/ExternalLoggerFactoryAdapter.cs | 3 +++ .../Logging/IUnsupportedDefaultFactoryLoggerFactory.cs | 2 ++ src/NServiceBus.Core/Logging/MicrosoftLoggerAdapter.cs | 1 + 3 files changed, 6 insertions(+) diff --git a/src/NServiceBus.Core/Logging/ExternalLoggerFactoryAdapter.cs b/src/NServiceBus.Core/Logging/ExternalLoggerFactoryAdapter.cs index 321978e8160..87588d5e4b6 100644 --- a/src/NServiceBus.Core/Logging/ExternalLoggerFactoryAdapter.cs +++ b/src/NServiceBus.Core/Logging/ExternalLoggerFactoryAdapter.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; @@ -11,6 +12,8 @@ sealed class ExternalLoggerFactoryAdapter(ILoggerFactory externalFactory, Micros readonly ILogger scopeLogger = microsoftLoggerFactory.CreateLogger(MicrosoftLoggerFactoryAdapter.ScopeLoggerName); public ILog GetLogger(Type type) => GetLogger(type.FullName ?? type.Name); + public ILog GetLogger(string name) => externalFactory.GetLogger(name); + public IDisposable BeginScope(LogScopeState scopeState) => scopeLogger.BeginScope(scopeState) ?? NullScope.Instance; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Logging/IUnsupportedDefaultFactoryLoggerFactory.cs b/src/NServiceBus.Core/Logging/IUnsupportedDefaultFactoryLoggerFactory.cs index 874798a6abf..3eeb2378f9c 100644 --- a/src/NServiceBus.Core/Logging/IUnsupportedDefaultFactoryLoggerFactory.cs +++ b/src/NServiceBus.Core/Logging/IUnsupportedDefaultFactoryLoggerFactory.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; interface IUnsupportedDefaultFactoryLoggerFactory; \ No newline at end of file diff --git a/src/NServiceBus.Core/Logging/MicrosoftLoggerAdapter.cs b/src/NServiceBus.Core/Logging/MicrosoftLoggerAdapter.cs index 829ce9154fb..a0f11e5452f 100644 --- a/src/NServiceBus.Core/Logging/MicrosoftLoggerAdapter.cs +++ b/src/NServiceBus.Core/Logging/MicrosoftLoggerAdapter.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; From d9bfa0eddb2ad713731fdc5c5ff06182445774e3 Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Wed, 15 Jul 2026 15:23:27 -0700 Subject: [PATCH 24/79] Add support for nullable reference types - OpenTelemetry (#7850) * support for nullable reference type * add support for nullable reference types in PopulateRecoverabilityTraceMetadataBehaviorTests --- .../PopulateRecoverabilityTraceMetadataBehaviorTests.cs | 2 ++ .../Tracing/PopulateRecoverabilityTraceMetadataBehavior.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/NServiceBus.Core.Tests/OpenTelemetry/PopulateRecoverabilityTraceMetadataBehaviorTests.cs b/src/NServiceBus.Core.Tests/OpenTelemetry/PopulateRecoverabilityTraceMetadataBehaviorTests.cs index cee304d848d..6b4850fabdd 100644 --- a/src/NServiceBus.Core.Tests/OpenTelemetry/PopulateRecoverabilityTraceMetadataBehaviorTests.cs +++ b/src/NServiceBus.Core.Tests/OpenTelemetry/PopulateRecoverabilityTraceMetadataBehaviorTests.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus.Core.Tests.OpenTelemetry; using System; diff --git a/src/NServiceBus.Core/OpenTelemetry/Tracing/PopulateRecoverabilityTraceMetadataBehavior.cs b/src/NServiceBus.Core/OpenTelemetry/Tracing/PopulateRecoverabilityTraceMetadataBehavior.cs index 0bb256ecef2..935683ebfcc 100644 --- a/src/NServiceBus.Core/OpenTelemetry/Tracing/PopulateRecoverabilityTraceMetadataBehavior.cs +++ b/src/NServiceBus.Core/OpenTelemetry/Tracing/PopulateRecoverabilityTraceMetadataBehavior.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; using System; From 92e4bd118a3da3256f624edd58ba4576a2087353 Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Wed, 15 Jul 2026 15:45:04 -0700 Subject: [PATCH 25/79] Add support for nullable types - Support (#7851) * add support for nullable types * add nullable support to test file * remove extra line --- .../NullableAnnotations.ApproveNullableTypes.approved.txt | 1 - src/NServiceBus.Core.Tests/Hosting/HostInfoSettingsTests.cs | 4 +++- src/NServiceBus.Core/Support/RuntimeEnvironment.cs | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt index 9116a220ddd..99cad102d70 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt @@ -66,7 +66,6 @@ NServiceBus.Settings.IReadOnlySettings NServiceBus.Settings.SettingsHolder NServiceBus.StaticHeadersConfigExtensions NServiceBus.SubscriptionMigrationModeSettings -NServiceBus.Support.RuntimeEnvironment NServiceBus.Transport.DispatchProperties NServiceBus.Transport.ErrorContext NServiceBus.Transport.IMessageDispatcher diff --git a/src/NServiceBus.Core.Tests/Hosting/HostInfoSettingsTests.cs b/src/NServiceBus.Core.Tests/Hosting/HostInfoSettingsTests.cs index 213df348fa5..9100f2c84b3 100644 --- a/src/NServiceBus.Core.Tests/Hosting/HostInfoSettingsTests.cs +++ b/src/NServiceBus.Core.Tests/Hosting/HostInfoSettingsTests.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Core.Tests.Host; +#nullable enable + +namespace NServiceBus.Core.Tests.Host; using System; using NServiceBus.Support; diff --git a/src/NServiceBus.Core/Support/RuntimeEnvironment.cs b/src/NServiceBus.Core/Support/RuntimeEnvironment.cs index 138455659a9..6292790dce0 100644 --- a/src/NServiceBus.Core/Support/RuntimeEnvironment.cs +++ b/src/NServiceBus.Core/Support/RuntimeEnvironment.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Support; +#nullable enable + +namespace NServiceBus.Support; using System; From b047937bc8ea96724f3537ad20181be5d2937b07 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:01:30 +0000 Subject: [PATCH 26/79] Update actions/setup-dotnet action to v6 (#7866) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1cd2b1b53eb..c204fe12544 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,7 @@ jobs: with: fetch-depth: 0 - name: Setup .NET SDK - uses: actions/setup-dotnet@v5.4.0 + uses: actions/setup-dotnet@v6.0.0 with: global-json-file: global.json - name: Build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3361226c0d5..97600d7a887 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: with: fetch-depth: 0 - name: Setup .NET SDK - uses: actions/setup-dotnet@v5.4.0 + uses: actions/setup-dotnet@v6.0.0 with: global-json-file: global.json - name: Build From 240373b2c0cd1b4817260549bf1a22c53bec1ffc Mon Sep 17 00:00:00 2001 From: Tamara Rivera Date: Thu, 16 Jul 2026 15:25:33 -0700 Subject: [PATCH 27/79] Delete guidelines directory (#7865) Deleting this file, as the guidelines are really out of date and they are not complete enough to be helpful. --- guidelines/coding-guidelines.md | 13 ------------- guidelines/readme.md | 5 ----- 2 files changed, 18 deletions(-) delete mode 100644 guidelines/coding-guidelines.md delete mode 100644 guidelines/readme.md diff --git a/guidelines/coding-guidelines.md b/guidelines/coding-guidelines.md deleted file mode 100644 index 427331910e6..00000000000 --- a/guidelines/coding-guidelines.md +++ /dev/null @@ -1,13 +0,0 @@ -## Language Features - -It is acceptable to use language features for the given `LangVersion` defined in the project that compile successfully against all targeted TFMs. Note that all language features later than C# 7.3 are not officially supported for .NET Framework so compatibility varies on a feature by feature level. This can result in some language features to cause compiler errors or slower performance. - -## Performance related - -For public facing interfaces we favor read-only collections and enumerables. For internal types we favor speed and allocation reduction. Avoid using collection interfaces internally. - -### In hot paths - -* Avoid allocations -* Avoid using `System.Linq` -* Avoid using `foreach` over collections that do not have a struct enumerator diff --git a/guidelines/readme.md b/guidelines/readme.md deleted file mode 100644 index 682d7fef897..00000000000 --- a/guidelines/readme.md +++ /dev/null @@ -1,5 +0,0 @@ -## Guidelines - -This is a set of guidelines that applies to NServiceBus and downstream components - -* [Coding](coding-guidelines.md) \ No newline at end of file From a5d96a5063e55225019f3d7c8a00e3a7b6dbbe17 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 01:01:51 -0400 Subject: [PATCH 28/79] Update dependency Particular.AnalyzerTesting to 2.2.0 (#7870) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- .../NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj index a154da9cade..ee99299d5f1 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj @@ -20,7 +20,7 @@ - + From 89c91b08eb1b21ba3b22e5ef6400f4cb4caa0f58 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 16:01:49 +0000 Subject: [PATCH 29/79] Update actions/checkout action to v7.0.1 (#7876) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c204fe12544..167825a7d37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: fail-fast: false steps: - name: Checkout - uses: actions/checkout@v7.0.0 + uses: actions/checkout@v7.0.1 with: fetch-depth: 0 - name: Setup .NET SDK diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 97600d7a887..3cd57dd3dc0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v7.0.0 + uses: actions/checkout@v7.0.1 with: fetch-depth: 0 - name: Setup .NET SDK From 7e27407392ebffe0feb16873a2003112e3f2a527 Mon Sep 17 00:00:00 2001 From: Michelle Chamberlin Date: Mon, 20 Jul 2026 11:52:01 -0700 Subject: [PATCH 30/79] Add blank line after #nullable enable to match repo convention Several files had the #nullable enable annotation on line 1 but skipped the blank line before the namespace declaration that the rest of the codebase uses. Fixing them separately from the new directory-completion approval tests that depend on this convention. --- src/NServiceBus.Core/Features/FeatureFactory.cs | 1 + src/NServiceBus.Core/Features/IFeatureStartupTaskController.cs | 1 + src/NServiceBus.Core/Hosting/HostingComponent.Configuration.cs | 1 + src/NServiceBus.Core/Hosting/HostingComponent.Settings.cs | 1 + .../Hosting/StartupDiagnostics/DiagnosticSettingsExtensions.cs | 1 + src/NServiceBus.Core/Hosting/StartupDiagnostics/Host.cs | 1 + .../Hosting/StartupDiagnostics/HostStartupDiagnosticsWriter.cs | 1 + .../StartupDiagnostics/HostStartupDiagnosticsWriterFactory.cs | 1 + .../Hosting/StartupDiagnostics/JsonPrettyPrinter.cs | 1 + .../Hosting/StartupDiagnostics/StartupDiagnosticEntries.cs | 1 + src/NServiceBus.Core/IdGeneration/CombGuid.cs | 1 + src/NServiceBus.Core/MessageInterfaces/IMessageMapper.cs | 1 + .../MessageMapper/Reflection/ConcreteProxyCreator.cs | 1 + .../MessageInterfaces/MessageMapper/Reflection/MessageMapper.cs | 1 + .../Serializers/SystemJson/JsonMessageSerializer.cs | 1 + .../Serializers/SystemJson/SystemJsonConfigurationExtensions.cs | 1 + .../Serializers/SystemJson/SystemJsonSerializer.cs | 1 + .../Serializers/SystemJson/SystemJsonSerializerSettings.cs | 1 + .../Unicast/Config/MessageHandlerRegistrationExtensions.cs | 1 + 19 files changed, 19 insertions(+) diff --git a/src/NServiceBus.Core/Features/FeatureFactory.cs b/src/NServiceBus.Core/Features/FeatureFactory.cs index c60e2fc380c..404618d9554 100644 --- a/src/NServiceBus.Core/Features/FeatureFactory.cs +++ b/src/NServiceBus.Core/Features/FeatureFactory.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus.Features; using System; diff --git a/src/NServiceBus.Core/Features/IFeatureStartupTaskController.cs b/src/NServiceBus.Core/Features/IFeatureStartupTaskController.cs index f51783acd40..c41efed38de 100644 --- a/src/NServiceBus.Core/Features/IFeatureStartupTaskController.cs +++ b/src/NServiceBus.Core/Features/IFeatureStartupTaskController.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus.Features; using System; diff --git a/src/NServiceBus.Core/Hosting/HostingComponent.Configuration.cs b/src/NServiceBus.Core/Hosting/HostingComponent.Configuration.cs index 4ed38527c71..21a9e7c736b 100644 --- a/src/NServiceBus.Core/Hosting/HostingComponent.Configuration.cs +++ b/src/NServiceBus.Core/Hosting/HostingComponent.Configuration.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Hosting/HostingComponent.Settings.cs b/src/NServiceBus.Core/Hosting/HostingComponent.Settings.cs index f46a6101f05..f0692adf5cd 100644 --- a/src/NServiceBus.Core/Hosting/HostingComponent.Settings.cs +++ b/src/NServiceBus.Core/Hosting/HostingComponent.Settings.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/DiagnosticSettingsExtensions.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/DiagnosticSettingsExtensions.cs index dd136baf126..221e7bcea8b 100644 --- a/src/NServiceBus.Core/Hosting/StartupDiagnostics/DiagnosticSettingsExtensions.cs +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/DiagnosticSettingsExtensions.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/Host.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/Host.cs index bec5e59e01a..cb6b5ce44cb 100644 --- a/src/NServiceBus.Core/Hosting/StartupDiagnostics/Host.cs +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/Host.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriter.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriter.cs index 1b71d976129..d7692d49074 100644 --- a/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriter.cs +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriter.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriterFactory.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriterFactory.cs index 68b7ee00785..a17b15fac02 100644 --- a/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriterFactory.cs +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriterFactory.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/JsonPrettyPrinter.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/JsonPrettyPrinter.cs index 8ae2d000fae..c48c8888ccd 100644 --- a/src/NServiceBus.Core/Hosting/StartupDiagnostics/JsonPrettyPrinter.cs +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/JsonPrettyPrinter.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System.Text.Encodings.Web; diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/StartupDiagnosticEntries.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/StartupDiagnosticEntries.cs index 23fa7eed86a..6a6fea09f87 100644 --- a/src/NServiceBus.Core/Hosting/StartupDiagnostics/StartupDiagnosticEntries.cs +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/StartupDiagnosticEntries.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System.Collections.Generic; diff --git a/src/NServiceBus.Core/IdGeneration/CombGuid.cs b/src/NServiceBus.Core/IdGeneration/CombGuid.cs index 619ef8d1b4f..6e7a5e5ef01 100644 --- a/src/NServiceBus.Core/IdGeneration/CombGuid.cs +++ b/src/NServiceBus.Core/IdGeneration/CombGuid.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/MessageInterfaces/IMessageMapper.cs b/src/NServiceBus.Core/MessageInterfaces/IMessageMapper.cs index a94f53915f9..f2277466392 100644 --- a/src/NServiceBus.Core/MessageInterfaces/IMessageMapper.cs +++ b/src/NServiceBus.Core/MessageInterfaces/IMessageMapper.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus.MessageInterfaces; using System; diff --git a/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/ConcreteProxyCreator.cs b/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/ConcreteProxyCreator.cs index ddf7d80d4dd..be1ebfcef4e 100644 --- a/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/ConcreteProxyCreator.cs +++ b/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/ConcreteProxyCreator.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/MessageMapper.cs b/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/MessageMapper.cs index 0a6343a3c68..6c8db4b311b 100644 --- a/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/MessageMapper.cs +++ b/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/MessageMapper.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus.MessageInterfaces.MessageMapper.Reflection; using System; diff --git a/src/NServiceBus.Core/Serializers/SystemJson/JsonMessageSerializer.cs b/src/NServiceBus.Core/Serializers/SystemJson/JsonMessageSerializer.cs index 6d66c49504b..60847618f43 100644 --- a/src/NServiceBus.Core/Serializers/SystemJson/JsonMessageSerializer.cs +++ b/src/NServiceBus.Core/Serializers/SystemJson/JsonMessageSerializer.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus.Serializers.SystemJson; using System; diff --git a/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonConfigurationExtensions.cs b/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonConfigurationExtensions.cs index e4e73fb88e8..f83208317cf 100644 --- a/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonConfigurationExtensions.cs +++ b/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonConfigurationExtensions.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonSerializer.cs b/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonSerializer.cs index a2fcae51aa8..651384b9805 100644 --- a/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonSerializer.cs +++ b/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonSerializer.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonSerializerSettings.cs b/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonSerializerSettings.cs index dd0b2720acd..7fa26fbba7f 100644 --- a/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonSerializerSettings.cs +++ b/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonSerializerSettings.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus.Serializers.SystemJson; using System.Text.Json; diff --git a/src/NServiceBus.Core/Unicast/Config/MessageHandlerRegistrationExtensions.cs b/src/NServiceBus.Core/Unicast/Config/MessageHandlerRegistrationExtensions.cs index 5949ed20ca5..714222917f6 100644 --- a/src/NServiceBus.Core/Unicast/Config/MessageHandlerRegistrationExtensions.cs +++ b/src/NServiceBus.Core/Unicast/Config/MessageHandlerRegistrationExtensions.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; From a3ef2ecf40af21cad8f72c3354aec75ffaafb281 Mon Sep 17 00:00:00 2001 From: Michelle Chamberlin Date: Tue, 21 Jul 2026 15:04:24 -0700 Subject: [PATCH 31/79] Add support for nullable reference types - Recoverability (#7855) --- .../NullableAnnotations.ApproveNullableTypes.approved.txt | 1 - src/NServiceBus.Core/Recoverability/Faults/FailedMessage.cs | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt index 99cad102d70..f98057267d8 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt @@ -9,7 +9,6 @@ NServiceBus.ContentTypes NServiceBus.DateTimeOffsetHelper NServiceBus.DistributionPolicy NServiceBus.EndpointConfiguration -NServiceBus.Faults.FailedMessage NServiceBus.Headers NServiceBus.IDistributionPolicy NServiceBus.IMessageHandlerContext diff --git a/src/NServiceBus.Core/Recoverability/Faults/FailedMessage.cs b/src/NServiceBus.Core/Recoverability/Faults/FailedMessage.cs index 68e39d69794..9bdc540fe58 100644 --- a/src/NServiceBus.Core/Recoverability/Faults/FailedMessage.cs +++ b/src/NServiceBus.Core/Recoverability/Faults/FailedMessage.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus.Faults; using System; From bf62951463abf59039ad27d260a2bdf17b7b15c6 Mon Sep 17 00:00:00 2001 From: Michelle Chamberlin Date: Tue, 21 Jul 2026 15:39:55 -0700 Subject: [PATCH 32/79] Add support for nullable reference types - UnitOfWork (#7856) * add nullable enabled to UnitOfWork * add #nullable enable to relevant unit tests * Tweak test --------- Co-authored-by: Brandon Ording --- .../When_transactionscope_enabled.cs | 14 ++++++++++---- .../When_used_with_default_transaction_mode.cs | 4 +++- .../When_used_with_transport_scopes.cs | 4 +++- .../When_using_timeout_greater_than_machine_max.cs | 4 +++- ...leAnnotations.ApproveNullableTypes.approved.txt | 1 - .../TransactionScopeUnitOfWork.cs | 4 +++- .../TransactionScopeUnitOfWorkBehavior.cs | 4 +++- .../UnitOfWork/UnitOfWorkSettings.cs | 4 +++- .../UnitOfWork/UnitOfWorkSettingsExtensions.cs | 2 ++ 9 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_transactionscope_enabled.cs b/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_transactionscope_enabled.cs index 301924e90b9..1ba4f06899a 100644 --- a/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_transactionscope_enabled.cs +++ b/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_transactionscope_enabled.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.AcceptanceTests.Core.UnitOfWork.TransactionScope; +#nullable enable + +namespace NServiceBus.AcceptanceTests.Core.UnitOfWork.TransactionScope; using System.Threading.Tasks; using System.Transactions; @@ -44,12 +46,16 @@ public class MyMessageHandler(Context testContext) : IHandleMessages { public Task Handle(MyMessage message, IMessageHandlerContext context) { - if (Transaction.Current != null) + var transaction = Transaction.Current; + + if (transaction is not null) { - testContext.AmbientTransactionPresent = Transaction.Current != null; - testContext.IsolationLevel = Transaction.Current.IsolationLevel; + testContext.AmbientTransactionPresent = true; + testContext.IsolationLevel = transaction.IsolationLevel; } + testContext.MarkAsCompleted(); + return Task.CompletedTask; } } diff --git a/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_used_with_default_transaction_mode.cs b/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_used_with_default_transaction_mode.cs index 112f5ed462d..a92130bb977 100644 --- a/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_used_with_default_transaction_mode.cs +++ b/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_used_with_default_transaction_mode.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.AcceptanceTests.Core.UnitOfWork.TransactionScope; +#nullable enable + +namespace NServiceBus.AcceptanceTests.Core.UnitOfWork.TransactionScope; using System.Threading.Tasks; using AcceptanceTesting; diff --git a/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_used_with_transport_scopes.cs b/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_used_with_transport_scopes.cs index 58545991516..2ef7a5f0d01 100644 --- a/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_used_with_transport_scopes.cs +++ b/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_used_with_transport_scopes.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.AcceptanceTests.Core.UnitOfWork.TransactionScope; +#nullable enable + +namespace NServiceBus.AcceptanceTests.Core.UnitOfWork.TransactionScope; using System; using AcceptanceTesting; diff --git a/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_using_timeout_greater_than_machine_max.cs b/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_using_timeout_greater_than_machine_max.cs index 820675043ed..bb685107c98 100644 --- a/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_using_timeout_greater_than_machine_max.cs +++ b/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_using_timeout_greater_than_machine_max.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.AcceptanceTests.Core.UnitOfWork.TransactionScope; +#nullable enable + +namespace NServiceBus.AcceptanceTests.Core.UnitOfWork.TransactionScope; using System; using AcceptanceTesting; diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt index f98057267d8..c9f0102bf54 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt @@ -97,6 +97,5 @@ NServiceBus.Unicast.Queuing.QueueNotFoundException NServiceBus.Unicast.Subscriptions.MessageDrivenSubscriptions.ISubscriptionStorage NServiceBus.Unicast.Subscriptions.MessageDrivenSubscriptions.Subscriber NServiceBus.Unicast.Subscriptions.MessageType -NServiceBus.UnitOfWorkSettingsExtensions NServiceBus.XmlSerializationExtensions NServiceBus.XmlSerializer diff --git a/src/NServiceBus.Core/UnitOfWork/TransactionScopes/TransactionScopeUnitOfWork.cs b/src/NServiceBus.Core/UnitOfWork/TransactionScopes/TransactionScopeUnitOfWork.cs index d9f18fee0a7..8f95125eba3 100644 --- a/src/NServiceBus.Core/UnitOfWork/TransactionScopes/TransactionScopeUnitOfWork.cs +++ b/src/NServiceBus.Core/UnitOfWork/TransactionScopes/TransactionScopeUnitOfWork.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Features; +#nullable enable + +namespace NServiceBus.Features; using System; using System.Transactions; diff --git a/src/NServiceBus.Core/UnitOfWork/TransactionScopes/TransactionScopeUnitOfWorkBehavior.cs b/src/NServiceBus.Core/UnitOfWork/TransactionScopes/TransactionScopeUnitOfWorkBehavior.cs index da412437846..8271b13dcc4 100644 --- a/src/NServiceBus.Core/UnitOfWork/TransactionScopes/TransactionScopeUnitOfWorkBehavior.cs +++ b/src/NServiceBus.Core/UnitOfWork/TransactionScopes/TransactionScopeUnitOfWorkBehavior.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System; using System.Threading.Tasks; diff --git a/src/NServiceBus.Core/UnitOfWork/UnitOfWorkSettings.cs b/src/NServiceBus.Core/UnitOfWork/UnitOfWorkSettings.cs index fd66c6958e2..56320293b8d 100644 --- a/src/NServiceBus.Core/UnitOfWork/UnitOfWorkSettings.cs +++ b/src/NServiceBus.Core/UnitOfWork/UnitOfWorkSettings.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System; using System.Transactions; diff --git a/src/NServiceBus.Core/UnitOfWork/UnitOfWorkSettingsExtensions.cs b/src/NServiceBus.Core/UnitOfWork/UnitOfWorkSettingsExtensions.cs index a775f38d594..498a7ce5d7f 100644 --- a/src/NServiceBus.Core/UnitOfWork/UnitOfWorkSettingsExtensions.cs +++ b/src/NServiceBus.Core/UnitOfWork/UnitOfWorkSettingsExtensions.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; using System; From 685e7c0f859bce4b12ea2d895f48da6aa2c310a1 Mon Sep 17 00:00:00 2001 From: Michelle Chamberlin Date: Wed, 22 Jul 2026 15:41:25 -0700 Subject: [PATCH 33/79] Add support for nullable reference types - ServicePlatform (#7863) * Add support for nullable reference types - ServicePlatform * add relevant tests * add possible null exception warning * add nullable annotation attributes * Restructure code for nullability * Update src/NServiceBus.Core.Tests/ServicePlatform/Retries/RetryAcknowledgementBehaviorTests.cs Co-authored-by: Brandon Ording * Update src/NServiceBus.Core.Tests/ServicePlatform/Retries/RetryAcknowledgementBehaviorTests.cs Co-authored-by: Brandon Ording * update test to place null outside EnterMultipleScope(), use newer formatting for assertions --------- Co-authored-by: Brandon Ording --- .../RetryAcknowledgementBehaviorTests.cs | 13 ++++---- .../Retries/PlatformRetryNotifications.cs | 4 ++- .../Retries/RetryAcknowledgementBehavior.cs | 30 +++++++++---------- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/NServiceBus.Core.Tests/ServicePlatform/Retries/RetryAcknowledgementBehaviorTests.cs b/src/NServiceBus.Core.Tests/ServicePlatform/Retries/RetryAcknowledgementBehaviorTests.cs index ee1ededda3f..5ca4f061907 100644 --- a/src/NServiceBus.Core.Tests/ServicePlatform/Retries/RetryAcknowledgementBehaviorTests.cs +++ b/src/NServiceBus.Core.Tests/ServicePlatform/Retries/RetryAcknowledgementBehaviorTests.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Core.Tests.ServicePlatform.Retries; +#nullable enable + +namespace NServiceBus.Core.Tests.ServicePlatform.Retries; using System; using System.Collections.Generic; @@ -36,12 +38,13 @@ public async Task Should_confirm_successful_retries_to_acknowledgement_queue() Assert.That(outgoingMessage.Message.Headers.ContainsKey("ServiceControl.Retry.Successful"), Is.True); - Assert.That(outgoingMessage.Message.Body.Length, Is.EqualTo(0)); + Assert.That(outgoingMessage.Message.Body.Length, Is.Zero); Assert.That(outgoingMessage.Message.Headers[Headers.ControlMessageHeader], Is.EqualTo(bool.TrueString)); } var addressTag = outgoingMessage.RoutingStrategies.Single().Apply([]) as UnicastAddressTag; + Assert.That(addressTag, Is.Not.Null); using (Assert.EnterMultipleScope()) { Assert.That(addressTag.Destination, Is.EqualTo(acknowledgementQueue)); @@ -67,7 +70,7 @@ public void Should_not_confirm_when_processing_fails() using (Assert.EnterMultipleScope()) { Assert.That(exception, Is.SameAs(thrownException)); - Assert.That(routingPipeline.ForkInvocations.Count, Is.EqualTo(0)); + Assert.That(routingPipeline.ForkInvocations, Is.Empty); } } @@ -85,7 +88,7 @@ public async Task Should_not_confirm_when_message_does_not_contain_acknowledgeme using (Assert.EnterMultipleScope()) { - Assert.That(routingPipeline.ForkInvocations.Count, Is.EqualTo(0)); + Assert.That(routingPipeline.ForkInvocations, Is.Empty); Assert.That(context.Extensions.TryGet(out _), Is.False); } } @@ -103,7 +106,7 @@ public async Task Should_not_confirm_when_message_does_not_contain_retry_header( using (Assert.EnterMultipleScope()) { - Assert.That(routingPipeline.ForkInvocations.Count, Is.EqualTo(0)); + Assert.That(routingPipeline.ForkInvocations, Is.Empty); Assert.That(context.Extensions.TryGet(out _), Is.False); } } diff --git a/src/NServiceBus.Core/ServicePlatform/Retries/PlatformRetryNotifications.cs b/src/NServiceBus.Core/ServicePlatform/Retries/PlatformRetryNotifications.cs index c8d217111d6..08ad0788b06 100644 --- a/src/NServiceBus.Core/ServicePlatform/Retries/PlatformRetryNotifications.cs +++ b/src/NServiceBus.Core/ServicePlatform/Retries/PlatformRetryNotifications.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Features; +#nullable enable + +namespace NServiceBus.Features; /// /// Provides notifications to ServiceControl about successfully retried messages. diff --git a/src/NServiceBus.Core/ServicePlatform/Retries/RetryAcknowledgementBehavior.cs b/src/NServiceBus.Core/ServicePlatform/Retries/RetryAcknowledgementBehavior.cs index 0b15a8d79c3..b493af54abd 100644 --- a/src/NServiceBus.Core/ServicePlatform/Retries/RetryAcknowledgementBehavior.cs +++ b/src/NServiceBus.Core/ServicePlatform/Retries/RetryAcknowledgementBehavior.cs @@ -1,7 +1,10 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Pipeline; using Routing; @@ -14,23 +17,13 @@ class RetryAcknowledgementBehavior : IForkConnector next) { - var useRetryAcknowledgement = IsRetriedMessage(context, out var id, out var acknowledgementQueue); + RoutingContext? routingContext = null; - if (useRetryAcknowledgement) + if (IsRetriedMessage(context, out var id, out var acknowledgementQueue)) { // notify the ServiceControl audit instance that the retry has already been acknowledged by the endpoint context.Extensions.Set(MarkAsAcknowledgedBehavior.State.Instance); - } - - await next(context).ConfigureAwait(false); - - if (useRetryAcknowledgement) - { - await ConfirmSuccessfulRetry().ConfigureAwait(false); - } - async Task ConfirmSuccessfulRetry() - { var messageToDispatch = new OutgoingMessage( CombGuid.Generate().ToString(), new Dictionary @@ -40,12 +33,19 @@ async Task ConfirmSuccessfulRetry() { Headers.ControlMessageHeader, bool.TrueString } }, Array.Empty()); - var routingContext = new RoutingContext(messageToDispatch, new UnicastRoutingStrategy(acknowledgementQueue), context); + + routingContext = new RoutingContext(messageToDispatch, new UnicastRoutingStrategy(acknowledgementQueue), context); + } + + await next(context).ConfigureAwait(false); + + if (routingContext is not null) + { await this.Fork(routingContext).ConfigureAwait(false); } } - static bool IsRetriedMessage(ITransportReceiveContext context, out string retryUniqueMessageId, out string retryAcknowledgementQueue) + static bool IsRetriedMessage(ITransportReceiveContext context, [NotNullWhen(true)] out string? retryUniqueMessageId, [NotNullWhen(true)] out string? retryAcknowledgementQueue) { // check if the message is coming from a manual retry attempt if (context.Message.Headers.TryGetValue(RetryUniqueMessageIdHeaderKey, out var uniqueMessageId) && From fd96308bcccbfe71c8d54545490b9b02cb099634 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Sat, 25 Jul 2026 15:52:00 +0000 Subject: [PATCH 34/79] Update dependency GitHubActionsTestLogger to 3.0.5 (#7881) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- .../NServiceBus.AcceptanceTests.csproj | 2 +- .../NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj | 2 +- src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj | 2 +- .../NServiceBus.Learning.AcceptanceTests.csproj | 2 +- .../NServiceBus.PersistenceTests.csproj | 2 +- .../NServiceBus.TransportTests.csproj | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj b/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj index 6ae319dd777..037e3ae0421 100644 --- a/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj +++ b/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj index ee99299d5f1..66c68d2c04f 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj index 7866421d0d9..3ebaa6af8dc 100644 --- a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj +++ b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj b/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj index b4b89f5cd00..0f603634cff 100644 --- a/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj +++ b/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj b/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj index 3fb3d82f667..f64554b017e 100644 --- a/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj +++ b/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj b/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj index 893980c67de..4840571374f 100644 --- a/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj +++ b/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj @@ -9,7 +9,7 @@ - + From 42ec962cb3c3936a3ea8596fc98f4a85de397145 Mon Sep 17 00:00:00 2001 From: Michelle Chamberlin Date: Mon, 27 Jul 2026 13:13:28 -0700 Subject: [PATCH 35/79] Add support for nullable reference types - Utils (#7862) * nullable enable in all Utils files * add #nullable enable to relevant tests * Add additional file * Formatting * Simplify implementation * be explicity about nameof the argument in the ArgumentNullException * missed one line * remove the null operator as this is an interal call and we can ensure null is not in passed here * remove the test since we removed the null behavior * Add paramName to call * Simplify with new language features --------- Co-authored-by: Brandon Ording --- .../Utils/Reflection/InspectTests.cs | 12 ++++--- .../Reflection/MethodInfoExtensionTests.cs | 22 +++++++------ .../Reflection/TypeExtensionMethodsTests.cs | 4 ++- .../Utils/FileVersionRetriever.cs | 8 +++-- .../Utils/LazyArrayPoolBufferWriter.cs | 1 + src/NServiceBus.Core/Utils/PathUtilities.cs | 4 ++- .../Utils/Reflection/DelegateFactory.cs | 31 +++++++++++++------ .../Utils/Reflection/TypeExtensionMethods.cs | 15 ++++----- 8 files changed, 58 insertions(+), 39 deletions(-) diff --git a/src/NServiceBus.Core.Tests/Utils/Reflection/InspectTests.cs b/src/NServiceBus.Core.Tests/Utils/Reflection/InspectTests.cs index ca2b095c302..2a543a2979b 100644 --- a/src/NServiceBus.Core.Tests/Utils/Reflection/InspectTests.cs +++ b/src/NServiceBus.Core.Tests/Utils/Reflection/InspectTests.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Core.Utils.Reflection; +#nullable enable + +namespace NServiceBus.Core.Utils.Reflection; using System; using NUnit.Framework; @@ -19,7 +21,7 @@ public void Should_return_property_name() public class Target { - public string Property { get; set; } + public required string Property { get; set; } } } @@ -36,7 +38,7 @@ public void Should_throw() public class Target { - public string Field; + public required string Field; } } @@ -61,12 +63,12 @@ public void Should_throw_when_dots_not_allowed() public class Target1 { - public Target2 Property1 { get; set; } + public required Target2 Property1 { get; set; } } public class Target2 { - public string Property2 { get; set; } + public required string Property2 { get; set; } } } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/Utils/Reflection/MethodInfoExtensionTests.cs b/src/NServiceBus.Core.Tests/Utils/Reflection/MethodInfoExtensionTests.cs index 04449dec318..a71350a394b 100644 --- a/src/NServiceBus.Core.Tests/Utils/Reflection/MethodInfoExtensionTests.cs +++ b/src/NServiceBus.Core.Tests/Utils/Reflection/MethodInfoExtensionTests.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus.Core.Utils.Reflection; using System.Reflection; @@ -10,7 +12,7 @@ public class MethodInfoExtensionTests public void When_method_has_no_return_type_Should_return_null() { var instance = new TypeWithMethods(); - var result = TypeWithMethods.MethodWithNoReturnInfo.InvokeGeneric(instance, [typeof(InputType)]); + var result = TypeWithMethods.MethodWithNoReturnInfo!.InvokeGeneric(instance, [typeof(InputType)]); Assert.That(result, Is.Null); } @@ -19,7 +21,7 @@ public void When_method_has_no_return_type_Should_return_null() public void When_method_has_return_type_Should_return_value() { var instance = new TypeWithMethods(); - var result = TypeWithMethods.MethodWithReturnInfo.InvokeGeneric(instance, [typeof(InputType)]); + var result = TypeWithMethods.MethodWithReturnInfo!.InvokeGeneric(instance, [typeof(InputType)]); Assert.That(result, Is.EqualTo(42)); } @@ -28,7 +30,7 @@ public void When_method_has_return_type_Should_return_value() public void When_method_has_return_type_Should_return_strong_typed_value() { var instance = new TypeWithMethods(); - var result = TypeWithMethods.MethodWithReturnInfo.InvokeGeneric(instance, [], [typeof(InputType)]); + var result = TypeWithMethods.MethodWithReturnInfo!.InvokeGeneric(instance, [], [typeof(InputType)]); Assert.That(result, Is.EqualTo(42)); } @@ -36,7 +38,7 @@ public void When_method_has_return_type_Should_return_strong_typed_value() [Test] public void When_static_method_has_no_return_type_Should_return_null() { - var result = TypeWithMethods.StaticMethodWithNoReturnInfo.InvokeGeneric(null, [], [typeof(InputType)]); + var result = TypeWithMethods.StaticMethodWithNoReturnInfo!.InvokeGeneric(null, [], [typeof(InputType)]); Assert.That(result, Is.Null); } @@ -44,7 +46,7 @@ public void When_static_method_has_no_return_type_Should_return_null() [Test] public void When_static_method_has_return_type_Should_return_value() { - var result = TypeWithMethods.StaticMethodWithReturnInfo.InvokeGeneric(null, [typeof(InputType)]); + var result = TypeWithMethods.StaticMethodWithReturnInfo!.InvokeGeneric(null, [typeof(InputType)]); Assert.That(result, Is.EqualTo(42)); } @@ -52,7 +54,7 @@ public void When_static_method_has_return_type_Should_return_value() [Test] public void When_static_method_has_return_type_Should_return_strong_typed_value() { - var result = TypeWithMethods.StaticMethodWithReturnInfo.InvokeGeneric(null, [], [typeof(InputType)]); + var result = TypeWithMethods.StaticMethodWithReturnInfo!.InvokeGeneric(null, [], [typeof(InputType)]); Assert.That(result, Is.EqualTo(42)); } @@ -70,9 +72,9 @@ public static void StaticMethodWithNoReturn() { } public static int StaticMethodWithReturn() => 42; - public static MethodInfo StaticMethodWithNoReturnInfo = typeof(TypeWithMethods).GetMethod("StaticMethodWithNoReturn", BindingFlags.Public | BindingFlags.Static); - public static MethodInfo MethodWithNoReturnInfo = typeof(TypeWithMethods).GetMethod("MethodWithNoReturn", BindingFlags.Public | BindingFlags.Instance); - public static MethodInfo MethodWithReturnInfo = typeof(TypeWithMethods).GetMethod("MethodWithReturn", BindingFlags.Public | BindingFlags.Instance); - public static MethodInfo StaticMethodWithReturnInfo = typeof(TypeWithMethods).GetMethod("StaticMethodWithReturn", BindingFlags.Public | BindingFlags.Static); + public static MethodInfo? StaticMethodWithNoReturnInfo = typeof(TypeWithMethods).GetMethod("StaticMethodWithNoReturn", BindingFlags.Public | BindingFlags.Static); + public static MethodInfo? MethodWithNoReturnInfo = typeof(TypeWithMethods).GetMethod("MethodWithNoReturn", BindingFlags.Public | BindingFlags.Instance); + public static MethodInfo? MethodWithReturnInfo = typeof(TypeWithMethods).GetMethod("MethodWithReturn", BindingFlags.Public | BindingFlags.Instance); + public static MethodInfo? StaticMethodWithReturnInfo = typeof(TypeWithMethods).GetMethod("StaticMethodWithReturn", BindingFlags.Public | BindingFlags.Static); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/Utils/Reflection/TypeExtensionMethodsTests.cs b/src/NServiceBus.Core.Tests/Utils/Reflection/TypeExtensionMethodsTests.cs index 5283837b9ec..bb80fb7dee9 100644 --- a/src/NServiceBus.Core.Tests/Utils/Reflection/TypeExtensionMethodsTests.cs +++ b/src/NServiceBus.Core.Tests/Utils/Reflection/TypeExtensionMethodsTests.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Core.Utils.Reflection; +#nullable enable + +namespace NServiceBus.Core.Utils.Reflection; using System; using System.Collections.Generic; diff --git a/src/NServiceBus.Core/Utils/FileVersionRetriever.cs b/src/NServiceBus.Core/Utils/FileVersionRetriever.cs index 581bba0cf34..cef30c9f37f 100644 --- a/src/NServiceBus.Core/Utils/FileVersionRetriever.cs +++ b/src/NServiceBus.Core/Utils/FileVersionRetriever.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System; using System.Diagnostics; @@ -19,11 +21,11 @@ public static string GetFileVersion(Assembly assembly) var fileVersionAttribute = assembly.GetCustomAttribute(); - if (Version.TryParse(fileVersionAttribute.Version, out var version)) + if (Version.TryParse(fileVersionAttribute?.Version, out var version)) { return version.ToString(3); } - return assembly.GetName().Version.ToString(3); + return assembly.GetName().Version?.ToString(3) ?? "0.0.0"; } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Utils/LazyArrayPoolBufferWriter.cs b/src/NServiceBus.Core/Utils/LazyArrayPoolBufferWriter.cs index 8cd0715741f..a125bd40c07 100644 --- a/src/NServiceBus.Core/Utils/LazyArrayPoolBufferWriter.cs +++ b/src/NServiceBus.Core/Utils/LazyArrayPoolBufferWriter.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Utils/PathUtilities.cs b/src/NServiceBus.Core/Utils/PathUtilities.cs index 43fd3cb3dac..4aa37249096 100644 --- a/src/NServiceBus.Core/Utils/PathUtilities.cs +++ b/src/NServiceBus.Core/Utils/PathUtilities.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Utils/Reflection/DelegateFactory.cs b/src/NServiceBus.Core/Utils/Reflection/DelegateFactory.cs index 2d088464b7e..7ec89c04603 100644 --- a/src/NServiceBus.Core/Utils/Reflection/DelegateFactory.cs +++ b/src/NServiceBus.Core/Utils/Reflection/DelegateFactory.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; using System; @@ -12,6 +14,8 @@ public static Func CreateGet(PropertyInfo property) { if (!PropertyInfoToLateBoundProperty.TryGetValue(property, out var lateBoundPropertyGet)) { + ArgumentNullException.ThrowIfNull(property.DeclaringType, nameof(property)); + var instanceParameter = Expression.Parameter(typeof(object), "target"); var member = Expression.Property(Expression.Convert(instanceParameter, property.DeclaringType), property); @@ -32,6 +36,8 @@ public static Func CreateGet(FieldInfo field) { if (!FieldInfoToLateBoundField.TryGetValue(field, out var lateBoundFieldGet)) { + ArgumentNullException.ThrowIfNull(field.DeclaringType, nameof(field)); + var instanceParameter = Expression.Parameter(typeof(object), "target"); var member = Expression.Field(Expression.Convert(instanceParameter, field.DeclaringType), field); @@ -52,12 +58,14 @@ public static Action CreateSet(FieldInfo field) { if (!FieldInfoToLateBoundFieldSet.TryGetValue(field, out var callback)) { + ArgumentNullException.ThrowIfNull(field.DeclaringType, nameof(field)); + var sourceType = field.DeclaringType; - var method = new DynamicMethod("Set" + field.Name, null, new[] - { + var method = new DynamicMethod("Set" + field.Name, null, + [ typeof(object), typeof(object) - }, true); + ], true); var gen = method.GetILGenerator(); gen.Emit(OpCodes.Ldarg_0); // Load input to stack @@ -87,15 +95,18 @@ public static Action CreateSet(PropertyInfo property) { if (!PropertyInfoToLateBoundPropertySet.TryGetValue(property, out var result)) { - var method = new DynamicMethod("Set" + property.Name, null, new[] - { + ArgumentNullException.ThrowIfNull(property.DeclaringType, nameof(property)); + + var method = new DynamicMethod("Set" + property.Name, null, + [ typeof(object), typeof(object) - }, true); + ], true); var gen = method.GetILGenerator(); var sourceType = property.DeclaringType; var setter = property.GetSetMethod(true); + ArgumentNullException.ThrowIfNull(setter, nameof(property)); gen.Emit(OpCodes.Ldarg_0); // Load input to stack @@ -129,8 +140,8 @@ public static Action CreateSet(PropertyInfo property) return result; } - static readonly ConcurrentDictionary> PropertyInfoToLateBoundProperty = new ConcurrentDictionary>(); - static readonly ConcurrentDictionary> FieldInfoToLateBoundField = new ConcurrentDictionary>(); - static readonly ConcurrentDictionary> PropertyInfoToLateBoundPropertySet = new ConcurrentDictionary>(); - static readonly ConcurrentDictionary> FieldInfoToLateBoundFieldSet = new ConcurrentDictionary>(); + static readonly ConcurrentDictionary> PropertyInfoToLateBoundProperty = new(); + static readonly ConcurrentDictionary> FieldInfoToLateBoundField = new(); + static readonly ConcurrentDictionary> PropertyInfoToLateBoundPropertySet = new(); + static readonly ConcurrentDictionary> FieldInfoToLateBoundFieldSet = new(); } diff --git a/src/NServiceBus.Core/Utils/Reflection/TypeExtensionMethods.cs b/src/NServiceBus.Core/Utils/Reflection/TypeExtensionMethods.cs index 89920d270e6..c4b0a3a26d1 100644 --- a/src/NServiceBus.Core/Utils/Reflection/TypeExtensionMethods.cs +++ b/src/NServiceBus.Core/Utils/Reflection/TypeExtensionMethods.cs @@ -1,9 +1,10 @@ +#nullable enable + namespace NServiceBus; using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; static class TypeExtensionMethods { @@ -46,16 +47,13 @@ public bool IsSystemType() } public bool IsFromParticularAssembly() => type.Assembly.IsParticularAssembly(); - } - extension([NotNull] Type t) - { /// /// Takes the name of the given type and makes it friendly for serialization /// by removing problematic characters. /// public string SerializationFriendlyName() => - TypeToNameLookup.GetOrAdd(t.TypeHandle, static (typeHandle, t) => + TypeToNameLookup.GetOrAdd(type.TypeHandle, static (typeHandle, t) => { var index = t.Name.IndexOf('`'); if (index >= 0) @@ -82,14 +80,13 @@ public string SerializationFriendlyName() => return result; } - return Type.GetTypeFromHandle(typeHandle)?.Name; - }, t); + return t.Name; + }, type); } static bool IsClrType(ReadOnlySpan publicKeyToken) => publicKeyToken.SequenceEqual(MsPublicKeyToken); - static readonly byte[] MsPublicKeyToken = typeof(string).Assembly.GetName().GetPublicKeyToken(); - + static readonly byte[] MsPublicKeyToken = typeof(string).Assembly.GetName().GetPublicKeyToken() ?? []; static readonly ConcurrentDictionary IsSystemTypeCache = new(); static readonly ConcurrentDictionary TypeToNameLookup = new(); } \ No newline at end of file From a1cd28540e2bbf504591cf12c6b3d10cb51a27c4 Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Mon, 27 Jul 2026 13:59:20 -0700 Subject: [PATCH 36/79] Add support for nullable types - StaticHeaders (#7852) * add-support for nullable * add ArgumentException.ThrowIfNullOrWhiteSpace for header value * Comment out validation and leave TODO for next major --------- Co-authored-by: Brandon Ording --- .../NullableAnnotations.ApproveNullableTypes.approved.txt | 1 - .../StaticHeaders/ApplyStaticHeadersBehavior.cs | 2 ++ src/NServiceBus.Core/StaticHeaders/CurrentStaticHeaders.cs | 2 ++ src/NServiceBus.Core/StaticHeaders/StaticHeaders.cs | 4 +++- .../StaticHeaders/StaticHeadersConfigExtensions.cs | 5 +++++ 5 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt index c9f0102bf54..9b63b55ef16 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt @@ -63,7 +63,6 @@ NServiceBus.SerializationContextExtensions NServiceBus.SerializationExtensionsExtensions NServiceBus.Settings.IReadOnlySettings NServiceBus.Settings.SettingsHolder -NServiceBus.StaticHeadersConfigExtensions NServiceBus.SubscriptionMigrationModeSettings NServiceBus.Transport.DispatchProperties NServiceBus.Transport.ErrorContext diff --git a/src/NServiceBus.Core/StaticHeaders/ApplyStaticHeadersBehavior.cs b/src/NServiceBus.Core/StaticHeaders/ApplyStaticHeadersBehavior.cs index e4bae3b6d0c..24efd759922 100644 --- a/src/NServiceBus.Core/StaticHeaders/ApplyStaticHeadersBehavior.cs +++ b/src/NServiceBus.Core/StaticHeaders/ApplyStaticHeadersBehavior.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/StaticHeaders/CurrentStaticHeaders.cs b/src/NServiceBus.Core/StaticHeaders/CurrentStaticHeaders.cs index 013afb13eb8..b706539b228 100644 --- a/src/NServiceBus.Core/StaticHeaders/CurrentStaticHeaders.cs +++ b/src/NServiceBus.Core/StaticHeaders/CurrentStaticHeaders.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; using System.Collections.Generic; diff --git a/src/NServiceBus.Core/StaticHeaders/StaticHeaders.cs b/src/NServiceBus.Core/StaticHeaders/StaticHeaders.cs index b85c3e258fd..535254c2a92 100644 --- a/src/NServiceBus.Core/StaticHeaders/StaticHeaders.cs +++ b/src/NServiceBus.Core/StaticHeaders/StaticHeaders.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using Features; diff --git a/src/NServiceBus.Core/StaticHeaders/StaticHeadersConfigExtensions.cs b/src/NServiceBus.Core/StaticHeaders/StaticHeadersConfigExtensions.cs index ecfd80e3a90..382a947ef41 100644 --- a/src/NServiceBus.Core/StaticHeaders/StaticHeadersConfigExtensions.cs +++ b/src/NServiceBus.Core/StaticHeaders/StaticHeadersConfigExtensions.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; using System; @@ -20,6 +22,9 @@ public static void AddHeaderToAllOutgoingMessages(this EndpointConfiguration con { ArgumentException.ThrowIfNullOrWhiteSpace(key); + // TODO Uncomment this in the next major version so that we actually enforce non-null values here + //ArgumentException.ThrowIfNullOrWhiteSpace(value); + if (!config.Settings.TryGet(out CurrentStaticHeaders headers)) { headers = []; From 506b78d04c9f7cf3184ae0deb0b390eebdac8236 Mon Sep 17 00:00:00 2001 From: Jayanthi Date: Mon, 27 Jul 2026 14:19:11 -0700 Subject: [PATCH 37/79] Add support for nullable reference types - Serialization (#7859) * added support for nullable types * added approval file * fixed spacing * add null check for endpointConfigurationSettings --- .../APIApprovals.ApproveNServiceBus.approved.txt | 2 +- ...llableAnnotations.ApproveNullableTypes.approved.txt | 6 ------ .../Serialization/IMessageSerializer.cs | 4 +++- .../Serialization/SerializationConfigExtensions.cs | 4 +++- .../Serialization/SerializationContextExtensions.cs | 2 ++ .../Serialization/SerializationDefinition.cs | 4 +++- .../Serialization/SerializationExtensions.cs | 10 ++++++++-- .../Serialization/SerializationExtensionsExtensions.cs | 4 +++- .../Serialization/SerializationFeature.cs | 4 +++- .../Serialization/SerializationSettingsExtensions.cs | 2 ++ 10 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index 6a55b5f799b..f51ac3efde3 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -2382,7 +2382,7 @@ namespace NServiceBus.Serialization public interface IMessageSerializer { string ContentType { get; } - object[] Deserialize(System.ReadOnlyMemory body, System.Collections.Generic.IList messageTypes = null); + object[] Deserialize(System.ReadOnlyMemory body, System.Collections.Generic.IList? messageTypes = null); void Serialize(object message, System.IO.Stream stream); } public abstract class SerializationDefinition diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt index 9b63b55ef16..6ad5dbcb11c 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt @@ -55,12 +55,6 @@ NServiceBus.Sagas.SagaCorrelationProperty NServiceBus.Sagas.SagaFinderDefinition NServiceBus.Sagas.SagaIdGeneratorContext NServiceBus.SagasConfigExtensions -NServiceBus.Serialization.IMessageSerializer -NServiceBus.Serialization.SerializationDefinition -NServiceBus.Serialization.SerializationExtensions`1 -NServiceBus.SerializationConfigExtensions -NServiceBus.SerializationContextExtensions -NServiceBus.SerializationExtensionsExtensions NServiceBus.Settings.IReadOnlySettings NServiceBus.Settings.SettingsHolder NServiceBus.SubscriptionMigrationModeSettings diff --git a/src/NServiceBus.Core/Serialization/IMessageSerializer.cs b/src/NServiceBus.Core/Serialization/IMessageSerializer.cs index 680807eea95..b2e52fa5a1f 100644 --- a/src/NServiceBus.Core/Serialization/IMessageSerializer.cs +++ b/src/NServiceBus.Core/Serialization/IMessageSerializer.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus.Serialization; using System; @@ -30,5 +32,5 @@ public interface IMessageSerializer /// serialized data. /// /// Deserialized messages. - object[] Deserialize(ReadOnlyMemory body, IList messageTypes = null); + object[] Deserialize(ReadOnlyMemory body, IList? messageTypes = null); } \ No newline at end of file diff --git a/src/NServiceBus.Core/Serialization/SerializationConfigExtensions.cs b/src/NServiceBus.Core/Serialization/SerializationConfigExtensions.cs index f9a6cf6f80e..77cf076ef1d 100644 --- a/src/NServiceBus.Core/Serialization/SerializationConfigExtensions.cs +++ b/src/NServiceBus.Core/Serialization/SerializationConfigExtensions.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System; using Configuration.AdvancedExtensibility; diff --git a/src/NServiceBus.Core/Serialization/SerializationContextExtensions.cs b/src/NServiceBus.Core/Serialization/SerializationContextExtensions.cs index bc4e657c085..e37423243be 100644 --- a/src/NServiceBus.Core/Serialization/SerializationContextExtensions.cs +++ b/src/NServiceBus.Core/Serialization/SerializationContextExtensions.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Serialization/SerializationDefinition.cs b/src/NServiceBus.Core/Serialization/SerializationDefinition.cs index 0106f5c06d1..06e93d5183c 100644 --- a/src/NServiceBus.Core/Serialization/SerializationDefinition.cs +++ b/src/NServiceBus.Core/Serialization/SerializationDefinition.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Serialization; +#nullable enable + +namespace NServiceBus.Serialization; using System; using MessageInterfaces; diff --git a/src/NServiceBus.Core/Serialization/SerializationExtensions.cs b/src/NServiceBus.Core/Serialization/SerializationExtensions.cs index 4c720ecb7be..2c0d46a06d7 100644 --- a/src/NServiceBus.Core/Serialization/SerializationExtensions.cs +++ b/src/NServiceBus.Core/Serialization/SerializationExtensions.cs @@ -1,5 +1,8 @@ -namespace NServiceBus.Serialization; +#nullable enable +namespace NServiceBus.Serialization; + +using System; using Configuration.AdvancedExtensibility; using Settings; @@ -13,7 +16,10 @@ public class SerializationExtensions : ExposeSettings where T : Serialization /// Initializes a new instance of . /// public SerializationExtensions(SettingsHolder serializerSettings, SettingsHolder endpointConfigurationSettings) : base(serializerSettings) - => EndpointConfigurationSettings = endpointConfigurationSettings; + { + ArgumentNullException.ThrowIfNull(endpointConfigurationSettings); + EndpointConfigurationSettings = endpointConfigurationSettings; + } // provides access to the settings backing EndpointConfiguration. The settings provided by the 'Settings' property are isolated settings for the serializer. internal readonly SettingsHolder EndpointConfigurationSettings; diff --git a/src/NServiceBus.Core/Serialization/SerializationExtensionsExtensions.cs b/src/NServiceBus.Core/Serialization/SerializationExtensionsExtensions.cs index efcc9eebeec..99a4602bc40 100644 --- a/src/NServiceBus.Core/Serialization/SerializationExtensionsExtensions.cs +++ b/src/NServiceBus.Core/Serialization/SerializationExtensionsExtensions.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System; using Serialization; diff --git a/src/NServiceBus.Core/Serialization/SerializationFeature.cs b/src/NServiceBus.Core/Serialization/SerializationFeature.cs index a332f50bd37..09a793ba578 100644 --- a/src/NServiceBus.Core/Serialization/SerializationFeature.cs +++ b/src/NServiceBus.Core/Serialization/SerializationFeature.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System; using System.Collections.Generic; diff --git a/src/NServiceBus.Core/Serialization/SerializationSettingsExtensions.cs b/src/NServiceBus.Core/Serialization/SerializationSettingsExtensions.cs index 4c8147f8439..89d274c9fa4 100644 --- a/src/NServiceBus.Core/Serialization/SerializationSettingsExtensions.cs +++ b/src/NServiceBus.Core/Serialization/SerializationSettingsExtensions.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; using System; From 63ac7b855e9b82d362b6cdb2745c74c435cce57c Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Tue, 28 Jul 2026 17:41:49 +0200 Subject: [PATCH 38/79] Add AOT-safe startup diagnostics serialization (#7882) * Add characterization tests for startup diagnostics writer * Add metadata overloads for AOT-safe diagnostics * Refactor writer to serialize one section at a time * Replace reflection-based factory with Func wrapper * Migrate Core diagnostics registrations to metadata overloads * Migrate remaining Core diagnostics registrations to metadata-bearing overloads * Update trimmability warnings after writer refactoring * Extract test writer helpers to reduce boilerplate * Use alias for Encoding.UTF8 to improve readability * Refactor entry resolution to use array syntax for improved clarity * Fix JSON contract: preserve original CLR types in diagnostics DTOs * Fix assembly scanning log compaction for metadata-backed entries * Add tests for metadata diagnostics APIs * Add targeted error for legacy sections when reflection serialization is disabled * Fix deduplication to avoid synthesized name collisions * Add deduplication collision test for synthesized name conflicts * Add real System.Type DTO with converter and rename projected-type test * Suppress IL2026 on legacy path guarded by IsReflectionEnabledByDefault * Make JsonPrettyPrinter AOT-safe via JsonElement.WriteTo * Add XML docs explaining factory vs value overload trade-off * Add PreObsolete to non-generic Add and AddStartupDiagnosticsSection * Spelling Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Remove non-generic AddStartupDiagnosticsSection method --------- Co-authored-by: Daniel Marbach Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ...IApprovals.ApproveNServiceBus.approved.txt | 4 + ...dSupportWritingToLogAndWriter.approved.txt | 1 - ...WriteAlphabeticalSectionOrder.approved.txt | 1 + ...sWithSynthesizedNameCollision.approved.txt | 1 + ...ouldWriteEscapedPropertyNames.approved.txt | 1 + ...teMixedTypedAndLegacySections.approved.txt | 1 + ...houldWriteNullAndNestedValues.approved.txt | 1 + ...uldWriteTypedDuplicateEntries.approved.txt | 1 + ...Tests.ShouldWriteTypedSection.approved.txt | 1 + ...dSectionWithNestedCollections.approved.txt | 1 + ...dSectionWithProjectedTypeName.approved.txt | 1 + ...iteTypedSectionWithSystemType.approved.txt | 1 + ...s.ApproveTrimmabilityWarnings.approved.txt | 6 - .../OpenTelemetry/DiagnosticsWriterTests.cs | 321 ++++++++++++++++-- .../TestDiagnosticsJsonContext.cs | 25 ++ src/NServiceBus.Core/Audit/Audit.cs | 6 +- src/NServiceBus.Core/EndpointCreator.cs | 13 +- .../Features/FeatureComponent.cs | 2 +- .../Helpers/AssemblyScanningComponent.cs | 2 +- .../Hosting/HostingComponent.Configuration.cs | 5 +- .../Hosting/HostingComponent.cs | 20 +- .../StartupDiagnostics/AuditDiagnostics.cs | 9 + .../ContainerDiagnostics.cs | 8 + .../DiagnosticSettingsExtensions.cs | 41 +++ .../StartupDiagnostics/EndpointDiagnostics.cs | 10 + .../HostStartupDiagnosticsWriter.cs | 123 +++++-- .../StartupDiagnostics/HostingDiagnostics.cs | 26 ++ .../InstallationDiagnostics.cs | 8 + .../StartupDiagnostics/JsonPrettyPrinter.cs | 16 +- .../LicensingDiagnostics.cs | 19 ++ .../StartupDiagnostics/MessagesDiagnostics.cs | 12 + .../PersistenceDiagnosticsEntry.cs | 9 + .../QueueAddressDiagnostics.cs | 13 + .../ReceivingDiagnostics.cs | 23 ++ .../RecoverabilityDiagnostics.cs | 12 + .../SerializationDiagnostics.cs | 26 ++ .../StartupDiagnosticEntries.cs | 49 +++ .../StartupDiagnosticsJsonContext.cs | 40 +++ .../Installation/InstallerComponent.cs | 6 +- .../Licensing/LicenseReminder.cs | 16 +- .../Persistence/PersistenceComponent.cs | 8 +- .../ReceiveComponent.Configuration.cs | 31 +- .../Receiving/ReceiveComponent.cs | 31 +- .../Recoverability/RecoverabilityComponent.cs | 8 +- .../Serialization/SerializationFeature.cs | 18 +- src/NServiceBus.Core/StartableEndpoint.cs | 5 +- .../Transports/ReceiveSettingsExtensions.cs | 2 +- 47 files changed, 840 insertions(+), 144 deletions(-) create mode 100644 src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteAlphabeticalSectionOrder.approved.txt create mode 100644 src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteDuplicateEntriesWithSynthesizedNameCollision.approved.txt create mode 100644 src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteEscapedPropertyNames.approved.txt create mode 100644 src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteMixedTypedAndLegacySections.approved.txt create mode 100644 src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteNullAndNestedValues.approved.txt create mode 100644 src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedDuplicateEntries.approved.txt create mode 100644 src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSection.approved.txt create mode 100644 src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSectionWithNestedCollections.approved.txt create mode 100644 src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSectionWithProjectedTypeName.approved.txt create mode 100644 src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSectionWithSystemType.approved.txt create mode 100644 src/NServiceBus.Core.Tests/OpenTelemetry/TestDiagnosticsJsonContext.cs create mode 100644 src/NServiceBus.Core/Hosting/StartupDiagnostics/AuditDiagnostics.cs create mode 100644 src/NServiceBus.Core/Hosting/StartupDiagnostics/ContainerDiagnostics.cs create mode 100644 src/NServiceBus.Core/Hosting/StartupDiagnostics/EndpointDiagnostics.cs create mode 100644 src/NServiceBus.Core/Hosting/StartupDiagnostics/HostingDiagnostics.cs create mode 100644 src/NServiceBus.Core/Hosting/StartupDiagnostics/InstallationDiagnostics.cs create mode 100644 src/NServiceBus.Core/Hosting/StartupDiagnostics/LicensingDiagnostics.cs create mode 100644 src/NServiceBus.Core/Hosting/StartupDiagnostics/MessagesDiagnostics.cs create mode 100644 src/NServiceBus.Core/Hosting/StartupDiagnostics/PersistenceDiagnosticsEntry.cs create mode 100644 src/NServiceBus.Core/Hosting/StartupDiagnostics/QueueAddressDiagnostics.cs create mode 100644 src/NServiceBus.Core/Hosting/StartupDiagnostics/ReceivingDiagnostics.cs create mode 100644 src/NServiceBus.Core/Hosting/StartupDiagnostics/RecoverabilityDiagnostics.cs create mode 100644 src/NServiceBus.Core/Hosting/StartupDiagnostics/SerializationDiagnostics.cs create mode 100644 src/NServiceBus.Core/Hosting/StartupDiagnostics/StartupDiagnosticsJsonContext.cs diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index f51ac3efde3..50f0ba87712 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -242,6 +242,8 @@ namespace NServiceBus public static class DiagnosticSettingsExtensions { public static void AddStartupDiagnosticsSection(this NServiceBus.Settings.IReadOnlySettings settings, string sectionName, object section) { } + public static void AddStartupDiagnosticsSection(this NServiceBus.Settings.IReadOnlySettings settings, string sectionName, T section, System.Text.Json.Serialization.Metadata.JsonTypeInfo typeInfo) { } + public static void AddStartupDiagnosticsSectionFactory(this NServiceBus.Settings.IReadOnlySettings settings, string sectionName, System.Func sectionFactory, System.Text.Json.Serialization.Metadata.JsonTypeInfo typeInfo) { } public static void CustomDiagnosticsWriter(this NServiceBus.EndpointConfiguration config, System.Func customDiagnosticsWriter) { } public static void SetDiagnosticsPath(this NServiceBus.EndpointConfiguration config, string path) { } public static void WriteDiagnosticsToLog(this NServiceBus.EndpointConfiguration config) { } @@ -1069,6 +1071,8 @@ namespace NServiceBus { public StartupDiagnosticEntries() { } public void Add(string sectionName, object section) { } + public void Add(string sectionName, T section, System.Text.Json.Serialization.Metadata.JsonTypeInfo typeInfo) { } + public void AddFactory(string sectionName, System.Func sectionFactory, System.Text.Json.Serialization.Metadata.JsonTypeInfo typeInfo) { } public class StartupDiagnosticEntry { public StartupDiagnosticEntry() { } diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldSupportWritingToLogAndWriter.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldSupportWritingToLogAndWriter.approved.txt index 8288aa603b5..c8850eae524 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldSupportWritingToLogAndWriter.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldSupportWritingToLogAndWriter.approved.txt @@ -1,3 +1,2 @@ {"Endpoint":{"EndpointName":"MyEndpointOne"}} - INFO TestingLoggerFactory Logging to testing logger with level Debug INFO NServiceBus.HostStartupDiagnosticsWriter Startup diagnostics: {"Endpoint":{"EndpointName":"MyEndpointOne"}}. diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteAlphabeticalSectionOrder.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteAlphabeticalSectionOrder.approved.txt new file mode 100644 index 00000000000..72902b4d1f7 --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteAlphabeticalSectionOrder.approved.txt @@ -0,0 +1 @@ +{"Alpha":{"Value":"a"},"Mike":{"Value":"m"},"Zulu":{"Value":"z"}} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteDuplicateEntriesWithSynthesizedNameCollision.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteDuplicateEntriesWithSynthesizedNameCollision.approved.txt new file mode 100644 index 00000000000..0f2236a5255 --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteDuplicateEntriesWithSynthesizedNameCollision.approved.txt @@ -0,0 +1 @@ +{"Section":{"Value":"First"},"Section-2":{"Value":"AlreadySynthesized"},"Section-3":{"Value":"Third"}} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteEscapedPropertyNames.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteEscapedPropertyNames.approved.txt new file mode 100644 index 00000000000..3d9befbeea8 --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteEscapedPropertyNames.approved.txt @@ -0,0 +1 @@ +{"Special":{"Property With Spaces":true,"Normal":42}} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteMixedTypedAndLegacySections.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteMixedTypedAndLegacySections.approved.txt new file mode 100644 index 00000000000..bdeaec570f8 --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteMixedTypedAndLegacySections.approved.txt @@ -0,0 +1 @@ +{"Legacy":{"Value":"old"},"Typed":{"Name":"MyEndpoint","SendOnly":false,"NServiceBusVersion":"1.0.0"}} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteNullAndNestedValues.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteNullAndNestedValues.approved.txt new file mode 100644 index 00000000000..99ec4686f2e --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteNullAndNestedValues.approved.txt @@ -0,0 +1 @@ +{"Nested":{"Inner":{"Deep":"value"}},"NullValue":{"Value":null}} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedDuplicateEntries.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedDuplicateEntries.approved.txt new file mode 100644 index 00000000000..5dc714ebc94 --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedDuplicateEntries.approved.txt @@ -0,0 +1 @@ +{"Section":{"Name":"First","SendOnly":false,"NServiceBusVersion":"1.0.0"},"Section-2":{"Name":"Second","SendOnly":false,"NServiceBusVersion":"2.0.0"}} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSection.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSection.approved.txt new file mode 100644 index 00000000000..278c2c516ce --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSection.approved.txt @@ -0,0 +1 @@ +{"Typed":{"Name":"MyEndpoint","SendOnly":false,"NServiceBusVersion":"1.0.0"}} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSectionWithNestedCollections.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSectionWithNestedCollections.approved.txt new file mode 100644 index 00000000000..2138da8a4ec --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSectionWithNestedCollections.approved.txt @@ -0,0 +1 @@ +{"NestedCollections":{"LocalQueueAddress":{"BaseAddress":"myqueue","Discriminator":null,"Properties":{},"Qualifier":null},"InstanceSpecificQueueAddress":null,"PurgeOnStartup":false,"TransactionMode":"TransactionScope","MaxConcurrency":10,"Satellites":[{"Name":"Sat1","ReceiveAddress":{"BaseAddress":"satqueue","Discriminator":null,"Properties":{},"Qualifier":null},"MaxConcurrency":5}],"MessageHandlers":{"MsgType1":["Handler1","Handler2"]}}} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSectionWithProjectedTypeName.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSectionWithProjectedTypeName.approved.txt new file mode 100644 index 00000000000..cfe55f962c4 --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSectionWithProjectedTypeName.approved.txt @@ -0,0 +1 @@ +{"WithType":{"Type":"NServiceBus.Core.Tests.OpenTelemetry.DiagnosticsWriterTests"}} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSectionWithSystemType.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSectionWithSystemType.approved.txt new file mode 100644 index 00000000000..8f910cd1ac3 --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSectionWithSystemType.approved.txt @@ -0,0 +1 @@ +{"WithType":{"TypeValue":"NServiceBus.Core.Tests.OpenTelemetry.DiagnosticsWriterTests"}} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt index 1fb90478f9d..89061f0bc76 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt @@ -13,12 +13,6 @@ src/NServiceBus.Core/Hosting/StartupDiagnostics/Host.cs IL2075: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods' in call to 'System.Type.GetMethod(String, BindingFlags)'. The return value of method 'System.Reflection.Assembly.GetType(String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. IL2075: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperty(String, BindingFlags)'. The return value of method 'System.Reflection.Assembly.GetType(String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. -src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriter.cs - IL2026: Using member 'System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. - -src/NServiceBus.Core/Hosting/StartupDiagnostics/JsonPrettyPrinter.cs - IL2026: Using member 'System.Text.Json.JsonSerializer.Serialize(TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved. - src/NServiceBus.Core/Pipeline/RegisterStep.cs IL2072: 'instanceType' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicConstructors' in call to 'Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateInstance(IServiceProvider, Type, params Object[])'. The return value of method 'NServiceBus.Pipeline.RegisterStep.BehaviorType.get' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. diff --git a/src/NServiceBus.Core.Tests/OpenTelemetry/DiagnosticsWriterTests.cs b/src/NServiceBus.Core.Tests/OpenTelemetry/DiagnosticsWriterTests.cs index 217a90555cf..dd4fa9805d8 100644 --- a/src/NServiceBus.Core.Tests/OpenTelemetry/DiagnosticsWriterTests.cs +++ b/src/NServiceBus.Core.Tests/OpenTelemetry/DiagnosticsWriterTests.cs @@ -1,6 +1,7 @@ namespace NServiceBus.Core.Tests.OpenTelemetry; using System; +using System.Collections.Generic; using System.IO; using System.Text; using System.Text.RegularExpressions; @@ -32,71 +33,48 @@ public void LoggerSetup() [Test] public async Task ShouldWriteWhenDuplicateEntriesPresent() { - var output = string.Empty; - var testWriter = new Func((diagnosticOutput, _) => - { - output = diagnosticOutput; - return Task.CompletedTask; - }); + var (writer, output) = CreateCaptureWriter(false); var diagnostics = new StartupDiagnosticEntries(); diagnostics.Add("Endpoint", new { EndpointName = "MyEndpointOne" }); diagnostics.Add("Endpoint", new { EndpointName = "MyEndpointTwo" }); diagnostics.Add("Version", new { Version = "1.0.0.0" }); - var writer = new HostStartupDiagnosticsWriter(testWriter, true, false); - await writer.Write(diagnostics.entries); - Approver.Verify(output); + Approver.Verify(output()); } [Test] public async Task ShouldWriteEntriesWithTypesUsingTheFullName() { - var output = string.Empty; - var testWriter = new Func((diagnosticOutput, _) => - { - output = diagnosticOutput; - return Task.CompletedTask; - }); + var (writer, output) = CreateCaptureWriter(false); var diagnostics = new StartupDiagnosticEntries(); diagnostics.Add("TypeIndicator", new { SomeType = typeof(DiagnosticsWriterTests) }); - var writer = new HostStartupDiagnosticsWriter(testWriter, true, false); - await writer.Write(diagnostics.entries); - Approver.Verify(output); + Approver.Verify(output()); } [Test] public async Task ShouldSupportWritingToLogAndWriter() { - var output = string.Empty; - var testWriter = new Func((diagnosticOutput, _) => - { - output = diagnosticOutput; - return Task.CompletedTask; - }); + var (writer, output) = CreateCaptureWriter(true); var diagnostics = new StartupDiagnosticEntries(); diagnostics.Add("Endpoint", new { EndpointName = "MyEndpointOne" }); - var writer = new HostStartupDiagnosticsWriter(testWriter, true, true); - await writer.Write(diagnostics.entries); - Approver.Verify(output + Environment.NewLine + logStatements, s => TimestampScrubber().Replace(s, "")); + Approver.Verify(output() + Environment.NewLine + logStatements, s => TimestampScrubber().Replace(s, "")); } [Test] public async Task ShouldSupportWritingToLogEvenWhenWriterIsNoOp() { - var testWriter = new Func((_, _) => Task.CompletedTask); + var writer = new HostStartupDiagnosticsWriter(NoOpWriter, true, true); var diagnostics = new StartupDiagnosticEntries(); diagnostics.Add("Endpoint", new { EndpointName = "MyEndpointOne" }); - var writer = new HostStartupDiagnosticsWriter(testWriter, true, true); - await writer.Write(diagnostics.entries); Approver.Verify(logStatements.ToString(), s => TimestampScrubber().Replace(s, "")); @@ -105,17 +83,292 @@ public async Task ShouldSupportWritingToLogEvenWhenWriterIsNoOp() [Test] public async Task ShouldSupportWritingToLogEvenWhenWriterFails() { - var testWriter = new Func((_, _) => Task.FromException(new InvalidOperationException("Test"))); + var writer = new HostStartupDiagnosticsWriter(FailingWriter, true, true); var diagnostics = new StartupDiagnosticEntries(); diagnostics.Add("Endpoint", new { EndpointName = "MyEndpointOne" }); - var writer = new HostStartupDiagnosticsWriter(testWriter, true, true); - await writer.Write(diagnostics.entries); Approver.Verify(logStatements.ToString(), inputToScrub => StackTraceScrubber.ScrubFileInfoFromStackTrace(TimestampScrubber().Replace(inputToScrub, ""))); } + [Test] + public async Task ShouldWriteAlphabeticalSectionOrder() + { + var (writer, output) = CreateCaptureWriter(false); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("Zulu", new { Value = "z" }); + diagnostics.Add("Mike", new { Value = "m" }); + diagnostics.Add("Alpha", new { Value = "a" }); + + await writer.Write(diagnostics.entries); + + Approver.Verify(output()); + } + + [Test] + public async Task ShouldWriteEscapedPropertyNames() + { + var (writer, output) = CreateCaptureWriter(false); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("Special", new Dictionary + { + { "Property With Spaces", true }, + { "Normal", 42 } + }); + + await writer.Write(diagnostics.entries); + + Approver.Verify(output()); + } + + [Test] + public async Task ShouldWriteNullAndNestedValues() + { + var (writer, output) = CreateCaptureWriter(false); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("NullValue", new { Value = default(object) }); + diagnostics.Add("Nested", new { Inner = new { Deep = "value" } }); + + await writer.Write(diagnostics.entries); + + Approver.Verify(output()); + } + + [Test] + public async Task ShouldInvokeLazySectionOnce() + { + var invocationCount = 0; + var (writer, _) = CreateCaptureWriter(true); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("Lazy", new Func(() => + { + invocationCount++; + return new { Value = "lazy" }; + })); + + await writer.Write(diagnostics.entries); + + Assert.That(invocationCount, Is.EqualTo(1)); + } + + [Test] + public async Task ShouldCompactAssemblyScanningOnlyInLog() + { + var (writer, output) = CreateCaptureWriter(true); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("AssemblyScanning", new AssemblyScanningDiagnostics( + [new AssemblyDetails("MyAssembly", "1.0.0.0")], + [], + false, + new AssemblyScannerConfiguration()), + StartupDiagnosticsJsonContext.Default.AssemblyScanningDiagnostics); + + await writer.Write(diagnostics.entries); + + var logOutput = logStatements.ToString(); + Assert.That(logOutput, Does.Contain("\"Assemblies\":[]"), "Log output should have compacted assemblies"); + Assert.That(output(), Does.Contain("MyAssembly"), "Custom writer output should have full assemblies"); + } + + [Test] + public async Task ShouldTruncateLogAtThreshold() + { + var writer = new HostStartupDiagnosticsWriter(NoOpWriter, true, true); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("Large", new { Data = new string('X', 40000) }); + + await writer.Write(diagnostics.entries); + + var logOutput = logStatements.ToString(); + Assert.That(logOutput, Does.Contain("... (truncated)")); + } + + [Test] + public async Task ShouldWriteTypedSection() + { + var (writer, output) = CreateCaptureWriter(false); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("Typed", new EndpointDiagnostics + { + Name = "MyEndpoint", + SendOnly = false, + NServiceBusVersion = "1.0.0" + }, StartupDiagnosticsJsonContext.Default.EndpointDiagnostics); + + await writer.Write(diagnostics.entries); + + Approver.Verify(output()); + } + + [Test] + public async Task ShouldInvokeTypedFactoryOnce() + { + var invocationCount = 0; + var (writer, output) = CreateCaptureWriter(true); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.AddFactory("Factory", () => + { + invocationCount++; + return new EndpointDiagnostics + { + Name = "FromFactory", + SendOnly = false, + NServiceBusVersion = "1.0.0" + }; + }, StartupDiagnosticsJsonContext.Default.EndpointDiagnostics); + + await writer.Write(diagnostics.entries); + + Assert.That(invocationCount, Is.EqualTo(1)); + Assert.That(output(), Does.Contain("FromFactory")); + } + + [Test] + public async Task ShouldWriteMixedTypedAndLegacySections() + { + var (writer, output) = CreateCaptureWriter(false); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("Legacy", new { Value = "old" }); + diagnostics.Add("Typed", new EndpointDiagnostics + { + Name = "MyEndpoint", + SendOnly = false, + NServiceBusVersion = "1.0.0" + }, StartupDiagnosticsJsonContext.Default.EndpointDiagnostics); + + await writer.Write(diagnostics.entries); + + Approver.Verify(output()); + } + + [Test] + public async Task ShouldWriteTypedDuplicateEntries() + { + var (writer, output) = CreateCaptureWriter(false); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("Section", new EndpointDiagnostics + { + Name = "First", + SendOnly = false, + NServiceBusVersion = "1.0.0" + }, StartupDiagnosticsJsonContext.Default.EndpointDiagnostics); + diagnostics.Add("Section", new EndpointDiagnostics + { + Name = "Second", + SendOnly = false, + NServiceBusVersion = "2.0.0" + }, StartupDiagnosticsJsonContext.Default.EndpointDiagnostics); + + await writer.Write(diagnostics.entries); + + Approver.Verify(output()); + } + + [Test] + public async Task ShouldWriteDuplicateEntriesWithSynthesizedNameCollision() + { + var (writer, output) = CreateCaptureWriter(false); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("Section", new { Value = "First" }); + diagnostics.Add("Section-2", new { Value = "AlreadySynthesized" }); + diagnostics.Add("Section", new { Value = "Third" }); + + await writer.Write(diagnostics.entries); + + Approver.Verify(output()); + } + + [Test] + public async Task ShouldWriteTypedSectionWithProjectedTypeName() + { + var (writer, output) = CreateCaptureWriter(false); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("WithType", new ContainerDiagnostics + { + Type = typeof(DiagnosticsWriterTests).FullName! + }, StartupDiagnosticsJsonContext.Default.ContainerDiagnostics); + + await writer.Write(diagnostics.entries); + + Approver.Verify(output()); + } + + [Test] + public async Task ShouldWriteTypedSectionWithSystemType() + { + var (writer, output) = CreateCaptureWriter(false); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("WithType", new TestTypeDiagnosticsDto + { + TypeValue = typeof(DiagnosticsWriterTests) + }, TestDiagnosticsJsonContext.Default.TestTypeDiagnosticsDto); + + await writer.Write(diagnostics.entries); + + Approver.Verify(output()); + } + + [Test] + public async Task ShouldWriteTypedSectionWithNestedCollections() + { + var (writer, output) = CreateCaptureWriter(false); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("NestedCollections", new ReceivingDiagnostics + { + LocalQueueAddress = new QueueAddressDiagnostics + { + BaseAddress = "myqueue", + Discriminator = null, + Properties = [], + Qualifier = null + }, + InstanceSpecificQueueAddress = null, + PurgeOnStartup = false, + TransactionMode = "TransactionScope", + MaxConcurrency = 10, + Satellites = + [ + new SatelliteDiagnostics + { + Name = "Sat1", + ReceiveAddress = new QueueAddressDiagnostics + { + BaseAddress = "satqueue", + Discriminator = null, + Properties = [], + Qualifier = null + }, + MaxConcurrency = 5 + } + ], + MessageHandlers = new Dictionary> + { + { "MsgType1", ["Handler1", "Handler2"] } + } + }, StartupDiagnosticsJsonContext.Default.ReceivingDiagnostics); + + await writer.Write(diagnostics.entries); + + Approver.Verify(output()); + } + + static (HostStartupDiagnosticsWriter Writer, Func GetOutput) CreateCaptureWriter(bool writeToLog) + { + var output = string.Empty; + var testWriter = new Func((diagnosticOutput, _) => + { + output = diagnosticOutput; + return Task.CompletedTask; + }); + return (new HostStartupDiagnosticsWriter(testWriter, true, writeToLog), () => output); + } + + static Task NoOpWriter(string _, CancellationToken cancellationToken) => Task.CompletedTask; + + static Task FailingWriter(string _, CancellationToken cancellationToken) => + Task.FromException(new InvalidOperationException("Test")); + [GeneratedRegex(@"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}", RegexOptions.Compiled)] private static partial Regex TimestampScrubber(); -} \ No newline at end of file +} diff --git a/src/NServiceBus.Core.Tests/OpenTelemetry/TestDiagnosticsJsonContext.cs b/src/NServiceBus.Core.Tests/OpenTelemetry/TestDiagnosticsJsonContext.cs new file mode 100644 index 00000000000..4baf0c84aa7 --- /dev/null +++ b/src/NServiceBus.Core.Tests/OpenTelemetry/TestDiagnosticsJsonContext.cs @@ -0,0 +1,25 @@ +namespace NServiceBus.Core.Tests.OpenTelemetry; + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +[JsonSerializable(typeof(TestTypeDiagnosticsDto))] +sealed partial class TestDiagnosticsJsonContext : JsonSerializerContext +{ +} + +sealed class TestTypeDiagnosticsDto +{ + [JsonConverter(typeof(TestFullNameTypeConverter))] + public required Type TypeValue { get; init; } +} + +sealed class TestFullNameTypeConverter : JsonConverter +{ + public override Type Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + throw new NotSupportedException(); + + public override void Write(Utf8JsonWriter writer, Type value, JsonSerializerOptions options) => + writer.WriteStringValue(value.FullName); +} diff --git a/src/NServiceBus.Core/Audit/Audit.cs b/src/NServiceBus.Core/Audit/Audit.cs index b160dab1f0a..5c845134090 100644 --- a/src/NServiceBus.Core/Audit/Audit.cs +++ b/src/NServiceBus.Core/Audit/Audit.cs @@ -38,12 +38,12 @@ protected override void Setup(FeatureConfigurationContext context) context.Settings.Get().BindSending(auditConfig.Address); - context.Settings.AddStartupDiagnosticsSection("Manifest-AuditQueue", auditConfig.Address); - context.Settings.AddStartupDiagnosticsSection("Audit", new + context.Settings.AddStartupDiagnosticsSection("Manifest-AuditQueue", auditConfig.Address, StartupDiagnosticsJsonContext.Default.String); + context.Settings.AddStartupDiagnosticsSection("Audit", new AuditDiagnostics { AuditQueue = auditConfig.Address, AuditTTBR = auditConfig.TimeToBeReceived?.ToString("g") ?? "-" - }); + }, StartupDiagnosticsJsonContext.Default.AuditDiagnostics); Logger.InfoFormat($"Auditing processed messages to '{auditConfig.Address}'"); } diff --git a/src/NServiceBus.Core/EndpointCreator.cs b/src/NServiceBus.Core/EndpointCreator.cs index 5839247f92d..90726761bc8 100644 --- a/src/NServiceBus.Core/EndpointCreator.cs +++ b/src/NServiceBus.Core/EndpointCreator.cs @@ -163,12 +163,13 @@ void Configure() pipelineSettings.PreventChanges(); settings.AddStartupDiagnosticsSection("Endpoint", - new + new EndpointDiagnostics { Name = settings.EndpointName(), SendOnly = settings.Get("Endpoint.SendOnly"), NServiceBusVersion = VersionInformation.MajorMinorPatch - } + }, + StartupDiagnosticsJsonContext.Default.EndpointDiagnostics ); // Make Metrics a first class citizen in Core by enabling once and for all them when creating the endpoint @@ -198,14 +199,14 @@ void ConfigureMessageTypes(IEnumerable messageTypesHandled) var foundMessages = messageMetadataRegistry.GetAllMessages(); - settings.AddStartupDiagnosticsSection("Messages", new + settings.AddStartupDiagnosticsSection("Messages", new MessagesDiagnostics { CustomConventionUsed = conventions.CustomMessageTypeConventionUsed, MessageConventions = conventions.RegisteredConventions, NumberOfMessagesFoundAtStartup = foundMessages.Length, - Messages = foundMessages.Select(m => m.MessageType.FullName), + Messages = foundMessages.Select(m => m.MessageType.FullName).ToArray(), AllowDynamicTypeLoading = allowDynamicTypeLoading - }); + }, StartupDiagnosticsJsonContext.Default.MessagesDiagnostics); } internal StartableEndpoint CreateStartableEndpoint(IServiceProvider serviceProvider, string containerType, IAsyncDisposable serviceProviderLease) @@ -214,7 +215,7 @@ internal StartableEndpoint CreateStartableEndpoint(IServiceProvider serviceProvi ArgumentNullException.ThrowIfNull(containerType); ArgumentNullException.ThrowIfNull(serviceProviderLease); - hostingConfiguration.AddStartupDiagnosticsSection("Container", new { Type = containerType }); + hostingConfiguration.AddStartupDiagnosticsSection("Container", new ContainerDiagnostics { Type = containerType }, StartupDiagnosticsJsonContext.Default.ContainerDiagnostics); return new StartableEndpoint(settings, featureComponent, diff --git a/src/NServiceBus.Core/Features/FeatureComponent.cs b/src/NServiceBus.Core/Features/FeatureComponent.cs index 30d4cd9edbe..1f1855efe4c 100644 --- a/src/NServiceBus.Core/Features/FeatureComponent.cs +++ b/src/NServiceBus.Core/Features/FeatureComponent.cs @@ -17,7 +17,7 @@ public void Initialize(FeatureConfigurationContext featureConfigurationContext, { var featureStats = SetupFeatures(featureConfigurationContext, settings); - settings.AddStartupDiagnosticsSection("Features", featureStats); + settings.AddStartupDiagnosticsSection("Features", featureStats, StartupDiagnosticsJsonContext.Default.FeatureDiagnosticDataArray); } public FeatureDiagnosticData[] SetupFeatures(FeatureConfigurationContext featureConfigurationContext, SettingsHolder settings) diff --git a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningComponent.cs b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningComponent.cs index 328483cb8ab..4122e6cfe8a 100644 --- a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningComponent.cs +++ b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningComponent.cs @@ -46,7 +46,7 @@ public static AssemblyScanningComponent Initialize(Configuration configuration, assemblyScannerSettings ); - settings.AddStartupDiagnosticsSection(AssemblyScanningDiagnostics.SectionName, assemblyScanningDiagnostics); + settings.AddStartupDiagnosticsSection(AssemblyScanningDiagnostics.SectionName, assemblyScanningDiagnostics, StartupDiagnosticsJsonContext.Default.AssemblyScanningDiagnostics); return new AssemblyScanningComponent(availableTypes); } diff --git a/src/NServiceBus.Core/Hosting/HostingComponent.Configuration.cs b/src/NServiceBus.Core/Hosting/HostingComponent.Configuration.cs index 21a9e7c736b..3f687060159 100644 --- a/src/NServiceBus.Core/Hosting/HostingComponent.Configuration.cs +++ b/src/NServiceBus.Core/Hosting/HostingComponent.Configuration.cs @@ -4,6 +4,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Text.Json.Serialization.Metadata; using System.Threading; using System.Threading.Tasks; using Hosting; @@ -90,7 +91,9 @@ internal Configuration(Settings settings, public string? DiagnosticsPath { get; } - public void AddStartupDiagnosticsSection(string sectionName, object section) => StartupDiagnostics.Add(sectionName, section); + public void AddStartupDiagnosticsSection(string sectionName, T section, JsonTypeInfo typeInfo) => StartupDiagnostics.Add(sectionName, section, typeInfo); + + public void AddStartupDiagnosticsSectionFactory(string sectionName, Func sectionFactory, JsonTypeInfo typeInfo) => StartupDiagnostics.AddFactory(sectionName, sectionFactory, typeInfo); public HostInformation HostInformation { get; } diff --git a/src/NServiceBus.Core/Hosting/HostingComponent.cs b/src/NServiceBus.Core/Hosting/HostingComponent.cs index 3c079166ab3..8b0e44cd95f 100644 --- a/src/NServiceBus.Core/Hosting/HostingComponent.cs +++ b/src/NServiceBus.Core/Hosting/HostingComponent.cs @@ -41,25 +41,25 @@ public static HostingComponent Initialize(Configuration configuration) registration(serviceCollection); } - configuration.AddStartupDiagnosticsSection("Hosting", new + configuration.AddStartupDiagnosticsSection("Hosting", new HostingDiagnostics { - configuration.HostInformation.HostId, + HostId = configuration.HostInformation.HostId, HostDisplayName = configuration.HostInformation.DisplayName, - RuntimeEnvironment.MachineName, + MachineName = RuntimeEnvironment.MachineName, OSPlatform = Environment.OSVersion.Platform, OSVersion = Environment.OSVersion.VersionString, - GCSettings.IsServerGC, + IsServerGC = GCSettings.IsServerGC, GCLatencyMode = GCSettings.LatencyMode, - Environment.ProcessorCount, - Environment.Is64BitProcess, + ProcessorCount = Environment.ProcessorCount, + Is64BitProcess = Environment.Is64BitProcess, CLRVersion = Environment.Version, - Environment.WorkingSet, - Environment.SystemPageSize, + WorkingSet = Environment.WorkingSet, + SystemPageSize = Environment.SystemPageSize, HostName = Dns.GetHostName(), - Environment.UserName, + UserName = Environment.UserName, PathToExe = PathUtilities.SanitizedPath(Environment.CommandLine), InstallersEnabled = configuration.ShouldRunInstallers - }); + }, StartupDiagnosticsJsonContext.Default.HostingDiagnostics); return new HostingComponent(configuration, configuration.InstallerComponent); } diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/AuditDiagnostics.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/AuditDiagnostics.cs new file mode 100644 index 00000000000..4001e87d9a6 --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/AuditDiagnostics.cs @@ -0,0 +1,9 @@ +#nullable enable + +namespace NServiceBus; + +sealed class AuditDiagnostics +{ + public required string AuditQueue { get; init; } + public required string AuditTTBR { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/ContainerDiagnostics.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/ContainerDiagnostics.cs new file mode 100644 index 00000000000..ffd20362abb --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/ContainerDiagnostics.cs @@ -0,0 +1,8 @@ +#nullable enable + +namespace NServiceBus; + +sealed class ContainerDiagnostics +{ + public required string Type { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/DiagnosticSettingsExtensions.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/DiagnosticSettingsExtensions.cs index 221e7bcea8b..7768bc0ac13 100644 --- a/src/NServiceBus.Core/Hosting/StartupDiagnostics/DiagnosticSettingsExtensions.cs +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/DiagnosticSettingsExtensions.cs @@ -3,9 +3,11 @@ namespace NServiceBus; using System; +using System.Text.Json.Serialization.Metadata; using System.Threading; using System.Threading.Tasks; using Configuration.AdvancedExtensibility; +using Particular.Obsoletes; using Settings; /// @@ -16,6 +18,9 @@ public static class DiagnosticSettingsExtensions /// /// Adds a section to the startup diagnostics. /// + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7883", + ReplacementTypeOrMember = "AddStartupDiagnosticsSection(IReadOnlySettings, string, T, JsonTypeInfo)", + Note = "The non-generic overload uses reflection-based serialization which is not AOT/trimming safe. Use the generic overload with a JsonTypeInfo instead.")] public static void AddStartupDiagnosticsSection(this IReadOnlySettings settings, string sectionName, object section) { ArgumentNullException.ThrowIfNull(settings); @@ -25,6 +30,42 @@ public static void AddStartupDiagnosticsSection(this IReadOnlySettings settings, settings.Get().StartupDiagnostics.Add(sectionName, section); } + /// + /// Adds a section to the startup diagnostics with a strongly-typed value and its JSON type info for AOT-safe serialization. + /// + /// + /// Use this overload when the diagnostics value is cheap to compute. + /// For expensive operations that should only run when diagnostics are actually written, use + /// instead. + /// + public static void AddStartupDiagnosticsSection(this IReadOnlySettings settings, string sectionName, T section, JsonTypeInfo typeInfo) + { + ArgumentNullException.ThrowIfNull(settings); + ArgumentException.ThrowIfNullOrWhiteSpace(sectionName); + ArgumentNullException.ThrowIfNull(section); + ArgumentNullException.ThrowIfNull(typeInfo); + + settings.Get().StartupDiagnostics.Add(sectionName, section, typeInfo); + } + + /// + /// Adds a section to the startup diagnostics with a factory that is evaluated lazily and its JSON type info for AOT-safe serialization. + /// + /// + /// Use this overload when the diagnostics value is expensive to compute and should only be evaluated + /// when diagnostics are actually written. For cheap values, prefer + /// instead. + /// + public static void AddStartupDiagnosticsSectionFactory(this IReadOnlySettings settings, string sectionName, Func sectionFactory, JsonTypeInfo typeInfo) + { + ArgumentNullException.ThrowIfNull(settings); + ArgumentException.ThrowIfNullOrWhiteSpace(sectionName); + ArgumentNullException.ThrowIfNull(sectionFactory); + ArgumentNullException.ThrowIfNull(typeInfo); + + settings.Get().StartupDiagnostics.AddFactory(sectionName, sectionFactory, typeInfo); + } + /// /// Configures a custom path where host diagnostics is written. /// diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/EndpointDiagnostics.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/EndpointDiagnostics.cs new file mode 100644 index 00000000000..78c109d96e2 --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/EndpointDiagnostics.cs @@ -0,0 +1,10 @@ +#nullable enable + +namespace NServiceBus; + +sealed class EndpointDiagnostics +{ + public required string Name { get; init; } + public required bool SendOnly { get; init; } + public required string NServiceBusVersion { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriter.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriter.cs index d7692d49074..a589c63cad8 100644 --- a/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriter.cs +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriter.cs @@ -1,12 +1,16 @@ -#nullable enable +#nullable enable namespace NServiceBus; using System; +using System.Buffers; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using System.Threading; using System.Threading.Tasks; using Logging; @@ -17,26 +21,13 @@ public async Task Write(List en { const int LogSafeThreshold = 30000; // Derived from application insights limits - var deduplicatedEntries = DeduplicateEntries(entries); - var dictionary = deduplicatedEntries - .OrderBy(e => e.Name) - //Note: this will allow for filtering out 'slow' operations to create a cut-down diagnostics should the need arise in future - .Select(e => new { e.Name, Data = e.Data is Func func ? func() : e.Data }) - .ToDictionary(e => e.Name, e => e.Data, StringComparer.OrdinalIgnoreCase); + var resolvedEntries = ResolveEntries(entries); if (writeDiagnosticsToLog) { - var logDictionary = new Dictionary(dictionary, StringComparer.OrdinalIgnoreCase); - // Always compact AssemblyScanning section when writing to log - if (logDictionary.TryGetValue(AssemblyScanningDiagnostics.SectionName, out var assemblyScanningSection) && - assemblyScanningSection is AssemblyScanningDiagnostics assemblyScanningDiagnostics) - { - logDictionary[AssemblyScanningDiagnostics.SectionName] = assemblyScanningDiagnostics.CreateCompactedVersion(); - } - try { - var data = JsonSerializer.Serialize(logDictionary, diagnosticsOptions); + var data = SerializeToJson(resolvedEntries, forLog: true); // Safety net: truncate if still exceeds threshold (e.g., due to other large sections) if (data.Length > LogSafeThreshold) { @@ -54,7 +45,7 @@ public async Task Write(List en try { - var data = JsonSerializer.Serialize(dictionary, diagnosticsOptions); + var data = SerializeToJson(resolvedEntries, forLog: false); await diagnosticsWriter(data, cancellationToken).ConfigureAwait(false); } catch (Exception ex) when (!ex.IsCausedBy(cancellationToken)) @@ -68,6 +59,80 @@ public async Task Write(List en } } + static List ResolveEntries(List entries) + { + var deduplicated = DeduplicateEntries(entries); + return + [ + .. deduplicated + .OrderBy(e => e.Name) + .Select(e => + { + object value; + if (e.Factory is not null) + { + value = e.Factory(); + } + else if (e.Data is Func func) + { + value = func(); + } + else + { + value = e.Data; + } + + return new ResolvedEntry(e.Name, value, e.JsonTypeInfo); + }) + ]; + } + + [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(); + using var writer = new Utf8JsonWriter(buffer); + + writer.WriteStartObject(); + + foreach (var entry in resolvedEntries) + { + var value = entry.Value; + var jsonTypeInfo = entry.JsonTypeInfo; + + // Compact AssemblyScanning section only for log output + if (forLog && value is AssemblyScanningDiagnostics assemblyScanning) + { + value = assemblyScanning.CreateCompactedVersion(); + } + + writer.WritePropertyName(entry.Name); + + if (jsonTypeInfo != null) + { + // AOT-safe path: use the provided JsonTypeInfo + JsonSerializer.Serialize(writer, value, jsonTypeInfo); + } + 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); + } + } + + writer.WriteEndObject(); + writer.Flush(); + + return Encoding.UTF8.GetString(buffer.WrittenSpan); + } + static IEnumerable DeduplicateEntries(List entries) { var countMap = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -83,26 +148,42 @@ public async Task Write(List en countMap[entry.Name] += 1; var entryNewName = $"{entry.Name}-{countMap[entry.Name]}"; + // Ensure the synthesized name does not collide with an existing entry + while (!countMap.TryAdd(entryNewName, 1)) + { + countMap[entry.Name] += 1; + entryNewName = $"{entry.Name}-{countMap[entry.Name]}"; + } + Logger.Warn($"A duplicate diagnostic entry was renamed from {entry.Name} to {entryNewName}."); yield return new StartupDiagnosticEntries.StartupDiagnosticEntry { Name = entryNewName, - Data = entry.Data + Data = entry.Data, + JsonTypeInfo = entry.JsonTypeInfo, + Factory = entry.Factory }; } } } + readonly struct ResolvedEntry(string name, object value, JsonTypeInfo? jsonTypeInfo) + { + public string Name { get; } = name; + public object Value { get; } = value; + public JsonTypeInfo? JsonTypeInfo { get; } = jsonTypeInfo; + } + static readonly JsonSerializerOptions diagnosticsOptions = new() { Converters = { new TypeConverter() } }; /// - /// By default System.Text.Json would throw with "Serialization and deserialization of 'System.Type' instances are not supported" which normally + /// By default, System.Text.Json would throw with "Serialization and deserialization of 'System.Type' instances are not supported" which normally /// would make sense because it can be considered unsafe to serialize and deserialize types. We add a custom converter here to make - /// sure when diagnostics entries accidentally use types it will just print the full name as a string. We never intent to read these things + /// sure when diagnostics entries accidentally use types it will just print the full name as a string. We never intend to read these things /// back so this is a safe approach. /// sealed class TypeConverter : JsonConverter @@ -114,4 +195,4 @@ sealed class TypeConverter : JsonConverter } static readonly ILog Logger = LogManager.GetLogger(); -} \ No newline at end of file +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostingDiagnostics.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostingDiagnostics.cs new file mode 100644 index 00000000000..723f4b3f072 --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostingDiagnostics.cs @@ -0,0 +1,26 @@ +#nullable enable + +namespace NServiceBus; + +using System; +using System.Runtime; + +sealed class HostingDiagnostics +{ + public required Guid HostId { get; init; } + public required string HostDisplayName { get; init; } + public required string MachineName { get; init; } + public required PlatformID OSPlatform { get; init; } + public required string OSVersion { get; init; } + public required bool IsServerGC { get; init; } + public required GCLatencyMode GCLatencyMode { get; init; } + public required int ProcessorCount { get; init; } + public required bool Is64BitProcess { get; init; } + public required Version CLRVersion { get; init; } + public required long WorkingSet { get; init; } + public required int SystemPageSize { get; init; } + public required string HostName { get; init; } + public required string UserName { get; init; } + public required string PathToExe { get; init; } + public required bool InstallersEnabled { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/InstallationDiagnostics.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/InstallationDiagnostics.cs new file mode 100644 index 00000000000..da132222450 --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/InstallationDiagnostics.cs @@ -0,0 +1,8 @@ +#nullable enable + +namespace NServiceBus; + +sealed class InstallationDiagnostics +{ + public required string[] InstallersEnabled { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/JsonPrettyPrinter.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/JsonPrettyPrinter.cs index c48c8888ccd..c8ca59250e9 100644 --- a/src/NServiceBus.Core/Hosting/StartupDiagnostics/JsonPrettyPrinter.cs +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/JsonPrettyPrinter.cs @@ -2,6 +2,8 @@ namespace NServiceBus; +using System.Buffers; +using System.Text; using System.Text.Encodings.Web; using System.Text.Json; @@ -10,14 +12,18 @@ static class JsonPrettyPrinter internal static string Print(string input) { using var doc = JsonDocument.Parse(input); - var root = doc.RootElement; + var buffer = new ArrayBufferWriter(); + using var writer = new Utf8JsonWriter(buffer, jsonWriterOptions); - return JsonSerializer.Serialize(root, jsonSerializerOptions); + doc.RootElement.WriteTo(writer); + + writer.Flush(); + return Encoding.UTF8.GetString(buffer.WrittenSpan); } - static readonly JsonSerializerOptions jsonSerializerOptions = new() + static readonly JsonWriterOptions jsonWriterOptions = new() { - WriteIndented = true, + Indented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; -} \ No newline at end of file +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/LicensingDiagnostics.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/LicensingDiagnostics.cs new file mode 100644 index 00000000000..632fa6802fd --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/LicensingDiagnostics.cs @@ -0,0 +1,19 @@ +#nullable enable + +namespace NServiceBus; + +using System; + +sealed class LicensingDiagnostics +{ + public string? RegisteredTo { get; init; } + public string? LicenseType { get; init; } + public string? Edition { get; init; } + public string? Tier { get; init; } + public int? LicenseStatus { get; init; } + public string? LicenseLocation { get; init; } + public required string ValidApplications { get; init; } + public bool? CommercialLicense { get; init; } + public required bool IsExpired { get; init; } + public DateTime? ExpirationDate { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/MessagesDiagnostics.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/MessagesDiagnostics.cs new file mode 100644 index 00000000000..4be5f85de51 --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/MessagesDiagnostics.cs @@ -0,0 +1,12 @@ +#nullable enable + +namespace NServiceBus; + +sealed class MessagesDiagnostics +{ + public required bool CustomConventionUsed { get; init; } + public required string[] MessageConventions { get; init; } + public required int NumberOfMessagesFoundAtStartup { get; init; } + public required string[] Messages { get; init; } + public required bool AllowDynamicTypeLoading { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/PersistenceDiagnosticsEntry.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/PersistenceDiagnosticsEntry.cs new file mode 100644 index 00000000000..be0e32dfee8 --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/PersistenceDiagnosticsEntry.cs @@ -0,0 +1,9 @@ +#nullable enable + +namespace NServiceBus; + +sealed class PersistenceDiagnosticsEntry +{ + public required string Type { get; init; } + public required string Version { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/QueueAddressDiagnostics.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/QueueAddressDiagnostics.cs new file mode 100644 index 00000000000..7eaab0d15e2 --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/QueueAddressDiagnostics.cs @@ -0,0 +1,13 @@ +#nullable enable + +namespace NServiceBus; + +using System.Collections.Generic; + +sealed class QueueAddressDiagnostics +{ + public required string BaseAddress { get; init; } + public string? Discriminator { get; init; } + public required Dictionary Properties { get; init; } + public string? Qualifier { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/ReceivingDiagnostics.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/ReceivingDiagnostics.cs new file mode 100644 index 00000000000..86757277cd8 --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/ReceivingDiagnostics.cs @@ -0,0 +1,23 @@ +#nullable enable + +namespace NServiceBus; + +using System.Collections.Generic; + +sealed class ReceivingDiagnostics +{ + public required QueueAddressDiagnostics LocalQueueAddress { get; init; } + public QueueAddressDiagnostics? InstanceSpecificQueueAddress { get; init; } + public required bool PurgeOnStartup { get; init; } + public required string TransactionMode { get; init; } + public required int MaxConcurrency { get; init; } + public required SatelliteDiagnostics[] Satellites { get; init; } + public required Dictionary> MessageHandlers { get; init; } +} + +sealed class SatelliteDiagnostics +{ + public required string Name { get; init; } + public required QueueAddressDiagnostics ReceiveAddress { get; init; } + public required int MaxConcurrency { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/RecoverabilityDiagnostics.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/RecoverabilityDiagnostics.cs new file mode 100644 index 00000000000..190a1429133 --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/RecoverabilityDiagnostics.cs @@ -0,0 +1,12 @@ +#nullable enable + +namespace NServiceBus; + +sealed class RecoverabilityDiagnostics +{ + public required int ImmediateRetries { get; init; } + public required int DelayedRetries { get; init; } + public required string DelayedRetriesTimeIncrease { get; init; } + public required string ErrorQueue { get; init; } + public required string[] UnrecoverableExceptions { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/SerializationDiagnostics.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/SerializationDiagnostics.cs new file mode 100644 index 00000000000..b3e811f94a4 --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/SerializationDiagnostics.cs @@ -0,0 +1,26 @@ +#nullable enable + +namespace NServiceBus; + +using System.Collections.Generic; + +sealed class SerializationDiagnostics +{ + public required MainSerializerDiagnostics MainSerializer { get; init; } + public required List AdditionalDeserializers { get; init; } + public required bool AllowMessageTypeInference { get; init; } +} + +sealed class MainSerializerDiagnostics +{ + public required string Type { get; init; } + public required string Version { get; init; } + public required string ContentType { get; init; } +} + +sealed class AdditionalDeserializerDiagnostics +{ + public required string Type { get; init; } + public required string Version { get; init; } + public required string ContentType { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/StartupDiagnosticEntries.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/StartupDiagnosticEntries.cs index 6a6fea09f87..36b67e831a1 100644 --- a/src/NServiceBus.Core/Hosting/StartupDiagnostics/StartupDiagnosticEntries.cs +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/StartupDiagnosticEntries.cs @@ -2,7 +2,10 @@ namespace NServiceBus; +using System; using System.Collections.Generic; +using System.Text.Json.Serialization.Metadata; +using Particular.Obsoletes; /// /// Holds diagnostics entries to be written at startup. @@ -12,6 +15,9 @@ public class StartupDiagnosticEntries /// /// Adds a new section to the diagnostics. /// + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7883", + ReplacementTypeOrMember = "Add(string, T, JsonTypeInfo)", + Note = "The non-generic overload uses reflection-based serialization which is not AOT/trimming safe. Use the generic overload with a JsonTypeInfo instead.")] public void Add(string sectionName, object section) => entries.Add(new StartupDiagnosticEntry { @@ -19,6 +25,45 @@ public void Add(string sectionName, object section) => Data = section }); + /// + /// Adds a new section to the diagnostics with a strongly-typed value and its JSON type info for AOT-safe serialization. + /// + /// + /// Use this overload when the diagnostics value is cheap to compute. + /// For expensive operations that should only run when diagnostics are actually written, use instead. + /// + public void Add(string sectionName, T section, JsonTypeInfo typeInfo) + { + ArgumentNullException.ThrowIfNull(typeInfo); + ArgumentNullException.ThrowIfNull(section); + entries.Add(new StartupDiagnosticEntry + { + Name = sectionName, + Data = section, + JsonTypeInfo = typeInfo + }); + } + + /// + /// Adds a new section to the diagnostics with a factory that is evaluated lazily and its JSON type info for AOT-safe serialization. + /// + /// + /// Use this overload when the diagnostics value is expensive to compute and should only be evaluated + /// when diagnostics are actually written. For cheap values, prefer instead. + /// + public void AddFactory(string sectionName, Func sectionFactory, JsonTypeInfo typeInfo) + { + ArgumentNullException.ThrowIfNull(sectionFactory); + ArgumentNullException.ThrowIfNull(typeInfo); + entries.Add(new StartupDiagnosticEntry + { + Name = sectionName, + Data = null!, + JsonTypeInfo = typeInfo, + Factory = () => sectionFactory()! + }); + } + internal readonly List entries = []; /// @@ -35,5 +80,9 @@ public class StartupDiagnosticEntry /// The actual diagnostics data. /// public required object Data { get; set; } + + internal JsonTypeInfo? JsonTypeInfo { get; set; } + + internal Func? Factory { get; set; } } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/StartupDiagnosticsJsonContext.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/StartupDiagnosticsJsonContext.cs new file mode 100644 index 00000000000..6979171c2b5 --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/StartupDiagnosticsJsonContext.cs @@ -0,0 +1,40 @@ +#nullable enable + +namespace NServiceBus; + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Features; +using Hosting.Helpers; + +[JsonSerializable(typeof(string))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(HostingDiagnostics))] +[JsonSerializable(typeof(EndpointDiagnostics))] +[JsonSerializable(typeof(MessagesDiagnostics))] +[JsonSerializable(typeof(ContainerDiagnostics))] +[JsonSerializable(typeof(SerializationDiagnostics))] +[JsonSerializable(typeof(MainSerializerDiagnostics))] +[JsonSerializable(typeof(AdditionalDeserializerDiagnostics))] +[JsonSerializable(typeof(FeatureDiagnosticData[]))] +[JsonSerializable(typeof(InstallationDiagnostics))] +[JsonSerializable(typeof(ReceivingDiagnostics))] +[JsonSerializable(typeof(QueueAddressDiagnostics))] +[JsonSerializable(typeof(SatelliteDiagnostics))] +[JsonSerializable(typeof(RecoverabilityDiagnostics))] +[JsonSerializable(typeof(AuditDiagnostics))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(PersistenceDiagnosticsEntry))] +[JsonSerializable(typeof(LicensingDiagnostics))] +[JsonSerializable(typeof(AssemblyScanningDiagnostics))] +[JsonSerializable(typeof(AssemblyDetails))] +[JsonSerializable(typeof(SkippedFile))] +[JsonSerializable(typeof(AssemblyScannerConfiguration))] +[JsonSerializable(typeof(ReceiveComponentManifestMessageType[]))] +[JsonSerializable(typeof(ReceiveComponentManifestMessageType))] +[JsonSerializable(typeof(ReceiveComponentManifestMessageType.SchemaProperty[]))] +[JsonSerializable(typeof(ReceiveComponentManifestMessageType.SchemaProperty))] +[JsonSerializable(typeof(Dictionary>))] +sealed partial class StartupDiagnosticsJsonContext : JsonSerializerContext +{ +} diff --git a/src/NServiceBus.Core/Installation/InstallerComponent.cs b/src/NServiceBus.Core/Installation/InstallerComponent.cs index d2ce0c12a23..06897953545 100644 --- a/src/NServiceBus.Core/Installation/InstallerComponent.cs +++ b/src/NServiceBus.Core/Installation/InstallerComponent.cs @@ -14,10 +14,10 @@ namespace NServiceBus; class InstallerComponent(InstallerComponent.Settings settings) { - public void Initialize(IReadOnlySettings globalSettings) => globalSettings.AddStartupDiagnosticsSection("Installation", new + public void Initialize(IReadOnlySettings globalSettings) => globalSettings.AddStartupDiagnosticsSection("Installation", new InstallationDiagnostics { - InstallersEnabled = settings.Installers.Select(i => i.InstallerType.FullName).ToArray() - }); + InstallersEnabled = settings.Installers.Select(i => i.InstallerType.FullName!).ToArray() + }, StartupDiagnosticsJsonContext.Default.InstallationDiagnostics); public async Task RunInstallers(IServiceProvider serviceProvider, CancellationToken cancellationToken = default) { diff --git a/src/NServiceBus.Core/Licensing/LicenseReminder.cs b/src/NServiceBus.Core/Licensing/LicenseReminder.cs index 2b17221d5c2..68cbdc23442 100644 --- a/src/NServiceBus.Core/Licensing/LicenseReminder.cs +++ b/src/NServiceBus.Core/Licensing/LicenseReminder.cs @@ -21,7 +21,7 @@ protected override void Setup(FeatureConfigurationContext context) { var result = LicenseManager.InitializeLicense(context.Settings.Get(LicenseTextSettingsKey), context.Settings.Get(LicenseFilePathSettingsKey)); - context.Settings.AddStartupDiagnosticsSection("Licensing", GenerateLicenseDiagnostics(result)); + context.Settings.AddStartupDiagnosticsSection("Licensing", GenerateLicenseDiagnostics(result), StartupDiagnosticsJsonContext.Default.LicensingDiagnostics); if (!result.HasLicenseExpired()) { @@ -42,19 +42,19 @@ protected override void Setup(FeatureConfigurationContext context) } } - static object GenerateLicenseDiagnostics(ActiveLicenseFindResult result) => - new + static LicensingDiagnostics GenerateLicenseDiagnostics(ActiveLicenseFindResult result) => + new LicensingDiagnostics { - result.License?.RegisteredTo, - result.License?.LicenseType, - result.License?.Edition, + RegisteredTo = result.License?.RegisteredTo, + LicenseType = result.License?.LicenseType, + Edition = result.License?.Edition, Tier = result.License?.Edition, - LicenseStatus = result.License?.GetLicenseStatus(), + LicenseStatus = (int?)result.License?.GetLicenseStatus(), LicenseLocation = result.Location, ValidApplications = string.Join(",", result.License?.ValidApplications ?? []), CommercialLicense = result.License?.IsCommercialLicense, IsExpired = result.HasLicenseExpired(), - result.License?.ExpirationDate, + ExpirationDate = result.License?.ExpirationDate, }; public const string LicenseTextSettingsKey = "LicenseText"; diff --git a/src/NServiceBus.Core/Persistence/PersistenceComponent.cs b/src/NServiceBus.Core/Persistence/PersistenceComponent.cs index 8df2f4bd36f..033156b5bbc 100644 --- a/src/NServiceBus.Core/Persistence/PersistenceComponent.cs +++ b/src/NServiceBus.Core/Persistence/PersistenceComponent.cs @@ -21,7 +21,7 @@ public Configuration Initialize(SettingsHolder settings) } var resultingSupportedStorages = new List<(StorageType Storage, StorageType.Options Options)>(); - var diagnostics = new Dictionary(); + var diagnostics = new Dictionary(); foreach (var enabledPersistence in persistenceSettings.Enabled) { @@ -34,15 +34,15 @@ public Configuration Initialize(SettingsHolder settings) persistenceDefinition.Apply(storageType.Storage, settings.Get()); resultingSupportedStorages.Add(storageType); - diagnostics.Add(storageType.Storage.ToString(), new + diagnostics.Add(storageType.Storage.ToString(), new PersistenceDiagnosticsEntry { - Type = persistenceDefinition.FullName, + Type = persistenceDefinition.FullName!, Version = FileVersionRetriever.GetFileVersion(persistenceDefinition.GetType()) }); } } - settings.AddStartupDiagnosticsSection("Persistence", diagnostics); + settings.AddStartupDiagnosticsSection("Persistence", diagnostics, StartupDiagnosticsJsonContext.Default.DictionaryStringPersistenceDiagnosticsEntry); return new Configuration(settings, persistenceSettings.Enabled, resultingSupportedStorages); } diff --git a/src/NServiceBus.Core/Receiving/ReceiveComponent.Configuration.cs b/src/NServiceBus.Core/Receiving/ReceiveComponent.Configuration.cs index 4927f658821..2abceb77a3a 100644 --- a/src/NServiceBus.Core/Receiving/ReceiveComponent.Configuration.cs +++ b/src/NServiceBus.Core/Receiving/ReceiveComponent.Configuration.cs @@ -24,7 +24,7 @@ public void AddManifest(HostingComponent.Configuration hostingConfiguration, Set IsEvent = conventions.IsEventType(type), }); - hostingConfiguration.AddStartupDiagnosticsSection("Manifest-MessageTypes", + hostingConfiguration.AddStartupDiagnosticsSectionFactory("Manifest-MessageTypes", () => handledMessages.Select(handledMessage => new ReceiveComponentManifestMessageType { Name = handledMessage.MessageType.Name, @@ -37,25 +37,28 @@ public void AddManifest(HostingComponent.Configuration hostingConfiguration, Set Name = prop.Name, Type = prop.PropertyType.Name, }).ToArray() - }).ToArray()); + }).ToArray(), StartupDiagnosticsJsonContext.Default.ReceiveComponentManifestMessageTypeArray); } +} - record ReceiveComponentManifestMessageType +sealed record ReceiveComponentManifestMessageType +{ + public sealed record SchemaProperty { - public record SchemaProperty - { - public required string Name { get; init; } - public required string Type { get; init; } - } - public required string Name { get; init; } - public required string FullName { get; init; } - public bool IsMessage { get; init; } - public bool IsEvent { get; init; } - public bool IsCommand { get; init; } - public required SchemaProperty[] Schema { get; init; } + public required string Type { get; init; } } + public required string Name { get; init; } + public required string FullName { get; init; } + public bool IsMessage { get; init; } + public bool IsEvent { get; init; } + public bool IsCommand { get; init; } + public required SchemaProperty[] Schema { get; init; } +} + +partial class ReceiveComponent +{ public static Configuration PrepareConfiguration(Settings settings, TransportSeam transportSeam) { var isSendOnlyEndpoint = settings.IsSendOnlyEndpoint; diff --git a/src/NServiceBus.Core/Receiving/ReceiveComponent.cs b/src/NServiceBus.Core/Receiving/ReceiveComponent.cs index 12ffdc5b7d2..044664b4949 100644 --- a/src/NServiceBus.Core/Receiving/ReceiveComponent.cs +++ b/src/NServiceBus.Core/Receiving/ReceiveComponent.cs @@ -113,25 +113,36 @@ public static ReceiveComponent Configure( configuration.TransportSeam.Configure([.. receiveSettings]); - hostingConfiguration.AddStartupDiagnosticsSection("Receiving", new + hostingConfiguration.AddStartupDiagnosticsSection("Receiving", new ReceivingDiagnostics { - configuration.LocalQueueAddress, - configuration.InstanceSpecificQueueAddress, - configuration.PurgeOnStartup, + LocalQueueAddress = ToQueueAddressDiagnostics(configuration.LocalQueueAddress), + InstanceSpecificQueueAddress = configuration.InstanceSpecificQueueAddress != null + ? ToQueueAddressDiagnostics(configuration.InstanceSpecificQueueAddress) + : null, + PurgeOnStartup = configuration.PurgeOnStartup, TransactionMode = configuration.TransportSeam.TransportDefinition.TransportTransactionMode.ToString("G"), - configuration.PushRuntimeSettings.MaxConcurrency, - Satellites = configuration.SatelliteDefinitions.Select(s => new + MaxConcurrency = configuration.PushRuntimeSettings.MaxConcurrency, + Satellites = configuration.SatelliteDefinitions.Select(s => new SatelliteDiagnostics { - s.Name, - s.ReceiveAddress, - s.RuntimeSettings.MaxConcurrency + Name = s.Name, + ReceiveAddress = ToQueueAddressDiagnostics(s.ReceiveAddress), + MaxConcurrency = s.RuntimeSettings.MaxConcurrency }).ToArray(), MessageHandlers = handlerDiagnostics - }); + }, StartupDiagnosticsJsonContext.Default.ReceivingDiagnostics); return receiveComponent; } + static QueueAddressDiagnostics ToQueueAddressDiagnostics(QueueAddress address) => + new QueueAddressDiagnostics + { + BaseAddress = address.BaseAddress, + Discriminator = address.Discriminator, + Properties = new Dictionary(address.Properties), + Qualifier = address.Qualifier + }; + public async Task Initialize( IServiceProvider builder, RecoverabilityComponent recoverabilityComponent, diff --git a/src/NServiceBus.Core/Recoverability/RecoverabilityComponent.cs b/src/NServiceBus.Core/Recoverability/RecoverabilityComponent.cs index a3be93fa090..01a67ce80b2 100644 --- a/src/NServiceBus.Core/Recoverability/RecoverabilityComponent.cs +++ b/src/NServiceBus.Core/Recoverability/RecoverabilityComponent.cs @@ -60,14 +60,14 @@ public void Initialize( pipelineSettings.Register(sp => new RecoverabilityRoutingConnector(sp.GetRequiredService(), messageRetryNotification, messageFaultedNotification), "Executes the configured retry policy"); - hostingConfiguration.AddStartupDiagnosticsSection("Recoverability", new + hostingConfiguration.AddStartupDiagnosticsSection("Recoverability", new RecoverabilityDiagnostics { ImmediateRetries = recoverabilityConfig.Immediate.MaxNumberOfRetries, DelayedRetries = recoverabilityConfig.Delayed.MaxNumberOfRetries, DelayedRetriesTimeIncrease = recoverabilityConfig.Delayed.TimeIncrease.ToString("g"), - recoverabilityConfig.Failed.ErrorQueue, - UnrecoverableExceptions = recoverabilityConfig.Failed.UnrecoverableExceptionTypes.Select(t => t.FullName).ToArray() - }); + ErrorQueue = recoverabilityConfig.Failed.ErrorQueue, + UnrecoverableExceptions = recoverabilityConfig.Failed.UnrecoverableExceptionTypes.Select(t => t.FullName!).ToArray() + }, StartupDiagnosticsJsonContext.Default.RecoverabilityDiagnostics); } public IRecoverabilityPipelineExecutor CreateRecoverabilityPipelineExecutor( diff --git a/src/NServiceBus.Core/Serialization/SerializationFeature.cs b/src/NServiceBus.Core/Serialization/SerializationFeature.cs index 09a793ba578..e7ec7127584 100644 --- a/src/NServiceBus.Core/Serialization/SerializationFeature.cs +++ b/src/NServiceBus.Core/Serialization/SerializationFeature.cs @@ -31,7 +31,7 @@ protected override void Setup(FeatureConfigurationContext context) var additionalDeserializerDefinitions = context.Settings.GetAdditionalSerializers(); var additionalDeserializers = new List(); - var additionalDeserializerDiagnostics = new List(); + var additionalDeserializerDiagnostics = new List(); foreach (var definitionAndSettings in additionalDeserializerDefinitions) { var deserializer = CreateMessageSerializer(definitionAndSettings, mapper, settings); @@ -39,11 +39,11 @@ protected override void Setup(FeatureConfigurationContext context) var deserializerType = definitionAndSettings.Item1.GetType(); - additionalDeserializerDiagnostics.Add(new + additionalDeserializerDiagnostics.Add(new AdditionalDeserializerDiagnostics { - Type = deserializerType.FullName, + Type = deserializerType.FullName!, Version = FileVersionRetriever.GetFileVersion(deserializerType), - deserializer.ContentType + ContentType = deserializer.ContentType }); } @@ -60,17 +60,17 @@ protected override void Setup(FeatureConfigurationContext context) LogFoundMessages(messageMetadataRegistry.GetAllMessages()); - context.Settings.AddStartupDiagnosticsSection("Serialization", new + context.Settings.AddStartupDiagnosticsSection("Serialization", new SerializationDiagnostics { - MainSerializer = new + MainSerializer = new MainSerializerDiagnostics { - Type = mainSerializerAndDefinition.Item1.GetType().FullName, + Type = mainSerializerAndDefinition.Item1.GetType().FullName!, Version = FileVersionRetriever.GetFileVersion(mainSerializerAndDefinition.Item1.GetType()), - mainSerializer.ContentType + ContentType = mainSerializer.ContentType }, AdditionalDeserializers = additionalDeserializerDiagnostics, AllowMessageTypeInference = allowMessageTypeInference - }); + }, StartupDiagnosticsJsonContext.Default.SerializationDiagnostics); } static IMessageSerializer CreateMessageSerializer(Tuple definitionAndSettings, IMessageMapper mapper, IReadOnlySettings mainSettings) diff --git a/src/NServiceBus.Core/StartableEndpoint.cs b/src/NServiceBus.Core/StartableEndpoint.cs index 0090b0c51de..4e7063ab97d 100644 --- a/src/NServiceBus.Core/StartableEndpoint.cs +++ b/src/NServiceBus.Core/StartableEndpoint.cs @@ -1,6 +1,7 @@ namespace NServiceBus; using System; +using System.Linq; using System.Runtime.InteropServices; using System.Security.Principal; using System.Threading; @@ -51,8 +52,8 @@ public async Task Setup(CancellationToken cancellationToken = default) void AddSendingQueueManifest() { - hostingComponent.Config.AddStartupDiagnosticsSection("Manifest-SendingQueues", transportSeam.QueueBindings.SendingAddresses); - hostingComponent.Config.AddStartupDiagnosticsSection("Manifest-ErrorQueue", settings.ErrorQueueAddress()); + hostingComponent.Config.AddStartupDiagnosticsSection("Manifest-SendingQueues", transportSeam.QueueBindings.SendingAddresses.ToList(), StartupDiagnosticsJsonContext.Default.ListString); + hostingComponent.Config.AddStartupDiagnosticsSection("Manifest-ErrorQueue", settings.ErrorQueueAddress(), StartupDiagnosticsJsonContext.Default.String); } public async Task Start(CancellationToken cancellationToken = default) diff --git a/src/NServiceBus.Core/Transports/ReceiveSettingsExtensions.cs b/src/NServiceBus.Core/Transports/ReceiveSettingsExtensions.cs index 817f9043442..9a9915b6561 100644 --- a/src/NServiceBus.Core/Transports/ReceiveSettingsExtensions.cs +++ b/src/NServiceBus.Core/Transports/ReceiveSettingsExtensions.cs @@ -18,7 +18,7 @@ public static void MakeInstanceUniquelyAddressable(this EndpointConfiguration co ArgumentException.ThrowIfNullOrWhiteSpace(discriminator); config.Settings.Set("EndpointInstanceDiscriminator", discriminator); - config.Settings.AddStartupDiagnosticsSection("Manifest-UniqueAddressDiscriminator", discriminator); + config.Settings.AddStartupDiagnosticsSection("Manifest-UniqueAddressDiscriminator", discriminator, StartupDiagnosticsJsonContext.Default.String); } /// From 316a52354d852d194ec397680c67e5e79b1ba768 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 29 Jul 2026 13:13:56 +0200 Subject: [PATCH 39/79] Add DynamicallyAccessedMembers attribute to AddEnvelopeHandler method (#7887) Co-authored-by: Daniel Marbach --- .../ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt | 2 +- ...immabilityWarnings.ApproveTrimmabilityWarnings.approved.txt | 3 --- src/NServiceBus.Core/Envelopes/EnvelopeComponent.cs | 3 ++- src/NServiceBus.Core/Envelopes/EnvelopeConfigExtensions.cs | 3 ++- 4 files changed, 5 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 50f0ba87712..71cb617a9bc 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -313,7 +313,7 @@ namespace NServiceBus { extension(NServiceBus.Features.FeatureConfigurationContext context) { - public void AddEnvelopeHandler() + public void AddEnvelopeHandler<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>() where THandler : class, NServiceBus.IEnvelopeHandler { } } } diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt index 89061f0bc76..49004a5c086 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt @@ -2,9 +2,6 @@ The following trimming warnings are present in NServiceBus.Core. Changes that make this list longer should not be approved. ----- -src/NServiceBus.Core/Envelopes/EnvelopeComponent.cs - IL2091: 'T' generic argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicConstructors' in 'Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateInstance(IServiceProvider, params Object[])'. The generic parameter 'THandler' of 'NServiceBus.EnvelopeComponent.Settings.AddEnvelopeHandler()' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. - src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceCollectionAdapter.cs IL2067: 'instanceType' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicConstructors' in call to 'Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateFactory(Type, Type[])'. The parameter 'type' of method 'lambda expression' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. diff --git a/src/NServiceBus.Core/Envelopes/EnvelopeComponent.cs b/src/NServiceBus.Core/Envelopes/EnvelopeComponent.cs index a0962296ae9..7af99025d60 100644 --- a/src/NServiceBus.Core/Envelopes/EnvelopeComponent.cs +++ b/src/NServiceBus.Core/Envelopes/EnvelopeComponent.cs @@ -4,6 +4,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.Extensions.DependencyInjection; @@ -16,7 +17,7 @@ public class Settings { readonly Dictionary> factories = []; - public void AddEnvelopeHandler() where THandler : IEnvelopeHandler + public void AddEnvelopeHandler<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>() where THandler : IEnvelopeHandler { if (factories.ContainsKey(typeof(THandler))) { diff --git a/src/NServiceBus.Core/Envelopes/EnvelopeConfigExtensions.cs b/src/NServiceBus.Core/Envelopes/EnvelopeConfigExtensions.cs index a1a0906f4b5..d15178013d6 100644 --- a/src/NServiceBus.Core/Envelopes/EnvelopeConfigExtensions.cs +++ b/src/NServiceBus.Core/Envelopes/EnvelopeConfigExtensions.cs @@ -3,6 +3,7 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using Features; /// @@ -15,7 +16,7 @@ public static class EnvelopeConfigExtensions /// /// Adds the envelope handler type. /// - public void AddEnvelopeHandler() where THandler : class, IEnvelopeHandler + public void AddEnvelopeHandler<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>() where THandler : class, IEnvelopeHandler { ArgumentNullException.ThrowIfNull(context); From 061348361b904a286a08e31c8fd7330f33ff9b2f Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 29 Jul 2026 13:25:09 +0200 Subject: [PATCH 40/79] Enhance trimmability by introducing TypeKey for keyed service factories (#7888) Co-authored-by: Daniel Marbach --- ...arnings.ApproveTrimmabilityWarnings.approved.txt | 3 --- .../KeyedServices/KeyedServiceCollectionAdapter.cs | 13 ++++++++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt index 49004a5c086..d74b447a806 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt @@ -2,9 +2,6 @@ The following trimming warnings are present in NServiceBus.Core. Changes that make this list longer should not be approved. ----- -src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceCollectionAdapter.cs - IL2067: 'instanceType' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicConstructors' in call to 'Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateFactory(Type, Type[])'. The parameter 'type' of method 'lambda expression' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. - src/NServiceBus.Core/Hosting/StartupDiagnostics/Host.cs IL2026: Using member 'System.Reflection.Assembly.GetType(String)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Types might be removed by trimming. If the type name is a string literal, consider using Type.GetType instead. IL2075: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods' in call to 'System.Type.GetMethod(String, BindingFlags)'. The return value of method 'System.Reflection.Assembly.GetType(String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. diff --git a/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceCollectionAdapter.cs b/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceCollectionAdapter.cs index 108618713be..f48f6e5a3a4 100644 --- a/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceCollectionAdapter.cs +++ b/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceCollectionAdapter.cs @@ -6,6 +6,7 @@ namespace NServiceBus; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Microsoft.Extensions.DependencyInjection; @@ -157,7 +158,7 @@ ServiceDescriptor EnsureKeyedDescriptor(ServiceDescriptor descriptor) var resultingKey = key is null ? ServiceKey : key as KeyedServiceKey ?? new KeyedServiceKey(key); var keyedProvider = new KeyedServiceProviderAdapter(serviceProvider, resultingKey, this); return descriptor.Lifetime == ServiceLifetime.Singleton ? ActivatorUtilities.CreateInstance(keyedProvider, descriptor.KeyedImplementationType) : - factories.GetOrAdd(descriptor.KeyedImplementationType, type => ActivatorUtilities.CreateFactory(type, Type.EmptyTypes))(keyedProvider, []); + factories.GetOrAdd(new TypeKey { Type = descriptor.KeyedImplementationType }, static typeKey => ActivatorUtilities.CreateFactory(typeKey.Type, Type.EmptyTypes))(keyedProvider, []); }, descriptor.Lifetime); UnsafeAccessor.GetImplementationType(keyedDescriptor) = descriptor.KeyedImplementationType; } @@ -189,7 +190,7 @@ ServiceDescriptor EnsureKeyedDescriptor(ServiceDescriptor descriptor) var resultingKey = key is null ? ServiceKey : key as KeyedServiceKey ?? new KeyedServiceKey(key); var keyedProvider = new KeyedServiceProviderAdapter(serviceProvider, resultingKey, this); return descriptor.Lifetime == ServiceLifetime.Singleton ? ActivatorUtilities.CreateInstance(keyedProvider, descriptor.ImplementationType) : - factories.GetOrAdd(descriptor.ImplementationType, type => ActivatorUtilities.CreateFactory(type, Type.EmptyTypes))(keyedProvider, []); + factories.GetOrAdd(new TypeKey { Type = descriptor.ImplementationType }, static typeKey => ActivatorUtilities.CreateFactory(typeKey.Type, Type.EmptyTypes))(keyedProvider, []); }, descriptor.Lifetime); UnsafeAccessor.GetImplementationType(keyedDescriptor) = descriptor.ImplementationType; } @@ -208,7 +209,13 @@ static class UnsafeAccessor public static extern ref Type GetImplementationType(ServiceDescriptor descriptor); } + readonly record struct TypeKey + { + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] + public Type Type { get; init; } + } + readonly List originalDescriptors = []; readonly List keyedDescriptors = []; - readonly ConcurrentDictionary factories = new(); + readonly ConcurrentDictionary factories = new(); } \ No newline at end of file From ff1f6070c5477fb7391c631fbb9fb549d768a4da Mon Sep 17 00:00:00 2001 From: Brandon Ording Date: Fri, 31 Jul 2026 10:59:03 -0400 Subject: [PATCH 41/79] Add support for nullable reference types - Reliability (#7886) * Add nullable to Reliability/Outbox * Add nullable to Reliability/SynchronizedStorage * Add TODO * Update approval tests * Add TODO comment --- .../Outbox/AcceptanceTestingOutboxStorage.cs | 6 +++--- ...APIApprovals.ApproveNServiceBus.approved.txt | 8 ++++---- ...nnotations.ApproveNullableTypes.approved.txt | 6 ------ .../Reliability/Outbox/FakeOutboxStorage.cs | 15 +++++++++------ ...ortReceiveToPhysicalMessageConnectorTests.cs | 14 ++++++++------ ...ransportReceiveToPhysicalMessageConnector.cs | 17 ++++++++++------- .../ForceBatchDispatchToBeIsolatedBehavior.cs | 2 ++ .../Reliability/Outbox/IOutboxStorage.cs | 9 +++++++-- .../Reliability/Outbox/NoOpOutboxStorage.cs | 8 +++++--- .../Reliability/Outbox/NoOpOutboxTransaction.cs | 4 +++- .../Reliability/Outbox/Outbox.cs | 4 +++- .../Outbox/OutboxConfigExtensions.cs | 4 +++- .../Reliability/Outbox/OutboxMessage.cs | 4 +++- .../Reliability/Outbox/OutboxSettings.cs | 4 +++- .../Reliability/Outbox/OutboxTransaction.cs | 2 ++ .../Reliability/Outbox/TransportOperation.cs | 10 ++++++---- ...tableSynchronizedStorageSessionExtensions.cs | 4 ++++ .../ICompletableSynchronizedStorageSession.cs | 4 +++- .../ISynchronizedStorageSession.cs | 4 +++- ...NoOpCompletableSynchronizedStorageSession.cs | 2 ++ .../SynchronizedStorage/SynchronizedStorage.cs | 4 +++- 21 files changed, 86 insertions(+), 49 deletions(-) diff --git a/src/NServiceBus.AcceptanceTesting/AcceptanceTestingPersistence/Outbox/AcceptanceTestingOutboxStorage.cs b/src/NServiceBus.AcceptanceTesting/AcceptanceTestingPersistence/Outbox/AcceptanceTestingOutboxStorage.cs index 5218574ad88..b790c3626e1 100644 --- a/src/NServiceBus.AcceptanceTesting/AcceptanceTestingPersistence/Outbox/AcceptanceTestingOutboxStorage.cs +++ b/src/NServiceBus.AcceptanceTesting/AcceptanceTestingPersistence/Outbox/AcceptanceTestingOutboxStorage.cs @@ -10,14 +10,14 @@ class AcceptanceTestingOutboxStorage : IOutboxStorage { - public Task Get(string messageId, ContextBag context, CancellationToken cancellationToken = default) + public Task Get(string messageId, ContextBag context, CancellationToken cancellationToken = default) { if (!storage.TryGetValue(messageId, out var storedMessage)) { - return NoOutboxMessageTask!; + return NoOutboxMessageTask; } - return Task.FromResult(new OutboxMessage(messageId, storedMessage.TransportOperations)); + return Task.FromResult(new OutboxMessage(messageId, storedMessage.TransportOperations)); } public Task BeginTransaction(ContextBag context, CancellationToken cancellationToken = default) => Task.FromResult(new AcceptanceTestingOutboxTransaction()); diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index 71cb617a9bc..48b527677aa 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -1792,7 +1792,7 @@ namespace NServiceBus.Outbox public interface IOutboxStorage { System.Threading.Tasks.Task BeginTransaction(NServiceBus.Extensibility.ContextBag context, System.Threading.CancellationToken cancellationToken = default); - System.Threading.Tasks.Task Get(string messageId, NServiceBus.Extensibility.ContextBag context, System.Threading.CancellationToken cancellationToken = default); + System.Threading.Tasks.Task Get(string messageId, NServiceBus.Extensibility.ContextBag context, System.Threading.CancellationToken cancellationToken = default); System.Threading.Tasks.Task SetAsDispatched(string messageId, NServiceBus.Extensibility.ContextBag context, System.Threading.CancellationToken cancellationToken = default); System.Threading.Tasks.Task Store(NServiceBus.Outbox.OutboxMessage message, NServiceBus.Outbox.IOutboxTransaction transaction, NServiceBus.Extensibility.ContextBag context, System.Threading.CancellationToken cancellationToken = default); } @@ -1809,11 +1809,11 @@ namespace NServiceBus.Outbox public class OutboxSettings : NServiceBus.Configuration.AdvancedExtensibility.ExposeSettings { } public class TransportOperation { - public TransportOperation(string messageId, NServiceBus.Transport.DispatchProperties properties, System.ReadOnlyMemory body, System.Collections.Generic.Dictionary headers) { } + public TransportOperation(string messageId, NServiceBus.Transport.DispatchProperties? properties, System.ReadOnlyMemory body, System.Collections.Generic.Dictionary? headers) { } public System.ReadOnlyMemory Body { get; } - public System.Collections.Generic.Dictionary Headers { get; } + public System.Collections.Generic.Dictionary? Headers { get; } public string MessageId { get; } - public NServiceBus.Transport.DispatchProperties Options { get; } + public NServiceBus.Transport.DispatchProperties? Options { get; } } } namespace NServiceBus.Performance.TimeToBeReceived diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt index 6ad5dbcb11c..6ee362bca6c 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt @@ -21,12 +21,6 @@ NServiceBus.MessageDrivenSubscriptionsConfigExtensions NServiceBus.MessageProcessingContextExtensions NServiceBus.MessageSessionExtensions NServiceBus.OnSatelliteMessage -NServiceBus.Outbox.IOutboxStorage -NServiceBus.Outbox.OutboxMessage -NServiceBus.Outbox.TransportOperation -NServiceBus.OutboxConfigExtensions -NServiceBus.Persistence.CompletableSynchronizedStorageSessionExtensions -NServiceBus.Persistence.ICompletableSynchronizedStorageSession NServiceBus.Pipeline.ISubscribeContext NServiceBus.Pipeline.IUnsubscribeContext NServiceBus.ReceiveSettingsExtensions diff --git a/src/NServiceBus.Core.Tests/Reliability/Outbox/FakeOutboxStorage.cs b/src/NServiceBus.Core.Tests/Reliability/Outbox/FakeOutboxStorage.cs index 50da4363934..10f06128c92 100644 --- a/src/NServiceBus.Core.Tests/Reliability/Outbox/FakeOutboxStorage.cs +++ b/src/NServiceBus.Core.Tests/Reliability/Outbox/FakeOutboxStorage.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Core.Tests.Reliability.Outbox; +#nullable enable + +namespace NServiceBus.Core.Tests.Reliability.Outbox; using System.Threading; using System.Threading.Tasks; @@ -8,16 +10,17 @@ class FakeOutboxStorage : IOutboxStorage { - public OutboxMessage ExistingMessage { get; set; } - public OutboxMessage StoredMessage { get; set; } + public OutboxMessage? ExistingMessage { get; set; } + + public OutboxMessage? StoredMessage { get; set; } public bool WasDispatched { get; set; } - public Task Get(string messageId, ContextBag options, CancellationToken cancellationToken = default) + public Task Get(string messageId, ContextBag options, CancellationToken cancellationToken = default) { - if (ExistingMessage != null && ExistingMessage.MessageId == messageId) + if (ExistingMessage is not null && ExistingMessage.MessageId == messageId) { - return Task.FromResult(ExistingMessage); + return Task.FromResult(ExistingMessage); } return Task.FromResult(default(OutboxMessage)); diff --git a/src/NServiceBus.Core.Tests/Reliability/Outbox/TransportReceiveToPhysicalMessageConnectorTests.cs b/src/NServiceBus.Core.Tests/Reliability/Outbox/TransportReceiveToPhysicalMessageConnectorTests.cs index 96e3f57b8e8..da9bfacaaa7 100644 --- a/src/NServiceBus.Core.Tests/Reliability/Outbox/TransportReceiveToPhysicalMessageConnectorTests.cs +++ b/src/NServiceBus.Core.Tests/Reliability/Outbox/TransportReceiveToPhysicalMessageConnectorTests.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Core.Tests.Reliability.Outbox; +#nullable enable + +namespace NServiceBus.Core.Tests.Reliability.Outbox; using System; using System.Collections.Generic; @@ -41,7 +43,7 @@ public async Task Should_honor_stored_delivery_constraints() await Invoke(context); - var operationProperties = new DispatchProperties(fakeBatchPipeline.TransportOperations.First().Properties); + var operationProperties = new DispatchProperties(fakeBatchPipeline.TransportOperations?.First().Properties); var delayDeliveryWith = operationProperties.DelayDeliveryWith; Assert.That(delayDeliveryWith, Is.Not.Null); Assert.That(delayDeliveryWith.Delay, Is.EqualTo(TimeSpan.FromSeconds(10))); @@ -76,7 +78,7 @@ public async Task Should_honor_stored_direct_routing() await Invoke(context); - var routing = fakeBatchPipeline.TransportOperations.First().AddressTag as UnicastAddressTag; + var routing = fakeBatchPipeline.TransportOperations?.First().AddressTag as UnicastAddressTag; Assert.That(routing, Is.Not.Null); using (Assert.EnterMultipleScope()) { @@ -105,7 +107,7 @@ public async Task Should_honor_stored_pubsub_routing() await Invoke(context); - var routing = fakeBatchPipeline.TransportOperations.First().AddressTag as MulticastAddressTag; + var routing = fakeBatchPipeline.TransportOperations?.First().AddressTag as MulticastAddressTag; Assert.That(routing, Is.Not.Null); using (Assert.EnterMultipleScope()) { @@ -195,7 +197,7 @@ public void SetUp() behavior = new TransportReceiveToPhysicalMessageConnector(fakeOutbox, new IncomingPipelineMetrics(new TestMeterFactory(), "queue", "disc")); } - Task Invoke(ITransportReceiveContext context, Func next = null) => behavior.Invoke(context, next ?? (_ => Task.CompletedTask)); + Task Invoke(ITransportReceiveContext context, Func? next = null) => behavior.Invoke(context, next ?? (_ => Task.CompletedTask)); TransportReceiveToPhysicalMessageConnector behavior; @@ -213,7 +215,7 @@ public IPipeline Pipeline() class FakeBatchPipeline : IPipeline { - public IEnumerable TransportOperations { get; set; } + public IEnumerable? TransportOperations { get; set; } public Task Invoke(IBatchDispatchContext context) { diff --git a/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs b/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs index 0504c24f16e..d057908077e 100644 --- a/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs +++ b/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs @@ -128,16 +128,19 @@ static void SerializeRoutingStrategy(AddressTag addressTag, Dictionary options) + static AddressTag DeserializeRoutingStrategy(Dictionary? options) { - if (options.Remove("Destination", out var destination)) + if (options is not null) { - return new UnicastAddressTag(destination); - } + if (options.Remove("Destination", out var destination)) + { + return new UnicastAddressTag(destination); + } - if (options.Remove("EventType", out var eventType)) - { - return new MulticastAddressTag(Type.GetType(eventType, true)); + if (options.Remove("EventType", out var eventType)) + { + return new MulticastAddressTag(Type.GetType(eventType, true)); + } } throw new Exception("Could not find routing strategy to deserialize"); diff --git a/src/NServiceBus.Core/Reliability/Outbox/ForceBatchDispatchToBeIsolatedBehavior.cs b/src/NServiceBus.Core/Reliability/Outbox/ForceBatchDispatchToBeIsolatedBehavior.cs index 195e34d3212..1127f9d6a8b 100644 --- a/src/NServiceBus.Core/Reliability/Outbox/ForceBatchDispatchToBeIsolatedBehavior.cs +++ b/src/NServiceBus.Core/Reliability/Outbox/ForceBatchDispatchToBeIsolatedBehavior.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Reliability/Outbox/IOutboxStorage.cs b/src/NServiceBus.Core/Reliability/Outbox/IOutboxStorage.cs index f1b2ba3bcbe..bbc7f8973d8 100644 --- a/src/NServiceBus.Core/Reliability/Outbox/IOutboxStorage.cs +++ b/src/NServiceBus.Core/Reliability/Outbox/IOutboxStorage.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Outbox; +#nullable enable + +namespace NServiceBus.Outbox; using System.Threading; using System.Threading.Tasks; @@ -9,6 +11,9 @@ /// public interface IOutboxStorage { + //TODO: Consider renaming this to TryGet in the next major to better reflect that it might not get a message + // and instead returns null. + /// /// Tries to find the given message in the outbox. /// @@ -16,7 +21,7 @@ public interface IOutboxStorage /// If there is no present for the given then null is /// returned. /// - Task Get(string messageId, ContextBag context, CancellationToken cancellationToken = default); + Task Get(string messageId, ContextBag context, CancellationToken cancellationToken = default); /// /// Stores the outbox message to enable deduplication an re-dispatching of related transport operations. diff --git a/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxStorage.cs b/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxStorage.cs index 66dfe2014cd..554ececc44e 100644 --- a/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxStorage.cs +++ b/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxStorage.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System.Threading; using System.Threading.Tasks; @@ -7,7 +9,7 @@ class NoOpOutboxStorage : IOutboxStorage { - public Task Get(string messageId, ContextBag options, CancellationToken cancellationToken = default) => NoOutboxMessageTask; + public Task Get(string messageId, ContextBag options, CancellationToken cancellationToken = default) => NoOutboxMessageTask; public Task Store(OutboxMessage message, IOutboxTransaction transaction, ContextBag options, CancellationToken cancellationToken = default) => Task.CompletedTask; @@ -15,6 +17,6 @@ class NoOpOutboxStorage : IOutboxStorage public Task BeginTransaction(ContextBag context, CancellationToken cancellationToken = default) => NoOutboxTransactionTask; - static readonly Task NoOutboxMessageTask = Task.FromResult(null); + static readonly Task NoOutboxMessageTask = Task.FromResult(null); static readonly Task NoOutboxTransactionTask = Task.FromResult(new NoOpOutboxTransaction()); } \ No newline at end of file diff --git a/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxTransaction.cs b/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxTransaction.cs index 28837b238a8..872827db1db 100644 --- a/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxTransaction.cs +++ b/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxTransaction.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System.Threading; using System.Threading.Tasks; diff --git a/src/NServiceBus.Core/Reliability/Outbox/Outbox.cs b/src/NServiceBus.Core/Reliability/Outbox/Outbox.cs index e5e1f3ef5a0..11188f6db96 100644 --- a/src/NServiceBus.Core/Reliability/Outbox/Outbox.cs +++ b/src/NServiceBus.Core/Reliability/Outbox/Outbox.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Features; +#nullable enable + +namespace NServiceBus.Features; using System; using ConsistencyGuarantees; diff --git a/src/NServiceBus.Core/Reliability/Outbox/OutboxConfigExtensions.cs b/src/NServiceBus.Core/Reliability/Outbox/OutboxConfigExtensions.cs index 09e27ce505a..f018bc330f4 100644 --- a/src/NServiceBus.Core/Reliability/Outbox/OutboxConfigExtensions.cs +++ b/src/NServiceBus.Core/Reliability/Outbox/OutboxConfigExtensions.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System; using Outbox; diff --git a/src/NServiceBus.Core/Reliability/Outbox/OutboxMessage.cs b/src/NServiceBus.Core/Reliability/Outbox/OutboxMessage.cs index 1422ca19eb3..d3fbab82636 100644 --- a/src/NServiceBus.Core/Reliability/Outbox/OutboxMessage.cs +++ b/src/NServiceBus.Core/Reliability/Outbox/OutboxMessage.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Outbox; +#nullable enable + +namespace NServiceBus.Outbox; using System; diff --git a/src/NServiceBus.Core/Reliability/Outbox/OutboxSettings.cs b/src/NServiceBus.Core/Reliability/Outbox/OutboxSettings.cs index 5577b0da43d..a349e3bcc23 100644 --- a/src/NServiceBus.Core/Reliability/Outbox/OutboxSettings.cs +++ b/src/NServiceBus.Core/Reliability/Outbox/OutboxSettings.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Outbox; +#nullable enable + +namespace NServiceBus.Outbox; using Configuration.AdvancedExtensibility; using Settings; diff --git a/src/NServiceBus.Core/Reliability/Outbox/OutboxTransaction.cs b/src/NServiceBus.Core/Reliability/Outbox/OutboxTransaction.cs index 20e8e1a4d6a..7190099d6fb 100644 --- a/src/NServiceBus.Core/Reliability/Outbox/OutboxTransaction.cs +++ b/src/NServiceBus.Core/Reliability/Outbox/OutboxTransaction.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus.Outbox; using System; diff --git a/src/NServiceBus.Core/Reliability/Outbox/TransportOperation.cs b/src/NServiceBus.Core/Reliability/Outbox/TransportOperation.cs index 2b9b2305a56..f6ee7018fd4 100644 --- a/src/NServiceBus.Core/Reliability/Outbox/TransportOperation.cs +++ b/src/NServiceBus.Core/Reliability/Outbox/TransportOperation.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Outbox; +#nullable enable + +namespace NServiceBus.Outbox; using System; using System.Collections.Generic; @@ -12,7 +14,7 @@ public class TransportOperation /// /// Creates a new instance of a . /// - public TransportOperation(string messageId, DispatchProperties properties, ReadOnlyMemory body, Dictionary headers) + public TransportOperation(string messageId, DispatchProperties? properties, ReadOnlyMemory body, Dictionary? headers) { ArgumentException.ThrowIfNullOrWhiteSpace(messageId); @@ -30,7 +32,7 @@ public TransportOperation(string messageId, DispatchProperties properties, ReadO /// /// Transport specific dispatch operation properties. /// - public DispatchProperties Options { get; } + public DispatchProperties? Options { get; } /// /// Gets a byte array to the body content of the outgoing message. @@ -40,5 +42,5 @@ public TransportOperation(string messageId, DispatchProperties properties, ReadO /// /// Gets outgoing message headers. /// - public Dictionary Headers { get; } + public Dictionary? Headers { get; } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Reliability/SynchronizedStorage/CompletableSynchronizedStorageSessionExtensions.cs b/src/NServiceBus.Core/Reliability/SynchronizedStorage/CompletableSynchronizedStorageSessionExtensions.cs index 081f9794790..a485b3f5e65 100644 --- a/src/NServiceBus.Core/Reliability/SynchronizedStorage/CompletableSynchronizedStorageSessionExtensions.cs +++ b/src/NServiceBus.Core/Reliability/SynchronizedStorage/CompletableSynchronizedStorageSessionExtensions.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus.Persistence; using System.Threading; @@ -7,6 +9,8 @@ namespace NServiceBus.Persistence; using Pipeline; using Transport; +// TODO Investigate if these should even be public. If they should, then they need to have argument validtion added. + /// /// Extension methods for . /// diff --git a/src/NServiceBus.Core/Reliability/SynchronizedStorage/ICompletableSynchronizedStorageSession.cs b/src/NServiceBus.Core/Reliability/SynchronizedStorage/ICompletableSynchronizedStorageSession.cs index a80be5ba8a3..777c90a8b89 100644 --- a/src/NServiceBus.Core/Reliability/SynchronizedStorage/ICompletableSynchronizedStorageSession.cs +++ b/src/NServiceBus.Core/Reliability/SynchronizedStorage/ICompletableSynchronizedStorageSession.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Persistence; +#nullable enable + +namespace NServiceBus.Persistence; using System; using System.Threading; diff --git a/src/NServiceBus.Core/Reliability/SynchronizedStorage/ISynchronizedStorageSession.cs b/src/NServiceBus.Core/Reliability/SynchronizedStorage/ISynchronizedStorageSession.cs index 05b27b367ce..72a937fb1d4 100644 --- a/src/NServiceBus.Core/Reliability/SynchronizedStorage/ISynchronizedStorageSession.cs +++ b/src/NServiceBus.Core/Reliability/SynchronizedStorage/ISynchronizedStorageSession.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Persistence; +#nullable enable + +namespace NServiceBus.Persistence; /// /// Represents a storage session. diff --git a/src/NServiceBus.Core/Reliability/SynchronizedStorage/NoOpCompletableSynchronizedStorageSession.cs b/src/NServiceBus.Core/Reliability/SynchronizedStorage/NoOpCompletableSynchronizedStorageSession.cs index 31e7d8e4fa4..0be833c2976 100644 --- a/src/NServiceBus.Core/Reliability/SynchronizedStorage/NoOpCompletableSynchronizedStorageSession.cs +++ b/src/NServiceBus.Core/Reliability/SynchronizedStorage/NoOpCompletableSynchronizedStorageSession.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; using System.Threading; diff --git a/src/NServiceBus.Core/Reliability/SynchronizedStorage/SynchronizedStorage.cs b/src/NServiceBus.Core/Reliability/SynchronizedStorage/SynchronizedStorage.cs index 1cd46f78021..1fa9a2f3e64 100644 --- a/src/NServiceBus.Core/Reliability/SynchronizedStorage/SynchronizedStorage.cs +++ b/src/NServiceBus.Core/Reliability/SynchronizedStorage/SynchronizedStorage.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus.Features; using Microsoft.Extensions.DependencyInjection; @@ -11,5 +13,5 @@ public sealed class SynchronizedStorage : Feature /// /// See . /// - protected override void Setup(FeatureConfigurationContext context) => context.Services.AddScoped(provider => provider.GetService()); + protected override void Setup(FeatureConfigurationContext context) => context.Services.AddScoped(provider => provider.GetRequiredService()); } \ No newline at end of file From 8f0276037d962bb76f05ab56d1f673df9b995db8 Mon Sep 17 00:00:00 2001 From: "internalautomation[bot]" <85681268+internalautomation[bot]@users.noreply.github.com> Date: Fri, 31 Jul 2026 16:37:38 -0500 Subject: [PATCH 42/79] GitHubSync update - master (#7893) Co-authored-by: internalautomation[bot] <85681268+internalautomation[bot]@users.noreply.github.com> --- src/.editorconfig | 4 ++++ src/Directory.Build.props | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/.editorconfig b/src/.editorconfig index 985a5a0f68b..93f87358737 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -374,3 +374,7 @@ dotnet_naming_style.camel_case.required_prefix = dotnet_naming_style.camel_case.required_suffix = dotnet_naming_style.camel_case.word_separator = dotnet_naming_style.camel_case.capitalization = camel_case + +# Silence Rider diagnostics that sometimes conflict with Roslyn analyzers +resharper_arrange_redundant_parentheses_highlighting = none + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index cb3436b7a2a..3291fe2654f 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -31,4 +31,8 @@ + + + + From 974a5c2de1463de7158b0d61f4c8ae53f22b85f8 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Tue, 4 Aug 2026 08:58:31 +0200 Subject: [PATCH 43/79] Optimize DispatchProperties and ReceiveProperties storage (#7843) * Optimize Dispatch and ReceiveProperties * Use fields directly * Optimize copy constructor * Cleanup DispatchProperties * Cleanup ReceiveProperties * Small tweaks * Null annotations * Fix another issue with deep copy * Bring back existing ctor and implement readonly dictionary * Fix typo * Fix after rebase --------- Co-authored-by: Daniel Marbach --- .../Support/DeepCopy.cs | 7 +- ...IApprovals.ApproveNServiceBus.approved.txt | 21 +- ...otations.ApproveNullableTypes.approved.txt | 1 - ...ichDontFollowStructGuidelines.approved.txt | 18 + ...tReceiveToPhysicalMessageConnectorTests.cs | 10 +- .../Transports/ReceivePropertiesTests.cs | 2 +- ...nsportReceiveToPhysicalMessageConnector.cs | 4 +- .../Transports/DispatchProperties.cs | 630 +++++++++++++++++- .../Transports/ReceiveProperties.cs | 178 ++++- 9 files changed, 825 insertions(+), 46 deletions(-) diff --git a/src/NServiceBus.AcceptanceTesting/Support/DeepCopy.cs b/src/NServiceBus.AcceptanceTesting/Support/DeepCopy.cs index 4465722a41e..363d0d58c9f 100644 --- a/src/NServiceBus.AcceptanceTesting/Support/DeepCopy.cs +++ b/src/NServiceBus.AcceptanceTesting/Support/DeepCopy.cs @@ -15,6 +15,7 @@ namespace System using System.ArrayExtensions; using System.Collections.Generic; using System.Reflection; + using System.Runtime.CompilerServices; [Diagnostics.CodeAnalysis.SuppressMessage("Code", "PS0025:Dictionary keys should implement IEquatable", Justification = "A DeepCopy algorithm requires reference counting necessitating dictionaries keyed on objects by reference")] @@ -123,7 +124,11 @@ public override int GetHashCode(object obj) return 0; } - return obj.GetHashCode(); + // RuntimeHelpers.GetHashCode returns the identity hash code, which avoids + // calling ValueType.GetHashCode() on structs marked with [InlineArray] + // (that throws NotSupportedException). This matches the BCL + // System.Collections.Generic.ReferenceEqualityComparer behavior. + return RuntimeHelpers.GetHashCode(obj); } } diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index 48b527677aa..698b7aa7fc7 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -2455,13 +2455,26 @@ namespace NServiceBus.Transport Default = 1, Isolated = 2, } - public class DispatchProperties : System.Collections.Generic.Dictionary + public class DispatchProperties : System.Collections.Generic.ICollection>, System.Collections.Generic.IDictionary, System.Collections.Generic.IEnumerable>, System.Collections.Generic.IReadOnlyCollection>, System.Collections.Generic.IReadOnlyDictionary, System.Collections.IEnumerable { public DispatchProperties() { } public DispatchProperties(System.Collections.Generic.Dictionary properties) { } - public NServiceBus.DelayedDelivery.DelayDeliveryWith DelayDeliveryWith { get; set; } - public NServiceBus.Performance.TimeToBeReceived.DiscardIfNotReceivedBefore DiscardIfNotReceivedBefore { get; set; } - public NServiceBus.DelayedDelivery.DoNotDeliverBefore DoNotDeliverBefore { get; set; } + public DispatchProperties(System.Collections.Generic.IDictionary properties) { } + public int Count { get; } + public NServiceBus.DelayedDelivery.DelayDeliveryWith? DelayDeliveryWith { get; set; } + public NServiceBus.Performance.TimeToBeReceived.DiscardIfNotReceivedBefore? DiscardIfNotReceivedBefore { get; set; } + public NServiceBus.DelayedDelivery.DoNotDeliverBefore? DoNotDeliverBefore { get; set; } + public bool IsReadOnly { get; } + public string this[string key] { get; set; } + public System.Collections.Generic.ICollection Keys { get; } + public System.Collections.Generic.ICollection Values { get; } + public void Add(string key, string value) { } + public void Clear() { } + public bool ContainsKey(string key) { } + public System.Collections.Generic.IEnumerator> GetEnumerator() { } + public bool Remove(string key) { } + public bool TryAdd(string key, string value) { } + public bool TryGetValue(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out string value) { } } public class ErrorContext { diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt index 6ee362bca6c..fec9fb40c88 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt @@ -52,7 +52,6 @@ NServiceBus.SagasConfigExtensions NServiceBus.Settings.IReadOnlySettings NServiceBus.Settings.SettingsHolder NServiceBus.SubscriptionMigrationModeSettings -NServiceBus.Transport.DispatchProperties NServiceBus.Transport.ErrorContext NServiceBus.Transport.IMessageDispatcher NServiceBus.Transport.IMessageReceiver diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/StructConventionsTests.ApproveStructsWhichDontFollowStructGuidelines.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/StructConventionsTests.ApproveStructsWhichDontFollowStructGuidelines.approved.txt index 1d23aafe370..5d4598a9783 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/StructConventionsTests.ApproveStructsWhichDontFollowStructGuidelines.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/StructConventionsTests.ApproveStructsWhichDontFollowStructGuidelines.approved.txt @@ -19,3 +19,21 @@ NServiceBus.Extensibility.ContextBag+Slot violates the following rules: - Field Value of type System.Object is a reference type. - The size cannot be determined because there are fields that are reference types. +NServiceBus.Transport.DispatchProperties+Slot violates the following rules: + - The following fields are public, so the type is not immutable: + - Field Key of type System.String is public. + - Field Value of type System.String is public. + - The following fields are reference types, which are potentially mutable: + - Field Key of type System.String is a reference type. + - Field Value of type System.String is a reference type. + - The size cannot be determined because there are fields that are reference types. + +NServiceBus.Transport.ReceiveProperties+Slot violates the following rules: + - The following fields are public, so the type is not immutable: + - Field Key of type System.String is public. + - Field Value of type System.String is public. + - The following fields are reference types, which are potentially mutable: + - Field Key of type System.String is a reference type. + - Field Value of type System.String is a reference type. + - The size cannot be determined because there are fields that are reference types. + diff --git a/src/NServiceBus.Core.Tests/Reliability/Outbox/TransportReceiveToPhysicalMessageConnectorTests.cs b/src/NServiceBus.Core.Tests/Reliability/Outbox/TransportReceiveToPhysicalMessageConnectorTests.cs index da9bfacaaa7..2e577b4c301 100644 --- a/src/NServiceBus.Core.Tests/Reliability/Outbox/TransportReceiveToPhysicalMessageConnectorTests.cs +++ b/src/NServiceBus.Core.Tests/Reliability/Outbox/TransportReceiveToPhysicalMessageConnectorTests.cs @@ -43,7 +43,7 @@ public async Task Should_honor_stored_delivery_constraints() await Invoke(context); - var operationProperties = new DispatchProperties(fakeBatchPipeline.TransportOperations?.First().Properties); + var operationProperties = new DispatchProperties(fakeBatchPipeline.TransportOperations?.First().Properties ?? []); var delayDeliveryWith = operationProperties.DelayDeliveryWith; Assert.That(delayDeliveryWith, Is.Not.Null); Assert.That(delayDeliveryWith.Delay, Is.EqualTo(TimeSpan.FromSeconds(10))); @@ -94,14 +94,12 @@ public async Task Should_honor_stored_pubsub_routing() var messageId = "id"; var properties = new DispatchProperties { - ["EventType"] = typeof(MyEvent).AssemblyQualifiedName + ["EventType"] = typeof(MyEvent).AssemblyQualifiedName! }; - - fakeOutbox.ExistingMessage = new OutboxMessage(messageId, new[] - { + fakeOutbox.ExistingMessage = new OutboxMessage(messageId, [ new NServiceBus.Outbox.TransportOperation("x", properties, Array.Empty(), []) - }); + ]); var context = CreateContext(fakeBatchPipeline, messageId); diff --git a/src/NServiceBus.Core.Tests/Transports/ReceivePropertiesTests.cs b/src/NServiceBus.Core.Tests/Transports/ReceivePropertiesTests.cs index 67f86708167..c0c8fae72e5 100644 --- a/src/NServiceBus.Core.Tests/Transports/ReceivePropertiesTests.cs +++ b/src/NServiceBus.Core.Tests/Transports/ReceivePropertiesTests.cs @@ -34,7 +34,7 @@ public void Should_wrap_provided_dictionary() } [Test] - public void Should_be_same_reference_as_source() + public void Should_copy_from_provided_dictionary() { var source = new Dictionary { ["Key"] = "Value" }; var properties = new ReceiveProperties(source); diff --git a/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs b/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs index d057908077e..1256659e15a 100644 --- a/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs +++ b/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs @@ -113,7 +113,7 @@ static TransportOperation[] ConvertToOutboxOperations(Transport.TransportOperati return transportOperations; } - static void SerializeRoutingStrategy(AddressTag addressTag, Dictionary options) + static void SerializeRoutingStrategy(AddressTag addressTag, DispatchProperties options) { switch (addressTag) { @@ -128,7 +128,7 @@ static void SerializeRoutingStrategy(AddressTag addressTag, Dictionary? options) + static AddressTag DeserializeRoutingStrategy(DispatchProperties? options) { if (options is not null) { diff --git a/src/NServiceBus.Core/Transports/DispatchProperties.cs b/src/NServiceBus.Core/Transports/DispatchProperties.cs index 92683892374..6a54d86b465 100644 --- a/src/NServiceBus.Core/Transports/DispatchProperties.cs +++ b/src/NServiceBus.Core/Transports/DispatchProperties.cs @@ -1,19 +1,30 @@ -namespace NServiceBus.Transport; +#nullable enable + +namespace NServiceBus.Transport; using System; +using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using DelayedDelivery; using Performance.TimeToBeReceived; /// /// Describes additional properties for an outgoing message. /// -public class DispatchProperties : Dictionary +[SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix", Justification = "Name reflects domain semantics, not collection implementation.")] +public class DispatchProperties : IDictionary, IReadOnlyDictionary { //These can't be changed to be backwards compatible with previous versions of the core - static readonly string DoNotDeliverBeforeKeyName = "DeliverAt"; - static readonly string DelayDeliveryWithKeyName = "DelayDeliveryFor"; - static readonly string DiscardIfNotReceivedBeforeKeyName = "TimeToBeReceived"; + const string DoNotDeliverBeforeKeyName = "DeliverAt"; + const string DelayDeliveryWithKeyName = "DelayDeliveryFor"; + const string DiscardIfNotReceivedBeforeKeyName = "TimeToBeReceived"; + + // Dedicated fields for the three well-known properties, avoiding string-key lookups + string? deliverAt; + string? delayDeliveryFor; + string? timeToBeReceived; /// /// Creates a new instance of . @@ -23,45 +34,626 @@ public DispatchProperties() } /// - /// Creates a new instance of an copies the values from the provided dictionary. + /// Creates a new instance of and copies the values from the provided dictionary. + /// + public DispatchProperties(Dictionary properties) + : this((IDictionary)properties) + { + } + + /// + /// Creates a new instance of and copies the values from the provided dictionary. /// - public DispatchProperties(Dictionary properties) : base(properties ?? []) + public DispatchProperties(IDictionary properties) { + if (properties is null) + { + return; + } + + if (properties is DispatchProperties source) + { + // Fast path: direct field copy when source is a DispatchProperties + deliverAt = source.deliverAt; + delayDeliveryFor = source.delayDeliveryFor; + timeToBeReceived = source.timeToBeReceived; + + count = source.count; + for (int i = 0; i < count; i++) + { + slots[i] = source.slots[i]; + } + + if (source.stash is { Count: > 0 }) + { + stash = new Dictionary(source.stash); + } + + return; + } + + foreach (var kvp in properties) + { + AddInternal(kvp.Key, kvp.Value); + } } /// /// Delay message delivery to a specific . /// - public DoNotDeliverBefore DoNotDeliverBefore + public DoNotDeliverBefore? DoNotDeliverBefore { - get => ContainsKey(DoNotDeliverBeforeKeyName) - ? new DoNotDeliverBefore(DateTimeOffsetHelper.ToDateTimeOffset(this[DoNotDeliverBeforeKeyName])) + get => deliverAt is not null + ? new DoNotDeliverBefore(DateTimeOffsetHelper.ToDateTimeOffset(deliverAt)) : null; - set => this[DoNotDeliverBeforeKeyName] = DateTimeOffsetHelper.ToWireFormattedString(value.At); + set => deliverAt = value is not null ? DateTimeOffsetHelper.ToWireFormattedString(value.At) : null; } /// /// Delay message delivery by a certain . /// - public DelayDeliveryWith DelayDeliveryWith + public DelayDeliveryWith? DelayDeliveryWith { - get => ContainsKey(DelayDeliveryWithKeyName) - ? new DelayDeliveryWith(TimeSpan.Parse(this[DelayDeliveryWithKeyName])) + get => delayDeliveryFor is not null + ? new DelayDeliveryWith(TimeSpan.Parse(delayDeliveryFor)) : null; - set => this[DelayDeliveryWithKeyName] = value.Delay.ToString(); + set => delayDeliveryFor = value?.Delay.ToString(); } /// /// Discard the message after a certain period of time. /// - public DiscardIfNotReceivedBefore DiscardIfNotReceivedBefore + public DiscardIfNotReceivedBefore? DiscardIfNotReceivedBefore { - get => ContainsKey(DiscardIfNotReceivedBeforeKeyName) - ? new DiscardIfNotReceivedBefore(TimeSpan.Parse(this[DiscardIfNotReceivedBeforeKeyName])) + get => timeToBeReceived is not null + ? new DiscardIfNotReceivedBefore(TimeSpan.Parse(timeToBeReceived)) : null; - set => this[DiscardIfNotReceivedBeforeKeyName] = value.MaxTime.ToString(); + set => timeToBeReceived = value?.MaxTime.ToString(); + } + + /// + public string this[string key] + { + get + { + ArgumentNullException.ThrowIfNull(key); + if (TryGetValueFromFields(key, out var fieldValue)) + { + return fieldValue; + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + return slot.Value; + } + } + + if (stash is not null && stash.TryGetValue(key, out var value)) + { + return value; + } + + ThrowKeyNotFoundException(key); + return null; + } + set + { + ArgumentNullException.ThrowIfNull(key); + if (TrySetField(key, value)) + { + return; + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + slot.Value = value; + return; + } + } + + if (count < InlineArrayLength) + { + slots[count] = new Slot { Key = key, Value = value }; + count++; + return; + } + + (stash ??= [])[key] = value; + } + } + + /// + public ICollection Keys + { + get + { + var keys = new string[Count]; + int index = 0; + + if (deliverAt is not null) + { + keys[index++] = DoNotDeliverBeforeKeyName; + } + + if (delayDeliveryFor is not null) + { + keys[index++] = DelayDeliveryWithKeyName; + } + + if (timeToBeReceived is not null) + { + keys[index++] = DiscardIfNotReceivedBeforeKeyName; + } + + for (int i = 0; i < count; i++) + { + keys[index++] = slots[i].Key; + } + + if (stash is null) + { + return keys; + } + + foreach (var key in stash.Keys) + { + keys[index++] = key; + } + + return keys; + } + } + + /// + public ICollection Values + { + get + { + var values = new string[Count]; + int index = 0; + + if (deliverAt is not null) + { + values[index++] = deliverAt; + } + + if (delayDeliveryFor is not null) + { + values[index++] = delayDeliveryFor; + } + + if (timeToBeReceived is not null) + { + values[index++] = timeToBeReceived; + } + + for (int i = 0; i < count; i++) + { + values[index++] = slots[i].Value; + } + + if (stash is null) + { + return values; + } + + foreach (var value in stash.Values) + { + values[index++] = value; + } + + return values; + } + } + + /// + public int Count + { + get + { + int fieldCount = 0; + + if (deliverAt is not null) + { + fieldCount++; + } + + if (delayDeliveryFor is not null) + { + fieldCount++; + } + + if (timeToBeReceived is not null) + { + fieldCount++; + } + + return fieldCount + count + (stash?.Count ?? 0); + } + } + + /// + public bool IsReadOnly => false; + + /// + public void Add(string key, string value) + { + ArgumentNullException.ThrowIfNull(key); + if (ContainsKeyInFields(key)) + { + throw new ArgumentException($"An item with the same key '{key}' has already been added."); + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + throw new ArgumentException($"An item with the same key '{key}' has already been added."); + } + } + + if (stash is not null && stash.ContainsKey(key)) + { + throw new ArgumentException($"An item with the same key '{key}' has already been added."); + } + + AddInternal(key, value); + } + + /// + public bool ContainsKey(string key) + { + ArgumentNullException.ThrowIfNull(key); + if (ContainsKeyInFields(key)) + { + return true; + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + return true; + } + } + + return stash is not null && stash.ContainsKey(key); + } + + /// + public bool Remove(string key) + { + ArgumentNullException.ThrowIfNull(key); + if (RemoveFromFields(key)) + { + return true; + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (!StringComparer.Ordinal.Equals(key, slot.Key)) + { + continue; + } + + count--; + if (i != count) + { + slots[i] = slots[count]; + } + + slots[count] = new Slot(); + return true; + } + + return stash?.Remove(key) ?? false; + } + + /// + public bool TryGetValue(string key, [MaybeNullWhen(false)] out string value) + { + ArgumentNullException.ThrowIfNull(key); + if (TryGetValueFromFields(key, out var fieldValue)) + { + value = fieldValue; + return true; + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (!StringComparer.Ordinal.Equals(key, slot.Key)) + { + continue; + } + + value = slot.Value; + return true; + } + + if (stash is not null && stash.TryGetValue(key, out var stashedValue)) + { + value = stashedValue; + return true; + } + + value = null!; + return false; + } + + /// + /// Attempts to add the specified key and value to the dictionary. + /// + /// true if the key/value pair was added; false if the key already exists. + public bool TryAdd(string key, string value) + { + ArgumentNullException.ThrowIfNull(key); + if (ContainsKeyInFields(key)) + { + return false; + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + return false; + } + } + + if (stash is not null && stash.ContainsKey(key)) + { + return false; + } + + AddInternal(key, value); + return true; + } + + /// + public void Clear() + { + deliverAt = null; + delayDeliveryFor = null; + timeToBeReceived = null; + + for (int i = 0; i < count; i++) + { + slots[i] = new Slot(); + } + + count = 0; + stash?.Clear(); + } + + /// + public IEnumerator> GetEnumerator() + { + if (deliverAt is not null) + { + yield return new KeyValuePair(DoNotDeliverBeforeKeyName, deliverAt); + } + + if (delayDeliveryFor is not null) + { + yield return new KeyValuePair(DelayDeliveryWithKeyName, delayDeliveryFor); + } + + if (timeToBeReceived is not null) + { + yield return new KeyValuePair(DiscardIfNotReceivedBeforeKeyName, timeToBeReceived); + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + yield return new KeyValuePair(slot.Key, slot.Value); + } + + if (stash is null) + { + yield break; + } + + foreach (var kvp in stash) + { + yield return kvp; + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + bool ContainsKeyInFields(string key) + { + if (deliverAt is not null && key == DoNotDeliverBeforeKeyName) + { + return true; + } + + if (delayDeliveryFor is not null && key == DelayDeliveryWithKeyName) + { + return true; + } + + return timeToBeReceived is not null && key == DiscardIfNotReceivedBeforeKeyName; + } + + bool TryGetValueFromFields(string key, out string value) + { + if (deliverAt is not null && key == DoNotDeliverBeforeKeyName) + { + value = deliverAt; + return true; + } + + if (delayDeliveryFor is not null && key == DelayDeliveryWithKeyName) + { + value = delayDeliveryFor; + return true; + } + + if (timeToBeReceived is not null && key == DiscardIfNotReceivedBeforeKeyName) + { + value = timeToBeReceived; + return true; + } + + value = null!; + return false; + } + + bool TrySetField(string key, string value) + { + switch (key) + { + case DoNotDeliverBeforeKeyName: + deliverAt = value; + return true; + case DelayDeliveryWithKeyName: + delayDeliveryFor = value; + return true; + case DiscardIfNotReceivedBeforeKeyName: + timeToBeReceived = value; + return true; + default: + return false; + } + } + + bool RemoveFromFields(string key) + { + if (deliverAt is not null && key == DoNotDeliverBeforeKeyName) + { + deliverAt = null; + return true; + } + + if (delayDeliveryFor is not null && key == DelayDeliveryWithKeyName) + { + delayDeliveryFor = null; + return true; + } + + if (timeToBeReceived is null || key != DiscardIfNotReceivedBeforeKeyName) + { + return false; + } + + timeToBeReceived = null; + return true; + + } + + void AddInternal(string key, string value) + { + if (TrySetField(key, value)) + { + return; + } + + if (count < InlineArrayLength) + { + slots[count] = new Slot { Key = key, Value = value }; + count++; + return; + } + + (stash ??= [])[key] = value; + } + + IEnumerable IReadOnlyDictionary.Keys => Keys; + IEnumerable IReadOnlyDictionary.Values => Values; + + void ICollection>.Add(KeyValuePair item) => Add(item.Key, item.Value); + + bool ICollection>.Contains(KeyValuePair item) => TryGetValue(item.Key, out var value) && StringComparer.Ordinal.Equals(value, item.Value); + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + ArgumentNullException.ThrowIfNull(array); + ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex); + + if (array.Length - arrayIndex < Count) + { + throw new ArgumentException("Destination array is not long enough to copy all items."); + } + + int index = arrayIndex; + + if (deliverAt is not null) + { + array[index++] = new KeyValuePair(DoNotDeliverBeforeKeyName, deliverAt); + } + + if (delayDeliveryFor is not null) + { + array[index++] = new KeyValuePair(DelayDeliveryWithKeyName, delayDeliveryFor); + } + + if (timeToBeReceived is not null) + { + array[index++] = new KeyValuePair(DiscardIfNotReceivedBeforeKeyName, timeToBeReceived); + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + array[index++] = new KeyValuePair(slot.Key, slot.Value); + } + + if (stash is null) + { + return; + } + + foreach (var kvp in stash) + { + array[index++] = kvp; + } + } + + bool ICollection>.Remove(KeyValuePair item) => ((ICollection>)this).Contains(item) && Remove(item.Key); + + [DoesNotReturn] + static void ThrowKeyNotFoundException(string key) => throw new KeyNotFoundException($"The given key '{key}' was not present in the dictionary."); + + SlotArray slots; + int count; + Dictionary? stash; + + // Only needed for custom properties added by transports; the three well-known + // properties are stored as dedicated fields to avoid string-key lookups. + const int InlineArrayLength = 2; + + struct Slot + { + public string Key; + public string Value; + } + + [InlineArray(InlineArrayLength)] + struct SlotArray + { + Slot _element0; } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Transports/ReceiveProperties.cs b/src/NServiceBus.Core/Transports/ReceiveProperties.cs index f4eecbab4c0..4b7466e7ac9 100644 --- a/src/NServiceBus.Core/Transports/ReceiveProperties.cs +++ b/src/NServiceBus.Core/Transports/ReceiveProperties.cs @@ -2,9 +2,11 @@ namespace NServiceBus.Transport; +using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; /// /// Properties received from the transport that can be propagated to outgoing dispatch operations. @@ -13,8 +15,6 @@ namespace NServiceBus.Transport; [SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix", Justification = "Name reflects domain semantics, not collection implementation.")] public sealed class ReceiveProperties : IReadOnlyDictionary { - readonly Dictionary properties; - /// /// An empty instance. /// @@ -23,34 +23,188 @@ public sealed class ReceiveProperties : IReadOnlyDictionary /// /// Creates an empty instance of . /// - public ReceiveProperties() => properties = []; + public ReceiveProperties() + { + } /// /// Creates a from the provided dictionary. - /// The dictionary is stored by reference — do not mutate it after passing to this constructor. /// - public ReceiveProperties(Dictionary dictionary) => properties = dictionary; + public ReceiveProperties(Dictionary dictionary) + { + ArgumentNullException.ThrowIfNull(dictionary); + + foreach (var kvp in dictionary) + { + AddInternal(kvp.Key, kvp.Value); + } + } /// - public string this[string key] => properties[key]; + public string this[string key] + { + get + { + ArgumentNullException.ThrowIfNull(key); + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + return slot.Value; + } + } + + if (stash is not null && stash.TryGetValue(key, out var value)) + { + return value; + } + + ThrowKeyNotFoundException(key); + return null; + } + } /// - public IEnumerable Keys => properties.Keys; + public IEnumerable Keys + { + get + { + for (int i = 0; i < count; i++) + { + yield return slots[i].Key; + } + + if (stash is not null) + { + foreach (var key in stash.Keys) + { + yield return key; + } + } + } + } /// - public IEnumerable Values => properties.Values; + public IEnumerable Values + { + get + { + for (int i = 0; i < count; i++) + { + yield return slots[i].Value; + } + + if (stash is not null) + { + foreach (var value in stash.Values) + { + yield return value; + } + } + } + } /// - public int Count => properties.Count; + public int Count => count + (stash?.Count ?? 0); /// - public bool ContainsKey(string key) => properties.ContainsKey(key); + public bool ContainsKey(string key) + { + ArgumentNullException.ThrowIfNull(key); + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + return true; + } + } + + return stash is not null && stash.ContainsKey(key); + } /// - public bool TryGetValue(string key, [MaybeNullWhen(false)] out string value) => properties.TryGetValue(key, out value); + public bool TryGetValue(string key, [MaybeNullWhen(false)] out string value) + { + ArgumentNullException.ThrowIfNull(key); + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (!StringComparer.Ordinal.Equals(key, slot.Key)) + { + continue; + } + + value = slot.Value; + return true; + } + + if (stash is not null && stash.TryGetValue(key, out var stashedValue)) + { + value = stashedValue; + return true; + } + + value = null; + return false; + } /// - public IEnumerator> GetEnumerator() => properties.GetEnumerator(); + public IEnumerator> GetEnumerator() + { + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + yield return new KeyValuePair(slot.Key, slot.Value); + } + + if (stash is null) + { + yield break; + } + + foreach (var kvp in stash) + { + yield return kvp; + } + } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + void AddInternal(string key, string value) + { + if (count < InlineArrayLength) + { + slots[count] = new Slot { Key = key, Value = value }; + count++; + return; + } + + (stash ??= [])[key] = value; + } + + [DoesNotReturn] + static void ThrowKeyNotFoundException(string key) => throw new KeyNotFoundException($"The given key '{key}' was not present in the dictionary."); + + SlotArray slots; + int count; + Dictionary? stash; + + const int InlineArrayLength = 4; + + struct Slot + { + public string Key; + public string Value; + } + + [InlineArray(InlineArrayLength)] + struct SlotArray + { + Slot _element0; + } } \ No newline at end of file From 83ee9beff0b10190eb74a61badb55be793af843e Mon Sep 17 00:00:00 2001 From: Laila Bougria Date: Wed, 5 Aug 2026 13:35:53 +0200 Subject: [PATCH 44/79] Preserve the native properties on delayed retries (#7900) * Preserve the native properties on delayed retries * Slightly better assertions hopefully * Align handler attribute usage with other tests since we forgot to change that --------- Co-authored-by: Daniel Marbach --- .../Recoverability/RecoverabilityContext.cs | 1 + ...hen_message_sent_with_LearningTransport.cs | 163 +++++++++++++----- 2 files changed, 121 insertions(+), 43 deletions(-) diff --git a/src/NServiceBus.Core/Recoverability/RecoverabilityContext.cs b/src/NServiceBus.Core/Recoverability/RecoverabilityContext.cs index 436bdcbb4f1..80c29096a49 100644 --- a/src/NServiceBus.Core/Recoverability/RecoverabilityContext.cs +++ b/src/NServiceBus.Core/Recoverability/RecoverabilityContext.cs @@ -42,6 +42,7 @@ public RecoverabilityContext( RecoverabilityAction = recoverabilityAction; Extensions.Set(errorContext.TransportTransaction); + Extensions.Set(new IncomingMessage(NativeMessageId, Headers, Body, ReceiveProperties)); } [ObsoleteMetadata(Message = "For access to the message body, headers, native message ID, or the receive properties use the corresponding properties directly exposed on the context", TreatAsErrorFromVersion = "11", RemoveInVersion = "12")] diff --git a/src/NServiceBus.Learning.AcceptanceTests/When_message_sent_with_LearningTransport.cs b/src/NServiceBus.Learning.AcceptanceTests/When_message_sent_with_LearningTransport.cs index 998485d6070..f88183c4502 100644 --- a/src/NServiceBus.Learning.AcceptanceTests/When_message_sent_with_LearningTransport.cs +++ b/src/NServiceBus.Learning.AcceptanceTests/When_message_sent_with_LearningTransport.cs @@ -22,8 +22,8 @@ public async Task Should_preserve_file_created_time_as_receive_property() using (Assert.EnterMultipleScope()) { Assert.That(context.MessageReceived, Is.True, "Message was not received"); - Assert.That(context.FileCreatedAt, Is.Not.Null, "FileCreatedAt property should be present"); - Assert.That(DateTime.TryParse(context.FileCreatedAt, out _), Is.True, "FileCreatedAt should be a valid datetime"); + Assert.That(context.ReceiveFileCreatedAt, Is.Not.Null, "FileCreatedAt property should be present"); + Assert.That(DateTime.TryParse(context.ReceiveFileCreatedAt, out _), Is.True, "FileCreatedAt should be a valid datetime"); } } @@ -35,7 +35,29 @@ public async Task Should_preserve_file_created_time_property_on_dispatched_copie .WithEndpoint() .Run(); - Assert.That(context.MessageAudited, Is.True, "Message was not audited"); + using (Assert.EnterMultipleScope()) + { + Assert.That(context.MessageAudited, Is.True, "Message was not audited"); + Assert.That(context.ReceiveFileCreatedAt, Is.Not.Null, "FileCreatedAt property should be present"); + Assert.That(DateTime.TryParse(context.ReceiveFileCreatedAt, out _), Is.True, "FileCreatedAt should be a valid datetime"); + } + } + + [Test] + public async Task Should_preserve_file_created_time_property_on_delayed_retry() + { + var context = await Scenario.Define() + .WithEndpoint(b => b + .When(session => session.SendLocal(new TestMessage())) + .DoNotFailOnErrorMessages()) + .Run(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.NumberOfRetries, Is.EqualTo(2), "Message was not retried"); + Assert.That(context.ReceiveFileCreatedAt, Is.Not.Null, "FileCreatedAt property should be present"); + Assert.That(DateTime.TryParse(context.ReceiveFileCreatedAt, out _), Is.True, "FileCreatedAt should be a valid datetime"); + } } [Test] @@ -45,7 +67,12 @@ public async Task Should_not_preserve_receive_properties_on_outgoing_messages() .WithEndpoint(b => b.When(session => session.SendLocal(new TestMessage()))) .Run(); - Assert.That(context.MessageReceived, Is.True, "Message was received"); + using (Assert.EnterMultipleScope()) + { + Assert.That(context.MessageReceived, Is.True, "Message was received"); + Assert.That(DateTime.TryParse(context.ReceiveFileCreatedAt, out _), Is.True, "FileCreatedAt should be a valid datetime"); + Assert.That(DateTime.TryParse(context.SendFileCreatedAt, out _), Is.True, "SendFileCreatedAt should be a valid datetime"); + } } [Test] @@ -73,8 +100,8 @@ public async Task Should_preserve_file_created_time_property_when_moved_to_error using (Assert.EnterMultipleScope()) { Assert.That(context.MessageMovedToErrorQueue, Is.True, "Message was not moved to error queue"); - Assert.That(context.FileCreatedAt, Is.Not.Null, "FileCreatedAt property should be present on message in error queue"); - Assert.That(DateTime.TryParse(context.FileCreatedAt, out _), Is.True, "FileCreatedAt should be a valid datetime"); + Assert.That(context.ReceiveFileCreatedAt, Is.Not.Null, "FileCreatedAt property should be present on message in error queue"); + Assert.That(DateTime.TryParse(context.ReceiveFileCreatedAt, out _), Is.True, "FileCreatedAt should be a valid datetime"); } } @@ -92,21 +119,24 @@ public async Task Should_not_override_error_queue_dispatch_properties_with_recei using (Assert.EnterMultipleScope()) { Assert.That(context.MessageMovedToErrorQueue, Is.True, "Message was not moved to error queue"); - Assert.That(context.FileCreatedAt, Is.Not.Null, "FileCreatedAt property should be captured from original message"); + Assert.That(context.ReceiveFileCreatedAt, Is.Not.Null, "FileCreatedAt property should be captured from original message"); Assert.That(context.ErrorQueueFileCreatedAtDiffersFromOriginal, Is.True, "Error queue message should have FileCreatedAt from dispatch properties, not receive properties"); } } - class Context : ScenarioContext + public class Context : ScenarioContext { public bool MessageReceived { get; set; } - public string FileCreatedAt { get; set; } public bool MessageAudited { get; set; } public bool MessageMovedToErrorQueue { get; set; } public bool ErrorQueueFileCreatedAtDiffersFromOriginal { get; set; } + public int NumberOfRetries { get; set; } + public string ReceiveFileCreatedAt { get; set; } + public string RetryFileCreatedAt { get; set; } + public string SendFileCreatedAt { get; set; } } - class EndPointThatReceivesFromAnotherAndAuditsEndpoint : EndpointConfigurationBuilder + public class EndPointThatReceivesFromAnotherAndAuditsEndpoint : EndpointConfigurationBuilder { public EndPointThatReceivesFromAnotherAndAuditsEndpoint() => EndpointSetup(endpointConfiguration => { @@ -114,7 +144,8 @@ public EndPointThatReceivesFromAnotherAndAuditsEndpoint() => EndpointSetup + [Handler] + public class OutgoingTestMessageHandler(Context testContext) : IHandleMessages { public Task Handle(OutgoingTestMessage message, IMessageHandlerContext context) { @@ -122,7 +153,7 @@ public Task Handle(OutgoingTestMessage message, IMessageHandlerContext context) if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) { - testContext.FileCreatedAt = fileCreatedAt; + testContext.ReceiveFileCreatedAt = fileCreatedAt; } else { @@ -144,11 +175,12 @@ public override Task Invoke(IRoutingContext context, Func next) } } - class AuditSpyForEndPointThatReceivesFromAnotherAndAuditsEndpoint : EndpointConfigurationBuilder + public class AuditSpyForEndPointThatReceivesFromAnotherAndAuditsEndpoint : EndpointConfigurationBuilder { public AuditSpyForEndPointThatReceivesFromAnotherAndAuditsEndpoint() => EndpointSetup(); + [Handler] public class AuditMessageHandler(Context testContext) : IHandleMessages { public Task Handle(OutgoingTestMessage message, IMessageHandlerContext context) @@ -157,12 +189,12 @@ public Task Handle(OutgoingTestMessage message, IMessageHandlerContext context) if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) { - if (fileCreatedAt == testContext.FileCreatedAt) + if (fileCreatedAt == testContext.ReceiveFileCreatedAt) { testContext.MarkAsFailed(new Exception("Receive properties from the original message is propagated to audit messages.")); } - testContext.MarkAsCompleted(testContext.MessageAudited, testContext.FileCreatedAt != fileCreatedAt); + testContext.MarkAsCompleted(testContext.MessageAudited, testContext.ReceiveFileCreatedAt != fileCreatedAt); } else { @@ -174,18 +206,19 @@ public Task Handle(OutgoingTestMessage message, IMessageHandlerContext context) } } - class SendingEndpoint : EndpointConfigurationBuilder + public class SendingEndpoint : EndpointConfigurationBuilder { public SendingEndpoint() => EndpointSetup(); - class TestMessageHandler(Context testContext) : IHandleMessages + [Handler] + public class TestMessageHandler(Context testContext) : IHandleMessages { public async Task Handle(TestMessage message, IMessageHandlerContext context) { testContext.MessageReceived = true; if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) { - testContext.FileCreatedAt = fileCreatedAt; + testContext.ReceiveFileCreatedAt = fileCreatedAt; await context.SendLocal(new OutgoingTestMessage()); } @@ -197,19 +230,21 @@ public async Task Handle(TestMessage message, IMessageHandlerContext context) } //handler for the outgoing message to verify that receive properties are not propagated to outgoing messages - class OutgoingTestMessageHandler(Context testContext) : IHandleMessages + [Handler] + public class OutgoingTestMessageHandler(Context testContext) : IHandleMessages { public Task Handle(OutgoingTestMessage message, IMessageHandlerContext context) { testContext.MessageReceived = true; if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) { - if (fileCreatedAt == testContext.FileCreatedAt) + testContext.SendFileCreatedAt = fileCreatedAt; + if (fileCreatedAt == testContext.ReceiveFileCreatedAt) { testContext.MarkAsFailed(new Exception("Receive properties from the original message is propagated to outgoing messages.")); } - testContext.MarkAsCompleted(testContext.MessageReceived, testContext.FileCreatedAt != fileCreatedAt); + testContext.MarkAsCompleted(testContext.MessageReceived, testContext.ReceiveFileCreatedAt != fileCreatedAt); } else { @@ -220,14 +255,15 @@ public Task Handle(OutgoingTestMessage message, IMessageHandlerContext context) } } - class Endpoint : EndpointConfigurationBuilder + public class Endpoint : EndpointConfigurationBuilder { public Endpoint() => EndpointSetup(endpointConfiguration => { endpointConfiguration.AuditProcessedMessagesTo(Conventions.EndpointNamingConvention(typeof(AuditSpy))); }); - class TestMessageHandler(Context testContext) : IHandleMessages + [Handler] + public class TestMessageHandler(Context testContext) : IHandleMessages { public Task Handle(TestMessage message, IMessageHandlerContext context) { @@ -235,28 +271,64 @@ public Task Handle(TestMessage message, IMessageHandlerContext context) if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) { - testContext.FileCreatedAt = fileCreatedAt; + testContext.ReceiveFileCreatedAt = fileCreatedAt; } else { testContext.MarkAsFailed(new Exception("Failed to retrieve receive properties from the message context.")); } - testContext.MarkAsCompleted(testContext.MessageReceived, testContext.FileCreatedAt != null); + testContext.MarkAsCompleted(testContext.MessageReceived, testContext.ReceiveFileCreatedAt != null); return Task.CompletedTask; } } } - class EndpointWithAuditOn : EndpointConfigurationBuilder + public class FailingEndpoint : EndpointConfigurationBuilder + { + public FailingEndpoint() => EndpointSetup(endpointConfiguration => + { + endpointConfiguration.Recoverability().Delayed(settings => settings.NumberOfRetries(1)); + endpointConfiguration.Recoverability().Immediate(settings => settings.NumberOfRetries(0)); + }); + + [Handler] + public class TestMessageHandler(Context testContext) : IHandleMessages + { + public Task Handle(TestMessage message, IMessageHandlerContext context) + { + if (testContext.NumberOfRetries == 0) + { + if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) + { + testContext.ReceiveFileCreatedAt = fileCreatedAt; + } + } + else + { + if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) + { + testContext.RetryFileCreatedAt = fileCreatedAt; + testContext.MarkAsCompleted(testContext.ReceiveFileCreatedAt == testContext.RetryFileCreatedAt); + } + } + testContext.NumberOfRetries++; + + throw new SimulatedException("Simulating an exception to see if it preserves receive properties on retries"); + } + } + } + + public class EndpointWithAuditOn : EndpointConfigurationBuilder { public EndpointWithAuditOn() => EndpointSetup(endpointConfiguration => { endpointConfiguration.AuditProcessedMessagesTo(Conventions.EndpointNamingConvention(typeof(AuditSpy))); }); - class TestMessageHandler(Context testContext) : IHandleMessages + [Handler] + public class TestMessageHandler(Context testContext) : IHandleMessages { public Task Handle(TestMessage message, IMessageHandlerContext context) { @@ -264,7 +336,7 @@ public Task Handle(TestMessage message, IMessageHandlerContext context) if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) { - testContext.FileCreatedAt = fileCreatedAt; + testContext.ReceiveFileCreatedAt = fileCreatedAt; } return Task.CompletedTask; @@ -272,11 +344,12 @@ public Task Handle(TestMessage message, IMessageHandlerContext context) } } - class AuditSpy : EndpointConfigurationBuilder + public class AuditSpy : EndpointConfigurationBuilder { public AuditSpy() => EndpointSetup(); + [Handler] public class AuditMessageHandler(Context testContext) : IHandleMessages { public Task Handle(TestMessage message, IMessageHandlerContext context) @@ -285,12 +358,12 @@ public Task Handle(TestMessage message, IMessageHandlerContext context) if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) { - if (fileCreatedAt != testContext.FileCreatedAt) + if (fileCreatedAt != testContext.ReceiveFileCreatedAt) { testContext.MarkAsFailed(new Exception("Receive properties from the original message is not propagated to audit messages.")); } - testContext.MarkAsCompleted(testContext.MessageAudited, testContext.FileCreatedAt == fileCreatedAt); + testContext.MarkAsCompleted(testContext.MessageAudited, testContext.ReceiveFileCreatedAt == fileCreatedAt); } else { @@ -302,7 +375,7 @@ public Task Handle(TestMessage message, IMessageHandlerContext context) } } - class EndpointWithFailingHandler : EndpointConfigurationBuilder + public class EndpointWithFailingHandler : EndpointConfigurationBuilder { public EndpointWithFailingHandler() => EndpointSetup(endpointConfiguration => { @@ -310,7 +383,8 @@ public EndpointWithFailingHandler() => EndpointSetup(endpointConf endpointConfiguration.SendFailedMessagesTo(Conventions.EndpointNamingConvention(typeof(ErrorSpy))); }); - class TestMessageHandler(Context testContext) : IHandleMessages + [Handler] + public class TestMessageHandler(Context testContext) : IHandleMessages { public Task Handle(TestMessage message, IMessageHandlerContext context) { @@ -318,7 +392,7 @@ public Task Handle(TestMessage message, IMessageHandlerContext context) if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) { - testContext.FileCreatedAt = fileCreatedAt; + testContext.ReceiveFileCreatedAt = fileCreatedAt; } throw new SimulatedException("Message should be moved to error queue"); @@ -326,7 +400,7 @@ public Task Handle(TestMessage message, IMessageHandlerContext context) } } - class EndpointWithFailingHandlerAndDispatchOverride : EndpointConfigurationBuilder + public class EndpointWithFailingHandlerAndDispatchOverride : EndpointConfigurationBuilder { public EndpointWithFailingHandlerAndDispatchOverride() => EndpointSetup(endpointConfiguration => { @@ -335,7 +409,8 @@ public EndpointWithFailingHandlerAndDispatchOverride() => EndpointSetup + [Handler] + public class TestMessageHandler(Context testContext) : IHandleMessages { public Task Handle(TestMessage message, IMessageHandlerContext context) { @@ -343,7 +418,7 @@ public Task Handle(TestMessage message, IMessageHandlerContext context) if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) { - testContext.FileCreatedAt = fileCreatedAt; + testContext.ReceiveFileCreatedAt = fileCreatedAt; } throw new SimulatedException("Message should be moved to error queue"); @@ -379,10 +454,11 @@ public override IReadOnlyCollection GetRoutingContexts(IRecover } } - class ErrorSpy : EndpointConfigurationBuilder + public class ErrorSpy : EndpointConfigurationBuilder { public ErrorSpy() => EndpointSetup(); + [Handler] public class ErrorMessageHandler(Context testContext) : IHandleMessages { public Task Handle(TestMessage message, IMessageHandlerContext context) @@ -391,8 +467,8 @@ public Task Handle(TestMessage message, IMessageHandlerContext context) if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) { - testContext.FileCreatedAt = fileCreatedAt; - testContext.MarkAsCompleted(testContext.MessageMovedToErrorQueue, testContext.FileCreatedAt != null); + testContext.ReceiveFileCreatedAt = fileCreatedAt; + testContext.MarkAsCompleted(testContext.MessageMovedToErrorQueue, testContext.ReceiveFileCreatedAt != null); } else { @@ -404,10 +480,11 @@ public Task Handle(TestMessage message, IMessageHandlerContext context) } } - class ErrorSpyWithDispatchPropertyVerification : EndpointConfigurationBuilder + public class ErrorSpyWithDispatchPropertyVerification : EndpointConfigurationBuilder { public ErrorSpyWithDispatchPropertyVerification() => EndpointSetup(); + [Handler] public class ErrorMessageHandler(Context testContext) : IHandleMessages { public Task Handle(TestMessage message, IMessageHandlerContext context) @@ -416,12 +493,12 @@ public Task Handle(TestMessage message, IMessageHandlerContext context) if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) { - if (fileCreatedAt == testContext.FileCreatedAt) + if (fileCreatedAt == testContext.ReceiveFileCreatedAt) { testContext.MarkAsFailed(new Exception("Receive properties from the original message were propagated to error queue message instead of dispatch properties.")); } - testContext.ErrorQueueFileCreatedAtDiffersFromOriginal = testContext.FileCreatedAt != fileCreatedAt; + testContext.ErrorQueueFileCreatedAtDiffersFromOriginal = testContext.ReceiveFileCreatedAt != fileCreatedAt; testContext.MarkAsCompleted(testContext.MessageMovedToErrorQueue, testContext.ErrorQueueFileCreatedAtDiffersFromOriginal); } else From d3682cb90ecad7b8dfa324a4a3dcd843ae371998 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 5 Aug 2026 17:29:46 +0200 Subject: [PATCH 45/79] Add test to track usage of legacy NServiceBus.Logging API (#7902) Co-authored-by: Daniel Marbach --- .../API/LogManagerUsage.cs | 73 +++++++++++++++++++ ...rUsage.ApproveLogManagerUsage.approved.txt | 54 ++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 src/NServiceBus.Core.Tests/API/LogManagerUsage.cs create mode 100644 src/NServiceBus.Core.Tests/ApprovalFiles/LogManagerUsage.ApproveLogManagerUsage.approved.txt diff --git a/src/NServiceBus.Core.Tests/API/LogManagerUsage.cs b/src/NServiceBus.Core.Tests/API/LogManagerUsage.cs new file mode 100644 index 00000000000..6622cfcaa2a --- /dev/null +++ b/src/NServiceBus.Core.Tests/API/LogManagerUsage.cs @@ -0,0 +1,73 @@ +namespace NServiceBus.Core.Tests.API; + +using System; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using NServiceBus.Core.Tests.API.Infra; +using NServiceBus.Logging; +using NUnit.Framework; +using Particular.Approvals; + +// The legacy logging API (NServiceBus.Logging.LogManager.GetLogger and the +// NServiceBus.Logging.ILog logger fields it produces) is still used throughout +// NServiceBus.Core. The goal is to migrate this code to the high performance, +// source-generated loggers provided by Microsoft.Extensions.Logging using the +// [LoggerMessage] source generator. +// +// This test captures the set of types that still hold an NServiceBus.Logging.ILog +// logger field so that any new usage is immediately visible. When a type is migrated +// to a source-generated logger its ILog field is removed and it drops off this list, +// so the list naturally shrinks over time. It is acceptable for the list to grow +// only when the type affects user-facing code or when DI (dependency injection) is +// not available to obtain an ILogger. +// +// See https://learn.microsoft.com/dotnet/core/extensions/logger-message-generator +// for more details about the Microsoft.Extensions.Logging source generator. +[TestFixture] +public class LogManagerUsage +{ + [Test] + public void ApproveLogManagerUsage() + { + var b = new StringBuilder() + .AppendLine("The following types hold an NServiceBus.Logging.ILog logger field obtained via LogManager.GetLogger.") + .AppendLine("For new code where DI is available, use the high performance source generated loggers from") + .AppendLine("Microsoft.Extensions.Logging instead (see https://learn.microsoft.com/dotnet/core/extensions/logger-message-generator).") + .AppendLine("Changes that make this list longer should only be approved when the type affects user-facing") + .AppendLine("code or when DI is not available. The list should otherwise shrink over time.") + .AppendLine("-----"); + + foreach (var type in NServiceBusAssembly.Types.OrderBy(t => t.FullName, StringComparer.Ordinal)) + { + if (HasILogField(type)) + { + b.AppendLine(type.FullName); + } + } + + Console.WriteLine(b.ToString()); + Approver.Verify(b.ToString()); + } + + static bool HasILogField(Type type) + { + // The NServiceBus.Logging namespace contains adapter types that intentionally + // implement the legacy API surface and are not candidates for migration. + if (type.Namespace == "NServiceBus.Logging") + { + return false; + } + + // Compiler-generated state machines and closures are not meaningful migration + // units; the ILog usage they contain belongs to their containing type. + if (Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false)) + { + return false; + } + + return type.GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly) + .Any(field => field.FieldType == typeof(ILog)); + } +} diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/LogManagerUsage.ApproveLogManagerUsage.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/LogManagerUsage.ApproveLogManagerUsage.approved.txt new file mode 100644 index 00000000000..d7a9399dad4 --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/LogManagerUsage.ApproveLogManagerUsage.approved.txt @@ -0,0 +1,54 @@ +The following types hold an NServiceBus.Logging.ILog logger field obtained via LogManager.GetLogger. +For new code where DI is available, use the high performance source generated loggers from +Microsoft.Extensions.Logging instead (see https://learn.microsoft.com/dotnet/core/extensions/logger-message-generator). +Changes that make this list longer should only be approved when the type affects user-facing +code or when DI is not available. The list should otherwise shrink over time. +----- +NServiceBus.AsyncFile +NServiceBus.AuditConfigReader +NServiceBus.AuditInvalidLicenseBehavior +NServiceBus.ConsecutiveFailuresCircuitBreaker +NServiceBus.Conventions +NServiceBus.DelayedMessagePoller +NServiceBus.DelayedRetry +NServiceBus.DeserializeMessageConnector +NServiceBus.DirectoryBasedTransaction +NServiceBus.Discard +NServiceBus.EnvelopeUnwrapper +NServiceBus.ErrorQueueSettings +NServiceBus.Features.Audit +NServiceBus.Features.AutoSubscribe+ApplySubscriptions +NServiceBus.Features.FeatureStartupTaskController`1 +NServiceBus.Features.LicenseReminder +NServiceBus.HostStartupDiagnosticsWriter +NServiceBus.HostStartupDiagnosticsWriterFactory +NServiceBus.HostingComponent +NServiceBus.ImmediateRetry +NServiceBus.LearningTransportMessagePump +NServiceBus.LicenseManager +NServiceBus.LoadHandlersConnector +NServiceBus.LogErrorOnInvalidLicenseBehavior +NServiceBus.MessageDrivenSubscribeTerminator +NServiceBus.MessageDrivenUnsubscribeTerminator +NServiceBus.MigrationSubscribeTerminator +NServiceBus.MigrationUnsubscribeTerminator +NServiceBus.MoveToError +NServiceBus.PersistenceComponent +NServiceBus.PipelineModelBuilder +NServiceBus.Pipeline`1 +NServiceBus.ReceiveComponent +NServiceBus.RecoverabilityComponent +NServiceBus.RoutingToDispatchConnector +NServiceBus.RunningEndpointInstance +NServiceBus.SagaPersistenceBehavior +NServiceBus.SerializationFeature +NServiceBus.SerializationSettingsExtensions +NServiceBus.SerializeMessageConnector +NServiceBus.SubscriptionReceiverBehavior +NServiceBus.TransportReceiveToPhysicalMessageConnector +NServiceBus.Unicast.Messages.MessageMetadataRegistry +NServiceBus.UnicastPublishRouter +NServiceBus.Validations +NServiceBus.WrappedMessageReceiver +NServiceBus.XmlDeserialization +NServiceBus.XmlSerializerCache From 016bb5ec4964ab6a5cfdabfcfdcb21f4e4881514 Mon Sep 17 00:00:00 2001 From: Michelle Chamberlin Date: Wed, 5 Aug 2026 09:31:21 -0700 Subject: [PATCH 46/79] Add acceptance tests to track nullable enabled directives (#7869) --- .../API/NullabilityWarnings.cs | 153 ++++++++++ .../API/NullableEnabledDirectories.cs | 169 +++++++++++ .../API/TrimmabilityWarnings.cs | 3 +- ...gs.ApproveNullabilityWarnings.approved.txt | 283 ++++++++++++++++++ ...llableEnable.CompletedFolders.approved.txt | 69 +++++ 5 files changed, 676 insertions(+), 1 deletion(-) create mode 100644 src/NServiceBus.Core.Tests/API/NullabilityWarnings.cs create mode 100644 src/NServiceBus.Core.Tests/API/NullableEnabledDirectories.cs create mode 100644 src/NServiceBus.Core.Tests/ApprovalFiles/NullabilityWarnings.ApproveNullabilityWarnings.approved.txt create mode 100644 src/NServiceBus.Core.Tests/ApprovalFiles/NullableEnable.CompletedFolders.approved.txt diff --git a/src/NServiceBus.Core.Tests/API/NullabilityWarnings.cs b/src/NServiceBus.Core.Tests/API/NullabilityWarnings.cs new file mode 100644 index 00000000000..63ec8e86324 --- /dev/null +++ b/src/NServiceBus.Core.Tests/API/NullabilityWarnings.cs @@ -0,0 +1,153 @@ +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 nullable reference type migration effort, individual folders are annotated with +// #nullable enable one at a time (tracked in NullableEnable.CompletedFolders.approved.txt and +// NullableEnable.IncompleteFolders.approved.txt). This test previews what would happen if nullable +// reference types were force-enabled for the whole project: files that already opt in via +// #nullable enable are unaffected, but any file without an explicit directive picks up the +// project-wide default instead of staying oblivious. This test captures the current set of +// resulting warnings so that any new regressions are immediately visible. The goal is to keep the +// list shrinking over time. Once all warnings are resolved +// and the approved file empty, this test can be deleted and enable can be set +// directly in the NServiceBus.Core.csproj. +// +// Only warnings matching the hardcoded set of nullable reference type diagnostic IDs below are captured +// (https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/nullable-warnings), +// +// See https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references for more details. +[TestFixture] +public partial class NullabilityWarnings +{ + [Test] + [CancelAfter(30_000)] + public async Task ApproveNullabilityWarnings(CancellationToken cancellationToken = default) + { + var projectPath = Path.GetFullPath(Path.Combine( + TestContext.CurrentContext.TestDirectory, + "..", "..", "..", "..", + "NServiceBus.Core", + "NServiceBus.Core.csproj")); + + var binlogPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "trimming.binlog"); + + try + { + var warnings = await BuildWithNullableEnabled(projectPath, binlogPath, cancellationToken); + + Approver.Verify(warnings); + } + finally + { + if (File.Exists(binlogPath)) + { + File.Delete(binlogPath); + } + } + } + + static async Task BuildWithNullableEnabled(string projectPath, string binlogPath, 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"); + // Limit the build scope to NServiceBus.Core so warnings from referenced analyzer projects are excluded. + startInfo.ArgumentList.Add("--no-dependencies"); + startInfo.ArgumentList.Add("--no-incremental"); + startInfo.ArgumentList.Add("-p:Nullable=enable"); + startInfo.ArgumentList.Add("-p:TreatWarningsAsErrors=false"); + startInfo.ArgumentList.Add("-p:IsPackable=false"); + startInfo.ArgumentList.Add($"-bl:{binlogPath}"); + + 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 = NullableWarningRegex().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 nullable 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 CS({NullableWarningCodes}):.+")] + private static partial Regex NullableWarningRegex(); + + [GeneratedRegex(@"^(?src/[^\s:]+)")] + private static partial Regex FileRegex(); + + [GeneratedRegex($@": warning (?CS({NullableWarningCodes}):.+)$")] + private static partial Regex MessageRegex(); + + // Sourced from https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/nullable-warnings + const string NullableWarningCodes = + "8597|8598|8600|8601|8602|8603|8604|8605|8607|8608|8609|8610|8611|8612|8613|8614|" + + "8615|8616|8617|8618|8619|8620|8621|8622|8623|8624|8625|8628|8629|8631|8632|8633|" + + "8634|8636|8637|8639|8643|8644|8645|8650|8651|8655|8667|8668|8669|8670|8714|8762|" + + "8763|8764|8765|8766|8767|8768|8769|8770|8774|8775|8776|8777|8819|8824|8825|8847"; +} diff --git a/src/NServiceBus.Core.Tests/API/NullableEnabledDirectories.cs b/src/NServiceBus.Core.Tests/API/NullableEnabledDirectories.cs new file mode 100644 index 00000000000..943746294c5 --- /dev/null +++ b/src/NServiceBus.Core.Tests/API/NullableEnabledDirectories.cs @@ -0,0 +1,169 @@ +namespace NServiceBus.Core.Tests.API; + +using System; +using System.IO; +using System.Linq; +using System.Text; +using NUnit.Framework; + +[TestFixture] +public class NullableEnabledDirectories +{ + [Test] + public void EnsureFilesInCompletedDirectoriesAreAnnotated() + { + var sourceRoot = FindSourceRoot(); + var completedDirectories = ReadCompletedDirectories(sourceRoot); + + var violations = new StringBuilder(); + + foreach (var relativeDirectory in completedDirectories) + { + var directory = Path.Combine(sourceRoot, relativeDirectory); + + if (!Directory.Exists(directory)) + { + violations.AppendLine($"{relativeDirectory} (directory listed in {ApprovedFileName} no longer exists)"); + continue; + } + + foreach (var file in Directory.EnumerateFiles(directory, "*.cs", SearchOption.TopDirectoryOnly)) + { + if (!IsAnnotated(file)) + { + violations.AppendLine(Path.GetRelativePath(sourceRoot, file).Replace('\\', '/')); + } + } + } + + if (violations.Length > 0) + { + Assert.Fail( + $"The following files are missing the '#nullable enable' annotation, followed by a blank line, " + + $"even though they live in a directory listed as fully migrated in {ApprovedFileName}. Either fix " + + $"the file or, if the directory is no longer fully migrated, remove it from {ApprovedFileName}:{Environment.NewLine}{violations}"); + } + } + + [Test] + public void EnsureFullyAnnotatedDirectoriesAreListedAsCompleted() + { + var sourceRoot = FindSourceRoot(); + var completedDirectories = ReadCompletedDirectories(sourceRoot).ToHashSet(StringComparer.Ordinal); + + var missingDirectories = Directory.EnumerateFiles(sourceRoot, "*.cs", SearchOption.AllDirectories) + .Where(file => !IsBinOrObj(file)) + .GroupBy(file => Path.GetDirectoryName(file)!) + .Select(group => new + { + Directory = Path.GetRelativePath(sourceRoot, group.Key).Replace('\\', '/'), + Total = group.Count(), + Annotated = group.Count(IsAnnotated) + }) + .Where(entry => entry.Annotated == entry.Total) + .Where(entry => !completedDirectories.Contains(entry.Directory)) + .OrderBy(entry => entry.Directory, StringComparer.Ordinal) + .Select(entry => entry.Directory) + .ToArray(); + + if (missingDirectories.Length > 0) + { + Assert.Fail( + $"The following directories have every '.cs' file annotated with '#nullable enable' but are not " + + $"listed in {ApprovedFileName}. Add them to the approval file to lock in the migration:" + + $"{Environment.NewLine}{string.Join(Environment.NewLine, missingDirectories)}"); + } + } + + [Test] + [Explicit("Run this test to generate a report of directories that are not fully annotated with '#nullable enable'.")] + public void GenerateIncompleteDirectoriesReport() + { + var sourceRoot = FindSourceRoot(); + var approvalFilesDirectory = Path.Combine(sourceRoot, "NServiceBus.Core.Tests", "ApprovalFiles"); + var incompleteReportFilePath = Path.Combine(approvalFilesDirectory, IncompleteReceivedFileName); + + var incompleteReport = BuildIncompleteDirectoriesReport(sourceRoot); + + if (Path.Exists(incompleteReport)) + { + File.Delete(incompleteReportFilePath); + } + + File.WriteAllText(incompleteReportFilePath, incompleteReport); + } + + static string BuildIncompleteDirectoriesReport(string sourceRoot) + { + var builder = new StringBuilder() + .AppendLine("The following directories have at least one .cs file that is NOT annotated with #nullable enable.") + .AppendLine("Format: directoryannotated/total files.") + .AppendLine("-----") + .AppendLine("This file is for analysis purposes only and should not be committed."); + + var directories = Directory.EnumerateFiles(sourceRoot, "*.cs", SearchOption.AllDirectories) + .Where(file => !IsBinOrObj(file)) + .GroupBy(file => Path.GetDirectoryName(file)!) + .Select(group => new + { + Directory = Path.GetRelativePath(sourceRoot, group.Key).Replace('\\', '/'), + Total = group.Count(), + Annotated = group.Count(IsAnnotated) + }) + .Where(entry => entry.Annotated < entry.Total) + .OrderBy(entry => entry.Directory, StringComparer.Ordinal); + + foreach (var entry in directories) + { + builder.AppendLine($"{entry.Directory}\t{entry.Annotated}/{entry.Total}"); + } + + return builder.ToString(); + } + + static bool IsAnnotated(string file) + { + var firstTwoLines = File.ReadLines(file).Take(2).ToArray(); + var firstLine = firstTwoLines.ElementAtOrDefault(0); + var secondLine = firstTwoLines.ElementAtOrDefault(1); + + return firstLine == "#nullable enable" && secondLine == ""; + } + + static bool IsBinOrObj(string path) + { + var normalized = path.Replace('\\', '/'); + return normalized.Contains("/bin/") || normalized.Contains("/obj/"); + } + + static string[] ReadCompletedDirectories(string sourceRoot) + { + var approvedFilePath = Path.Combine(sourceRoot, "NServiceBus.Core.Tests", "ApprovalFiles", ApprovedFileName); + + return File.ReadAllLines(approvedFilePath) + .SkipWhile(line => line != "-----") + .Skip(1) + .Where(line => !string.IsNullOrWhiteSpace(line)) + .ToArray(); + } + + static string FindSourceRoot() + { + var directory = TestContext.CurrentContext.TestDirectory; + + while (directory != null) + { + if (Directory.GetFiles(directory, "*.csproj").Length == 1) + { + return Directory.GetParent(directory)!.FullName; + } + + directory = Directory.GetParent(directory)?.FullName; + } + + throw new InvalidOperationException("Could not find the src directory."); + } + + const string ApprovedFileName = "NullableEnable.CompletedFolders.approved.txt"; + const string IncompleteReceivedFileName = "NullableEnable.IncompleteFolders.txt"; +} diff --git a/src/NServiceBus.Core.Tests/API/TrimmabilityWarnings.cs b/src/NServiceBus.Core.Tests/API/TrimmabilityWarnings.cs index cfe2a14b3be..2dcd3d77278 100644 --- a/src/NServiceBus.Core.Tests/API/TrimmabilityWarnings.cs +++ b/src/NServiceBus.Core.Tests/API/TrimmabilityWarnings.cs @@ -39,7 +39,7 @@ public async Task ApproveTrimmabilityWarnings(CancellationToken cancellationToke "NServiceBus.Core", "NServiceBus.Core.csproj")); - var binlogPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "out.binlog"); + var binlogPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "nullable.binlog"); try { @@ -69,6 +69,7 @@ static async Task BuildWithTrimmingAnalyzerEnabled(string projectPath, s 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"); diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/NullabilityWarnings.ApproveNullabilityWarnings.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/NullabilityWarnings.ApproveNullabilityWarnings.approved.txt new file mode 100644 index 00000000000..b66f2de8d60 --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/NullabilityWarnings.ApproveNullabilityWarnings.approved.txt @@ -0,0 +1,283 @@ +The following nullable warnings are present in NServiceBus.Core. +Changes that make this list longer should not be approved. +----- + +src/NServiceBus.Core/EndpointConfiguration.cs + CS8600: Converting null literal or possible null value to non-nullable type. + CS8604: Possible null reference argument for parameter 'obj' in 'void Action.Invoke(TInitializationExtension obj)'. + +src/NServiceBus.Core/EndpointCreator.cs + CS8618: Non-nullable field 'envelopeComponent' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'featureComponent' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'hostingComponent' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'pipelineComponent' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'receiveComponent' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'recoverabilityComponent' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'sendComponent' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'transportSeam' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable property 'MessageSession' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8619: Nullability of reference types in value of type 'string?[]' doesn't match target type 'string[]'. + +src/NServiceBus.Core/Hosting/HostingComponent.Settings.cs + CS8604: Possible null reference argument for parameter 'value' in 'void SettingsHolder.Set(string key, object value)'. + +src/NServiceBus.Core/Licensing/LicenseReminder.cs + CS8625: Cannot convert null literal to non-nullable reference type. + +src/NServiceBus.Core/Pipeline/Incoming/LogicalMessageFactory.cs + CS8604: Possible null reference argument for parameter 'messageType' in 'MessageMetadata MessageMetadataRegistry.GetMessageMetadata(Type messageType)'. + +src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs + CS8604: Possible null reference argument for parameter 'headers' in 'OutgoingMessage.OutgoingMessage(string messageId, Dictionary headers, ReadOnlyMemory body)'. + CS8604: Possible null reference argument for parameter 'messageType' in 'MulticastAddressTag.MulticastAddressTag(Type messageType)'. + CS8604: Possible null reference argument for parameter 'properties' in 'TransportOperation.TransportOperation(OutgoingMessage message, AddressTag addressTag, DispatchProperties properties = null, DispatchConsistency requiredDispatchConsistency = DispatchConsistency.Default)'. + +src/NServiceBus.Core/Pipeline/Outgoing/SerializeMessageConnector.cs + CS8604: Possible null reference argument for parameter 'messageType' in 'MessageMetadata MessageMetadataRegistry.GetMessageMetadata(Type messageType)'. + +src/NServiceBus.Core/Routing/AutomaticSubscriptions/AutoSubscribe.cs + CS8602: Dereference of a possibly null reference. + +src/NServiceBus.Core/Routing/EndpointInstance.cs + CS8625: Cannot convert null literal to non-nullable reference type. + CS8765: Nullability of type of parameter 'obj' doesn't match overridden member (possibly because of nullability attributes). + +src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/MessageDrivenSubscribeTerminator.cs + CS8597: Thrown value may be null. + CS8601: Possible null reference assignment. + CS8604: Possible null reference argument for parameter 'messageType' in 'Task MessageDrivenSubscribeTerminator.SendSubscribeMessageWithRetries(string destination, OutgoingMessage subscriptionMessage, string messageType, ContextBag context, int retriesCount, CancellationToken cancellationToken)'. + +src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/MessageDrivenSubscriptionsConfigExtensions.cs + CS8600: Converting null literal or possible null value to non-nullable type. + CS8604: Possible null reference argument for parameter 'messageNamespace' in 'NamespacePublisherSource.NamespacePublisherSource(Assembly messageAssembly, string messageNamespace, PublisherAddress address)'. + +src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/MessageDrivenUnsubscribeTerminator.cs + CS8601: Possible null reference assignment. + CS8604: Possible null reference argument for parameter 'messageType' in 'Task MessageDrivenUnsubscribeTerminator.SendUnsubscribeMessageWithRetries(string destination, OutgoingMessage unsubscribeMessage, string messageType, ContextBag context, int retriesCount, CancellationToken cancellationToken)'. + +src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/MessageType.cs + CS8601: Possible null reference assignment. + CS8618: Non-nullable property 'TypeName' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'Version' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8765: Nullability of type of parameter 'obj' doesn't match overridden member (possibly because of nullability attributes). + +src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/PublisherAddress.cs + CS8602: Dereference of a possibly null reference. + CS8618: Non-nullable field 'addresses' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'endpoint' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'instances' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8765: Nullability of type of parameter 'obj' doesn't match overridden member (possibly because of nullability attributes). + +src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/SubscriptionReceiverBehavior.cs + CS8600: Converting null literal or possible null value to non-nullable type. + CS8603: Possible null reference return. + CS8604: Possible null reference argument for parameter 'endpoint' in 'Subscriber.Subscriber(string transportAddress, string endpoint)'. + +src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/SubscriptionRouter.cs + CS8600: Converting null literal or possible null value to non-nullable type. + +src/NServiceBus.Core/Routing/RoutingComponent.cs + CS8604: Possible null reference argument for parameter 'instanceSpecificQueue' in 'UnicastSendRouter.UnicastSendRouter(bool isSendOnly, string receiveQueueName, QueueAddress instanceSpecificQueue, IDistributionPolicy defaultDistributionPolicy, UnicastRoutingTable unicastRoutingTable, EndpointInstances endpointInstances, ITransportAddressResolver transportAddressResolver)'. + +src/NServiceBus.Core/Routing/RoutingSettings.cs + CS8600: Converting null literal or possible null value to non-nullable type. + CS8604: Possible null reference argument for parameter 'messageNamespace' in 'NamespaceRouteSource.NamespaceRouteSource(Assembly messageAssembly, string messageNamespace, UnicastRoute route)'. + +src/NServiceBus.Core/Routing/SingleInstanceRoundRobinDistributionStrategy.cs + CS8603: Possible null reference return. + +src/NServiceBus.Core/Routing/SubscriptionMigrationMode/MigrationSubscribeTerminator.cs + CS8597: Thrown value may be null. + CS8601: Possible null reference assignment. + CS8604: Possible null reference argument for parameter 'messageType' in 'Task MigrationSubscribeTerminator.SendSubscribeMessageWithRetries(string destination, OutgoingMessage subscriptionMessage, string messageType, ContextBag context, int retriesCount, CancellationToken cancellationToken)'. + +src/NServiceBus.Core/Routing/SubscriptionMigrationMode/MigrationUnsubscribeTerminator.cs + CS8601: Possible null reference assignment. + CS8604: Possible null reference argument for parameter 'messageType' in 'Task MigrationUnsubscribeTerminator.SendUnsubscribeMessageWithRetries(string destination, OutgoingMessage unsubscribeMessage, string messageType, ContextBag context, int retriesCount, CancellationToken cancellationToken)'. + +src/NServiceBus.Core/Routing/SubscriptionMigrationMode/SubscriptionMigrationModeSettings.cs + CS8600: Converting null literal or possible null value to non-nullable type. + CS8604: Possible null reference argument for parameter 'messageNamespace' in 'NamespacePublisherSource.NamespacePublisherSource(Assembly messageAssembly, string messageNamespace, PublisherAddress address)'. + +src/NServiceBus.Core/Routing/UnicastPublishRouter.cs + CS8600: Converting null literal or possible null value to non-nullable type. + +src/NServiceBus.Core/Routing/UnicastRoute.cs + CS8618: Non-nullable property 'Endpoint' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'Instance' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'PhysicalAddress' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8625: Cannot convert null literal to non-nullable reference type. + +src/NServiceBus.Core/Routing/UnicastRoutingTable.cs + CS8603: Possible null reference return. + +src/NServiceBus.Core/Routing/UnicastSendRouter.cs + CS8618: Non-nullable field 'instanceSpecificQueue' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable property 'ExplicitDestination' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'SpecificInstance' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8625: Cannot convert null literal to non-nullable reference type. + +src/NServiceBus.Core/Sagas/ActiveSagaInstance.cs + CS8604: Possible null reference argument for parameter 'InitialValue' in 'CorrelationPropertyInfo.CorrelationPropertyInfo(string Name, Type Type, object InitialValue, bool HasInitialValue)'. + CS8604: Possible null reference argument for parameter 'currentCorrelationPropertyValue' in 'void ActiveSagaInstance.ValidateCorrelationPropertyHaveValue(object currentCorrelationPropertyValue)'. + CS8604: Possible null reference argument for parameter 'currentCorrelationPropertyValue' in 'void ActiveSagaInstance.ValidateCorrelationPropertyNotModified(object currentCorrelationPropertyValue)'. + CS8618: Non-nullable field 'correlationProperty' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable property 'SagaId' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + +src/NServiceBus.Core/Sagas/CustomFinderAdapter.cs + CS8603: Possible null reference return. + +src/NServiceBus.Core/Sagas/Saga.cs + CS8604: Possible null reference argument for parameter 'message' in 'Task IPipelineContext.Send(object message, SendOptions options)'. + +src/NServiceBus.Core/Sagas/SagaCorrelationProperty.cs + CS8603: Possible null reference return. + +src/NServiceBus.Core/Sagas/SagaFinderDefinition.cs + CS8618: Non-nullable property 'MessageTypeName' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'Properties' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + +src/NServiceBus.Core/Sagas/SagaLookupValues.cs + CS8601: Possible null reference assignment. + +src/NServiceBus.Core/Sagas/SagaPersistenceBehavior.cs + CS8603: Possible null reference return. + CS8604: Possible null reference argument for parameter 'messageType' in 'bool SagaMetadata.TryGetFinder(string messageType, out SagaFinderDefinition? finderDefinition)'. + CS8604: Possible null reference argument for parameter 'value' in 'SagaCorrelationProperty.SagaCorrelationProperty(string name, object value)'. + CS8619: Nullability of reference types in value of type 'Task' doesn't match target type 'Task'. + +src/NServiceBus.Core/Serializers/XML/XmlDeserialization.cs + CS8600: Converting null literal or possible null value to non-nullable type. + CS8603: Possible null reference return. + CS8604: Possible null reference argument for parameter 'fields' in 'FieldInfo XmlDeserialization.GetField(FieldInfo[] fields, string name)'. + CS8604: Possible null reference argument for parameter 'node' in 'object XmlDeserialization.Process(XmlNode node, object parent, Type nodeType = null)'. + CS8604: Possible null reference argument for parameter 'properties' in 'PropertyInfo XmlDeserialization.GetProperty(PropertyInfo[] properties, string name)'. + CS8618: Non-nullable field 'defaultNamespace' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8625: Cannot convert null literal to non-nullable reference type. + +src/NServiceBus.Core/Serializers/XML/XmlMessageSerializer.cs + CS8603: Possible null reference return. + CS8604: Possible null reference argument for parameter 'messageType' in 'XmlSerialization.XmlSerialization(Type messageType, Stream stream, object message, Conventions conventions, XmlSerializerCache cache, bool skipWrappingRawXml, string @namespace = "http://tempuri.net")'. + CS8625: Cannot convert null literal to non-nullable reference type. + CS8767: Nullability of reference types in type of parameter 'messageTypesToDeserialize' of 'object[] XmlMessageSerializer.Deserialize(ReadOnlyMemory body, IList messageTypesToDeserialize = null)' doesn't match implicitly implemented member 'object[] IMessageSerializer.Deserialize(ReadOnlyMemory body, IList? messageTypes = null)' (possibly because of nullability attributes). + +src/NServiceBus.Core/Serializers/XML/XmlSanitizingStream.cs + CS8603: Possible null reference return. + +src/NServiceBus.Core/Serializers/XML/XmlSerialization.cs + CS8604: Possible null reference argument for parameter 'elem' in 'void XmlSerialization.WriteObject(XElement elem, string name, Type type, object value, bool useNS = false)'. + CS8604: Possible null reference argument for parameter 'item' in 'bool List.Contains(string item)'. + CS8604: Possible null reference argument for parameter 'value' in 'void XmlSerialization.WriteObject(XElement elem, string name, Type type, object value, bool useNS = false)'. + +src/NServiceBus.Core/Serializers/XML/XmlSerializerCache.cs + CS8604: Possible null reference argument for parameter 'typeArguments' in 'Type Type.MakeGenericType(params Type[] typeArguments)'. + +src/NServiceBus.Core/Settings/SettingsHolder.cs + CS8601: Possible null reference assignment. + CS8603: Possible null reference return. + CS8604: Possible null reference argument for parameter 'key' in 'T SettingsHolder.GetOrDefault(string key)'. + CS8604: Possible null reference argument for parameter 'key' in 'bool SettingsHolder.HasExplicitValue(string key)'. + CS8604: Possible null reference argument for parameter 'key' in 'bool SettingsHolder.HasSetting(string key)'. + CS8604: Possible null reference argument for parameter 'key' in 'bool SettingsHolder.TryGet(string key, out T val)'. + CS8604: Possible null reference argument for parameter 'key' in 'object SettingsHolder.Get(string key)'. + CS8604: Possible null reference argument for parameter 'key' in 'void SettingsHolder.Set(string key, object value)'. + CS8604: Possible null reference argument for parameter 'key' in 'void SettingsHolder.SetDefault(string key, object value)'. + CS8604: Possible null reference argument for parameter 'value' in 'void SettingsHolder.Set(string key, object value)'. + CS8604: Possible null reference argument for parameter 'value' in 'void SettingsHolder.SetDefault(string key, object value)'. + +src/NServiceBus.Core/StartableEndpoint.cs + CS8618: Non-nullable field 'stoppingTokenSource' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'transportInfrastructure' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + +src/NServiceBus.Core/Transports/IncomingMessageExtensions.cs + CS8603: Possible null reference return. + +src/NServiceBus.Core/Transports/Learning/DelayedMessagePoller.cs + CS8618: Non-nullable field 'polling' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + +src/NServiceBus.Core/Transports/Learning/DirectoryBasedTransaction.cs + CS8618: Non-nullable property 'FileToProcess' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + +src/NServiceBus.Core/Transports/Learning/HeaderSerializer.cs + CS8603: Possible null reference return. + +src/NServiceBus.Core/Transports/Learning/LearningTransport.cs + CS8618: Non-nullable property 'StorageDirectory' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + +src/NServiceBus.Core/Transports/Learning/LearningTransportDispatcher.cs + CS8604: Possible null reference argument for parameter 'path3' in 'string Path.Combine(string path1, string path2, string path3)'. + +src/NServiceBus.Core/Transports/Learning/LearningTransportInfrastructure.cs + CS8604: Possible null reference argument for parameter 'subscriptionManager' in 'LearningTransportMessagePump.LearningTransportMessagePump(string id, string receiveAddress, string basePath, Action criticalErrorAction, ISubscriptionManager subscriptionManager, ReceiveSettings receiveSettings, TransportTransactionMode transactionMode)'. + +src/NServiceBus.Core/Transports/Learning/LearningTransportMessagePump.cs + CS8618: Non-nullable field 'bodyDir' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'committedTransactionDir' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'concurrencyLimiter' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'delayedDir' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'delayedMessagePoller' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'messageProcessingCancellationTokenSource' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'messagePumpBasePath' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'messagePumpCancellationTokenSource' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'messagePumpTask' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'onError' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'onMessage' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'pendingTransactionDir' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8625: Cannot convert null literal to non-nullable reference type. + +src/NServiceBus.Core/Transports/Learning/LearningTransportSubscriptionManager.cs + CS8604: Possible null reference argument for parameter 'path2' in 'string Path.Combine(string path1, string path2)'. + +src/NServiceBus.Core/Transports/Learning/NoTransaction.cs + CS8618: Non-nullable property 'FileToProcess' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + +src/NServiceBus.Core/Transports/QueueAddress.cs + CS8625: Cannot convert null literal to non-nullable reference type. + +src/NServiceBus.Core/Transports/TransportInfrastructure.cs + CS8618: Non-nullable property 'Dispatcher' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'Receivers' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + +src/NServiceBus.Core/Transports/TransportOperation.cs + CS8625: Cannot convert null literal to non-nullable reference type. + +src/NServiceBus.Core/Transports/TransportSeam.cs + CS8618: Non-nullable field 'receiverSettings' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'transportInfrastructure' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + +src/NServiceBus.Core/Unicast/MessageOperations.cs + CS8604: Possible null reference argument for parameter 'message' in 'Task MessageOperations.Publish(IBehaviorContext context, Type messageType, object message, PublishOptions options)'. + CS8604: Possible null reference argument for parameter 'message' in 'Task MessageOperations.ReplyMessage(IBehaviorContext context, Type messageType, object message, ReplyOptions options)'. + CS8604: Possible null reference argument for parameter 'message' in 'Task MessageOperations.SendMessage(IBehaviorContext context, Type messageType, object message, SendOptions options)'. + CS8604: Possible null reference argument for parameter 'messageType' in 'Task MessageOperations.Publish(IBehaviorContext context, Type messageType, object message, PublishOptions options)'. + CS8604: Possible null reference argument for parameter 'messageType' in 'Task MessageOperations.ReplyMessage(IBehaviorContext context, Type messageType, object message, ReplyOptions options)'. + CS8604: Possible null reference argument for parameter 'messageType' in 'Task MessageOperations.SendMessage(IBehaviorContext context, Type messageType, object message, SendOptions options)'. + +src/NServiceBus.Core/Unicast/Messages/MessageMetadata.cs + CS8604: Possible null reference argument for parameter 'item' in 'bool ICollection.Contains(string item)'. + CS8625: Cannot convert null literal to non-nullable reference type. + +src/NServiceBus.Core/Unicast/Messages/MessageMetadataRegistry.cs + CS8601: Possible null reference assignment. + CS8603: Possible null reference return. + CS8604: Possible null reference argument for parameter 'key' in 'bool ConcurrentDictionary.TryAdd(string key, Type value)'. + CS8618: Non-nullable field 'isMessageType' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + +src/NServiceBus.Core/Unicast/Queuing/QueueNotFoundException.cs + CS8618: Non-nullable property 'Queue' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + +src/NServiceBus.Core/VersionInformation.cs + CS8601: Possible null reference assignment. + CS8618: Non-nullable property 'MajorMinorPatch' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + +src/NServiceBus.Core/obsoletes-v10.cs + CS8618: Non-nullable field 'isMessageType' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'sagaFinders' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable property 'AssociatedMessages' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'EntityName' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'Loader' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'NotFoundHandler' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'SagaDataFactory' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'SagaEntityType' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'SagaType' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableEnable.CompletedFolders.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableEnable.CompletedFolders.approved.txt new file mode 100644 index 00000000000..a01570c7de4 --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableEnable.CompletedFolders.approved.txt @@ -0,0 +1,69 @@ +The following directories have every .cs file annotated with #nullable enable. +Changes that remove a directory from this list, or add a directory whose files are not +all annotated, should not be approved. New files added to a listed directory must also +be annotated with #nullable enable. +----- +NServiceBus.AcceptanceTests/Core/LoggingIntegration +NServiceBus.Core.Analyzer/Features +NServiceBus.Core.Tests/Receiving +NServiceBus.Core.Tests/Utils +NServiceBus.Core/Audit +NServiceBus.Core/Causation +NServiceBus.Core/CircuitBreakers +NServiceBus.Core/Conventions +NServiceBus.Core/Correlation +NServiceBus.Core/CriticalError +NServiceBus.Core/DelayedDelivery +NServiceBus.Core/Envelopes +NServiceBus.Core/Extensibility +NServiceBus.Core/Features +NServiceBus.Core/Hosting +NServiceBus.Core/Hosting/Helpers +NServiceBus.Core/Hosting/KeyedServices +NServiceBus.Core/Hosting/StartupDiagnostics +NServiceBus.Core/IdGeneration +NServiceBus.Core/Installation +NServiceBus.Core/Licensing +NServiceBus.Core/Logging +NServiceBus.Core/MessageInterfaces +NServiceBus.Core/MessageInterfaces/MessageMapper +NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection +NServiceBus.Core/MessageMutators +NServiceBus.Core/MessageMutators/MutateInstanceMessage +NServiceBus.Core/MessageMutators/MutateTransportMessage +NServiceBus.Core/Notifications +NServiceBus.Core/OpenTelemetry +NServiceBus.Core/OpenTelemetry/Metrics +NServiceBus.Core/OpenTelemetry/Tracing +NServiceBus.Core/Performance/MessageProcessingOptimizations +NServiceBus.Core/Performance/Statistics +NServiceBus.Core/Performance/TimeToBeReceived +NServiceBus.Core/Persistence +NServiceBus.Core/Persistence/Learning +NServiceBus.Core/Persistence/Learning/SagaPersister +NServiceBus.Core/Pipeline +NServiceBus.Core/Pipeline/Incoming +NServiceBus.Core/Pipeline/Outgoing +NServiceBus.Core/Receiving +NServiceBus.Core/Recoverability +NServiceBus.Core/Recoverability/DelayedRetries +NServiceBus.Core/Recoverability/ImmediateRetries +NServiceBus.Core/Recoverability/Settings +NServiceBus.Core/Serializers/SystemJson +NServiceBus.Core/Support +NServiceBus.Core/Unicast/Config +NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope +NServiceBus.Core.Tests/Reliability/Outbox +NServiceBus.Core.Tests/ServicePlatform/Retries +NServiceBus.Core.Tests/Utils/Reflection +NServiceBus.Core/Recoverability/Faults +NServiceBus.Core/Reliability/Outbox +NServiceBus.Core/Reliability/SynchronizedStorage +NServiceBus.Core/Serialization +NServiceBus.Core/ServicePlatform/Retries +NServiceBus.Core/StaticHeaders +NServiceBus.Core/UnitOfWork +NServiceBus.Core/UnitOfWork/TransactionScopes +NServiceBus.Core/Utils +NServiceBus.Core/Utils/Reflection +NServiceBus.Core/Utils/Reflection \ No newline at end of file From 0c2dc7307ada3d731b6c80327709de8505c8ebf2 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Wed, 5 Aug 2026 17:53:55 -0400 Subject: [PATCH 47/79] Update dependency Particular.Licensing.Sources to 7.4.0 (#7891) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- 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 e181f3c8185..b760413f868 100644 --- a/src/NServiceBus.Core/NServiceBus.Core.csproj +++ b/src/NServiceBus.Core/NServiceBus.Core.csproj @@ -20,7 +20,7 @@ - + From 01b76ac521e3d8c3b08c09db6333c1b53c9246ec Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 6 Aug 2026 06:29:44 +0200 Subject: [PATCH 48/79] Rename binlog file for nullable warnings (#7903) * Rename binlog file for nullable warnings * Rename binlog file from nullable to trimming --- src/NServiceBus.Core.Tests/API/NullabilityWarnings.cs | 2 +- src/NServiceBus.Core.Tests/API/TrimmabilityWarnings.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NServiceBus.Core.Tests/API/NullabilityWarnings.cs b/src/NServiceBus.Core.Tests/API/NullabilityWarnings.cs index 63ec8e86324..df390f9e456 100644 --- a/src/NServiceBus.Core.Tests/API/NullabilityWarnings.cs +++ b/src/NServiceBus.Core.Tests/API/NullabilityWarnings.cs @@ -39,7 +39,7 @@ public async Task ApproveNullabilityWarnings(CancellationToken cancellationToken "NServiceBus.Core", "NServiceBus.Core.csproj")); - var binlogPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "trimming.binlog"); + var binlogPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "nullable.binlog"); try { diff --git a/src/NServiceBus.Core.Tests/API/TrimmabilityWarnings.cs b/src/NServiceBus.Core.Tests/API/TrimmabilityWarnings.cs index 2dcd3d77278..968c6225d5d 100644 --- a/src/NServiceBus.Core.Tests/API/TrimmabilityWarnings.cs +++ b/src/NServiceBus.Core.Tests/API/TrimmabilityWarnings.cs @@ -39,7 +39,7 @@ public async Task ApproveTrimmabilityWarnings(CancellationToken cancellationToke "NServiceBus.Core", "NServiceBus.Core.csproj")); - var binlogPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "nullable.binlog"); + var binlogPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "trimming.binlog"); try { @@ -140,4 +140,4 @@ static string ScrubLine(string line) [GeneratedRegex(@": warning (?IL[23]\d{3}:.+)$")] private static partial Regex MessageRegex(); -} \ No newline at end of file +} From 5de16fc0d98567a34cd990163fc40f4febf5161d Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:01:08 +0200 Subject: [PATCH 49/79] Update .NET Packages (#7857) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj | 2 +- src/NServiceBus.Core/NServiceBus.Core.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj index 3ebaa6af8dc..84ef9d26d5b 100644 --- a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj +++ b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/NServiceBus.Core/NServiceBus.Core.csproj b/src/NServiceBus.Core/NServiceBus.Core.csproj index b760413f868..d159c904ba3 100644 --- a/src/NServiceBus.Core/NServiceBus.Core.csproj +++ b/src/NServiceBus.Core/NServiceBus.Core.csproj @@ -13,9 +13,9 @@ - + - + From 0f4a6fda837060851f3dec5dfcda6a8b40000e3a Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 6 Aug 2026 17:13:53 +0200 Subject: [PATCH 50/79] Remove binlog generation from nullable and trimming warnings tests (#7904) Co-authored-by: Daniel Marbach --- .../API/NullabilityWarnings.cs | 19 +++---------------- .../API/TrimmabilityWarnings.cs | 19 +++---------------- 2 files changed, 6 insertions(+), 32 deletions(-) diff --git a/src/NServiceBus.Core.Tests/API/NullabilityWarnings.cs b/src/NServiceBus.Core.Tests/API/NullabilityWarnings.cs index df390f9e456..96103ab8773 100644 --- a/src/NServiceBus.Core.Tests/API/NullabilityWarnings.cs +++ b/src/NServiceBus.Core.Tests/API/NullabilityWarnings.cs @@ -39,24 +39,12 @@ public async Task ApproveNullabilityWarnings(CancellationToken cancellationToken "NServiceBus.Core", "NServiceBus.Core.csproj")); - var binlogPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "nullable.binlog"); + var warnings = await BuildWithNullableEnabled(projectPath, cancellationToken); - try - { - var warnings = await BuildWithNullableEnabled(projectPath, binlogPath, cancellationToken); - - Approver.Verify(warnings); - } - finally - { - if (File.Exists(binlogPath)) - { - File.Delete(binlogPath); - } - } + Approver.Verify(warnings); } - static async Task BuildWithNullableEnabled(string projectPath, string binlogPath, CancellationToken cancellationToken = default) + static async Task BuildWithNullableEnabled(string projectPath, CancellationToken cancellationToken = default) { var startInfo = new ProcessStartInfo { @@ -76,7 +64,6 @@ static async Task BuildWithNullableEnabled(string projectPath, string bi startInfo.ArgumentList.Add("-p:Nullable=enable"); startInfo.ArgumentList.Add("-p:TreatWarningsAsErrors=false"); startInfo.ArgumentList.Add("-p:IsPackable=false"); - startInfo.ArgumentList.Add($"-bl:{binlogPath}"); using var process = Process.Start(startInfo)!; diff --git a/src/NServiceBus.Core.Tests/API/TrimmabilityWarnings.cs b/src/NServiceBus.Core.Tests/API/TrimmabilityWarnings.cs index 968c6225d5d..7fbd402b329 100644 --- a/src/NServiceBus.Core.Tests/API/TrimmabilityWarnings.cs +++ b/src/NServiceBus.Core.Tests/API/TrimmabilityWarnings.cs @@ -39,24 +39,12 @@ public async Task ApproveTrimmabilityWarnings(CancellationToken cancellationToke "NServiceBus.Core", "NServiceBus.Core.csproj")); - var binlogPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "trimming.binlog"); + var warnings = await BuildWithTrimmingAnalyzerEnabled(projectPath, cancellationToken); - try - { - var warnings = await BuildWithTrimmingAnalyzerEnabled(projectPath, binlogPath, cancellationToken); - - Approver.Verify(warnings); - } - finally - { - if (File.Exists(binlogPath)) - { - File.Delete(binlogPath); - } - } + Approver.Verify(warnings); } - static async Task BuildWithTrimmingAnalyzerEnabled(string projectPath, string binlogPath, CancellationToken cancellationToken = default) + static async Task BuildWithTrimmingAnalyzerEnabled(string projectPath, CancellationToken cancellationToken = default) { var startInfo = new ProcessStartInfo { @@ -73,7 +61,6 @@ static async Task BuildWithTrimmingAnalyzerEnabled(string projectPath, s startInfo.ArgumentList.Add("-p:EnableTrimAnalyzer=true"); startInfo.ArgumentList.Add("-p:TreatWarningsAsErrors=false"); startInfo.ArgumentList.Add("-p:IsPackable=false"); - startInfo.ArgumentList.Add($"-bl:{binlogPath}"); using var process = Process.Start(startInfo)!; From 1d534882dd4359c15be97e804a3ce7579550bc2f Mon Sep 17 00:00:00 2001 From: Michelle Chamberlin Date: Thu, 6 Aug 2026 16:36:45 -0700 Subject: [PATCH 51/79] Refine TODO comment for nullable changes (#7905) * Refine TODO comment for TryGet method * Update TODO comment for enforcing non-null values * suffixed the todo --------- Co-authored-by: Jayanthi --- src/NServiceBus.Core/Reliability/Outbox/IOutboxStorage.cs | 4 ++-- .../CompletableSynchronizedStorageSessionExtensions.cs | 2 +- .../StaticHeaders/StaticHeadersConfigExtensions.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/NServiceBus.Core/Reliability/Outbox/IOutboxStorage.cs b/src/NServiceBus.Core/Reliability/Outbox/IOutboxStorage.cs index bbc7f8973d8..31f8137568e 100644 --- a/src/NServiceBus.Core/Reliability/Outbox/IOutboxStorage.cs +++ b/src/NServiceBus.Core/Reliability/Outbox/IOutboxStorage.cs @@ -11,7 +11,7 @@ namespace NServiceBus.Outbox; /// public interface IOutboxStorage { - //TODO: Consider renaming this to TryGet in the next major to better reflect that it might not get a message + //TODO(nullable-major-release): Consider renaming this to TryGet in the next major to better reflect that it might not get a message // and instead returns null. /// @@ -40,4 +40,4 @@ public interface IOutboxStorage /// A to observe. /// The created outbox transaction. Task BeginTransaction(ContextBag context, CancellationToken cancellationToken = default); -} \ No newline at end of file +} diff --git a/src/NServiceBus.Core/Reliability/SynchronizedStorage/CompletableSynchronizedStorageSessionExtensions.cs b/src/NServiceBus.Core/Reliability/SynchronizedStorage/CompletableSynchronizedStorageSessionExtensions.cs index a485b3f5e65..cf8e139a931 100644 --- a/src/NServiceBus.Core/Reliability/SynchronizedStorage/CompletableSynchronizedStorageSessionExtensions.cs +++ b/src/NServiceBus.Core/Reliability/SynchronizedStorage/CompletableSynchronizedStorageSessionExtensions.cs @@ -9,7 +9,7 @@ namespace NServiceBus.Persistence; using Pipeline; using Transport; -// TODO Investigate if these should even be public. If they should, then they need to have argument validtion added. +// TODO(nullable-major-release): Investigate if these should even be public. If they should, then they need to have argument validtion added. /// /// Extension methods for . diff --git a/src/NServiceBus.Core/StaticHeaders/StaticHeadersConfigExtensions.cs b/src/NServiceBus.Core/StaticHeaders/StaticHeadersConfigExtensions.cs index 382a947ef41..bad8ad5921c 100644 --- a/src/NServiceBus.Core/StaticHeaders/StaticHeadersConfigExtensions.cs +++ b/src/NServiceBus.Core/StaticHeaders/StaticHeadersConfigExtensions.cs @@ -22,7 +22,7 @@ public static void AddHeaderToAllOutgoingMessages(this EndpointConfiguration con { ArgumentException.ThrowIfNullOrWhiteSpace(key); - // TODO Uncomment this in the next major version so that we actually enforce non-null values here + // TODO(nullable-major-release): Uncomment this in the next major version so that we actually enforce non-null values here //ArgumentException.ThrowIfNullOrWhiteSpace(value); if (!config.Settings.TryGet(out CurrentStaticHeaders headers)) @@ -35,4 +35,4 @@ public static void AddHeaderToAllOutgoingMessages(this EndpointConfiguration con headers[key] = value; } -} \ No newline at end of file +} From 110d7e92527590c0ca5b5366aa16b07c7eafc0fc Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Tue, 11 Aug 2026 17:40:30 -0400 Subject: [PATCH 52/79] Update .NET Packages to 10.0.11 (#7909) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- src/NServiceBus.Core/NServiceBus.Core.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/NServiceBus.Core/NServiceBus.Core.csproj b/src/NServiceBus.Core/NServiceBus.Core.csproj index d159c904ba3..1d69e29300a 100644 --- a/src/NServiceBus.Core/NServiceBus.Core.csproj +++ b/src/NServiceBus.Core/NServiceBus.Core.csproj @@ -13,9 +13,9 @@ - + - + From e64390ff8b6fb8e701c68a4ab7c9909eb0efa3dd Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:43:43 +0000 Subject: [PATCH 53/79] Update dependency Microsoft.NET.Test.Sdk to 18.9.0 (#7913) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- .../NServiceBus.AcceptanceTests.csproj | 2 +- .../NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj | 2 +- src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj | 2 +- .../NServiceBus.Learning.AcceptanceTests.csproj | 2 +- .../NServiceBus.PersistenceTests.csproj | 2 +- .../NServiceBus.TransportTests.csproj | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj b/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj index 037e3ae0421..d181fe5b3bc 100644 --- a/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj +++ b/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj index 66c68d2c04f..35c0111a6f7 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj index 84ef9d26d5b..9ddec1e97fe 100644 --- a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj +++ b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj b/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj index 0f603634cff..d7f6bd8f6da 100644 --- a/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj +++ b/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj b/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj index f64554b017e..7f31352f514 100644 --- a/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj +++ b/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj b/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj index 4840571374f..26412a155ec 100644 --- a/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj +++ b/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj @@ -10,7 +10,7 @@ - + From 81b375b335143d5eed82390dd6464a9d5fa6cc96 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:15:49 -0400 Subject: [PATCH 54/79] Update dependency Microsoft.Extensions.Diagnostics.Testing to 10.9.0 (#7910) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj index 9ddec1e97fe..c500ec289cd 100644 --- a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj +++ b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj @@ -14,7 +14,7 @@ - + From 833fcfbab948fec3cdb1678a01c573f2ce2f88b8 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Mon, 24 Aug 2026 12:10:29 +0200 Subject: [PATCH 55/79] Add trimming-safe messaging overloads and migration analyzer (#7889) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Annotate assembly routing for trimming * Scope conditional trim suppressions * Use exact handler reflection lookups * Use message hierarchy for learning transport * Isolate runtime message hierarchy reflection * Simplify marker checks * Preserve message members for trimming * Remove legacy System.Web hosting detection * Add typed message instance overloads * Add typed message migration analyzer * Suppress intentional runtime routing warnings * Annotate object overloads correctly * Address Saga Trimming warning * Add explicit type overloads for advanced uses cases to avoid having to fall back to any form of reflection * Pre-obsolete * UpdateMessage * Deprecate ReplyToOriginator * Nit Co-authored-by: Andreas Öhlund * Better wording * Validate explicit type in testable outgoing context * Deprecate object-only pipeline convenience methods * Test existing-instance typed and explicit type paths * Avoid NSB0039 fix producing object message type * Add annotation parity tests for testing fakes * Document declared logical message types * Better wording * First crude version with msbuild property * Switch to editorconfig as a trial * Approve API changes * Support more properties * Use severity * Global severity * Expand messaging migration analyzer coverage * Support method groups in messaging migration fixer * Improve performance by memoizing * Replace issue link --------- Co-authored-by: Daniel Marbach Co-authored-by: Andreas Öhlund --- ...n_overriding_conversation_id_generation.cs | 2 + .../When_incoming_message_handled.cs | 2 + ...using_per_uow_component_in_the_pipeline.cs | 2 + ...hen_replying_with_pre_created_interface.cs | 2 + ...apping_saga_messages_using_base_classes.cs | 4 + .../Sagas/When_using_ReplyToOriginator.cs | 2 +- ...ReplyToOriginator_and_outgoing_behavior.cs | 2 +- ...en_using_ReplyToOriginator_with_headers.cs | 2 +- ...When_headers_contain_special_characters.cs | 2 + ...ctional_message_is_moved_to_error_queue.cs | 2 + ...iveonly_message_is_moved_to_error_queue.cs | 2 + ...oncrete_event_with_trimming_safe_mapper.cs | 2 + ...t_implementing_two_unrelated_interfaces.cs | 2 + .../When_publishing_using_base_type.cs | 2 + .../Sagas/When_message_has_a_saga_id.cs | 2 + .../Sagas/When_replying_to_originator.cs | 2 +- ...n_replying_to_originator_from_a_timeout.cs | 2 +- .../MessagingMigrationFixer.cs | 235 ++ .../MessagingMigrationAnalyzerTests.cs | 3362 +++++++++++++++++ .../MessagingMigrationFixerTests.cs | 977 +++++ .../MessagingMigrationSyntaxTests.cs | 40 + ...viceBus.Core.Analyzer.Tests.Roslyn5.csproj | 1 + .../DiagnosticIds.cs | 5 + .../MessagingMigrationAnalyzer.cs | 784 ++++ ...IApprovals.ApproveNServiceBus.approved.txt | 200 +- ...gs.ApproveNullabilityWarnings.approved.txt | 10 +- ...s.ApproveTrimmabilityWarnings.approved.txt | 35 +- .../Hosting/HostTests.cs | 2 +- .../OutgoingLogicalMessageContextTests.cs | 24 +- .../TestingFakesAnnotationParityTests.cs | 113 + .../LearningTransportDispatcherTests.cs | 123 +- .../TypedMessageInstanceOverloadsTests.cs | 935 +++++ .../Unicast/MessageOperationsTests.cs | 125 +- .../NServiceBusMarkerInterfaceConvention.cs | 13 +- .../DynamicMemberTypeAccess.cs | 5 + .../Hosting/StartupDiagnostics/Host.cs | 56 +- src/NServiceBus.Core/IMessageCreator.cs | 2 +- .../IMessageProcessingContext.cs | 40 +- src/NServiceBus.Core/IMessageSession.cs | 78 +- src/NServiceBus.Core/IPipelineContext.cs | 74 +- .../MessageProcessingContextExtensions.cs | 40 +- src/NServiceBus.Core/MessageSession.cs | 58 + .../MessageSessionExtensions.cs | 182 +- src/NServiceBus.Core/NServiceBus.Core.csproj | 4 + src/NServiceBus.Core/NServiceBus.props | 9 + .../Pipeline/Incoming/IncomingContext.cs | 37 + .../IOutgoingLogicalMessageContext.cs | 36 + .../Pipeline/Outgoing/OutgoingContext.cs | 25 + .../Outgoing/OutgoingLogicalMessage.cs | 6 +- .../Outgoing/OutgoingLogicalMessageContext.cs | 35 + .../PipelineContextExtensions.cs | 174 +- .../ReceiveComponent.Configuration.cs | 2 + .../Routing/AssemblyRouteSource.cs | 5 + .../AssemblyPublisherSource.cs | 6 + ...sageDrivenSubscriptionsConfigExtensions.cs | 3 + .../NamespacePublisherSource.cs | 3 + .../Routing/MulticastAddressTag.cs | 4 +- .../Routing/MulticastRoutingStrategy.cs | 4 +- .../Routing/NamespaceRouteSource.cs | 3 + .../Routing/RoutingSettings.cs | 3 + .../SubscriptionMigrationModeSettings.cs | 3 + src/NServiceBus.Core/Sagas/Saga.cs | 42 +- .../Learning/LearningTransportDispatcher.cs | 54 +- .../Transports/MulticastTransportOperation.cs | 4 +- .../Transports/TransportSeam.cs | 8 +- .../Unicast/MessageHandlerRegistry.cs | 6 +- .../Unicast/MessageOperations.cs | 49 +- .../Messages/MessageMetadataRegistry.cs | 58 +- .../Unicast/RunningEndpointInstance.cs | 50 + .../DynamicMemberTypeAccess.cs | 14 + .../NServiceBus.Testing.Fakes.csproj | 4 + .../TestableMessageProcessingContext.cs | 28 +- .../TestableMessageSession.cs | 54 +- .../TestableOutgoingLogicalMessageContext.cs | 22 + .../TestablePipelineContext.cs | 53 +- src/Shared/MessageTypeValidator.cs | 22 + 76 files changed, 8155 insertions(+), 230 deletions(-) create mode 100644 src/NServiceBus.Core.Analyzer.Fixes/MessagingMigrationFixer.cs create mode 100644 src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationAnalyzerTests.cs create mode 100644 src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationFixerTests.cs create mode 100644 src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationSyntaxTests.cs create mode 100644 src/NServiceBus.Core.Analyzer/MessagingMigrationAnalyzer.cs create mode 100644 src/NServiceBus.Core.Tests/TestingFakesAnnotationParityTests.cs create mode 100644 src/NServiceBus.Core.Tests/TypedMessageInstanceOverloadsTests.cs create mode 100644 src/NServiceBus.Testing.Fakes/DynamicMemberTypeAccess.cs create mode 100644 src/Shared/MessageTypeValidator.cs diff --git a/src/NServiceBus.AcceptanceTests/Core/Causation/When_overriding_conversation_id_generation.cs b/src/NServiceBus.AcceptanceTests/Core/Causation/When_overriding_conversation_id_generation.cs index 5b4705467fe..f51eee123b6 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Causation/When_overriding_conversation_id_generation.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Causation/When_overriding_conversation_id_generation.cs @@ -28,7 +28,9 @@ public async Task Should_use_custom_id() options.RouteToThisEndpoint(); options.SetHeader(TennantIdHeaderKey, tennantId); +#pragma warning disable NSB0040 // Intentional runtime-type routing await session.Send(myBusinessMessage, options); +#pragma warning restore NSB0040 await session.SendLocal(new MessageSentOutsideOfHandlerNotMatchingTheConvention()); })) .Run(); diff --git a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/When_incoming_message_handled.cs b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/When_incoming_message_handled.cs index 5faef750d6c..b7a85105b2c 100644 --- a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/When_incoming_message_handled.cs +++ b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/When_incoming_message_handled.cs @@ -73,7 +73,9 @@ static async Task WhenMessagesHandled(Func mess { try { +#pragma warning disable NSB0040 // Intentional runtime-type routing await session.SendLocal(messageFactory.Invoke()); +#pragma warning restore NSB0040 } catch (Exception e) { diff --git a/src/NServiceBus.AcceptanceTests/Core/Pipeline/When_using_per_uow_component_in_the_pipeline.cs b/src/NServiceBus.AcceptanceTests/Core/Pipeline/When_using_per_uow_component_in_the_pipeline.cs index 5b2ff3b388b..0fe68546a95 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Pipeline/When_using_per_uow_component_in_the_pipeline.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Pipeline/When_using_per_uow_component_in_the_pipeline.cs @@ -38,7 +38,9 @@ static Task SendMessage(IMessageSession s) options.SetHeader("Value", uniqueValue); var message = new Message { Value = uniqueValue }; +#pragma warning disable NSB0040 // Intentional runtime-type routing return s.Send(message, options); +#pragma warning restore NSB0040 } public class Context : ScenarioContext diff --git a/src/NServiceBus.AcceptanceTests/Core/Routing/When_replying_with_pre_created_interface.cs b/src/NServiceBus.AcceptanceTests/Core/Routing/When_replying_with_pre_created_interface.cs index 6a1dbea213b..e02d47e3049 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Routing/When_replying_with_pre_created_interface.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Routing/When_replying_with_pre_created_interface.cs @@ -39,7 +39,9 @@ public class StartMessageHandler(IMessageCreator messageCreator) : IHandleMessag public Task Handle(MyRequest message, IMessageHandlerContext context) { var interfaceMessage = messageCreator.CreateInstance(); +#pragma warning disable NSB0040 // Intentional runtime-type routing return context.Reply(interfaceMessage); +#pragma warning restore NSB0040 } } diff --git a/src/NServiceBus.AcceptanceTests/Core/Sagas/When_mapping_saga_messages_using_base_classes.cs b/src/NServiceBus.AcceptanceTests/Core/Sagas/When_mapping_saga_messages_using_base_classes.cs index 0332eef2031..559277366b1 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Sagas/When_mapping_saga_messages_using_base_classes.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Sagas/When_mapping_saga_messages_using_base_classes.cs @@ -21,7 +21,9 @@ public async Task Should_apply_base_class_mapping_to_sub_classes(CancellationTok { SomeId = correlationId }; +#pragma warning disable NSB0040 // Intentional runtime-type routing return session.SendLocal(startSagaMessage); +#pragma warning restore NSB0040 })) .Run(cancellationToken); @@ -55,7 +57,9 @@ public Task Handle(StartSagaMessage message, IMessageHandlerContext context) { SomeId = message.SomeId }; +#pragma warning disable NSB0040 // Intentional runtime-type routing return context.SendLocal(sagaMessage); +#pragma warning restore NSB0040 } protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) => diff --git a/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator.cs b/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator.cs index 6be7a1ebb3f..c75feb8dc2a 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator.cs @@ -62,7 +62,7 @@ public Task Handle(InitiateRequestingSaga message, IMessageHandlerContext contex return Task.CompletedTask; } - public Task Handle(MessageThatWillCauseSagaToReplyToOriginator message, IMessageHandlerContext context) => ReplyToOriginator(context, new MyReplyToOriginator()); + public Task Handle(MessageThatWillCauseSagaToReplyToOriginator message, IMessageHandlerContext context) => ReplyToOriginator(context, new MyReplyToOriginator()); protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) => mapper.MapSaga(s => s.SagaCorrelationId) diff --git a/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator_and_outgoing_behavior.cs b/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator_and_outgoing_behavior.cs index ecad611a87e..1aed6b5e757 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator_and_outgoing_behavior.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator_and_outgoing_behavior.cs @@ -93,7 +93,7 @@ public Task Handle(ContinueSagaMessage message, IMessageHandlerContext context) testContext.ContinueSagaMessageCorrId = context.MessageHeaders[Headers.CorrelationId]; testContext.SagaContinued = true; testContext.MaybeCompleted(); - return ReplyToOriginator(context, new ReplyToOriginatorMessage()); + return ReplyToOriginator(context, new ReplyToOriginatorMessage()); } protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) => diff --git a/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator_with_headers.cs b/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator_with_headers.cs index c79707751e7..cdc13d7c817 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator_with_headers.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator_with_headers.cs @@ -43,7 +43,7 @@ public class ReplyingSaga : Saga, public Task Handle(InitiateRequestingSaga message, IMessageHandlerContext context) { var customHeaders = new Dictionary { { "CustomHeader", Data.CustomHeaderValue.ToString() } }; - return ReplyToOriginator(context, new MyReplyToOriginator(), customHeaders); + return ReplyToOriginator(context, new MyReplyToOriginator(), customHeaders); } protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) => diff --git a/src/NServiceBus.AcceptanceTests/Outbox/When_headers_contain_special_characters.cs b/src/NServiceBus.AcceptanceTests/Outbox/When_headers_contain_special_characters.cs index ad5f841537a..f1d5afc1291 100644 --- a/src/NServiceBus.AcceptanceTests/Outbox/When_headers_contain_special_characters.cs +++ b/src/NServiceBus.AcceptanceTests/Outbox/When_headers_contain_special_characters.cs @@ -82,7 +82,9 @@ public Task Handle(PlaceOrder message, IMessageHandlerContext context) { sendOptions.SetHeader(header.Key, header.Value); } +#pragma warning disable NSB0040 // Intentional runtime-type routing return context.Send(sendOrderAcknowledgement, sendOptions); +#pragma warning restore NSB0040 } } diff --git a/src/NServiceBus.AcceptanceTests/Recoverability/When_non_transactional_message_is_moved_to_error_queue.cs b/src/NServiceBus.AcceptanceTests/Recoverability/When_non_transactional_message_is_moved_to_error_queue.cs index e6ca2b2d1a5..0e0dcf895ad 100644 --- a/src/NServiceBus.AcceptanceTests/Recoverability/When_non_transactional_message_is_moved_to_error_queue.cs +++ b/src/NServiceBus.AcceptanceTests/Recoverability/When_non_transactional_message_is_moved_to_error_queue.cs @@ -57,7 +57,9 @@ public Task Handle(InitiatingMessage initiatingMessage, IMessageHandlerContext c { Id = initiatingMessage.Id }; +#pragma warning disable NSB0040 // Intentional runtime-type routing return context.Send(ErrorSpyAddress, message); +#pragma warning restore NSB0040 } return Task.CompletedTask; } diff --git a/src/NServiceBus.AcceptanceTests/Recoverability/When_receiveonly_message_is_moved_to_error_queue.cs b/src/NServiceBus.AcceptanceTests/Recoverability/When_receiveonly_message_is_moved_to_error_queue.cs index 21aed7f1ad7..8d117a0feca 100644 --- a/src/NServiceBus.AcceptanceTests/Recoverability/When_receiveonly_message_is_moved_to_error_queue.cs +++ b/src/NServiceBus.AcceptanceTests/Recoverability/When_receiveonly_message_is_moved_to_error_queue.cs @@ -57,7 +57,9 @@ public Task Handle(InitiatingMessage initiatingMessage, IMessageHandlerContext c { Id = initiatingMessage.Id }; +#pragma warning disable NSB0040 // Intentional runtime-type routing return context.Send(ErrorSpyAddress, message); +#pragma warning restore NSB0040 } return Task.CompletedTask; diff --git a/src/NServiceBus.AcceptanceTests/Routing/When_publishing_a_concrete_event_with_trimming_safe_mapper.cs b/src/NServiceBus.AcceptanceTests/Routing/When_publishing_a_concrete_event_with_trimming_safe_mapper.cs index 01f6b7abf9a..2b32dcb4cc7 100644 --- a/src/NServiceBus.AcceptanceTests/Routing/When_publishing_a_concrete_event_with_trimming_safe_mapper.cs +++ b/src/NServiceBus.AcceptanceTests/Routing/When_publishing_a_concrete_event_with_trimming_safe_mapper.cs @@ -25,7 +25,9 @@ public async Task Should_dispatch_to_interface_handlers_without_generating_a_pro { ContextId = ctx.Id }; +#pragma warning disable NSB0040 // Intentional runtime-type routing return session.Publish(message); +#pragma warning restore NSB0040 })) .WithEndpoint(b => b.When(async (session, ctx) => { diff --git a/src/NServiceBus.AcceptanceTests/Routing/When_publishing_an_event_implementing_two_unrelated_interfaces.cs b/src/NServiceBus.AcceptanceTests/Routing/When_publishing_an_event_implementing_two_unrelated_interfaces.cs index 737477ef410..d27a64a720a 100644 --- a/src/NServiceBus.AcceptanceTests/Routing/When_publishing_an_event_implementing_two_unrelated_interfaces.cs +++ b/src/NServiceBus.AcceptanceTests/Routing/When_publishing_an_event_implementing_two_unrelated_interfaces.cs @@ -22,7 +22,9 @@ public async Task Event_should_be_published_using_instance_type(CancellationToke { ContextId = ctx.Id }; +#pragma warning disable NSB0040 // Intentional runtime-type routing return session.Publish(message); +#pragma warning restore NSB0040 })) .WithEndpoint(b => b.When(async (session, ctx) => { diff --git a/src/NServiceBus.AcceptanceTests/Routing/When_publishing_using_base_type.cs b/src/NServiceBus.AcceptanceTests/Routing/When_publishing_using_base_type.cs index 920647046ff..50d8b8ccf5e 100644 --- a/src/NServiceBus.AcceptanceTests/Routing/When_publishing_using_base_type.cs +++ b/src/NServiceBus.AcceptanceTests/Routing/When_publishing_using_base_type.cs @@ -20,7 +20,9 @@ public async Task Event_should_be_published_using_instance_type(CancellationToke { IMyEvent message = new EventMessage(); +#pragma warning disable NSB0040 // Intentional runtime-type routing return session.Publish(message); +#pragma warning restore NSB0040 })) .WithEndpoint(b => b.When(async (session, ctx) => { diff --git a/src/NServiceBus.AcceptanceTests/Sagas/When_message_has_a_saga_id.cs b/src/NServiceBus.AcceptanceTests/Sagas/When_message_has_a_saga_id.cs index 86040e19997..a26425a0afe 100644 --- a/src/NServiceBus.AcceptanceTests/Sagas/When_message_has_a_saga_id.cs +++ b/src/NServiceBus.AcceptanceTests/Sagas/When_message_has_a_saga_id.cs @@ -20,7 +20,9 @@ public async Task Should_not_start_a_new_saga_if_not_found() options.SetHeader(Headers.SagaId, Guid.NewGuid().ToString()); options.SetHeader(Headers.SagaType, typeof(SagaEndpoint.MessageWithSagaIdSaga).AssemblyQualifiedName); options.RouteToThisEndpoint(); +#pragma warning disable NSB0040 // Intentional runtime-type routing return session.Send(message, options); +#pragma warning restore NSB0040 })) .Run(); diff --git a/src/NServiceBus.AcceptanceTests/Sagas/When_replying_to_originator.cs b/src/NServiceBus.AcceptanceTests/Sagas/When_replying_to_originator.cs index 76ac0a98435..c2e56003c57 100644 --- a/src/NServiceBus.AcceptanceTests/Sagas/When_replying_to_originator.cs +++ b/src/NServiceBus.AcceptanceTests/Sagas/When_replying_to_originator.cs @@ -71,7 +71,7 @@ public Task Handle(RequestToRespondingSaga message, IMessageHandlerContext conte public Task Handle(SendReplyFromNonInitiatingHandler message, IMessageHandlerContext context) => //reply to originator must be used here since the sender of the incoming message is this saga and not the requesting saga - ReplyToOriginator(context, new ResponseFromOtherSaga + ReplyToOriginator(context, new ResponseFromOtherSaga { SomeCorrelationId = Data.CorrIdForRequest }); diff --git a/src/NServiceBus.AcceptanceTests/Sagas/When_replying_to_originator_from_a_timeout.cs b/src/NServiceBus.AcceptanceTests/Sagas/When_replying_to_originator_from_a_timeout.cs index 510a43065db..7b73ab4f599 100644 --- a/src/NServiceBus.AcceptanceTests/Sagas/When_replying_to_originator_from_a_timeout.cs +++ b/src/NServiceBus.AcceptanceTests/Sagas/When_replying_to_originator_from_a_timeout.cs @@ -70,7 +70,7 @@ public class RequestResponseRespondingSaga3 : Saga //reply to originator must be used here since the sender of the incoming message is the TimeoutManager and not the requesting saga - ReplyToOriginator(context, new ResponseFromOtherSaga + ReplyToOriginator(context, new ResponseFromOtherSaga { SomeCorrelationId = Data.CorrIdForRequest }); diff --git a/src/NServiceBus.Core.Analyzer.Fixes/MessagingMigrationFixer.cs b/src/NServiceBus.Core.Analyzer.Fixes/MessagingMigrationFixer.cs new file mode 100644 index 00000000000..e3808dca7de --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Fixes/MessagingMigrationFixer.cs @@ -0,0 +1,235 @@ +#nullable enable + +namespace NServiceBus.Core.Analyzer.Fixes; + +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Simplification; + +[Shared] +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MessagingMigrationFixer))] +public sealed class MessagingMigrationFixer : CodeFixProvider +{ + const string MessageTypeProperty = "MessageType"; + const string EquivalenceKey = "UseStronglyTypedMessageOverload"; + + public override ImmutableArray FixableDiagnosticIds { get; } = + [DiagnosticIds.UseGenericMessageType]; + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root is null) + { + return; + } + + var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + + foreach (var diagnostic in context.Diagnostics) + { + if (!diagnostic.Properties.TryGetValue(MessageTypeProperty, out var messageType) || + string.IsNullOrWhiteSpace(messageType)) + { + continue; + } + + var node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + + if (node is ExpressionSyntax methodReference && CanAddTypeArgument(methodReference)) + { + if (!CanOfferFix(semanticModel, methodReference)) + { + continue; + } + + context.RegisterCodeFix( + CodeAction.Create( + "Use the strongly typed message overload", + cancellationToken => AddTypeArgumentToMethodReference( + context.Document, + root, + methodReference, + messageType!, + cancellationToken), + EquivalenceKey), + diagnostic); + continue; + } + + if (node.FirstAncestorOrSelf() is not { } invocation || + !CanAddTypeArgument(invocation.Expression)) + { + continue; + } + + if (!CanOfferFix(semanticModel, invocation.Expression)) + { + continue; + } + + context.RegisterCodeFix( + CodeAction.Create( + "Use the strongly typed message overload", + cancellationToken => AddTypeArgument( + context.Document, + root, + invocation, + messageType!, + cancellationToken), + EquivalenceKey), + diagnostic); + } + } + + static bool CanAddTypeArgument(ExpressionSyntax expression) => expression switch + { + MemberAccessExpressionSyntax { Name: IdentifierNameSyntax } => true, + MemberBindingExpressionSyntax { Name: IdentifierNameSyntax } => true, + IdentifierNameSyntax => true, + _ => false + }; + + // Default interface members are not callable through a concrete receiver. + static bool CanOfferFix(SemanticModel? semanticModel, ExpressionSyntax expression) + { + if (semanticModel is null || expression is not ( + MemberAccessExpressionSyntax or MemberBindingExpressionSyntax or IdentifierNameSyntax)) + { + return false; + } + + var methodName = expression switch + { + MemberAccessExpressionSyntax { Name: IdentifierNameSyntax name } => name.Identifier.ValueText, + MemberBindingExpressionSyntax { Name: IdentifierNameSyntax name } => name.Identifier.ValueText, + IdentifierNameSyntax identifier => identifier.Identifier.ValueText, + _ => null + }; + if (methodName is null) + { + return false; + } + + var receiverType = GetReceiverType(semanticModel, expression); + if (receiverType is null || receiverType.TypeKind == TypeKind.Error) + { + return false; + } + + if (receiverType.TypeKind == TypeKind.Interface) + { + return true; + } + + var within = semanticModel.GetEnclosingSymbol(expression.SpanStart)?.ContainingType; + for (var type = receiverType; type is not null; type = type.BaseType) + { + foreach (var member in type.GetMembers(methodName).OfType()) + { + if (!member.IsGenericMethod || member.TypeParameters.Length == 0) + { + continue; + } + + // Exclude creator overloads such as Send(Action, ...). + var messageTypeParameter = member.TypeParameters[0]; + if (!member.Parameters.Any(parameter => + SymbolEqualityComparer.Default.Equals(parameter.Type, messageTypeParameter))) + { + continue; + } + + if (within is null + ? member.DeclaredAccessibility == Accessibility.Public + : semanticModel.Compilation.IsSymbolAccessibleWithin(member, within)) + { + return true; + } + } + } + + return false; + } + + static ITypeSymbol? GetReceiverType(SemanticModel semanticModel, ExpressionSyntax expression) + { + var receiverSyntax = expression switch + { + MemberAccessExpressionSyntax memberAccess => memberAccess.Expression, + MemberBindingExpressionSyntax memberBinding when + memberBinding.Parent is ConditionalAccessExpressionSyntax conditional => conditional.Expression, + _ => null + }; + + if (receiverSyntax is not null) + { + return semanticModel.GetTypeInfo(receiverSyntax).Type; + } + + return semanticModel.GetEnclosingSymbol(expression.SpanStart)?.ContainingType; + } + + static Task AddTypeArgument( + Document document, + SyntaxNode root, + InvocationExpressionSyntax invocation, + string messageType, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var updatedInvocation = invocation.WithExpression(AddTypeArgumentToExpression(invocation.Expression, messageType)) + .WithAdditionalAnnotations(Formatter.Annotation); + return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(invocation, updatedInvocation))); + } + + static Task AddTypeArgumentToMethodReference( + Document document, + SyntaxNode root, + ExpressionSyntax methodReference, + string messageType, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var updatedMethodReference = AddTypeArgumentToExpression(methodReference, messageType) + .WithAdditionalAnnotations(Formatter.Annotation); + return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(methodReference, updatedMethodReference))); + } + + static ExpressionSyntax AddTypeArgumentToExpression(ExpressionSyntax expression, string messageType) + { + var typeArgument = SyntaxFactory.ParseTypeName(messageType) + .WithAdditionalAnnotations(Simplifier.Annotation); + var typeArguments = SyntaxFactory.TypeArgumentList( + SyntaxFactory.SingletonSeparatedList(typeArgument)); + + return expression switch + { + MemberAccessExpressionSyntax { Name: IdentifierNameSyntax name } memberAccess => + memberAccess.WithName( + SyntaxFactory.GenericName(name.Identifier, typeArguments) + .WithTriviaFrom(name)), + MemberBindingExpressionSyntax { Name: IdentifierNameSyntax name } memberBinding => + memberBinding.WithName( + SyntaxFactory.GenericName(name.Identifier, typeArguments) + .WithTriviaFrom(name)), + IdentifierNameSyntax name => + SyntaxFactory.GenericName(name.Identifier, typeArguments) + .WithTriviaFrom(name), + _ => expression + }; + } +} diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationAnalyzerTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationAnalyzerTests.cs new file mode 100644 index 00000000000..560cea107f4 --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationAnalyzerTests.cs @@ -0,0 +1,3362 @@ +#nullable enable + +#pragma warning disable NUnit1034 // Base TestFixtures should be abstract + +namespace NServiceBus.Core.Analyzer.Tests; + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using NUnit.Framework; +using Particular.AnalyzerTesting; + +[TestFixture] +public class MessagingMigrationAnalyzerTests : AnalyzerTestFixture +{ + protected override void ConfigureFixtureTests(AnalyzerTest test) + { + base.ConfigureFixtureTests(test); + test.WithProperty("build_property.PublishTrimmed", "true"); + } + + static AnalyzerTest MigrationTest(string source) => + AnalyzerTest.ForAnalyzer() + .WithSource(source); + + static readonly MetadataReference TestingFakesReference = + MetadataReference.CreateFromFile(typeof(NServiceBus.Testing.TestableMessageSession).Assembly.Location); + + static AnalyzerTest MigrationAuditTest(string source) => + MigrationTest(source).WithProperty("build_property.PublishTrimmed", "true"); + + static AnalyzerTest FakeMigrationTest(string source) => + MigrationAuditTest(source).AddReferences(TestingFakesReference); + + static async Task AssertEditorConfigSeverity( + string source, + string diagnosticId, + ReportDiagnostic severity, + bool automaticActivation, + params string[] expectedDiagnosticIds) + { + var syntaxTree = CSharpSyntaxTree.ParseText(source, path: "Test.cs"); + var analyzerDiagnostics = await GetEditorConfigDiagnostics( + [syntaxTree], + syntaxTree, + diagnosticId, + severity, + automaticActivation); + + NUnit.Framework.Assert.That(analyzerDiagnostics.Select(diagnostic => diagnostic.Id), Is.EquivalentTo(expectedDiagnosticIds)); + } + + static async Task> GetEditorConfigDiagnostics( + ImmutableArray syntaxTrees, + SyntaxTree configuredTree, + string diagnosticId, + ReportDiagnostic severity, + bool automaticActivation) + { + var compilation = CSharpCompilation.Create( + "AnalyzerConfigOptionsTest", + syntaxTrees, + SetUpFixture.ProjectReferences, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) + .WithSyntaxTreeOptionsProvider(new TestSyntaxTreeOptionsProvider(configuredTree, diagnosticId, severity))); + return await compilation.WithAnalyzers( + [new MessagingMigrationAnalyzer()], + new CompilationWithAnalyzersOptions( + new AnalyzerOptions( + ImmutableArray.Empty, + new TestAnalyzerConfigOptionsProvider(automaticActivation)), + onAnalyzerException: null, + concurrentAnalysis: true, + logAnalyzerExecutionTime: false, + reportSuppressedDiagnostics: true)).GetAnalyzerDiagnosticsAsync(); + } + + static async Task> GetSeverityConfigDiagnostics( + string source, + string diagnosticId, + ReportDiagnostic? severity = null, + ReportDiagnostic? globalSeverity = null, + ImmutableDictionary? treeOptions = null, + bool automaticActivation = false) + { + var syntaxTree = CSharpSyntaxTree.ParseText(source, path: "Test.cs"); + var optionsProvider = severity is null && globalSeverity is null + ? null + : new TestSyntaxTreeOptionsProvider(syntaxTree, diagnosticId, severity, globalSeverity); + + var compilation = CSharpCompilation.Create( + "AnalyzerConfigOptionsTest", + [syntaxTree], + SetUpFixture.ProjectReferences, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) + .WithSyntaxTreeOptionsProvider(optionsProvider)); + return await compilation.WithAnalyzers( + [new MessagingMigrationAnalyzer()], + new CompilationWithAnalyzersOptions( + new AnalyzerOptions( + ImmutableArray.Empty, + new TestAnalyzerConfigOptionsProvider(automaticActivation, treeOptions)), + onAnalyzerException: null, + concurrentAnalysis: true, + logAnalyzerExecutionTime: false, + reportSuppressedDiagnostics: true)).GetAnalyzerDiagnosticsAsync(); + } + + // dotnet_diagnostic severity is a compiler tree option, not an AnalyzerConfigOptions value. + // SyntaxTreeOptionsProvider is the Roslyn API that retains the .editorconfig file scope. + sealed class TestSyntaxTreeOptionsProvider( + SyntaxTree configuredTree, + string configuredDiagnosticId, + ReportDiagnostic? configuredSeverity = null, + ReportDiagnostic? configuredGlobalSeverity = null) : SyntaxTreeOptionsProvider + { + public override GeneratedKind IsGenerated(SyntaxTree tree, CancellationToken cancellationToken = default) => GeneratedKind.NotGenerated; + +#pragma warning disable PS0003 // A parameter of type CancellationToken on a non-private delegate or method should be optional + public override bool TryGetDiagnosticValue( + SyntaxTree tree, + string diagnosticId, + CancellationToken cancellationToken, + out ReportDiagnostic severity) + { + if (tree == configuredTree && + diagnosticId == configuredDiagnosticId && + configuredSeverity is { } configuredTreeSeverity) + { + severity = configuredTreeSeverity; + return true; + } + + severity = ReportDiagnostic.Default; + return false; + } + + public override bool TryGetGlobalDiagnosticValue( + string diagnosticId, + CancellationToken cancellationToken, + out ReportDiagnostic severity) + { + if (configuredGlobalSeverity is { } globalSeverity && + diagnosticId == configuredDiagnosticId) + { + severity = globalSeverity; + return true; + } + + severity = ReportDiagnostic.Default; + return false; + } +#pragma warning restore PS0003 // A parameter of type CancellationToken on a non-private delegate or method should be optional + } + + sealed class TestAnalyzerConfigOptionsProvider(bool automaticActivation, ImmutableDictionary? treeOptions = null) : AnalyzerConfigOptionsProvider + { + static readonly AnalyzerConfigOptions EmptyOptions = new TestAnalyzerConfigOptions(ImmutableDictionary.Empty); + static readonly AnalyzerConfigOptions AutomaticActivationOptions = new TestAnalyzerConfigOptions( + ImmutableDictionary.Empty.Add("build_property.PublishTrimmed", "true")); + + readonly AnalyzerConfigOptions treeConfigOptions = treeOptions is { Count: > 0 } + ? new TestAnalyzerConfigOptions(treeOptions) + : EmptyOptions; + + public override AnalyzerConfigOptions GlobalOptions => automaticActivation ? AutomaticActivationOptions : EmptyOptions; + + public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => treeConfigOptions; + + public override AnalyzerConfigOptions GetOptions(AdditionalText text) => EmptyOptions; + } + + sealed class TestAnalyzerConfigOptions(ImmutableDictionary values) : AnalyzerConfigOptions + { + public override bool TryGetValue(string key, out string value) => values.TryGetValue(key, out value!); + + public override IEnumerable Keys => values.Keys; + } + + // ===== NSB0039: Safe object creation ===== + + [Test] + public Task NSB0039_DirectObjectCreation_SessionSend() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Send(new MyMessage(), new SendOptions())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_SessionPublish() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Publish(new MyEvent(), new PublishOptions())|]; + } + } + + class MyEvent : IEvent { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_PipelineContextSend() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await [|context.Send(new MyMessage(), new SendOptions())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_PipelineContextPublish() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await [|context.Publish(new MyEvent(), new PublishOptions())|]; + } + } + + class MyEvent : IEvent { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_MessageProcessingContextReply() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageProcessingContext context) + { + await [|context.Reply(new MyMessage(), new ReplyOptions())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_SessionExtensionSend() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Send(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_SessionExtensionSendLocal() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.SendLocal(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_SessionExtensionPublish() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Publish(new MyEvent())|]; + } + } + + class MyEvent : IEvent { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_PipelineContextExtensionSend() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await [|context.Send(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_PipelineContextExtensionSendLocal() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await [|context.SendLocal(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_PipelineContextExtensionPublish() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await [|context.Publish(new MyEvent())|]; + } + } + + class MyEvent : IEvent { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_MessageProcessingContextExtensionReply() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageProcessingContext context) + { + await [|context.Reply(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_SagaReplyToOriginator() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MySaga : Saga + { + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) { } + + async Task Bar(IMessageHandlerContext context) + { + await [|ReplyToOriginator(context, new MyMessage())|]; + } + } + + class MySagaData : ContainSagaData { } + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_SessionSendWithDestination() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Send("destination", new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_PipelineContextSendWithDestination() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await [|context.Send("destination", new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_SealedTypeVariable() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + sealed class MyMessage : IMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + MyMessage msg = new MyMessage(); + await [|session.Send(msg)|]; + } + } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_UnsealedVarObjectCreation() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + var message = new MyMessage(); + await [|session.Send(message)|]; + } + } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_ValueType() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + struct MyMessage : IMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + MyMessage msg = new MyMessage(); + await [|session.Send(msg)|]; + } + } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0040_NullableValueType() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, int? message) + { + await [|session.Send(message)|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0039_MessageCreatorCreateInstance() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, IMessageCreator creator) + { + await [|session.Send(creator.CreateInstance())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MessageCreatorCreateInstanceWithAction() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, IMessageCreator creator) + { + await [|session.Send(creator.CreateInstance(_ => { }))|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_UpdateMessageDirectObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + using System.Threading.Tasks; + + class Foo + { + void Bar(IOutgoingLogicalMessageContext context) + { + [|context.UpdateMessage(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + // ===== NSB0040: Potentially polymorphic ===== + + [Test] + public Task NSB0040_UpdateMessageVarObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class MyMessage : IMessage { } + + class Foo + { + void Bar(IOutgoingLogicalMessageContext context) + { + var message = new MyMessage(); + [|context.UpdateMessage(message)|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_UpdateMessageCreatedByMessageCreator() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IOutgoingLogicalMessageContext context, IMessageCreator creator) + { + [|context.UpdateMessage(creator.CreateInstance())|]; + } + } + + public interface IMyMessage { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_UpdateMessageSealedVariable() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IOutgoingLogicalMessageContext context, MyMessage message) + { + [|context.UpdateMessage(message)|]; + } + } + + public sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_InterfaceVariable() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, IMessage msg) + { + await [|session.Send(msg)|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_BaseClassVariable() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class BaseMessage : IMessage { } + class DerivedMessage : BaseMessage { } + + class Foo + { + async Task Bar(IMessageSession session, BaseMessage msg) + { + await [|session.Send(msg)|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_UnsealedClassVariable() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + + class Foo + { + async Task Bar(IMessageSession session, MyMessage msg) + { + await [|session.Send(msg)|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_ReassignedVarObjectCreation() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + var message = new MyMessage(); + message = new MyMessage(); + await [|session.Send(message)|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_RefUseOfVarObjectCreation() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + var message = new MyMessage(); + Replace(ref message); + await [|session.Send(message)|]; + } + + static void Replace(ref MyMessage message) => message = new MyMessage(); + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_NestedInvocationWithEarlierRefMutation() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + class DerivedMessage : MyMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + var message = new MyMessage(); + await Task.WhenAll(Mutate(ref message), [|session.Send(message)|]); + } + + static Task Mutate(ref MyMessage message) + { + message = new DerivedMessage(); + return Task.CompletedTask; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_ReceiverRefUseOfVarObjectCreation() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + class DerivedMessage : MyMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + var message = new MyMessage(); + await [|Replace(ref message, session).Send(message)|]; + } + + static IMessageSession Replace(ref MyMessage message, IMessageSession session) + { + message = new DerivedMessage(); + return session; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_LocalFunctionReceiverMutatesCapturedVarObjectCreation() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + class DerivedMessage : MyMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + var message = new MyMessage(); + + IMessageSession GetSession() + { + message = new DerivedMessage(); + return session; + } + + await [|GetSession().Send(message)|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_EarlierArgumentRefUseOfVarObjectCreation() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage + { + public string Destination { get; } = "destination"; + } + + class Foo + { + async Task Bar(IMessageSession session) + { + var message = new MyMessage(); + await [|session.Send(message.Destination, message)|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_ConditionalVarInitializer() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + + class Foo + { + async Task Bar(IMessageSession session, bool condition) + { + var message = condition ? new MyMessage() : new MyMessage(); + await [|session.Send(message)|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_InvocationVarInitializer() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + var message = CreateMessage(); + await [|session.Send(message)|]; + } + + MyMessage CreateMessage() => new MyMessage(); + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_MethodReturnType() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + var msg = GetMessage(); + await [|session.Send(msg)|]; + } + + MyMessage GetMessage() => new MyMessage(); + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + // ===== NSB0041: Generic T == object ===== + + [Test] + public Task NSB0041_GenericTIsObject_SessionSend() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + object msg = new object(); + await [|session.Send(msg, new SendOptions())|]; + } + } + """; + return Assert(source, DiagnosticIds.GenericMessageTypeIsObject); + } + + [Test] + public Task NSB0041_GenericTIsObject_SessionPublish() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + object msg = new object(); + await [|session.Publish(msg, new PublishOptions())|]; + } + } + """; + return Assert(source, DiagnosticIds.GenericMessageTypeIsObject); + } + + [Test] + public Task NSB0041_GenericTIsObject_PipelineContextSend() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + object msg = new object(); + await [|context.Send(msg, new SendOptions())|]; + } + } + """; + return Assert(source, DiagnosticIds.GenericMessageTypeIsObject); + } + + [Test] + public Task NSB0041_GenericTIsObject_MessageProcessingContextReply() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageProcessingContext context) + { + object msg = new object(); + await [|context.Reply(msg, new ReplyOptions())|]; + } + } + """; + return Assert(source, DiagnosticIds.GenericMessageTypeIsObject); + } + + [Test] + public Task NSB0041_GenericTIsObject_SessionExtensionSend() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + object msg = new object(); + await [|session.Send(msg)|]; + } + } + """; + return Assert(source, DiagnosticIds.GenericMessageTypeIsObject); + } + + [Test] + public Task NSB0041_GenericTIsObject_SagaReplyToOriginator() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MySaga : Saga + { + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) { } + + async Task Bar(IMessageHandlerContext context) + { + object msg = new object(); + await [|ReplyToOriginator(context, msg)|]; + } + } + + class MySagaData : ContainSagaData { } + """; + return Assert(source, DiagnosticIds.GenericMessageTypeIsObject); + } + + [Test] + public Task NSB0041_GenericTIsObject_UpdateMessage() + { + var source = + """ + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IOutgoingLogicalMessageContext context, object message) + { + [|context.UpdateMessage(message)|]; + } + } + """; + return Assert(source, DiagnosticIds.GenericMessageTypeIsObject); + } + + // ===== Method groups and delegates ===== + + [Test] + public Task NSB0039_MethodGroup_SealedMessage_SessionSend() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func send = [|session.Send|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MethodGroup_SealedMessage_SessionPublish() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func publish = [|session.Publish|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MethodGroup_SealedMessage_SessionExtensionSend() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func send = [|session.Send|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MethodGroup_SealedMessage_SessionSendLocal() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func sendLocal = [|session.SendLocal|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MethodGroup_SealedMessage_PipelineContextSend() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading.Tasks; + + class Foo + { + void Bar(IPipelineContext context) + { + Func send = [|context.Send|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MethodGroup_SealedMessage_PipelineContextPublish() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading.Tasks; + + class Foo + { + void Bar(IPipelineContext context) + { + Func publish = [|context.Publish|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MethodGroup_SealedMessage_MessageProcessingContextReply() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageProcessingContext context) + { + Func reply = [|context.Reply|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MethodGroup_SealedMessage_SagaReplyToOriginator() + { + var source = + """ + using NServiceBus; + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + + class MySaga : Saga + { + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) { } + + void Bar(IMessageHandlerContext context) + { + Func, Task> reply = [|ReplyToOriginator|]; + } + } + + class MySagaData : ContainSagaData { } + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MethodGroup_CustomDelegate_PositionalMessageParameter() + { + var source = + """ + using NServiceBus; + using System.Threading; + using System.Threading.Tasks; + + delegate Task MySendDelegate(MyMessage msg, SendOptions options, CancellationToken cancellationToken); + + class Foo + { + void Bar(IMessageSession session) + { + MySendDelegate send = [|session.Send|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MethodGroup_SealedMessage_AsArgument() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + RegisterHandler([|session.Send|]); + } + + void RegisterHandler(Func handler) { } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0040_MethodGroup_InterfaceMessage_SessionSend() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func send = [|session.Send|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_MethodGroup_UnsealedMessage_SessionSend() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func send = [|session.Send|]; + } + } + + class UnsealedMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_MethodGroup_ObjectMessage_SessionSend() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func send = [|session.Send|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_MethodGroup_ObjectMessage_SessionSendLocal() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func sendLocal = [|session.SendLocal|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0041_MethodGroup_ExplicitGenericObject_SessionSend() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func send = [|session.Send|]; + } + } + """; + return Assert(source, DiagnosticIds.GenericMessageTypeIsObject); + } + + [Test] + public Task NSB0041_MethodGroup_IsReportedWithoutMigrationAudit() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func send = [|session.Send|]; + } + } + """; + return MigrationTest(source).AssertDiagnostics(DiagnosticIds.GenericMessageTypeIsObject); + } + + [Test] + public Task NoDiagnostic_MethodGroup_ExplicitGenericNonObject() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func send = session.Send; + } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_MethodGroup_UnrelatedMethod() + { + var source = + """ + using System; + using System.Threading.Tasks; + + class Helper + { + public Task DoSomething(object message) => Task.CompletedTask; + } + + class Foo + { + void Bar(Helper helper) + { + Func action = helper.DoSomething; + } + } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_MethodGroup_NonMessagingMember() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func subscribe = session.Subscribe; + } + } + """; + return Assert(source); + } + + // ===== Concrete implementations and testing fakes ===== + + [Test] + public Task NSB0039_TestableMessageSession_DirectObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.Testing; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(TestableMessageSession session) + { + await [|session.Send(new MyMessage(), new SendOptions())|]; + } + } + + class MyMessage : IMessage { } + """; + return FakeMigrationTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0040_TestableMessageSession_InterfaceMessage() + { + var source = + """ + using NServiceBus; + using NServiceBus.Testing; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(TestableMessageSession session, IMessage message) + { + await [|session.Send(message, new SendOptions())|]; + } + } + """; + return FakeMigrationTest(source).AssertDiagnostics(DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0041_TestableMessageSession_ExplicitGenericObject() + { + var source = + """ + using NServiceBus; + using NServiceBus.Testing; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(TestableMessageSession session, object message) + { + await [|session.Send(message, new SendOptions())|]; + } + } + """; + return FakeMigrationTest(source).AssertDiagnostics(DiagnosticIds.GenericMessageTypeIsObject); + } + + [Test] + public Task NSB0039_TestableMessageSession_MethodGroup() + { + var source = + """ + using NServiceBus; + using NServiceBus.Testing; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(TestableMessageSession session) + { + Func send = [|session.Send|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return FakeMigrationTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_TestablePipelineContext_Send() + { + var source = + """ + using NServiceBus; + using NServiceBus.Testing; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(TestablePipelineContext context) + { + await [|context.Send(new MyMessage(), new SendOptions())|]; + } + } + + class MyMessage : IMessage { } + """; + return FakeMigrationTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_TestablePipelineContext_Publish() + { + var source = + """ + using NServiceBus; + using NServiceBus.Testing; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(TestablePipelineContext context) + { + await [|context.Publish(new MyEvent(), new PublishOptions())|]; + } + } + + class MyEvent : IEvent { } + """; + return FakeMigrationTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_TestableMessageProcessingContext_Reply() + { + var source = + """ + using NServiceBus; + using NServiceBus.Testing; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(TestableMessageProcessingContext context) + { + await [|context.Reply(new MyMessage(), new ReplyOptions())|]; + } + } + + class MyMessage : IMessage { } + """; + return FakeMigrationTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_TestableOutgoingLogicalMessageContext_UpdateMessage() + { + var source = + """ + using NServiceBus; + using NServiceBus.Testing; + + class Foo + { + void Bar(TestableOutgoingLogicalMessageContext context) + { + [|context.UpdateMessage(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return FakeMigrationTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_CustomMessageSession_DirectObjectCreation() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object message, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(CustomMessageSession session) + { + await [|session.Send(new MyMessage(), new SendOptions())|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0040_CustomMessageSession_InterfaceMessage() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object message, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(CustomMessageSession session, IMessage message) + { + await [|session.Send(message, new SendOptions())|]; + } + } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0039_CustomMessageSession_MethodGroup() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object message, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + void Bar(CustomMessageSession session) + { + Func send = [|session.Send|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_CustomMessageSession_ImplementationOnBaseClass() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object message, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class DerivedMessageSession : CustomMessageSession { } + + class Foo + { + async Task Bar(DerivedMessageSession session) + { + await [|session.Send(new MyMessage(), new SendOptions())|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_ExplicitMessageSession_InvokedThroughInterface() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class ExplicitMessageSession : IMessageSession + { + Task IMessageSession.Send(object message, SendOptions options, CancellationToken cancellationToken) => Task.CompletedTask; + Task IMessageSession.Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken) => Task.CompletedTask; + Task IMessageSession.Publish(object message, PublishOptions options, CancellationToken cancellationToken) => Task.CompletedTask; + Task IMessageSession.Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken) => Task.CompletedTask; + Task IMessageSession.Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken) => Task.CompletedTask; + Task IMessageSession.Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Send(new MyMessage(), new SendOptions())|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NoDiagnostic_NonContractSession() + { + var source = + """ + using NServiceBus; + using System.Threading; + using System.Threading.Tasks; + + class NotAMessageSession + { + public Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(NotAMessageSession session) + { + await session.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + return MigrationAuditTest(source).AssertDiagnostics(); + } + + [Test] + public Task NoDiagnostic_ImplementingSession_UnrelatedOverload() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object message, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(object message) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(CustomMessageSession session) + { + await session.Send(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + return MigrationAuditTest(source).AssertDiagnostics(); + } + + // ===== Generic forwarding ===== + + [Test] + public Task NSB0040_GenericForwarding_Unconstrained() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Forward(T message, IMessageSession session) + { + await [|session.Send(message, new SendOptions())|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_GenericForwarding_ClassConstraint() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Forward(T message, IMessageSession session) where T : class + { + await [|session.Send(message, new SendOptions())|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_GenericForwarding_InterfaceConstraint() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Forward(T message, IMessageSession session) where T : IMyMessage + { + await [|session.Send(message, new SendOptions())|]; + } + } + + interface IMyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_GenericForwarding_MethodGroup() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Forward(T message, IMessageSession session) where T : class + { + Func send = [|session.Send|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + // ===== Inferred versus explicit object ===== + + [Test] + public Task NoDiagnostic_InferredObjectArgument_10xObjectOnlyOverload() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, object message) + { + await session.Send(message, new SendOptions()); + } + } + """; + return Assert(source); + } + + // ===== Expression trees ===== + + [Test] + public Task NSB0039_ExpressionTree_SealedMessage() + { + var source = + """ + using NServiceBus; + using System; + using System.Linq.Expressions; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Expression> expression = message => [|session.Send(message)|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0040_ExpressionTree_UnsealedMessage() + { + var source = + """ + using NServiceBus; + using System; + using System.Linq.Expressions; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Expression> expression = message => [|session.Send(message)|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + // ===== Activation ===== + + [Test] + public Task NSB0041_IsReportedWithoutMigrationAudit() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + object message = new MyMessage(); + await [|session.Send(message)|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source).AssertDiagnostics(DiagnosticIds.GenericMessageTypeIsObject); + } + + [Test] + public Task MigrationDiagnostics_AreInactiveByDefault() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, int? message) + { + await session.Send(new MyMessage()); + await session.Send(message); + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source).AssertDiagnostics(); + } + + [Test] + public Task MigrationDiagnostics_AreEnabledForPublishTrimmed() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Send(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source).WithProperty("build_property.PublishTrimmed", "true") + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task MigrationDiagnostics_AreEnabledForPublishTrimmed_EnablesNSB0040() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, int? message) + { + await [|session.Send(message)|]; + } + } + """; + return MigrationTest(source).WithProperty("build_property.PublishTrimmed", "true") + .AssertDiagnostics(DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task MigrationDiagnostics_AreEnabledForPublishAot() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Send(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source).WithProperty("build_property.PublishAot", "true") + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task MigrationDiagnostics_AreEnabledForIsAotCompatible() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Send(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source).WithProperty("build_property.IsAotCompatible", "true") + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task MigrationDiagnostics_AreEnabledForIsTrimmable() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Send(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source).WithProperty("build_property.IsTrimmable", "true") + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task MigrationDiagnostics_AreEnabledForEnableTrimAnalyzer() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Send(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source).WithProperty("build_property.EnableTrimAnalyzer", "true") + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task MigrationDiagnostics_AreEnabledForNSB0039EditorConfigSeverityOnly() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, int? message) + { + await session.Send(new MyMessage()); + await session.Send(message); + } + } + + class MyMessage : IMessage { } + """; + return AssertEditorConfigSeverity( + source, + DiagnosticIds.UseGenericMessageType, + ReportDiagnostic.Info, + automaticActivation: false, + DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task MigrationDiagnostics_AreEnabledForNSB0040EditorConfigSeverityOnly() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, int? message) + { + await session.Send(new MyMessage()); + await session.Send(message); + } + } + + class MyMessage : IMessage { } + """; + return AssertEditorConfigSeverity( + source, + DiagnosticIds.RuntimeTypeMayDiffer, + ReportDiagnostic.Warn, + automaticActivation: false, + DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task MigrationDiagnostics_AutomaticActivation_RespectsNSB0039NoneSeverity() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, int? message) + { + await session.Send(new MyMessage()); + await session.Send(message); + } + } + + class MyMessage : IMessage { } + """; + return AssertEditorConfigSeverity( + source, + DiagnosticIds.UseGenericMessageType, + ReportDiagnostic.Suppress, + automaticActivation: true, + DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task MigrationDiagnostics_AutomaticActivation_RespectsNSB0040NoneSeverity() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, int? message) + { + await session.Send(new MyMessage()); + await session.Send(message); + } + } + + class MyMessage : IMessage { } + """; + return AssertEditorConfigSeverity( + source, + DiagnosticIds.RuntimeTypeMayDiffer, + ReportDiagnostic.Suppress, + automaticActivation: true, + DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task MigrationDiagnostics_AutomaticActivation_DefaultSeverityFallsBackToAutomatic() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, int? message) + { + await session.Send(new MyMessage()); + await session.Send(message); + } + } + + class MyMessage : IMessage { } + """; + return AssertEditorConfigSeverity( + source, + DiagnosticIds.UseGenericMessageType, + ReportDiagnostic.Default, + automaticActivation: true, + DiagnosticIds.UseGenericMessageType, + DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public async Task MigrationDiagnostics_SeverityIsScopedToConfiguredSyntaxTree() + { + const string configuredSource = + """ + using NServiceBus; + using System.Threading.Tasks; + + class ConfiguredFoo + { + async Task Bar(IMessageSession session) + { + await session.Send(new ConfiguredMessage()); + } + } + + class ConfiguredMessage : IMessage { } + """; + const string unconfiguredSource = + """ + using NServiceBus; + using System.Threading.Tasks; + + class UnconfiguredFoo + { + async Task Bar(IMessageSession session) + { + await session.Send(new UnconfiguredMessage()); + } + } + + class UnconfiguredMessage : IMessage { } + """; + var configuredTree = CSharpSyntaxTree.ParseText(configuredSource, path: "Configured.cs"); + var unconfiguredTree = CSharpSyntaxTree.ParseText(unconfiguredSource, path: "Unconfigured.cs"); + + var diagnostics = await GetEditorConfigDiagnostics( + [configuredTree, unconfiguredTree], + configuredTree, + DiagnosticIds.UseGenericMessageType, + ReportDiagnostic.Suppress, + automaticActivation: true); + + NUnit.Framework.Assert.That(diagnostics.Select(diagnostic => diagnostic.Id), Is.EquivalentTo([DiagnosticIds.UseGenericMessageType])); + NUnit.Framework.Assert.That(diagnostics[0].Location.SourceTree?.FilePath, Is.EqualTo("Unconfigured.cs")); + } + + // The global channel (TryGetGlobalDiagnosticValue) was invisible to the old implementation, so + // global severities that enable the diagnostics were ignored by the analyzer gate. + + [Test] + public async Task MigrationDiagnostics_AutomaticActivation_RespectsGlobalNoneSeverity() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + var diagnostics = await GetSeverityConfigDiagnostics( + source, + DiagnosticIds.UseGenericMessageType, + globalSeverity: ReportDiagnostic.Suppress, + automaticActivation: true); + + NUnit.Framework.Assert.That(diagnostics.Select(diagnostic => diagnostic.Id), Is.Empty); + } + + [Test] + public async Task MigrationDiagnostics_GlobalSeverity_EnablesWithoutAutomaticActivation() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + var diagnostics = await GetSeverityConfigDiagnostics( + source, + DiagnosticIds.UseGenericMessageType, + globalSeverity: ReportDiagnostic.Warn, + automaticActivation: false); + + NUnit.Framework.Assert.That(diagnostics.Select(diagnostic => diagnostic.Id), Is.EquivalentTo([DiagnosticIds.UseGenericMessageType])); + } + + [Test] + public async Task MigrationDiagnostics_AutomaticActivation_RespectsBulkCategoryNoneSeverity() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + var diagnostics = await GetSeverityConfigDiagnostics( + source, + DiagnosticIds.UseGenericMessageType, + treeOptions: ImmutableDictionary.Empty.Add( + "dotnet_analyzer_diagnostic.category-NServiceBus.Code.severity", "none"), + automaticActivation: true); + + NUnit.Framework.Assert.That(diagnostics.Select(diagnostic => diagnostic.Id), Is.Empty); + } + + [Test] + public async Task MigrationDiagnostics_BulkCategorySeverity_EnablesWithoutAutomaticActivation() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + var diagnostics = await GetSeverityConfigDiagnostics( + source, + DiagnosticIds.UseGenericMessageType, + treeOptions: ImmutableDictionary.Empty.Add( + "dotnet_analyzer_diagnostic.category-NServiceBus.Code.severity", "warning"), + automaticActivation: false); + + NUnit.Framework.Assert.That(diagnostics.Select(diagnostic => diagnostic.Id), Is.EquivalentTo([DiagnosticIds.UseGenericMessageType])); + } + + [Test] + public async Task MigrationDiagnostics_ExplicitPerRuleDefaultSeverity_BlocksBulkConfiguration() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + var diagnostics = await GetSeverityConfigDiagnostics( + source, + DiagnosticIds.UseGenericMessageType, + severity: ReportDiagnostic.Default, + treeOptions: ImmutableDictionary.Empty.Add( + "dotnet_analyzer_diagnostic.category-NServiceBus.Code.severity", "warning"), + automaticActivation: false); + + NUnit.Framework.Assert.That(diagnostics.Select(diagnostic => diagnostic.Id), Is.Empty); + } + + [Test] + public async Task MigrationDiagnostics_ExplicitGlobalDefaultSeverity_BlocksBulkConfiguration() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + var diagnostics = await GetSeverityConfigDiagnostics( + source, + DiagnosticIds.UseGenericMessageType, + globalSeverity: ReportDiagnostic.Default, + treeOptions: ImmutableDictionary.Empty.Add( + "dotnet_analyzer_diagnostic.category-NServiceBus.Code.severity", "warning"), + automaticActivation: false); + + NUnit.Framework.Assert.That(diagnostics.Select(diagnostic => diagnostic.Id), Is.Empty); + } + + [Test] + public async Task MigrationDiagnostics_ExplicitPerRuleDefaultSeverity_StillHonorsAutomaticActivation() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + var diagnostics = await GetSeverityConfigDiagnostics( + source, + DiagnosticIds.UseGenericMessageType, + severity: ReportDiagnostic.Default, + automaticActivation: true); + + NUnit.Framework.Assert.That(diagnostics.Select(diagnostic => diagnostic.Id), Is.EquivalentTo([DiagnosticIds.UseGenericMessageType])); + } + + // ===== Negative tests ===== + + [Test] + public Task NoDiagnostic_ObjectCreation_NewObject() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new object(), new SendOptions()); + } + } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ObjectCreation_TargetTypedNewObject() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new(), new SendOptions()); + } + } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ObjectVariable() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, object message) + { + await session.Send(message); + } + } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_AnonymousType() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + var message = new { Value = 1 }; + await session.Send(message); + } + } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ExplicitGenericNonObject() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ActionTOverload() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(_ => { }, new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_Subscribe() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Subscribe(typeof(MyEvent), new SubscribeOptions()); + } + } + + class MyEvent : IEvent { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_Unsubscribe() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Unsubscribe(typeof(MyEvent), new UnsubscribeOptions()); + } + } + + class MyEvent : IEvent { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_UnrelatedMethod() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await SomeOtherMethod(new MyMessage()); + } + + Task SomeOtherMethod(object msg) => Task.CompletedTask; + } + + class MyMessage : IMessage { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ForwardCurrentMessageTo() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageProcessingContext context) + { + await context.ForwardCurrentMessageTo("destination"); + } + } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_RequestTimeout() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + using System; + + class MySaga : Saga + { + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) { } + + async Task Bar(IMessageHandlerContext context) + { + await RequestTimeout(context, TimeSpan.FromSeconds(10)); + } + } + + class MySagaData : ContainSagaData { } + class MyTimeout { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ExplicitType_SessionSend() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + using System; + + class Foo + { + async Task Bar(IMessageSession session) + { + object message = new MyMessage(); + await session.Send(message, typeof(IMyInterface), new SendOptions()); + } + } + + interface IMyInterface : IMessage { } + class MyMessage : IMyInterface { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ExplicitType_SessionPublish() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + using System; + + class Foo + { + async Task Bar(IMessageSession session) + { + object message = new MyEvent(); + await session.Publish(message, typeof(IMyInterface), new PublishOptions()); + } + } + + interface IMyInterface : IEvent { } + class MyEvent : IMyInterface { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ExplicitType_SessionSendLocal() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + using System; + + class Foo + { + async Task Bar(IMessageSession session) + { + object message = new MyMessage(); + await session.SendLocal(message, typeof(IMyInterface)); + } + } + + interface IMyInterface : IMessage { } + class MyMessage : IMyInterface { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ExplicitType_SessionSendDestination() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + using System; + + class Foo + { + async Task Bar(IMessageSession session) + { + object message = new MyMessage(); + await session.Send("destination", message, typeof(IMyInterface)); + } + } + + interface IMyInterface : IMessage { } + class MyMessage : IMyInterface { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ExplicitType_PipelineContextSend() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + using System; + + class Foo + { + async Task Bar(IPipelineContext context) + { + object message = new MyMessage(); + await context.Send(message, typeof(IMyInterface), new SendOptions()); + } + } + + interface IMyInterface : IMessage { } + class MyMessage : IMyInterface { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ExplicitType_PipelineContextPublish() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + using System; + + class Foo + { + async Task Bar(IPipelineContext context) + { + object message = new MyEvent(); + await context.Publish(message, typeof(IMyInterface), new PublishOptions()); + } + } + + interface IMyInterface : IEvent { } + class MyEvent : IMyInterface { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ExplicitType_MessageProcessingContextReply() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + using System; + + class Foo + { + async Task Bar(IMessageProcessingContext context) + { + object message = new MyReply(); + await context.Reply(message, typeof(IMyInterface), new ReplyOptions()); + } + } + + interface IMyInterface : IMessage { } + class MyReply : IMyInterface { } + """; + return Assert(source); + } + + // ===== UpdateMessage method groups ===== + + [Test] + public Task NSB0040_UpdateMessageMethodGroup_SealedMessage() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + using System; + + class Foo + { + void Bar(IOutgoingLogicalMessageContext context) + { + Action update = [|context.UpdateMessage|]; + } + } + + sealed class SealedMessage : IMessage { } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_UpdateMessageMethodGroup_ObjectMessage() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + using System; + + class Foo + { + void Bar(IOutgoingLogicalMessageContext context) + { + Action update = [|context.UpdateMessage|]; + } + } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_UpdateMessageMethodGroup_TestableFake() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + using NServiceBus.Testing; + using System; + + class Foo + { + void Bar(TestableOutgoingLogicalMessageContext context) + { + Action update = [|context.UpdateMessage|]; + } + } + + sealed class SealedMessage : IMessage { } + """; + return FakeMigrationTest(source).AssertDiagnostics(DiagnosticIds.RuntimeTypeMayDiffer); + } + + // ===== Renamed implementation parameters ===== + + [Test] + public Task NSB0039_CustomMessageSession_RenamedParameter_DirectObjectCreation() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object payload, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object payload, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(CustomMessageSession session) + { + await [|session.Send(new MyMessage(), new SendOptions())|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0040_CustomMessageSession_RenamedParameter_InterfaceArgument() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object payload, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object payload, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(CustomMessageSession session, IMessage message) + { + await [|session.Send(message, new SendOptions())|]; + } + } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0039_CustomMessageSession_RenamedParameter_MethodGroup() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object payload, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object payload, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + void Bar(CustomMessageSession session) + { + Func send = [|session.Send|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0040_CustomMessageSession_RenamedParameter_MethodGroupInterface() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object payload, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object payload, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + void Bar(CustomMessageSession session) + { + Func send = [|session.Send|]; + } + } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0041_CustomMessageSession_RenamedParameter_ExplicitGenericObject() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object payload, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(T payload, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object payload, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(T payload, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(CustomMessageSession session, object message) + { + await [|session.Send(message, new SendOptions())|]; + } + } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.GenericMessageTypeIsObject); + } + + // ===== Interface-map gate ===== + + [Test] + public Task NoDiagnostic_InterfaceRichType_UnrelatedSendMethod() + { + var source = + """ + using NServiceBus; + using System.Threading; + using System.Threading.Tasks; + + interface IMarkerA { void A(); } + interface IMarkerB { void B(); } + interface IMarkerC : IMarkerA, IMarkerB { void C(); } + + class InterfaceRichType : IMarkerC + { + public void A() { } + public void B() { } + public void C() { } + public Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(InterfaceRichType session) + { + await session.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + return MigrationAuditTest(source).AssertDiagnostics(); + } +} + +#pragma warning restore NUnit1034 diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationFixerTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationFixerTests.cs new file mode 100644 index 00000000000..27e1fb13785 --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationFixerTests.cs @@ -0,0 +1,977 @@ +#pragma warning disable NUnit1034 // Base TestFixtures should be abstract + +namespace NServiceBus.Core.Analyzer.Tests; + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using NServiceBus.Core.Analyzer.Fixes; +using NUnit.Framework; +using Particular.AnalyzerTesting; + +[TestFixture] +public class MessagingMigrationFixerTests : CodeFixTestFixture +{ + static readonly MetadataReference TestingFakesReference = + MetadataReference.CreateFromFile(typeof(NServiceBus.Testing.TestableMessageSession).Assembly.Location); + + protected override void ConfigureFixtureTests(CodeFixTest test) + { + base.ConfigureFixtureTests(test); + test.WithProperty("build_property.PublishTrimmed", "true"); + test.AddReferences(TestingFakesReference); + } + + [Test] + public Task SessionPublish() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Publish(new MyEvent(), new PublishOptions()); + } + } + + class MyEvent : IEvent { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Publish(new MyEvent(), new PublishOptions()); + } + } + + class MyEvent : IEvent { } + """; + + return Assert(original, expected); + } + + [Test] + public Task SessionSend() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task UnsealedVarObjectCreation() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + var message = new MyMessage(); + await session.Send(message); + } + } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + var message = new MyMessage(); + await session.Send(message); + } + } + """; + + return Assert(original, expected); + } + + [Test] + public Task PipelineContextSend() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await context.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await context.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task PipelineContextSendLocal() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await context.SendLocal(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await context.SendLocal(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task MessageProcessingContextReply() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageProcessingContext context) + { + await context.Reply(new MyMessage(), new ReplyOptions()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageProcessingContext context) + { + await context.Reply(new MyMessage(), new ReplyOptions()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task SessionExtensionSend() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task SessionExtensionSendLocal() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.SendLocal(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.SendLocal(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task SessionExtensionPublish() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Publish(new MyEvent()); + } + } + + class MyEvent : IEvent { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Publish(new MyEvent()); + } + } + + class MyEvent : IEvent { } + """; + + return Assert(original, expected); + } + + [Test] + public Task PipelineContextExtensionSend() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await context.Send(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await context.Send(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task PipelineContextExtensionPublish() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await context.Publish(new MyEvent()); + } + } + + class MyEvent : IEvent { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await context.Publish(new MyEvent()); + } + } + + class MyEvent : IEvent { } + """; + + return Assert(original, expected); + } + + [Test] + public Task MessageProcessingContextExtensionReply() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageProcessingContext context) + { + await context.Reply(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageProcessingContext context) + { + await context.Reply(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task NoFixForNewObject() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new object(), new SendOptions()); + } + } + """; + + return Assert(original, original); + } + + [Test] + public Task NoFixForTargetTypedNewObject() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new(), new SendOptions()); + } + } + """; + + return Assert(original, original); + } + + [Test] + public Task SagaReplyToOriginator() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MySaga : Saga + { + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) { } + + async Task Bar(IMessageHandlerContext context) + { + await ReplyToOriginator(context, new MyMessage()); + } + } + + class MySagaData : ContainSagaData { } + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MySaga : Saga + { + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) { } + + async Task Bar(IMessageHandlerContext context) + { + await ReplyToOriginator(context, new MyMessage()); + } + } + + class MySagaData : ContainSagaData { } + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task SessionSendWithDestination() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send("destination", new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send("destination", new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task UpdateMessage() + { + var original = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IOutgoingLogicalMessageContext context) + { + context.UpdateMessage(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IOutgoingLogicalMessageContext context) + { + context.UpdateMessage(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task MethodGroup_SessionSend() + { + var original = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func send = session.Send; + } + } + + sealed class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func send = session.Send; + } + } + + sealed class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task MethodGroup_SessionSendLocal() + { + var original = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func sendLocal = session.SendLocal; + } + } + + sealed class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func sendLocal = session.SendLocal; + } + } + + sealed class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task MethodGroup_AsArgument() + { + var original = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + RegisterHandler(session.Send); + } + + void RegisterHandler(Func handler) { } + } + + sealed class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + RegisterHandler(session.Send); + } + + void RegisterHandler(Func handler) { } + } + + sealed class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task NoFixForExplicitGenericMethodGroup() + { + var original = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func send = session.Send; + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, original); + } + + [Test] + public Task NoFixForDIMReliantConcreteImplementation() + { + var original = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object message, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(CustomMessageSession session) + { + await session.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, original); + } + + [Test] + public Task NoFixForDIMReliantConcreteImplementation_MethodGroup() + { + var original = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object message, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + void Bar(CustomMessageSession session) + { + Func send = session.Send; + } + } + + sealed class MyMessage : IMessage { } + """; + + return Assert(original, original); + } + + [Test] + public Task ConcreteImplementationWithGenericOverload() + { + var original = + """ + using NServiceBus; + using System; + using System.Runtime.CompilerServices; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + [OverloadResolutionPriority(-1)] + public Task Send(T message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object message, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + [OverloadResolutionPriority(-1)] + public Task Publish(T message, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(CustomMessageSession session) + { + await session.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System; + using System.Runtime.CompilerServices; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + [OverloadResolutionPriority(-1)] + public Task Send(T message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object message, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + [OverloadResolutionPriority(-1)] + public Task Publish(T message, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(CustomMessageSession session) + { + await session.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task TestableMessageSession() + { + var original = + """ + using NServiceBus; + using NServiceBus.Testing; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(TestableMessageSession session) + { + await session.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using NServiceBus.Testing; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(TestableMessageSession session) + { + await session.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } +} + +#pragma warning restore NUnit1034 diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationSyntaxTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationSyntaxTests.cs new file mode 100644 index 00000000000..87addb7bba5 --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationSyntaxTests.cs @@ -0,0 +1,40 @@ +#nullable enable + +namespace NServiceBus.Core.Analyzer.Tests; + +using System.Linq; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using NUnit.Framework; + +[TestFixture] +public class MessagingMigrationSyntaxTests +{ + // V11 needs syntax to distinguish explicit from inferred object. + static bool HasExplicitTypeArgumentList(InvocationExpressionSyntax invocation) => + invocation.Expression switch + { + GenericNameSyntax genericName => genericName.TypeArgumentList.Arguments.Count > 0, + MemberAccessExpressionSyntax { Name: GenericNameSyntax name } => name.TypeArgumentList.Arguments.Count > 0, + MemberBindingExpressionSyntax { Name: GenericNameSyntax name } => name.TypeArgumentList.Arguments.Count > 0, + _ => false + }; + + [TestCase("session.Send(message)", true)] + [TestCase("session.Send(message)", false)] + [TestCase("Send(message)", true)] + [TestCase("Send(message)", false)] + [TestCase("session.Send(message)", true)] + [TestCase("session?.Send(message)", true)] + [TestCase("session?.Send(message)", false)] + public void HasExplicitTypeArgumentList(string expression, bool expected) + { + var source = $"class C {{ void M() {{ {expression}; }} }}"; + var tree = CSharpSyntaxTree.ParseText(source); + var invocation = tree.GetRoot() + .DescendantNodes() + .OfType() + .Single(); + Assert.That(HasExplicitTypeArgumentList(invocation), Is.EqualTo(expected)); + } +} diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj index 35c0111a6f7..42d1b8009fa 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj @@ -8,6 +8,7 @@ + diff --git a/src/NServiceBus.Core.Analyzer/DiagnosticIds.cs b/src/NServiceBus.Core.Analyzer/DiagnosticIds.cs index d006581ff85..7c4550c2915 100644 --- a/src/NServiceBus.Core.Analyzer/DiagnosticIds.cs +++ b/src/NServiceBus.Core.Analyzer/DiagnosticIds.cs @@ -51,4 +51,9 @@ public static class DiagnosticIds public const string ConventionBasedHandlerNoAccessibleConstructor = "NSB0036"; public const string ConventionBasedHandlerAmbiguousConstructor = "NSB0037"; public const string HandlerClassCannotBeStatic = "NSB0038"; + + // Messaging migration diagnostics + public const string UseGenericMessageType = "NSB0039"; + public const string RuntimeTypeMayDiffer = "NSB0040"; + public const string GenericMessageTypeIsObject = "NSB0041"; } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer/MessagingMigrationAnalyzer.cs b/src/NServiceBus.Core.Analyzer/MessagingMigrationAnalyzer.cs new file mode 100644 index 00000000000..6d7a0a82886 --- /dev/null +++ b/src/NServiceBus.Core.Analyzer/MessagingMigrationAnalyzer.cs @@ -0,0 +1,784 @@ +#nullable enable + +namespace NServiceBus.Core.Analyzer; + +using System; +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class MessagingMigrationAnalyzer : DiagnosticAnalyzer +{ + const string HelpLink = "https://docs.particular.net/nservicebus/messaging/messages-events-commands"; + const string MessageTypeProperty = "MessageType"; + const string PublishTrimmedProperty = "build_property.PublishTrimmed"; + const string PublishAotProperty = "build_property.PublishAot"; + const string IsAotCompatibleProperty = "build_property.IsAotCompatible"; + const string IsTrimmableProperty = "build_property.IsTrimmable"; + const string EnableTrimAnalyzerProperty = "build_property.EnableTrimAnalyzer"; + + static readonly DiagnosticDescriptor UseGenericTypeRule = new( + DiagnosticIds.UseGenericMessageType, + "Use the strongly typed message overload", + "Use the strongly typed overload with message type '{0}' to make this operation trimming-safe", + "NServiceBus.Code", + DiagnosticSeverity.Info, + isEnabledByDefault: true, + helpLinkUri: HelpLink); + + static readonly DiagnosticDescriptor RuntimeTypeMayDifferRule = new( + DiagnosticIds.RuntimeTypeMayDiffer, + "Message routing uses the runtime type", + "This operation routes using the runtime message type; the strongly typed overload would route using '{0}'", + "NServiceBus.Code", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + helpLinkUri: HelpLink); + + static readonly DiagnosticDescriptor GenericTypeIsObjectRule = new( + DiagnosticIds.GenericMessageTypeIsObject, + "The message type must not be System.Object", + "The strongly typed overload would route this message as System.Object; specify the actual message type", + "NServiceBus.Code", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + helpLinkUri: HelpLink); + + public override ImmutableArray SupportedDiagnostics { get; } = + [UseGenericTypeRule, RuntimeTypeMayDifferRule, GenericTypeIsObjectRule]; + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.RegisterCompilationStartAction(static startContext => + { + if (!KnownTypes.TryCreate(startContext.Compilation, out var knownTypes)) + { + return; + } + + var severityConfiguration = new MigrationDiagnosticConfiguration( + AreMigrationDiagnosticsAutomaticallyEnabled( + startContext.Options.AnalyzerConfigOptionsProvider.GlobalOptions), + startContext.Compilation); + startContext.RegisterOperationAction( + operationContext => AnalyzeInvocation(operationContext, knownTypes, severityConfiguration), + OperationKind.Invocation); + startContext.RegisterOperationAction( + operationContext => AnalyzeDelegateCreation(operationContext, knownTypes, severityConfiguration), + OperationKind.DelegateCreation); + }); + } + + static void AnalyzeDelegateCreation( + OperationAnalysisContext context, + KnownTypes knownTypes, + MigrationDiagnosticConfiguration severityConfiguration) + { + var delegateCreation = (IDelegateCreationOperation)context.Operation; + if (delegateCreation.Target is not IMethodReferenceOperation methodReference) + { + return; + } + + var invokedMethod = methodReference.Method; + var declaration = (invokedMethod.ReducedFrom ?? invokedMethod).OriginalDefinition; + + if (!IsTargetMethod(declaration, knownTypes, out var contractMember) || + !TryGetMessageParameter(declaration, contractMember, out var messageParameter)) + { + return; + } + + // Method groups bind to object-only overloads unless explicitly generic. + if (invokedMethod.IsGenericMethod) + { + if (invokedMethod.TypeArguments.Length > 0 && + invokedMethod.TypeArguments[0].SpecialType == SpecialType.System_Object) + { + context.ReportDiagnostic(Diagnostic.Create( + GenericTypeIsObjectRule, + methodReference.Syntax.GetLocation())); + } + + return; + } + + // Classify by the delegate parameter type; bound extension method references are unreduced. + if (delegateCreation.Type is not INamedTypeSymbol { DelegateInvokeMethod: { } invokeMethod } || + !TryMapMessageParameter(invokeMethod, declaration, messageParameter, methodReference, out var delegateMessageParameter)) + { + return; + } + + var messageType = delegateMessageParameter.Type; + if (messageType is null || messageType.TypeKind == TypeKind.Dynamic) + { + return; + } + + if (!messageType.CanBeReferencedByName) + { + return; + } + + // UpdateMessage reference types remain ambiguous because same-instance replacement can + // preserve the previous logical type. Value-type method groups cannot bind here (CS0123). + var isRoutingEquivalent = declaration.Name == "UpdateMessage" + ? messageType.IsValueType + : IsRoutingEquivalentMessageType(messageType); + if (isRoutingEquivalent) + { + if (!severityConfiguration.IsEnabled(context, methodReference.Syntax.SyntaxTree, UseGenericTypeRule)) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + UseGenericTypeRule, + methodReference.Syntax.GetLocation(), + ImmutableDictionary.Empty.Add( + MessageTypeProperty, + messageType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), + messageType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat))); + } + else + { + if (!severityConfiguration.IsEnabled(context, methodReference.Syntax.SyntaxTree, RuntimeTypeMayDifferRule)) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + RuntimeTypeMayDifferRule, + methodReference.Syntax.GetLocation(), + messageType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat))); + } + } + + static void AnalyzeInvocation( + OperationAnalysisContext context, + KnownTypes knownTypes, + MigrationDiagnosticConfiguration severityConfiguration) + { + var invocation = (IInvocationOperation)context.Operation; + var invokedMethod = invocation.TargetMethod; + var declaration = (invokedMethod.ReducedFrom ?? invokedMethod).OriginalDefinition; + + if (!IsTargetMethod(declaration, knownTypes, out var contractMember) || + !TryGetMessageParameter(declaration, contractMember, out var messageParameter)) + { + return; + } + + if (IsGenericMessageInstanceOverload(declaration, messageParameter)) + { + // In 10.x, T=object is only reachable through an explicit generic call. + if (invokedMethod.TypeArguments.Length > 0 && + invokedMethod.TypeArguments[0].SpecialType == SpecialType.System_Object) + { + context.ReportDiagnostic(Diagnostic.Create(GenericTypeIsObjectRule, invocation.Syntax.GetLocation())); + } + + return; + } + + if (!IsObjectOverload(declaration, messageParameter)) + { + return; + } + + // Interface implementations may rename parameters. Reduced extensions require the name fallback. + var messageArgument = invocation.Arguments.FirstOrDefault(argument => + argument.Parameter is not null && + (SymbolEqualityComparer.Default.Equals(argument.Parameter.OriginalDefinition, messageParameter) || + argument.Parameter.Name == messageParameter.Name)); + if (messageArgument is null) + { + return; + } + + var messageValue = UnwrapImplicitConversions(messageArgument.Value); + var messageType = messageValue.Type; + if (messageType is null || messageType.TypeKind == TypeKind.Dynamic || + messageValue.ConstantValue is { HasValue: true, Value: null }) + { + return; + } + + if (!messageType.CanBeReferencedByName) + { + return; + } + + // An object-typed creation would be fixed to the generic overload with T = System.Object, which + // immediately violates NSB0041. Never offer a fixable NSB0039 for the object type. + if (messageType.SpecialType == SpecialType.System_Object) + { + return; + } + + var isUpdateMessage = declaration.Name == "UpdateMessage"; + var isStableVarObjectCreation = !isUpdateMessage && + IsStableVarObjectCreation(messageValue, messageArgument, invocation, invocation.SemanticModel!); + if (IsRoutingEquivalent(messageValue, messageType, knownTypes.IMessageCreator, isUpdateMessage) || + isStableVarObjectCreation) + { + if (!severityConfiguration.IsEnabled(context, invocation.Syntax.SyntaxTree, UseGenericTypeRule)) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + UseGenericTypeRule, + invocation.Syntax.GetLocation(), + ImmutableDictionary.Empty.Add( + MessageTypeProperty, + messageType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), + messageType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat))); + } + else + { + if (!severityConfiguration.IsEnabled(context, invocation.Syntax.SyntaxTree, RuntimeTypeMayDifferRule)) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + RuntimeTypeMayDifferRule, + invocation.Syntax.GetLocation(), + messageType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat))); + } + } + + static bool AreMigrationDiagnosticsAutomaticallyEnabled(AnalyzerConfigOptions globalOptions) => + IsTrue(globalOptions, PublishTrimmedProperty) || + IsTrue(globalOptions, PublishAotProperty) || + IsTrue(globalOptions, IsAotCompatibleProperty) || + IsTrue(globalOptions, IsTrimmableProperty) || + IsTrue(globalOptions, EnableTrimAnalyzerProperty); + + // Mirrors Roslyn's effective severity resolution (CSharpDiagnosticFilter.GetDiagnosticReport plus + // bulk configuration): command line, tree-level editorconfig, global, bulk, then the descriptor + // default. The migration diagnostics are opt-in, so the descriptor default only applies through + // automatic activation (trimming/AOT build properties). Command line, tree-level and global + // configuration block the bulk fallback even when their value is Default, mirroring Roslyn + // (AnalyzerDriver.GetEffectiveSeverities and + // AnalyzerOptionsExtensions.TryGetSeverityFromBulkConfiguration). + sealed class MigrationDiagnosticConfiguration(bool automaticallyEnabled, Compilation compilation) + { + readonly CompilationOptions compilationOptions = compilation.Options; + readonly SyntaxTreeOptionsProvider? syntaxTreeOptionsProvider = compilation.Options.SyntaxTreeOptionsProvider; +#pragma warning disable PS0025 // Dictionary keys should implement IEquatable - trees are per-compilation and use reference equality + readonly ConcurrentDictionary treeCaches = new(); +#pragma warning restore PS0025 + + public bool IsEnabled(OperationAnalysisContext context, SyntaxTree syntaxTree, DiagnosticDescriptor descriptor) + { + var configuredSeverity = ResolveConfiguredSeverity(context, syntaxTree, descriptor); + + if (configuredSeverity != ReportDiagnostic.Default) + { + return configuredSeverity != ReportDiagnostic.Suppress; + } + + return automaticallyEnabled; + } + + ReportDiagnostic ResolveConfiguredSeverity( + OperationAnalysisContext context, + SyntaxTree syntaxTree, + DiagnosticDescriptor descriptor) + { + var cancellationToken = context.CancellationToken; + + if (compilationOptions.SpecificDiagnosticOptions.TryGetValue(descriptor.Id, out var severity) || (syntaxTreeOptionsProvider is not null && + (syntaxTreeOptionsProvider.TryGetDiagnosticValue(syntaxTree, descriptor.Id, cancellationToken, out severity) || + syntaxTreeOptionsProvider.TryGetGlobalDiagnosticValue(descriptor.Id, cancellationToken, out severity)))) + { + return severity; + } + + // Bulk-configuration helper is internal to Roslyn, so mirror it: category-level first, then + // all-analyzer level. Memoized per tree since the keys don't depend on the descriptor id. + if (descriptor.IsEnabledByDefault) + { + var treeCache = treeCaches.GetOrAdd(syntaxTree, static tree => new TreeSeverityCache(tree)); + return treeCache.ResolveBulkSeverity(context.Options.AnalyzerConfigOptionsProvider, descriptor); + } + + return ReportDiagnostic.Default; + } + + sealed class TreeSeverityCache(SyntaxTree tree) + { + readonly ConcurrentDictionary categorySeverities = new(); + volatile bool allResolved; + bool hasAllSeverity; + ReportDiagnostic allSeverity; + + // Category-level bulk config varies by descriptor category; all-level is category-independent. + public ReportDiagnostic ResolveBulkSeverity( + AnalyzerConfigOptionsProvider optionsProvider, + DiagnosticDescriptor descriptor) + { + if (!categorySeverities.TryGetValue(descriptor.Category, out var categorySeverity)) + { + var treeOptions = optionsProvider.GetOptions(tree); + var found = TryGetBulkSeverity( + treeOptions, + $"dotnet_analyzer_diagnostic.category-{descriptor.Category}.severity", + out var severity); + categorySeverity = (found, severity); + categorySeverities[descriptor.Category] = categorySeverity; + } + + if (!allResolved) + { + var treeOptions = optionsProvider.GetOptions(tree); + hasAllSeverity = TryGetBulkSeverity(treeOptions, "dotnet_analyzer_diagnostic.severity", out allSeverity); + allResolved = true; + } + + if (categorySeverity.Found) + { + return categorySeverity.Severity; + } + + return hasAllSeverity ? allSeverity : ReportDiagnostic.Default; + } + + static bool TryGetBulkSeverity(AnalyzerConfigOptions options, string key, out ReportDiagnostic severity) + { + if (options.TryGetValue(key, out var value) && TryParseSeverity(value, out severity)) + { + return true; + } + + severity = ReportDiagnostic.Default; + return false; + } + } + } + + // Mirrors AnalyzerConfigSet.TryParseSeverity. + static bool TryParseSeverity(string value, out ReportDiagnostic severity) + { + if (string.Equals(value, "default", StringComparison.OrdinalIgnoreCase)) + { + severity = ReportDiagnostic.Default; + return true; + } + + if (string.Equals(value, "error", StringComparison.OrdinalIgnoreCase)) + { + severity = ReportDiagnostic.Error; + return true; + } + + if (string.Equals(value, "warning", StringComparison.OrdinalIgnoreCase)) + { + severity = ReportDiagnostic.Warn; + return true; + } + + if (string.Equals(value, "suggestion", StringComparison.OrdinalIgnoreCase)) + { + severity = ReportDiagnostic.Info; + return true; + } + + if (string.Equals(value, "silent", StringComparison.OrdinalIgnoreCase) || + string.Equals(value, "refactoring", StringComparison.OrdinalIgnoreCase)) + { + severity = ReportDiagnostic.Hidden; + return true; + } + + if (string.Equals(value, "none", StringComparison.OrdinalIgnoreCase)) + { + severity = ReportDiagnostic.Suppress; + return true; + } + + severity = ReportDiagnostic.Default; + return false; + } + + static bool IsTrue(AnalyzerConfigOptions options, string propertyName) => + options.TryGetValue(propertyName, out var value) && + bool.TryParse(value, out var enabled) && + enabled; + + static IOperation UnwrapImplicitConversions(IOperation operation) + { + while (operation is IConversionOperation { IsImplicit: true } conversion) + { + operation = conversion.Operand; + } + + return operation; + } + + static bool IsStableVarObjectCreation( + IOperation operation, + IArgumentOperation messageArgument, + IInvocationOperation invocation, + SemanticModel semanticModel) + { + if (!IsCompleteStatementExpression(invocation) || + operation is not ILocalReferenceOperation localReference || + localReference.Local.DeclaringSyntaxReferences is not [{ } declarationReference] || + declarationReference.GetSyntax() is not VariableDeclaratorSyntax + { + Initializer: { Value: ObjectCreationExpressionSyntax }, + Parent: VariableDeclarationSyntax + } variableDeclarator || + variableDeclarator.Parent?.Parent is not LocalDeclarationStatementSyntax declarationStatement || + declarationStatement.Declaration.Type is not IdentifierNameSyntax { Identifier.ValueText: "var" } || + semanticModel.GetOperation(variableDeclarator) is not IVariableDeclaratorOperation + { + Initializer.Value: IObjectCreationOperation + }) + { + return false; + } + + // This is intentionally a structural proof rather than a control-flow proof. The local + // declaration must be immediately followed by the invocation in the same block, with no + // executable statement between them. + if (declarationStatement.Parent is not BlockSyntax block || + invocation.Syntax.FirstAncestorOrSelf() is not { Parent: BlockSyntax invocationBlock } invocationStatement || + block.Span != invocationBlock.Span) + { + return false; + } + + var declarationIndex = block.Statements.IndexOf(declarationStatement); + if (declarationIndex < 0 || declarationIndex + 1 >= block.Statements.Count || + block.Statements[declarationIndex + 1].Span != invocationStatement.Span) + { + return false; + } + + // Only a direct local/parameter identifier receiver is accepted. In particular, this + // rejects a local function, delegate, property, member access, or any other computed + // receiver that can run code before the message argument is read. Reduced extension + // invocations expose the receiver as an implicit argument rather than Instance. + var receiver = invocation.Instance ?? + invocation.Arguments.FirstOrDefault(argument => argument.IsImplicit)?.Value; + if (receiver is not (ILocalReferenceOperation or IParameterReferenceOperation) || + receiver.Syntax is not IdentifierNameSyntax) + { + return false; + } + + // The message must be the first explicit argument. This excludes destinations, options, and + // any other preceding argument whose evaluation could mutate the captured local. + var firstExplicitArgument = invocation.Arguments.FirstOrDefault(argument => !argument.IsImplicit); + return firstExplicitArgument is not null && + firstExplicitArgument.Syntax.Span == messageArgument.Syntax.Span; + } + + static bool IsCompleteStatementExpression(IInvocationOperation invocation) + { + if (invocation.Syntax.Parent is ExpressionStatementSyntax { Expression: var expression } && + ReferenceEquals(expression, invocation.Syntax)) + { + return true; + } + + return invocation.Syntax.Parent is AwaitExpressionSyntax { Parent: ExpressionStatementSyntax { Expression: var awaitStatementExpression } } awaitExpression && + ReferenceEquals(awaitStatementExpression, awaitExpression); + } + + static bool IsRoutingEquivalent( + IOperation operation, + ITypeSymbol messageType, + INamedTypeSymbol messageCreator, + bool isUpdateMessage) + { + if (operation is IObjectCreationOperation) + { + return true; + } + + if (messageType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + { + return false; + } + + if (isUpdateMessage) + { + return messageType.IsValueType; + } + + if (operation is IInvocationOperation { TargetMethod: { Name: "CreateInstance", IsGenericMethod: true } creatorMethod } && + IsOrImplements(creatorMethod.ContainingType, messageCreator)) + { + return true; + } + + return messageType.IsSealed || messageType.IsValueType; + } + + static bool IsRoutingEquivalentMessageType(ITypeSymbol messageType) => + messageType.OriginalDefinition.SpecialType != SpecialType.System_Nullable_T && + (messageType.IsSealed || messageType.IsValueType); + + static bool TryMapMessageParameter( + IMethodSymbol invokeMethod, + IMethodSymbol declaredMethod, + IParameterSymbol messageParameter, + IMethodReferenceOperation methodReference, + out IParameterSymbol delegateParameter) + { + var messageIndex = declaredMethod.Parameters.IndexOf(messageParameter); + if (messageIndex < 0) + { + delegateParameter = null!; + return false; + } + + // Bound extension method references are unreduced, so exclude the receiver. + if (methodReference.Instance is not null && declaredMethod.IsExtensionMethod) + { + messageIndex--; + } + + if (messageIndex < 0 || messageIndex >= invokeMethod.Parameters.Length) + { + delegateParameter = null!; + return false; + } + + delegateParameter = invokeMethod.Parameters[messageIndex]; + return true; + } + + static bool IsOrImplements(INamedTypeSymbol type, INamedTypeSymbol contract) => + SymbolEqualityComparer.Default.Equals(type, contract) || + type.AllInterfaces.Any(candidate => SymbolEqualityComparer.Default.Equals(candidate, contract)); + + static bool IsTargetMethod(IMethodSymbol method, KnownTypes knownTypes, out IMethodSymbol? contractMember) + { + contractMember = null; + var containingType = method.ContainingType; + if (SymbolEqualityComparer.Default.Equals(containingType, knownTypes.IMessageSession) || SymbolEqualityComparer.Default.Equals(containingType, knownTypes.IPipelineContext)) + { + return method.Name is "Send" or "Publish"; + } + + if (SymbolEqualityComparer.Default.Equals(containingType, knownTypes.IMessageProcessingContext)) + { + return method.Name == "Reply"; + } + + if (SymbolEqualityComparer.Default.Equals(containingType, knownTypes.MessageSessionExtensions) || + SymbolEqualityComparer.Default.Equals(containingType, knownTypes.PipelineContextExtensions)) + { + return method.Name is "Send" or "SendLocal" or "Publish"; + } + + if (SymbolEqualityComparer.Default.Equals(containingType, knownTypes.MessageProcessingContextExtensions)) + { + return method.Name == "Reply"; + } + + if (SymbolEqualityComparer.Default.Equals(containingType, knownTypes.Saga)) + { + return method.Name == "ReplyToOriginator"; + } + + if (SymbolEqualityComparer.Default.Equals(containingType, knownTypes.IOutgoingLogicalMessageContext)) + { + return method.Name == "UpdateMessage"; + } + + return ImplementsKnownContractMember(method, knownTypes, out contractMember); + } + + // Return the contract member so renamed implementation parameters can be mapped by ordinal. + static bool ImplementsKnownContractMember(IMethodSymbol method, KnownTypes knownTypes, out IMethodSymbol? contractMember) + { + contractMember = null; + if (method.Name is not ("Send" or "Publish" or "Reply" or "UpdateMessage")) + { + return false; + } + + // Avoid building interface maps for unrelated types; memoized per type. + return knownTypes.ImplementsAnyContractInterface(method.ContainingType) && knownTypes.TryResolveContractMember(method, out contractMember); + } + + static bool TryGetMessageParameter(IMethodSymbol method, IMethodSymbol? contractMember, out IParameterSymbol parameter) + { + // Implementations may rename interface parameters. + if (contractMember is not null) + { + var contractParameter = contractMember.Parameters.FirstOrDefault(candidate => + candidate.Name is "message" or "newInstance"); + if (contractParameter is not null) + { + var contractOrdinal = contractMember.Parameters.IndexOf(contractParameter); + if (contractOrdinal >= 0 && contractOrdinal < method.Parameters.Length) + { + parameter = method.Parameters[contractOrdinal]; + return true; + } + } + } + + parameter = method.Parameters.FirstOrDefault(candidate => candidate.Name is "message" or "newInstance")!; + return parameter is not null; + } + + static bool IsObjectOverload(IMethodSymbol method, IParameterSymbol messageParameter) => + !method.IsGenericMethod && messageParameter.Type.SpecialType == SpecialType.System_Object && !HasExplicitMessageTypeParameter(method); + + static bool HasExplicitMessageTypeParameter(IMethodSymbol method) => + method.Parameters.Any(candidate => candidate is { Name: "messageType", Type: INamedTypeSymbol { SpecialType: SpecialType.None, Name: "Type", ContainingNamespace.Name: "System" } }); + + static bool IsGenericMessageInstanceOverload(IMethodSymbol method, IParameterSymbol messageParameter) => + method is { IsGenericMethod: true, TypeParameters: [var messageTypeParameter, ..] } && + SymbolEqualityComparer.Default.Equals(messageParameter.Type, messageTypeParameter); + + sealed class KnownTypes + { + KnownTypes( + INamedTypeSymbol messageSession, + INamedTypeSymbol pipelineContext, + INamedTypeSymbol messageProcessingContext, + INamedTypeSymbol messageSessionExtensions, + INamedTypeSymbol pipelineContextExtensions, + INamedTypeSymbol messageProcessingContextExtensions, + INamedTypeSymbol saga, + INamedTypeSymbol outgoingLogicalMessageContext, + INamedTypeSymbol messageCreator) + { + IMessageSession = messageSession; + IPipelineContext = pipelineContext; + IMessageProcessingContext = messageProcessingContext; + MessageSessionExtensions = messageSessionExtensions; + PipelineContextExtensions = pipelineContextExtensions; + MessageProcessingContextExtensions = messageProcessingContextExtensions; + Saga = saga; + IOutgoingLogicalMessageContext = outgoingLogicalMessageContext; + IMessageCreator = messageCreator; + ContractInterfaces = + [ + messageSession, + pipelineContext, + messageProcessingContext, + outgoingLogicalMessageContext + ]; + } + + public INamedTypeSymbol IMessageSession { get; } + public INamedTypeSymbol IPipelineContext { get; } + public INamedTypeSymbol IMessageProcessingContext { get; } + public INamedTypeSymbol MessageSessionExtensions { get; } + public INamedTypeSymbol PipelineContextExtensions { get; } + public INamedTypeSymbol MessageProcessingContextExtensions { get; } + public INamedTypeSymbol Saga { get; } + public INamedTypeSymbol IOutgoingLogicalMessageContext { get; } + public INamedTypeSymbol IMessageCreator { get; } + public ImmutableArray ContractInterfaces { get; } + + readonly ConcurrentDictionary implementsContractCache = new(SymbolEqualityComparer.Default); + readonly ConcurrentDictionary contractMemberCache = new(SymbolEqualityComparer.Default); + + // Memoized per type so unrelated Send/Publish/Reply methods never build interface maps. + public bool ImplementsAnyContractInterface(INamedTypeSymbol type) + { + if (implementsContractCache.TryGetValue(type, out var result)) + { + return result; + } + + result = ContractInterfaces.Any(contract => + type.AllInterfaces.Any(implemented => + SymbolEqualityComparer.Default.Equals(implemented, contract))); + implementsContractCache[type] = result; + return result; + } + + // Memoized per method so interface-walking runs once per declaration across call sites. + public bool TryResolveContractMember(IMethodSymbol method, out IMethodSymbol? contractMember) + { + if (contractMemberCache.TryGetValue(method, out contractMember)) + { + return contractMember is not null; + } + + contractMember = method.ExplicitInterfaceImplementations.FirstOrDefault(implementedMember => + ContractInterfaces.Any(contract => + SymbolEqualityComparer.Default.Equals(implementedMember.ContainingType, contract))); + + if (contractMember is null) + { + foreach (var contract in ContractInterfaces) + { + foreach (var candidateMember in contract.GetMembers(method.Name).OfType()) + { + var implementation = method.ContainingType.FindImplementationForInterfaceMember(candidateMember); + if (implementation is not null && + SymbolEqualityComparer.Default.Equals(implementation.OriginalDefinition, method.OriginalDefinition)) + { + contractMember = candidateMember; + break; + } + } + + if (contractMember is not null) + { + break; + } + } + } + + contractMemberCache[method] = contractMember; + return contractMember is not null; + } + + public static bool TryCreate(Compilation compilation, out KnownTypes knownTypes) + { + var messageSession = compilation.GetTypeByMetadataName("NServiceBus.IMessageSession"); + var pipelineContext = compilation.GetTypeByMetadataName("NServiceBus.IPipelineContext"); + var messageProcessingContext = compilation.GetTypeByMetadataName("NServiceBus.IMessageProcessingContext"); + var messageSessionExtensions = compilation.GetTypeByMetadataName("NServiceBus.MessageSessionExtensions"); + var pipelineContextExtensions = compilation.GetTypeByMetadataName("NServiceBus.PipelineContextExtensions"); + var messageProcessingContextExtensions = compilation.GetTypeByMetadataName("NServiceBus.MessageProcessingContextExtensions"); + var saga = compilation.GetTypeByMetadataName("NServiceBus.Saga"); + var outgoingLogicalMessageContext = compilation.GetTypeByMetadataName("NServiceBus.Pipeline.IOutgoingLogicalMessageContext"); + var messageCreator = compilation.GetTypeByMetadataName("NServiceBus.IMessageCreator"); + + if (messageSession is null || pipelineContext is null || messageProcessingContext is null || + messageSessionExtensions is null || pipelineContextExtensions is null || + messageProcessingContextExtensions is null || saga is null || + outgoingLogicalMessageContext is null || messageCreator is null) + { + knownTypes = null!; + return false; + } + + knownTypes = new KnownTypes( + messageSession, + pipelineContext, + messageProcessingContext, + messageSessionExtensions, + pipelineContextExtensions, + messageProcessingContextExtensions, + saga, + outgoingLogicalMessageContext, + messageCreator); + return true; + } + } +} diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index 698b7aa7fc7..2cefcd367ce 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -517,9 +517,9 @@ namespace NServiceBus } public interface IMessageCreator { - object CreateInstance([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.Type messageType); - T CreateInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(); - T CreateInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(System.Action action); + object CreateInstance([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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType); + T CreateInstance<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(); + T CreateInstance<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(System.Action action); } public interface IMessageHandlerContext : NServiceBus.Extensibility.IExtendable, NServiceBus.ICancellableContext, NServiceBus.IMessageProcessingContext, NServiceBus.IPipelineContext { @@ -532,15 +532,42 @@ namespace NServiceBus string MessageId { get; } string ReplyToAddress { get; } System.Threading.Tasks.Task ForwardCurrentMessageTo(string destination); + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] System.Threading.Tasks.Task Reply(object message, NServiceBus.ReplyOptions options); - System.Threading.Tasks.Task Reply<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(System.Action messageConstructor, NServiceBus.ReplyOptions options); + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification="The default interface implementation preserves compatibility with third-party imp" + + "lementations. Built-in implementations override this method and preserve the dec" + + "lared message type.")] + System.Threading.Tasks.Task Reply(object message, [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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType, NServiceBus.ReplyOptions options); + System.Threading.Tasks.Task Reply<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(System.Action messageConstructor, NServiceBus.ReplyOptions options); + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + System.Threading.Tasks.Task Reply<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T message, NServiceBus.ReplyOptions options); } public interface IMessageSession { + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] System.Threading.Tasks.Task Publish(object message, NServiceBus.PublishOptions publishOptions, System.Threading.CancellationToken cancellationToken = default); - System.Threading.Tasks.Task Publish<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(System.Action messageConstructor, NServiceBus.PublishOptions publishOptions, System.Threading.CancellationToken cancellationToken = default); + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification="The default interface implementation preserves compatibility with third-party imp" + + "lementations. Built-in implementations override this method and preserve the dec" + + "lared message type.")] + System.Threading.Tasks.Task Publish(object message, [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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType, NServiceBus.PublishOptions publishOptions, System.Threading.CancellationToken cancellationToken = default); + System.Threading.Tasks.Task Publish<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(System.Action messageConstructor, NServiceBus.PublishOptions publishOptions, System.Threading.CancellationToken cancellationToken = default); + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + System.Threading.Tasks.Task Publish<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T message, NServiceBus.PublishOptions publishOptions, System.Threading.CancellationToken cancellationToken = default); + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] System.Threading.Tasks.Task Send(object message, NServiceBus.SendOptions sendOptions, System.Threading.CancellationToken cancellationToken = default); - System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(System.Action messageConstructor, NServiceBus.SendOptions sendOptions, System.Threading.CancellationToken cancellationToken = default); + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification="The default interface implementation preserves compatibility with third-party imp" + + "lementations. Built-in implementations override this method and preserve the dec" + + "lared message type.")] + System.Threading.Tasks.Task Send(object message, [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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType, NServiceBus.SendOptions sendOptions, System.Threading.CancellationToken cancellationToken = default); + System.Threading.Tasks.Task Send<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(System.Action messageConstructor, NServiceBus.SendOptions sendOptions, System.Threading.CancellationToken cancellationToken = default); + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + System.Threading.Tasks.Task Send<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T message, NServiceBus.SendOptions sendOptions, System.Threading.CancellationToken cancellationToken = default); System.Threading.Tasks.Task Subscribe(System.Type eventType, NServiceBus.SubscribeOptions subscribeOptions, System.Threading.CancellationToken cancellationToken = default); System.Threading.Tasks.Task Unsubscribe(System.Type eventType, NServiceBus.UnsubscribeOptions unsubscribeOptions, System.Threading.CancellationToken cancellationToken = default); } @@ -550,10 +577,28 @@ namespace NServiceBus } public interface IPipelineContext : NServiceBus.Extensibility.IExtendable, NServiceBus.ICancellableContext { + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] System.Threading.Tasks.Task Publish(object message, NServiceBus.PublishOptions options); - System.Threading.Tasks.Task Publish<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(System.Action messageConstructor, NServiceBus.PublishOptions publishOptions); + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification="The default interface implementation preserves compatibility with third-party imp" + + "lementations. Built-in implementations override this method and preserve the dec" + + "lared message type.")] + System.Threading.Tasks.Task Publish(object message, [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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType, NServiceBus.PublishOptions options); + System.Threading.Tasks.Task Publish<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(System.Action messageConstructor, NServiceBus.PublishOptions publishOptions); + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + System.Threading.Tasks.Task Publish<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T message, NServiceBus.PublishOptions options); + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] System.Threading.Tasks.Task Send(object message, NServiceBus.SendOptions options); - System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(System.Action messageConstructor, NServiceBus.SendOptions options); + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification="The default interface implementation preserves compatibility with third-party imp" + + "lementations. Built-in implementations override this method and preserve the dec" + + "lared message type.")] + System.Threading.Tasks.Task Send(object message, [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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType, NServiceBus.SendOptions options); + System.Threading.Tasks.Task Send<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(System.Action messageConstructor, NServiceBus.SendOptions options); + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + System.Threading.Tasks.Task Send<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T message, NServiceBus.SendOptions options); } public interface ISagaNotFoundHandler { @@ -689,10 +734,16 @@ namespace NServiceBus { public static void DisablePublishing(this NServiceBus.RoutingSettings routingSettings) where T : NServiceBus.Transport.TransportDefinition, NServiceBus.Routing.IMessageDrivenSubscriptionTransport { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Registering publishers by assembly or namespace requires assembly scanning and is" + + " not supported in trimming scenarios. Register publishers by message type instea" + + "d.")] public static void RegisterPublisher(this NServiceBus.RoutingSettings routingSettings, System.Reflection.Assembly assembly, string publisherEndpoint) where T : NServiceBus.Transport.TransportDefinition, NServiceBus.Routing.IMessageDrivenSubscriptionTransport { } public static void RegisterPublisher(this NServiceBus.RoutingSettings routingSettings, System.Type eventType, string publisherEndpoint) where T : NServiceBus.Transport.TransportDefinition, NServiceBus.Routing.IMessageDrivenSubscriptionTransport { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Registering publishers by assembly or namespace requires assembly scanning and is" + + " not supported in trimming scenarios. Register publishers by message type instea" + + "d.")] public static void RegisterPublisher(this NServiceBus.RoutingSettings routingSettings, System.Reflection.Assembly assembly, string @namespace, string publisherEndpoint) where T : NServiceBus.Transport.TransportDefinition, NServiceBus.Routing.IMessageDrivenSubscriptionTransport { } public static void SubscriptionAuthorizer(this NServiceBus.RoutingSettings routingSettings, System.Func authorizer) @@ -722,8 +773,14 @@ namespace NServiceBus } public static class MessageProcessingContextExtensions { + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] public static System.Threading.Tasks.Task Reply(this NServiceBus.IMessageProcessingContext context, object message) { } - public static System.Threading.Tasks.Task Reply<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this NServiceBus.IMessageProcessingContext context, System.Action messageConstructor) { } + public static System.Threading.Tasks.Task Reply(this NServiceBus.IMessageProcessingContext context, object message, [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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType) { } + public static System.Threading.Tasks.Task Reply<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IMessageProcessingContext context, System.Action messageConstructor) { } + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static System.Threading.Tasks.Task Reply<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IMessageProcessingContext context, T message) { } } public static class MessageProcessingOptimizationExtensions { @@ -734,15 +791,39 @@ namespace NServiceBus } public static class MessageSessionExtensions { + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] public static System.Threading.Tasks.Task Publish(this NServiceBus.IMessageSession session, object message, System.Threading.CancellationToken cancellationToken = default) { } - public static System.Threading.Tasks.Task Publish<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this NServiceBus.IMessageSession session, System.Threading.CancellationToken cancellationToken = default) { } - public static System.Threading.Tasks.Task Publish<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this NServiceBus.IMessageSession session, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } + public static System.Threading.Tasks.Task Publish(this NServiceBus.IMessageSession session, object message, [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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType, System.Threading.CancellationToken cancellationToken = default) { } + public static System.Threading.Tasks.Task Publish<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IMessageSession session, System.Threading.CancellationToken cancellationToken = default) { } + public static System.Threading.Tasks.Task Publish<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IMessageSession session, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static System.Threading.Tasks.Task Publish<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IMessageSession session, T message, System.Threading.CancellationToken cancellationToken = default) { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] public static System.Threading.Tasks.Task Send(this NServiceBus.IMessageSession session, object message, System.Threading.CancellationToken cancellationToken = default) { } + public static System.Threading.Tasks.Task Send(this NServiceBus.IMessageSession session, object message, [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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType, System.Threading.CancellationToken cancellationToken = default) { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] public static System.Threading.Tasks.Task Send(this NServiceBus.IMessageSession session, string destination, object message, System.Threading.CancellationToken cancellationToken = default) { } - public static System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this NServiceBus.IMessageSession session, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } - public static System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this NServiceBus.IMessageSession session, string destination, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } + public static System.Threading.Tasks.Task Send(this NServiceBus.IMessageSession session, string destination, object message, [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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType, System.Threading.CancellationToken cancellationToken = default) { } + public static System.Threading.Tasks.Task Send<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IMessageSession session, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static System.Threading.Tasks.Task Send<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IMessageSession session, T message, System.Threading.CancellationToken cancellationToken = default) { } + public static System.Threading.Tasks.Task Send<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IMessageSession session, string destination, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static System.Threading.Tasks.Task Send<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IMessageSession session, string destination, T message, System.Threading.CancellationToken cancellationToken = default) { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] public static System.Threading.Tasks.Task SendLocal(this NServiceBus.IMessageSession session, object message, System.Threading.CancellationToken cancellationToken = default) { } - public static System.Threading.Tasks.Task SendLocal<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this NServiceBus.IMessageSession session, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } + public static System.Threading.Tasks.Task SendLocal(this NServiceBus.IMessageSession session, object message, [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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType, System.Threading.CancellationToken cancellationToken = default) { } + public static System.Threading.Tasks.Task SendLocal<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IMessageSession session, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static System.Threading.Tasks.Task SendLocal<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IMessageSession session, T message, System.Threading.CancellationToken cancellationToken = default) { } public static System.Threading.Tasks.Task Subscribe(this NServiceBus.IMessageSession session, System.Type messageType, System.Threading.CancellationToken cancellationToken = default) { } public static System.Threading.Tasks.Task Subscribe(this NServiceBus.IMessageSession session, System.Threading.CancellationToken cancellationToken = default) { } public static System.Threading.Tasks.Task Unsubscribe(this NServiceBus.IMessageSession session, System.Type messageType, System.Threading.CancellationToken cancellationToken = default) { } @@ -818,15 +899,39 @@ namespace NServiceBus } public static class PipelineContextExtensions { + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] public static System.Threading.Tasks.Task Publish(this NServiceBus.IPipelineContext context, object message) { } - public static System.Threading.Tasks.Task Publish<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this NServiceBus.IPipelineContext context) { } - public static System.Threading.Tasks.Task Publish<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this NServiceBus.IPipelineContext context, System.Action messageConstructor) { } + public static System.Threading.Tasks.Task Publish(this NServiceBus.IPipelineContext context, object message, [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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType) { } + public static System.Threading.Tasks.Task Publish<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IPipelineContext context) { } + public static System.Threading.Tasks.Task Publish<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IPipelineContext context, System.Action messageConstructor) { } + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static System.Threading.Tasks.Task Publish<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IPipelineContext context, T message) { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] public static System.Threading.Tasks.Task Send(this NServiceBus.IPipelineContext context, object message) { } + public static System.Threading.Tasks.Task Send(this NServiceBus.IPipelineContext context, object message, [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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType) { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] public static System.Threading.Tasks.Task Send(this NServiceBus.IPipelineContext context, string destination, object message) { } - public static System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this NServiceBus.IPipelineContext context, System.Action messageConstructor) { } - public static System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this NServiceBus.IPipelineContext context, string destination, System.Action messageConstructor) { } + public static System.Threading.Tasks.Task Send(this NServiceBus.IPipelineContext context, string destination, object message, [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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType) { } + public static System.Threading.Tasks.Task Send<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IPipelineContext context, System.Action messageConstructor) { } + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static System.Threading.Tasks.Task Send<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IPipelineContext context, T message) { } + public static System.Threading.Tasks.Task Send<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IPipelineContext context, string destination, System.Action messageConstructor) { } + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static System.Threading.Tasks.Task Send<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IPipelineContext context, string destination, T message) { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] public static System.Threading.Tasks.Task SendLocal(this NServiceBus.IPipelineContext context, object message) { } - public static System.Threading.Tasks.Task SendLocal<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(this NServiceBus.IPipelineContext context, System.Action messageConstructor) { } + public static System.Threading.Tasks.Task SendLocal(this NServiceBus.IPipelineContext context, object message, [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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType) { } + public static System.Threading.Tasks.Task SendLocal<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IPipelineContext context, System.Action messageConstructor) { } + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static System.Threading.Tasks.Task SendLocal<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IPipelineContext context, T message) { } } public class PublishOptions : NServiceBus.Extensibility.ExtendableOptions { @@ -963,8 +1068,12 @@ namespace NServiceBus { public RoutingSettings(NServiceBus.Settings.SettingsHolder settings) { } public void DoNotEnforceBestPractices() { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Routing messages by assembly or namespace requires assembly scanning and is not s" + + "upported in trimming scenarios. Register routes by message type instead.")] public void RouteToEndpoint(System.Reflection.Assembly assembly, string destination) { } public void RouteToEndpoint(System.Type messageType, string destination) { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Routing messages by assembly or namespace requires assembly scanning and is not s" + + "upported in trimming scenarios. Register routes by message type instead.")] public void RouteToEndpoint(System.Reflection.Assembly assembly, string @namespace, string destination) { } } public class RoutingSettings : NServiceBus.RoutingSettings @@ -979,13 +1088,20 @@ namespace NServiceBus public NServiceBus.IContainSagaData Entity { get; set; } protected abstract void ConfigureHowToFindSaga(NServiceBus.IConfigureHowToFindSagaWithMessage sagaMessageFindingConfiguration); protected void MarkAsComplete() { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] + [System.Obsolete("Use \'ReplyToOriginator\' instead. Will be treated as an error from version 11.0" + + ".0. Will be removed in version 12.0.0.", false)] protected System.Threading.Tasks.Task ReplyToOriginator(NServiceBus.IMessageHandlerContext context, object message, System.Collections.Generic.IReadOnlyDictionary? outgoingHeaders = null) { } - protected System.Threading.Tasks.Task RequestTimeout(NServiceBus.IMessageHandlerContext context, System.DateTimeOffset at) + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + protected System.Threading.Tasks.Task ReplyToOriginator<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(NServiceBus.IMessageHandlerContext context, T message, System.Collections.Generic.IReadOnlyDictionary? outgoingHeaders = null) { } + protected System.Threading.Tasks.Task RequestTimeout<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TTimeoutMessageType>(NServiceBus.IMessageHandlerContext context, System.DateTimeOffset at) where TTimeoutMessageType : new() { } - protected System.Threading.Tasks.Task RequestTimeout(NServiceBus.IMessageHandlerContext context, System.TimeSpan within) + protected System.Threading.Tasks.Task RequestTimeout<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TTimeoutMessageType>(NServiceBus.IMessageHandlerContext context, System.TimeSpan within) where TTimeoutMessageType : new() { } - protected System.Threading.Tasks.Task RequestTimeout(NServiceBus.IMessageHandlerContext context, System.DateTimeOffset at, TTimeoutMessageType timeoutMessage) { } - protected System.Threading.Tasks.Task RequestTimeout(NServiceBus.IMessageHandlerContext context, System.TimeSpan within, TTimeoutMessageType timeoutMessage) { } + protected System.Threading.Tasks.Task RequestTimeout<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TTimeoutMessageType>(NServiceBus.IMessageHandlerContext context, System.DateTimeOffset at, TTimeoutMessageType timeoutMessage) { } + protected System.Threading.Tasks.Task RequestTimeout<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TTimeoutMessageType>(NServiceBus.IMessageHandlerContext context, System.TimeSpan within, TTimeoutMessageType timeoutMessage) { } } [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple=false, Inherited=false)] public sealed class SagaAttribute : System.Attribute @@ -1120,8 +1236,14 @@ namespace NServiceBus public class SubscriptionMigrationModeSettings : NServiceBus.Configuration.AdvancedExtensibility.ExposeSettings { public SubscriptionMigrationModeSettings(NServiceBus.Settings.SettingsHolder settings) { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Registering publishers by assembly or namespace requires assembly scanning and is" + + " not supported in trimming scenarios. Register publishers by message type instea" + + "d.")] public void RegisterPublisher(System.Reflection.Assembly assembly, string publisherEndpoint) { } public void RegisterPublisher(System.Type eventType, string publisherEndpoint) { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Registering publishers by assembly or namespace requires assembly scanning and is" + + " not supported in trimming scenarios. Register publishers by message type instea" + + "d.")] public void RegisterPublisher(System.Reflection.Assembly assembly, string @namespace, string publisherEndpoint) { } public void SubscriptionAuthorizer(System.Func authorizer) { } } @@ -1713,9 +1835,9 @@ namespace NServiceBus.MessageInterfaces.MessageMapper.Reflection public class MessageMapper : NServiceBus.IMessageCreator, NServiceBus.MessageInterfaces.IMessageMapper { public MessageMapper() { } - public object CreateInstance([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.Type t) { } - public T CreateInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>() { } - public T CreateInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(System.Action action) { } + public object CreateInstance([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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type t) { } + public T CreateInstance<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>() { } + public T CreateInstance<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(System.Action action) { } public System.Type? GetMappedTypeFor(System.Type t) { } public System.Type? GetMappedTypeFor(string typeName) { } public void Initialize(System.Collections.Generic.IEnumerable? types) { } @@ -1723,9 +1845,9 @@ namespace NServiceBus.MessageInterfaces.MessageMapper.Reflection public sealed class TrimmingSafeMessageMapper : NServiceBus.IMessageCreator, NServiceBus.MessageInterfaces.IMessageMapper { public TrimmingSafeMessageMapper() { } - public object CreateInstance([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.Type t) { } - public T CreateInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>() { } - public T CreateInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>(System.Action action) { } + public object CreateInstance([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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type t) { } + public T CreateInstance<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>() { } + public T CreateInstance<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(System.Action action) { } public System.Type? GetMappedTypeFor(System.Type t) { } public System.Type? GetMappedTypeFor(string typeName) { } public void Initialize(System.Collections.Generic.IEnumerable? types) { } @@ -1942,7 +2064,16 @@ namespace NServiceBus.Pipeline { NServiceBus.Pipeline.OutgoingLogicalMessage Message { get; } System.Collections.Generic.IReadOnlyCollection RoutingStrategies { get; } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] void UpdateMessage(object newInstance); + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification="The default interface implementation preserves compatibility with third-party imp" + + "lementations. Built-in implementations override this method and preserve the dec" + + "lared message type.")] + void UpdateMessage(object newInstance, [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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType); + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + void UpdateMessage<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T newInstance); } public interface IOutgoingPhysicalMessageContext : NServiceBus.Extensibility.IExtendable, NServiceBus.ICancellableContext, NServiceBus.IPipelineContext, NServiceBus.Pipeline.IBehaviorContext, NServiceBus.Pipeline.IOutgoingContext { @@ -2040,8 +2171,9 @@ namespace NServiceBus.Pipeline } public class OutgoingLogicalMessage { - public OutgoingLogicalMessage(System.Type messageType, object message) { } + public OutgoingLogicalMessage([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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType, object message) { } public object Instance { get; } + [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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] public System.Type MessageType { get; } } public class PipelineSettings : NServiceBus.Configuration.AdvancedExtensibility.ExposeSettings @@ -2166,12 +2298,13 @@ namespace NServiceBus.Routing public interface IMessageDrivenSubscriptionTransport { } public class MulticastAddressTag : NServiceBus.Routing.AddressTag { - public MulticastAddressTag(System.Type messageType) { } + public MulticastAddressTag([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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType) { } + [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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] public System.Type MessageType { get; } } public class MulticastRoutingStrategy : NServiceBus.Routing.RoutingStrategy { - public MulticastRoutingStrategy(System.Type messageType) { } + public MulticastRoutingStrategy([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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType) { } public override NServiceBus.Routing.AddressTag Apply(System.Collections.Generic.Dictionary headers) { } } public class RouteTableEntry @@ -2576,8 +2709,9 @@ namespace NServiceBus.Transport } public class MulticastTransportOperation : NServiceBus.Transport.IOutgoingTransportOperation { - public MulticastTransportOperation(NServiceBus.Transport.OutgoingMessage message, System.Type messageType, NServiceBus.Transport.DispatchProperties properties, NServiceBus.Transport.DispatchConsistency requiredDispatchConsistency = 1) { } + public MulticastTransportOperation(NServiceBus.Transport.OutgoingMessage message, [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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType, NServiceBus.Transport.DispatchProperties properties, NServiceBus.Transport.DispatchConsistency requiredDispatchConsistency = 1) { } public NServiceBus.Transport.OutgoingMessage Message { get; } + [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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] public System.Type MessageType { get; } public NServiceBus.Transport.DispatchProperties Properties { get; } public NServiceBus.Transport.DispatchConsistency RequiredDispatchConsistency { get; } diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/NullabilityWarnings.ApproveNullabilityWarnings.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/NullabilityWarnings.ApproveNullabilityWarnings.approved.txt index b66f2de8d60..ea20cafe71c 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/NullabilityWarnings.ApproveNullabilityWarnings.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/NullabilityWarnings.ApproveNullabilityWarnings.approved.txt @@ -127,9 +127,6 @@ src/NServiceBus.Core/Sagas/ActiveSagaInstance.cs src/NServiceBus.Core/Sagas/CustomFinderAdapter.cs CS8603: Possible null reference return. -src/NServiceBus.Core/Sagas/Saga.cs - CS8604: Possible null reference argument for parameter 'message' in 'Task IPipelineContext.Send(object message, SendOptions options)'. - src/NServiceBus.Core/Sagas/SagaCorrelationProperty.cs CS8603: Possible null reference return. @@ -204,9 +201,6 @@ src/NServiceBus.Core/Transports/Learning/HeaderSerializer.cs src/NServiceBus.Core/Transports/Learning/LearningTransport.cs CS8618: Non-nullable property 'StorageDirectory' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. -src/NServiceBus.Core/Transports/Learning/LearningTransportDispatcher.cs - CS8604: Possible null reference argument for parameter 'path3' in 'string Path.Combine(string path1, string path2, string path3)'. - src/NServiceBus.Core/Transports/Learning/LearningTransportInfrastructure.cs CS8604: Possible null reference argument for parameter 'subscriptionManager' in 'LearningTransportMessagePump.LearningTransportMessagePump(string id, string receiveAddress, string basePath, Action criticalErrorAction, ISubscriptionManager subscriptionManager, ReceiveSettings receiveSettings, TransportTransactionMode transactionMode)'. @@ -246,10 +240,10 @@ src/NServiceBus.Core/Transports/TransportSeam.cs CS8618: Non-nullable field 'transportInfrastructure' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. src/NServiceBus.Core/Unicast/MessageOperations.cs - CS8604: Possible null reference argument for parameter 'message' in 'Task MessageOperations.Publish(IBehaviorContext context, Type messageType, object message, PublishOptions options)'. + CS8604: Possible null reference argument for parameter 'message' in 'Task MessageOperations.PublishMessage(IBehaviorContext context, Type messageType, object message, PublishOptions options)'. CS8604: Possible null reference argument for parameter 'message' in 'Task MessageOperations.ReplyMessage(IBehaviorContext context, Type messageType, object message, ReplyOptions options)'. CS8604: Possible null reference argument for parameter 'message' in 'Task MessageOperations.SendMessage(IBehaviorContext context, Type messageType, object message, SendOptions options)'. - CS8604: Possible null reference argument for parameter 'messageType' in 'Task MessageOperations.Publish(IBehaviorContext context, Type messageType, object message, PublishOptions options)'. + CS8604: Possible null reference argument for parameter 'messageType' in 'Task MessageOperations.PublishMessage(IBehaviorContext context, Type messageType, object message, PublishOptions options)'. CS8604: Possible null reference argument for parameter 'messageType' in 'Task MessageOperations.ReplyMessage(IBehaviorContext context, Type messageType, object message, ReplyOptions options)'. CS8604: Possible null reference argument for parameter 'messageType' in 'Task MessageOperations.SendMessage(IBehaviorContext context, Type messageType, object message, SendOptions options)'. diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt index d74b447a806..7be47134f9a 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt @@ -2,10 +2,8 @@ The following trimming warnings are present in NServiceBus.Core. Changes that make this list longer should not be approved. ----- -src/NServiceBus.Core/Hosting/StartupDiagnostics/Host.cs - IL2026: Using member 'System.Reflection.Assembly.GetType(String)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Types might be removed by trimming. If the type name is a string literal, consider using Type.GetType instead. - IL2075: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods' in call to 'System.Type.GetMethod(String, BindingFlags)'. The return value of method 'System.Reflection.Assembly.GetType(String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. - IL2075: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperty(String, BindingFlags)'. The return value of method 'System.Reflection.Assembly.GetType(String)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. +src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs + IL2026: Using member 'NServiceBus.Pipeline.IOutgoingLogicalMessageContext.UpdateMessage(Object)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. When trimming is enabled, routing a message using its runtime type cannot be statically analyzed by the trimmer. Use the generic overload or, when the message type is not known at compile time, the overload accepting an explicit Type. src/NServiceBus.Core/Pipeline/RegisterStep.cs IL2072: 'instanceType' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicConstructors' in call to 'Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateInstance(IServiceProvider, Type, params Object[])'. The return value of method 'NServiceBus.Pipeline.RegisterStep.BehaviorType.get' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. @@ -14,33 +12,12 @@ src/NServiceBus.Core/Pipeline/RegisterStepExtensions.cs IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 'behaviorType' of method 'NServiceBus.RegisterStepExtensions.GetBehaviorInterface(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 'behaviorType' of method 'NServiceBus.RegisterStepExtensions.IsBehavior(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. -src/NServiceBus.Core/Receiving/ReceiveComponent.Configuration.cs - IL2075: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in call to 'System.Type.GetProperties()'. The return value of method '.MessageType.get' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. - src/NServiceBus.Core/Routing/AssemblyRouteSource.cs - IL2026: Using member 'System.Reflection.Assembly.GetTypes()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Types might be removed. + 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 - IL2026: Using member 'System.Reflection.Assembly.GetTypes()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Types might be removed. - -src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/NamespacePublisherSource.cs - IL2026: Using member 'System.Reflection.Assembly.GetTypes()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Types might be removed. + 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 - IL2026: Using member 'System.Reflection.Assembly.GetTypes()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Types might be removed. - -src/NServiceBus.Core/Transports/Learning/LearningTransportDispatcher.cs - IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 'messageType' of method 'NServiceBus.LearningTransportDispatcher.GetPotentialEventTypes(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. - -src/NServiceBus.Core/Transports/TransportSeam.cs - IL2026: Using member 'System.Transactions.TransactionManager.ImplicitDistributedTransactions.set' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Distributed transactions support may not be compatible with trimming. If your program creates a distributed transaction via System.Transactions, the correctness of the application cannot be guaranteed after trimming. - -src/NServiceBus.Core/Unicast/MessageHandlerRegistry.cs - IL2026: Using member 'NServiceBus.Unicast.MessageHandlerRegistry.AddHandler()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Registering handlers using assembly scanning is not supported in trimming scenarios. - IL2026: Using member 'NServiceBus.Unicast.MessageHandlerRegistry.AddScannedHandlers(IEnumerable)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Registering handlers using assembly scanning is not supported in trimming scenarios. - IL2026: Using member 'NServiceBus.Unicast.MessageHandlerRegistry.RegisterHandler(Type)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. Registering handlers using assembly scanning is not supported in trimming scenarios. - -src/NServiceBus.Core/Unicast/Messages/MessageMetadataRegistry.cs - IL2057: Unrecognized value passed to the parameter 'typeName' of method 'System.Type.GetType(String)'. It's not possible to guarantee the availability of the target type. - IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 'type' of method 'NServiceBus.Unicast.Messages.MessageMetadataRegistry.GetParentTypes(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. - IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 'type' of method 'NServiceBus.Unicast.Messages.MessageMetadataRegistry.PlaceInMessageHierarchy(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. + 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/Hosting/HostTests.cs b/src/NServiceBus.Core.Tests/Hosting/HostTests.cs index 05e591cd6f4..a607aa52ccb 100644 --- a/src/NServiceBus.Core.Tests/Hosting/HostTests.cs +++ b/src/NServiceBus.Core.Tests/Hosting/HostTests.cs @@ -8,7 +8,7 @@ public class HostTests { [Test] - public void When_not_running_ASP_NET_should_choose_BaseDirectory_as_output_directory() + public void Should_choose_BaseDirectory_as_output_directory() { var directory = Host.GetOutputDirectory(); diff --git a/src/NServiceBus.Core.Tests/Pipeline/Outgoing/OutgoingLogicalMessageContextTests.cs b/src/NServiceBus.Core.Tests/Pipeline/Outgoing/OutgoingLogicalMessageContextTests.cs index 14120d2b2f3..73f2a97c18b 100644 --- a/src/NServiceBus.Core.Tests/Pipeline/Outgoing/OutgoingLogicalMessageContextTests.cs +++ b/src/NServiceBus.Core.Tests/Pipeline/Outgoing/OutgoingLogicalMessageContextTests.cs @@ -53,7 +53,29 @@ public void Updating_the_message_to_a_new_type_should_update_the_MessageType() Assert.That(context.Message.MessageType, Is.EqualTo(typeof(MyDifferentMessage))); } - class MyDifferentMessage + [Test] + public void Updating_the_message_with_an_explicit_type_should_use_that_type() + { + var context = new OutgoingLogicalMessageContext("message1234", [], new OutgoingLogicalMessage(typeof(MyDifferentMessage), new MyDifferentMessage()), null, new FakeRootContext()); + var replacement = new MyDifferentMessage(); + + context.UpdateMessage(replacement); + + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + } + + [Test] + public void Updating_the_existing_instance_with_an_explicit_type_should_use_that_type() + { + var message = new MyDifferentMessage(); + var context = new OutgoingLogicalMessageContext("message1234", [], new OutgoingLogicalMessage(typeof(MyDifferentMessage), message), null, new FakeRootContext()); + + context.UpdateMessage(message); + + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + } + + class MyDifferentMessage : IMyMessage { public Guid Id { get; set; } } diff --git a/src/NServiceBus.Core.Tests/TestingFakesAnnotationParityTests.cs b/src/NServiceBus.Core.Tests/TestingFakesAnnotationParityTests.cs new file mode 100644 index 00000000000..25008216b99 --- /dev/null +++ b/src/NServiceBus.Core.Tests/TestingFakesAnnotationParityTests.cs @@ -0,0 +1,113 @@ +namespace NServiceBus.Core.Tests; + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using NServiceBus.Pipeline; +using NServiceBus.Testing; +using NUnit.Framework; + +[TestFixture] +public class TestingFakesAnnotationParityTests +{ + [TestCase(typeof(TestablePipelineContext), typeof(IPipelineContext), "Send")] + [TestCase(typeof(TestablePipelineContext), typeof(IPipelineContext), "Publish")] + [TestCase(typeof(TestableMessageSession), typeof(IMessageSession), "Send")] + [TestCase(typeof(TestableMessageSession), typeof(IMessageSession), "Publish")] + [TestCase(typeof(TestableMessageProcessingContext), typeof(IMessageProcessingContext), "Reply")] + [TestCase(typeof(TestableOutgoingLogicalMessageContext), typeof(IOutgoingLogicalMessageContext), "UpdateMessage")] + public void Object_only_method_matches_interface_RequiresUnreferencedCode(Type fakeType, Type interfaceType, string methodName) + { + var fake = FindObjectOnly(fakeType, methodName); + var iface = FindObjectOnly(interfaceType, methodName); + + Assert.That(fake, Is.Not.Null, $"{fakeType.Name}.{methodName} object-only overload not found"); + Assert.That(iface, Is.Not.Null, $"{interfaceType.Name}.{methodName} object-only overload not found"); + + Assert.That(HasAttribute(fake), + Is.EqualTo(HasAttribute(iface)), + $"RequiresUnreferencedCode annotation mismatch on {fakeType.Name}.{methodName}(object)"); + } + + [TestCase(typeof(TestablePipelineContext), typeof(IPipelineContext), "Send")] + [TestCase(typeof(TestablePipelineContext), typeof(IPipelineContext), "Publish")] + [TestCase(typeof(TestableMessageSession), typeof(IMessageSession), "Send")] + [TestCase(typeof(TestableMessageSession), typeof(IMessageSession), "Publish")] + [TestCase(typeof(TestableMessageProcessingContext), typeof(IMessageProcessingContext), "Reply")] + [TestCase(typeof(TestableOutgoingLogicalMessageContext), typeof(IOutgoingLogicalMessageContext), "UpdateMessage")] + public void Typed_method_matches_interface_annotations(Type fakeType, Type interfaceType, string methodName) + { + var fake = FindTyped(fakeType, methodName); + var iface = FindTyped(interfaceType, methodName); + + Assert.That(fake, Is.Not.Null, $"{fakeType.Name}.{methodName} typed overload not found"); + Assert.That(iface, Is.Not.Null, $"{interfaceType.Name}.{methodName} typed overload not found"); + + Assert.That(GetOverloadResolutionPriority(fake), Is.EqualTo(GetOverloadResolutionPriority(iface)), + $"OverloadResolutionPriority mismatch on {fakeType.Name}.{methodName}"); + + Assert.That(HasDynamicallyAccessedMembersOnGenericArgument(fake), + Is.EqualTo(HasDynamicallyAccessedMembersOnGenericArgument(iface)), + $"DynamicallyAccessedMembers on T mismatch on {fakeType.Name}.{methodName}"); + } + + [TestCase(typeof(TestablePipelineContext), typeof(IPipelineContext), "Send")] + [TestCase(typeof(TestablePipelineContext), typeof(IPipelineContext), "Publish")] + [TestCase(typeof(TestableMessageSession), typeof(IMessageSession), "Send")] + [TestCase(typeof(TestableMessageSession), typeof(IMessageSession), "Publish")] + [TestCase(typeof(TestableMessageProcessingContext), typeof(IMessageProcessingContext), "Reply")] + [TestCase(typeof(TestableOutgoingLogicalMessageContext), typeof(IOutgoingLogicalMessageContext), "UpdateMessage")] + public void Explicit_type_method_matches_interface_DynamicallyAccessedMembers(Type fakeType, Type interfaceType, string methodName) + { + var fake = FindExplicitType(fakeType, methodName); + var iface = FindExplicitType(interfaceType, methodName); + + Assert.That(fake, Is.Not.Null, $"{fakeType.Name}.{methodName}(object, Type) explicit-type overload not found"); + Assert.That(iface, Is.Not.Null, $"{interfaceType.Name}.{methodName}(object, Type) explicit-type overload not found"); + + Assert.That(HasDynamicallyAccessedMembersOnTypeParameter(fake), + Is.EqualTo(HasDynamicallyAccessedMembersOnTypeParameter(iface)), + $"DynamicallyAccessedMembers on messageType mismatch on {fakeType.Name}.{methodName}(object, Type)"); + } + + static MethodInfo FindObjectOnly(Type type, string name) => + type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) + .SingleOrDefault(m => m.Name == name + && !m.IsGenericMethodDefinition + && m.GetParameters().Length > 0 + && m.GetParameters()[0].ParameterType == typeof(object) + && !m.GetParameters().Any(p => p.ParameterType == typeof(Type))); + + static MethodInfo FindTyped(Type type, string name) => + type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) + .SingleOrDefault(m => m.Name == name + && m.IsGenericMethodDefinition + && m.GetParameters().Length > 0 + && m.GetParameters()[0].ParameterType.IsGenericParameter); + + static MethodInfo FindExplicitType(Type type, string name) => + type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) + .SingleOrDefault(m => m.Name == name + && !m.IsGenericMethodDefinition + && m.GetParameters().Length > 1 + && m.GetParameters()[0].ParameterType == typeof(object) + && m.GetParameters().Any(p => p.ParameterType == typeof(Type))); + + static bool HasAttribute(MethodInfo method) where T : Attribute => + method.GetCustomAttributes().Any(); + + static int? GetOverloadResolutionPriority(MethodInfo method) => + method.GetCustomAttributes() + .Select(a => (int?)a.Priority) + .FirstOrDefault(); + + static bool HasDynamicallyAccessedMembersOnGenericArgument(MethodInfo method) => + method.IsGenericMethodDefinition + && method.GetGenericArguments()[0].GetCustomAttributes().Any(); + + static bool HasDynamicallyAccessedMembersOnTypeParameter(MethodInfo method) => + method.GetParameters().First(p => p.ParameterType == typeof(Type)) + .GetCustomAttributes().Any(); +} diff --git a/src/NServiceBus.Core.Tests/Transports/Learning/LearningTransportDispatcherTests.cs b/src/NServiceBus.Core.Tests/Transports/Learning/LearningTransportDispatcherTests.cs index 9d990e1b31d..b76690d2b07 100644 --- a/src/NServiceBus.Core.Tests/Transports/Learning/LearningTransportDispatcherTests.cs +++ b/src/NServiceBus.Core.Tests/Transports/Learning/LearningTransportDispatcherTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using NServiceBus.Routing; using NUnit.Framework; @@ -10,6 +11,107 @@ public class LearningTransportDispatcherTests { + [Test] + public async Task Should_use_enclosed_message_hierarchy_for_multicast_subscribers() + { + var path = Path.Combine(TestContext.CurrentContext.TestDirectory, $"hierarchy-{Guid.NewGuid():N}"); + + try + { + AddSubscriber(path, typeof(ConcreteEvent), "concrete-subscriber"); + AddSubscriber(path, typeof(BaseEvent), "base-subscriber"); + AddSubscriber(path, typeof(IEventContract), "interface-subscriber"); + AddSubscriber(path, typeof(IHeaderOnlyEventContract), "header-only-subscriber"); + AddSubscriber(path, typeof(IReflectedOnlyEventContract), "reflected-only-subscriber"); + AddSubscriber(path, typeof(IEvent), "marker-interface-subscriber"); + + var hierarchy = string.Join(';', new[] { typeof(ConcreteEvent), typeof(BaseEvent), typeof(IEventContract), typeof(IHeaderOnlyEventContract), typeof(IEvent) } + .Select(static type => type.AssemblyQualifiedName)); + var message = new OutgoingMessage("id", new Dictionary { [Headers.EnclosedMessageTypes] = hierarchy }, ReadOnlyMemory.Empty); + var operation = new TransportOperation(message, new MulticastAddressTag(typeof(ConcreteEvent))); + var dispatcher = new LearningTransportDispatcher(path, 64); + + await dispatcher.Dispatch(new TransportOperations(operation), new TransportTransaction()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(HasDispatchedMessage(path, "concrete-subscriber"), Is.True); + Assert.That(HasDispatchedMessage(path, "base-subscriber"), Is.True); + Assert.That(HasDispatchedMessage(path, "interface-subscriber"), Is.True); + Assert.That(HasDispatchedMessage(path, "header-only-subscriber"), Is.True); + Assert.That(HasDispatchedMessage(path, "reflected-only-subscriber"), Is.False); + Assert.That(HasDispatchedMessage(path, "marker-interface-subscriber"), Is.False); + } + } + finally + { + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + } + } + + [Test] + public async Task Should_parse_generic_type_names_from_enclosed_message_hierarchy() + { + var path = Path.Combine(TestContext.CurrentContext.TestDirectory, $"generic-hierarchy-{Guid.NewGuid():N}"); + + try + { + var eventType = typeof(GenericEvent); + AddSubscriber(path, eventType, "generic-subscriber"); + + var message = new OutgoingMessage("id", new Dictionary { [Headers.EnclosedMessageTypes] = eventType.AssemblyQualifiedName! }, ReadOnlyMemory.Empty); + var operation = new TransportOperation(message, new MulticastAddressTag(eventType)); + var dispatcher = new LearningTransportDispatcher(path, 64); + + await dispatcher.Dispatch(new TransportOperations(operation), new TransportTransaction()); + + Assert.That(HasDispatchedMessage(path, "generic-subscriber"), Is.True); + } + finally + { + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + } + } + + [Test] + public async Task Should_infer_hierarchy_when_enclosed_message_types_header_is_missing() + { + var path = Path.Combine(TestContext.CurrentContext.TestDirectory, $"missing-hierarchy-{Guid.NewGuid():N}"); + + try + { + AddSubscriber(path, typeof(ConcreteEvent), "concrete-subscriber"); + AddSubscriber(path, typeof(BaseEvent), "base-subscriber"); + AddSubscriber(path, typeof(IEventContract), "interface-subscriber"); + + var message = new OutgoingMessage("id", [], ReadOnlyMemory.Empty); + var operation = new TransportOperation(message, new MulticastAddressTag(typeof(ConcreteEvent))); + var dispatcher = new LearningTransportDispatcher(path, 64); + + await dispatcher.Dispatch(new TransportOperations(operation), new TransportTransaction()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(HasDispatchedMessage(path, "concrete-subscriber"), Is.True); + Assert.That(HasDispatchedMessage(path, "base-subscriber"), Is.True); + Assert.That(HasDispatchedMessage(path, "interface-subscriber"), Is.True); + } + } + finally + { + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + } + } + [Test] public async Task Should_throw_for_size_above_threshold() { @@ -25,6 +127,25 @@ public async Task Should_throw_for_size_above_threshold() Assert.That(ex.Message, Does.Contain("The total size of the 'TestMessage' message")); } + static void AddSubscriber(string path, Type eventType, string subscriber) + { + var eventDirectory = Path.Combine(path, ".events", eventType.FullName!); + Directory.CreateDirectory(eventDirectory); + File.WriteAllText(Path.Combine(eventDirectory, $"{subscriber}.subscription"), subscriber); + } + + static bool HasDispatchedMessage(string path, string subscriber) => + Directory.Exists(Path.Combine(path, subscriber)) && Directory.EnumerateFiles(Path.Combine(path, subscriber), "*.metadata.txt").Any(); + const int MessageSizeLimit = (64 * 1024) - headerSize; const int headerSize = 57; -} \ No newline at end of file + + sealed class ConcreteEvent : BaseEvent, IEventContract, IReflectedOnlyEventContract; + class BaseEvent : IEvent; + interface IEventContract : IEvent; + interface IHeaderOnlyEventContract : IEvent; + interface IReflectedOnlyEventContract : IEvent; +} + +sealed class GenericEvent : IEvent; +sealed class NestedEvent; \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/TypedMessageInstanceOverloadsTests.cs b/src/NServiceBus.Core.Tests/TypedMessageInstanceOverloadsTests.cs new file mode 100644 index 00000000000..b72c5522885 --- /dev/null +++ b/src/NServiceBus.Core.Tests/TypedMessageInstanceOverloadsTests.cs @@ -0,0 +1,935 @@ +namespace NServiceBus.Core.Tests; + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using NServiceBus.Testing; +using NUnit.Framework; + +[TestFixture] +public class TypedMessageInstanceOverloadsTests +{ + [Test] + public async Task Send_object_variable_via_IMessageSession_uses_object_overload() + { + var session = new TrackingMessageSession(); + var message = (object)new MyMessage(); + await session.Send(message); + Assert.That(session.SendObjectCount, Is.EqualTo(1)); + Assert.That(session.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task Send_typed_variable_via_IMessageSession_uses_object_overload() + { + var session = new TrackingMessageSession(); + var message = new MyMessage(); + await session.Send(message); + Assert.That(session.SendObjectCount, Is.EqualTo(1)); + Assert.That(session.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task Send_interface_typed_variable_via_IMessageSession_uses_object_overload() + { + var session = new TrackingMessageSession(); + var message = (IMyMessage)new MyMessage(); + await session.Send(message); + Assert.That(session.SendObjectCount, Is.EqualTo(1)); + Assert.That(session.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task Send_explicit_generic_call_via_IMessageSession_uses_generic_overload() + { + var session = new TrackingMessageSession(); + var message = new MyMessage(); + await session.Send(message); + Assert.That(session.SendObjectCount, Is.EqualTo(0)); + Assert.That(session.SendGenericCount, Is.EqualTo(1)); + } + + [Test] + public async Task Send_with_destination_object_variable_via_IMessageSession_uses_object_overload() + { + var session = new TrackingMessageSession(); + var message = (object)new MyMessage(); + await session.Send("destination", message); + Assert.That(session.SendObjectCount, Is.EqualTo(1)); + Assert.That(session.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task Send_with_destination_typed_variable_via_IMessageSession_uses_object_overload() + { + var session = new TrackingMessageSession(); + var message = new MyMessage(); + await session.Send("destination", message); + Assert.That(session.SendObjectCount, Is.EqualTo(1)); + Assert.That(session.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task SendLocal_object_variable_via_IMessageSession_uses_object_overload() + { + var session = new TrackingMessageSession(); + var message = (object)new MyMessage(); + await session.SendLocal(message); + Assert.That(session.SendObjectCount, Is.EqualTo(1)); + Assert.That(session.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task SendLocal_typed_variable_via_IMessageSession_uses_object_overload() + { + var session = new TrackingMessageSession(); + var message = new MyMessage(); + await session.SendLocal(message); + Assert.That(session.SendObjectCount, Is.EqualTo(1)); + Assert.That(session.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task Publish_object_variable_via_IMessageSession_uses_object_overload() + { + var session = new TrackingMessageSession(); + var message = (object)new MyMessage(); + await session.Publish(message); + Assert.That(session.PublishObjectCount, Is.EqualTo(1)); + Assert.That(session.PublishGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task Publish_typed_variable_via_IMessageSession_uses_object_overload() + { + var session = new TrackingMessageSession(); + var message = new MyMessage(); + await session.Publish(message); + Assert.That(session.PublishObjectCount, Is.EqualTo(1)); + Assert.That(session.PublishGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task Publish_interface_typed_variable_via_IMessageSession_uses_object_overload() + { + var session = new TrackingMessageSession(); + var message = (IMyMessage)new MyMessage(); + await session.Publish(message); + Assert.That(session.PublishObjectCount, Is.EqualTo(1)); + Assert.That(session.PublishGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task Publish_explicit_generic_call_via_IMessageSession_uses_generic_overload() + { + var session = new TrackingMessageSession(); + var message = new MyMessage(); + await session.Publish(message); + Assert.That(session.PublishObjectCount, Is.EqualTo(0)); + Assert.That(session.PublishGenericCount, Is.EqualTo(1)); + } + + [Test] + public async Task Reply_object_variable_via_IMessageProcessingContext_uses_object_overload() + { + var context = new TrackingMessageProcessingContext(); + var message = (object)new MyMessage(); + await context.Reply(message); + Assert.That(context.ReplyObjectCount, Is.EqualTo(1)); + Assert.That(context.ReplyGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task Reply_typed_variable_via_IMessageProcessingContext_uses_object_overload() + { + var context = new TrackingMessageProcessingContext(); + var message = new MyMessage(); + await context.Reply(message); + Assert.That(context.ReplyObjectCount, Is.EqualTo(1)); + Assert.That(context.ReplyGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task Reply_interface_typed_variable_via_IMessageProcessingContext_uses_object_overload() + { + var context = new TrackingMessageProcessingContext(); + var message = (IMyMessage)new MyMessage(); + await context.Reply(message); + Assert.That(context.ReplyObjectCount, Is.EqualTo(1)); + Assert.That(context.ReplyGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task Reply_explicit_generic_call_via_IMessageProcessingContext_uses_generic_overload() + { + var context = new TrackingMessageProcessingContext(); + var message = new MyMessage(); + await context.Reply(message); + Assert.That(context.ReplyObjectCount, Is.EqualTo(0)); + Assert.That(context.ReplyGenericCount, Is.EqualTo(1)); + } + + [Test] + public async Task PipelineContext_Send_object_variable_via_IPipelineContext_uses_object_overload() + { + var context = new TrackingPipelineContext(); + var message = (object)new MyMessage(); + await context.Send(message); + Assert.That(context.SendObjectCount, Is.EqualTo(1)); + Assert.That(context.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_Send_typed_variable_via_IPipelineContext_uses_object_overload() + { + var context = new TrackingPipelineContext(); + var message = new MyMessage(); + await context.Send(message); + Assert.That(context.SendObjectCount, Is.EqualTo(1)); + Assert.That(context.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_Send_explicit_generic_call_via_IPipelineContext_uses_generic_overload() + { + var context = new TrackingPipelineContext(); + var message = new MyMessage(); + await context.Send(message); + Assert.That(context.SendObjectCount, Is.EqualTo(0)); + Assert.That(context.SendGenericCount, Is.EqualTo(1)); + } + + [Test] + public async Task PipelineContext_Publish_object_variable_via_IPipelineContext_uses_object_overload() + { + var context = new TrackingPipelineContext(); + var message = (object)new MyMessage(); + await context.Publish(message); + Assert.That(context.PublishObjectCount, Is.EqualTo(1)); + Assert.That(context.PublishGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_Publish_typed_variable_via_IPipelineContext_uses_object_overload() + { + var context = new TrackingPipelineContext(); + var message = new MyMessage(); + await context.Publish(message); + Assert.That(context.PublishObjectCount, Is.EqualTo(1)); + Assert.That(context.PublishGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_Publish_explicit_generic_call_via_IPipelineContext_uses_generic_overload() + { + var context = new TrackingPipelineContext(); + var message = new MyMessage(); + await context.Publish(message); + Assert.That(context.PublishObjectCount, Is.EqualTo(0)); + Assert.That(context.PublishGenericCount, Is.EqualTo(1)); + } + + [Test] + public async Task PipelineContext_SendLocal_object_variable_via_IPipelineContext_uses_object_overload() + { + var context = new TrackingPipelineContext(); + var message = (object)new MyMessage(); + await context.SendLocal(message); + Assert.That(context.SendObjectCount, Is.EqualTo(1)); + Assert.That(context.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_SendLocal_typed_variable_via_IPipelineContext_uses_object_overload() + { + var context = new TrackingPipelineContext(); + var message = new MyMessage(); + await context.SendLocal(message); + Assert.That(context.SendObjectCount, Is.EqualTo(1)); + Assert.That(context.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_Send_with_destination_object_variable_via_IPipelineContext_uses_object_overload() + { + var context = new TrackingPipelineContext(); + var message = (object)new MyMessage(); + await context.Send("destination", message); + Assert.That(context.SendObjectCount, Is.EqualTo(1)); + Assert.That(context.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_Send_with_destination_typed_variable_via_IPipelineContext_uses_object_overload() + { + var context = new TrackingPipelineContext(); + var message = new MyMessage(); + await context.Send("destination", message); + Assert.That(context.SendObjectCount, Is.EqualTo(1)); + Assert.That(context.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public void Testable_outgoing_context_ordinary_call_uses_runtime_type() + { + var context = new TestableOutgoingLogicalMessageContext(); + var message = (IMyMessage)new MyMessage(); + + context.UpdateMessage(message); + + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(MyMessage))); + } + + [Test] + public void Testable_outgoing_context_explicit_generic_call_uses_specified_type() + { + var context = new TestableOutgoingLogicalMessageContext(); + var message = new MyMessage(); + + context.UpdateMessage(message); + + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + } + + [Test] + public void Testable_outgoing_context_explicit_type_validates_declared_type() + { + var context = new TestableOutgoingLogicalMessageContext(); + object message = new MyMessage(); + + var ex = Assert.Throws(() => context.UpdateMessage(message, typeof(string))); + Assert.That(ex!.ParamName, Is.EqualTo("message")); + } + + [Test] + public void Testable_outgoing_context_explicit_type_rejects_null_instance() + { + var context = new TestableOutgoingLogicalMessageContext(); + + Assert.Throws(() => context.UpdateMessage(null!, typeof(IMyMessage))); + } + + [Test] + public void Testable_outgoing_context_explicit_type_rejects_null_message_type() + { + var context = new TestableOutgoingLogicalMessageContext(); + var message = new MyMessage(); + + Assert.Throws(() => context.UpdateMessage(message, null!)); + } + + [Test] + public void Testable_outgoing_context_explicit_type_preserves_declared_type_and_instance() + { + var context = new TestableOutgoingLogicalMessageContext(); + object message = new MyMessage(); + + context.UpdateMessage(message, typeof(IMyMessage)); + + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + Assert.That(context.Message.Instance, Is.SameAs(message)); + } + + [Test] + public async Task Default_interface_fallback_Send_uses_object_overload() + { + var legacy = new LegacyMessageSession(); + IMessageSession session = legacy; + var message = new MyMessage(); + await session.Send(message, new SendOptions()); + Assert.That(legacy.SendObjectCount, Is.EqualTo(1)); + } + + [Test] + public async Task Default_interface_fallback_Publish_uses_object_overload() + { + var legacy = new LegacyMessageSession(); + IMessageSession session = legacy; + var message = new MyMessage(); + await session.Publish(message, new PublishOptions()); + Assert.That(legacy.PublishObjectCount, Is.EqualTo(1)); + } + + [Test] + public async Task Default_interface_fallback_Reply_uses_object_overload() + { + var legacy = new LegacyMessageProcessingContext(); + IMessageProcessingContext context = legacy; + var message = new MyMessage(); + await context.Reply(message, new ReplyOptions()); + Assert.That(legacy.ReplyObjectCount, Is.EqualTo(1)); + } + + [Test] + public async Task Send_explicit_type_via_IMessageSession_uses_explicit_type_overload() + { + var session = new TrackingMessageSession(); + object message = new MyMessage(); + await session.Send(message, typeof(IMyMessage)); + Assert.That(session.SendExplicitTypeCount, Is.EqualTo(1)); + Assert.That(session.SendObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task Send_explicit_type_with_options_via_IMessageSession_uses_explicit_type_overload() + { + var session = new TrackingMessageSession(); + object message = new MyMessage(); + await session.Send(message, typeof(IMyMessage), new SendOptions()); + Assert.That(session.SendExplicitTypeCount, Is.EqualTo(1)); + Assert.That(session.SendObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task Send_with_destination_explicit_type_via_IMessageSession_uses_explicit_type_overload() + { + var session = new TrackingMessageSession(); + object message = new MyMessage(); + await session.Send("destination", message, typeof(IMyMessage)); + Assert.That(session.SendExplicitTypeCount, Is.EqualTo(1)); + Assert.That(session.SendObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task SendLocal_explicit_type_via_IMessageSession_uses_explicit_type_overload() + { + var session = new TrackingMessageSession(); + object message = new MyMessage(); + await session.SendLocal(message, typeof(IMyMessage)); + Assert.That(session.SendExplicitTypeCount, Is.EqualTo(1)); + Assert.That(session.SendObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task Publish_explicit_type_via_IMessageSession_uses_explicit_type_overload() + { + var session = new TrackingMessageSession(); + object message = new MyMessage(); + await session.Publish(message, typeof(IMyMessage)); + Assert.That(session.PublishExplicitTypeCount, Is.EqualTo(1)); + Assert.That(session.PublishObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task Publish_explicit_type_with_options_via_IMessageSession_uses_explicit_type_overload() + { + var session = new TrackingMessageSession(); + object message = new MyMessage(); + await session.Publish(message, typeof(IMyMessage), new PublishOptions()); + Assert.That(session.PublishExplicitTypeCount, Is.EqualTo(1)); + Assert.That(session.PublishObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task Reply_explicit_type_via_IMessageProcessingContext_uses_explicit_type_overload() + { + var context = new TrackingMessageProcessingContext(); + object message = new MyMessage(); + await context.Reply(message, typeof(IMyMessage)); + Assert.That(context.ReplyExplicitTypeCount, Is.EqualTo(1)); + Assert.That(context.ReplyObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task Reply_explicit_type_with_options_via_IMessageProcessingContext_uses_explicit_type_overload() + { + var context = new TrackingMessageProcessingContext(); + object message = new MyMessage(); + await context.Reply(message, typeof(IMyMessage), new ReplyOptions()); + Assert.That(context.ReplyExplicitTypeCount, Is.EqualTo(1)); + Assert.That(context.ReplyObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_Send_explicit_type_via_IPipelineContext_uses_explicit_type_overload() + { + var context = new TrackingPipelineContext(); + object message = new MyMessage(); + await context.Send(message, typeof(IMyMessage)); + Assert.That(context.SendExplicitTypeCount, Is.EqualTo(1)); + Assert.That(context.SendObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_Send_explicit_type_with_options_via_IPipelineContext_uses_explicit_type_overload() + { + var context = new TrackingPipelineContext(); + object message = new MyMessage(); + await context.Send(message, typeof(IMyMessage), new SendOptions()); + Assert.That(context.SendExplicitTypeCount, Is.EqualTo(1)); + Assert.That(context.SendObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_Send_with_destination_explicit_type_via_IPipelineContext_uses_explicit_type_overload() + { + var context = new TrackingPipelineContext(); + object message = new MyMessage(); + await context.Send("destination", message, typeof(IMyMessage)); + Assert.That(context.SendExplicitTypeCount, Is.EqualTo(1)); + Assert.That(context.SendObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_SendLocal_explicit_type_via_IPipelineContext_uses_explicit_type_overload() + { + var context = new TrackingPipelineContext(); + object message = new MyMessage(); + await context.SendLocal(message, typeof(IMyMessage)); + Assert.That(context.SendExplicitTypeCount, Is.EqualTo(1)); + Assert.That(context.SendObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_Publish_explicit_type_via_IPipelineContext_uses_explicit_type_overload() + { + var context = new TrackingPipelineContext(); + object message = new MyMessage(); + await context.Publish(message, typeof(IMyMessage)); + Assert.That(context.PublishExplicitTypeCount, Is.EqualTo(1)); + Assert.That(context.PublishObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_Publish_explicit_type_with_options_via_IPipelineContext_uses_explicit_type_overload() + { + var context = new TrackingPipelineContext(); + object message = new MyMessage(); + await context.Publish(message, typeof(IMyMessage), new PublishOptions()); + Assert.That(context.PublishExplicitTypeCount, Is.EqualTo(1)); + Assert.That(context.PublishObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task Default_interface_fallback_Send_explicit_type_uses_object_overload() + { + var legacy = new LegacyMessageSession(); + IMessageSession session = legacy; + object message = new MyMessage(); + await session.Send(message, typeof(IMyMessage), new SendOptions()); + Assert.That(legacy.SendObjectCount, Is.EqualTo(1)); + } + + [Test] + public async Task Default_interface_fallback_Publish_explicit_type_uses_object_overload() + { + var legacy = new LegacyMessageSession(); + IMessageSession session = legacy; + object message = new MyMessage(); + await session.Publish(message, typeof(IMyMessage), new PublishOptions()); + Assert.That(legacy.PublishObjectCount, Is.EqualTo(1)); + } + + [Test] + public async Task Default_interface_fallback_Reply_explicit_type_uses_object_overload() + { + var legacy = new LegacyMessageProcessingContext(); + IMessageProcessingContext context = legacy; + object message = new MyMessage(); + await context.Reply(message, typeof(IMyMessage), new ReplyOptions()); + Assert.That(legacy.ReplyObjectCount, Is.EqualTo(1)); + } + + [Test] + public void Send_explicit_type_with_unrelated_type_throws() + { + var session = new TestableMessageSession(); + object message = new MyMessage(); + var ex = Assert.ThrowsAsync(async () => + await session.Send(message, typeof(MyOtherMessage), new SendOptions())); + Assert.That(ex!.ParamName, Is.EqualTo("message")); + } + + [Test] + public void Send_explicit_type_with_null_type_throws() + { + var session = new TestableMessageSession(); + object message = new MyMessage(); + Assert.ThrowsAsync(async () => + await session.Send(message, null!, new SendOptions())); + } + + [Test] + public void Send_explicit_type_with_null_message_throws() + { + var session = new TestableMessageSession(); + Assert.ThrowsAsync(async () => + await session.Send(null!, typeof(IMyMessage), new SendOptions())); + } + + [Test] + public void Default_interface_fallback_Send_explicit_type_with_null_message_throws() + { + var legacy = new LegacyMessageSession(); + IMessageSession session = legacy; + Assert.ThrowsAsync(async () => + await session.Send(null!, typeof(IMyMessage), new SendOptions())); + } + + [Test] + public void Publish_explicit_type_with_unrelated_type_throws() + { + var session = new TestableMessageSession(); + object message = new MyMessage(); + var ex = Assert.ThrowsAsync(async () => + await session.Publish(message, typeof(MyOtherMessage), new PublishOptions())); + Assert.That(ex!.ParamName, Is.EqualTo("message")); + } + + [Test] + public void Reply_explicit_type_with_unrelated_type_throws() + { + var context = new TestableMessageProcessingContext(); + object message = new MyMessage(); + var ex = Assert.ThrowsAsync(async () => + await context.Reply(message, typeof(MyOtherMessage), new ReplyOptions())); + Assert.That(ex!.ParamName, Is.EqualTo("message")); + } + + [Test] + public void PipelineContext_Send_explicit_type_with_unrelated_type_throws() + { + var context = new TestablePipelineContext(); + object message = new MyMessage(); + var ex = Assert.ThrowsAsync(async () => + await context.Send(message, typeof(MyOtherMessage), new SendOptions())); + Assert.That(ex!.ParamName, Is.EqualTo("message")); + } + + [Test] + public void PipelineContext_Publish_explicit_type_with_unrelated_type_throws() + { + var context = new TestablePipelineContext(); + object message = new MyMessage(); + var ex = Assert.ThrowsAsync(async () => + await context.Publish(message, typeof(MyOtherMessage), new PublishOptions())); + Assert.That(ex!.ParamName, Is.EqualTo("message")); + } + + [Test] + public void Default_interface_fallback_Send_explicit_type_with_unrelated_type_throws() + { + var legacy = new LegacyMessageSession(); + IMessageSession session = legacy; + object message = new MyMessage(); + Assert.ThrowsAsync(async () => + await session.Send(message, typeof(MyOtherMessage), new SendOptions())); + } + + [Test] + public void Default_interface_fallback_Publish_explicit_type_with_unrelated_type_throws() + { + var legacy = new LegacyMessageSession(); + IMessageSession session = legacy; + object message = new MyMessage(); + Assert.ThrowsAsync(async () => + await session.Publish(message, typeof(MyOtherMessage), new PublishOptions())); + } + + [Test] + public void Default_interface_fallback_Reply_explicit_type_with_unrelated_type_throws() + { + var legacy = new LegacyMessageProcessingContext(); + IMessageProcessingContext context = legacy; + object message = new MyMessage(); + Assert.ThrowsAsync(async () => + await context.Reply(message, typeof(MyOtherMessage), new ReplyOptions())); + } + + public interface IMyMessage + { + } + + public class MyMessage : IMyMessage + { + } + + public class MyOtherMessage + { + } + + class TrackingMessageSession : TestableMessageSession + { + public int SendObjectCount; + public int SendGenericCount; + public int SendExplicitTypeCount; + public int PublishObjectCount; + public int PublishGenericCount; + public int PublishExplicitTypeCount; + + public override Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) + { + if (!trackingGenericSend && !trackingExplicitTypeSend) + { + SendObjectCount++; + } + + return base.Send(message, options, cancellationToken); + } + + public override Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, SendOptions options, CancellationToken cancellationToken = default) + { + SendGenericCount++; + trackingGenericSend = true; + try + { + return base.Send(message, options, cancellationToken); + } + finally + { + trackingGenericSend = false; + } + } + + public override Task Send(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, SendOptions options, CancellationToken cancellationToken = default) + { + SendExplicitTypeCount++; + trackingExplicitTypeSend = true; + try + { + return base.Send(message, messageType, options, cancellationToken); + } + finally + { + trackingExplicitTypeSend = false; + } + } + + public override Task Publish(object message, PublishOptions options, CancellationToken cancellationToken = default) + { + if (!trackingGenericPublish && !trackingExplicitTypePublish) + { + PublishObjectCount++; + } + + return base.Publish(message, options, cancellationToken); + } + + public override Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, PublishOptions options, CancellationToken cancellationToken = default) + { + PublishGenericCount++; + trackingGenericPublish = true; + try + { + return base.Publish(message, options, cancellationToken); + } + finally + { + trackingGenericPublish = false; + } + } + + public override Task Publish(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, PublishOptions options, CancellationToken cancellationToken = default) + { + PublishExplicitTypeCount++; + trackingExplicitTypePublish = true; + try + { + return base.Publish(message, messageType, options, cancellationToken); + } + finally + { + trackingExplicitTypePublish = false; + } + } + + bool trackingGenericSend; + bool trackingGenericPublish; + bool trackingExplicitTypeSend; + bool trackingExplicitTypePublish; + } + + class TrackingPipelineContext : TestablePipelineContext + { + public int SendObjectCount; + public int SendGenericCount; + public int SendExplicitTypeCount; + public int PublishObjectCount; + public int PublishGenericCount; + public int PublishExplicitTypeCount; + + public override Task Send(object message, SendOptions options) + { + if (!trackingGenericSend && !trackingExplicitTypeSend) + { + SendObjectCount++; + } + + return base.Send(message, options); + } + + public override Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, SendOptions options) + { + SendGenericCount++; + trackingGenericSend = true; + try + { + return base.Send(message, options); + } + finally + { + trackingGenericSend = false; + } + } + + public override Task Send(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, SendOptions options) + { + SendExplicitTypeCount++; + trackingExplicitTypeSend = true; + try + { + return base.Send(message, messageType, options); + } + finally + { + trackingExplicitTypeSend = false; + } + } + + public override Task Publish(object message, PublishOptions options) + { + if (!trackingGenericPublish && !trackingExplicitTypePublish) + { + PublishObjectCount++; + } + + return base.Publish(message, options); + } + + public override Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, PublishOptions options) + { + PublishGenericCount++; + trackingGenericPublish = true; + try + { + return base.Publish(message, options); + } + finally + { + trackingGenericPublish = false; + } + } + + public override Task Publish(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, PublishOptions options) + { + PublishExplicitTypeCount++; + trackingExplicitTypePublish = true; + try + { + return base.Publish(message, messageType, options); + } + finally + { + trackingExplicitTypePublish = false; + } + } + + bool trackingGenericSend; + bool trackingGenericPublish; + bool trackingExplicitTypeSend; + bool trackingExplicitTypePublish; + } + + class TrackingMessageProcessingContext : TestableMessageProcessingContext + { + public int ReplyObjectCount; + public int ReplyGenericCount; + public int ReplyExplicitTypeCount; + + public override Task Reply(object message, ReplyOptions options) + { + if (!trackingGenericReply && !trackingExplicitTypeReply) + { + ReplyObjectCount++; + } + + return base.Reply(message, options); + } + + public override Task Reply<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, ReplyOptions options) + { + ReplyGenericCount++; + trackingGenericReply = true; + try + { + return base.Reply(message, options); + } + finally + { + trackingGenericReply = false; + } + } + + public override Task Reply(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, ReplyOptions options) + { + ReplyExplicitTypeCount++; + trackingExplicitTypeReply = true; + try + { + return base.Reply(message, messageType, options); + } + finally + { + trackingExplicitTypeReply = false; + } + } + + bool trackingGenericReply; + bool trackingExplicitTypeReply; + } + + class LegacyMessageSession : IMessageSession + { + public int SendObjectCount; + public int PublishObjectCount; + + public Task Send(object message, SendOptions sendOptions, CancellationToken cancellationToken = default) + { + SendObjectCount++; + return Task.CompletedTask; + } + + public Task Send(Action messageConstructor, SendOptions sendOptions, CancellationToken cancellationToken = default) + => throw new NotImplementedException(); + + public Task Publish(object message, PublishOptions publishOptions, CancellationToken cancellationToken = default) + { + PublishObjectCount++; + return Task.CompletedTask; + } + + public Task Publish(Action messageConstructor, PublishOptions publishOptions, CancellationToken cancellationToken = default) + => throw new NotImplementedException(); + + public Task Subscribe(Type eventType, SubscribeOptions subscribeOptions, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions unsubscribeOptions, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class LegacyMessageProcessingContext : IMessageProcessingContext + { + public int ReplyObjectCount; + + CancellationToken ICancellableContext.CancellationToken => default; + public Extensibility.ContextBag Extensions { get; set; } = new Extensibility.ContextBag(); + + public Task Reply(object message, ReplyOptions options) + { + ReplyObjectCount++; + return Task.CompletedTask; + } + + public Task Reply(Action messageConstructor, ReplyOptions options) => throw new NotImplementedException(); + + public string MessageId => throw new NotImplementedException(); + public string ReplyToAddress => throw new NotImplementedException(); + public IReadOnlyDictionary MessageHeaders => throw new NotImplementedException(); + public Task ForwardCurrentMessageTo(string destination) => throw new NotImplementedException(); + + public Task Send(object message, SendOptions options) => throw new NotImplementedException(); + public Task Send(Action messageConstructor, SendOptions options) => throw new NotImplementedException(); + public Task Publish(object message, PublishOptions options) => throw new NotImplementedException(); + public Task Publish(Action messageConstructor, PublishOptions publishOptions) => throw new NotImplementedException(); + } +} diff --git a/src/NServiceBus.Core.Tests/Unicast/MessageOperationsTests.cs b/src/NServiceBus.Core.Tests/Unicast/MessageOperationsTests.cs index 64067a843df..481720e95e0 100644 --- a/src/NServiceBus.Core.Tests/Unicast/MessageOperationsTests.cs +++ b/src/NServiceBus.Core.Tests/Unicast/MessageOperationsTests.cs @@ -76,6 +76,47 @@ public async Task When_sending_should_clone_headersAsync() Assert.That(optionsHeaders["header1"], Is.EqualTo("header1 value")); } + [Test] + public async Task When_sending_typed_instance_should_preserve_declared_type_and_instance() + { + var messageOperations = new TestableMessageOperations(); + var message = new MyMessage(); + + await messageOperations.Send(new FakeRootContext(), message, new SendOptions()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(messageOperations.SendPipeline.LastContext.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + Assert.That(messageOperations.SendPipeline.LastContext.Message.Instance, Is.SameAs(message)); + } + } + + [Test] + public async Task When_sending_instance_with_explicit_type_should_preserve_declared_type_and_instance() + { + var messageOperations = new TestableMessageOperations(); + object message = new MyMessage(); + + await messageOperations.Send(new FakeRootContext(), message, typeof(IMyMessage), new SendOptions()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(messageOperations.SendPipeline.LastContext.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + Assert.That(messageOperations.SendPipeline.LastContext.Message.Instance, Is.SameAs(message)); + } + } + + [Test] + public async Task When_sending_instance_without_declared_type_should_use_runtime_type() + { + var messageOperations = new TestableMessageOperations(); + object message = new MyMessage(); + + await messageOperations.Send(new FakeRootContext(), message, new SendOptions()); + + Assert.That(messageOperations.SendPipeline.LastContext.Message.MessageType, Is.EqualTo(typeof(MyMessage))); + } + [Test] public async Task When_replying_message_interface_should_set_interface_as_message_typeAsync() { @@ -145,6 +186,47 @@ public async Task When_replying_should_clone_headersAsync() Assert.That(optionsHeaders["header1"], Is.EqualTo("header1 value")); } + [Test] + public async Task When_replying_typed_instance_should_preserve_declared_type_and_instance() + { + var messageOperations = new TestableMessageOperations(); + var message = new MyMessage(); + + await messageOperations.Reply(new FakeRootContext(), message, new ReplyOptions()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(messageOperations.ReplyPipeline.LastContext.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + Assert.That(messageOperations.ReplyPipeline.LastContext.Message.Instance, Is.SameAs(message)); + } + } + + [Test] + public async Task When_replying_instance_with_explicit_type_should_preserve_declared_type_and_instance() + { + var messageOperations = new TestableMessageOperations(); + object message = new MyMessage(); + + await messageOperations.Reply(new FakeRootContext(), message, typeof(IMyMessage), new ReplyOptions()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(messageOperations.ReplyPipeline.LastContext.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + Assert.That(messageOperations.ReplyPipeline.LastContext.Message.Instance, Is.SameAs(message)); + } + } + + [Test] + public async Task When_replying_instance_without_declared_type_should_use_runtime_type() + { + var messageOperations = new TestableMessageOperations(); + object message = new MyMessage(); + + await messageOperations.Reply(new FakeRootContext(), message, new ReplyOptions()); + + Assert.That(messageOperations.ReplyPipeline.LastContext.Message.MessageType, Is.EqualTo(typeof(MyMessage))); + } + [Test] public async Task When_publishing_event_interface_should_set_interface_as_message_typeAsync() { @@ -214,11 +296,52 @@ public async Task When_publishing_should_clone_headers() Assert.That(optionsHeaders["header1"], Is.EqualTo("header1 value")); } + [Test] + public async Task When_publishing_typed_instance_should_preserve_declared_type_and_instance() + { + var messageOperations = new TestableMessageOperations(); + var message = new MyMessage(); + + await messageOperations.Publish(new FakeRootContext(), message, new PublishOptions()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(messageOperations.PublishPipeline.LastContext.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + Assert.That(messageOperations.PublishPipeline.LastContext.Message.Instance, Is.SameAs(message)); + } + } + + [Test] + public async Task When_publishing_instance_with_explicit_type_should_preserve_declared_type_and_instance() + { + var messageOperations = new TestableMessageOperations(); + object message = new MyMessage(); + + await messageOperations.Publish(new FakeRootContext(), message, typeof(IMyMessage), new PublishOptions()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(messageOperations.PublishPipeline.LastContext.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + Assert.That(messageOperations.PublishPipeline.LastContext.Message.Instance, Is.SameAs(message)); + } + } + + [Test] + public async Task When_publishing_instance_without_declared_type_should_use_runtime_type() + { + var messageOperations = new TestableMessageOperations(); + object message = new MyMessage(); + + await messageOperations.Publish(new FakeRootContext(), message, new PublishOptions()); + + Assert.That(messageOperations.PublishPipeline.LastContext.Message.MessageType, Is.EqualTo(typeof(MyMessage))); + } + public interface IMyMessage { } - class MyMessage + class MyMessage : IMyMessage { } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Conventions/NServiceBusMarkerInterfaceConvention.cs b/src/NServiceBus.Core/Conventions/NServiceBusMarkerInterfaceConvention.cs index e3042278606..845c99d9f80 100644 --- a/src/NServiceBus.Core/Conventions/NServiceBusMarkerInterfaceConvention.cs +++ b/src/NServiceBus.Core/Conventions/NServiceBusMarkerInterfaceConvention.cs @@ -16,23 +16,24 @@ public class NServiceBusMarkerInterfaceConvention : IMessageConvention public bool IsCommandType(Type type) { ArgumentNullException.ThrowIfNull(type); - return typeof(ICommand).IsAssignableFrom(type) && typeof(ICommand) != type; + return typeof(ICommand).IsAssignableFrom(type) && !IsMarkerType(type); } /// public bool IsEventType(Type type) { ArgumentNullException.ThrowIfNull(type); - return typeof(IEvent).IsAssignableFrom(type) && typeof(IEvent) != type; + return typeof(IEvent).IsAssignableFrom(type) && !IsMarkerType(type); } /// public bool IsMessageType(Type type) { ArgumentNullException.ThrowIfNull(type); - return typeof(IMessage).IsAssignableFrom(type) && - typeof(IMessage) != type && - typeof(IEvent) != type && - typeof(ICommand) != type; + return typeof(IMessage).IsAssignableFrom(type) && !IsMarkerType(type); } + + internal static bool IsMarkerType(Type type) => type == typeof(IMessage) || type == typeof(IEvent) || type == typeof(ICommand); + + internal static bool IsMarkerType(string typeName) => typeName == typeof(IMessage).FullName || typeName == typeof(IEvent).FullName || typeName == typeof(ICommand).FullName; } \ No newline at end of file diff --git a/src/NServiceBus.Core/DynamicMemberTypeAccess.cs b/src/NServiceBus.Core/DynamicMemberTypeAccess.cs index 4e3691e05f4..e356e741e92 100644 --- a/src/NServiceBus.Core/DynamicMemberTypeAccess.cs +++ b/src/NServiceBus.Core/DynamicMemberTypeAccess.cs @@ -4,6 +4,11 @@ namespace NServiceBus; static class DynamicMemberTypeAccess { + public const DynamicallyAccessedMemberTypes Message = DynamicallyAccessedMemberTypes.PublicConstructors + | DynamicallyAccessedMemberTypes.NonPublicConstructors + | DynamicallyAccessedMemberTypes.PublicProperties + | DynamicallyAccessedMemberTypes.Interfaces; + public const DynamicallyAccessedMemberTypes SagaData = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties; diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/Host.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/Host.cs index cb6b5ce44cb..9c1108bdc2c 100644 --- a/src/NServiceBus.Core/Hosting/StartupDiagnostics/Host.cs +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/Host.cs @@ -3,62 +3,8 @@ namespace NServiceBus; using System; -using System.Reflection; -using IODirectory = System.IO.Directory; static class Host { - public static string GetOutputDirectory() - { - Assembly? systemWebAssembly = null; - - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) - { - if (assembly.GetName().Name != "System.Web") - { - continue; - } - - systemWebAssembly = assembly; - break; - } - - var httpRuntime = systemWebAssembly?.GetType("System.Web.HttpRuntime"); - var appDomainAppId = httpRuntime?.GetProperty("AppDomainAppId", BindingFlags.Public | BindingFlags.Static); - var result = appDomainAppId?.GetValue(null); - - return result == null ? AppDomain.CurrentDomain.BaseDirectory : DeriveAppDataPath(systemWebAssembly!); - } - - static string DeriveAppDataPath(Assembly systemWebAssembly) - { - var appDataPath = TryMapPath(systemWebAssembly) ?? throw new Exception(GetMapPathError("Failed since MapPath returned null.")); - - if (IODirectory.Exists(appDataPath)) - { - return appDataPath; - } - - throw new Exception(GetMapPathError($"Failed since path returned ({appDataPath}) does not exist. Ensure this directory is created and restart the endpoint.")); - } - - static readonly object[] parameters = ["~/App_Data/"]; - - static string? TryMapPath(Assembly systemWebAssembly) - { - try - { - var hostingEnvironment = systemWebAssembly.GetType("System.Web.Hosting.HostingEnvironment"); - var mapPath = hostingEnvironment?.GetMethod("MapPath", BindingFlags.Static | BindingFlags.Public); - var result = mapPath?.Invoke(null, parameters) as string; - - return result; - } - catch (Exception exception) - { - throw new Exception(GetMapPathError("Failed since MapPath threw an exception."), exception); - } - } - - static string GetMapPathError(string reason) => $"Detected running in a website and attempted to use HostingEnvironment.MapPath(\"~/App_Data/\") to derive the logging path. {reason}"; + public static string GetOutputDirectory() => AppDomain.CurrentDomain.BaseDirectory; } \ No newline at end of file diff --git a/src/NServiceBus.Core/IMessageCreator.cs b/src/NServiceBus.Core/IMessageCreator.cs index b68a1421d4d..898ced73164 100644 --- a/src/NServiceBus.Core/IMessageCreator.cs +++ b/src/NServiceBus.Core/IMessageCreator.cs @@ -10,7 +10,7 @@ namespace NServiceBus; /// public interface IMessageCreator { - internal const DynamicallyAccessedMemberTypes CreatorMembersRequired = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + internal const DynamicallyAccessedMemberTypes CreatorMembersRequired = DynamicMemberTypeAccess.Message; /// /// Creates an instance of the message type T. diff --git a/src/NServiceBus.Core/IMessageProcessingContext.cs b/src/NServiceBus.Core/IMessageProcessingContext.cs index 4da04485752..a957ff97498 100644 --- a/src/NServiceBus.Core/IMessageProcessingContext.cs +++ b/src/NServiceBus.Core/IMessageProcessingContext.cs @@ -3,7 +3,9 @@ namespace NServiceBus; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading.Tasks; +using Particular.Obsoletes; /// /// The context of the currently processed message within the processing pipeline. @@ -30,12 +32,46 @@ public interface IMessageProcessingContext : IPipelineContext /// /// The message to send. /// Options for this reply. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Reply(T, ReplyOptions) or Reply(object, Type, ReplyOptions)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic or explicit Type overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] Task Reply(object message, ReplyOptions options); /// - /// Instantiates a message of type T and performs a regular . + /// Sends the typed message to the endpoint which sent the message currently being handled. /// - /// The type of message, usually an interface. + /// The type used to reply with the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The message to send. + /// Options for this reply. + [OverloadResolutionPriority(-1)] + Task Reply<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, ReplyOptions options) + { + return Reply(message!, typeof(T), options); + } + + /// + /// Sends the message to the endpoint which sent the message currently being handled with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The message to send. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// Options for this reply. + /// or is . + /// is not assignable to . + /// + /// Third-party implementations that inherit this default implementation fall back to the object overload and route by the runtime type of . Override this method to preserve a declared that differs from the runtime type. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = MessageOperations.DefaultInterfaceTrimmingSuppressionJustification)] + Task Reply(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, ReplyOptions options) + { + MessageTypeValidator.Validate(message, messageType); + return Reply(message, options); + } + + /// + /// Instantiates a message of type T and performs a regular Reply. + /// + /// The type used to reply with the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// An action which initializes properties of the message. /// Options for this reply. Task Reply<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, ReplyOptions options); diff --git a/src/NServiceBus.Core/IMessageSession.cs b/src/NServiceBus.Core/IMessageSession.cs index 2852e020b47..d5f3e20d525 100644 --- a/src/NServiceBus.Core/IMessageSession.cs +++ b/src/NServiceBus.Core/IMessageSession.cs @@ -4,8 +4,10 @@ namespace NServiceBus; using System; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using Particular.Obsoletes; /// /// A session which provides basic message operations. @@ -18,12 +20,48 @@ public interface IMessageSession /// The message to send. /// The options for the send. /// A to observe. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Send(T, SendOptions, CancellationToken) or Send(object, Type, SendOptions, CancellationToken)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic or explicit Type overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] Task Send(object message, SendOptions sendOptions, CancellationToken cancellationToken = default); + /// + /// Sends the provided typed message. + /// + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The message to send. + /// The options for the send. + /// A to observe. + [OverloadResolutionPriority(-1)] + Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, SendOptions sendOptions, CancellationToken cancellationToken = default) + { + return Send(message!, typeof(T), sendOptions, cancellationToken); + } + + /// + /// Sends the provided message with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The message to send. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// The options for the send. + /// A to observe. + /// or is . + /// is not assignable to . + /// + /// Third-party implementations that inherit this default implementation fall back to the object overload and route by the runtime type of . Override this method to preserve a declared that differs from the runtime type. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = MessageOperations.DefaultInterfaceTrimmingSuppressionJustification)] + Task Send(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, SendOptions sendOptions, CancellationToken cancellationToken = default) + { + MessageTypeValidator.Validate(message, messageType); + return Send(message, sendOptions, cancellationToken); + } + /// /// Instantiates a message of type T and sends it. /// - /// The type of message, usually an interface. + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// An action which initializes properties of the message. /// The options for the send. /// A to observe. @@ -35,12 +73,48 @@ public interface IMessageSession /// The message to publish. /// The options for the publish. /// A to observe. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Publish(T, PublishOptions, CancellationToken) or Publish(object, Type, PublishOptions, CancellationToken)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic or explicit Type overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] Task Publish(object message, PublishOptions publishOptions, CancellationToken cancellationToken = default); + /// + /// Publishes the provided typed message. + /// + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The message to publish. + /// The options for the publish. + /// A to observe. + [OverloadResolutionPriority(-1)] + Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, PublishOptions publishOptions, CancellationToken cancellationToken = default) + { + return Publish(message!, typeof(T), publishOptions, cancellationToken); + } + + /// + /// Publishes the provided message with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The message to publish. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// The options for the publish. + /// A to observe. + /// or is . + /// is not assignable to . + /// + /// Third-party implementations that inherit this default implementation fall back to the object overload and route by the runtime type of . Override this method to preserve a declared that differs from the runtime type. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = MessageOperations.DefaultInterfaceTrimmingSuppressionJustification)] + Task Publish(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, PublishOptions publishOptions, CancellationToken cancellationToken = default) + { + MessageTypeValidator.Validate(message, messageType); + return Publish(message, publishOptions, cancellationToken); + } + /// /// Instantiates a message of type T and publishes it. /// - /// The type of message, usually an interface. + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// An action which initializes properties of the message. /// Specific options for this event. /// A to observe. diff --git a/src/NServiceBus.Core/IPipelineContext.cs b/src/NServiceBus.Core/IPipelineContext.cs index 69a46eb7789..a071bbd5bb9 100644 --- a/src/NServiceBus.Core/IPipelineContext.cs +++ b/src/NServiceBus.Core/IPipelineContext.cs @@ -2,8 +2,10 @@ namespace NServiceBus; using System; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Extensibility; +using Particular.Obsoletes; /// /// The context for the current message handling pipeline. @@ -15,12 +17,46 @@ public interface IPipelineContext : ICancellableContext, IExtendable /// /// The message to send. /// The options for the send. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Send(T, SendOptions) or Send(object, Type, SendOptions)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic or explicit Type overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] Task Send(object message, SendOptions options); + /// + /// Sends the provided typed message. + /// + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The message to send. + /// The options for the send. + [OverloadResolutionPriority(-1)] + Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, SendOptions options) + { + return Send(message!, typeof(T), options); + } + + /// + /// Sends the provided message with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The message to send. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// The options for the send. + /// or is . + /// is not assignable to . + /// + /// Third-party implementations that inherit this default implementation fall back to the object overload and route by the runtime type of . Override this method to preserve a declared that differs from the runtime type. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = MessageOperations.DefaultInterfaceTrimmingSuppressionJustification)] + Task Send(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, SendOptions options) + { + MessageTypeValidator.Validate(message, messageType); + return Send(message, options); + } + /// /// Instantiates a message of type T and sends it. /// - /// The type of message, usually an interface. + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// An action which initializes properties of the message. /// The options for the send. Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, SendOptions options); @@ -30,12 +66,46 @@ public interface IPipelineContext : ICancellableContext, IExtendable /// /// The message to publish. /// The options for the publish. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Publish(T, PublishOptions) or Publish(object, Type, PublishOptions)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic or explicit Type overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] Task Publish(object message, PublishOptions options); + /// + /// Publishes the provided typed message. + /// + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The message to publish. + /// The options for the publish. + [OverloadResolutionPriority(-1)] + Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, PublishOptions options) + { + return Publish(message!, typeof(T), options); + } + + /// + /// Publishes the provided message with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The message to publish. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// The options for the publish. + /// or is . + /// is not assignable to . + /// + /// Third-party implementations that inherit this default implementation fall back to the object overload and route by the runtime type of . Override this method to preserve a declared that differs from the runtime type. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = MessageOperations.DefaultInterfaceTrimmingSuppressionJustification)] + Task Publish(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, PublishOptions options) + { + MessageTypeValidator.Validate(message, messageType); + return Publish(message, options); + } + /// /// Instantiates a message of type T and publishes it. /// - /// The type of message, usually an interface. + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// An action which initializes properties of the message. /// Specific options for this event. Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, PublishOptions publishOptions); diff --git a/src/NServiceBus.Core/MessageProcessingContextExtensions.cs b/src/NServiceBus.Core/MessageProcessingContextExtensions.cs index 36d6d6951e1..4f1fba59ffd 100644 --- a/src/NServiceBus.Core/MessageProcessingContextExtensions.cs +++ b/src/NServiceBus.Core/MessageProcessingContextExtensions.cs @@ -2,7 +2,9 @@ namespace NServiceBus; using System; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading.Tasks; +using Particular.Obsoletes; /// /// Syntactic sugar for . @@ -14,6 +16,10 @@ public static class MessageProcessingContextExtensions /// /// Object being extended. /// The message to send. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Reply(this IMessageProcessingContext, T)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead, or the overload accepting an explicit messageType when the static type is unavailable.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public static Task Reply(this IMessageProcessingContext context, object message) { ArgumentNullException.ThrowIfNull(context); @@ -22,10 +28,42 @@ public static Task Reply(this IMessageProcessingContext context, object message) return context.Reply(message, new ReplyOptions()); } + /// + /// Sends the typed message to the endpoint which sent the message currently being handled on this thread. + /// + /// The type used to reply with the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// Object being extended. + /// The message to send. + [OverloadResolutionPriority(-1)] + public static Task Reply<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(this IMessageProcessingContext context, T message) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(message); + + return context.Reply(message, new ReplyOptions()); + } + + /// + /// Sends the message with the specified message type to the endpoint which sent the message currently being handled on this thread. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// Object being extended. + /// The message to send. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + public static Task Reply(this IMessageProcessingContext context, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + + return context.Reply(message, messageType, new ReplyOptions()); + } + /// /// Instantiates a message of type T and performs a regular Reply. /// - /// The type of message, usually an interface. + /// The type used to reply with the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// Object being extended. /// An action which initializes properties of the message. public static Task Reply<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IMessageProcessingContext context, Action messageConstructor) diff --git a/src/NServiceBus.Core/MessageSession.cs b/src/NServiceBus.Core/MessageSession.cs index 935825aa707..566200a86a5 100644 --- a/src/NServiceBus.Core/MessageSession.cs +++ b/src/NServiceBus.Core/MessageSession.cs @@ -4,9 +4,11 @@ namespace NServiceBus; using System; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Logging; +using Particular.Obsoletes; class MessageSession : IMessageSession { @@ -79,6 +81,10 @@ CancellationTokenSource CreateOperationLinkedTokenSource(CancellationToken cance } } + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Send(T, SendOptions, CancellationToken) or Send(object, Type, SendOptions, CancellationToken)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic or explicit Type overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public async Task Send(object message, SendOptions sendOptions, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(message); @@ -90,6 +96,30 @@ public async Task Send(object message, SendOptions sendOptions, CancellationToke await messageOperations.Send(CreateContext(linkedTokenSource.Token), message, sendOptions).ConfigureAwait(false); } + [OverloadResolutionPriority(-1)] + public async Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, SendOptions sendOptions, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(sendOptions); + + using var _ = LogManager.BeginSlotScope(loggingSlot); + await WaitUntilInitialized(cancellationToken).ConfigureAwait(false); + using var linkedTokenSource = CreateOperationLinkedTokenSource(cancellationToken); + await messageOperations.Send(CreateContext(linkedTokenSource.Token), message, sendOptions).ConfigureAwait(false); + } + + public async Task Send(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, SendOptions sendOptions, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + ArgumentNullException.ThrowIfNull(sendOptions); + + using var _ = LogManager.BeginSlotScope(loggingSlot); + await WaitUntilInitialized(cancellationToken).ConfigureAwait(false); + using var linkedTokenSource = CreateOperationLinkedTokenSource(cancellationToken); + await messageOperations.Send(CreateContext(linkedTokenSource.Token), message, messageType, sendOptions).ConfigureAwait(false); + } + public async Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, SendOptions sendOptions, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(messageConstructor); @@ -101,6 +131,10 @@ public async Task Send(object message, SendOptions sendOptions, CancellationToke await messageOperations.Send(CreateContext(linkedTokenSource.Token), messageConstructor, sendOptions).ConfigureAwait(false); } + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Publish(T, PublishOptions, CancellationToken) or Publish(object, Type, PublishOptions, CancellationToken)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic or explicit Type overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public async Task Publish(object message, PublishOptions publishOptions, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(message); @@ -112,6 +146,30 @@ public async Task Publish(object message, PublishOptions publishOptions, Cancell await messageOperations.Publish(CreateContext(linkedTokenSource.Token), message, publishOptions).ConfigureAwait(false); } + [OverloadResolutionPriority(-1)] + public async Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, PublishOptions publishOptions, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(publishOptions); + + using var _ = LogManager.BeginSlotScope(loggingSlot); + await WaitUntilInitialized(cancellationToken).ConfigureAwait(false); + using var linkedTokenSource = CreateOperationLinkedTokenSource(cancellationToken); + await messageOperations.Publish(CreateContext(linkedTokenSource.Token), message, publishOptions).ConfigureAwait(false); + } + + public async Task Publish(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, PublishOptions publishOptions, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + ArgumentNullException.ThrowIfNull(publishOptions); + + using var _ = LogManager.BeginSlotScope(loggingSlot); + await WaitUntilInitialized(cancellationToken).ConfigureAwait(false); + using var linkedTokenSource = CreateOperationLinkedTokenSource(cancellationToken); + await messageOperations.Publish(CreateContext(linkedTokenSource.Token), message, messageType, publishOptions).ConfigureAwait(false); + } + public async Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, PublishOptions publishOptions, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(messageConstructor); diff --git a/src/NServiceBus.Core/MessageSessionExtensions.cs b/src/NServiceBus.Core/MessageSessionExtensions.cs index c115ecb96fe..4b7b10b26d1 100644 --- a/src/NServiceBus.Core/MessageSessionExtensions.cs +++ b/src/NServiceBus.Core/MessageSessionExtensions.cs @@ -2,8 +2,10 @@ namespace NServiceBus; using System; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using Particular.Obsoletes; /// /// Syntactic sugar for . @@ -16,6 +18,10 @@ public static class MessageSessionExtensions /// The instance of to use for the action. /// The message to send. /// A to observe. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Send(this IMessageSession, T, CancellationToken)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public static Task Send(this IMessageSession session, object message, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(session); @@ -24,10 +30,44 @@ public static Task Send(this IMessageSession session, object message, Cancellati return session.Send(message, new SendOptions(), cancellationToken); } + /// + /// Sends the provided typed message. + /// + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The instance of to use for the action. + /// The message to send. + /// A to observe. + [OverloadResolutionPriority(-1)] + public static Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(this IMessageSession session, T message, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + ArgumentNullException.ThrowIfNull(message); + + return session.Send(message, new SendOptions(), cancellationToken); + } + + /// + /// Sends the provided message with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The instance of to use for the action. + /// The message to send. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// A to observe. + /// or is . + /// is not assignable to . + public static Task Send(this IMessageSession session, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + + return session.Send(message, messageType, new SendOptions(), cancellationToken); + } + /// /// Instantiates a message of and sends it. /// - /// The type of message, usually an interface. + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// The instance of to use for the action. /// An action which initializes properties of the message. /// A to observe. @@ -49,6 +89,10 @@ public static Task Send(this IMessageSession session, object message, Cancellati /// The address of the destination to which the message will be sent. /// The message to send. /// A to observe. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Send(this IMessageSession, string, T, CancellationToken)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public static Task Send(this IMessageSession session, string destination, object message, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(session); @@ -62,10 +106,56 @@ public static Task Send(this IMessageSession session, string destination, object return session.Send(message, options, cancellationToken); } + /// + /// Sends the typed message to the given destination. + /// + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The instance of to use for the action. + /// The destination to which the message will be sent. + /// The message to send. + /// A to observe. + [OverloadResolutionPriority(-1)] + public static Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(this IMessageSession session, string destination, T message, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + ArgumentException.ThrowIfNullOrWhiteSpace(destination); + ArgumentNullException.ThrowIfNull(message); + + var options = new SendOptions(); + + options.SetDestination(destination); + + return session.Send(message, options, cancellationToken); + } + + /// + /// Sends the message with the specified message type to the given destination. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The instance of to use for the action. + /// The destination to which the message will be sent. + /// The message to send. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// A to observe. + /// or is . + /// is not assignable to . + public static Task Send(this IMessageSession session, string destination, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + ArgumentException.ThrowIfNullOrWhiteSpace(destination); + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + + var options = new SendOptions(); + + options.SetDestination(destination); + + return session.Send(message, messageType, options, cancellationToken); + } + /// /// Instantiates a message of type T and sends it to the given destination. /// - /// The type of message, usually an interface. + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// The instance of to use for the action. /// The destination to which the message will be sent. /// An action which initializes properties of the message. @@ -89,6 +179,10 @@ public static Task Send(this IMessageSession session, string destination, object /// Object being extended. /// The message to send. /// A to observe. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "SendLocal(this IMessageSession, T, CancellationToken)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public static Task SendLocal(this IMessageSession session, object message, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(session); @@ -101,10 +195,52 @@ public static Task SendLocal(this IMessageSession session, object message, Cance return session.Send(message, options, cancellationToken); } + /// + /// Sends the typed message back to the current endpoint. Shortcut for sendOptions.RouteToThisEndpoint(). + /// + /// The type used to send the message locally. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// Object being extended. + /// The message to send. + /// A to observe. + [OverloadResolutionPriority(-1)] + public static Task SendLocal<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(this IMessageSession session, T message, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + ArgumentNullException.ThrowIfNull(message); + + var options = new SendOptions(); + + options.RouteToThisEndpoint(); + + return session.Send(message, options, cancellationToken); + } + + /// + /// Sends the message with the specified message type back to the current endpoint. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// Object being extended. + /// The message to send. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// A to observe. + /// or is . + /// is not assignable to . + public static Task SendLocal(this IMessageSession session, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + + var options = new SendOptions(); + + options.RouteToThisEndpoint(); + + return session.Send(message, messageType, options, cancellationToken); + } + /// /// Instantiates a message of type T and sends it back to the current endpoint. Shortcut for sendOptions.RouteToThisEndpoint(). /// - /// The type of message, usually an interface. + /// The type used to send the message locally. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// Object being extended. /// An action which initializes properties of the message. /// A to observe. @@ -126,6 +262,10 @@ public static Task SendLocal(this IMessageSession session, object message, Cance /// The instance of to use for the action. /// The message to publish. /// A to observe. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Publish(this IMessageSession, T, CancellationToken)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public static Task Publish(this IMessageSession session, object message, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(session); @@ -134,6 +274,40 @@ public static Task Publish(this IMessageSession session, object message, Cancell return session.Publish(message, new PublishOptions(), cancellationToken); } + /// + /// Publishes the provided typed message. + /// + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The instance of to use for the action. + /// The message to publish. + /// A to observe. + [OverloadResolutionPriority(-1)] + public static Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(this IMessageSession session, T message, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + ArgumentNullException.ThrowIfNull(message); + + return session.Publish(message, new PublishOptions(), cancellationToken); + } + + /// + /// Publishes the provided message with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The instance of to use for the action. + /// The message to publish. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// A to observe. + /// or is . + /// is not assignable to . + public static Task Publish(this IMessageSession session, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + + return session.Publish(message, messageType, new PublishOptions(), cancellationToken); + } + /// /// Publish the message to subscribers. /// @@ -150,7 +324,7 @@ public static Task Publish(this IMessageSession session, object message, Cancell /// /// Instantiates a message of type T and publishes it. /// - /// The type of message, usually an interface. + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// The instance of to use for the action. /// An action which initializes properties of the message. /// A to observe. diff --git a/src/NServiceBus.Core/NServiceBus.Core.csproj b/src/NServiceBus.Core/NServiceBus.Core.csproj index 1d69e29300a..bdd99ca6377 100644 --- a/src/NServiceBus.Core/NServiceBus.Core.csproj +++ b/src/NServiceBus.Core/NServiceBus.Core.csproj @@ -12,6 +12,10 @@ + + + + diff --git a/src/NServiceBus.Core/NServiceBus.props b/src/NServiceBus.Core/NServiceBus.props index f4dd9059c3f..f2fa7ae714f 100644 --- a/src/NServiceBus.Core/NServiceBus.props +++ b/src/NServiceBus.Core/NServiceBus.props @@ -8,4 +8,13 @@ $(InterceptorsNamespaces);NServiceBus + + + + + + + + + diff --git a/src/NServiceBus.Core/Pipeline/Incoming/IncomingContext.cs b/src/NServiceBus.Core/Pipeline/Incoming/IncomingContext.cs index 094e99626ee..e07880c2cfa 100644 --- a/src/NServiceBus.Core/Pipeline/Incoming/IncomingContext.cs +++ b/src/NServiceBus.Core/Pipeline/Incoming/IncomingContext.cs @@ -5,6 +5,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Pipeline; @@ -26,31 +27,67 @@ protected IncomingContext(string messageId, string replyToAddress, IReadOnlyDict public IReadOnlyDictionary MessageHeaders { get; } + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public Task Send(object message, SendOptions options) { return MessageOperations.Send(this, message, options); } + [OverloadResolutionPriority(-1)] + public Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, SendOptions options) + { + return MessageOperations.Send(this, message, options); + } + + public Task Send(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, SendOptions options) + { + return MessageOperations.Send(this, message, messageType, options); + } + public Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, SendOptions options) { return MessageOperations.Send(this, messageConstructor, options); } + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public Task Publish(object message, PublishOptions options) { return MessageOperations.Publish(this, message, options); } + [OverloadResolutionPriority(-1)] + public Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, PublishOptions options) + { + return MessageOperations.Publish(this, message, options); + } + + public Task Publish(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, PublishOptions options) + { + return MessageOperations.Publish(this, message, messageType, options); + } + public Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, PublishOptions publishOptions) { return MessageOperations.Publish(this, messageConstructor, publishOptions); } + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public Task Reply(object message, ReplyOptions options) { return MessageOperations.Reply(this, message, options); } + [OverloadResolutionPriority(-1)] + public Task Reply<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, ReplyOptions options) + { + return MessageOperations.Reply(this, message, options); + } + + public Task Reply(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, ReplyOptions options) + { + return MessageOperations.Reply(this, message, messageType, options); + } + public Task Reply<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, ReplyOptions options) { return MessageOperations.Reply(this, messageConstructor, options); diff --git a/src/NServiceBus.Core/Pipeline/Outgoing/IOutgoingLogicalMessageContext.cs b/src/NServiceBus.Core/Pipeline/Outgoing/IOutgoingLogicalMessageContext.cs index abe07c83e89..04edfafd28f 100644 --- a/src/NServiceBus.Core/Pipeline/Outgoing/IOutgoingLogicalMessageContext.cs +++ b/src/NServiceBus.Core/Pipeline/Outgoing/IOutgoingLogicalMessageContext.cs @@ -2,7 +2,11 @@ namespace NServiceBus.Pipeline; +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Particular.Obsoletes; using Routing; /// @@ -23,5 +27,37 @@ public interface IOutgoingLogicalMessageContext : IOutgoingContext /// /// Updates the message instance. /// + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "UpdateMessage(T)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] void UpdateMessage(object newInstance); + + /// + /// Updates the message instance while preserving the specified message type. + /// + /// The type used to update the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The replacement message instance. + [OverloadResolutionPriority(-1)] + void UpdateMessage<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newInstance) + { + UpdateMessage(newInstance!, typeof(T)); + } + + /// + /// Updates the message instance with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The replacement message instance. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + /// + /// Third-party implementations that inherit this default implementation fall back to the object overload and route by the runtime type of . Override this method to preserve a declared that differs from the runtime type. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = MessageOperations.DefaultInterfaceTrimmingSuppressionJustification)] + void UpdateMessage(object newInstance, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + MessageTypeValidator.Validate(newInstance, messageType); + UpdateMessage(newInstance); + } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingContext.cs b/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingContext.cs index 61c9b275f7f..03dc8bed05b 100644 --- a/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingContext.cs +++ b/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingContext.cs @@ -5,6 +5,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Pipeline; @@ -23,21 +24,45 @@ protected OutgoingContext(string messageId, Dictionary headers, public Dictionary Headers { get; } + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public Task Send(object message, SendOptions options) { return MessageOperations.Send(this, message, options); } + [OverloadResolutionPriority(-1)] + public Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, SendOptions options) + { + return MessageOperations.Send(this, message, options); + } + + public Task Send(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, SendOptions options) + { + return MessageOperations.Send(this, message, messageType, options); + } + public Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, SendOptions options) { return MessageOperations.Send(this, messageConstructor, options); } + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public Task Publish(object message, PublishOptions options) { return MessageOperations.Publish(this, message, options); } + [OverloadResolutionPriority(-1)] + public Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, PublishOptions options) + { + return MessageOperations.Publish(this, message, options); + } + + public Task Publish(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, PublishOptions options) + { + return MessageOperations.Publish(this, message, messageType, options); + } + public Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, PublishOptions publishOptions) { return MessageOperations.Publish(this, messageConstructor, publishOptions); diff --git a/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingLogicalMessage.cs b/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingLogicalMessage.cs index 77baff82904..642166c4e76 100644 --- a/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingLogicalMessage.cs +++ b/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingLogicalMessage.cs @@ -3,6 +3,7 @@ namespace NServiceBus.Pipeline; using System; +using System.Diagnostics.CodeAnalysis; /// /// Represents a logical message about to be push out to the transport. @@ -13,7 +14,7 @@ public class OutgoingLogicalMessage /// Initializes the message with a explicit message type and instance. Use this constructor if the message type is /// different from the instance type. /// - public OutgoingLogicalMessage(Type messageType, object message) + public OutgoingLogicalMessage([DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, object message) { ArgumentNullException.ThrowIfNull(messageType); ArgumentNullException.ThrowIfNull(message); @@ -23,8 +24,9 @@ public OutgoingLogicalMessage(Type messageType, object message) } /// - /// The of the message instance. + /// The declared logical message type used for routing and the enclosed-message-types header. It may differ from the runtime type of . /// + [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] public Type MessageType { get; } /// diff --git a/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingLogicalMessageContext.cs b/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingLogicalMessageContext.cs index dde65f516d6..29a07562852 100644 --- a/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingLogicalMessageContext.cs +++ b/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingLogicalMessageContext.cs @@ -4,6 +4,9 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Particular.Obsoletes; using Pipeline; using Routing; @@ -21,6 +24,10 @@ public OutgoingLogicalMessageContext(string messageId, Dictionary RoutingStrategies { get; } + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "UpdateMessage(T)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public void UpdateMessage(object newInstance) { ArgumentNullException.ThrowIfNull(newInstance); @@ -30,4 +37,32 @@ public void UpdateMessage(object newInstance) Message = new OutgoingLogicalMessage(newInstance.GetType(), newInstance); } } + + /// + /// Replaces the current message with the provided typed message instance. + /// + /// The type used to update the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The new message instance. + [OverloadResolutionPriority(-1)] + public void UpdateMessage<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newInstance) + { + ArgumentNullException.ThrowIfNull(newInstance); + + if (Message.Instance != (object)newInstance || Message.MessageType != typeof(T)) + { + Message = new OutgoingLogicalMessage(typeof(T), newInstance); + } + } + + public void UpdateMessage(object newInstance, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + ArgumentNullException.ThrowIfNull(newInstance); + ArgumentNullException.ThrowIfNull(messageType); + MessageTypeValidator.Validate(newInstance, messageType); + + if (Message.Instance != newInstance || Message.MessageType != messageType) + { + Message = new OutgoingLogicalMessage(messageType, newInstance); + } + } } \ No newline at end of file diff --git a/src/NServiceBus.Core/PipelineContextExtensions.cs b/src/NServiceBus.Core/PipelineContextExtensions.cs index a8540fcfee0..df3008f7ec8 100644 --- a/src/NServiceBus.Core/PipelineContextExtensions.cs +++ b/src/NServiceBus.Core/PipelineContextExtensions.cs @@ -4,7 +4,9 @@ namespace NServiceBus; using System; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading.Tasks; +using Particular.Obsoletes; /// /// Syntactic sugar for . @@ -16,6 +18,10 @@ public static class PipelineContextExtensions /// /// The instance of to use for the action. /// The message to send. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Send(this IPipelineContext, T)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead, or the overload accepting an explicit messageType when the static type is unavailable.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public static Task Send(this IPipelineContext context, object message) { ArgumentNullException.ThrowIfNull(context); @@ -24,10 +30,42 @@ public static Task Send(this IPipelineContext context, object message) return context.Send(message, new SendOptions()); } + /// + /// Sends the provided typed message. + /// + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The instance of to use for the action. + /// The message to send. + [OverloadResolutionPriority(-1)] + public static Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(this IPipelineContext context, T message) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(message); + + return context.Send(message, new SendOptions()); + } + + /// + /// Sends the provided message with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The instance of to use for the action. + /// The message to send. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + public static Task Send(this IPipelineContext context, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + + return context.Send(message, messageType, new SendOptions()); + } + /// /// Instantiates a message of and sends it. /// - /// The type of message, usually an interface. + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// The instance of to use for the action. /// An action which initializes properties of the message. /// @@ -47,6 +85,10 @@ public static Task Send(this IPipelineContext context, object message) /// The instance of to use for the action. /// The address of the destination to which the message will be sent. /// The message to send. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Send(this IPipelineContext, string, T)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead, or the overload accepting an explicit messageType when the static type is unavailable.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public static Task Send(this IPipelineContext context, string destination, object message) { ArgumentNullException.ThrowIfNull(context); @@ -60,10 +102,54 @@ public static Task Send(this IPipelineContext context, string destination, objec return context.Send(message, options); } + /// + /// Sends the typed message to the given destination. + /// + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The instance of to use for the action. + /// The destination to which the message will be sent. + /// The message to send. + [OverloadResolutionPriority(-1)] + public static Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(this IPipelineContext context, string destination, T message) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentException.ThrowIfNullOrWhiteSpace(destination); + ArgumentNullException.ThrowIfNull(message); + + var options = new SendOptions(); + + options.SetDestination(destination); + + return context.Send(message, options); + } + + /// + /// Sends the message with the specified message type to the given destination. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The instance of to use for the action. + /// The destination to which the message will be sent. + /// The message to send. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + public static Task Send(this IPipelineContext context, string destination, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentException.ThrowIfNullOrWhiteSpace(destination); + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + + var options = new SendOptions(); + + options.SetDestination(destination); + + return context.Send(message, messageType, options); + } + /// /// Instantiates a message of type T and sends it to the given destination. /// - /// The type of message, usually an interface. + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// The instance of to use for the action. /// The destination to which the message will be sent. /// An action which initializes properties of the message. @@ -85,6 +171,10 @@ public static Task Send(this IPipelineContext context, string destination, objec /// /// Object being extended. /// The message to send. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "SendLocal(this IPipelineContext, T)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead, or the overload accepting an explicit messageType when the static type is unavailable.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public static Task SendLocal(this IPipelineContext context, object message) { ArgumentNullException.ThrowIfNull(context); @@ -97,10 +187,50 @@ public static Task SendLocal(this IPipelineContext context, object message) return context.Send(message, options); } + /// + /// Sends the typed message back to the current endpoint. + /// + /// The type used to send the message locally. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// Object being extended. + /// The message to send. + [OverloadResolutionPriority(-1)] + public static Task SendLocal<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(this IPipelineContext context, T message) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(message); + + var options = new SendOptions(); + + options.RouteToThisEndpoint(); + + return context.Send(message, options); + } + + /// + /// Sends the message with the specified message type back to the current endpoint. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// Object being extended. + /// The message to send. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + public static Task SendLocal(this IPipelineContext context, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + + var options = new SendOptions(); + + options.RouteToThisEndpoint(); + + return context.Send(message, messageType, options); + } + /// /// Instantiates a message of type T and sends it back to the current endpoint. /// - /// The type of message, usually an interface. + /// The type used to send the message locally. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// Object being extended. /// An action which initializes properties of the message. public static Task SendLocal<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IPipelineContext context, Action messageConstructor) @@ -120,6 +250,10 @@ public static Task SendLocal(this IPipelineContext context, object message) /// /// The instance of to use for the action. /// The message to publish. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Publish(this IPipelineContext, T)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead, or the overload accepting an explicit messageType when the static type is unavailable.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public static Task Publish(this IPipelineContext context, object message) { ArgumentNullException.ThrowIfNull(context); @@ -128,6 +262,38 @@ public static Task Publish(this IPipelineContext context, object message) return context.Publish(message, new PublishOptions()); } + /// + /// Publishes the provided typed message. + /// + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The instance of to use for the action. + /// The message to publish. + [OverloadResolutionPriority(-1)] + public static Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(this IPipelineContext context, T message) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(message); + + return context.Publish(message, new PublishOptions()); + } + + /// + /// Publishes the provided message with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The instance of to use for the action. + /// The message to publish. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + public static Task Publish(this IPipelineContext context, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + + return context.Publish(message, messageType, new PublishOptions()); + } + /// /// Publish the message to subscribers. /// @@ -143,7 +309,7 @@ public static Task Publish(this IPipelineContext context, object message) /// /// Instantiates a message of type T and publishes it. /// - /// The type of message, usually an interface. + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// The instance of to use for the action. /// An action which initializes properties of the message. public static Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IPipelineContext context, Action messageConstructor) diff --git a/src/NServiceBus.Core/Receiving/ReceiveComponent.Configuration.cs b/src/NServiceBus.Core/Receiving/ReceiveComponent.Configuration.cs index 2abceb77a3a..53e5c19dca4 100644 --- a/src/NServiceBus.Core/Receiving/ReceiveComponent.Configuration.cs +++ b/src/NServiceBus.Core/Receiving/ReceiveComponent.Configuration.cs @@ -4,6 +4,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using NServiceBus.Settings; using Transport; @@ -11,6 +12,7 @@ namespace NServiceBus; partial class ReceiveComponent { + [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "The startup diagnostics message schema is best-effort and reflects over application message properties that may be removed when trimming.")] public void AddManifest(HostingComponent.Configuration hostingConfiguration, SettingsHolder settings) { var messageTypes = configuration.MessageHandlerRegistry.GetMessageTypes(); diff --git a/src/NServiceBus.Core/Routing/AssemblyRouteSource.cs b/src/NServiceBus.Core/Routing/AssemblyRouteSource.cs index ebdc49ed7bb..8bc47a24d67 100644 --- a/src/NServiceBus.Core/Routing/AssemblyRouteSource.cs +++ b/src/NServiceBus.Core/Routing/AssemblyRouteSource.cs @@ -2,6 +2,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using Routing; @@ -11,12 +12,14 @@ class AssemblyRouteSource : IRouteSource readonly Assembly messageAssembly; readonly UnicastRoute route; + [RequiresUnreferencedCode(TrimmingMessage)] public AssemblyRouteSource(Assembly messageAssembly, UnicastRoute route) { this.messageAssembly = messageAssembly; this.route = route; } + [RequiresUnreferencedCode(TrimmingMessage)] public IEnumerable GenerateRoutes(Conventions conventions) { var routes = messageAssembly.GetTypes() @@ -33,4 +36,6 @@ public IEnumerable GenerateRoutes(Conventions conventions) } public RouteSourcePriority Priority => RouteSourcePriority.Assembly; + + internal const string TrimmingMessage = "Routing messages by assembly or namespace requires assembly scanning and is not supported in trimming scenarios. Register routes by message type instead."; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/AssemblyPublisherSource.cs b/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/AssemblyPublisherSource.cs index ffd556cf15f..0226815f131 100644 --- a/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/AssemblyPublisherSource.cs +++ b/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/AssemblyPublisherSource.cs @@ -2,6 +2,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using Routing.MessageDrivenSubscriptions; @@ -11,12 +12,14 @@ class AssemblyPublisherSource : IPublisherSource readonly Assembly messageAssembly; readonly PublisherAddress address; + [RequiresUnreferencedCode(TrimmingMessage)] public AssemblyPublisherSource(Assembly messageAssembly, PublisherAddress address) { this.messageAssembly = messageAssembly; this.address = address; } + [RequiresUnreferencedCode(TrimmingMessage)] public IEnumerable GenerateWithBestPracticeEnforcement(Conventions conventions) { var entries = messageAssembly.GetTypes() @@ -32,6 +35,7 @@ public IEnumerable GenerateWithBestPracticeEnforcement(Conv return entries; } + [RequiresUnreferencedCode(TrimmingMessage)] public IEnumerable GenerateWithoutBestPracticeEnforcement(Conventions conventions) { var entries = messageAssembly.GetTypes() @@ -48,4 +52,6 @@ public IEnumerable GenerateWithoutBestPracticeEnforcement(C } public RouteSourcePriority Priority => RouteSourcePriority.Assembly; + + internal const string TrimmingMessage = "Registering publishers by assembly or namespace requires assembly scanning and is not supported in trimming scenarios. Register publishers by message type instead."; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/MessageDrivenSubscriptionsConfigExtensions.cs b/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/MessageDrivenSubscriptionsConfigExtensions.cs index 5d4210b0601..73ec000e642 100644 --- a/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/MessageDrivenSubscriptionsConfigExtensions.cs +++ b/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/MessageDrivenSubscriptionsConfigExtensions.cs @@ -1,6 +1,7 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Features; using Pipeline; @@ -62,6 +63,7 @@ public static void RegisterPublisher(this RoutingSettings routingSettings, /// The to extend. /// The assembly containing the event types. /// The publisher endpoint. + [RequiresUnreferencedCode(AssemblyPublisherSource.TrimmingMessage)] public static void RegisterPublisher(this RoutingSettings routingSettings, Assembly assembly, string publisherEndpoint) where T : TransportDefinition, IMessageDrivenSubscriptionTransport { ArgumentNullException.ThrowIfNull(assembly); @@ -79,6 +81,7 @@ public static void RegisterPublisher(this RoutingSettings routingSettings, /// The assembly containing the event types. /// The namespace containing the event types. The given value must exactly match the target namespace. /// The publisher endpoint. + [RequiresUnreferencedCode(AssemblyPublisherSource.TrimmingMessage)] public static void RegisterPublisher(this RoutingSettings routingSettings, Assembly assembly, string @namespace, string publisherEndpoint) where T : TransportDefinition, IMessageDrivenSubscriptionTransport { ArgumentNullException.ThrowIfNull(assembly); diff --git a/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/NamespacePublisherSource.cs b/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/NamespacePublisherSource.cs index 632ed60f36d..29c3e80bbe5 100644 --- a/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/NamespacePublisherSource.cs +++ b/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/NamespacePublisherSource.cs @@ -2,6 +2,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using Routing.MessageDrivenSubscriptions; @@ -19,6 +20,7 @@ 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.")] public IEnumerable GenerateWithBestPracticeEnforcement(Conventions conventions) { var entries = messageAssembly.GetTypes() @@ -34,6 +36,7 @@ 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() diff --git a/src/NServiceBus.Core/Routing/MulticastAddressTag.cs b/src/NServiceBus.Core/Routing/MulticastAddressTag.cs index cc4a509816f..55470047adf 100644 --- a/src/NServiceBus.Core/Routing/MulticastAddressTag.cs +++ b/src/NServiceBus.Core/Routing/MulticastAddressTag.cs @@ -1,6 +1,7 @@ namespace NServiceBus.Routing; using System; +using System.Diagnostics.CodeAnalysis; /// /// Represents a route that should deliver the message to all interested subscribers. @@ -11,7 +12,7 @@ public class MulticastAddressTag : AddressTag /// Initializes a new instance of . /// /// The event being published. - public MulticastAddressTag(Type messageType) + public MulticastAddressTag([DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) { MessageType = messageType; } @@ -19,5 +20,6 @@ public MulticastAddressTag(Type messageType) /// /// The event being published. /// + [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] public Type MessageType { get; } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Routing/MulticastRoutingStrategy.cs b/src/NServiceBus.Core/Routing/MulticastRoutingStrategy.cs index 1ca8e9f1983..12e67dc8092 100644 --- a/src/NServiceBus.Core/Routing/MulticastRoutingStrategy.cs +++ b/src/NServiceBus.Core/Routing/MulticastRoutingStrategy.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; /// /// A routing strategy for multicast routing. @@ -11,7 +12,7 @@ public class MulticastRoutingStrategy : RoutingStrategy /// /// Creates new routing strategy. /// - public MulticastRoutingStrategy(Type messageType) + public MulticastRoutingStrategy([DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) { this.messageType = messageType; } @@ -26,5 +27,6 @@ public override AddressTag Apply(Dictionary headers) return new MulticastAddressTag(messageType); } + [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] readonly Type messageType; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Routing/NamespaceRouteSource.cs b/src/NServiceBus.Core/Routing/NamespaceRouteSource.cs index d27837ec285..f77fd23aed0 100644 --- a/src/NServiceBus.Core/Routing/NamespaceRouteSource.cs +++ b/src/NServiceBus.Core/Routing/NamespaceRouteSource.cs @@ -2,6 +2,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using Routing; @@ -12,6 +13,7 @@ class NamespaceRouteSource : IRouteSource readonly string messageNamespace; readonly UnicastRoute route; + [RequiresUnreferencedCode(AssemblyRouteSource.TrimmingMessage)] public NamespaceRouteSource(Assembly messageAssembly, string messageNamespace, UnicastRoute route) { this.messageAssembly = messageAssembly; @@ -19,6 +21,7 @@ public NamespaceRouteSource(Assembly messageAssembly, string messageNamespace, U this.messageNamespace = messageNamespace; } + [RequiresUnreferencedCode(AssemblyRouteSource.TrimmingMessage)] public IEnumerable GenerateRoutes(Conventions conventions) { var routes = messageAssembly.GetTypes() diff --git a/src/NServiceBus.Core/Routing/RoutingSettings.cs b/src/NServiceBus.Core/Routing/RoutingSettings.cs index 720b024d176..d1855d7994c 100644 --- a/src/NServiceBus.Core/Routing/RoutingSettings.cs +++ b/src/NServiceBus.Core/Routing/RoutingSettings.cs @@ -1,6 +1,7 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Configuration.AdvancedExtensibility; using Routing; @@ -39,6 +40,7 @@ public void RouteToEndpoint(Type messageType, string destination) /// /// The assembly whose messages should be routed. /// Destination endpoint. + [RequiresUnreferencedCode(AssemblyRouteSource.TrimmingMessage)] public void RouteToEndpoint(Assembly assembly, string destination) { ArgumentNullException.ThrowIfNull(assembly); @@ -55,6 +57,7 @@ public void RouteToEndpoint(Assembly assembly, string destination) /// The assembly whose messages should be routed. /// The namespace of the messages which should be routed. The given value must exactly match the target namespace. /// Destination endpoint. + [RequiresUnreferencedCode(AssemblyRouteSource.TrimmingMessage)] public void RouteToEndpoint(Assembly assembly, string @namespace, string destination) { ArgumentNullException.ThrowIfNull(assembly); diff --git a/src/NServiceBus.Core/Routing/SubscriptionMigrationMode/SubscriptionMigrationModeSettings.cs b/src/NServiceBus.Core/Routing/SubscriptionMigrationMode/SubscriptionMigrationModeSettings.cs index f72ee85070a..6bb0de704ce 100644 --- a/src/NServiceBus.Core/Routing/SubscriptionMigrationMode/SubscriptionMigrationModeSettings.cs +++ b/src/NServiceBus.Core/Routing/SubscriptionMigrationMode/SubscriptionMigrationModeSettings.cs @@ -1,6 +1,7 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Configuration.AdvancedExtensibility; using Pipeline; @@ -49,6 +50,7 @@ public void RegisterPublisher(Type eventType, string publisherEndpoint) /// /// The assembly containing the event types. /// The publisher endpoint. + [RequiresUnreferencedCode(AssemblyPublisherSource.TrimmingMessage)] public void RegisterPublisher(Assembly assembly, string publisherEndpoint) { ArgumentNullException.ThrowIfNull(assembly); @@ -65,6 +67,7 @@ public void RegisterPublisher(Assembly assembly, string publisherEndpoint) /// The assembly containing the event types. /// The namespace containing the event types. The given value must exactly match the target namespace. /// The publisher endpoint. + [RequiresUnreferencedCode(AssemblyPublisherSource.TrimmingMessage)] public void RegisterPublisher(Assembly assembly, string @namespace, string publisherEndpoint) { ArgumentNullException.ThrowIfNull(assembly); diff --git a/src/NServiceBus.Core/Sagas/Saga.cs b/src/NServiceBus.Core/Sagas/Saga.cs index 87e9804a28a..457463da1c1 100644 --- a/src/NServiceBus.Core/Sagas/Saga.cs +++ b/src/NServiceBus.Core/Sagas/Saga.cs @@ -4,9 +4,12 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Extensibility; +using Particular.Obsoletes; /// /// This class is used to define sagas containing data and handling a message. @@ -38,7 +41,7 @@ public abstract class Saga /// /// The context which is used to send the timeout. /// to send timeout . - protected Task RequestTimeout(IMessageHandlerContext context, DateTimeOffset at) where TTimeoutMessageType : new() + protected Task RequestTimeout<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] TTimeoutMessageType>(IMessageHandlerContext context, DateTimeOffset at) where TTimeoutMessageType : new() { return RequestTimeout(context, at, new TTimeoutMessageType()); } @@ -49,7 +52,7 @@ public abstract class Saga /// The context which is used to send the timeout. /// to send timeout . /// The message to send after is reached. - protected Task RequestTimeout(IMessageHandlerContext context, DateTimeOffset at, TTimeoutMessageType timeoutMessage) + protected Task RequestTimeout<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] TTimeoutMessageType>(IMessageHandlerContext context, DateTimeOffset at, TTimeoutMessageType timeoutMessage) { VerifySagaCanHandleTimeout(timeoutMessage); @@ -60,7 +63,7 @@ protected Task RequestTimeout(IMessageHandlerContext contex SetTimeoutHeaders(options); - return context.Send(timeoutMessage, options); + return context.Send(timeoutMessage, options); } /// @@ -68,7 +71,7 @@ protected Task RequestTimeout(IMessageHandlerContext contex /// /// The context which is used to send the timeout. /// Given to delay timeout message by. - protected Task RequestTimeout(IMessageHandlerContext context, TimeSpan within) where TTimeoutMessageType : new() + protected Task RequestTimeout<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] TTimeoutMessageType>(IMessageHandlerContext context, TimeSpan within) where TTimeoutMessageType : new() { return RequestTimeout(context, within, new TTimeoutMessageType()); } @@ -79,7 +82,7 @@ protected Task RequestTimeout(IMessageHandlerContext contex /// The context which is used to send the timeout. /// Given to delay timeout message by. /// The message to send after expires. - protected Task RequestTimeout(IMessageHandlerContext context, TimeSpan within, TTimeoutMessageType timeoutMessage) + protected Task RequestTimeout<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] TTimeoutMessageType>(IMessageHandlerContext context, TimeSpan within, TTimeoutMessageType timeoutMessage) { VerifySagaCanHandleTimeout(timeoutMessage); @@ -90,13 +93,38 @@ protected Task RequestTimeout(IMessageHandlerContext contex SetTimeoutHeaders(sendOptions); - return context.Send(timeoutMessage, sendOptions); + return context.Send(timeoutMessage, sendOptions); } /// /// Sends the using the bus to the endpoint that caused this saga to start. /// + [ObsoleteMetadata(ReplacementTypeOrMember = "ReplyToOriginator", + TreatAsErrorFromVersion = "11", + RemoveInVersion = "12")] + [Obsolete("Use 'ReplyToOriginator' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] protected Task ReplyToOriginator(IMessageHandlerContext context, object message, IReadOnlyDictionary? outgoingHeaders = null) + { + var options = BuildReplyToOriginatorOptions(outgoingHeaders); + return context.Reply(message, options); + } + + /// + /// Sends the typed using the bus to the endpoint that caused this saga to start. + /// + /// The type used to reply. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The context of the currently handled message. + /// The message to send. + /// The headers to attach to the outgoing message. + [OverloadResolutionPriority(-1)] + protected Task ReplyToOriginator<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(IMessageHandlerContext context, T message, IReadOnlyDictionary? outgoingHeaders = null) + { + var options = BuildReplyToOriginatorOptions(outgoingHeaders); + return context.Reply(message, options); + } + + ReplyOptions BuildReplyToOriginatorOptions(IReadOnlyDictionary? outgoingHeaders) { if (string.IsNullOrEmpty(Entity.Originator)) { @@ -122,7 +150,7 @@ protected Task ReplyToOriginator(IMessageHandlerContext context, object message, SagaIdToUse = null }); - return context.Reply(message, options); + return options; } /// diff --git a/src/NServiceBus.Core/Transports/Learning/LearningTransportDispatcher.cs b/src/NServiceBus.Core/Transports/Learning/LearningTransportDispatcher.cs index efa24037ee1..e529ae8d7b1 100644 --- a/src/NServiceBus.Core/Transports/Learning/LearningTransportDispatcher.cs +++ b/src/NServiceBus.Core/Transports/Learning/LearningTransportDispatcher.cs @@ -2,12 +2,14 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using Transport; +using static NServiceBusMarkerInterfaceConvention; class LearningTransportDispatcher : IMessageDispatcher { @@ -35,7 +37,7 @@ async Task DispatchMulticast(IEnumerable transportO foreach (var transportOperation in transportOperations) { - var subscribers = await GetSubscribersFor(transportOperation.MessageType, cancellationToken) + var subscribers = await GetSubscribersFor(transportOperation, cancellationToken) .ConfigureAwait(false); foreach (var subscriber in subscribers) @@ -139,15 +141,13 @@ await AsyncFile.WriteTextAtomic(messagePath, headerPayload, creationTime, cancel } } - async Task> GetSubscribersFor(Type messageType, CancellationToken cancellationToken) + async Task> GetSubscribersFor(MulticastTransportOperation transportOperation, CancellationToken cancellationToken) { var subscribers = new HashSet(); - var allEventTypes = GetPotentialEventTypes(messageType); - - foreach (var eventType in allEventTypes) + foreach (var eventTypeName in GetPotentialEventTypeNames(transportOperation)) { - var eventDir = Path.Combine(basePath, ".events", eventType.FullName); + var eventDir = Path.Combine(basePath, ".events", eventTypeName); if (!Directory.Exists(eventDir)) { @@ -166,35 +166,57 @@ async Task> GetSubscribersFor(Type messageType, Cancellation return subscribers; } - static HashSet GetPotentialEventTypes(Type messageType) + static HashSet GetPotentialEventTypeNames(MulticastTransportOperation transportOperation) { - var allEventTypes = new HashSet(); + if (!transportOperation.Message.Headers.TryGetValue(Headers.EnclosedMessageTypes, out var messageHierarchy) || string.IsNullOrWhiteSpace(messageHierarchy)) + { + return InferEventTypeNames(transportOperation.MessageType); + } + + var eventTypeNames = new HashSet(StringComparer.Ordinal); + foreach (var typeName in messageHierarchy.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + var messageTypeName = AssemblyQualifiedNameParser.GetMessageTypeNameWithoutAssembly(typeName).ToString(); + if (!IsMarkerType(messageTypeName)) + { + eventTypeNames.Add(messageTypeName); + } + } + + return eventTypeNames; + } + + [UnconditionalSuppressMessage("Trimming", "IL2070", Justification = "Used only for legacy or custom multicast operations without the enclosed message hierarchy header. Normal pipeline and outbox operations carry message metadata.")] + static HashSet InferEventTypeNames(Type messageType) + { + var allEventTypeNames = new HashSet(StringComparer.Ordinal); var currentType = messageType; while (currentType != null) { //do not include the marker interfaces - if (IsCoreMarkerInterface(currentType)) + if (IsMarkerType(currentType)) { break; } - allEventTypes.Add(currentType); + allEventTypeNames.Add(currentType.FullName!); currentType = currentType.BaseType; } - foreach (var type in messageType.GetInterfaces().Where(i => !IsCoreMarkerInterface(i))) + foreach (var type in messageType.GetInterfaces()) { - allEventTypes.Add(type); + if (!IsMarkerType(type)) + { + allEventTypeNames.Add(type.FullName!); + } } - return allEventTypes; + return allEventTypeNames; } - static bool IsCoreMarkerInterface(Type type) => type == typeof(IMessage) || type == typeof(IEvent) || type == typeof(ICommand); - readonly int maxMessageSizeKB; readonly string basePath; -} +} \ No newline at end of file diff --git a/src/NServiceBus.Core/Transports/MulticastTransportOperation.cs b/src/NServiceBus.Core/Transports/MulticastTransportOperation.cs index 0458d2073ca..a8523e896ad 100644 --- a/src/NServiceBus.Core/Transports/MulticastTransportOperation.cs +++ b/src/NServiceBus.Core/Transports/MulticastTransportOperation.cs @@ -1,6 +1,7 @@ namespace NServiceBus.Transport; using System; +using System.Diagnostics.CodeAnalysis; /// /// Represents a transport operation which should be delivered to multiple receivers. @@ -10,7 +11,7 @@ public class MulticastTransportOperation : IOutgoingTransportOperation /// /// Creates a new instance. /// - public MulticastTransportOperation(OutgoingMessage message, Type messageType, DispatchProperties properties, DispatchConsistency requiredDispatchConsistency = DispatchConsistency.Default) + public MulticastTransportOperation(OutgoingMessage message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, DispatchProperties properties, DispatchConsistency requiredDispatchConsistency = DispatchConsistency.Default) { Message = message; MessageType = messageType; @@ -21,6 +22,7 @@ public MulticastTransportOperation(OutgoingMessage message, Type messageType, Di /// /// Defines the message type which needs to be multicasted. /// + [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] public Type MessageType { get; } /// diff --git a/src/NServiceBus.Core/Transports/TransportSeam.cs b/src/NServiceBus.Core/Transports/TransportSeam.cs index 649e8520a16..370662b6cd3 100644 --- a/src/NServiceBus.Core/Transports/TransportSeam.cs +++ b/src/NServiceBus.Core/Transports/TransportSeam.cs @@ -1,6 +1,8 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; using System.Transactions; @@ -19,7 +21,7 @@ public async Task CreateTransportInfrastructure(IServic { if (OperatingSystem.IsWindows() && TransportDefinition.TransportTransactionMode == TransportTransactionMode.TransactionScope) { - TransactionManager.ImplicitDistributedTransactions = true; + EnableImplicitDistributedTransactions(); } hostSettings.ServiceProvider = serviceProvider; @@ -29,6 +31,10 @@ public async Task CreateTransportInfrastructure(IServic return transportInfrastructure; } + [SupportedOSPlatform("windows")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The trimming limitation is conditional on TransactionScope mode and cannot be expressed at the shared transport configuration boundary; the setter is called only for that mode on Windows.")] + static void EnableImplicitDistributedTransactions() => TransactionManager.ImplicitDistributedTransactions = true; + public static TransportSeam Create(Settings transportSeamSettings, HostingComponent.Configuration hostingConfiguration) { var transportDefinition = transportSeamSettings.TransportDefinition; diff --git a/src/NServiceBus.Core/Unicast/MessageHandlerRegistry.cs b/src/NServiceBus.Core/Unicast/MessageHandlerRegistry.cs index 4fb04136814..0d81379ba3a 100644 --- a/src/NServiceBus.Core/Unicast/MessageHandlerRegistry.cs +++ b/src/NServiceBus.Core/Unicast/MessageHandlerRegistry.cs @@ -200,13 +200,11 @@ void AddHandlerWithReflection(Type handlerType) => .GetMethod(nameof(AddHandler), BindingFlags.Public | BindingFlags.Instance, []) ?? throw new MissingMethodException(nameof(AddHandler)); static readonly MethodInfo AddMessageHandlerForMessageMethod = typeof(MessageHandlerRegistry) - .GetMethods(BindingFlags.Public | BindingFlags.Instance) - .SingleOrDefault(m => m.Name == nameof(AddMessageHandlerForMessage) && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 2) + .GetMethod(nameof(AddMessageHandlerForMessage), 2, BindingFlags.Public | BindingFlags.Instance, null, [], null) ?? throw new MissingMethodException(nameof(AddMessageHandlerForMessage)); static readonly MethodInfo AddTimeoutHandlerForMessageMethod = typeof(MessageHandlerRegistry) - .GetMethods(BindingFlags.Public | BindingFlags.Instance) - .SingleOrDefault(m => m.Name == nameof(AddTimeoutHandlerForMessage) && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 2) + .GetMethod(nameof(AddTimeoutHandlerForMessage), 2, BindingFlags.Public | BindingFlags.Instance, null, [], null) ?? throw new MissingMethodException(nameof(AddTimeoutHandlerForMessage)); readonly Dictionary> messageHandlerFactories = []; diff --git a/src/NServiceBus.Core/Unicast/MessageOperations.cs b/src/NServiceBus.Core/Unicast/MessageOperations.cs index 99e770a0883..42434953eef 100644 --- a/src/NServiceBus.Core/Unicast/MessageOperations.cs +++ b/src/NServiceBus.Core/Unicast/MessageOperations.cs @@ -37,19 +37,31 @@ public MessageOperations( this.activityFactory = activityFactory; } + public Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(IBehaviorContext context, T message, PublishOptions options) + { + return PublishMessage(context, typeof(T), message!, options); + } + + public Task Publish(IBehaviorContext context, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, PublishOptions options) + { + MessageTypeValidator.Validate(message, messageType); + return PublishMessage(context, messageType, message, options); + } + public Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(IBehaviorContext context, Action messageConstructor, PublishOptions options) { - return Publish(context, typeof(T), messageMapper.CreateInstance(messageConstructor), options); + return PublishMessage(context, typeof(T), messageMapper.CreateInstance(messageConstructor), options); } + [RequiresUnreferencedCode(RuntimeTypeRoutingTrimmingMessage)] public Task Publish(IBehaviorContext context, object message, PublishOptions options) { var messageType = messageMapper.GetMappedTypeFor(message.GetType()); - return Publish(context, messageType, message, options); + return PublishMessage(context, messageType, message, options); } - async Task Publish(IBehaviorContext context, Type messageType, object message, PublishOptions options) + async Task PublishMessage(IBehaviorContext context, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, object message, PublishOptions options) { var messageId = options.UserDefinedMessageId ?? CombGuid.Generate().ToString(); var headers = new Dictionary(options.OutgoingHeaders) @@ -104,11 +116,23 @@ public async Task Unsubscribe(IBehaviorContext context, Type eventType, Unsubscr await unsubscribePipeline.Invoke(unsubscribeContext, activity).ConfigureAwait(false); } + public Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(IBehaviorContext context, T message, SendOptions options) + { + return SendMessage(context, typeof(T), message!, options); + } + + public Task Send(IBehaviorContext context, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, SendOptions options) + { + MessageTypeValidator.Validate(message, messageType); + return SendMessage(context, messageType, message, options); + } + public Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(IBehaviorContext context, Action messageConstructor, SendOptions options) { return SendMessage(context, typeof(T), messageMapper.CreateInstance(messageConstructor), options); } + [RequiresUnreferencedCode(RuntimeTypeRoutingTrimmingMessage)] public Task Send(IBehaviorContext context, object message, SendOptions options) { var messageType = messageMapper.GetMappedTypeFor(message.GetType()); @@ -116,7 +140,7 @@ public Task Send(IBehaviorContext context, object message, SendOptions options) return SendMessage(context, messageType, message, options); } - async Task SendMessage(IBehaviorContext context, Type messageType, object message, SendOptions options) + async Task SendMessage(IBehaviorContext context, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, object message, SendOptions options) { var messageId = options.UserDefinedMessageId ?? CombGuid.Generate().ToString(); var headers = new Dictionary(options.OutgoingHeaders) @@ -138,6 +162,18 @@ async Task SendMessage(IBehaviorContext context, Type messageType, object messag await sendPipeline.Invoke(outgoingContext, activity).ConfigureAwait(false); } + public Task Reply<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(IBehaviorContext context, T message, ReplyOptions options) + { + return ReplyMessage(context, typeof(T), message!, options); + } + + public Task Reply(IBehaviorContext context, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, ReplyOptions options) + { + MessageTypeValidator.Validate(message, messageType); + return ReplyMessage(context, messageType, message, options); + } + + [RequiresUnreferencedCode(RuntimeTypeRoutingTrimmingMessage)] public Task Reply(IBehaviorContext context, object message, ReplyOptions options) { var messageType = messageMapper.GetMappedTypeFor(message.GetType()); @@ -150,7 +186,7 @@ public Task Reply(IBehaviorContext context, object message, ReplyOptions options return ReplyMessage(context, typeof(T), messageMapper.CreateInstance(messageConstructor), options); } - async Task ReplyMessage(IBehaviorContext context, Type messageType, object message, ReplyOptions options) + async Task ReplyMessage(IBehaviorContext context, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, object message, ReplyOptions options) { var messageId = options.UserDefinedMessageId ?? CombGuid.Generate().ToString(); var headers = new Dictionary(options.OutgoingHeaders) @@ -172,6 +208,9 @@ async Task ReplyMessage(IBehaviorContext context, Type messageType, object messa await replyPipeline.Invoke(outgoingContext, activity).ConfigureAwait(false); } + internal const string RuntimeTypeRoutingTrimmingMessage = "When trimming is enabled, routing a message using its runtime type cannot be statically analyzed by the trimmer. Use the generic overload or, when the message type is not known at compile time, the overload accepting an explicit Type."; + internal const string DefaultInterfaceTrimmingSuppressionJustification = "The default interface implementation preserves compatibility with third-party implementations. Built-in implementations override this method and preserve the declared message type."; + static void MergeDispatchProperties(ContextBag context, DispatchProperties dispatchProperties) { // we can't add the constraints directly to the SendOptions ContextBag as the options can be reused diff --git a/src/NServiceBus.Core/Unicast/Messages/MessageMetadataRegistry.cs b/src/NServiceBus.Core/Unicast/Messages/MessageMetadataRegistry.cs index 00d00491b26..4a5999992e8 100644 --- a/src/NServiceBus.Core/Unicast/Messages/MessageMetadataRegistry.cs +++ b/src/NServiceBus.Core/Unicast/Messages/MessageMetadataRegistry.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Logging; using NServiceBus; @@ -209,6 +210,7 @@ void RegisterMessageType(Type messageType) } } + [UnconditionalSuppressMessage("Trimming", "IL2057", Justification = "Dynamic type loading is best-effort; when trimming removes a type, resolution falls back to known registered message metadata.")] Type GetType(string messageTypeIdentifier) { if (allowDynamicTypeLoading) @@ -254,10 +256,7 @@ MessageMetadata RegisterMessageTypeCore(Type messageType) LogGenericMessageTypeWarning(messageType); - //get the parent types - var parentMessages = GetParentTypes(messageType) - .Where(isMessageType) - .OrderByDescending(PlaceInMessageHierarchy); + var parentMessages = GetRuntimeMessageHierarchy(messageType); metadata = new MessageMetadata(messageType, [messageType, .. parentMessages]); @@ -284,40 +283,37 @@ void AssertIsInitialized() } } - static int PlaceInMessageHierarchy(Type type) + [UnconditionalSuppressMessage("Trimming", "IL2070", Justification = "Runtime hierarchy inference is used for message types not pre-registered with source-generated hierarchy metadata, including scanned, dynamically loaded, published-only, and legacy message types.")] + Type[] GetRuntimeMessageHierarchy(Type messageType) { - if (type.IsInterface) - { - return type.GetInterfaces().Length; - } - - var result = 0; - - while (type.BaseType != null) - { - result++; + var parentTypes = new List(messageType.GetInterfaces()); - type = type.BaseType; - } - - return result; - } - - static IEnumerable GetParentTypes(Type type) - { - foreach (var i in type.GetInterfaces()) - { - yield return i; - } - - // return all inherited types - var currentBaseType = type.BaseType; + var currentBaseType = messageType.BaseType; var objectType = typeof(object); while (currentBaseType != null && currentBaseType != objectType) { - yield return currentBaseType; + parentTypes.Add(currentBaseType); currentBaseType = currentBaseType.BaseType; } + + return [.. parentTypes + .Where(isMessageType) + .OrderByDescending(static type => + { + if (type.IsInterface) + { + return type.GetInterfaces().Length; + } + + var result = 0; + while (type.BaseType != null) + { + result++; + type = type.BaseType; + } + + return result; + })]; } bool initialized; diff --git a/src/NServiceBus.Core/Unicast/RunningEndpointInstance.cs b/src/NServiceBus.Core/Unicast/RunningEndpointInstance.cs index 34f23d82d7e..90893513f95 100644 --- a/src/NServiceBus.Core/Unicast/RunningEndpointInstance.cs +++ b/src/NServiceBus.Core/Unicast/RunningEndpointInstance.cs @@ -4,10 +4,12 @@ namespace NServiceBus; using System; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Features; using Logging; +using Particular.Obsoletes; using Settings; using Transport; @@ -124,6 +126,10 @@ public async ValueTask DisposeAsync() } } + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Send(T, SendOptions, CancellationToken) or Send(object, Type, SendOptions, CancellationToken)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic or explicit Type overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public Task Send(object message, SendOptions sendOptions, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(message); @@ -133,6 +139,26 @@ public Task Send(object message, SendOptions sendOptions, CancellationToken canc return messageSession.Send(message, sendOptions, cancellationToken); } + [OverloadResolutionPriority(-1)] + public Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, SendOptions sendOptions, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(sendOptions); + + GuardAgainstUseWhenNotStarted(); + return messageSession.Send(message, sendOptions, cancellationToken); + } + + public Task Send(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, SendOptions sendOptions, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + ArgumentNullException.ThrowIfNull(sendOptions); + + GuardAgainstUseWhenNotStarted(); + return messageSession.Send(message, messageType, sendOptions, cancellationToken); + } + public Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, SendOptions sendOptions, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(messageConstructor); @@ -142,6 +168,10 @@ public Task Send(object message, SendOptions sendOptions, CancellationToken canc return messageSession.Send(messageConstructor, sendOptions, cancellationToken); } + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Publish(T, PublishOptions, CancellationToken) or Publish(object, Type, PublishOptions, CancellationToken)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic or explicit Type overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public Task Publish(object message, PublishOptions publishOptions, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(message); @@ -151,6 +181,26 @@ public Task Publish(object message, PublishOptions publishOptions, CancellationT return messageSession.Publish(message, publishOptions, cancellationToken); } + [OverloadResolutionPriority(-1)] + public Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, PublishOptions publishOptions, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(publishOptions); + + GuardAgainstUseWhenNotStarted(); + return messageSession.Publish(message, publishOptions, cancellationToken); + } + + public Task Publish(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, PublishOptions publishOptions, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + ArgumentNullException.ThrowIfNull(publishOptions); + + GuardAgainstUseWhenNotStarted(); + return messageSession.Publish(message, messageType, publishOptions, cancellationToken); + } + public Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, PublishOptions publishOptions, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(messageConstructor); diff --git a/src/NServiceBus.Testing.Fakes/DynamicMemberTypeAccess.cs b/src/NServiceBus.Testing.Fakes/DynamicMemberTypeAccess.cs new file mode 100644 index 00000000000..2686c9b749c --- /dev/null +++ b/src/NServiceBus.Testing.Fakes/DynamicMemberTypeAccess.cs @@ -0,0 +1,14 @@ +namespace NServiceBus.Testing; + +using System.Diagnostics.CodeAnalysis; + +static class DynamicMemberTypeAccess +{ + public const DynamicallyAccessedMemberTypes Message = DynamicallyAccessedMemberTypes.PublicConstructors + | DynamicallyAccessedMemberTypes.NonPublicConstructors + | DynamicallyAccessedMemberTypes.PublicProperties + | DynamicallyAccessedMemberTypes.Interfaces; + + public const string RuntimeTypeRoutingTrimmingMessage = "When trimming is enabled, routing a message using its runtime type cannot be statically analyzed by the trimmer. Use the generic overload or, when the message type is not known at compile time, the overload accepting an explicit Type."; + +} diff --git a/src/NServiceBus.Testing.Fakes/NServiceBus.Testing.Fakes.csproj b/src/NServiceBus.Testing.Fakes/NServiceBus.Testing.Fakes.csproj index 38497d82768..64ad33a4a79 100644 --- a/src/NServiceBus.Testing.Fakes/NServiceBus.Testing.Fakes.csproj +++ b/src/NServiceBus.Testing.Fakes/NServiceBus.Testing.Fakes.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/src/NServiceBus.Testing.Fakes/TestableMessageProcessingContext.cs b/src/NServiceBus.Testing.Fakes/TestableMessageProcessingContext.cs index 376b17fb671..b7b1e4f3674 100644 --- a/src/NServiceBus.Testing.Fakes/TestableMessageProcessingContext.cs +++ b/src/NServiceBus.Testing.Fakes/TestableMessageProcessingContext.cs @@ -4,6 +4,8 @@ namespace NServiceBus.Testing; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading.Tasks; /// @@ -38,17 +40,41 @@ public TestableMessageProcessingContext(IMessageCreator messageCreator = null) : /// /// The message to send. /// Options for this reply. + [RequiresUnreferencedCode(DynamicMemberTypeAccess.RuntimeTypeRoutingTrimmingMessage)] public virtual Task Reply(object message, ReplyOptions options) { repliedMessages.Enqueue(new RepliedMessage(message, options)); return Task.CompletedTask; } + /// + /// Sends the typed message to the endpoint which sent the message currently being handled. + /// + /// The type used to reply with the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The message to send. + /// Options for this reply. + [OverloadResolutionPriority(-1)] + public virtual Task Reply<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, ReplyOptions options) + { +#pragma warning disable IDE0004 // Cast is redundant + return Reply((object)message!, options); +#pragma warning restore IDE0004 + } + + /// + /// Sends the message with the specified message type to the endpoint which sent the message currently being handled. + /// + public virtual Task Reply(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, ReplyOptions options) + { + MessageTypeValidator.Validate(message, messageType); + return Reply(message, options); + } + /// /// Instantiates a message of type T and performs a regular /// . /// - /// The type of message, usually an interface. + /// The type used to reply with the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// An action which initializes properties of the message. /// Options for this reply. public virtual Task Reply(Action messageConstructor, ReplyOptions options) diff --git a/src/NServiceBus.Testing.Fakes/TestableMessageSession.cs b/src/NServiceBus.Testing.Fakes/TestableMessageSession.cs index 6a92fa88501..3f9ccf74f3a 100644 --- a/src/NServiceBus.Testing.Fakes/TestableMessageSession.cs +++ b/src/NServiceBus.Testing.Fakes/TestableMessageSession.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using NServiceBus.Extensibility; @@ -45,6 +47,7 @@ public TestableMessageSession(IMessageCreator messageCreator = null) /// /// The message to send. /// The options for the send. + [RequiresUnreferencedCode(DynamicMemberTypeAccess.RuntimeTypeRoutingTrimmingMessage)] public virtual Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) { var headers = options.GetHeaders(); @@ -58,10 +61,33 @@ public virtual Task Send(object message, SendOptions options, CancellationToken return Task.CompletedTask; } + /// + /// Sends the provided typed message. + /// + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The message to send. + /// The options for the send. + [OverloadResolutionPriority(-1)] + public virtual Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, SendOptions options, CancellationToken cancellationToken = default) + { +#pragma warning disable IDE0004 // Cast is redundant + return Send((object)message!, options, cancellationToken); +#pragma warning restore IDE0004 + } + + /// + /// Sends the provided message with the specified message type. + /// + public virtual Task Send(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, SendOptions options, CancellationToken cancellationToken = default) + { + MessageTypeValidator.Validate(message, messageType); + return Send(message, options, cancellationToken); + } + /// /// Instantiates a message of type T and sends it. /// - /// The type of message, usually an interface. + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// An action which initializes properties of the message. /// The options for the send. public virtual Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) @@ -74,16 +100,40 @@ public virtual Task Send(Action messageConstructor, SendOptions options, C /// /// The message to publish. /// The options for the publish. + [RequiresUnreferencedCode(DynamicMemberTypeAccess.RuntimeTypeRoutingTrimmingMessage)] public virtual Task Publish(object message, PublishOptions options, CancellationToken cancellationToken = default) { publishedMessages.Enqueue(new PublishedMessage(message, options)); return Task.CompletedTask; } + /// + /// Publishes the provided typed message. + /// + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The message to publish. + /// The options for the publish. + [OverloadResolutionPriority(-1)] + public virtual Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, PublishOptions options, CancellationToken cancellationToken = default) + { +#pragma warning disable IDE0004 // Cast is redundant + return Publish((object)message!, options, cancellationToken); +#pragma warning restore IDE0004 + } + + /// + /// Publishes the provided message with the specified message type. + /// + public virtual Task Publish(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, PublishOptions options, CancellationToken cancellationToken = default) + { + MessageTypeValidator.Validate(message, messageType); + return Publish(message, options, cancellationToken); + } + /// /// Instantiates a message of type T and publishes it. /// - /// The type of message, usually an interface. + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// An action which initializes properties of the message. /// Specific options for this event. public virtual Task Publish(Action messageConstructor, PublishOptions publishOptions, CancellationToken cancellationToken = default) diff --git a/src/NServiceBus.Testing.Fakes/TestableOutgoingLogicalMessageContext.cs b/src/NServiceBus.Testing.Fakes/TestableOutgoingLogicalMessageContext.cs index 01277fb6018..170b8bec21b 100644 --- a/src/NServiceBus.Testing.Fakes/TestableOutgoingLogicalMessageContext.cs +++ b/src/NServiceBus.Testing.Fakes/TestableOutgoingLogicalMessageContext.cs @@ -1,6 +1,9 @@ namespace NServiceBus.Testing; +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using Pipeline; using Routing; @@ -12,11 +15,30 @@ public partial class TestableOutgoingLogicalMessageContext : TestableOutgoingCon /// /// Updates the message instance. /// + [RequiresUnreferencedCode(DynamicMemberTypeAccess.RuntimeTypeRoutingTrimmingMessage)] public virtual void UpdateMessage(object newInstance) { Message = new OutgoingLogicalMessage(newInstance.GetType(), newInstance); } + /// + /// Updates the message instance while preserving the specified message type. + /// + [OverloadResolutionPriority(-1)] + public virtual void UpdateMessage<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newInstance) + { + UpdateMessage(newInstance!, typeof(T)); + } + + /// + /// Updates the message instance with the specified message type. + /// + public virtual void UpdateMessage(object newInstance, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + MessageTypeValidator.Validate(newInstance, messageType); + Message = new OutgoingLogicalMessage(messageType, newInstance); + } + /// /// The outgoing message. /// diff --git a/src/NServiceBus.Testing.Fakes/TestablePipelineContext.cs b/src/NServiceBus.Testing.Fakes/TestablePipelineContext.cs index ca256ad6b42..27673834210 100644 --- a/src/NServiceBus.Testing.Fakes/TestablePipelineContext.cs +++ b/src/NServiceBus.Testing.Fakes/TestablePipelineContext.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Extensibility; @@ -47,6 +48,7 @@ public TestablePipelineContext(IMessageCreator messageCreator = null) /// /// The message to send. /// The options for the send. + [RequiresUnreferencedCode(DynamicMemberTypeAccess.RuntimeTypeRoutingTrimmingMessage)] public virtual Task Send(object message, SendOptions options) { var headers = options.GetHeaders(); @@ -60,10 +62,33 @@ public virtual Task Send(object message, SendOptions options) return Task.CompletedTask; } + /// + /// Sends the provided typed message. + /// + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The message to send. + /// The options for the send. + [OverloadResolutionPriority(-1)] + public virtual Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, SendOptions options) + { +#pragma warning disable IDE0004 // Cast is redundant + return Send((object)message!, options); +#pragma warning restore IDE0004 + } + + /// + /// Sends the provided message with the specified message type. + /// + public virtual Task Send(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, SendOptions options) + { + MessageTypeValidator.Validate(message, messageType); + return Send(message, options); + } + /// /// Instantiates a message of type T and sends it. /// - /// The type of message, usually an interface. + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// An action which initializes properties of the message. /// The options for the send. public virtual Task Send(Action messageConstructor, SendOptions options) @@ -76,16 +101,40 @@ public virtual Task Send(Action messageConstructor, SendOptions options) /// /// The message to publish. /// The options for the publish. + [RequiresUnreferencedCode(DynamicMemberTypeAccess.RuntimeTypeRoutingTrimmingMessage)] public virtual Task Publish(object message, PublishOptions options) { publishedMessages.Enqueue(new PublishedMessage(message, options)); return Task.CompletedTask; } + /// + /// Publishes the provided typed message. + /// + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The message to publish. + /// The options for the publish. + [OverloadResolutionPriority(-1)] + public virtual Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, PublishOptions options) + { +#pragma warning disable IDE0004 // Cast is redundant + return Publish((object)message!, options); +#pragma warning restore IDE0004 + } + + /// + /// Publishes the provided message with the specified message type. + /// + public virtual Task Publish(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, PublishOptions options) + { + MessageTypeValidator.Validate(message, messageType); + return Publish(message, options); + } + /// /// Instantiates a message of type T and publishes it. /// - /// The type of message, usually an interface. + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// An action which initializes properties of the message. /// Specific options for this event. public virtual Task Publish(Action messageConstructor, PublishOptions publishOptions) diff --git a/src/Shared/MessageTypeValidator.cs b/src/Shared/MessageTypeValidator.cs new file mode 100644 index 00000000000..dbfdd0d0d63 --- /dev/null +++ b/src/Shared/MessageTypeValidator.cs @@ -0,0 +1,22 @@ +namespace NServiceBus; + +using System; +using System.Diagnostics.CodeAnalysis; + +static class MessageTypeValidator +{ + internal static void Validate(object message, [DynamicallyAccessedMembers(MessageTypeAccess)] Type messageType) + { + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + if (!messageType.IsInstanceOfType(message)) + { + throw new ArgumentException($"The message instance of type '{message.GetType()}' is not assignable to the declared message type '{messageType}'.", nameof(message)); + } + } + + const DynamicallyAccessedMemberTypes MessageTypeAccess = DynamicallyAccessedMemberTypes.PublicConstructors + | DynamicallyAccessedMemberTypes.NonPublicConstructors + | DynamicallyAccessedMemberTypes.PublicProperties + | DynamicallyAccessedMemberTypes.Interfaces; +} From 3618b605395e4d420a67aa25bbc460a220802d8e Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 21:48:17 +0000 Subject: [PATCH 56/79] Update dependency NUnit3TestAdapter to 6.3.0 (#7915) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- .../NServiceBus.AcceptanceTests.csproj | 2 +- .../NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj | 2 +- src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj | 2 +- .../NServiceBus.Learning.AcceptanceTests.csproj | 2 +- .../NServiceBus.PersistenceTests.csproj | 2 +- .../NServiceBus.TransportTests.csproj | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj b/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj index d181fe5b3bc..3413d11e2ba 100644 --- a/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj +++ b/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj index 42d1b8009fa..297aa3abad2 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj @@ -20,7 +20,7 @@ - + diff --git a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj index c500ec289cd..c581ebb7e4f 100644 --- a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj +++ b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj b/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj index d7f6bd8f6da..960de1aa004 100644 --- a/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj +++ b/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj b/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj index 7f31352f514..c7faf593d5f 100644 --- a/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj +++ b/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj b/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj index 26412a155ec..dc70a6fc9d1 100644 --- a/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj +++ b/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj @@ -13,7 +13,7 @@ - + From 0b4238722caf923f4fcec28fbbbac9c0e255a0ad Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 27 Aug 2026 21:20:31 +0200 Subject: [PATCH 57/79] Prepare Core for unsafe context changes in v11 (#7916) * Address unsafe context in Pooled Buffer Writers * Switch NonCryptoHash to span based only to avoid unsafe propagation * Bound safety in SourceWriter to be contained in the safe boundaries * Safety comments on Unsafe.As (they don't flag yet in v11 preview 7) --------- Co-authored-by: Daniel Marbach --- .../Utility/NonCryptographicHash.cs | 18 +++++------------- .../Utility/SourceWriter.cs | 10 +++++++--- src/NServiceBus.Core/Pipeline/IBehavior.cs | 1 + src/NServiceBus.Core/Pipeline/InvokerNode.cs | 1 + .../Unicast/MessageHandlerRegistry.cs | 2 ++ .../Utils/ArrayPoolBufferWriter.cs | 16 ++++++++++++++-- .../Utils/LazyArrayPoolBufferWriter.cs | 3 +-- 7 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/NServiceBus.Core.Analyzer/Utility/NonCryptographicHash.cs b/src/NServiceBus.Core.Analyzer/Utility/NonCryptographicHash.cs index 648015c28cf..1b218c6c6ad 100644 --- a/src/NServiceBus.Core.Analyzer/Utility/NonCryptographicHash.cs +++ b/src/NServiceBus.Core.Analyzer/Utility/NonCryptographicHash.cs @@ -1,8 +1,6 @@ namespace NServiceBus.Core.Analyzer.Utility; using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; /// /// 64-bit FNV-1a over chars, https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function @@ -21,34 +19,28 @@ public static ulong GetHash(params ReadOnlySpan parts) { string part = parts[index]; ReadOnlySpan span = part.AsSpan(); - ref char first = ref MemoryMarshal.GetReference(span); int length = span.Length; int i = 0; // Process 4 chars at a time for (; i + 3 < length; i += 4) { - char c0 = Unsafe.Add(ref first, i); - hash ^= c0; + hash ^= span[i]; hash *= prime; - char c1 = Unsafe.Add(ref first, i + 1); - hash ^= c1; + hash ^= span[i + 1]; hash *= prime; - char c2 = Unsafe.Add(ref first, i + 2); - hash ^= c2; + hash ^= span[i + 2]; hash *= prime; - char c3 = Unsafe.Add(ref first, i + 3); - hash ^= c3; + hash ^= span[i + 3]; hash *= prime; } // Handle remainder (0–3 chars) for (; i < length; i++) { - char c = Unsafe.Add(ref first, i); - hash ^= c; + hash ^= span[i]; hash *= prime; } } diff --git a/src/NServiceBus.Core.Analyzer/Utility/SourceWriter.cs b/src/NServiceBus.Core.Analyzer/Utility/SourceWriter.cs index 678d331ac8e..b0f28138b3c 100644 --- a/src/NServiceBus.Core.Analyzer/Utility/SourceWriter.cs +++ b/src/NServiceBus.Core.Analyzer/Utility/SourceWriter.cs @@ -110,12 +110,16 @@ static ReadOnlySpan GetNextLine(ref ReadOnlySpan remainingText, out return next; } - static unsafe void AppendSpan(StringBuilder builder, ReadOnlySpan span) + static void AppendSpan(StringBuilder builder, ReadOnlySpan span) { // There is no StringBuilder.Append(ReadOnlySpan) overload in the NS2.0 - fixed (char* ptr = span) + // SAFETY: fixed pins the span for the synchronous Append call; length is explicit, no null terminator assumed. + unsafe { - builder.Append(ptr, span.Length); + fixed (char* ptr = span) + { + builder.Append(ptr, span.Length); + } } } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Pipeline/IBehavior.cs b/src/NServiceBus.Core/Pipeline/IBehavior.cs index 8568c4a48e9..b6f517264a6 100644 --- a/src/NServiceBus.Core/Pipeline/IBehavior.cs +++ b/src/NServiceBus.Core/Pipeline/IBehavior.cs @@ -32,6 +32,7 @@ InvokerNode IBehavior.CreateInvokerNode(InvokerNode? next) else { Func fn = next.Invoke; + // SAFETY: Pipeline wiring guarantees next is invoked only with TOutContext (a reference type); the delegate retype is representation-preserving. nextFunc = Unsafe.As, Func>(ref fn); } diff --git a/src/NServiceBus.Core/Pipeline/InvokerNode.cs b/src/NServiceBus.Core/Pipeline/InvokerNode.cs index 0094f5d5578..21d1116e99f 100644 --- a/src/NServiceBus.Core/Pipeline/InvokerNode.cs +++ b/src/NServiceBus.Core/Pipeline/InvokerNode.cs @@ -30,6 +30,7 @@ sealed class InvokerNode(IBehavior(ref obj), next); } } diff --git a/src/NServiceBus.Core/Unicast/MessageHandlerRegistry.cs b/src/NServiceBus.Core/Unicast/MessageHandlerRegistry.cs index 0d81379ba3a..ab7b2d0bba1 100644 --- a/src/NServiceBus.Core/Unicast/MessageHandlerRegistry.cs +++ b/src/NServiceBus.Core/Unicast/MessageHandlerRegistry.cs @@ -248,6 +248,7 @@ public MessageHandler Create() => static readonly ObjectFactory factory = ActivatorUtilities.CreateFactory([]); static readonly ObjectFactory> handlerFactory = + // SAFETY: Registration validated THandler implements IHandleTimeouts; retype skips the interface cast. static (sp, args) => Unsafe.As>(factory(sp, args)); } @@ -274,6 +275,7 @@ public MessageHandler Create() => static readonly ObjectFactory factory = ActivatorUtilities.CreateFactory([]); static readonly ObjectFactory> handlerFactory = + // SAFETY: Registration validated THandler implements IHandleMessages; retype skips the interface cast. static (sp, args) => Unsafe.As>(factory(sp, args)); } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Utils/ArrayPoolBufferWriter.cs b/src/NServiceBus.Core/Utils/ArrayPoolBufferWriter.cs index 3f83dfc16e2..8cc86e157c7 100644 --- a/src/NServiceBus.Core/Utils/ArrayPoolBufferWriter.cs +++ b/src/NServiceBus.Core/Utils/ArrayPoolBufferWriter.cs @@ -5,7 +5,6 @@ namespace NServiceBus; using System; using System.Buffers; using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; sealed class ArrayPoolBufferWriter( ArrayPool pool, @@ -126,7 +125,20 @@ public Span GetSpan(int sizeHint = 0) } - Memory IMemoryOwner.Memory => MemoryMarshal.AsMemory(WrittenMemory); + Memory IMemoryOwner.Memory + { + get + { + T[]? array = buffer; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return array.AsMemory(0, WrittenCount); + } + } public void Dispose() { diff --git a/src/NServiceBus.Core/Utils/LazyArrayPoolBufferWriter.cs b/src/NServiceBus.Core/Utils/LazyArrayPoolBufferWriter.cs index a125bd40c07..9d021c8449c 100644 --- a/src/NServiceBus.Core/Utils/LazyArrayPoolBufferWriter.cs +++ b/src/NServiceBus.Core/Utils/LazyArrayPoolBufferWriter.cs @@ -5,7 +5,6 @@ namespace NServiceBus; using System; using System.Buffers; using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; sealed class LazyArrayPoolBufferWriter(int? capacity = null) : IBufferWriter, IMemoryOwner { @@ -56,7 +55,7 @@ public Span GetSpan(int sizeHint = 0) public void Clear() => innerWriter?.Clear(); public void Dispose() => innerWriter?.Dispose(); - Memory IMemoryOwner.Memory => MemoryMarshal.AsMemory(WrittenMemory); + Memory IMemoryOwner.Memory => innerWriter is null ? Memory.Empty : ((IMemoryOwner)innerWriter).Memory; [MemberNotNull(nameof(innerWriter))] void EnsureInnerWriter() => innerWriter ??= capacity is null ? new ArrayPoolBufferWriter() : new ArrayPoolBufferWriter(capacity.Value); From 7660e88ea94bc1620a3cbbceea3610ac063025a7 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Wed, 2 Sep 2026 17:24:46 +0200 Subject: [PATCH 58/79] Support trimming-safe message metadata without assembly scanning (#7918) * Add characterization tests for message metadata and serializer behavior * Add cache-only message metadata lookup and reflection-free logical message creation * Add AddMessageType with interceptor and trimming-safe registration * Add executable scanner-disabled trimmed endpoint validation and fix pipeline behavior trimming * Pass generated correlation accessor to SagaMapper, emit finder-only sagas, and gate interceptor suppressions * Key generated saga correlation accessors by saga-data type and exercise them at runtime * Share message hierarchy ordering between handler and AddMessageType generation * Fix saga trim annotations and exercise saga scenarios in the trimmed endpoint sample * Verify build-transitive package consumption of trimming-safe registrations * Add executable NativeAOT validation for scanner-disabled endpoint * Activate strict registered-only message metadata mode for disabled scanning in trimmed endpoints * Assert strict registered-only metadata mode in the trimmed endpoint sample * Verify EnableTrimAnalyzer generates interceptors without activating strict runtime mode * Verify PublishAot alone emits the runtime strict-mode signal * Cleanup trimming program * Modernize slightly * Remove attribute approach and replace with target * Reuse existing configs * Remove overly excessive test because it is too internals focused * Remove Directory.Build.props changes * Tweaks to TrimmedEndpoint naming / location * Import the files instead * Rename target * Clean up usings * Formatting * Clarify conventions * Extend trimming tests * Restoring the trimming warnings from before * Refactor strict missing metadata exception handling * Wording suggestion Co-authored-by: David Boike * Add nullable reference type support to GetType method * Improve message --------- Co-authored-by: Daniel Marbach Co-authored-by: Brandon Ording Co-authored-by: David Boike --- ...ceptorTests.BasicMessageTypes.approved.txt | 51 +++ ...eTypesWithEqualRankInterfaces.approved.txt | 46 +++ ...sts.MessageTypesWithHierarchy.approved.txt | 57 ++++ ...SagaGeneratorTests.BasicSagas.approved.txt | 58 +++- ...ratorTests.CastSyntaxWrappers.approved.txt | 16 +- ...initionsWithCompilationErrors.approved.txt | 16 +- ...nBodiedConfigureHowToFindSaga.approved.txt | 16 +- ...eneratorTests.FinderOnlySagas.approved.txt | 172 ++++++++++ ...ests.InheritedMessageProperty.approved.txt | 16 +- ...dMappingWithCompilationErrors.approved.txt | 16 +- ...agaGeneratorTests.NestedSagas.approved.txt | 37 ++- ...NullableReferenceTypeMappings.approved.txt | 16 +- ...bleReferenceTypeMixedMappings.approved.txt | 37 ++- ...yConstructorAndSyntaxWrappers.approved.txt | 16 +- ...egistrationMethodNamePatterns.approved.txt | 16 +- ...Tests.RootClassEntryPointName.approved.txt | 16 +- ...otClassVisibilityAndNamespace.approved.txt | 16 +- ...sts.UnrelatedCompilationError.approved.txt | 16 +- ...mpilationErrorInDifferentFile.approved.txt | 16 +- ...gaInterceptorTests.BasicSagas.approved.txt | 58 +++- ...eptorTests.CastSyntaxWrappers.approved.txt | 16 +- ...initionsWithCompilationErrors.approved.txt | 16 +- ...nBodiedConfigureHowToFindSaga.approved.txt | 16 +- ...ests.InheritedMessageProperty.approved.txt | 16 +- ...dMappingWithCompilationErrors.approved.txt | 16 +- ...aInterceptorTests.NestedSagas.approved.txt | 37 ++- ...NullableReferenceTypeMappings.approved.txt | 16 +- ...bleReferenceTypeMixedMappings.approved.txt | 37 ++- ...yConstructorAndSyntaxWrappers.approved.txt | 16 +- ...sts.UnrelatedCompilationError.approved.txt | 16 +- ...mpilationErrorInDifferentFile.approved.txt | 16 +- ...ddMessageTypeInterceptorSuppressorTests.cs | 114 +++++++ .../AddMessageTypeInterceptorTests.cs | 165 +++++++++ .../Sagas/AddSagaGeneratorTests.cs | 55 +++ .../AddSagaInterceptorSuppressorTests.cs | 54 +++ ...eratedCorrelationAccessorExecutionTests.cs | 137 ++++++++ .../Handlers/Handlers.Parser.cs | 51 +-- .../AddMessageTypeInterceptor.Emitter.cs | 124 +++++++ .../AddMessageTypeInterceptor.Parser.cs | 101 ++++++ .../AddMessageTypeInterceptor.Suppressor.cs | 61 ++++ ...AddMessageTypeInterceptor.TrackingNames.cs | 16 + .../Messages/AddMessageTypeInterceptor.cs | 39 +++ .../Sagas/AddSagaInterceptor.Suppressor.cs | 17 +- .../Sagas/Sagas.Emitter.cs | 52 ++- .../Sagas/Sagas.Parser.cs | 9 +- .../SupressionIds.cs | 1 + .../Utility/MessageHierarchyBuilder.cs | 58 ++++ ...IApprovals.ApproveNServiceBus.approved.txt | 35 +- ...s.ApproveTrimmabilityWarnings.approved.txt | 7 - .../AssemblyScanningComponentTests.cs | 106 +++++- .../Hosting/AppContextSwitchesTests.cs | 56 ++++ ...oming_mutator_replaces_message_instance.cs | 101 ++++++ .../Pipeline/LogicalMessageFactoryTests.cs | 135 ++++++++ .../Sagas/SagaMetadataCreationTests.cs | 46 +++ .../JsonMessageSerializerRootTypeTests.cs | 59 ++++ ...lMessageSerializerCharacterizationTests.cs | 89 +++++ .../TrimmedEndpoint/PackageConsumerTests.cs | 210 ++++++++++++ .../TrimmedEndpointTests.cs | 186 +++++++++++ .../MessageTypeRegistrationExtensionsTests.cs | 92 ++++++ ...ageMetadataRegistryCacheOnlyLookupTests.cs | 112 +++++++ ...geMetadataRegistryCharacterizationTests.cs | 103 ++++++ ...egistryStrictModeInteractionMatrixTests.cs | 145 ++++++++ .../MessageMetadataRegistryStrictModeTests.cs | 96 ++++++ src/NServiceBus.Core/AppContextSwitches.cs | 29 ++ src/NServiceBus.Core/EndpointCreator.cs | 9 +- .../Helpers/AssemblyScanningComponent.cs | 33 +- src/NServiceBus.Core/NServiceBus.Core.csproj | 5 +- src/NServiceBus.Core/NServiceBus.targets | 21 ++ .../Pipeline/BehaviorTypeChecker.cs | 3 +- .../Incoming/LogicalMessageFactory.cs | 18 +- .../Pipeline/PipelineSettings.cs | 27 +- .../Pipeline/RegisterOrReplaceStep.cs | 3 +- src/NServiceBus.Core/Pipeline/RegisterStep.cs | 9 +- .../Pipeline/RegisterStepExtensions.cs | 9 +- src/NServiceBus.Core/Pipeline/ReplaceStep.cs | 4 +- src/NServiceBus.Core/Sagas/SagaMetadata.cs | 2 +- .../Sagas/SagaPersistenceBehavior.cs | 2 + .../MessageTypeRegistrationExtensions.cs | 36 ++ .../MessageMetadataRegistry.Legacy.cs | 66 ++++ .../Messages/MessageMetadataRegistry.cs | 113 ++++--- src/NServiceBus.slnx | 1 + src/TrimmedEndpoint/Program.cs | 312 ++++++++++++++++++ src/TrimmedEndpoint/TrimmedEndpoint.csproj | 35 ++ 83 files changed, 3893 insertions(+), 402 deletions(-) create mode 100644 src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddMessageTypeInterceptorTests.BasicMessageTypes.approved.txt create mode 100644 src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddMessageTypeInterceptorTests.MessageTypesWithEqualRankInterfaces.approved.txt create mode 100644 src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddMessageTypeInterceptorTests.MessageTypesWithHierarchy.approved.txt create mode 100644 src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.FinderOnlySagas.approved.txt create mode 100644 src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Messages/AddMessageTypeInterceptorSuppressorTests.cs create mode 100644 src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Messages/AddMessageTypeInterceptorTests.cs create mode 100644 src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/GeneratedCorrelationAccessorExecutionTests.cs create mode 100644 src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.Emitter.cs create mode 100644 src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.Parser.cs create mode 100644 src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.Suppressor.cs create mode 100644 src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.TrackingNames.cs create mode 100644 src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.cs create mode 100644 src/NServiceBus.Core.Analyzer/Utility/MessageHierarchyBuilder.cs create mode 100644 src/NServiceBus.Core.Tests/Hosting/AppContextSwitchesTests.cs create mode 100644 src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/When_incoming_mutator_replaces_message_instance.cs create mode 100644 src/NServiceBus.Core.Tests/Pipeline/LogicalMessageFactoryTests.cs create mode 100644 src/NServiceBus.Core.Tests/Serializers/SystemTextJson/JsonMessageSerializerRootTypeTests.cs create mode 100644 src/NServiceBus.Core.Tests/Serializers/XML/XmlMessageSerializerCharacterizationTests.cs create mode 100644 src/NServiceBus.Core.Tests/TrimmedEndpoint/PackageConsumerTests.cs create mode 100644 src/NServiceBus.Core.Tests/TrimmedEndpointTests.cs create mode 100644 src/NServiceBus.Core.Tests/Unicast/Config/MessageTypeRegistrationExtensionsTests.cs create mode 100644 src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryCacheOnlyLookupTests.cs create mode 100644 src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryCharacterizationTests.cs create mode 100644 src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryStrictModeInteractionMatrixTests.cs create mode 100644 src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryStrictModeTests.cs create mode 100644 src/NServiceBus.Core/NServiceBus.targets create mode 100644 src/NServiceBus.Core/Unicast/Config/MessageTypeRegistrationExtensions.cs create mode 100644 src/NServiceBus.Core/Unicast/Messages/MessageMetadataRegistry.Legacy.cs create mode 100644 src/TrimmedEndpoint/Program.cs create mode 100644 src/TrimmedEndpoint/TrimmedEndpoint.csproj diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddMessageTypeInterceptorTests.BasicMessageTypes.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddMessageTypeInterceptorTests.BasicMessageTypes.approved.txt new file mode 100644 index 00000000000..1afcf437878 --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddMessageTypeInterceptorTests.BasicMessageTypes.approved.txt @@ -0,0 +1,51 @@ +// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.Messages.AddMessageTypeInterceptor/InterceptionsOfAddMessageTypeMethod.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace System.Runtime.CompilerServices +{ + [global::System.Diagnostics.Conditional("DEBUG")] + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] + sealed file class InterceptsLocationAttribute : global::System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + _ = version; + _ = data; + } + } +} + +namespace NServiceBus +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + static file class InterceptionsOfAddMessageTypeMethod + { + extension (NServiceBus.EndpointConfiguration endpointConfiguration) + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "{PLATFORM-SPECIFIC-BASE64-DATA}")] // test.cs(8,13) + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "{PLATFORM-SPECIFIC-BASE64-DATA}")] // test.cs(10,13) + public void AddMessageType_OrderBilled_baccb8cecdf46c5c() + { + System.ArgumentNullException.ThrowIfNull(endpointConfiguration); + var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); + var messageMetadataRegistry = settings.GetOrCreate(); + messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof(global::Messages.OrderBilled), []); + } + + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "{PLATFORM-SPECIFIC-BASE64-DATA}")] // test.cs(7,13) + public void AddMessageType_OrderPlaced_957130e0bad70f6d() + { + System.ArgumentNullException.ThrowIfNull(endpointConfiguration); + var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); + var messageMetadataRegistry = settings.GetOrCreate(); + messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof(global::Messages.OrderPlaced), []); + } + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddMessageTypeInterceptorTests.MessageTypesWithEqualRankInterfaces.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddMessageTypeInterceptorTests.MessageTypesWithEqualRankInterfaces.approved.txt new file mode 100644 index 00000000000..4033d38acf9 --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddMessageTypeInterceptorTests.MessageTypesWithEqualRankInterfaces.approved.txt @@ -0,0 +1,46 @@ +// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.Messages.AddMessageTypeInterceptor/InterceptionsOfAddMessageTypeMethod.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace System.Runtime.CompilerServices +{ + [global::System.Diagnostics.Conditional("DEBUG")] + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] + sealed file class InterceptsLocationAttribute : global::System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + _ = version; + _ = data; + } + } +} + +namespace NServiceBus +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + static file class InterceptionsOfAddMessageTypeMethod + { + extension (NServiceBus.EndpointConfiguration endpointConfiguration) + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "{PLATFORM-SPECIFIC-BASE64-DATA}")] // test.cs(7,13) + public void AddMessageType_OrderAccepted_f4c4dd469ffffa61() + { + System.ArgumentNullException.ThrowIfNull(endpointConfiguration); + var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); + var messageMetadataRegistry = settings.GetOrCreate(); + messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof(global::Messages.OrderAccepted), + [ + typeof(global::Messages.ISecond), + typeof(global::Messages.IFirst), + typeof(global::Messages.OrderEventBase), + ]); + } + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddMessageTypeInterceptorTests.MessageTypesWithHierarchy.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddMessageTypeInterceptorTests.MessageTypesWithHierarchy.approved.txt new file mode 100644 index 00000000000..ab0de4f1621 --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddMessageTypeInterceptorTests.MessageTypesWithHierarchy.approved.txt @@ -0,0 +1,57 @@ +// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.Messages.AddMessageTypeInterceptor/InterceptionsOfAddMessageTypeMethod.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace System.Runtime.CompilerServices +{ + [global::System.Diagnostics.Conditional("DEBUG")] + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] + sealed file class InterceptsLocationAttribute : global::System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + _ = version; + _ = data; + } + } +} + +namespace NServiceBus +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + static file class InterceptionsOfAddMessageTypeMethod + { + extension (NServiceBus.EndpointConfiguration endpointConfiguration) + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "{PLATFORM-SPECIFIC-BASE64-DATA}")] // test.cs(7,13) + public void AddMessageType_OrderAccepted_f4c4dd469ffffa61() + { + System.ArgumentNullException.ThrowIfNull(endpointConfiguration); + var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); + var messageMetadataRegistry = settings.GetOrCreate(); + messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof(global::Messages.OrderAccepted), + [ + typeof(global::Messages.IOrderEvent), + typeof(global::Messages.OrderEventBase), + ]); + } + + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "{PLATFORM-SPECIFIC-BASE64-DATA}")] // test.cs(8,13) + public void AddMessageType_OrderRejected_6d51746e2bdec64a() + { + System.ArgumentNullException.ThrowIfNull(endpointConfiguration); + var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); + var messageMetadataRegistry = settings.GetOrCreate(); + messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof(global::Messages.OrderRejected), + [ + typeof(global::Messages.OrderEventBase), + ]); + } + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.BasicSagas.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.BasicSagas.approved.txt index d00b044903f..0f38c697e01 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.BasicSagas.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.BasicSagas.approved.txt @@ -39,7 +39,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_f7dc3394c771851e.Instance, OrderPlacedOrderIdAccessor_ae6ad694572b9fcb.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_5b84242b5a03d625.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -77,7 +77,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_08b7b00354e1d041.Instance, OrderPlacedOrderIdAccessor_13dea9c2dc628538.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_1073497622a938fb.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -115,7 +115,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_2fd27f5436ef20b9.Instance, OrderPlacedOrderIdAccessor_c561311f0fcc5cf0.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_6d931b1825e0c4ca.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -216,21 +216,59 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_5b84242b5a03d625 : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_5b84242b5a03d625() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.Billing.OrderBillingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::Orders.Billing.OrderBillingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.Billing.OrderBillingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::Orders.Billing.OrderBillingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_5b84242b5a03d625(); + } + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + file sealed class OrderIdAsStringAccessor_1073497622a938fb : NServiceBus.Sagas.CorrelationPropertyAccessor + { + OrderIdAsStringAccessor_1073497622a938fb() { } + + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.Shipping.OrderShippingPolicyData)sagaData); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] + static extern string AccessFrom_Property(global::Orders.Shipping.OrderShippingPolicyData sagaData); + + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.Shipping.OrderShippingPolicyData)sagaData, ((string)value)); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] + static extern void WriteTo_Property(global::Orders.Shipping.OrderShippingPolicyData sagaData, string value); + + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_1073497622a938fb(); + } + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + file sealed class OrderIdAsStringAccessor_6d931b1825e0c4ca : NServiceBus.Sagas.CorrelationPropertyAccessor + { + OrderIdAsStringAccessor_6d931b1825e0c4ca() { } + + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Payments.PaymentsPolicyData)sagaData); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] + static extern string AccessFrom_Property(global::Payments.PaymentsPolicyData sagaData); + + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Payments.PaymentsPolicyData)sagaData, ((string)value)); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] + static extern void WriteTo_Property(global::Payments.PaymentsPolicyData sagaData, string value); + + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_6d931b1825e0c4ca(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.CastSyntaxWrappers.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.CastSyntaxWrappers.approved.txt index 265d4284842..78c9bbac3bc 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.CastSyntaxWrappers.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.CastSyntaxWrappers.approved.txt @@ -32,7 +32,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -60,21 +60,21 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.DuplicatePropertyDefinitionsWithCompilationErrors.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.DuplicatePropertyDefinitionsWithCompilationErrors.approved.txt index 724b801bec9..87732f11532 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.DuplicatePropertyDefinitionsWithCompilationErrors.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.DuplicatePropertyDefinitionsWithCompilationErrors.approved.txt @@ -36,7 +36,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -64,21 +64,21 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.ExpressionBodiedConfigureHowToFindSaga.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.ExpressionBodiedConfigureHowToFindSaga.approved.txt index c2665ccb763..8b3074abd2d 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.ExpressionBodiedConfigureHowToFindSaga.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.ExpressionBodiedConfigureHowToFindSaga.approved.txt @@ -32,7 +32,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -60,21 +60,21 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.FinderOnlySagas.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.FinderOnlySagas.approved.txt new file mode 100644 index 00000000000..58ad635b85d --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.FinderOnlySagas.approved.txt @@ -0,0 +1,172 @@ +// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.Sagas.AddSagaGenerator/HandlerRegistrations.Sagas.g.cs ======= +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace NServiceBus +{ + public static partial class FinderOnlySagasHandlerRegistryExtensions + { + public sealed partial class FinderOnlySagasRootRegistry + { + public sealed partial class FinderOnlyRegistry + { + partial void AddAllSagasCore() + { + AddFinderOnlySaga(); + } + + /// + /// Registers the saga with the endpoint configuration. + /// + public void AddFinderOnlySaga() + { + var sagaMetadataCollection = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration) + .GetOrCreate(); + var associatedMessages = new NServiceBus.Sagas.SagaMessage[] + { + new NServiceBus.Sagas.SagaMessage(typeof(global::FinderOnly.StartSagaMessage), true, false), + }; + NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ + ]; + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, null, propertyAccessors); + sagaMetadataCollection.Add(metadata); + + var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); + var messageHandlerRegistry = settings.GetOrCreate(); + var messageMetadataRegistry = settings.GetOrCreate(); + messageHandlerRegistry.AddMessageHandlerForMessage(); + messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof(global::FinderOnly.StartSagaMessage), []); + } + } + } + } +} + +// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.AddHandlerAndSagasRegistrationGenerator/HandlerRegistrations.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace NServiceBus +{ + /// + /// Provides access to handler and saga registries discovered in the FinderOnlySagas assembly. + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + public static partial class FinderOnlySagasHandlerRegistryExtensions + { + extension (global::NServiceBus.HandlerRegistry registry) + { + /// + /// Gets the root registry for handler and saga types in the FinderOnlySagas assembly. + /// + /// + /// Use the returned registry to access namespace-specific registries and add-all methods for this assembly. + /// + public FinderOnlySagasRootRegistry FinderOnlySagasAssembly => new(registry.Configuration); + } + + /// + /// Root registry to add handlers and sagas for the entire assembly. + /// + public sealed partial class FinderOnlySagasRootRegistry(global::NServiceBus.EndpointConfiguration configuration) + { + readonly global::NServiceBus.EndpointConfiguration _configuration = configuration ?? throw new System.ArgumentNullException(nameof(configuration)); + + /// + /// Gets the registry for the FinderOnly namespace segment under this branch. + /// + public FinderOnlyRegistry FinderOnly => new(_configuration); + + /// + /// Registers all handlers and sagas for this namespace segment and its child namespaces. + /// + /// + /// Includes child registries: . + /// + public void AddAll() + { + AddAllHandlers(); + AddAllSagas(); + } + + /// + /// Registers all handlers for this namespace segment and its child namespaces. + /// + /// + /// Includes child registries: . + /// + public void AddAllHandlers() + { + AddAllHandlersCore(); + FinderOnly.AddAllHandlers(); + } + + /// + /// Registers all sagas for this namespace segment and its child namespaces. + /// + /// + /// Includes child registries: . + /// + public void AddAllSagas() + { + AddAllSagasCore(); + FinderOnly.AddAllSagas(); + } + + partial void AddAllHandlersCore(); + partial void AddAllSagasCore(); + + /// + /// Registry for the FinderOnly namespace segment. Use this registry to add handlers and sagas for this branch. + /// + public sealed partial class FinderOnlyRegistry(global::NServiceBus.EndpointConfiguration configuration) + { + readonly global::NServiceBus.EndpointConfiguration _configuration = configuration ?? throw new System.ArgumentNullException(nameof(configuration)); + + /// + /// Registers all handlers and sagas for this namespace segment and its child namespaces. + /// + /// + /// Includes sagas in this namespace: . + /// + public void AddAll() + { + AddAllHandlers(); + AddAllSagas(); + } + + /// + /// Registers all handlers for this namespace segment and its child namespaces. + /// + public void AddAllHandlers() + { + AddAllHandlersCore(); + } + + /// + /// Registers all sagas for this namespace segment and its child namespaces. + /// + /// + /// Includes sagas in this namespace: . + /// + public void AddAllSagas() + { + AddAllSagasCore(); + } + + partial void AddAllHandlersCore(); + partial void AddAllSagasCore(); + } + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.InheritedMessageProperty.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.InheritedMessageProperty.approved.txt index 06e984864bb..df2a17f6793 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.InheritedMessageProperty.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.InheritedMessageProperty.approved.txt @@ -34,7 +34,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_b1eaacebc632acfc.Instance, OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -78,21 +78,21 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.InvalidMappingWithCompilationErrors.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.InvalidMappingWithCompilationErrors.approved.txt index f2c9729d9b0..de20830dbeb 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.InvalidMappingWithCompilationErrors.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.InvalidMappingWithCompilationErrors.approved.txt @@ -37,7 +37,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -67,21 +67,21 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NestedSagas.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NestedSagas.approved.txt index 5b51dace7c6..96efb4cd9d2 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NestedSagas.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NestedSagas.approved.txt @@ -40,7 +40,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_b1eaacebc632acfc.Instance, OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_f53ba98a08e6a113.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -71,7 +71,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_b1eaacebc632acfc.Instance, OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_14af697526a7e650.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -119,21 +119,40 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_f53ba98a08e6a113 : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_f53ba98a08e6a113() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.Shipping.AnotherOuterClass.InnerClass.OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::Orders.Shipping.AnotherOuterClass.InnerClass.OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.Shipping.AnotherOuterClass.InnerClass.OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::Orders.Shipping.AnotherOuterClass.InnerClass.OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_f53ba98a08e6a113(); + } + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + file sealed class OrderIdAsStringAccessor_14af697526a7e650 : NServiceBus.Sagas.CorrelationPropertyAccessor + { + OrderIdAsStringAccessor_14af697526a7e650() { } + + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.Shipping.OuterClass.OrderShippingPolicyData)sagaData); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] + static extern string AccessFrom_Property(global::Orders.Shipping.OuterClass.OrderShippingPolicyData sagaData); + + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.Shipping.OuterClass.OrderShippingPolicyData)sagaData, ((string)value)); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] + static extern void WriteTo_Property(global::Orders.Shipping.OuterClass.OrderShippingPolicyData sagaData, string value); + + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_14af697526a7e650(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NullableReferenceTypeMappings.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NullableReferenceTypeMappings.approved.txt index a03f7dfca72..0760d9f2908 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NullableReferenceTypeMappings.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NullableReferenceTypeMappings.approved.txt @@ -32,7 +32,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -60,21 +60,21 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NullableReferenceTypeMixedMappings.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NullableReferenceTypeMixedMappings.approved.txt index 0820fbd3914..c2e2436f0b8 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NullableReferenceTypeMixedMappings.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NullableReferenceTypeMixedMappings.approved.txt @@ -33,7 +33,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ NullableMessageOrderIdAccessor_9894122651576960.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_5e4ee6a1b6193efa.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -57,7 +57,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ NonNullableMessageOrderIdAccessor_e023774f080a36b1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_acfa87ac0bc7f48b.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -99,21 +99,40 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_5e4ee6a1b6193efa : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_5e4ee6a1b6193efa() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::NonNullableSagaData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::NonNullableSagaData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::NonNullableSagaData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::NonNullableSagaData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_5e4ee6a1b6193efa(); + } + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + file sealed class OrderIdAsStringAccessor_acfa87ac0bc7f48b : NServiceBus.Sagas.CorrelationPropertyAccessor + { + OrderIdAsStringAccessor_acfa87ac0bc7f48b() { } + + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::NullableSagaData)sagaData); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] + static extern string AccessFrom_Property(global::NullableSagaData sagaData); + + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::NullableSagaData)sagaData, ((string)value)); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] + static extern void WriteTo_Property(global::NullableSagaData sagaData, string value); + + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_acfa87ac0bc7f48b(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.PrimaryConstructorAndSyntaxWrappers.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.PrimaryConstructorAndSyntaxWrappers.approved.txt index 236fcd287fc..b3fc1a83617 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.PrimaryConstructorAndSyntaxWrappers.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.PrimaryConstructorAndSyntaxWrappers.approved.txt @@ -36,7 +36,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ StartPartitionSagaCommandCorrelationIdAccessor_64596642731407e6.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, CorrelationIdAsStringAccessor_566e8fad769818c2.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, CorrelationIdAsStringAccessor_90ab90feac5d44a9.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -66,21 +66,21 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class CorrelationIdAsStringAccessor_566e8fad769818c2 : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class CorrelationIdAsStringAccessor_90ab90feac5d44a9 : NServiceBus.Sagas.CorrelationPropertyAccessor { - CorrelationIdAsStringAccessor_566e8fad769818c2() { } + CorrelationIdAsStringAccessor_90ab90feac5d44a9() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.Shipping.PartitionedEndpointSagaData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_CorrelationId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::Orders.Shipping.PartitionedEndpointSagaData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.Shipping.PartitionedEndpointSagaData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_CorrelationId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::Orders.Shipping.PartitionedEndpointSagaData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new CorrelationIdAsStringAccessor_566e8fad769818c2(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new CorrelationIdAsStringAccessor_90ab90feac5d44a9(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RegistrationMethodNamePatterns.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RegistrationMethodNamePatterns.approved.txt index 464bc193ff3..a56c5b2f9e7 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RegistrationMethodNamePatterns.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RegistrationMethodNamePatterns.approved.txt @@ -37,7 +37,7 @@ namespace CustomRegistrations OrderBilledOrderIdAccessor_6b40737e550b4237.Instance, OrderPlacedOrderIdAccessor_92819afdda929c86.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_828f31182d7d1121.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -84,21 +84,21 @@ namespace CustomRegistrations [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_828f31182d7d1121 : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_828f31182d7d1121() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::Orders.OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::Orders.OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_828f31182d7d1121(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RootClassEntryPointName.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RootClassEntryPointName.approved.txt index 6f585f24d40..6000f2c3c7e 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RootClassEntryPointName.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RootClassEntryPointName.approved.txt @@ -37,7 +37,7 @@ namespace CustomRegistrations OrderBilledOrderIdAccessor_6b40737e550b4237.Instance, OrderPlacedOrderIdAccessor_92819afdda929c86.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_828f31182d7d1121.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -84,21 +84,21 @@ namespace CustomRegistrations [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_828f31182d7d1121 : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_828f31182d7d1121() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::Orders.OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::Orders.OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_828f31182d7d1121(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RootClassVisibilityAndNamespace.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RootClassVisibilityAndNamespace.approved.txt index 90a1263b548..edbb57111e7 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RootClassVisibilityAndNamespace.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RootClassVisibilityAndNamespace.approved.txt @@ -37,7 +37,7 @@ namespace CustomRegistrations OrderBilledOrderIdAccessor_6b40737e550b4237.Instance, OrderPlacedOrderIdAccessor_92819afdda929c86.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_828f31182d7d1121.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -84,21 +84,21 @@ namespace CustomRegistrations [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_828f31182d7d1121 : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_828f31182d7d1121() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::Orders.OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::Orders.OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_828f31182d7d1121(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.UnrelatedCompilationError.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.UnrelatedCompilationError.approved.txt index 7fdf5d741d2..43663e2e5d1 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.UnrelatedCompilationError.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.UnrelatedCompilationError.approved.txt @@ -40,7 +40,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_13dea9c2dc628538.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_1073497622a938fb.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -70,21 +70,21 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_1073497622a938fb : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_1073497622a938fb() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.Shipping.OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::Orders.Shipping.OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.Shipping.OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::Orders.Shipping.OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_1073497622a938fb(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.UnrelatedCompilationErrorInDifferentFile.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.UnrelatedCompilationErrorInDifferentFile.approved.txt index dc7542e5795..51051bbec47 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.UnrelatedCompilationErrorInDifferentFile.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.UnrelatedCompilationErrorInDifferentFile.approved.txt @@ -35,7 +35,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -63,21 +63,21 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.BasicSagas.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.BasicSagas.approved.txt index 33346c87b2b..7e44b65f9db 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.BasicSagas.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.BasicSagas.approved.txt @@ -43,7 +43,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_f7dc3394c771851e.Instance, OrderPlacedOrderIdAccessor_ae6ad694572b9fcb.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_5b84242b5a03d625.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -70,7 +70,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_08b7b00354e1d041.Instance, OrderPlacedOrderIdAccessor_13dea9c2dc628538.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_1073497622a938fb.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -98,7 +98,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_2fd27f5436ef20b9.Instance, OrderPlacedOrderIdAccessor_c561311f0fcc5cf0.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_6d931b1825e0c4ca.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -198,20 +198,58 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_5b84242b5a03d625 : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_5b84242b5a03d625() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.Billing.OrderBillingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::Orders.Billing.OrderBillingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.Billing.OrderBillingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::Orders.Billing.OrderBillingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_5b84242b5a03d625(); + } + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + file sealed class OrderIdAsStringAccessor_1073497622a938fb : NServiceBus.Sagas.CorrelationPropertyAccessor + { + OrderIdAsStringAccessor_1073497622a938fb() { } + + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.Shipping.OrderShippingPolicyData)sagaData); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] + static extern string AccessFrom_Property(global::Orders.Shipping.OrderShippingPolicyData sagaData); + + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.Shipping.OrderShippingPolicyData)sagaData, ((string)value)); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] + static extern void WriteTo_Property(global::Orders.Shipping.OrderShippingPolicyData sagaData, string value); + + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_1073497622a938fb(); + } + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + file sealed class OrderIdAsStringAccessor_6d931b1825e0c4ca : NServiceBus.Sagas.CorrelationPropertyAccessor + { + OrderIdAsStringAccessor_6d931b1825e0c4ca() { } + + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Payments.PaymentsPolicyData)sagaData); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] + static extern string AccessFrom_Property(global::Payments.PaymentsPolicyData sagaData); + + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Payments.PaymentsPolicyData)sagaData, ((string)value)); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] + static extern void WriteTo_Property(global::Payments.PaymentsPolicyData sagaData, string value); + + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_6d931b1825e0c4ca(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.CastSyntaxWrappers.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.CastSyntaxWrappers.approved.txt index cd1210d4c17..8df0da1fb82 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.CastSyntaxWrappers.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.CastSyntaxWrappers.approved.txt @@ -41,7 +41,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -69,20 +69,20 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.DuplicatePropertyDefinitionsWithCompilationErrors.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.DuplicatePropertyDefinitionsWithCompilationErrors.approved.txt index 1280eb58a00..f966e69c5c8 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.DuplicatePropertyDefinitionsWithCompilationErrors.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.DuplicatePropertyDefinitionsWithCompilationErrors.approved.txt @@ -45,7 +45,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -73,20 +73,20 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.ExpressionBodiedConfigureHowToFindSaga.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.ExpressionBodiedConfigureHowToFindSaga.approved.txt index cd1210d4c17..8df0da1fb82 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.ExpressionBodiedConfigureHowToFindSaga.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.ExpressionBodiedConfigureHowToFindSaga.approved.txt @@ -41,7 +41,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -69,20 +69,20 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.InheritedMessageProperty.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.InheritedMessageProperty.approved.txt index 53c7e23bd80..575988c4066 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.InheritedMessageProperty.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.InheritedMessageProperty.approved.txt @@ -43,7 +43,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_b1eaacebc632acfc.Instance, OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -87,20 +87,20 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.InvalidMappingWithCompilationErrors.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.InvalidMappingWithCompilationErrors.approved.txt index 2b682c353e5..7be1504be11 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.InvalidMappingWithCompilationErrors.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.InvalidMappingWithCompilationErrors.approved.txt @@ -46,7 +46,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -76,20 +76,20 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NestedSagas.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NestedSagas.approved.txt index 4bd8988b7ae..3625190fe64 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NestedSagas.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NestedSagas.approved.txt @@ -44,7 +44,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_b1eaacebc632acfc.Instance, OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_f53ba98a08e6a113.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -71,7 +71,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_b1eaacebc632acfc.Instance, OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_14af697526a7e650.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -115,20 +115,39 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_f53ba98a08e6a113 : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_f53ba98a08e6a113() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.Shipping.AnotherOuterClass.InnerClass.OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::Orders.Shipping.AnotherOuterClass.InnerClass.OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.Shipping.AnotherOuterClass.InnerClass.OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::Orders.Shipping.AnotherOuterClass.InnerClass.OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_f53ba98a08e6a113(); + } + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + file sealed class OrderIdAsStringAccessor_14af697526a7e650 : NServiceBus.Sagas.CorrelationPropertyAccessor + { + OrderIdAsStringAccessor_14af697526a7e650() { } + + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.Shipping.OuterClass.OrderShippingPolicyData)sagaData); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] + static extern string AccessFrom_Property(global::Orders.Shipping.OuterClass.OrderShippingPolicyData sagaData); + + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.Shipping.OuterClass.OrderShippingPolicyData)sagaData, ((string)value)); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] + static extern void WriteTo_Property(global::Orders.Shipping.OuterClass.OrderShippingPolicyData sagaData, string value); + + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_14af697526a7e650(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NullableReferenceTypeMappings.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NullableReferenceTypeMappings.approved.txt index bee3c640509..20e177e79b7 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NullableReferenceTypeMappings.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NullableReferenceTypeMappings.approved.txt @@ -41,7 +41,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -69,20 +69,20 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NullableReferenceTypeMixedMappings.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NullableReferenceTypeMixedMappings.approved.txt index 939a7eefc53..8b82b283923 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NullableReferenceTypeMixedMappings.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NullableReferenceTypeMixedMappings.approved.txt @@ -41,7 +41,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ NullableMessageOrderIdAccessor_9894122651576960.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_5e4ee6a1b6193efa.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -64,7 +64,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ NonNullableMessageOrderIdAccessor_e023774f080a36b1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_acfa87ac0bc7f48b.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -106,20 +106,39 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_5e4ee6a1b6193efa : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_5e4ee6a1b6193efa() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::NonNullableSagaData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::NonNullableSagaData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::NonNullableSagaData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::NonNullableSagaData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_5e4ee6a1b6193efa(); + } + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + file sealed class OrderIdAsStringAccessor_acfa87ac0bc7f48b : NServiceBus.Sagas.CorrelationPropertyAccessor + { + OrderIdAsStringAccessor_acfa87ac0bc7f48b() { } + + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::NullableSagaData)sagaData); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] + static extern string AccessFrom_Property(global::NullableSagaData sagaData); + + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::NullableSagaData)sagaData, ((string)value)); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] + static extern void WriteTo_Property(global::NullableSagaData sagaData, string value); + + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_acfa87ac0bc7f48b(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.PrimaryConstructorAndSyntaxWrappers.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.PrimaryConstructorAndSyntaxWrappers.approved.txt index ac792a61d81..db03f8c2565 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.PrimaryConstructorAndSyntaxWrappers.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.PrimaryConstructorAndSyntaxWrappers.approved.txt @@ -41,7 +41,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ StartPartitionSagaCommandCorrelationIdAccessor_82483382109e24ab.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, CorrelationIdAsStringAccessor_566e8fad769818c2.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, CorrelationIdAsStringAccessor_14cc7db5f4370166.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -69,20 +69,20 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class CorrelationIdAsStringAccessor_566e8fad769818c2 : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class CorrelationIdAsStringAccessor_14cc7db5f4370166 : NServiceBus.Sagas.CorrelationPropertyAccessor { - CorrelationIdAsStringAccessor_566e8fad769818c2() { } + CorrelationIdAsStringAccessor_14cc7db5f4370166() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::PartitionedEndpointSagaData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_CorrelationId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::PartitionedEndpointSagaData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::PartitionedEndpointSagaData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_CorrelationId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::PartitionedEndpointSagaData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new CorrelationIdAsStringAccessor_566e8fad769818c2(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new CorrelationIdAsStringAccessor_14cc7db5f4370166(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.UnrelatedCompilationError.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.UnrelatedCompilationError.approved.txt index 65e1d2b5d70..e035f56dc56 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.UnrelatedCompilationError.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.UnrelatedCompilationError.approved.txt @@ -44,7 +44,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -72,20 +72,20 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.UnrelatedCompilationErrorInDifferentFile.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.UnrelatedCompilationErrorInDifferentFile.approved.txt index 81382fd821c..d0af0ef91ee 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.UnrelatedCompilationErrorInDifferentFile.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.UnrelatedCompilationErrorInDifferentFile.approved.txt @@ -44,7 +44,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -72,20 +72,20 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Messages/AddMessageTypeInterceptorSuppressorTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Messages/AddMessageTypeInterceptorSuppressorTests.cs new file mode 100644 index 00000000000..27a69fa4afd --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Messages/AddMessageTypeInterceptorSuppressorTests.cs @@ -0,0 +1,114 @@ +namespace NServiceBus.Core.Analyzer.Tests.Messages; + +using Analyzer.Messages; +using Helpers; +using NUnit.Framework; +using Particular.AnalyzerTesting; + +[TestFixture] +public class AddMessageTypeInterceptorSuppressorTests +{ + [Test] + public void SuppressesIL2026ForAddMessageType() + { + var source = """ + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.AddMessageType(); + } + } + + public class SampleMessage : IEvent + { + public string OrderId { get; set; } + } + """; + + var result = SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .WithAnalyzer() + .WithSuppressor() + .Run(); + + var diagnostics = result.GetCompilationOutput(); + + Assert.That(diagnostics, Does.Not.Contain("IL2026")); + } + + [Test] + public void DoesNotSuppressIL2026ForAddMessageTypeWithGenericTypeParameter() + { + var source = """ + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + Register(cfg); + } + + // The type argument is a generic type parameter, so no interceptor can be generated + // and the RequiresUnreferencedCode fallback warning must not be suppressed. + public void Register(EndpointConfiguration cfg) where TMessage : IMessage + { + cfg.AddMessageType(); + } + } + + public class MyMessage : IEvent + { + public string OrderId { get; set; } + } + """; + + var result = SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .WithAnalyzer() + .WithSuppressor() + .SuppressDiagnosticErrors() + .SuppressCompilationErrors() + .Run(); + + var diagnostics = result.GetCompilationOutput(); + + Assert.That(diagnostics, Does.Contain("IL2026")); + } + + [Test] + public void DoesNotSuppressIL2026ForNonAddMessageTypeCalls() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + // This call should still produce IL2026 since it's not intercepted + SomeOtherMethod(); + } + + [RequiresUnreferencedCode("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("IL2026")); + } +} diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Messages/AddMessageTypeInterceptorTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Messages/AddMessageTypeInterceptorTests.cs new file mode 100644 index 00000000000..3c6d9a2548b --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Messages/AddMessageTypeInterceptorTests.cs @@ -0,0 +1,165 @@ +namespace NServiceBus.Core.Analyzer.Tests.Messages; + +using Analyzer.Messages; +using NUnit.Framework; +using Particular.AnalyzerTesting; + +[TestFixture] +public class AddMessageTypeInterceptorTests +{ + [Test] + public void BasicMessageTypes() + { + var source = """ + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.AddMessageType(); + cfg.AddMessageType(); + // Duplicate call, methods should be deduped with 2 InterceptsLocation attributes + cfg.AddMessageType(); + } + } + + namespace Messages + { + public class OrderPlaced : IEvent + { + public string OrderId { get; set; } + } + + public class OrderBilled : IEvent + { + public string OrderId { get; set; } + } + } + """; + + SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .Run() + .Approve() + .AssertRunsAreEqual(); + } + + [Test] + public void MessageTypesWithEqualRankInterfaces() + { + var source = """ + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.AddMessageType(); + } + } + + namespace Messages + { + // Declared in reverse-alphabetical order: equal-rank interfaces must keep declaration order + // (matching runtime reflection and handler generation) rather than being alphabetically reordered. + public class OrderAccepted : OrderEventBase, ISecond, IFirst + { + } + + public class OrderEventBase : IEvent + { + public string OrderId { get; set; } + } + + public interface IFirst : IEvent + { + } + + public interface ISecond : IEvent + { + } + } + """; + + SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .Run() + .Approve() + .AssertRunsAreEqual(); + } + + [Test] + public void MessageTypesWithHierarchy() + { + var source = """ + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.AddMessageType(); + cfg.AddMessageType(); + } + } + + namespace Messages + { + public class OrderAccepted : OrderEventBase, IOrderEvent + { + } + + public class OrderRejected : OrderEventBase + { + } + + public class OrderEventBase : IEvent + { + public string OrderId { get; set; } + } + + public interface IOrderEvent : IEvent + { + } + } + """; + + SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .Run() + .Approve() + .AssertRunsAreEqual(); + } + + [Test] + public void Interceptors_are_generated_in_warning_only_trim_analyzer_builds() + { + var source = """ + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.AddMessageType(); + } + } + + public class MyMessage : IEvent + { + public string OrderId { get; set; } + } + """; + + // Interceptor support must be generated in warning-only (EnableTrimAnalyzer) builds so the reflection + // fallback warning can be suppressed there. + var output = SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .WithProperty("build_property.EnableTrimAnalyzer", "true") + .Run() + .GetCompilationOutput(); + + Assert.That(output, Does.Contain("InterceptionsOfAddMessageTypeMethod.g.cs")); + } +} diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/AddSagaGeneratorTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/AddSagaGeneratorTests.cs index 59beefb9597..29f01011ae3 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/AddSagaGeneratorTests.cs +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/AddSagaGeneratorTests.cs @@ -7,6 +7,61 @@ [TestFixture] public class AddSagaGeneratorTests { + [Test] + public void FinderOnlySagas() + { + 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.Handlers.FinderOnlySagasAssembly.AddAll(); + } + } + + namespace FinderOnly + { + [Saga] + 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; + } + """; + + SourceGeneratorTest.ForIncrementalGenerator() + .WithIncrementalGenerator() + .WithSource(source, "test.cs") + .Run() + .Approve() + .AssertRunsAreEqual(); + } + [Test] public void BasicSagas() { diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/AddSagaInterceptorSuppressorTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/AddSagaInterceptorSuppressorTests.cs index ef0f6d5a498..cbcc9fdfaab 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/AddSagaInterceptorSuppressorTests.cs +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/AddSagaInterceptorSuppressorTests.cs @@ -57,6 +57,60 @@ public class SampleCommand : ICommand Assert.That(diagnostics, Does.Not.Contain("IL2026")); } + [Test] + public void SuppressesIL2026ForFinderOnlySaga() + { + 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("IL2026")); + } + [Test] public void DoesNotSuppressIL2026ForNonAddSagaCalls() { diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/GeneratedCorrelationAccessorExecutionTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/GeneratedCorrelationAccessorExecutionTests.cs new file mode 100644 index 00000000000..17a05cb69fd --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/GeneratedCorrelationAccessorExecutionTests.cs @@ -0,0 +1,137 @@ +namespace NServiceBus.Core.Analyzer.Tests.Sagas; + +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using Analyzer; +using Analyzer.Sagas; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using NServiceBus.Sagas; +using NUnit.Framework; + +[TestFixture] +public class GeneratedCorrelationAccessorExecutionTests +{ + [Test] + public void Generated_correlation_accessors_round_trip_for_colliding_saga_data_properties() + { + var source = """ + using System.Threading.Tasks; + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.Handlers.CollidingAccessorsAssembly.AddAll(); + } + } + + namespace First + { + [Saga] + public class SagaA : Saga, IAmStartedByMessages + { + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) => + mapper.MapSaga(s => s.CorrelationId).ToMessage(m => m.CorrelationId); + + public Task Handle(StartMessageA message, IMessageHandlerContext context) => Task.CompletedTask; + } + + public class SagaAData : ContainSagaData + { + public string CorrelationId { get; set; } + } + + public class StartMessageA : ICommand + { + public string CorrelationId { get; set; } + } + } + + namespace Second + { + [Saga] + public class SagaB : Saga, IAmStartedByMessages + { + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) => + mapper.MapSaga(s => s.CorrelationId).ToMessage(m => m.CorrelationId); + + public Task Handle(StartMessageB message, IMessageHandlerContext context) => Task.CompletedTask; + } + + public class SagaBData : ContainSagaData + { + public string CorrelationId { get; set; } + } + + public class StartMessageB : ICommand + { + public string CorrelationId { get; set; } + } + } + """; + + var assembly = CompileAndLoad(source); + + // Two saga-data classes with a colliding property name/type must not share one generated accessor. + var accessorTypes = assembly.GetTypes() + .Where(t => typeof(CorrelationPropertyAccessor).IsAssignableFrom(t) && !t.IsAbstract) + .ToArray(); + + Assert.That(accessorTypes, Has.Length.EqualTo(2), "Each saga-data class must get its own generated correlation accessor."); + + foreach (var accessorType in accessorTypes) + { + var sagaDataType = accessorType + .GetMethod("AccessFrom_Property", BindingFlags.Static | BindingFlags.NonPublic)! + .GetParameters()[0] + .ParameterType; + var sagaData = (IContainSagaData)Activator.CreateInstance(sagaDataType); + var accessor = (CorrelationPropertyAccessor)accessorType.GetField("Instance")!.GetValue(null)!; + + accessor.WriteTo(sagaData, "correlation-value"); + var value = accessor.AccessFrom(sagaData); + + Assert.That(value, Is.EqualTo("correlation-value"), $"Accessor for {sagaDataType.Name} did not round-trip the correlation value."); + } + } + + static Assembly CompileAndLoad(string source) + { + var parseOptions = new CSharpParseOptions(LanguageVersion.Preview); + var sourceTree = CSharpSyntaxTree.ParseText(source, parseOptions); + + var compilation = CSharpCompilation.Create( + "CollidingAccessors", + [sourceTree], + ReferenceAssemblyPaths(), + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable)); + + var driver = CSharpGeneratorDriver.Create( + [ + new AddSagaGenerator().AsSourceGenerator(), + new AddHandlerAndSagasRegistrationGenerator().AsSourceGenerator() + ], + parseOptions: parseOptions); + + driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out _); + + using var peStream = new MemoryStream(); + var emitResult = outputCompilation.Emit(peStream); + + var errors = emitResult.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToArray(); + Assert.That(errors, Is.Empty, string.Join(Environment.NewLine, errors.Select(e => e.ToString()))); + + return Assembly.Load(peStream.ToArray()); + } + + static MetadataReference[] ReferenceAssemblyPaths() => + [ + .. AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !string.IsNullOrWhiteSpace(a.Location)) + .Select(MetadataReference (a) => MetadataReference.CreateFromFile(a.Location)) + ]; +} diff --git a/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Parser.cs b/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Parser.cs index abdb9fe17b6..1bb644d3303 100644 --- a/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Parser.cs +++ b/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Parser.cs @@ -444,55 +444,6 @@ static string BuildAdapterName(INamedTypeSymbol handlerType, IMethodSymbol metho static bool IsHandlerInterface(INamedTypeSymbol type) => HandlerConventions.IsHandlerInterface(type); static IEnumerable GetTypeHierarchy(INamedTypeSymbol type, MarkerTypes markers) => - // This matches the behavior of the reflection-based code, but it's unclear why this ordering is needed. - // It would be more efficient to yield the base types (except where type.SpecialType is not SpecialType.System_Object) - // and then to yield the interfaces from type.AllInterfaces except those in the MarkerTypes. - // We're hesitant to change the implementation, however, due to wire compatibility concerns of outputting - // an EnclosedMessageTypes header with a different ordering. - GetParentTypes(type) - .Where(t => !markers.IsMarkerInterface(t)) - .Select(t => new { Type = t, Rank = PlaceInMessageHierarchy(t) }) - .OrderByDescending(item => item.Rank) - .Select(item => item.Type); - - static IEnumerable GetParentTypes(INamedTypeSymbol type) - { - // All interfaces implemented by the type (includes inherited interfaces) - foreach (var iface in type.AllInterfaces) - { - yield return iface; - } - - // All base types up to but excluding System.Object - var currentBase = type.BaseType; - while (currentBase is { SpecialType: not SpecialType.System_Object }) - { - if (currentBase is { } named) - { - yield return named; - } - - currentBase = currentBase.BaseType; - } - } - - static int PlaceInMessageHierarchy(INamedTypeSymbol type) - { - if (type.TypeKind == TypeKind.Interface) - { - // Approximate: number of interfaces implemented by this interface - return type.AllInterfaces.Length; - } - - var result = 0; - var current = type.BaseType; - while (current is not null) - { - result++; - current = current.BaseType; - } - - return result; - } + MessageHierarchyBuilder.GetTypeHierarchy(type, markers); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.Emitter.cs b/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.Emitter.cs new file mode 100644 index 00000000000..4e2e3b7125b --- /dev/null +++ b/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.Emitter.cs @@ -0,0 +1,124 @@ +#nullable enable + +namespace NServiceBus.Core.Analyzer.Messages; + +using System; +using System.Linq; +using Microsoft.CodeAnalysis; +using NServiceBus.Core.Analyzer; +using Utility; + +public sealed partial class AddMessageTypeInterceptor +{ + internal class Emitter(SourceProductionContext sourceProductionContext) + { + public void Emit(InterceptableMessageTypeSpecs messageTypeSpecs) => Emit(sourceProductionContext, messageTypeSpecs); + + static void Emit(SourceProductionContext context, InterceptableMessageTypeSpecs messageTypeSpecs) + { + var interceptableMessageTypeSpecs = messageTypeSpecs.MessageTypes; + if (interceptableMessageTypeSpecs.Count == 0) + { + return; + } + + var sourceWriter = new SourceWriter() + .ForInterceptor() + .WithGeneratedCodeAttribute(); + + sourceWriter.WriteLine(""" + static file class InterceptionsOfAddMessageTypeMethod + { + """); + + sourceWriter.Indentation++; + + sourceWriter.WriteLine(""" + extension (NServiceBus.EndpointConfiguration endpointConfiguration) + { + """); + sourceWriter.Indentation++; + + var groups = interceptableMessageTypeSpecs.Select(m => (MethodName: AddMethodName(m.MessageTypeSpec.Name, m.MessageTypeSpec.FullyQualifiedName), InterceptableMessageType: m)) + .GroupBy(i => i.MethodName) + .OrderBy(g => g.Key, StringComparer.Ordinal) + .ToArray(); + for (int index = 0; index < groups.Length; index++) + { + IGrouping group = groups[index]; + (string MethodName, InterceptableMessageTypeSpec InterceptableMessageType)? first = null; + foreach (var location in group) + { + first ??= location; + + var (_, messageType) = location; + sourceWriter.WriteLine($"{messageType.LocationSpec.Attribute} // {messageType.LocationSpec.DisplayLocation}"); + } + + if (!first.HasValue) + { + continue; + } + + (string methodName, InterceptableMessageTypeSpec interceptableMessageType) = first.Value; + sourceWriter.WriteLine($$""" + public void {{methodName}}() + { + """); + sourceWriter.Indentation++; + + sourceWriter.WriteLine("System.ArgumentNullException.ThrowIfNull(endpointConfiguration);"); + + EmitMessageTypeRegistration(sourceWriter, interceptableMessageType.MessageTypeSpec); + + sourceWriter.Indentation--; + sourceWriter.WriteLine("}"); + + if (index < groups.Length - 1) + { + sourceWriter.WriteLine(); + } + } + + sourceWriter.Indentation--; + sourceWriter.WriteLine("}"); + + sourceWriter.Indentation--; + sourceWriter.WriteLine("}"); + + sourceWriter.CloseCurlies(); + + context.AddSource("InterceptionsOfAddMessageTypeMethod.g.cs", sourceWriter.ToSourceText()); + } + + static void EmitMessageTypeRegistration(SourceWriter sourceWriter, MessageTypeSpec messageTypeSpec) + { + sourceWriter.WriteLine(""" + var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); + var messageMetadataRegistry = settings.GetOrCreate(); + """); + + if (messageTypeSpec.HierarchyTypeNames.Count == 0) + { + sourceWriter.WriteLine($"messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof({messageTypeSpec.FullyQualifiedName}), []);"); + return; + } + + sourceWriter.WriteLine($"messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof({messageTypeSpec.FullyQualifiedName}),"); + sourceWriter.WriteLine("["); + sourceWriter.Indentation++; + foreach (var hierarchyTypeName in messageTypeSpec.HierarchyTypeNames) + { + sourceWriter.WriteLine($"typeof({hierarchyTypeName}),"); + } + sourceWriter.Indentation--; + sourceWriter.WriteLine("]);"); + } + + static string AddMethodName(string name, string messageType) + { + const string NamePrefix = "AddMessageType_"; + return InterceptorMethodNameBuilder.Build(NamePrefix, name, messageType); + } + } +} diff --git a/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.Parser.cs b/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.Parser.cs new file mode 100644 index 00000000000..36d645d9670 --- /dev/null +++ b/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.Parser.cs @@ -0,0 +1,101 @@ +#nullable enable + +namespace NServiceBus.Core.Analyzer.Messages; + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Handlers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; +using NServiceBus.Core.Analyzer; +using Utility; + +public sealed partial class AddMessageTypeInterceptor +{ + internal readonly record struct MessageTypeSpec(string Name, string FullyQualifiedName, ImmutableEquatableArray HierarchyTypeNames) + { + public static MessageTypeSpec From(INamedTypeSymbol messageType, Compilation compilation) => + new( + string.Join("__", messageType.ToDisplayParts(SymbolDisplayFormat.FullyQualifiedFormat).Where(x => x.Kind == SymbolDisplayPartKind.ClassName)), + messageType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + GetHierarchyTypeNames(messageType, compilation)); + + static ImmutableEquatableArray GetHierarchyTypeNames(INamedTypeSymbol messageType, Compilation compilation) + { + // Shared with handler/saga generation so the emitted hierarchy has the same ordering regardless of which + // registration path wins (duplicate registration is first-wins in the message metadata registry). + return MessageHierarchyBuilder.GetTypeHierarchy(messageType, new MarkerTypes(compilation)) + .Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)) + .ToImmutableEquatableArray(); + } + } + + internal readonly record struct InterceptableMessageTypeSpec(InterceptLocationSpec LocationSpec, MessageTypeSpec MessageTypeSpec); + + internal readonly record struct InterceptableMessageTypeSpecs(ImmutableEquatableArray MessageTypes); + + internal static class Parser + { + public static bool SyntaxLooksLikeAddMessageTypeMethod(SyntaxNode node) => node is InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax + { + Name: GenericNameSyntax + { + Identifier.ValueText: AddMessageTypeMethodName, + TypeArgumentList.Arguments.Count: 1 + } + }, + ArgumentList.Arguments.Count: 0 + }; + + internal static bool IsAddMessageTypeMethod(IMethodSymbol method) => method is + { + Name: AddMessageTypeMethodName, + IsGenericMethod: true, + TypeArguments.Length: 1, + ContainingType: + { + Name: AddMessageTypeClassName, + ContainingNamespace: + { + Name: "NServiceBus", + ContainingNamespace.IsGlobalNamespace: true + } + } + }; + + public static InterceptableMessageTypeSpec? Parse(InvocationExpressionSyntax invocation, SemanticModel semanticModel, CancellationToken cancellationToken = default) + { + if (semanticModel.GetOperation(invocation, cancellationToken) is not IInvocationOperation operation) + { + return null; + } + + // Make sure the method we're looking at is ours and not some (extremely unlikely) copycat + if (!IsAddMessageTypeMethod(operation.TargetMethod)) + { + return null; + } + + if (operation.TargetMethod.TypeArguments[0] is not INamedTypeSymbol messageType) + { + return null; + } + + if (semanticModel.GetInterceptableLocation(invocation, cancellationToken) is not { } location) + { + return null; + } + + return new InterceptableMessageTypeSpec(InterceptLocationSpec.From(location), MessageTypeSpec.From(messageType, semanticModel.Compilation)); + } + + const string AddMessageTypeMethodName = "AddMessageType"; + const string AddMessageTypeClassName = "MessageTypeRegistrationExtensions"; + } +} diff --git a/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.Suppressor.cs b/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.Suppressor.cs new file mode 100644 index 00000000000..1b32ad84fad --- /dev/null +++ b/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.Suppressor.cs @@ -0,0 +1,61 @@ +namespace NServiceBus.Core.Analyzer.Messages; + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class AddMessageTypeInterceptorSuppressor : DiagnosticSuppressor +{ + static readonly SuppressionDescriptor SuppressRUCDiagnostic = new( + SupressionIds.AddMessageTypeInterceptorSuppression, + suppressedDiagnosticId: "IL2026", + justification: "The AddMessageType method has been intercepted by a statically generated variant."); + + public override void ReportSuppressions(SuppressionAnalysisContext context) + { + foreach (var diagnostic in context.ReportedDiagnostics) + { + if (diagnostic.Id != SuppressRUCDiagnostic.SuppressedDiagnosticId) + { + continue; + } + + var location = diagnostic.AdditionalLocations.Count > 0 ? diagnostic.AdditionalLocations[0] : diagnostic.Location; + if (location.SourceTree is not { } sourceTree) + { + continue; + } + + // The trim analyzer warns on the MemberAccessExpression since https://github.com/dotnet/runtime/pull/110086, + // so the location can be the invocation or its child. + var node = sourceTree.GetRoot().FindNode(location.SourceSpan) switch + { + InvocationExpressionSyntax s => s, + { Parent: InvocationExpressionSyntax s } => s, + _ => null, + }; + + if (node is null || !AddMessageTypeInterceptor.Parser.SyntaxLooksLikeAddMessageTypeMethod(node)) + { + continue; + } + + var semanticModel = context.GetSemanticModel(sourceTree); + + // Only suppress when an interceptor can actually be emitted for this call site. Calls with a generic + // type parameter cannot be intercepted because the hierarchy cannot be computed statically, so they keep + // the RequiresUnreferencedCode fallback warning. + if (AddMessageTypeInterceptor.Parser.Parse(node, semanticModel, context.CancellationToken) is null) + { + continue; + } + + context.ReportSuppression(Suppression.Create(SuppressRUCDiagnostic, diagnostic)); + } + } + + public override ImmutableArray SupportedSuppressions => [SuppressRUCDiagnostic]; +} diff --git a/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.TrackingNames.cs b/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.TrackingNames.cs new file mode 100644 index 00000000000..35d86f13abb --- /dev/null +++ b/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.TrackingNames.cs @@ -0,0 +1,16 @@ +namespace NServiceBus.Core.Analyzer.Messages; + +public sealed partial class AddMessageTypeInterceptor +{ + internal static class TrackingNames + { + public const string MessageTypeSpec = nameof(MessageTypeSpec); + public const string MessageTypeSpecs = nameof(MessageTypeSpecs); + + public static readonly string[] All = + [ + MessageTypeSpec, + MessageTypeSpecs, + ]; + } +} diff --git a/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.cs b/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.cs new file mode 100644 index 00000000000..4b594e2493a --- /dev/null +++ b/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.cs @@ -0,0 +1,39 @@ +#nullable enable + +namespace NServiceBus.Core.Analyzer.Messages; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using NServiceBus.Core.Analyzer; + +[Generator(LanguageNames.CSharp)] +public sealed partial class AddMessageTypeInterceptor : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var addMessageTypes = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: static (node, _) => Parser.SyntaxLooksLikeAddMessageTypeMethod(node), + transform: static (ctx, _) => (invocation: (InvocationExpressionSyntax)ctx.Node, semanticModel: ctx.SemanticModel)) + .Select(static (pair, cancellationToken) => + { + var (invocation, semanticModel) = pair; + return Parser.Parse(invocation!, semanticModel!, cancellationToken); + }) + .Where(static spec => spec.HasValue) + .Select(static (spec, _) => spec!.Value) + .WithTrackingName(TrackingNames.MessageTypeSpec); + + var collected = addMessageTypes.Collect() + .Select((specs, _) => new InterceptableMessageTypeSpecs(specs.ToImmutableEquatableArray())) + .WithTrackingName(TrackingNames.MessageTypeSpecs); + + context.RegisterSourceOutput(collected, + static (productionContext, spec) => + { + var emitter = new Emitter(productionContext); + emitter.Emit(spec); + }); + } +} diff --git a/src/NServiceBus.Core.Analyzer/Sagas/AddSagaInterceptor.Suppressor.cs b/src/NServiceBus.Core.Analyzer/Sagas/AddSagaInterceptor.Suppressor.cs index 796b0b6ba37..76be3d5a09e 100644 --- a/src/NServiceBus.Core.Analyzer/Sagas/AddSagaInterceptor.Suppressor.cs +++ b/src/NServiceBus.Core.Analyzer/Sagas/AddSagaInterceptor.Suppressor.cs @@ -1,6 +1,7 @@ namespace NServiceBus.Core.Analyzer.Sagas; using System.Collections.Immutable; +using Handlers; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; @@ -45,10 +46,22 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) var semanticModel = context.GetSemanticModel(sourceTree); var operation = semanticModel.GetOperation(node, context.CancellationToken); - if (operation is IInvocationOperation { TargetMethod: { } methodSymbol } && AddSagaInterceptor.Parser.IsAddSagaMethod(methodSymbol)) + if (operation is not IInvocationOperation { TargetMethod: { } methodSymbol } || !AddSagaInterceptor.Parser.IsAddSagaMethod(methodSymbol)) { - context.ReportSuppression(Suppression.Create(SuppressRUCDiagnostic, diagnostic)); + continue; + } + + // 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. + 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) + { + continue; } + + context.ReportSuppression(Suppression.Create(SuppressRUCDiagnostic, diagnostic)); } } diff --git a/src/NServiceBus.Core.Analyzer/Sagas/Sagas.Emitter.cs b/src/NServiceBus.Core.Analyzer/Sagas/Sagas.Emitter.cs index 85b61bbf32a..1d3033b9885 100644 --- a/src/NServiceBus.Core.Analyzer/Sagas/Sagas.Emitter.cs +++ b/src/NServiceBus.Core.Analyzer/Sagas/Sagas.Emitter.cs @@ -48,11 +48,14 @@ static void EmitSagaMetadataAdd(SourceWriter sourceWriter, SagaSpec details) sourceWriter.WriteLine($"{propertyAccessorClassName}.Instance,"); } - var correlationPropertyAccessorClassName = CorrelationPropertyAccessorName(details.CorrelationPropertyMapping); - var correlationPropertyAccessor = $"{correlationPropertyAccessorClassName}.Instance"; - sourceWriter.Indentation--; sourceWriter.WriteLine("];"); + + // Finder-only sagas have no correlation property and therefore no generated correlation accessor. + var correlationPropertyAccessor = details.CorrelationPropertyMapping is { } correlationProperty + ? $"{CorrelationPropertyAccessorName(details.SagaDataFullyQualifiedName, correlationProperty)}.Instance" + : "null"; + sourceWriter.WriteLine($"var metadata = NServiceBus.Sagas.SagaMetadata.Create<{details.FullyQualifiedName}, {details.SagaDataFullyQualifiedName}>(associatedMessages, {correlationPropertyAccessor}, propertyAccessors);"); sourceWriter.WriteLine("sagaMetadataCollection.Add(metadata);"); } @@ -131,14 +134,21 @@ static string MessagePropertyAccessorName(PropertyMappingSpec mapping) static void EmitCorrelationPropertyAccessors(SourceWriter sourceWriter, ImmutableEquatableArray sagas) { - var uniqueMappings = new Dictionary<(string PropertyType, string PropertyName), CorrelationPropertyMappingSpec>(); + // Accessors are keyed by the concrete saga-data type plus property identity: two saga-data classes with + // the same correlation property name and type must not share an accessor, because the UnsafeAccessor + // receiver is the concrete saga-data type. + var uniqueMappings = new Dictionary<(string SagaDataType, string PropertyType, string PropertyName), (CorrelationPropertyMappingSpec Mapping, string SagaDataType)>(); foreach (var saga in sagas) { - var mapping = saga.CorrelationPropertyMapping; - var key = (mapping.PropertyType, mapping.PropertyName); + if (saga.CorrelationPropertyMapping is not { } mapping) + { + continue; + } + + var key = (saga.SagaDataFullyQualifiedName, mapping.PropertyType, mapping.PropertyName); if (!uniqueMappings.ContainsKey(key)) { - uniqueMappings.Add(key, mapping); + uniqueMappings.Add(key, (mapping, saga.SagaDataFullyQualifiedName)); } } @@ -147,19 +157,25 @@ static void EmitCorrelationPropertyAccessors(SourceWriter sourceWriter, Immutabl return; } - var allPropertyMappings = new List(uniqueMappings.Values); + var allPropertyMappings = new List<(CorrelationPropertyMappingSpec Mapping, string SagaDataType)>(uniqueMappings.Values); allPropertyMappings.Sort(static (a, b) => { - var typeComparison = string.CompareOrdinal(a.PropertyType, b.PropertyType); - return typeComparison != 0 ? typeComparison : string.CompareOrdinal(a.PropertyName, b.PropertyName); + var sagaTypeComparison = string.CompareOrdinal(a.SagaDataType, b.SagaDataType); + if (sagaTypeComparison != 0) + { + return sagaTypeComparison; + } + + var typeComparison = string.CompareOrdinal(a.Mapping.PropertyType, b.Mapping.PropertyType); + return typeComparison != 0 ? typeComparison : string.CompareOrdinal(a.Mapping.PropertyName, b.Mapping.PropertyName); }); sourceWriter.WriteLine(); for (var index = 0; index < allPropertyMappings.Count; index++) { - var mapping = allPropertyMappings[index]; - var accessorClassName = CorrelationPropertyAccessorName(mapping); + var (mapping, sagaDataType) = allPropertyMappings[index]; + var accessorClassName = CorrelationPropertyAccessorName(sagaDataType, mapping); _ = sourceWriter.WithCompilerGeneratedAttribute() .WithGeneratedCodeAttribute(); sourceWriter.WriteLine($"file sealed class {accessorClassName} : NServiceBus.Sagas.CorrelationPropertyAccessor"); @@ -169,15 +185,15 @@ static void EmitCorrelationPropertyAccessors(SourceWriter sourceWriter, Immutabl sourceWriter.WriteLine($$"""{{accessorClassName}}() { }"""); sourceWriter.WriteLine(); - sourceWriter.WriteLine("public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData);"); + sourceWriter.WriteLine($"public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(({sagaDataType})sagaData);"); sourceWriter.WriteLine(); sourceWriter.WriteLine($"[global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = \"get_{mapping.PropertyName}\")]"); - sourceWriter.WriteLine($"static extern {mapping.PropertyType} AccessFrom_Property(NServiceBus.IContainSagaData sagaData);"); + sourceWriter.WriteLine($"static extern {mapping.PropertyType} AccessFrom_Property({sagaDataType} sagaData);"); sourceWriter.WriteLine(); - sourceWriter.WriteLine($"public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, (({mapping.PropertyType})value));"); + sourceWriter.WriteLine($"public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(({sagaDataType})sagaData, (({mapping.PropertyType})value));"); sourceWriter.WriteLine(); sourceWriter.WriteLine($"[global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = \"set_{mapping.PropertyName}\")]"); - sourceWriter.WriteLine($"static extern {mapping.PropertyType} WriteTo_Property(NServiceBus.IContainSagaData sagaData, {mapping.PropertyType} value);"); + sourceWriter.WriteLine($"static extern void WriteTo_Property({sagaDataType} sagaData, {mapping.PropertyType} value);"); sourceWriter.WriteLine(); sourceWriter.WriteLine($"public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new {accessorClassName}();"); sourceWriter.Indentation--; @@ -190,9 +206,9 @@ static void EmitCorrelationPropertyAccessors(SourceWriter sourceWriter, Immutabl } } - static string CorrelationPropertyAccessorName(CorrelationPropertyMappingSpec mapping) + static string CorrelationPropertyAccessorName(string sagaDataType, CorrelationPropertyMappingSpec mapping) { - var hash = NonCryptographicHash.GetHash(mapping.PropertyType, "_", mapping.PropertyName); + var hash = NonCryptographicHash.GetHash(sagaDataType, "_", mapping.PropertyType, "_", mapping.PropertyName); return $"{mapping.PropertyName}As{mapping.PropertyTypeMetadataName}Accessor_{hash:x16}"; } } diff --git a/src/NServiceBus.Core.Analyzer/Sagas/Sagas.Parser.cs b/src/NServiceBus.Core.Analyzer/Sagas/Sagas.Parser.cs index 991f26a0172..b959dc3babb 100644 --- a/src/NServiceBus.Core.Analyzer/Sagas/Sagas.Parser.cs +++ b/src/NServiceBus.Core.Analyzer/Sagas/Sagas.Parser.cs @@ -18,7 +18,7 @@ public static partial class Sagas public record SagaSpec : AddHandlerAndSagasRegistrationGenerator.Parser.BaseSpec { - public SagaSpec(HandlerSpec handler, string sagaDataFullyQualifiedName, CorrelationPropertyMappingSpec correlationProperty, ImmutableEquatableArray propertyMappings) + public SagaSpec(HandlerSpec handler, string sagaDataFullyQualifiedName, CorrelationPropertyMappingSpec? correlationProperty, ImmutableEquatableArray propertyMappings) : base(handler) { SagaDataFullyQualifiedName = sagaDataFullyQualifiedName; @@ -29,7 +29,7 @@ public SagaSpec(HandlerSpec handler, string sagaDataFullyQualifiedName, Correlat public string SagaDataFullyQualifiedName { get; } - public CorrelationPropertyMappingSpec CorrelationPropertyMapping { get; } + public CorrelationPropertyMappingSpec? CorrelationPropertyMapping { get; } public ImmutableEquatableArray PropertyMappings { get; } public HandlerSpec Handler { get; } } @@ -60,10 +60,11 @@ public static class Parser var sagaBaseSpec = Handlers.Parser.Parse(sagaType, BaseParser.SpecKind.Saga, knownTypes, cancellationToken); var sagaDataFullyQualifiedName = sagaDataType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - // Analyze ConfigureHowToFindSaga to extract mappings + // Analyze ConfigureHowToFindSaga to extract mappings. Finder-only sagas have no correlation property + // and no property mappings but are still valid sagas that must be registered. var (correlationProperty, propertyMappings) = ExtractPropertyMappings(sagaType, sagaSemanticModel, cancellationToken); - return correlationProperty is null ? null : new SagaSpec(sagaBaseSpec, sagaDataFullyQualifiedName, correlationProperty.Value, propertyMappings); + return new SagaSpec(sagaBaseSpec, sagaDataFullyQualifiedName, correlationProperty, propertyMappings); } static INamedTypeSymbol? GetSagaDataType(INamedTypeSymbol sagaType) diff --git a/src/NServiceBus.Core.Analyzer/SupressionIds.cs b/src/NServiceBus.Core.Analyzer/SupressionIds.cs index e0a22a10a11..eb9fe17499c 100644 --- a/src/NServiceBus.Core.Analyzer/SupressionIds.cs +++ b/src/NServiceBus.Core.Analyzer/SupressionIds.cs @@ -4,4 +4,5 @@ public static class SupressionIds { public const string AddHandlerInterceptorSuppression = "NSBS0001"; public const string AddSagaInterceptorSuppression = "NSBS0002"; + public const string AddMessageTypeInterceptorSuppression = "NSBS0003"; } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer/Utility/MessageHierarchyBuilder.cs b/src/NServiceBus.Core.Analyzer/Utility/MessageHierarchyBuilder.cs new file mode 100644 index 00000000000..d7a69b36b66 --- /dev/null +++ b/src/NServiceBus.Core.Analyzer/Utility/MessageHierarchyBuilder.cs @@ -0,0 +1,58 @@ +#nullable enable + +namespace NServiceBus.Core.Analyzer.Utility; + +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using NServiceBus.Core.Analyzer.Handlers; + +// Shared by handler/saga and AddMessageType generation so the emitted hierarchy has the same ordering regardless of +// which registration path wins. Mirrors the reflection-based runtime inference: interfaces rank by how many interfaces +// they inherit, classes by base-type chain depth, ordered descending with a stable sort. +static class MessageHierarchyBuilder +{ + public static IEnumerable GetTypeHierarchy(INamedTypeSymbol type, MarkerTypes markers) => + GetParentTypes(type) + .Where(t => !markers.IsMarkerInterface(t)) + .Select(t => new { Type = t, Rank = PlaceInMessageHierarchy(t) }) + .OrderByDescending(item => item.Rank) + .Select(item => item.Type); + + static IEnumerable GetParentTypes(INamedTypeSymbol type) + { + foreach (var iface in type.AllInterfaces) + { + yield return iface; + } + + var currentBase = type.BaseType; + while (currentBase is { SpecialType: not SpecialType.System_Object }) + { + if (currentBase is { } named) + { + yield return named; + } + + currentBase = currentBase.BaseType; + } + } + + static int PlaceInMessageHierarchy(INamedTypeSymbol type) + { + if (type.TypeKind == TypeKind.Interface) + { + return type.AllInterfaces.Length; + } + + var result = 0; + var current = type.BaseType; + while (current is not null) + { + result++; + current = current.BaseType; + } + + return result; + } +} diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index 2cefcd367ce..b292515cc56 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -829,6 +829,11 @@ namespace NServiceBus public static System.Threading.Tasks.Task Unsubscribe(this NServiceBus.IMessageSession session, System.Type messageType, System.Threading.CancellationToken cancellationToken = default) { } public static System.Threading.Tasks.Task Unsubscribe(this NServiceBus.IMessageSession session, System.Threading.CancellationToken cancellationToken = default) { } } + public static class MessageTypeRegistrationExtensions + { + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode(@"AddMessageType relies on an NServiceBus source-generated interceptor to register the statically known message hierarchy without reflection when trimming is enabled. If this warning is reported, the interceptor was not used for this call; enable or restore the NServiceBus analyzer/source-generator tooling and use a statically known message type.")] + public static void AddMessageType<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TMessage>(this NServiceBus.EndpointConfiguration config) { } + } public class MoveToError : NServiceBus.RecoverabilityAction { protected MoveToError(string errorQueue) { } @@ -2158,6 +2163,7 @@ namespace NServiceBus.Pipeline { public LogicalMessageFactory(NServiceBus.Unicast.Messages.MessageMetadataRegistry messageMetadataRegistry, NServiceBus.MessageInterfaces.IMessageMapper messageMapper) { } public NServiceBus.Pipeline.LogicalMessage Create(object message) { } + public NServiceBus.Pipeline.LogicalMessage Create(NServiceBus.Unicast.Messages.MessageMetadata metadata, object message) { } public NServiceBus.Pipeline.LogicalMessage Create(System.Type messageType, object message) { } } public class MessageHandler @@ -2179,28 +2185,28 @@ namespace NServiceBus.Pipeline public class PipelineSettings : NServiceBus.Configuration.AdvancedExtensibility.ExposeSettings { public void Register(NServiceBus.Pipeline.RegisterStep registration) { } - public void Register(System.Type behavior, string description) { } - public void Register(string stepId, System.Type behavior, string description) { } + public void Register([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type behavior, string description) { } + public void Register(string stepId, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type behavior, string description) { } public void Register() where TRegisterStep : NServiceBus.Pipeline.RegisterStep, new () { } - public void Register(string description) { } - public void Register(System.Func factoryMethod, string description) + public void Register<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TBehavior>(string description) { } + public void Register<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(System.Func factoryMethod, string description) where T : NServiceBus.Pipeline.IBehavior { } - public void Register(T behavior, string description) + public void Register<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T behavior, string description) where T : NServiceBus.Pipeline.IBehavior { } - public void Register(string stepId, System.Func factoryMethod, string description) + public void Register<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, System.Func factoryMethod, string description) where T : NServiceBus.Pipeline.IBehavior { } - public void Register(string stepId, T behavior, string description) + public void Register<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, T behavior, string description) where T : NServiceBus.Pipeline.IBehavior { } - public void RegisterOrReplace(string stepId, System.Type behavior, string? description = null) { } - public void RegisterOrReplace(string stepId, System.Func factoryMethod, string? description = null) + public void RegisterOrReplace(string stepId, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type behavior, string? description = null) { } + public void RegisterOrReplace<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, System.Func factoryMethod, string? description = null) where T : NServiceBus.Pipeline.IBehavior { } - public void RegisterOrReplace(string stepId, T behavior, string? description = null) + public void RegisterOrReplace<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, T behavior, string? description = null) where T : NServiceBus.Pipeline.IBehavior { } - public void Replace(string stepId, System.Type newBehavior, string? description = null) { } - public void Replace(string stepId, System.Func factoryMethod, string? description = null) + public void Replace(string stepId, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type newBehavior, string? description = null) { } + public void Replace<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, System.Func factoryMethod, string? description = null) where T : NServiceBus.Pipeline.IBehavior { } - public void Replace(string stepId, T newBehavior, string? description = null) + public void Replace<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, T newBehavior, string? description = null) where T : NServiceBus.Pipeline.IBehavior { } } public abstract class PipelineTerminator : NServiceBus.Pipeline.StageConnector.ITerminatingContext> @@ -2214,7 +2220,8 @@ namespace NServiceBus.Pipeline [System.Diagnostics.DebuggerDisplay("{StepId}({BehaviorType.FullName}) - {Description}")] public abstract class RegisterStep { - protected RegisterStep(string stepId, System.Type behavior, string? description, System.Func? factoryMethod = null) { } + protected RegisterStep(string stepId, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type behavior, string? description, System.Func? factoryMethod = null) { } + [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] public System.Type BehaviorType { get; } public string Description { get; } public string StepId { get; } diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt index 7be47134f9a..4f8f1f1378e 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt @@ -5,13 +5,6 @@ Changes that make this list longer should not be approved. src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs IL2026: Using member 'NServiceBus.Pipeline.IOutgoingLogicalMessageContext.UpdateMessage(Object)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. When trimming is enabled, routing a message using its runtime type cannot be statically analyzed by the trimmer. Use the generic overload or, when the message type is not known at compile time, the overload accepting an explicit Type. -src/NServiceBus.Core/Pipeline/RegisterStep.cs - IL2072: 'instanceType' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicConstructors' in call to 'Microsoft.Extensions.DependencyInjection.ActivatorUtilities.CreateInstance(IServiceProvider, Type, params Object[])'. The return value of method 'NServiceBus.Pipeline.RegisterStep.BehaviorType.get' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. - -src/NServiceBus.Core/Pipeline/RegisterStepExtensions.cs - IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 'behaviorType' of method 'NServiceBus.RegisterStepExtensions.GetBehaviorInterface(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. - IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.Interfaces' in call to 'System.Type.GetInterfaces()'. The parameter 'behaviorType' of method 'NServiceBus.RegisterStepExtensions.IsBehavior(Type)' does not have matching annotations. The source value must declare at least the same requirements as those declared on the target location it is assigned to. - 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. diff --git a/src/NServiceBus.Core.Tests/AssemblyScanner/AssemblyScanningComponentTests.cs b/src/NServiceBus.Core.Tests/AssemblyScanner/AssemblyScanningComponentTests.cs index 56ea6f238ab..6c408de4871 100644 --- a/src/NServiceBus.Core.Tests/AssemblyScanner/AssemblyScanningComponentTests.cs +++ b/src/NServiceBus.Core.Tests/AssemblyScanner/AssemblyScanningComponentTests.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Core.Tests.AssemblyScanner; +#nullable enable + +namespace NServiceBus.Core.Tests.AssemblyScanner; using System; using System.IO; @@ -57,6 +59,88 @@ public void Should_allow_assembly_scanning_to_be_disabled() Assert.That(component.AvailableTypes, Is.Empty); } + [Test] + public void Should_enable_strict_registered_only_mode_when_scanning_disabled_and_dynamic_code_not_supported() + { + var settingsHolder = new SettingsHolder(); + settingsHolder.Set(new HostingComponent.Settings(settingsHolder)); + + var configuration = new AssemblyScanningComponent.Configuration(settingsHolder) + { + DynamicCodeSupported = false, + AssemblyScannerConfiguration = { Disable = true } + }; + + var component = AssemblyScanningComponent.Initialize(configuration, settingsHolder); + + Assert.That(component.IsStrictRegisteredOnlyMode, Is.True); + } + + [Test] + public void Should_enable_strict_registered_only_mode_when_scanning_disabled_and_feature_switch_enabled() + { + var settingsHolder = new SettingsHolder(); + settingsHolder.Set(new HostingComponent.Settings(settingsHolder)); + + var configuration = new AssemblyScanningComponent.Configuration(settingsHolder) + { + StrictRegisteredOnlyMessageMetadataEnabled = true, + AssemblyScannerConfiguration = { Disable = true } + }; + + var component = AssemblyScanningComponent.Initialize(configuration, settingsHolder); + + Assert.That(component.IsStrictRegisteredOnlyMode, Is.True); + } + + [Test] + public void Should_enable_strict_registered_only_mode_when_scanning_disabled_and_runtime_switch_enabled() + { + var settingsHolder = new SettingsHolder(); + settingsHolder.Set(new HostingComponent.Settings(settingsHolder)); + + using (AppContextSwitchHelper.Enable(AppContextSwitches.StrictRegisteredOnlyMessageMetadataSwitchName)) + { + var configuration = new AssemblyScanningComponent.Configuration(settingsHolder) { AssemblyScannerConfiguration = { Disable = true } }; + + var component = AssemblyScanningComponent.Initialize(configuration, settingsHolder); + + Assert.That(component.IsStrictRegisteredOnlyMode, Is.True); + } + } + + [Test] + public void Should_not_enable_strict_registered_only_mode_when_scanning_disabled_in_normal_jit() + { + var settingsHolder = new SettingsHolder(); + settingsHolder.Set(new HostingComponent.Settings(settingsHolder)); + + var configuration = new AssemblyScanningComponent.Configuration(settingsHolder) { AssemblyScannerConfiguration = { Disable = true } }; + + var component = AssemblyScanningComponent.Initialize(configuration, settingsHolder); + + Assert.That(component.IsStrictRegisteredOnlyMode, Is.False); + } + + [Test] + public void Should_not_enable_strict_registered_only_mode_when_scanning_enabled_and_feature_switch_enabled() + { + var settingsHolder = new SettingsHolder(); + settingsHolder.Set(new HostingComponent.Settings(settingsHolder)); + + var configuration = new AssemblyScanningComponent.Configuration(settingsHolder) + { + StrictRegisteredOnlyMessageMetadataEnabled = true, + DynamicCodeSupported = true + }; + + var component = AssemblyScanningComponent.Initialize(configuration, settingsHolder); + + // Strict mode is only ever enabled when assembly scanning is disabled. With scanning enabled the + // component must not enable it even when the feature switch is enabled. + Assert.That(component.IsStrictRegisteredOnlyMode, Is.False); + } + [Test] public void Should_throw_enabled_and_dynamic_code_not_supported() { @@ -69,4 +153,24 @@ public void Should_throw_enabled_and_dynamic_code_not_supported() Assert.That(exception?.Message, Does.Contain("Assembly scanning requires to access unreferenced and dynamic code")); } + + sealed class AppContextSwitchHelper : IDisposable + { + readonly string switchName; + + public static AppContextSwitchHelper Enable(string switchName) => new(switchName, true); + + AppContextSwitchHelper(string switchName, bool value) + { + this.switchName = switchName; + AppContext.SetSwitch(switchName, value); + AppContextSwitches.ResetStrictRegisteredOnlyMessageMetadata(); + } + + public void Dispose() + { + AppContext.SetSwitch(switchName, false); + AppContextSwitches.ResetStrictRegisteredOnlyMessageMetadata(); + } + } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/Hosting/AppContextSwitchesTests.cs b/src/NServiceBus.Core.Tests/Hosting/AppContextSwitchesTests.cs new file mode 100644 index 00000000000..7aeabfdf592 --- /dev/null +++ b/src/NServiceBus.Core.Tests/Hosting/AppContextSwitchesTests.cs @@ -0,0 +1,56 @@ +namespace NServiceBus.Core.Tests.Host; + +using System; +using NUnit.Framework; + +[TestFixture] +public class AppContextSwitchesTests +{ + [Test] + public void Strict_registered_only_message_metadata_defaults_to_false_when_switch_unset() + { + AppContextSwitches.ResetStrictRegisteredOnlyMessageMetadata(); + + Assert.That(AppContextSwitches.IsStrictRegisteredOnlyMessageMetadataEnabled, Is.False); + } + + [Test] + public void Strict_registered_only_message_metadata_is_enabled_when_switch_is_set() + { + using (AppContextSwitchHelper.Enable(AppContextSwitches.StrictRegisteredOnlyMessageMetadataSwitchName)) + { + Assert.That(AppContextSwitches.IsStrictRegisteredOnlyMessageMetadataEnabled, Is.True); + } + } + + [Test] + public void Strict_registered_only_message_metadata_is_disabled_when_switch_is_set_to_false() + { + using (AppContextSwitchHelper.Disable(AppContextSwitches.StrictRegisteredOnlyMessageMetadataSwitchName)) + { + Assert.That(AppContextSwitches.IsStrictRegisteredOnlyMessageMetadataEnabled, Is.False); + } + } + + sealed class AppContextSwitchHelper : IDisposable + { + readonly string switchName; + + public static AppContextSwitchHelper Enable(string switchName) => new(switchName, true); + + public static AppContextSwitchHelper Disable(string switchName) => new(switchName, false); + + AppContextSwitchHelper(string switchName, bool value) + { + this.switchName = switchName; + AppContext.SetSwitch(switchName, value); + AppContextSwitches.ResetStrictRegisteredOnlyMessageMetadata(); + } + + public void Dispose() + { + AppContext.SetSwitch(switchName, false); + AppContextSwitches.ResetStrictRegisteredOnlyMessageMetadata(); + } + } +} diff --git a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/When_incoming_mutator_replaces_message_instance.cs b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/When_incoming_mutator_replaces_message_instance.cs new file mode 100644 index 00000000000..c1dc73df095 --- /dev/null +++ b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/When_incoming_mutator_replaces_message_instance.cs @@ -0,0 +1,101 @@ +namespace NServiceBus.Core.Tests.MessageMutators.MutateInstanceMessage; + +using System; +using System.Threading.Tasks; +using MessageMutator; +using Microsoft.Extensions.DependencyInjection; +using NServiceBus.MessageInterfaces; +using NServiceBus.MessageInterfaces.MessageMapper.Reflection; +using NServiceBus.Pipeline; +using NUnit.Framework; +using Testing; +using Unicast.Messages; + +[TestFixture] +public class When_incoming_mutator_replaces_message_instance +{ + [Test] + public async Task Should_recompute_metadata_for_the_replacement_instance_type() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + registry.RegisterMessageTypes([typeof(OriginalMessage), typeof(ReplacementMessage)]); + + var context = CreateContext(registry, new ReplaceWithReplacementMessageMutator()); + + await context.Behavior.Invoke(context.Context, ctx => Task.CompletedTask); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.Context.Message.Instance, Is.TypeOf()); + Assert.That(context.Context.Message.MessageType, Is.EqualTo(typeof(ReplacementMessage))); + Assert.That(context.Context.Message.Metadata.MessageType, Is.EqualTo(typeof(ReplacementMessage))); + } + } + + [Test] + public async Task Should_keep_original_metadata_when_instance_is_not_replaced() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + registry.RegisterMessageTypes([typeof(OriginalMessage), typeof(ReplacementMessage)]); + + var context = CreateContext(registry, new DoNothingMutator()); + + await context.Behavior.Invoke(context.Context, ctx => Task.CompletedTask); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.Context.Message.Instance, Is.TypeOf()); + Assert.That(context.Context.Message.MessageType, Is.EqualTo(typeof(OriginalMessage))); + } + } + + static ContextFixture CreateContext(MessageMetadataRegistry registry, IMutateIncomingMessages mutator) + { + var services = new ServiceCollection(); + services.AddSingleton(registry); + services.AddSingleton(); + services.AddSingleton(new TrimmingSafeMessageMapper()); + IServiceProvider provider = services.BuildServiceProvider(); + + var parentContext = new TestableIncomingPhysicalMessageContext(); + parentContext.Extensions.Set(provider); + + var logicalMessage = new LogicalMessage(registry.GetMessageMetadata(typeof(OriginalMessage)), new OriginalMessage()); + var context = new IncomingLogicalMessageContext(logicalMessage, parentContext); + var behavior = new MutateIncomingMessageBehavior([mutator]); + + return new ContextFixture(context, behavior); + } + + class ContextFixture(IncomingLogicalMessageContext context, MutateIncomingMessageBehavior behavior) + { + public IncomingLogicalMessageContext Context { get; } = context; + public MutateIncomingMessageBehavior Behavior { get; } = behavior; + } + + class ReplaceWithReplacementMessageMutator : IMutateIncomingMessages + { + public Task MutateIncoming(MutateIncomingMessageContext context) + { + context.Message = new ReplacementMessage(); + return Task.CompletedTask; + } + } + + class DoNothingMutator : IMutateIncomingMessages + { + public Task MutateIncoming(MutateIncomingMessageContext context) => Task.CompletedTask; + } + + public class OriginalMessage : IMessage + { + public string SomeProperty { get; set; } + } + + public class ReplacementMessage : IMessage + { + public string SomeProperty { get; set; } + } +} diff --git a/src/NServiceBus.Core.Tests/Pipeline/LogicalMessageFactoryTests.cs b/src/NServiceBus.Core.Tests/Pipeline/LogicalMessageFactoryTests.cs new file mode 100644 index 00000000000..ae2034a7826 --- /dev/null +++ b/src/NServiceBus.Core.Tests/Pipeline/LogicalMessageFactoryTests.cs @@ -0,0 +1,135 @@ +namespace NServiceBus.Core.Tests.Pipeline; + +using System; +using System.Collections.Generic; +using MessageInterfaces; +using MessageInterfaces.MessageMapper.Reflection; +using NServiceBus.Pipeline; +using NUnit.Framework; +using Unicast.Messages; + +[TestFixture] +public class LogicalMessageFactoryTests +{ + MessageMetadataRegistry registry; + MessageMapper mapper; + LogicalMessageFactory factory; + + [SetUp] + public void SetUp() + { + registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + mapper = new MessageMapper(); + factory = new LogicalMessageFactory(registry, mapper); + } + + [Test] + public void Create_with_object_overload_uses_runtime_type_for_metadata() + { + var message = new ConcreteMessage(); + + var logicalMessage = factory.Create(message); + + using (Assert.EnterMultipleScope()) + { + Assert.That(logicalMessage.MessageType, Is.EqualTo(typeof(ConcreteMessage))); + Assert.That(logicalMessage.Instance, Is.SameAs(message)); + } + } + + [Test] + public void Create_with_explicit_concrete_type_uses_that_type_for_metadata() + { + var message = new ConcreteMessage(); + + var logicalMessage = factory.Create(typeof(ConcreteMessage), message); + + Assert.That(logicalMessage.MessageType, Is.EqualTo(typeof(ConcreteMessage))); + } + + [Test] + public void Create_normalizes_proxy_instances_to_the_interface_type() + { + mapper.Initialize([typeof(IMyMessage)]); + var proxy = mapper.CreateInstance(); + + var logicalMessage = factory.Create(proxy.GetType(), proxy); + + Assert.That(logicalMessage.MessageType, Is.EqualTo(typeof(IMyMessage))); + } + + [Test] + public void Create_with_interface_type_resolves_metadata_for_the_generated_proxy_type() + { + mapper.Initialize([typeof(IMyMessage)]); + var proxy = mapper.CreateInstance(); + + var logicalMessage = factory.Create(typeof(IMyMessage), proxy); + + Assert.That(logicalMessage.MessageType, Is.EqualTo(proxy.GetType())); + } + + [Test] + public void Create_throws_not_supported_when_mapper_cannot_map_interface_type() + { + var trimmingSafeFactory = new LogicalMessageFactory(registry, new TrimmingSafeMessageMapper()); + + Assert.Throws(() => trimmingSafeFactory.Create(typeof(IMyMessage), new ConcreteMessage())); + } + + [Test] + public void Create_with_metadata_overload_uses_given_metadata_without_invoking_mapper() + { + var throwingMapper = new ThrowingMessageMapper(); + var throwingFactory = new LogicalMessageFactory(registry, throwingMapper); + var metadata = registry.GetMessageMetadata(typeof(ConcreteMessage)); + var message = new ConcreteMessage(); + + var logicalMessage = throwingFactory.Create(metadata, message); + + using (Assert.EnterMultipleScope()) + { + Assert.That(logicalMessage.MessageType, Is.EqualTo(typeof(ConcreteMessage))); + Assert.That(logicalMessage.Metadata, Is.SameAs(metadata)); + Assert.That(logicalMessage.Instance, Is.SameAs(message)); + } + } + + [Test] + public void Create_with_metadata_overload_throws_when_metadata_is_null() + { + Assert.Throws(() => factory.Create((MessageMetadata)null, new ConcreteMessage())); + } + + [Test] + public void Create_throws_when_type_has_no_metadata_and_is_not_a_message_type() + { + Assert.Throws(() => factory.Create(typeof(string), "not a message")); + } + + public interface IMyMessage : IMessage + { + string SomeProperty { get; set; } + } + + public class ConcreteMessage : IMessage + { + public string SomeProperty { get; set; } + } + + class ThrowingMessageMapper : IMessageMapper + { + public void Initialize(IEnumerable types) => throw new InvalidOperationException("Should not be called"); + + public Type GetMappedTypeFor(Type t) => throw new InvalidOperationException("Should not be called"); + + public Type GetMappedTypeFor(string typeName) => throw new InvalidOperationException("Should not be called"); + + public T CreateInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>() => throw new InvalidOperationException("Should not be called"); + + public T CreateInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action action) => throw new InvalidOperationException("Should not be called"); + + public object CreateInstance([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] Type t) => throw new InvalidOperationException("Should not be called"); + } +} diff --git a/src/NServiceBus.Core.Tests/Sagas/SagaMetadataCreationTests.cs b/src/NServiceBus.Core.Tests/Sagas/SagaMetadataCreationTests.cs index 426dd5b6067..9f18b65c3b3 100644 --- a/src/NServiceBus.Core.Tests/Sagas/SagaMetadataCreationTests.cs +++ b/src/NServiceBus.Core.Tests/Sagas/SagaMetadataCreationTests.cs @@ -78,6 +78,52 @@ public void HandleBothUniqueAttributeAndMapping() } } + [Test] + public void When_generated_correlation_accessor_is_supplied_it_is_used() + { + var sagaData = new MySagaWithMappedProperty.SagaData(); + var accessor = new TestCorrelationPropertyAccessor(); + var metadata = SagaMetadata.Create( + [new SagaMessage(typeof(SomeMessage), true, false)], + accessor, + []); + + Assert.That(metadata.TryGetCorrelationProperty(out var correlatedProperty), Is.True); + Assert.That(correlatedProperty.Accessor, Is.SameAs(accessor)); + + // The supplied accessor must actually be used to write and read the correlation value on the saga data. + correlatedProperty.Accessor.WriteTo(sagaData, 42); + Assert.That(sagaData.UniqueProperty, Is.EqualTo(42)); + Assert.That(correlatedProperty.Accessor.AccessFrom(sagaData), Is.EqualTo(42)); + } + + [Test] + public void When_no_correlation_accessor_is_supplied_an_expression_based_accessor_is_created() + { + var sagaData = new MySagaWithMappedProperty.SagaData(); + var metadata = SagaMetadata.Create( + [new SagaMessage(typeof(SomeMessage), true, false)], + null, + []); + + Assert.That(metadata.TryGetCorrelationProperty(out var correlatedProperty), Is.True); + Assert.That(correlatedProperty.Accessor, Is.TypeOf>()); + + // The fallback accessor must round-trip the correlation value on the saga data. + correlatedProperty.Accessor.WriteTo(sagaData, 42); + Assert.That(sagaData.UniqueProperty, Is.EqualTo(42)); + Assert.That(correlatedProperty.Accessor.AccessFrom(sagaData), Is.EqualTo(42)); + } + + class TestCorrelationPropertyAccessor : CorrelationPropertyAccessor + { + public override void WriteTo(IContainSagaData sagaData, object value) => + ((MySagaWithMappedProperty.SagaData)sagaData).UniqueProperty = (int)value; + + public override object AccessFrom(IContainSagaData sagaData) => + ((MySagaWithMappedProperty.SagaData)sagaData).UniqueProperty; + } + [Test] public void AutomaticallyAddUniqueForMappedProperty() { diff --git a/src/NServiceBus.Core.Tests/Serializers/SystemTextJson/JsonMessageSerializerRootTypeTests.cs b/src/NServiceBus.Core.Tests/Serializers/SystemTextJson/JsonMessageSerializerRootTypeTests.cs new file mode 100644 index 00000000000..47247bf486c --- /dev/null +++ b/src/NServiceBus.Core.Tests/Serializers/SystemTextJson/JsonMessageSerializerRootTypeTests.cs @@ -0,0 +1,59 @@ +namespace NServiceBus.Core.Tests.SystemTextJson; + +using System; +using System.IO; +using System.Text.Json; +using NServiceBus.MessageInterfaces.MessageMapper.Reflection; +using NServiceBus.Serializers.SystemJson; +using NUnit.Framework; + +[TestFixture] +public class JsonMessageSerializerRootTypeTests +{ + [Test] + public void Should_deserialize_once_when_a_later_message_type_is_a_base_of_the_current_root() + { + var serializer = CreateSerializer(); + var body = Serialize(serializer, new DerivedMessage { SomeProperty = "value", AdditionalProperty = "additional" }); + + var result = serializer.Deserialize(body, [typeof(DerivedMessage), typeof(BaseMessage)]); + + Assert.That(result, Has.Length.EqualTo(1)); + Assert.That(result[0], Is.TypeOf()); + } + + [Test] + public void Should_deserialize_each_message_type_when_neither_type_is_assignable_from_the_current_root() + { + var serializer = CreateSerializer(); + var body = Serialize(serializer, new DerivedMessage { SomeProperty = "value", AdditionalProperty = "additional" }); + + var result = serializer.Deserialize(body, [typeof(BaseMessage), typeof(DerivedMessage)]); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result, Has.Length.EqualTo(2)); + Assert.That(result[0], Is.TypeOf()); + Assert.That(result[1], Is.TypeOf()); + } + } + + static JsonMessageSerializer CreateSerializer() => new(new JsonSerializerOptions(), ContentTypes.Json, new TrimmingSafeMessageMapper()); + + static byte[] Serialize(JsonMessageSerializer serializer, object message) + { + using var stream = new MemoryStream(); + serializer.Serialize(message, stream); + return stream.ToArray(); + } + + public class BaseMessage : IMessage + { + public string SomeProperty { get; set; } + } + + public class DerivedMessage : BaseMessage + { + public string AdditionalProperty { get; set; } + } +} diff --git a/src/NServiceBus.Core.Tests/Serializers/XML/XmlMessageSerializerCharacterizationTests.cs b/src/NServiceBus.Core.Tests/Serializers/XML/XmlMessageSerializerCharacterizationTests.cs new file mode 100644 index 00000000000..e9e237832f9 --- /dev/null +++ b/src/NServiceBus.Core.Tests/Serializers/XML/XmlMessageSerializerCharacterizationTests.cs @@ -0,0 +1,89 @@ +namespace NServiceBus.Core.Tests.Serializers.XML; + +using System; +using System.IO; +using System.Text; +using NServiceBus.Serializers.XML.Test; +using NServiceBus.Serializers.XML.Test.A; +using NServiceBus.Serializers.XML.Test.B; +using NUnit.Framework; + +[TestFixture] +public class XmlMessageSerializerCharacterizationTests +{ + [Test] + public void Should_deserialize_once_when_a_later_message_type_is_a_base_of_the_current_root() + { + var serializer = SerializerFactory.Create(typeof(DerivedXmlMessage), typeof(BaseXmlMessage)); + var body = Serialize(serializer, new DerivedXmlMessage { SomeProperty = "value", AdditionalProperty = "additional" }); + + var result = serializer.Deserialize(body, [typeof(DerivedXmlMessage), typeof(BaseXmlMessage)]); + + Assert.That(result, Has.Length.EqualTo(1)); + Assert.That(result[0], Is.TypeOf()); + } + + [Test] + public void Should_deserialize_each_message_type_when_neither_type_is_assignable_from_the_current_root() + { + var serializer = SerializerFactory.Create(typeof(BaseXmlMessage), typeof(DerivedXmlMessage)); + var body = Serialize(serializer, new DerivedXmlMessage { SomeProperty = "value", AdditionalProperty = "additional" }); + + var result = serializer.Deserialize(body, [typeof(BaseXmlMessage), typeof(DerivedXmlMessage)]); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result, Has.Length.EqualTo(2)); + Assert.That(result[0], Is.TypeOf()); + Assert.That(result[1], Is.TypeOf()); + } + } + + [Test] + public void Should_deserialize_all_messages_from_a_legacy_multi_message_payload() + { + var serializer = SerializerFactory.Create(typeof(Command1), typeof(Command2)); + var command1Id = Guid.NewGuid(); + var command2Id = Guid.NewGuid(); + + var body = Encoding.UTF8.GetBytes( + "" + + SerializeBody(serializer, new Command1(command1Id)) + + SerializeBody(serializer, new Command2(command2Id)) + + ""); + + var result = serializer.Deserialize(body, [typeof(Command1), typeof(Command2)]); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result, Has.Length.EqualTo(2)); + Assert.That(result[0], Is.TypeOf()); + Assert.That(((Command1)result[0]).Id, Is.EqualTo(command1Id)); + Assert.That(result[1], Is.TypeOf()); + Assert.That(((Command2)result[1]).Id, Is.EqualTo(command2Id)); + } + } + + static byte[] Serialize(XmlMessageSerializer serializer, object message) + { + using var stream = new MemoryStream(); + serializer.Serialize(message, stream); + return stream.ToArray(); + } + + static string SerializeBody(XmlMessageSerializer serializer, object message) + { + var xml = Encoding.UTF8.GetString(Serialize(serializer, message)); + return xml[(xml.IndexOf('>') + 1)..]; + } + + public class BaseXmlMessage : IMessage + { + public string SomeProperty { get; set; } + } + + public class DerivedXmlMessage : BaseXmlMessage + { + public string AdditionalProperty { get; set; } + } +} diff --git a/src/NServiceBus.Core.Tests/TrimmedEndpoint/PackageConsumerTests.cs b/src/NServiceBus.Core.Tests/TrimmedEndpoint/PackageConsumerTests.cs new file mode 100644 index 00000000000..4aa6a8c5db0 --- /dev/null +++ b/src/NServiceBus.Core.Tests/TrimmedEndpoint/PackageConsumerTests.cs @@ -0,0 +1,210 @@ +namespace NServiceBus.Core.Tests.TrimmedEndpoint; + +using System; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; + +[TestFixture] +public class PackageConsumerTests +{ + [Test] + [CancelAfter(1_800_000)] + public async Task Packaged_consumer_gets_interceptors_and_runtime_feature_switch_without_explicit_props(CancellationToken cancellationToken = default) + { + var root = Path.Combine(Path.GetTempPath(), "nservicebus-package-tests", Guid.NewGuid().ToString("N")); + var feed = Path.Combine(root, "feed"); + var consumer = Path.Combine(root, "consumer"); + var publishOutput = Path.Combine(root, "publish"); + // A dedicated packages folder keeps the restore isolated from any stale global-cache extraction of the + // same package version, so the freshly packed NServiceBus.targets is always the one that is imported. + var packagesDir = Path.Combine(root, "packages"); + try + { + // 1. Pack NServiceBus.Core into a local feed. + var coreProject = Path.GetFullPath(Path.Combine( + TestContext.CurrentContext.TestDirectory, + "..", "..", "..", "..", + "NServiceBus.Core", + "NServiceBus.Core.csproj")); + + var packResult = await RunProcess("dotnet", $"pack \"{coreProject}\" -c Release -o \"{feed}\" --nologo", cancellationToken); + Assert.That(packResult.ExitCode, Is.Zero, $"Pack failed:{Environment.NewLine}{packResult.Output}"); + + var nupkg = Directory.GetFiles(feed, "NServiceBus.*.nupkg").SingleOrDefault(); + Assert.That(nupkg, Is.Not.Null, "No NServiceBus package was produced by the pack."); + + using (var archive = ZipFile.OpenRead(nupkg!)) + using (Assert.EnterMultipleScope()) + { + Assert.That(archive.GetEntry("build/net10.0/NServiceBus.targets"), Is.Not.Null); + Assert.That(archive.GetEntry("buildTransitive/net10.0/NServiceBus.targets"), Is.Not.Null); + } + + var packageVersion = Path.GetFileNameWithoutExtension(nupkg)["NServiceBus.".Length..]; + + // 2. Create a consumer that references the PACKAGE (no project reference, no explicit + // CompilerVisibleProperty / InterceptorsNamespaces): everything must flow from the packed + // buildTransitive NServiceBus.props. + Directory.CreateDirectory(consumer); + await File.WriteAllTextAsync(Path.Combine(consumer, "nuget.config"), $$""" + + + + + + + + + """, cancellationToken); + + await File.WriteAllTextAsync(Path.Combine(consumer, "PackageConsumer.csproj"), $$""" + + + Exe + net10.0 + preview + enable + enable + true + + + + + + """, cancellationToken); + + await File.WriteAllTextAsync(Path.Combine(consumer, "Program.cs"), """ + using System.Text.Json; + using System.Text.Json.Serialization; + using NServiceBus; + + var configuration = new EndpointConfiguration("PackageConsumer"); + configuration.AssemblyScanner().Disable = true; + configuration.UseSerialization().Options(new JsonSerializerOptions + { + TypeInfoResolver = ConsumerJsonContext.Default + }); + configuration.UseTransport().StorageDirectory(Path.Combine(Path.GetTempPath(), "nservicebus-package-consumer")); + configuration.AddMessageType(); + configuration.AddHandler(); + + var endpoint = await Endpoint.Start(configuration); + IMessageSession session = endpoint; + await session.SendLocal(new MyCommand { SomeValue = "hello" }); + + for (var i = 0; i < 100 && !MyHandler.Invoked; i++) + { + await Task.Delay(100); + } + + await endpoint.Stop(); + + if (!MyHandler.Invoked) + { + Console.Error.WriteLine("Handler was not invoked."); + return 1; + } + + Console.WriteLine("TRIM-VALIDATION-SUCCESS"); + return 0; + + [Handler] + public class MyHandler : IHandleMessages + { + public static bool Invoked; + + public Task Handle(MyCommand message, IMessageHandlerContext context) + { + Invoked = true; + return Task.CompletedTask; + } + } + + public class MyCommand : ICommand + { + public string SomeValue { get; set; } = string.Empty; + } + + [JsonSerializable(typeof(MyCommand))] + public partial class ConsumerJsonContext : JsonSerializerContext + { + } + """, cancellationToken); + + // 3. Publish the consumer trimmed. Interceptor support and the runtime trim signal must come from the + // packed NServiceBus.props, not from explicit project settings. + var consumerProject = Path.Combine(consumer, "PackageConsumer.csproj"); + var publishResult = await RunProcess("dotnet", + $"publish \"{consumerProject}\" -c Release -p:TreatWarningsAsErrors=false -p:RestorePackagesPath=\"{packagesDir}\" -o \"{publishOutput}\" --nologo", + cancellationToken); + + Assert.That(publishResult.ExitCode, Is.Zero, $"Consumer publish failed:{Environment.NewLine}{publishResult.Output}"); + + // The AddMessageType/AddHandler calls must be intercepted; otherwise the RequiresUnreferencedCode + // fallback surfaces as IL2026 at the consumer's own call sites. + var consumerTrimWarnings = publishResult.Output.Split(Environment.NewLine) + .Where(line => line.Contains("Program.cs") && line.Contains("IL2026")) + .ToArray(); + Assert.That(consumerTrimWarnings, Is.Empty, "Packaged interceptor support did not suppress IL2026 for the consumer source."); + + // The build-transitive NServiceBus.targets must have emitted the strict registered-only message + // metadata switch into the runtime configuration of this trimmed executable. + var runtimeConfigPath = Path.Combine(publishOutput, "PackageConsumer.runtimeconfig.json"); + Assert.That(File.Exists(runtimeConfigPath), Is.True, "Published consumer has no runtimeconfig.json."); + var runtimeConfig = System.Text.Json.JsonDocument.Parse(await File.ReadAllTextAsync(runtimeConfigPath, cancellationToken)); + var configProperties = runtimeConfig.RootElement.GetProperty("runtimeOptions").GetProperty("configProperties"); + Assert.That(configProperties.TryGetProperty(AppContextSwitches.StrictRegisteredOnlyMessageMetadataSwitchName, out var switchValue), Is.True, "Strict registered-only message metadata switch missing from runtimeconfig.json."); + Assert.That(switchValue.GetBoolean(), Is.True, "Strict registered-only message metadata switch must be true for a trimmed executable."); + + // 4. Run the packaged consumer executable. + var executable = Path.Combine(publishOutput, OperatingSystem.IsWindows() ? "PackageConsumer.exe" : "PackageConsumer"); + var runResult = await RunProcess(executable, "", cancellationToken); + + using (Assert.EnterMultipleScope()) + { + Assert.That(runResult.ExitCode, Is.Zero, $"Packaged consumer failed:{Environment.NewLine}{runResult.Output}"); + Assert.That(runResult.Output, Does.Contain("TRIM-VALIDATION-SUCCESS")); + } + } + finally + { + try + { + Directory.Delete(root, recursive: true); + } + catch (Exception ex) + { + TestContext.Progress.WriteLine($"Failed to clean up test directory {root}: {ex.Message}"); + } + } + } + + static async Task RunProcess(string fileName, string arguments, CancellationToken cancellationToken) + { + var startInfo = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }; + + using var process = Process.Start(startInfo)!; + + var outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken); + var errorTask = process.StandardError.ReadToEndAsync(cancellationToken); + + await process.WaitForExitAsync(cancellationToken); + + return new ProcessResult(process.ExitCode, (await outputTask) + Environment.NewLine + (await errorTask)); + } + + sealed record ProcessResult(int ExitCode, string Output); +} diff --git a/src/NServiceBus.Core.Tests/TrimmedEndpointTests.cs b/src/NServiceBus.Core.Tests/TrimmedEndpointTests.cs new file mode 100644 index 00000000000..37f8dcf3566 --- /dev/null +++ b/src/NServiceBus.Core.Tests/TrimmedEndpointTests.cs @@ -0,0 +1,186 @@ +#nullable enable + +namespace NServiceBus.Core.Tests.TrimmedEndpoint; + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; + +[TestFixture] +public class TrimmedEndpointTests +{ + [Test] + [CancelAfter(600_000)] + public async Task Scanner_disabled_endpoint_publishes_trimmed_and_processes_a_message(CancellationToken cancellationToken = default) + { + var sampleProject = Path.GetFullPath(Path.Combine( + TestContext.CurrentContext.TestDirectory, + "..", "..", "..", "..", + "TrimmedEndpoint", + "TrimmedEndpoint.csproj")); + + var publishOutput = Path.Combine(Path.GetTempPath(), "nservicebus-trimmed-tests", Guid.NewGuid().ToString("N")); + try + { + var publishResult = await RunProcess("dotnet", + $"publish \"{sampleProject}\" -c Release -p:TreatWarningsAsErrors=false -o \"{publishOutput}\" --nologo", + cancellationToken); + + Assert.That(publishResult.ExitCode, Is.Zero, $"Publish failed:{Environment.NewLine}{publishResult.Output}"); + + // 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. + var sampleTrimWarnings = publishResult.Output.Split(Environment.NewLine) + .Where(line => line.Contains("Program.cs") && line.Contains("IL2026")) + .ToArray(); + Assert.That(sampleTrimWarnings, Is.Empty, "Interception of AddMessageType/AddHandler failed, IL2026 warnings were emitted for the sample source."); + + var executable = Path.Combine(publishOutput, OperatingSystem.IsWindows() ? "TrimmedEndpoint.exe" : "TrimmedEndpoint"); + var runResult = await RunProcess(executable, "", cancellationToken); + + using (Assert.EnterMultipleScope()) + { + Assert.That(runResult.ExitCode, Is.Zero, $"Trimmed endpoint failed:{Environment.NewLine}{runResult.Output}"); + Assert.That(runResult.Output, Does.Contain("TRIM-VALIDATION-SUCCESS")); + } + } + finally + { + try + { + Directory.Delete(publishOutput, recursive: true); + } + catch + { + // best-effort cleanup + } + } + } + + [Test] + // NativeAOT publish on a cold cache can take substantially longer than a trimmed publish. + [CancelAfter(1_200_000)] + public async Task Scanner_disabled_endpoint_publishes_and_runs_with_native_aot(CancellationToken cancellationToken = default) + { + var runtimeIdentifier = RuntimeIdentifierForCurrentPlatform(); + if (runtimeIdentifier is null) + { + Assert.Ignore("NativeAOT is not supported on this platform/architecture."); + return; + } + + var sampleProject = Path.GetFullPath(Path.Combine( + TestContext.CurrentContext.TestDirectory, + "..", "..", "..", "..", + "TrimmedEndpoint", + "TrimmedEndpoint.csproj")); + + var publishOutput = Path.Combine(Path.GetTempPath(), "nservicebus-aot-tests", Guid.NewGuid().ToString("N")); + try + { + var publishResult = await RunProcess("dotnet", + $"publish \"{sampleProject}\" -c Release -p:RunAotValidation=true -p:TreatWarningsAsErrors=false -r {runtimeIdentifier} -o \"{publishOutput}\" --nologo", + cancellationToken); + + Assert.That(publishResult.ExitCode, Is.Zero, $"NativeAOT publish failed:{Environment.NewLine}{publishResult.Output}"); + + // The sample source must not rely on dynamic code: any IL2026/IL3050 at the sample's own call sites + // would indicate an intercepted registration fell back to reflection. + var sampleAotWarnings = publishResult.Output.Split(Environment.NewLine) + .Where(line => line.Contains("Program.cs") && (line.Contains("IL2026") || line.Contains("IL3050"))) + .ToArray(); + Assert.That(sampleAotWarnings, Is.Empty, "Interception of AddMessageType/AddHandler failed, AOT warnings were emitted for the sample source."); + + var executable = Path.Combine(publishOutput, OperatingSystem.IsWindows() ? "TrimmedEndpoint.exe" : "TrimmedEndpoint"); + var runResult = await RunProcess(executable, "", cancellationToken); + + using (Assert.EnterMultipleScope()) + { + Assert.That(runResult.ExitCode, Is.Zero, $"NativeAOT endpoint failed:{Environment.NewLine}{runResult.Output}"); + Assert.That(runResult.Output, Does.Contain("TRIM-VALIDATION-SUCCESS")); + } + } + finally + { + try + { + Directory.Delete(publishOutput, recursive: true); + } + catch + { + // best-effort cleanup + } + } + } + + static string? RuntimeIdentifierForCurrentPlatform() + { + var architecture = RuntimeInformation.ProcessArchitecture switch + { + Architecture.X64 => "x64", + Architecture.X86 => null, + Architecture.Arm => null, + Architecture.Arm64 => "arm64", + Architecture.Wasm => null, + Architecture.S390x => null, + Architecture.LoongArch64 => null, + Architecture.Armv6 => null, + Architecture.Ppc64le => null, + Architecture.RiscV64 => null, + _ => null + }; + + if (architecture is null) + { + return null; + } + + if (OperatingSystem.IsWindows()) + { + return $"win-{architecture}"; + } + + if (OperatingSystem.IsLinux()) + { + return $"linux-{architecture}"; + } + + if (OperatingSystem.IsMacOS()) + { + return $"osx-{architecture}"; + } + + return null; + } + + static async Task RunProcess(string fileName, string arguments, CancellationToken cancellationToken) + { + var startInfo = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }; + + using var process = Process.Start(startInfo)!; + + var outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken); + var errorTask = process.StandardError.ReadToEndAsync(cancellationToken); + + await process.WaitForExitAsync(cancellationToken); + + return new ProcessResult(process.ExitCode, (await outputTask) + Environment.NewLine + (await errorTask)); + } + + sealed record ProcessResult(int ExitCode, string Output); +} diff --git a/src/NServiceBus.Core.Tests/Unicast/Config/MessageTypeRegistrationExtensionsTests.cs b/src/NServiceBus.Core.Tests/Unicast/Config/MessageTypeRegistrationExtensionsTests.cs new file mode 100644 index 00000000000..afbf0d52ace --- /dev/null +++ b/src/NServiceBus.Core.Tests/Unicast/Config/MessageTypeRegistrationExtensionsTests.cs @@ -0,0 +1,92 @@ +namespace NServiceBus.Unicast.Tests; + +using System; +using System.Linq; +using NUnit.Framework; +using Unicast.Messages; + +[TestFixture] +public class MessageTypeRegistrationExtensionsTests +{ + [Test] + public void Should_register_message_type_with_runtime_inferred_hierarchy() + { + var config = new EndpointConfiguration("test"); + config.AddMessageType(); + + var registry = config.Settings.GetOrCreate(); + registry.Initialize(new Conventions().IsMessageType, true); + + var metadata = registry.GetMessageMetadata(typeof(MyEvent)); + + using (Assert.EnterMultipleScope()) + { + Assert.That(metadata.MessageType, Is.EqualTo(typeof(MyEvent))); + Assert.That(metadata.MessageHierarchy, Is.EqualTo(new[] { typeof(MyEvent), typeof(IInterfaceParent1), typeof(ConcreteParent1), typeof(IInterfaceParent1Base), typeof(ConcreteParentBase) })); + } + } + + [Test] + public void Should_register_message_type_when_registry_is_already_initialized() + { + var config = new EndpointConfiguration("test"); + var registry = config.Settings.GetOrCreate(); + registry.Initialize(new Conventions().IsMessageType, true); + + config.AddMessageType(); + + Assert.That(registry.GetAllMessages().Select(m => m.MessageType), Does.Contain(typeof(MyEvent))); + } + + [Test] + public void Should_not_register_type_rejected_by_conventions() + { + var config = new EndpointConfiguration("test"); + config.AddMessageType(); + + var registry = config.Settings.GetOrCreate(); + registry.Initialize(new Conventions().IsMessageType, true); + + using (Assert.EnterMultipleScope()) + { + Assert.That(registry.GetAllMessages(), Is.Empty); + var exception = Assert.Throws(() => registry.GetMessageMetadata(typeof(NotAMessage))); + Assert.That(exception?.Message, Does.Contain("Could not find metadata")); + } + } + + [Test] + public void Should_register_message_type_when_conventions_are_configured_after_registration() + { + var config = new EndpointConfiguration("test"); + config.AddMessageType(); + + config.Conventions().DefiningMessagesAs(type => type == typeof(UnobtrusiveMessage)); + + // Mirrors EndpointCreator.ConfigureMessageTypes: the deferred registration is evaluated against the finalized conventions at initialization time. + var registry = config.Settings.GetOrCreate(); + registry.Initialize(config.Conventions().Conventions.IsMessageType, true); + + var metadata = registry.GetAllMessages().Single(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(metadata.MessageType, Is.EqualTo(typeof(UnobtrusiveMessage))); + Assert.That(metadata.MessageHierarchy, Is.EqualTo(new[] { typeof(UnobtrusiveMessage) })); + } + } + + [Test] + public void Should_throw_when_configuration_is_null() + { + Assert.Throws(() => MessageTypeRegistrationExtensions.AddMessageType(null)); + } + + class MyEvent : ConcreteParent1, IInterfaceParent1; + class NotAMessage; + class UnobtrusiveMessage; + class ConcreteParent1 : ConcreteParentBase; + class ConcreteParentBase : IMessage; + interface IInterfaceParent1 : IInterfaceParent1Base; + interface IInterfaceParent1Base : IMessage; +} diff --git a/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryCacheOnlyLookupTests.cs b/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryCacheOnlyLookupTests.cs new file mode 100644 index 00000000000..bafe42fd7e1 --- /dev/null +++ b/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryCacheOnlyLookupTests.cs @@ -0,0 +1,112 @@ +namespace NServiceBus.Unicast.Tests; + +using System; +using System.Linq; +using NUnit.Framework; +using Unicast.Messages; + +[TestFixture] +public class MessageMetadataRegistryCacheOnlyLookupTests +{ + [Test] + public void Should_return_metadata_for_registered_type() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + registry.RegisterMessageTypes([typeof(MyMessage)]); + + var found = registry.TryGetMessageMetadata(typeof(MyMessage), out var metadata); + + using (Assert.EnterMultipleScope()) + { + Assert.That(found, Is.True); + Assert.That(metadata.MessageType, Is.EqualTo(typeof(MyMessage))); + } + } + + [Test] + public void Should_not_register_unregistered_type_when_lookup_misses() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + + var found = registry.TryGetMessageMetadata(typeof(MyMessage), out _); + + using (Assert.EnterMultipleScope()) + { + Assert.That(found, Is.False); + Assert.That(registry.GetAllMessages().Select(m => m.MessageType), Does.Not.Contain(typeof(MyMessage))); + } + } + + [Test] + public void Should_return_metadata_for_registered_type_identifier() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + registry.RegisterMessageTypes([typeof(MyMessage)]); + + var found = registry.TryGetMessageMetadata(typeof(MyMessage).AssemblyQualifiedName, out var metadata); + + using (Assert.EnterMultipleScope()) + { + Assert.That(found, Is.True); + Assert.That(metadata.MessageType, Is.EqualTo(typeof(MyMessage))); + } + } + + [Test] + public void Should_return_false_for_unknown_identifier_without_dynamic_type_loading() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + + var found = registry.TryGetMessageMetadata("Some.Namespace.SomeType, SomeAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", out _); + + Assert.That(found, Is.False); + } + + [Test] + public void Should_return_false_for_loadable_type_that_is_not_registered() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, false); + registry.RegisterMessageTypes([typeof(MyMessage)]); + + var found = registry.TryGetMessageMetadata(typeof(EndpointConfiguration).AssemblyQualifiedName, out _); + + Assert.That(found, Is.False); + } + + [Test] + public void Should_return_false_for_unregistered_type_that_matches_convention() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, false); + registry.RegisterMessageTypes([typeof(MyMessage)]); + + var found = registry.TryGetMessageMetadata(typeof(OtherMessage), out _); + + Assert.That(found, Is.False); + } + + [Test] + public void Should_return_false_without_throwing_after_a_negative_identifier_was_cached_by_legacy_lookup() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, false); + registry.RegisterMessageTypes([typeof(MyMessage)]); + + // The legacy string lookup caches unresolved identifiers as a null entry. + var legacyResult = registry.GetMessageMetadata("Some.Namespace.SomeType, SomeAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"); + Assert.That(legacyResult, Is.Null); + + // The cache-only lookup must treat the negative cache entry as a miss instead of dereferencing it. + var found = registry.TryGetMessageMetadata("Some.Namespace.SomeType, SomeAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", out _); + + Assert.That(found, Is.False); + } + + public class MyMessage : IMessage; + public class OtherMessage : IMessage; +} diff --git a/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryCharacterizationTests.cs b/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryCharacterizationTests.cs new file mode 100644 index 00000000000..f8ddf124a8f --- /dev/null +++ b/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryCharacterizationTests.cs @@ -0,0 +1,103 @@ +namespace NServiceBus.Unicast.Tests; + +using System; +using System.Linq; +using NUnit.Framework; +using Unicast.Messages; + +[TestFixture] +public class MessageMetadataRegistryCharacterizationTests +{ + [Test] + public void Should_return_the_same_cached_metadata_instance_for_a_registered_concrete_type() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + registry.RegisterMessageTypes([typeof(MyEvent)]); + + var first = registry.GetMessageMetadata(typeof(MyEvent)); + var second = registry.GetMessageMetadata(typeof(MyEvent)); + + Assert.That(second, Is.SameAs(first)); + } + + [Test] + public void Should_include_registered_concrete_types_in_get_all_messages() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + registry.RegisterMessageTypes([typeof(MyEvent), typeof(MyOtherEvent)]); + + var allMessageTypes = registry.GetAllMessages().Select(m => m.MessageType).ToList(); + + Assert.That(allMessageTypes, Does.Contain(typeof(MyEvent)).And.Contain(typeof(MyOtherEvent))); + } + + [Test] + public void Should_use_the_explicitly_supplied_hierarchy_when_registered_before_initialization() + { + var registry = new MessageMetadataRegistry(); + registry.RegisterMessageTypeWithHierarchy(typeof(MyEvent), [typeof(ConcreteParent1), typeof(IMessage)]); + registry.Initialize(new Conventions().IsMessageType, true); + + var messageMetadata = registry.GetMessageMetadata(typeof(MyEvent)); + + Assert.That(messageMetadata.MessageHierarchy, Is.EqualTo(new[] { typeof(MyEvent), typeof(ConcreteParent1) })); + } + + [Test] + public void Should_infer_the_hierarchy_at_runtime_when_registered_before_initialization_without_hierarchy() + { + var registry = new MessageMetadataRegistry(); + registry.RegisterMessageTypes([typeof(MyEvent)]); + registry.Initialize(new Conventions().IsMessageType, true); + + var messageMetadata = registry.GetMessageMetadata(typeof(MyEvent)); + + Assert.That(messageMetadata.MessageHierarchy, Is.EqualTo(new[] { typeof(MyEvent), typeof(IInterfaceParent1), typeof(ConcreteParent1), typeof(IInterfaceParent1Base), typeof(ConcreteParentBase) })); + } + + [Test] + public void Should_resolve_and_register_unregistered_concrete_types_on_demand() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, false); + + var messageMetadata = registry.GetMessageMetadata(typeof(MyEvent)); + + using (Assert.EnterMultipleScope()) + { + Assert.That(messageMetadata.MessageType, Is.EqualTo(typeof(MyEvent))); + Assert.That(messageMetadata.MessageHierarchy, Is.EqualTo(new[] { typeof(MyEvent), typeof(IInterfaceParent1), typeof(ConcreteParent1), typeof(IInterfaceParent1Base), typeof(ConcreteParentBase) })); + } + } + + [Test] + public void Should_throw_an_actionable_exception_when_type_is_not_registered_and_not_a_message() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + + var exception = Assert.Throws(() => registry.GetMessageMetadata(typeof(string))); + + Assert.That(exception?.Message, Does.Contain("Could not find metadata for 'System.String'").And.Contain("included in initial scanning").And.Contain("implements either 'IMessage', 'IEvent' or 'ICommand'")); + } + + [Test] + public void Should_return_null_when_type_identifier_loads_a_type_that_is_not_a_message() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + + var messageMetadata = registry.GetMessageMetadata(typeof(EndpointConfiguration).AssemblyQualifiedName); + + Assert.That(messageMetadata, Is.Null); + } + + class MyEvent : ConcreteParent1, IInterfaceParent1; + class MyOtherEvent : IMessage; + class ConcreteParent1 : ConcreteParentBase; + class ConcreteParentBase : IMessage; + interface IInterfaceParent1 : IInterfaceParent1Base; + interface IInterfaceParent1Base : IMessage; +} diff --git a/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryStrictModeInteractionMatrixTests.cs b/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryStrictModeInteractionMatrixTests.cs new file mode 100644 index 00000000000..25e4aff6079 --- /dev/null +++ b/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryStrictModeInteractionMatrixTests.cs @@ -0,0 +1,145 @@ +namespace NServiceBus.Unicast.Tests; + +using System; +using NUnit.Framework; +using Unicast.Messages; + +// Interaction matrix for strict registered-only mode x configured dynamic type loading. +// +// Strict mode is the stronger non-overridable policy: it forbids Type.GetType-based string loading and all runtime +// hierarchy inference/registration on cache misses. With strict off, DynamicTypeLoadingEnabled only controls +// unresolved string/header Type.GetType loading, while Type-based legacy hierarchy inference still works. +// +// | Strict | DynamicLoading | Registered string id | Unregistered string id | Type cache miss | Bare pre-registration | Generated-hierarchy pre-registration | +// |--------|----------------|----------------------|------------------------|-----------------|-----------------------|--------------------------------------| +// | off | off | resolve | null (no Type.GetType) | register (inference) | register at init | register at init | +// | off | on | resolve | Type.GetType + register | register (inference) | register at init | register at init | +// | on | off | resolve | null (no load/register) | throw strict | throw strict at init | register at init | +// | on | on | resolve | null (no load/register) | throw strict | throw strict at init | register at init | +[TestFixture] +public class MessageMetadataRegistryStrictModeInteractionMatrixTests +{ + [TestCase(false, false)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(true, true)] + public void Registered_string_identifier_resolves_in_all_modes(bool strictMode, bool dynamicTypeLoading) + { + var registry = CreateRegistry(strictMode, dynamicTypeLoading); + registry.RegisterMessageTypeWithHierarchy(typeof(MyMessage), [typeof(IMessage)]); + + var metadata = registry.GetMessageMetadata(typeof(MyMessage).AssemblyQualifiedName); + + Assert.That(metadata.MessageType, Is.EqualTo(typeof(MyMessage))); + } + + [TestCase(false, true)] + public void Unregistered_string_identifier_loads_and_registers_when_dynamic_loading_enabled_and_strict_off(bool strictMode, bool dynamicTypeLoading) + { + var registry = CreateRegistry(strictMode, dynamicTypeLoading); + registry.RegisterMessageTypeWithHierarchy(typeof(MyMessage), [typeof(IMessage)]); + + var metadata = registry.GetMessageMetadata(typeof(MyOtherMessage).AssemblyQualifiedName); + + Assert.That(metadata.MessageType, Is.EqualTo(typeof(MyOtherMessage))); + } + + [TestCase(false, false)] + public void Unregistered_string_identifier_returns_null_when_dynamic_loading_disabled_and_strict_off(bool strictMode, bool dynamicTypeLoading) + { + var registry = CreateRegistry(strictMode, dynamicTypeLoading); + registry.RegisterMessageTypeWithHierarchy(typeof(MyMessage), [typeof(IMessage)]); + + var metadata = registry.GetMessageMetadata(typeof(MyOtherMessage).AssemblyQualifiedName); + + Assert.That(metadata, Is.Null); + } + + [TestCase(true, false)] + [TestCase(true, true)] + public void Unregistered_string_identifier_returns_null_in_strict_mode(bool strictMode, bool dynamicTypeLoading) + { + var registry = CreateRegistry(strictMode, dynamicTypeLoading); + registry.RegisterMessageTypeWithHierarchy(typeof(MyMessage), [typeof(IMessage)]); + + var metadata = registry.GetMessageMetadata(typeof(MyOtherMessage).AssemblyQualifiedName); + + Assert.That(metadata, Is.Null); + } + + [TestCase(false, false)] + [TestCase(false, true)] + public void Type_cache_miss_registers_via_runtime_inference_when_strict_off(bool strictMode, bool dynamicTypeLoading) + { + var registry = CreateRegistry(strictMode, dynamicTypeLoading); + registry.RegisterMessageTypeWithHierarchy(typeof(MyMessage), [typeof(IMessage)]); + + var metadata = registry.GetMessageMetadata(typeof(MyOtherMessage)); + + Assert.That(metadata.MessageType, Is.EqualTo(typeof(MyOtherMessage))); + } + + [TestCase(true, false)] + [TestCase(true, true)] + public void Type_cache_miss_throws_in_strict_mode(bool strictMode, bool dynamicTypeLoading) + { + var registry = CreateRegistry(strictMode, dynamicTypeLoading); + registry.RegisterMessageTypeWithHierarchy(typeof(MyMessage), [typeof(IMessage)]); + + var exception = Assert.Throws(() => registry.GetMessageMetadata(typeof(MyOtherMessage))); + + Assert.That(exception?.Message, Does.Contain("strict registered-only message metadata mode") + .And.Contain("AddMessageType()") + .And.Contain("AddHandler()") + .And.Contain("AddSaga()")); + } + + [TestCase(false, false)] + [TestCase(false, true)] + public void Bare_pre_registration_registers_at_initialization_when_strict_off(bool strictMode, bool dynamicTypeLoading) + { + var registry = new MessageMetadataRegistry { StrictRegisteredOnlyMode = strictMode }; + registry.RegisterMessageTypes([typeof(MyMessage)]); + registry.Initialize(new Conventions().IsMessageType, dynamicTypeLoading); + + Assert.That(registry.GetMessageMetadata(typeof(MyMessage)).MessageType, Is.EqualTo(typeof(MyMessage))); + } + + [TestCase(true, false)] + [TestCase(true, true)] + public void Bare_pre_registration_fails_at_initialization_in_strict_mode(bool strictMode, bool dynamicTypeLoading) + { + var registry = new MessageMetadataRegistry { StrictRegisteredOnlyMode = strictMode }; + registry.RegisterMessageTypes([typeof(MyMessage)]); + + var exception = Assert.Throws(() => registry.Initialize(new Conventions().IsMessageType, dynamicTypeLoading)); + + Assert.That(exception?.Message, Does.Contain("strict registered-only message metadata mode") + .And.Contain("AddMessageType()") + .And.Contain("AddHandler()") + .And.Contain("AddSaga()")); + } + + [TestCase(false, false)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(true, true)] + public void Generated_hierarchy_pre_registration_registers_at_initialization_in_all_modes(bool strictMode, bool dynamicTypeLoading) + { + var registry = new MessageMetadataRegistry { StrictRegisteredOnlyMode = strictMode }; + registry.RegisterMessageTypeWithHierarchy(typeof(MyMessage), [typeof(IMessage)]); + registry.Initialize(new Conventions().IsMessageType, dynamicTypeLoading); + + Assert.That(registry.GetMessageMetadata(typeof(MyMessage)).MessageType, Is.EqualTo(typeof(MyMessage))); + } + + static MessageMetadataRegistry CreateRegistry(bool strictMode, bool dynamicTypeLoading) + { + var registry = new MessageMetadataRegistry { StrictRegisteredOnlyMode = strictMode }; + registry.Initialize(new Conventions().IsMessageType, dynamicTypeLoading); + return registry; + } + + public class MyMessage : IMessage; + public class MyOtherMessage : IMessage; +} diff --git a/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryStrictModeTests.cs b/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryStrictModeTests.cs new file mode 100644 index 00000000000..614daee47b5 --- /dev/null +++ b/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryStrictModeTests.cs @@ -0,0 +1,96 @@ +namespace NServiceBus.Unicast.Tests; + +using System; +using NUnit.Framework; +using Unicast.Messages; + +[TestFixture] +public class MessageMetadataRegistryStrictModeTests +{ + [Test] + public void Should_throw_actionable_exception_for_unregistered_message_type() + { + var registry = CreateStrictRegistry(); + + var exception = Assert.Throws(() => registry.GetMessageMetadata(typeof(MyOtherMessage))); + + AssertStrictModeExceptionGuidance(exception); + } + + [Test] + public void Should_resolve_registered_message_type_in_strict_mode() + { + var registry = CreateStrictRegistry(); + + var metadata = registry.GetMessageMetadata(typeof(MyMessage)); + + Assert.That(metadata.MessageType, Is.EqualTo(typeof(MyMessage))); + } + + [Test] + public void Should_register_generated_hierarchy_pre_registration_during_initialization_in_strict_mode() + { + var registry = new MessageMetadataRegistry { StrictRegisteredOnlyMode = true }; + registry.RegisterMessageTypeWithHierarchy(typeof(MyMessage), [typeof(IMessage)]); + registry.Initialize(new Conventions().IsMessageType, true); + + Assert.That(registry.GetMessageMetadata(typeof(MyMessage)).MessageType, Is.EqualTo(typeof(MyMessage))); + } + + [Test] + public void Should_fail_bare_pre_registration_during_initialization_in_strict_mode() + { + var registry = new MessageMetadataRegistry { StrictRegisteredOnlyMode = true }; + registry.RegisterMessageTypes([typeof(MyMessage)]); + + var exception = Assert.Throws(() => registry.Initialize(new Conventions().IsMessageType, true)); + + AssertStrictModeExceptionGuidance(exception); + } + + [Test] + public void Should_return_null_for_unknown_identifier_in_strict_mode_without_dynamic_loading() + { + var registry = CreateStrictRegistry(); + + // The type loads from a real assembly, but strict mode must not resolve or register it. + var metadata = registry.GetMessageMetadata(typeof(EndpointConfiguration).AssemblyQualifiedName); + + Assert.That(metadata, Is.Null); + } + + [Test] + public void Should_return_metadata_for_registered_identifier_in_strict_mode() + { + var registry = CreateStrictRegistry(); + + var metadata = registry.GetMessageMetadata(typeof(MyMessage).AssemblyQualifiedName); + + Assert.That(metadata.MessageType, Is.EqualTo(typeof(MyMessage))); + } + + static void AssertStrictModeExceptionGuidance(Exception exception) + { + var message = exception.Message; + + Assert.That(message, Does.Contain("strict registered-only message metadata mode") + .And.Contain("Ensure one of the following registration paths is used:") + .And.Contain("1. Register the message type before the endpoint starts using 'AddMessageType()' or 'RegisterMessageTypeWithHierarchy'.") + .And.Contain("2. If '") + .And.Contain("register the handler or saga with 'AddHandler()' or 'AddSaga()' and the message type with 'AddMessageType()'.") + .And.Contain("In either case, ensure") + .And.Contain("implements either 'IMessage', 'IEvent' or 'ICommand'") + .And.Contain("Unobtrusive Mode")); + } + + static MessageMetadataRegistry CreateStrictRegistry() + { + var registry = new MessageMetadataRegistry { StrictRegisteredOnlyMode = true }; + registry.RegisterMessageTypeWithHierarchy(typeof(MyMessage), [typeof(IMessage)]); + registry.Initialize(new Conventions().IsMessageType, true); + return registry; + } + + public class MyMessage : IMessage; + public class MyOtherMessage : IMessage; +} \ No newline at end of file diff --git a/src/NServiceBus.Core/AppContextSwitches.cs b/src/NServiceBus.Core/AppContextSwitches.cs index 4f1173bbc70..107af814173 100644 --- a/src/NServiceBus.Core/AppContextSwitches.cs +++ b/src/NServiceBus.Core/AppContextSwitches.cs @@ -3,6 +3,7 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using Particular.Obsoletes; static class AppContextSwitches @@ -15,6 +16,7 @@ enum SwitchState : byte } static SwitchState cachedUseV2DeterministicGuid; + static SwitchState cachedStrictRegisteredOnlyMessageMetadata; [PreObsolete("https://github.com/Particular/NServiceBus/issues/7734", Note = "In v11, DeterministicGuid (XxHash128) becomes the default and this switch will be inverted so that setting it to false opts into the legacy MD5 algorithm. Both the switch and LegacyDeterministicGuid will be removed in v12.", @@ -46,4 +48,31 @@ public static bool UseV2DeterministicGuid } internal static void ResetUseV2DeterministicGuid() => cachedUseV2DeterministicGuid = SwitchState.Unchecked; + + // Emitted by the build-transitive NServiceBus.targets into the runtime configuration of executable applications + // that are published with trimming or AOT enabled. Users can force the value via the + // NServiceBusEnableStrictRegisteredOnlyMessageMetadata MSBuild property or by setting the switch in code. + public const string StrictRegisteredOnlyMessageMetadataSwitchName = "NServiceBus.EnableStrictRegisteredOnlyMessageMetadata"; + + [FeatureSwitchDefinition(StrictRegisteredOnlyMessageMetadataSwitchName)] + public static bool IsStrictRegisteredOnlyMessageMetadataEnabled + { + get + { + var state = cachedStrictRegisteredOnlyMessageMetadata; + if (state != SwitchState.Unchecked) + { + return state == SwitchState.Enabled; + } + + state = AppContext.TryGetSwitch(StrictRegisteredOnlyMessageMetadataSwitchName, out var isEnabled) && isEnabled + ? SwitchState.Enabled + : SwitchState.Disabled; + cachedStrictRegisteredOnlyMessageMetadata = state; + + return state == SwitchState.Enabled; + } + } + + internal static void ResetStrictRegisteredOnlyMessageMetadata() => cachedStrictRegisteredOnlyMessageMetadata = SwitchState.Unchecked; } \ No newline at end of file diff --git a/src/NServiceBus.Core/EndpointCreator.cs b/src/NServiceBus.Core/EndpointCreator.cs index 90726761bc8..bdca7d39f4c 100644 --- a/src/NServiceBus.Core/EndpointCreator.cs +++ b/src/NServiceBus.Core/EndpointCreator.cs @@ -33,6 +33,9 @@ public static EndpointCreator Create(EndpointConfiguration endpointConfiguration var assemblyScanningComponent = AssemblyScanningComponent.Initialize(assemblyScanningConfiguration, settings); assemblyScanningConfiguration.SetDefaultAvailableTypes(assemblyScanningComponent.AvailableTypes); + // The component is the authority on strict registered-only mode: it is only enabled when scanning is disabled + // AND the application is trimmed or dynamic code is unavailable. + assemblyScanningConfiguration.StrictRegisteredOnlyMode = assemblyScanningComponent.IsStrictRegisteredOnlyMode; endpointConfiguration.FinalizeConfiguration(assemblyScanningComponent.AvailableTypes); @@ -191,8 +194,12 @@ void Configure() void ConfigureMessageTypes(IEnumerable messageTypesHandled) { var allowDynamicTypeLoading = settings.IsDynamicTypeLoadingEnabled(); + var strictMode = settings.Get().StrictRegisteredOnlyMode; var messageMetadataRegistry = settings.GetOrCreate(); - messageMetadataRegistry.Initialize(conventions.IsMessageType, allowDynamicTypeLoading); + // 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. + messageMetadataRegistry.StrictRegisteredOnlyMode = strictMode; + messageMetadataRegistry.Initialize(conventions.IsMessageType, allowDynamicTypeLoading && !strictMode); messageMetadataRegistry.RegisterMessageTypes(hostingConfiguration.AvailableTypes); messageMetadataRegistry.RegisterMessageTypesBypassingChecks(messageTypesHandled); diff --git a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningComponent.cs b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningComponent.cs index 4122e6cfe8a..f5887561e9d 100644 --- a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningComponent.cs +++ b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningComponent.cs @@ -15,7 +15,7 @@ public static AssemblyScanningComponent Initialize(Configuration configuration, { if (configuration.AssemblyScannerConfiguration.Disable) { - return new AssemblyScanningComponent([]); + return new AssemblyScanningComponent([], configuration.StrictRegisteredOnlyMode); } if (!configuration.DynamicCodeSupported) @@ -70,15 +70,42 @@ static AssemblyScannerResults ScanAssemblies(AssemblyScannerConfiguration assemb return assemblyScanner.GetScannableAssemblies(); } - AssemblyScanningComponent(IList availableTypes) => AvailableTypes = availableTypes; + AssemblyScanningComponent(IList availableTypes, bool isStrictRegisteredOnlyMode = false) + { + AvailableTypes = availableTypes; + IsStrictRegisteredOnlyMode = isStrictRegisteredOnlyMode; + } public IList AvailableTypes { get; } + /// + /// When assembly scanning is disabled and the application is trimmed or dynamic code is unavailable, the endpoint + /// operates in strict registered-only message metadata mode: metadata is only resolved for types registered up + /// front (e.g. via the source-generated AddMessageType<T> registration) and no runtime registration or + /// dynamic type loading is performed. + /// + public bool IsStrictRegisteredOnlyMode { get; } + public class Configuration(SettingsHolder settings) { - // This is only used for testability until we have a dedicated AOT test project. Not called on the hot path so has no performance implications. + // Testability hook until a dedicated AOT test project exists. public bool DynamicCodeSupported { get; set; } = System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported; + // Mirrors the NServiceBus.EnableStrictRegisteredOnlyMessageMetadata runtime switch emitted by the + // build-transitive NServiceBus.targets for trimmed/AOT executables. Testability only; production code + // never assigns to it. + public bool StrictRegisteredOnlyMessageMetadataEnabled { get; set; } = AppContextSwitches.IsStrictRegisteredOnlyMessageMetadataEnabled; + + // The computed default only matters when scanning is disabled; EndpointCreator overwrites it with the + // component's decision after Initialize has run. + public bool StrictRegisteredOnlyMode + { + get => strictRegisteredOnlyMode ?? (StrictRegisteredOnlyMessageMetadataEnabled || !DynamicCodeSupported); + set => strictRegisteredOnlyMode = value; + } + + bool? strictRegisteredOnlyMode; + public List? UserProvidedTypes { get; set; } public AssemblyScannerConfiguration AssemblyScannerConfiguration => settings.GetOrCreate(); diff --git a/src/NServiceBus.Core/NServiceBus.Core.csproj b/src/NServiceBus.Core/NServiceBus.Core.csproj index bdd99ca6377..d505a9ea8df 100644 --- a/src/NServiceBus.Core/NServiceBus.Core.csproj +++ b/src/NServiceBus.Core/NServiceBus.Core.csproj @@ -32,7 +32,7 @@ NServiceBus Build, version, and monitor better microservices with the most powerful service platform for .NET - $(TargetsForTfmSpecificContentInPackage);AddPropsFileToPackage + $(TargetsForTfmSpecificContentInPackage);AddPropsAndTargetsFilesToPackage @@ -44,9 +44,10 @@ - + + diff --git a/src/NServiceBus.Core/NServiceBus.targets b/src/NServiceBus.Core/NServiceBus.targets new file mode 100644 index 00000000000..969762b0adb --- /dev/null +++ b/src/NServiceBus.Core/NServiceBus.targets @@ -0,0 +1,21 @@ + + + + + true + + + + + + + diff --git a/src/NServiceBus.Core/Pipeline/BehaviorTypeChecker.cs b/src/NServiceBus.Core/Pipeline/BehaviorTypeChecker.cs index c8b54f4c2da..6d52bc95c56 100644 --- a/src/NServiceBus.Core/Pipeline/BehaviorTypeChecker.cs +++ b/src/NServiceBus.Core/Pipeline/BehaviorTypeChecker.cs @@ -3,12 +3,13 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using System.Collections.Generic; using Pipeline; static class BehaviorTypeChecker { - public static void ThrowIfInvalid(Type behavior, string paramName) + public static void ThrowIfInvalid([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type behavior, string paramName) { ArgumentNullException.ThrowIfNull(behavior); if (behavior.IsAbstract) diff --git a/src/NServiceBus.Core/Pipeline/Incoming/LogicalMessageFactory.cs b/src/NServiceBus.Core/Pipeline/Incoming/LogicalMessageFactory.cs index 31edd0e3a06..d1700ac913a 100644 --- a/src/NServiceBus.Core/Pipeline/Incoming/LogicalMessageFactory.cs +++ b/src/NServiceBus.Core/Pipeline/Incoming/LogicalMessageFactory.cs @@ -45,9 +45,25 @@ public LogicalMessage Create(Type messageType, object message) var realMessageType = messageMapper.GetMappedTypeFor(messageType); - return new LogicalMessage(messageMetadataRegistry.GetMessageMetadata(realMessageType), message); + return Create(messageMetadataRegistry.GetMessageMetadata(realMessageType), message); } + /// + /// Creates a new using the specified metadata and message instance without invoking the message mapper. + /// + /// The metadata for the message. + /// The message instance. + /// A new . +#pragma warning disable CA1822 // Mark members as static + public LogicalMessage Create(MessageMetadata metadata, object message) + { + ArgumentNullException.ThrowIfNull(metadata); + ArgumentNullException.ThrowIfNull(message); + + return new LogicalMessage(metadata, message); + } +#pragma warning restore CA1822 // Mark members as static + readonly IMessageMapper messageMapper; readonly MessageMetadataRegistry messageMetadataRegistry; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Pipeline/PipelineSettings.cs b/src/NServiceBus.Core/Pipeline/PipelineSettings.cs index c88e67c8fe0..64bce229784 100644 --- a/src/NServiceBus.Core/Pipeline/PipelineSettings.cs +++ b/src/NServiceBus.Core/Pipeline/PipelineSettings.cs @@ -3,6 +3,7 @@ namespace NServiceBus.Pipeline; using System; +using System.Diagnostics.CodeAnalysis; using Configuration.AdvancedExtensibility; using Settings; @@ -25,7 +26,7 @@ internal PipelineSettings(SettingsHolder settings) : base(settings) /// The new to use. /// The description of the new behavior. /// Throws an exception when the stepId cannot be found in the pipeline. - public void Replace(string stepId, Type newBehavior, string? description = null) + public void Replace(string stepId, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type newBehavior, string? description = null) { BehaviorTypeChecker.ThrowIfInvalid(newBehavior, nameof(newBehavior)); ArgumentException.ThrowIfNullOrWhiteSpace(stepId); @@ -43,7 +44,7 @@ public void Replace(string stepId, Type newBehavior, string? description = null) /// The new to use. /// The description of the new behavior. /// Throws an exception when the stepId cannot be found in the pipeline. - public void Replace(string stepId, T newBehavior, string? description = null) + public void Replace<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, T newBehavior, string? description = null) where T : IBehavior { BehaviorTypeChecker.ThrowIfInvalid(typeof(T), nameof(newBehavior)); @@ -62,7 +63,7 @@ public void Replace(string stepId, T newBehavior, string? description = null) /// The factory method to create new instances of the behavior. /// The description of the new behavior. /// Throws an exception when the stepId cannot be found in the pipeline. - public void Replace(string stepId, Func factoryMethod, string? description = null) + public void Replace<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, Func factoryMethod, string? description = null) where T : IBehavior { BehaviorTypeChecker.ThrowIfInvalid(typeof(T), "newBehavior"); @@ -80,7 +81,7 @@ public void Replace(string stepId, Func factoryMethod, s /// The identifier of the step to replace its implementation. /// The new to use. /// The description of the new behavior. - public void RegisterOrReplace(string stepId, Type behavior, string? description = null) + public void RegisterOrReplace(string stepId, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type behavior, string? description = null) { BehaviorTypeChecker.ThrowIfInvalid(behavior, nameof(behavior)); ArgumentException.ThrowIfNullOrWhiteSpace(stepId); @@ -98,7 +99,7 @@ public void RegisterOrReplace(string stepId, Type behavior, string? description /// The identifier of the step to replace its implementation. /// The new to use. /// The description of the new behavior. - public void RegisterOrReplace(string stepId, T behavior, string? description = null) + public void RegisterOrReplace<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, T behavior, string? description = null) where T : IBehavior { BehaviorTypeChecker.ThrowIfInvalid(typeof(T), nameof(behavior)); @@ -117,7 +118,7 @@ public void RegisterOrReplace(string stepId, T behavior, string? description /// The identifier of the step to replace its implementation. /// The factory method to create new instances of the behavior. /// The description of the new behavior. - public void RegisterOrReplace(string stepId, Func factoryMethod, string? description = null) + public void RegisterOrReplace<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, Func factoryMethod, string? description = null) where T : IBehavior { BehaviorTypeChecker.ThrowIfInvalid(typeof(T), "behavior"); @@ -135,7 +136,7 @@ public void RegisterOrReplace(string stepId, Func factor /// /// The description of the behavior. /// Throws an exception when this behavior is already present in the pipeline. - public void Register(string description) => Register(typeof(TBehavior), description); + public void Register<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] TBehavior>(string description) => Register(typeof(TBehavior), description); /// /// Register a new step into the pipeline. @@ -143,7 +144,7 @@ public void RegisterOrReplace(string stepId, Func factor /// The to execute. /// The description of the behavior. /// Throws an exception when this behavior is already present in the pipeline. - public void Register(Type behavior, string description) + public void Register([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type behavior, string description) { BehaviorTypeChecker.ThrowIfInvalid(behavior, nameof(behavior)); @@ -157,7 +158,7 @@ public void Register(Type behavior, string description) /// The to execute. /// The description of the behavior. /// Throws an exception when this behavior is already present in the pipeline. - public void Register(string stepId, Type behavior, string description) + public void Register(string stepId, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type behavior, string description) { BehaviorTypeChecker.ThrowIfInvalid(behavior, nameof(behavior)); EnsureWriteEnabled(stepId, nameof(Register)); @@ -176,7 +177,7 @@ public void Register(string stepId, Type behavior, string description) /// A callback that creates the behavior instance. /// The description of the behavior. /// Throws an exception when this behavior is already present in the pipeline. - public void Register(Func factoryMethod, string description) + public void Register<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] T>(Func factoryMethod, string description) where T : IBehavior { BehaviorTypeChecker.ThrowIfInvalid(typeof(T), "behavior"); @@ -191,7 +192,7 @@ public void Register(Func factoryMethod, string descript /// A callback that creates the behavior instance. /// The description of the behavior. /// Throws an exception when this behavior is already present in the pipeline. - public void Register(string stepId, Func factoryMethod, string description) + public void Register<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, Func factoryMethod, string description) where T : IBehavior { BehaviorTypeChecker.ThrowIfInvalid(typeof(T), "behavior"); @@ -211,7 +212,7 @@ public void Register(string stepId, Func factoryMethod, /// The behavior instance. /// The description of the behavior. /// Throws an exception when this behavior is already present in the pipeline. - public void Register(T behavior, string description) + public void Register<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] T>(T behavior, string description) where T : IBehavior { BehaviorTypeChecker.ThrowIfInvalid(typeof(T), nameof(behavior)); @@ -226,7 +227,7 @@ public void Register(T behavior, string description) /// The behavior instance. /// The description of the behavior. /// Throws an exception when this behavior is already present in the pipeline. - public void Register(string stepId, T behavior, string description) + public void Register<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, T behavior, string description) where T : IBehavior { BehaviorTypeChecker.ThrowIfInvalid(typeof(T), nameof(behavior)); diff --git a/src/NServiceBus.Core/Pipeline/RegisterOrReplaceStep.cs b/src/NServiceBus.Core/Pipeline/RegisterOrReplaceStep.cs index bc8feb88fde..6dd97be1ca1 100644 --- a/src/NServiceBus.Core/Pipeline/RegisterOrReplaceStep.cs +++ b/src/NServiceBus.Core/Pipeline/RegisterOrReplaceStep.cs @@ -3,6 +3,7 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using Pipeline; sealed class RegisterOrReplaceStep @@ -18,7 +19,7 @@ sealed class RegisterOrReplaceStep public ReplaceStep ReplaceStep { get; } public string StepId { get; } - public static RegisterOrReplaceStep Create(string stepId, Type behaviorType, string? description = null, Func? factoryMethod = null) + public static RegisterOrReplaceStep Create(string stepId, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type behaviorType, string? description = null, Func? factoryMethod = null) { var register = RegisterStep.Create(stepId, behaviorType, description, factoryMethod); var replace = new ReplaceStep(stepId, behaviorType, description, factoryMethod); diff --git a/src/NServiceBus.Core/Pipeline/RegisterStep.cs b/src/NServiceBus.Core/Pipeline/RegisterStep.cs index 2dba3729dfc..6ddd6137d22 100644 --- a/src/NServiceBus.Core/Pipeline/RegisterStep.cs +++ b/src/NServiceBus.Core/Pipeline/RegisterStep.cs @@ -21,7 +21,7 @@ public abstract class RegisterStep /// The type of to register. /// A brief description of what this step does. /// A factory method for creating the behavior. - protected RegisterStep(string stepId, Type behavior, string? description, Func? factoryMethod = null) + protected RegisterStep(string stepId, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type behavior, string? description, Func? factoryMethod = null) { BehaviorTypeChecker.ThrowIfInvalid(behavior, nameof(behavior)); ArgumentException.ThrowIfNullOrWhiteSpace(stepId); @@ -53,6 +53,7 @@ protected RegisterStep(string stepId, Type behavior, string? description, Func /// Gets the type of that is being registered. /// + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] public Type BehaviorType { get; private set; } internal Type BehaviorInterfaceType { get; private set; } @@ -132,11 +133,11 @@ internal void Replace(ReplaceStep replacement) internal IBehavior CreateBehavior(IServiceProvider defaultBuilder) => factoryMethod(defaultBuilder); - internal static RegisterStep Create(string pipelineStep, Type behavior, string? description, Func? factoryMethod = null) + internal static RegisterStep Create(string pipelineStep, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type behavior, string? description, Func? factoryMethod = null) => new DefaultRegisterStep(behavior, pipelineStep, description, factoryMethod); [MemberNotNull(nameof(BehaviorInterfaceType), nameof(InputContextType), nameof(OutputContextType))] - void UpdateBehaviorMetadata(Type behaviorType) + void UpdateBehaviorMetadata([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type behaviorType) { var behaviorInterface = behaviorType.GetBehaviorInterface(); @@ -152,7 +153,7 @@ void UpdateBehaviorMetadata(Type behaviorType) Func DefaultFactoryMethod => provider => (IBehavior)ActivatorUtilities.CreateInstance(provider, BehaviorType); class DefaultRegisterStep( - Type behavior, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type behavior, string stepId, string? description, Func? factoryMethod) diff --git a/src/NServiceBus.Core/Pipeline/RegisterStepExtensions.cs b/src/NServiceBus.Core/Pipeline/RegisterStepExtensions.cs index 4cfa6510978..7555fdffab4 100644 --- a/src/NServiceBus.Core/Pipeline/RegisterStepExtensions.cs +++ b/src/NServiceBus.Core/Pipeline/RegisterStepExtensions.cs @@ -3,26 +3,27 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Pipeline; static class RegisterStepExtensions { - public static bool IsBehavior(this Type behaviorType) => + public static bool IsBehavior([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type behaviorType) => behaviorType.GetInterfaces() .Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == BehaviorInterfaceType); - public static Type GetBehaviorInterface(this Type behaviorType) => + public static Type GetBehaviorInterface([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type behaviorType) => behaviorType.GetInterfaces() .First(x => x.IsGenericType && x.GetGenericTypeDefinition() == BehaviorInterfaceType); - public static Type GetOutputContext(this Type behaviorType) + public static Type GetOutputContext([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type behaviorType) { var behaviorInterface = behaviorType.GetBehaviorInterface(); return behaviorInterface.GetGenericArguments()[1]; } - public static Type GetInputContext(this Type behaviorType) + public static Type GetInputContext([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type behaviorType) { var behaviorInterface = behaviorType.GetBehaviorInterface(); return behaviorInterface.GetGenericArguments()[0]; diff --git a/src/NServiceBus.Core/Pipeline/ReplaceStep.cs b/src/NServiceBus.Core/Pipeline/ReplaceStep.cs index 06b9d613c8c..c8e8b05d910 100644 --- a/src/NServiceBus.Core/Pipeline/ReplaceStep.cs +++ b/src/NServiceBus.Core/Pipeline/ReplaceStep.cs @@ -3,16 +3,18 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using Pipeline; sealed class ReplaceStep( string idToReplace, - Type behavior, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type behavior, string? description = null, Func? factoryMethod = null) { public string ReplaceId { get; } = idToReplace; public string? Description { get; } = description; + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] public Type BehaviorType { get; } = behavior; public Func? FactoryMethod { get; } = factoryMethod; public int RegistrationOrder { get; set; } diff --git a/src/NServiceBus.Core/Sagas/SagaMetadata.cs b/src/NServiceBus.Core/Sagas/SagaMetadata.cs index 2fdacc9db6c..35f32a1bc60 100644 --- a/src/NServiceBus.Core/Sagas/SagaMetadata.cs +++ b/src/NServiceBus.Core/Sagas/SagaMetadata.cs @@ -143,7 +143,7 @@ public static IEnumerable CreateMany(IEnumerable sagaTypes) var saga = (Saga)RuntimeHelpers.GetUninitializedObject(sagaType); - var mapper = new SagaMapper(sagaType, associatedMessages, propertyAccessors ?? []); + var mapper = new SagaMapper(sagaType, associatedMessages, propertyAccessors ?? [], correlationPropertyAccessor); saga.ConfigureHowToFindSaga(mapper); diff --git a/src/NServiceBus.Core/Sagas/SagaPersistenceBehavior.cs b/src/NServiceBus.Core/Sagas/SagaPersistenceBehavior.cs index 577a748e28a..130c1021cb6 100644 --- a/src/NServiceBus.Core/Sagas/SagaPersistenceBehavior.cs +++ b/src/NServiceBus.Core/Sagas/SagaPersistenceBehavior.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; using Logging; @@ -12,6 +13,7 @@ class SagaPersistenceBehavior(ISagaPersister persister, ISagaIdGenerator sagaIdGenerator, SagaMetadataCollection sagaMetadataCollection, IServiceProvider serviceProvider) : IBehavior { + [UnconditionalSuppressMessage("Trimming", "IL2057", Justification = "Best-effort saga type resolution from saga headers; when trimming removes the type, processing falls back to querying the persister for the current saga type.")] public async Task Invoke(IInvokeHandlerContext context, Func next) { var isTimeoutMessage = IsTimeoutMessage(context.Headers); diff --git a/src/NServiceBus.Core/Unicast/Config/MessageTypeRegistrationExtensions.cs b/src/NServiceBus.Core/Unicast/Config/MessageTypeRegistrationExtensions.cs new file mode 100644 index 00000000000..6736ce59e0c --- /dev/null +++ b/src/NServiceBus.Core/Unicast/Config/MessageTypeRegistrationExtensions.cs @@ -0,0 +1,36 @@ +#nullable enable + +namespace NServiceBus; + +using System; +using System.Diagnostics.CodeAnalysis; +using Unicast.Messages; + +/// +/// Provides extensions to manually register message types. +/// +public static class MessageTypeRegistrationExtensions +{ + internal const string TrimmingMessage = "AddMessageType relies on an NServiceBus source-generated interceptor to register the statically known message hierarchy without reflection when trimming is enabled. If this warning is reported, the interceptor was not used for this call; enable or restore the NServiceBus analyzer/source-generator tooling and use a statically known message type."; + + /// + /// Registers the message type including its hierarchy of base types and implemented interfaces. + /// + /// + /// The type is checked against the endpoint's message conventions when the message metadata registry is initialized. + /// This method registers metadata for a type that the conventions already identify as a message; it does not itself + /// classify an arbitrary type as a message. Types that do not implement , , + /// or (unobtrusive mode) need matching conventions defined with + /// . + /// The hierarchy is inferred at runtime using reflection. Under trimming or NativeAOT the call is replaced by a + /// source-generated, reflection-free registration that registers the statically known hierarchy instead. + /// + [RequiresUnreferencedCode(TrimmingMessage)] + public static void AddMessageType<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] TMessage>(this EndpointConfiguration config) + { + ArgumentNullException.ThrowIfNull(config); + + var messageMetadataRegistry = config.Settings.GetOrCreate(); + messageMetadataRegistry.RegisterMessageTypes([typeof(TMessage)]); + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core/Unicast/Messages/MessageMetadataRegistry.Legacy.cs b/src/NServiceBus.Core/Unicast/Messages/MessageMetadataRegistry.Legacy.cs new file mode 100644 index 00000000000..26726d51916 --- /dev/null +++ b/src/NServiceBus.Core/Unicast/Messages/MessageMetadataRegistry.Legacy.cs @@ -0,0 +1,66 @@ +#nullable enable + +namespace NServiceBus.Unicast.Messages; + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +public partial class MessageMetadataRegistry +{ + [UnconditionalSuppressMessage("Trimming", "IL2057", Justification = "Dynamic type loading is best-effort; when trimming removes a type, resolution falls back to known registered message metadata.")] + Type? GetType(string messageTypeIdentifier) + { + if (allowDynamicTypeLoading) + { + try + { + return Type.GetType(messageTypeIdentifier); + } + catch (Exception ex) + { + Logger.Warn($"Message type identifier '{messageTypeIdentifier}' could not be loaded", ex); + } + } + else + { + Logger.Warn($"Unknown message type identifier '{messageTypeIdentifier}'. Dynamic type loading is disabled. Make sure the type is loaded before starting the endpoint or enable dynamic type loading."); + } + + return null; + } + + [UnconditionalSuppressMessage("Trimming", "IL2070", Justification = "Runtime hierarchy inference is used for message types not pre-registered with source-generated hierarchy metadata, including scanned, dynamically loaded, published-only, and legacy message types.")] + Type[] GetRuntimeMessageHierarchy(Type messageType) + { + var parentTypes = new List(messageType.GetInterfaces()); + + var currentBaseType = messageType.BaseType; + var objectType = typeof(object); + while (currentBaseType != null && currentBaseType != objectType) + { + parentTypes.Add(currentBaseType); + currentBaseType = currentBaseType.BaseType; + } + + return [.. parentTypes + .Where(isMessageType) + .OrderByDescending(static type => + { + if (type.IsInterface) + { + return type.GetInterfaces().Length; + } + + var result = 0; + while (type.BaseType != null) + { + result++; + type = type.BaseType; + } + + return result; + })]; + } +} diff --git a/src/NServiceBus.Core/Unicast/Messages/MessageMetadataRegistry.cs b/src/NServiceBus.Core/Unicast/Messages/MessageMetadataRegistry.cs index 4a5999992e8..54db9e87268 100644 --- a/src/NServiceBus.Core/Unicast/Messages/MessageMetadataRegistry.cs +++ b/src/NServiceBus.Core/Unicast/Messages/MessageMetadataRegistry.cs @@ -96,7 +96,10 @@ public MessageMetadata GetMessageMetadata(string messageTypeIdentifier) if (!cacheHit) { - messageType = GetType(messageTypeIdentifier); + if (!StrictRegisteredOnlyMode) + { + messageType = GetType(messageTypeIdentifier); + } if (messageType == null) { @@ -137,6 +140,12 @@ public MessageMetadata GetMessageMetadata(string messageTypeIdentifier) if (isMessageType(messageType)) { + if (StrictRegisteredOnlyMode) + { + Logger.WarnFormat("Message header '{0}' was mapped to type '{1}' but that type was not found in the message registry. Register the message type explicitly using 'AddMessageType()' when running with assembly scanning disabled in a trimmed application. ", messageTypeIdentifier, messageType.FullName); + return null; + } + return RegisterMessageTypeCore(messageType); } @@ -210,26 +219,44 @@ void RegisterMessageType(Type messageType) } } - [UnconditionalSuppressMessage("Trimming", "IL2057", Justification = "Dynamic type loading is best-effort; when trimming removes a type, resolution falls back to known registered message metadata.")] - Type GetType(string messageTypeIdentifier) + /// + /// Attempts to retrieve the for the specified type from the already registered metadata cache without + /// performing any runtime resolution or registration. + /// + /// The message type to retrieve metadata for. + /// The when the type is registered; otherwise null. + /// true when the type is already registered, otherwise false. + internal bool TryGetMessageMetadata(Type messageType, out MessageMetadata metadata) { - if (allowDynamicTypeLoading) - { - try - { - return Type.GetType(messageTypeIdentifier); - } - catch (Exception ex) - { - Logger.Warn($"Message type identifier '{messageTypeIdentifier}' could not be loaded", ex); - } - } - else + ArgumentNullException.ThrowIfNull(messageType); + AssertIsInitialized(); + + return messages.TryGetValue(messageType.TypeHandle, out metadata); + } + + /// + /// Attempts to retrieve the for the message identifier from the already registered metadata cache without + /// performing any runtime type resolution, registration, or logging. + /// + /// The message identifier to retrieve metadata for. + /// The when the identifier is registered; otherwise null. + /// true when the identifier is already registered, otherwise false. + internal bool TryGetMessageMetadata(string messageTypeIdentifier, out MessageMetadata metadata) + { + ArgumentException.ThrowIfNullOrWhiteSpace(messageTypeIdentifier); + AssertIsInitialized(); + + // cachedTypes can hold a null entry for identifiers that failed to resolve (see GetMessageMetadata(string)); + // treat those negative entries as a cache miss instead of dereferencing them. + if (cachedTypes.TryGetValue(messageTypeIdentifier, out var messageType) && + messageType is not null && + messages.TryGetValue(messageType.TypeHandle, out metadata)) { - Logger.Warn($"Unknown message type identifier '{messageTypeIdentifier}'. Dynamic type loading is disabled. Make sure the type is loaded before starting the endpoint or enable dynamic type loading."); + return true; } - return null; + metadata = null!; + return false; } void RegisterMessageTypeWithHierarchyCore(Type messageType, IEnumerable parentMessages) @@ -254,6 +281,13 @@ MessageMetadata RegisterMessageTypeCore(Type messageType) return metadata; } + // Strict mode forbids runtime hierarchy inference and registration on cache misses. The guard lives here so + // pre-initialization bare registrations are also rejected during Initialize. + if (StrictRegisteredOnlyMode) + { + ThrowStrictMissingMetadataException(messageType); + } + LogGenericMessageTypeWarning(messageType); var parentMessages = GetRuntimeMessageHierarchy(messageType); @@ -266,6 +300,10 @@ MessageMetadata RegisterMessageTypeCore(Type messageType) return metadata; } + [DoesNotReturn] + static void ThrowStrictMissingMetadataException(Type messageType) => + throw new Exception($"Could not find metadata for '{messageType.FullName}' because the endpoint runs in strict registered-only message metadata mode.{Environment.NewLine}Ensure one of the following registration paths is used:{Environment.NewLine}1. Register the message type before the endpoint starts using 'AddMessageType()' or 'RegisterMessageTypeWithHierarchy'.{Environment.NewLine}2. If '{messageType.FullName}' is handled by a handler or saga, register the handler or saga with 'AddHandler()' or 'AddSaga()' and the message type with 'AddMessageType()'.{Environment.NewLine}In either case, ensure '{messageType.FullName}' implements either 'IMessage', 'IEvent' or 'ICommand' or alternatively, if you don't want to implement an interface, you can use 'Unobtrusive Mode'."); + static void LogGenericMessageTypeWarning(Type messageType) { if (messageType.IsGenericType) @@ -283,40 +321,15 @@ void AssertIsInitialized() } } - [UnconditionalSuppressMessage("Trimming", "IL2070", Justification = "Runtime hierarchy inference is used for message types not pre-registered with source-generated hierarchy metadata, including scanned, dynamically loaded, published-only, and legacy message types.")] - Type[] GetRuntimeMessageHierarchy(Type messageType) - { - var parentTypes = new List(messageType.GetInterfaces()); - - var currentBaseType = messageType.BaseType; - var objectType = typeof(object); - while (currentBaseType != null && currentBaseType != objectType) - { - parentTypes.Add(currentBaseType); - currentBaseType = currentBaseType.BaseType; - } - - return [.. parentTypes - .Where(isMessageType) - .OrderByDescending(static type => - { - if (type.IsInterface) - { - return type.GetInterfaces().Length; - } - - var result = 0; - while (type.BaseType != null) - { - result++; - type = type.BaseType; - } - - return result; - })]; - } - bool initialized; + + /// + /// When enabled, message metadata is only resolved for types registered up front (e.g. via the source-generated + /// AddMessageType<T> registration) and no runtime registration, hierarchy inference, or dynamic type + /// loading is performed. Must be set before so pre-initialization registrations are + /// enforced against the strict policy. + /// + internal bool StrictRegisteredOnlyMode { get; set; } readonly List<(Type MessageType, IEnumerable Hierarchy)> preRegisteredMessagesWithHierarchy = []; readonly List preRegisteredMessageTypes = []; readonly ConcurrentDictionary messages = new(); diff --git a/src/NServiceBus.slnx b/src/NServiceBus.slnx index 63b3860b326..db4c21f2494 100644 --- a/src/NServiceBus.slnx +++ b/src/NServiceBus.slnx @@ -17,6 +17,7 @@ + diff --git a/src/TrimmedEndpoint/Program.cs b/src/TrimmedEndpoint/Program.cs new file mode 100644 index 00000000000..34f56941799 --- /dev/null +++ b/src/TrimmedEndpoint/Program.cs @@ -0,0 +1,312 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using NServiceBus.Features; +using NServiceBus.Installation; +using NServiceBus.Pipeline; + +#if INCLUDE_SAGA +using NServiceBus.Configuration.AdvancedExtensibility; +#endif + +var configuration = new EndpointConfiguration("TrimmedEndpoint"); +string? startupDiagnostics = null; +configuration.CustomDiagnosticsWriter((diagnostics, _) => +{ + startupDiagnostics = diagnostics; + return Task.CompletedTask; +}); +configuration.AssemblyScanner().Disable = true; +configuration.UseSerialization().Options(new JsonSerializerOptions +{ + TypeInfoResolver = TrimmedEndpointJsonContext.Default +}); +var storageDirectory = Path.Combine(Path.GetTempPath(), "nservicebus-learning-trimmed"); +configuration.UseTransport().StorageDirectory(storageDirectory); +configuration.UsePersistence(); +#if INCLUDE_SAGA +// The learning saga persister serializes saga data with System.Text.Json; provide source-generated metadata for AOT. +configuration.GetSettings().Set("LearningSagaPersistence.SerializerOptions", new JsonSerializerOptions +{ + TypeInfoResolver = SagaDataJsonContext.Default +}); +#endif + +// Duplicate registration must be harmless (first registration wins). +configuration.AddMessageType(); +configuration.AddMessageType(); +configuration.AddMessageType(); +configuration.AddMessageType(); +configuration.Conventions().DefiningCommandsAs(type => + type == typeof(UnobtrusiveCommand) || + (type != typeof(ICommand) && typeof(ICommand).IsAssignableFrom(type))); +configuration.EnableFeature(); +configuration.EnableInstallers(); +configuration.AddHandler(); +#if INCLUDE_SAGA +configuration.AddMessageType(); +configuration.AddMessageType(); +configuration.AddMessageType(); +configuration.AddSaga(); +#endif + +var endpoint = await Endpoint.Start(configuration); +IMessageSession session = endpoint; + +// Generated handler path. +await session.SendLocal(new MyCommand { SomeValue = "hello" }); +await WaitFor(() => MyHandler.Invoked); + +#if INCLUDE_SAGA +// Saga start + timeout path. +await session.SendLocal(new StartOrderCommand { OrderId = "order-1" }); +await WaitFor(() => OrderSaga.Started && OrderSaga.TimedOut); + +// Saga handle path (correlated via OrderId). +await session.SendLocal(new HandleOrderCommand { OrderId = "order-1" }); +await WaitFor(() => OrderSaga.Handled); +#endif + +// Outgoing-only AddMessageType path: routing to a non-existent queue exercises metadata + serialization only. +await session.Send("NonExistentQueue", new OutgoingCommand { SomeValue = "outgoing" }); + +// Unobtrusive message type: AddMessageType was called before the convention was configured, proving that +// generated registration is evaluated against the finalized conventions in trimmed and AOT deployments. +await session.Send("NonExistentQueue", new UnobtrusiveCommand { SomeValue = "unobtrusive" }); + +// The endpoint runs with assembly scanning disabled in a trimmed/AOT application, so strict registered-only +// message metadata mode must be active: sending an unregistered message type has to fail with an actionable +// error instead of being registered on demand. +var strictModeVerified = false; +try +{ + await session.SendLocal(new UnregisteredMessage()); +} +catch (Exception ex) +{ + strictModeVerified = ContainsStrictModeMessage(ex); +} + +await endpoint.Stop(); + +var startupDiagnosticsVerified = false; +if (startupDiagnostics is not null) +{ + using var diagnosticsDocument = JsonDocument.Parse(startupDiagnostics); + var root = diagnosticsDocument.RootElement; + startupDiagnosticsVerified = root.TryGetProperty("Endpoint", out _) && + root.TryGetProperty("Hosting", out _) && + root.TryGetProperty("Messages", out _) && + root.TryGetProperty(TrimmedScenarioFeature.DiagnosticsSectionName, out var featureDiagnostics) && + featureDiagnostics.TryGetProperty("FeatureConfigured", out var featureConfigured) && + featureConfigured.GetBoolean(); +} + +if (!MyHandler.Invoked) +{ + Console.Error.WriteLine("Handler was not invoked."); + return 1; +} + +#if INCLUDE_SAGA +if (!OrderSaga.Started || !OrderSaga.TimedOut || !OrderSaga.Handled) +{ + Console.Error.WriteLine($"SagaStarted={OrderSaga.Started} SagaTimedOut={OrderSaga.TimedOut} SagaHandled={OrderSaga.Handled}"); + return 2; +} +#endif + +if (!strictModeVerified) +{ + Console.Error.WriteLine("Strict registered-only message metadata mode was not active."); + return 3; +} + +if (!startupDiagnosticsVerified) +{ + Console.Error.WriteLine("Startup diagnostics did not contain the expected Core and custom feature sections."); + return 4; +} + +if (!TrimmedScenarioInstaller.Invoked || !TrimmedScenarioBehavior.Invoked) +{ + Console.Error.WriteLine($"InstallerInvoked={TrimmedScenarioInstaller.Invoked} BehaviorInvoked={TrimmedScenarioBehavior.Invoked}"); + return 5; +} + +Console.WriteLine("TRIM-VALIDATION-SUCCESS"); +return 0; + +static async Task WaitFor(Func condition, int iterations = 150) +{ + for (var i = 0; i < iterations && !condition(); i++) + { + await Task.Delay(100); + } +} + +static bool ContainsStrictModeMessage(Exception exception) +{ + for (var current = exception; current is not null; current = current.InnerException) + { + if (current.Message.Contains("strict registered-only message metadata mode")) + { + return true; + } + } + + return false; +} + +[Handler] +public class MyHandler : IHandleMessages +{ + public static bool Invoked; + + public Task Handle(MyCommand message, IMessageHandlerContext context) + { + Invoked = true; + return Task.CompletedTask; + } +} + +#if INCLUDE_SAGA +[Saga] +public class OrderSaga : Saga, + IAmStartedByMessages, + IHandleMessages, + IHandleTimeouts +{ + public static bool Started; + public static bool Handled; + public static bool TimedOut; + + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) + { + mapper.MapSaga(s => s.OrderId) + .ToMessage(m => m.OrderId) + .ToMessage(m => m.OrderId); + } + + public Task Handle(StartOrderCommand message, IMessageHandlerContext context) + { + Started = true; + return RequestTimeout(context, TimeSpan.FromMilliseconds(300)); + } + + public Task Handle(HandleOrderCommand message, IMessageHandlerContext context) + { + Handled = true; + return Task.CompletedTask; + } + + public Task Timeout(OrderTimeout state, IMessageHandlerContext context) + { + TimedOut = true; + return Task.CompletedTask; + } +} + +public class OrderSagaData : ContainSagaData +{ + public string OrderId { get; set; } = string.Empty; +} + +public class StartOrderCommand : ICommand +{ + public string OrderId { get; set; } = string.Empty; +} + +public class HandleOrderCommand : ICommand +{ + public string OrderId { get; set; } = string.Empty; +} + +public class OrderTimeout : IMessage; +#endif + +public class MyCommand : ICommand +{ + public string SomeValue { get; set; } = string.Empty; +} + +public class OutgoingCommand : ICommand +{ + public string SomeValue { get; set; } = string.Empty; +} + +public class UnregisteredMessage : ICommand +{ + public string SomeValue { get; set; } = string.Empty; +} + +public class UnobtrusiveCommand +{ + public string SomeValue { get; set; } = string.Empty; +} + +public sealed class TrimmedScenarioFeature : Feature +{ + public const string DiagnosticsSectionName = "TrimmedScenario"; + + protected override void Setup(FeatureConfigurationContext context) + { + context.AddInstaller(); + context.Pipeline.Register("TrimmedScenarioBehavior", new TrimmedScenarioBehavior(), "Verifies a user-defined behavior under trimming and NativeAOT"); + context.Settings.AddStartupDiagnosticsSection( + DiagnosticsSectionName, + new TrimmedScenarioDiagnostics { FeatureConfigured = true }, + TrimmedScenarioJsonContext.Default.TrimmedScenarioDiagnostics); + } +} + +public sealed class TrimmedScenarioInstaller : INeedToInstallSomething +{ + public static bool Invoked; + + public Task Install(string identity, CancellationToken cancellationToken = default) + { + Invoked = true; + return Task.CompletedTask; + } +} + +public sealed class TrimmedScenarioBehavior : Behavior +{ + public static bool Invoked; + + public override Task Invoke(IOutgoingLogicalMessageContext context, Func next) + { + Invoked = true; + return next(); + } +} + +public sealed class TrimmedScenarioDiagnostics +{ + public bool FeatureConfigured { get; set; } +} + +[JsonSerializable(typeof(MyCommand))] +[JsonSerializable(typeof(OutgoingCommand))] +[JsonSerializable(typeof(UnobtrusiveCommand))] +[JsonSerializable(typeof(UnregisteredMessage))] +#if INCLUDE_SAGA +[JsonSerializable(typeof(StartOrderCommand))] +[JsonSerializable(typeof(HandleOrderCommand))] +[JsonSerializable(typeof(OrderTimeout))] +#endif +public partial class TrimmedEndpointJsonContext : JsonSerializerContext +{ +} + +[JsonSerializable(typeof(TrimmedScenarioDiagnostics))] +public partial class TrimmedScenarioJsonContext : JsonSerializerContext +{ +} + +#if INCLUDE_SAGA +[JsonSerializable(typeof(OrderSagaData))] +public partial class SagaDataJsonContext : JsonSerializerContext +{ +} +#endif \ No newline at end of file diff --git a/src/TrimmedEndpoint/TrimmedEndpoint.csproj b/src/TrimmedEndpoint/TrimmedEndpoint.csproj new file mode 100644 index 00000000000..f6ceef69bf4 --- /dev/null +++ b/src/TrimmedEndpoint/TrimmedEndpoint.csproj @@ -0,0 +1,35 @@ + + + + Exe + net10.0 + enable + enable + $(InterceptorsNamespaces);NServiceBus + + $(NoWarn);CS0618;CA2007;CA1050;CA2211 + + true + + true + + $(DefineConstants);INCLUDE_SAGA + + + + + + + + + + + + + + + + + + From 62b6c62f0293f0cb6f34ec768c6b8b1eb1a8bad1 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 3 Sep 2026 07:37:06 +0200 Subject: [PATCH 59/79] RollingLogger race condition due to file system failures (#7921) * Reproduce rolling logger TOCTU race that already existed before but becomes now apparent with the slot infrastructure * Fix the TOCTU on the rolling file appender * Address review comment * Failing tests for review comment * Fix * Strengthen filename retry regression test Co-authored-by: danielmarbach <174258+danielmarbach@users.noreply.github.com> --------- Co-authored-by: Daniel Marbach Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: danielmarbach <174258+danielmarbach@users.noreply.github.com> --- .../Logging/RollingLoggerTests.cs | 127 ++++++++++++++++++ src/NServiceBus.Core/Logging/RollingLogger.cs | 45 +++++-- 2 files changed, 164 insertions(+), 8 deletions(-) diff --git a/src/NServiceBus.Core.Tests/Logging/RollingLoggerTests.cs b/src/NServiceBus.Core.Tests/Logging/RollingLoggerTests.cs index 7f201b6a152..f121efd64c8 100644 --- a/src/NServiceBus.Core.Tests/Logging/RollingLoggerTests.cs +++ b/src/NServiceBus.Core.Tests/Logging/RollingLoggerTests.cs @@ -96,6 +96,108 @@ protected override void AppendLine(string message) } } + [Test] + public void When_file_is_deleted_underneath_immediately_before_size_lookup() + { + using var tempPath = new TempPath("RollingLoggerTests"); + var logger = new RollingLoggerThatDeletesBeforeSizeLookup(tempPath.TempDirectory, maxFileSize: 2) + { + GetDate = () => new DateTimeOffset(2010, 10, 1, 0, 0, 0, TimeSpan.Zero) + }; + logger.WriteLine("Foo"); + + // Second write exceeds maxFileSize, so the synchronization enumerates today's file; + // the override removes it between enumeration and the metadata read + logger.WriteLine("Bar"); + + // The name proves the sequence number was reused (vanished file counts as empty) + // instead of rolling to a new sequence + var file = tempPath.GetSingle(); + Assert.That(Path.GetFileName(file), Is.EqualTo("nsb_log_2010-10-01_0.txt")); + } + + class RollingLoggerThatDeletesBeforeSizeLookup(string targetDirectory, long maxFileSize) : + RollingLogger(targetDirectory, maxFileSize: maxFileSize) + { + protected override long GetFileSizeOrZero(string path) + { + File.Delete(path); + return base.GetFileSizeOrZero(path); + } + } + + [Test] + public void When_size_lookup_fails_with_io_error_sync_is_aborted_and_retried() + { + using var tempPath = new TempPath("RollingLoggerTests"); + var logger = new RollingLoggerThatFailsSizeLookup(tempPath.TempDirectory, maxFileSize: 10) + { + GetDate = () => new DateTimeOffset(2010, 10, 1, 0, 0, 0, TimeSpan.Zero) + }; + logger.WriteLine("Foo"); + logger.WriteLine("Bar"); + logger.WriteLine("Baz"); + + // The metadata read fails with a transient I/O error, which must abort the synchronization + // instead of treating the file as empty and reusing it + logger.FailSizeLookup = true; + logger.WriteLine("Qux"); + logger.FailSizeLookup = false; + + // The next write retries the synchronization and rolls to a new sequence + logger.WriteLine("Foo2"); + + var files = tempPath.GetFiles(); + Assert.That(files, Has.Count.EqualTo(2)); + Assert.That(Path.GetFileName(files[1]), Is.EqualTo("nsb_log_2010-10-01_1.txt")); + } + + [Test] + public void When_sync_fails_during_filename_calculation_state_is_not_committed_and_next_write_retries() + { + using var tempPath = new TempPath("RollingLoggerTests"); + var logger = new RollingLoggerThatFailsSizeLookup(tempPath.TempDirectory, maxFileSize: 20) + { + GetDate = () => new DateTimeOffset(2010, 10, 1, 0, 0, 0, TimeSpan.Zero) + }; + logger.WriteLine("LongMessage"); + logger.WriteLine("LongMessage"); + + logger.GetDate = () => new DateTimeOffset(2010, 10, 2, 0, 0, 0, TimeSpan.Zero); + logger.WriteLine("Baz"); + + // The clock moves back to a date that already has an oversized file while the stale current file + // remains below the size limit. The metadata read fails, so the synchronization must abort without + // committing the new date; otherwise the next write would stay on the stale file and not retry. + logger.GetDate = () => new DateTimeOffset(2010, 10, 1, 0, 0, 0, TimeSpan.Zero); + logger.FailSizeLookup = true; + logger.WriteLine("Qux"); + logger.FailSizeLookup = false; + + // The next write retries the synchronization and rolls the oversized day one file + logger.WriteLine("Foo2"); + + var files = tempPath.GetFiles(); + Assert.That(files, Has.Count.EqualTo(3)); + Assert.That(Path.GetFileName(files[1]), Is.EqualTo("nsb_log_2010-10-01_1.txt")); + } + + class RollingLoggerThatFailsSizeLookup(string targetDirectory, long maxFileSize) : + RollingLogger(targetDirectory, maxFileSize: maxFileSize) + { + public bool FailSizeLookup { get; set; } + + protected override long GetFileSizeOrZero(string path) + { + if (FailSizeLookup) + { + // An overlong path component makes the metadata read throw a PathTooLongException + return base.GetFileSizeOrZero(Path.Combine(Path.GetDirectoryName(path)!, new string('x', 300))); + } + return base.GetFileSizeOrZero(path); + } + } + [Test] public void When_file_already_exists_and_is_too_large_a_new_sequence_file_is_written() { @@ -396,4 +498,29 @@ public void When_line_is_write_file_has_correct_name() var singleFile = tempPath.GetSingle(); Assert.That(Path.GetFileName(singleFile), Is.EqualTo("nsb_log_2010-10-01_0.txt")); } + + [Test] + public void When_log_directory_is_deleted_underneath_write_does_not_throw_and_recovers() + { + using var tempPath = new TempPath("RollingLoggerTests"); + var logger = new RollingLogger(tempPath.TempDirectory) + { + GetDate = () => new DateTimeOffset(2010, 10, 1, 0, 0, 0, TimeSpan.Zero) + }; + logger.WriteLine("Foo"); + + Directory.Delete(tempPath.TempDirectory, true); + logger.GetDate = () => new DateTimeOffset(2010, 10, 2, 0, 0, 0, TimeSpan.Zero); + + // Simulates e.g. a deployment or slot swap removing the whole log directory: the synchronization + // and the write must degrade to tracing instead of throwing out of WriteLine + Assert.That(() => logger.WriteLine("Bar"), Throws.Nothing); + + Directory.CreateDirectory(tempPath.TempDirectory); + logger.WriteLine("Baz"); + + // GetSingle also verifies the day one file is not resurrected after the directory came back + var singleFile = tempPath.GetSingle(); + Assert.That(Path.GetFileName(singleFile), Is.EqualTo("nsb_log_2010-10-02_0.txt")); + } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Logging/RollingLogger.cs b/src/NServiceBus.Core/Logging/RollingLogger.cs index a1bbcd0467b..f2c6288bb05 100644 --- a/src/NServiceBus.Core/Logging/RollingLogger.cs +++ b/src/NServiceBus.Core/Logging/RollingLogger.cs @@ -44,15 +44,27 @@ protected virtual void AppendLine(string message) void SyncFileSystem() { - if (!HasCurrentDateChanged() && !IsCurrentFileTooLarge()) + try { - return; + if (!HasCurrentDateChanged() && !IsCurrentFileTooLarge()) + { + return; + } + var today = GetDate(); + var nsbLogFiles = GetNsbLogFiles(targetDirectory).ToList(); + CalculateNewFileName(nsbLogFiles, today); + lastWriteDate = today; + PurgeOldFiles(nsbLogFiles); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + // Tolerate environmental file system failures like the log directory or its files being removed + // by overlapping processes, deployments, slot swaps or cleanups. Anything else is a bug and propagates. + // The synchronization state is only committed once the filename calculation succeeded so a failed + // attempt is retried on the next write. + var errorMessage = $"NServiceBus.RollingLogger Could not synchronize log files in directory '{targetDirectory}'. Exception: {exception}"; + Trace.WriteLine(errorMessage); } - var today = GetDate(); - lastWriteDate = today; - var nsbLogFiles = GetNsbLogFiles(targetDirectory).ToList(); - CalculateNewFileName(nsbLogFiles, today); - PurgeOldFiles(nsbLogFiles); } bool HasCurrentDateChanged() => GetDate() != lastWriteDate; @@ -126,6 +138,23 @@ static bool TryDeriveLogInformationFromPath(string file, [NotNullWhen(true)] out static bool TryParseDate(string datePart, out DateTimeOffset dateTime) => DateTimeOffset.TryParseExact(datePart, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out dateTime); + protected virtual long GetFileSizeOrZero(string path) + { + try + { + return new FileInfo(path).Length; + } + catch (Exception exception) when (exception is FileNotFoundException or DirectoryNotFoundException) + { + // The file can vanish between enumerating it and reading its metadata, e.g. removed by + // overlapping processes, deployments or external cleanup. Treat it as empty so the + // sequence number is reused and the file is recreated by the next append. Any other + // failure, like a transient I/O error or an ACL that denies metadata reads, aborts the + // synchronization instead of resetting the tracked size and reusing the oversized file. + return 0; + } + } + void CalculateNewFileName(List logFiles, DateTimeOffset today) { var logFile = GetTodaysNewest(logFiles, today); @@ -137,7 +166,7 @@ void CalculateNewFileName(List logFiles, DateTimeOffset today) } else { - var existingFileSize = new FileInfo(logFile.Path).Length; + var existingFileSize = GetFileSizeOrZero(logFile.Path); if (existingFileSize > maxFileSize) { sequenceNumber = logFile.SequenceNumber + 1; From 433a688fbebc1960aaeb7e49b6700dbfebc070b2 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 12:59:31 +0000 Subject: [PATCH 60/79] Update dependency Particular.AnalyzerTesting to 2.3.0 --- .../NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj index 297aa3abad2..c67a5c7a001 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj @@ -21,7 +21,7 @@ - + From ed6639155fc360d7f3f5e9fcb028fe0bcd9787c1 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 3 Sep 2026 15:15:48 +0200 Subject: [PATCH 61/79] Use the new analyzer functionality --- .../MessagingMigrationAnalyzerTests.cs | 364 +++++------------- 1 file changed, 93 insertions(+), 271 deletions(-) diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationAnalyzerTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationAnalyzerTests.cs index 560cea107f4..ba8fdbeb25d 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationAnalyzerTests.cs +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationAnalyzerTests.cs @@ -4,14 +4,8 @@ namespace NServiceBus.Core.Analyzer.Tests; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Diagnostics; using NUnit.Framework; using Particular.AnalyzerTesting; @@ -37,151 +31,6 @@ static AnalyzerTest MigrationAuditTest(string source) => static AnalyzerTest FakeMigrationTest(string source) => MigrationAuditTest(source).AddReferences(TestingFakesReference); - static async Task AssertEditorConfigSeverity( - string source, - string diagnosticId, - ReportDiagnostic severity, - bool automaticActivation, - params string[] expectedDiagnosticIds) - { - var syntaxTree = CSharpSyntaxTree.ParseText(source, path: "Test.cs"); - var analyzerDiagnostics = await GetEditorConfigDiagnostics( - [syntaxTree], - syntaxTree, - diagnosticId, - severity, - automaticActivation); - - NUnit.Framework.Assert.That(analyzerDiagnostics.Select(diagnostic => diagnostic.Id), Is.EquivalentTo(expectedDiagnosticIds)); - } - - static async Task> GetEditorConfigDiagnostics( - ImmutableArray syntaxTrees, - SyntaxTree configuredTree, - string diagnosticId, - ReportDiagnostic severity, - bool automaticActivation) - { - var compilation = CSharpCompilation.Create( - "AnalyzerConfigOptionsTest", - syntaxTrees, - SetUpFixture.ProjectReferences, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) - .WithSyntaxTreeOptionsProvider(new TestSyntaxTreeOptionsProvider(configuredTree, diagnosticId, severity))); - return await compilation.WithAnalyzers( - [new MessagingMigrationAnalyzer()], - new CompilationWithAnalyzersOptions( - new AnalyzerOptions( - ImmutableArray.Empty, - new TestAnalyzerConfigOptionsProvider(automaticActivation)), - onAnalyzerException: null, - concurrentAnalysis: true, - logAnalyzerExecutionTime: false, - reportSuppressedDiagnostics: true)).GetAnalyzerDiagnosticsAsync(); - } - - static async Task> GetSeverityConfigDiagnostics( - string source, - string diagnosticId, - ReportDiagnostic? severity = null, - ReportDiagnostic? globalSeverity = null, - ImmutableDictionary? treeOptions = null, - bool automaticActivation = false) - { - var syntaxTree = CSharpSyntaxTree.ParseText(source, path: "Test.cs"); - var optionsProvider = severity is null && globalSeverity is null - ? null - : new TestSyntaxTreeOptionsProvider(syntaxTree, diagnosticId, severity, globalSeverity); - - var compilation = CSharpCompilation.Create( - "AnalyzerConfigOptionsTest", - [syntaxTree], - SetUpFixture.ProjectReferences, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) - .WithSyntaxTreeOptionsProvider(optionsProvider)); - return await compilation.WithAnalyzers( - [new MessagingMigrationAnalyzer()], - new CompilationWithAnalyzersOptions( - new AnalyzerOptions( - ImmutableArray.Empty, - new TestAnalyzerConfigOptionsProvider(automaticActivation, treeOptions)), - onAnalyzerException: null, - concurrentAnalysis: true, - logAnalyzerExecutionTime: false, - reportSuppressedDiagnostics: true)).GetAnalyzerDiagnosticsAsync(); - } - - // dotnet_diagnostic severity is a compiler tree option, not an AnalyzerConfigOptions value. - // SyntaxTreeOptionsProvider is the Roslyn API that retains the .editorconfig file scope. - sealed class TestSyntaxTreeOptionsProvider( - SyntaxTree configuredTree, - string configuredDiagnosticId, - ReportDiagnostic? configuredSeverity = null, - ReportDiagnostic? configuredGlobalSeverity = null) : SyntaxTreeOptionsProvider - { - public override GeneratedKind IsGenerated(SyntaxTree tree, CancellationToken cancellationToken = default) => GeneratedKind.NotGenerated; - -#pragma warning disable PS0003 // A parameter of type CancellationToken on a non-private delegate or method should be optional - public override bool TryGetDiagnosticValue( - SyntaxTree tree, - string diagnosticId, - CancellationToken cancellationToken, - out ReportDiagnostic severity) - { - if (tree == configuredTree && - diagnosticId == configuredDiagnosticId && - configuredSeverity is { } configuredTreeSeverity) - { - severity = configuredTreeSeverity; - return true; - } - - severity = ReportDiagnostic.Default; - return false; - } - - public override bool TryGetGlobalDiagnosticValue( - string diagnosticId, - CancellationToken cancellationToken, - out ReportDiagnostic severity) - { - if (configuredGlobalSeverity is { } globalSeverity && - diagnosticId == configuredDiagnosticId) - { - severity = globalSeverity; - return true; - } - - severity = ReportDiagnostic.Default; - return false; - } -#pragma warning restore PS0003 // A parameter of type CancellationToken on a non-private delegate or method should be optional - } - - sealed class TestAnalyzerConfigOptionsProvider(bool automaticActivation, ImmutableDictionary? treeOptions = null) : AnalyzerConfigOptionsProvider - { - static readonly AnalyzerConfigOptions EmptyOptions = new TestAnalyzerConfigOptions(ImmutableDictionary.Empty); - static readonly AnalyzerConfigOptions AutomaticActivationOptions = new TestAnalyzerConfigOptions( - ImmutableDictionary.Empty.Add("build_property.PublishTrimmed", "true")); - - readonly AnalyzerConfigOptions treeConfigOptions = treeOptions is { Count: > 0 } - ? new TestAnalyzerConfigOptions(treeOptions) - : EmptyOptions; - - public override AnalyzerConfigOptions GlobalOptions => automaticActivation ? AutomaticActivationOptions : EmptyOptions; - - public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => treeConfigOptions; - - public override AnalyzerConfigOptions GetOptions(AdditionalText text) => EmptyOptions; - } - - sealed class TestAnalyzerConfigOptions(ImmutableDictionary values) : AnalyzerConfigOptions - { - public override bool TryGetValue(string key, out string value) => values.TryGetValue(key, out value!); - - public override IEnumerable Keys => values.Keys; - } - // ===== NSB0039: Safe object creation ===== [Test] @@ -2325,19 +2174,16 @@ class Foo { async Task Bar(IMessageSession session, int? message) { - await session.Send(new MyMessage()); + await [|session.Send(new MyMessage())|]; await session.Send(message); } } class MyMessage : IMessage { } """; - return AssertEditorConfigSeverity( - source, - DiagnosticIds.UseGenericMessageType, - ReportDiagnostic.Info, - automaticActivation: false, - DiagnosticIds.UseGenericMessageType); + return MigrationTest(source) + .WithDiagnosticSeverity(DiagnosticIds.UseGenericMessageType, ReportDiagnostic.Info) + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); } [Test] @@ -2353,18 +2199,15 @@ class Foo async Task Bar(IMessageSession session, int? message) { await session.Send(new MyMessage()); - await session.Send(message); + await [|session.Send(message)|]; } } class MyMessage : IMessage { } """; - return AssertEditorConfigSeverity( - source, - DiagnosticIds.RuntimeTypeMayDiffer, - ReportDiagnostic.Warn, - automaticActivation: false, - DiagnosticIds.RuntimeTypeMayDiffer); + return MigrationTest(source) + .WithDiagnosticSeverity(DiagnosticIds.RuntimeTypeMayDiffer, ReportDiagnostic.Warn) + .AssertDiagnostics(DiagnosticIds.RuntimeTypeMayDiffer); } [Test] @@ -2380,18 +2223,16 @@ class Foo async Task Bar(IMessageSession session, int? message) { await session.Send(new MyMessage()); - await session.Send(message); + await [|session.Send(message)|]; } } class MyMessage : IMessage { } """; - return AssertEditorConfigSeverity( - source, - DiagnosticIds.UseGenericMessageType, - ReportDiagnostic.Suppress, - automaticActivation: true, - DiagnosticIds.RuntimeTypeMayDiffer); + return MigrationTest(source) + .WithProperty("build_property.PublishTrimmed", "true") + .WithDiagnosticSeverity(DiagnosticIds.UseGenericMessageType, ReportDiagnostic.Suppress) + .AssertDiagnostics(DiagnosticIds.RuntimeTypeMayDiffer); } [Test] @@ -2406,51 +2247,67 @@ class Foo { async Task Bar(IMessageSession session, int? message) { - await session.Send(new MyMessage()); + await [|session.Send(new MyMessage())|]; await session.Send(message); } } class MyMessage : IMessage { } """; - return AssertEditorConfigSeverity( - source, - DiagnosticIds.RuntimeTypeMayDiffer, - ReportDiagnostic.Suppress, - automaticActivation: true, - DiagnosticIds.UseGenericMessageType); + return MigrationTest(source) + .WithProperty("build_property.PublishTrimmed", "true") + .WithDiagnosticSeverity(DiagnosticIds.RuntimeTypeMayDiffer, ReportDiagnostic.Suppress) + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); } [Test] public Task MigrationDiagnostics_AutomaticActivation_DefaultSeverityFallsBackToAutomatic() { - var source = + const string source = """ using NServiceBus; using System.Threading.Tasks; class Foo { - async Task Bar(IMessageSession session, int? message) + async Task Bar(IMessageSession session) { - await session.Send(new MyMessage()); - await session.Send(message); + await [|session.Send(new MyMessage())|]; } } class MyMessage : IMessage { } """; - return AssertEditorConfigSeverity( - source, - DiagnosticIds.UseGenericMessageType, - ReportDiagnostic.Default, - automaticActivation: true, - DiagnosticIds.UseGenericMessageType, - DiagnosticIds.RuntimeTypeMayDiffer); + return MigrationTest(source) + .WithProperty("build_property.PublishTrimmed", "true") + .WithDiagnosticSeverity(DiagnosticIds.UseGenericMessageType, ReportDiagnostic.Default) + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); } [Test] - public async Task MigrationDiagnostics_SeverityIsScopedToConfiguredSyntaxTree() + public Task MigrationDiagnostics_AutomaticActivation_DefaultSeverityOnNSB0039KeepsNSB0040Enabled() + { + const string source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, int? message) + { + await [|session.Send(message)|]; + } + } + """; + return MigrationTest(source) + .WithProperty("build_property.PublishTrimmed", "true") + .WithDiagnosticSeverity(DiagnosticIds.UseGenericMessageType, ReportDiagnostic.Default) + .AssertDiagnostics(DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task MigrationDiagnostics_SeverityIsScopedToConfiguredSyntaxTree() { const string configuredSource = """ @@ -2476,31 +2333,25 @@ class UnconfiguredFoo { async Task Bar(IMessageSession session) { - await session.Send(new UnconfiguredMessage()); + await [|session.Send(new UnconfiguredMessage())|]; } } class UnconfiguredMessage : IMessage { } """; - var configuredTree = CSharpSyntaxTree.ParseText(configuredSource, path: "Configured.cs"); - var unconfiguredTree = CSharpSyntaxTree.ParseText(unconfiguredSource, path: "Unconfigured.cs"); - - var diagnostics = await GetEditorConfigDiagnostics( - [configuredTree, unconfiguredTree], - configuredTree, - DiagnosticIds.UseGenericMessageType, - ReportDiagnostic.Suppress, - automaticActivation: true); - - NUnit.Framework.Assert.That(diagnostics.Select(diagnostic => diagnostic.Id), Is.EquivalentTo([DiagnosticIds.UseGenericMessageType])); - NUnit.Framework.Assert.That(diagnostics[0].Location.SourceTree?.FilePath, Is.EqualTo("Unconfigured.cs")); + return AnalyzerTest.ForAnalyzer() + .WithSource(configuredSource, "Configured.cs") + .WithSource(unconfiguredSource, "Unconfigured.cs") + .WithProperty("build_property.PublishTrimmed", "true") + .WithDiagnosticSeverity(DiagnosticIds.UseGenericMessageType, ReportDiagnostic.Suppress, "Configured.cs") + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); } - // The global channel (TryGetGlobalDiagnosticValue) was invisible to the old implementation, so - // global severities that enable the diagnostics were ignored by the analyzer gate. + // The global severity channel (TryGetGlobalDiagnosticValue) was previously invisible to the + // analyzer gate, so global severities that enable these diagnostics were ignored. [Test] - public async Task MigrationDiagnostics_AutomaticActivation_RespectsGlobalNoneSeverity() + public Task MigrationDiagnostics_AutomaticActivation_RespectsGlobalNoneSeverity() { var source = """ @@ -2517,17 +2368,14 @@ async Task Bar(IMessageSession session) class MyMessage : IMessage { } """; - var diagnostics = await GetSeverityConfigDiagnostics( - source, - DiagnosticIds.UseGenericMessageType, - globalSeverity: ReportDiagnostic.Suppress, - automaticActivation: true); - - NUnit.Framework.Assert.That(diagnostics.Select(diagnostic => diagnostic.Id), Is.Empty); + return MigrationTest(source) + .WithProperty("build_property.PublishTrimmed", "true") + .WithGlobalDiagnosticSeverity(DiagnosticIds.UseGenericMessageType, ReportDiagnostic.Suppress) + .AssertDiagnostics(); } [Test] - public async Task MigrationDiagnostics_GlobalSeverity_EnablesWithoutAutomaticActivation() + public Task MigrationDiagnostics_GlobalSeverity_EnablesWithoutAutomaticActivation() { var source = """ @@ -2538,23 +2386,19 @@ class Foo { async Task Bar(IMessageSession session) { - await session.Send(new MyMessage()); + await [|session.Send(new MyMessage())|]; } } class MyMessage : IMessage { } """; - var diagnostics = await GetSeverityConfigDiagnostics( - source, - DiagnosticIds.UseGenericMessageType, - globalSeverity: ReportDiagnostic.Warn, - automaticActivation: false); - - NUnit.Framework.Assert.That(diagnostics.Select(diagnostic => diagnostic.Id), Is.EquivalentTo([DiagnosticIds.UseGenericMessageType])); + return MigrationTest(source) + .WithGlobalDiagnosticSeverity(DiagnosticIds.UseGenericMessageType, ReportDiagnostic.Warn) + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); } [Test] - public async Task MigrationDiagnostics_AutomaticActivation_RespectsBulkCategoryNoneSeverity() + public Task MigrationDiagnostics_AutomaticActivation_RespectsBulkCategoryNoneSeverity() { var source = """ @@ -2571,18 +2415,14 @@ async Task Bar(IMessageSession session) class MyMessage : IMessage { } """; - var diagnostics = await GetSeverityConfigDiagnostics( - source, - DiagnosticIds.UseGenericMessageType, - treeOptions: ImmutableDictionary.Empty.Add( - "dotnet_analyzer_diagnostic.category-NServiceBus.Code.severity", "none"), - automaticActivation: true); - - NUnit.Framework.Assert.That(diagnostics.Select(diagnostic => diagnostic.Id), Is.Empty); + return MigrationTest(source) + .WithProperty("build_property.PublishTrimmed", "true") + .WithEditorConfigOption("dotnet_analyzer_diagnostic.category-NServiceBus.Code.severity", "none") + .AssertDiagnostics(); } [Test] - public async Task MigrationDiagnostics_BulkCategorySeverity_EnablesWithoutAutomaticActivation() + public Task MigrationDiagnostics_BulkCategorySeverity_EnablesWithoutAutomaticActivation() { var source = """ @@ -2593,24 +2433,19 @@ class Foo { async Task Bar(IMessageSession session) { - await session.Send(new MyMessage()); + await [|session.Send(new MyMessage())|]; } } class MyMessage : IMessage { } """; - var diagnostics = await GetSeverityConfigDiagnostics( - source, - DiagnosticIds.UseGenericMessageType, - treeOptions: ImmutableDictionary.Empty.Add( - "dotnet_analyzer_diagnostic.category-NServiceBus.Code.severity", "warning"), - automaticActivation: false); - - NUnit.Framework.Assert.That(diagnostics.Select(diagnostic => diagnostic.Id), Is.EquivalentTo([DiagnosticIds.UseGenericMessageType])); + return MigrationTest(source) + .WithEditorConfigOption("dotnet_analyzer_diagnostic.category-NServiceBus.Code.severity", "warning") + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); } [Test] - public async Task MigrationDiagnostics_ExplicitPerRuleDefaultSeverity_BlocksBulkConfiguration() + public Task MigrationDiagnostics_ExplicitPerRuleDefaultSeverity_BlocksBulkConfiguration() { var source = """ @@ -2627,19 +2462,14 @@ async Task Bar(IMessageSession session) class MyMessage : IMessage { } """; - var diagnostics = await GetSeverityConfigDiagnostics( - source, - DiagnosticIds.UseGenericMessageType, - severity: ReportDiagnostic.Default, - treeOptions: ImmutableDictionary.Empty.Add( - "dotnet_analyzer_diagnostic.category-NServiceBus.Code.severity", "warning"), - automaticActivation: false); - - NUnit.Framework.Assert.That(diagnostics.Select(diagnostic => diagnostic.Id), Is.Empty); + return MigrationTest(source) + .WithDiagnosticSeverity(DiagnosticIds.UseGenericMessageType, ReportDiagnostic.Default) + .WithEditorConfigOption("dotnet_analyzer_diagnostic.category-NServiceBus.Code.severity", "warning") + .AssertDiagnostics(); } [Test] - public async Task MigrationDiagnostics_ExplicitGlobalDefaultSeverity_BlocksBulkConfiguration() + public Task MigrationDiagnostics_ExplicitGlobalDefaultSeverity_BlocksBulkConfiguration() { var source = """ @@ -2656,19 +2486,14 @@ async Task Bar(IMessageSession session) class MyMessage : IMessage { } """; - var diagnostics = await GetSeverityConfigDiagnostics( - source, - DiagnosticIds.UseGenericMessageType, - globalSeverity: ReportDiagnostic.Default, - treeOptions: ImmutableDictionary.Empty.Add( - "dotnet_analyzer_diagnostic.category-NServiceBus.Code.severity", "warning"), - automaticActivation: false); - - NUnit.Framework.Assert.That(diagnostics.Select(diagnostic => diagnostic.Id), Is.Empty); + return MigrationTest(source) + .WithGlobalDiagnosticSeverity(DiagnosticIds.UseGenericMessageType, ReportDiagnostic.Default) + .WithEditorConfigOption("dotnet_analyzer_diagnostic.category-NServiceBus.Code.severity", "warning") + .AssertDiagnostics(); } [Test] - public async Task MigrationDiagnostics_ExplicitPerRuleDefaultSeverity_StillHonorsAutomaticActivation() + public Task MigrationDiagnostics_ExplicitPerRuleDefaultSeverity_StillHonorsAutomaticActivation() { var source = """ @@ -2679,19 +2504,16 @@ class Foo { async Task Bar(IMessageSession session) { - await session.Send(new MyMessage()); + await [|session.Send(new MyMessage())|]; } } class MyMessage : IMessage { } """; - var diagnostics = await GetSeverityConfigDiagnostics( - source, - DiagnosticIds.UseGenericMessageType, - severity: ReportDiagnostic.Default, - automaticActivation: true); - - NUnit.Framework.Assert.That(diagnostics.Select(diagnostic => diagnostic.Id), Is.EquivalentTo([DiagnosticIds.UseGenericMessageType])); + return MigrationTest(source) + .WithProperty("build_property.PublishTrimmed", "true") + .WithDiagnosticSeverity(DiagnosticIds.UseGenericMessageType, ReportDiagnostic.Default) + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); } // ===== Negative tests ===== From 7bb5a57f06bf4e8a29dd9b944b386f6087cbe5db Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Fri, 4 Sep 2026 11:48:55 +0200 Subject: [PATCH 62/79] Logical mutators trimming and AOT improvements (#7925) * Make logical message mutators trimming-aware with declared message types Add typed replacement APIs so incoming and outgoing instance mutators can declare the logical message type instead of relying on runtime type discovery: - IIncomingLogicalMessageContext gains UpdateMessageInstance(T) and UpdateMessageInstance(object, Type) following the typed UpdateMessage precedent on IOutgoingLogicalMessageContext, with PreObsolete and RequiresUnreferencedCode on the runtime-type-routing object overload - IncomingLogicalMessageContext resolves replacement metadata through LogicalMessageFactory without reflection over the instance, preserving strict registered-only metadata mode - MutateIncomingMessageContext.UpdateMessageInstance and MutateOutgoingMessageContext.UpdateMessage accept typed or explicitly typed replacements and carry the declared type via a DAM-annotated internal property - Mutator behaviors pass the declared type into the pipeline contexts, keeping the legacy object path untouched - TestableIncomingLogicalMessageContext mirrors the typed members * Extend message migration analyzer to mutator context setters The legacy mutator pattern assigns an object to the Message or OutgoingMessage property of a mutator context, which routes by the runtime type. Analyze SimpleAssignment operations on those two setters and reuse the existing NSB0039/NSB0040 classification (direct creation and value types are provably safe; everything else is a runtime-type routing warning), gated by the same trimming/AOT build properties. The code fixer rewrites a safe assignment to the typed replacement API: - context.Message = new MyMessage() -> context.UpdateMessageInstance(new MyMessage()) - context.OutgoingMessage = new MyEvent() -> context.UpdateMessage(new MyEvent()) * Add incoming UpdateMessageInstance coverage to the migration analyzer * Simplify * Deprecate mutator context setters in favor of typed replacement APIs The Message and OutgoingMessage setters on the logical mutator contexts route by the runtime type, which the trimmer cannot analyze. The typed UpdateMessage/UpdateMessageInstance APIs added in this branch are the replacement, so the setters now warn on assignment using the Particular.Obsoletes authoring pair (error from 11, removed in 12). The setter-level attribute leaves the getter silent, verified on the current compiler. The typed methods assign the backing field directly to avoid self-triggered warnings, and deliberate legacy-path test coverage is pragma-suppressed until the setters are removed with the object-overload batch tracked in #7906. * Simplify interface method * Suppress mutator warnings because code path only reachable if using other RequiresUnreferencedCode is used (#7928) * Fix property name in outgoing mutator suppression justification --------- Co-authored-by: Daniel Marbach Co-authored-by: David Boike --- ..._01a0662e-95f5-7570-adde-5ca565b482e5.html | 4332 +++++++++++++++++ ...n_incoming_mutator_changes_message_type.cs | 2 + ...When_outgoing_mutator_replaces_instance.cs | 2 + .../MessagingMigrationFixer.cs | 85 + .../MessagingMigrationAnalyzerTests.cs | 344 ++ .../MessagingMigrationFixerTests.cs | 152 + .../MessagingMigrationAnalyzer.cs | 129 +- ...IApprovals.ApproveNServiceBus.approved.txt | 24 + ...s.ApproveTrimmabilityWarnings.approved.txt | 3 - .../MutateIncomingMessageBehaviorTests.cs | 72 + .../MutateOutgoingMessageBehaviorTests.cs | 72 + ...oming_mutator_replaces_message_instance.cs | 47 +- .../IncomingLogicalMessageContextTests.cs | 109 + .../TypedMessageInstanceOverloadsTests.cs | 60 + .../MutateIncomingMessageBehavior.cs | 20 +- .../MutateIncomingMessageContext.cs | 32 + .../MutateOutgoingMessageBehavior.cs | 20 +- .../MutateOutgoingMessageContext.cs | 37 + .../IIncomingLogicalMessageContext.cs | 38 +- .../Incoming/IncomingLogicalMessageContext.cs | 44 + .../IOutgoingLogicalMessageContext.cs | 5 +- .../TestableIncomingLogicalMessageContext.cs | 28 + 22 files changed, 5613 insertions(+), 44 deletions(-) create mode 100644 pi-session-2026-09-03T07-32-04-213Z_01a0662e-95f5-7570-adde-5ca565b482e5.html create mode 100644 src/NServiceBus.Core.Tests/Pipeline/Incoming/IncomingLogicalMessageContextTests.cs diff --git a/pi-session-2026-09-03T07-32-04-213Z_01a0662e-95f5-7570-adde-5ca565b482e5.html b/pi-session-2026-09-03T07-32-04-213Z_01a0662e-95f5-7570-adde-5ca565b482e5.html new file mode 100644 index 00000000000..3851f4dcb13 --- /dev/null +++ b/pi-session-2026-09-03T07-32-04-213Z_01a0662e-95f5-7570-adde-5ca565b482e5.html @@ -0,0 +1,4332 @@ + + + + + + Session Export + + + + + +
+ + +
+
+
+
+
+ +
+
+ + + + + + + + + + + + + diff --git a/src/NServiceBus.AcceptanceTests/Core/Mutators/When_incoming_mutator_changes_message_type.cs b/src/NServiceBus.AcceptanceTests/Core/Mutators/When_incoming_mutator_changes_message_type.cs index 0fcec518f63..ba140f0f21d 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Mutators/When_incoming_mutator_changes_message_type.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Mutators/When_incoming_mutator_changes_message_type.cs @@ -45,7 +45,9 @@ public class MessageMutator : IMutateIncomingMessages public Task MutateIncoming(MutateIncomingMessageContext context) { var original = (OriginalMessage)context.Message; +#pragma warning disable CS0618 // Deliberate coverage of the legacy runtime-type-routing setter until its removal context.Message = new NewMessage { SomeId = original.SomeId }; +#pragma warning restore CS0618 return Task.CompletedTask; } } diff --git a/src/NServiceBus.AcceptanceTests/Core/Mutators/When_outgoing_mutator_replaces_instance.cs b/src/NServiceBus.AcceptanceTests/Core/Mutators/When_outgoing_mutator_replaces_instance.cs index 53c8c2cdcfb..5f91a6c299d 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Mutators/When_outgoing_mutator_replaces_instance.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Mutators/When_outgoing_mutator_replaces_instance.cs @@ -38,7 +38,9 @@ public Task MutateOutgoing(MutateOutgoingMessageContext context) { if (context.OutgoingMessage is V1Message) { +#pragma warning disable CS0618 // Deliberate coverage of the legacy runtime-type-routing setter until its removal context.OutgoingMessage = new V2Message(); +#pragma warning restore CS0618 } return Task.CompletedTask; } diff --git a/src/NServiceBus.Core.Analyzer.Fixes/MessagingMigrationFixer.cs b/src/NServiceBus.Core.Analyzer.Fixes/MessagingMigrationFixer.cs index e3808dca7de..f7edb0f1931 100644 --- a/src/NServiceBus.Core.Analyzer.Fixes/MessagingMigrationFixer.cs +++ b/src/NServiceBus.Core.Analyzer.Fixes/MessagingMigrationFixer.cs @@ -68,6 +68,24 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) continue; } + if (node is AssignmentExpressionSyntax assignment && + TryGetMutatorReplacementMethod(semanticModel, assignment, out var replacementMethodName)) + { + context.RegisterCodeFix( + CodeAction.Create( + "Use the strongly typed message overload", + cancellationToken => ReplaceAssignmentWithTypedCall( + context.Document, + root, + assignment, + replacementMethodName, + messageType!, + cancellationToken), + EquivalenceKey), + diagnostic); + continue; + } + if (node.FirstAncestorOrSelf() is not { } invocation || !CanAddTypeArgument(invocation.Expression)) { @@ -209,6 +227,73 @@ static Task AddTypeArgumentToMethodReference( return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(methodReference, updatedMethodReference))); } + static bool TryGetMutatorReplacementMethod( + SemanticModel? semanticModel, + AssignmentExpressionSyntax assignment, + out string methodName) + { + methodName = null!; + if (semanticModel is null || assignment.Left is not MemberAccessExpressionSyntax memberAccess) + { + return false; + } + + var propertySymbol = semanticModel.GetSymbolInfo(memberAccess).Symbol; + if (propertySymbol is not IPropertySymbol + { + ContainingType: { } containingType + }) + { + return false; + } + + var containingTypeName = containingType.ToDisplayString(); + if (propertySymbol.Name == "Message" && + containingTypeName == "NServiceBus.MessageMutator.MutateIncomingMessageContext") + { + methodName = "UpdateMessageInstance"; + return true; + } + + if (propertySymbol.Name == "OutgoingMessage" && + containingTypeName == "NServiceBus.MessageMutator.MutateOutgoingMessageContext") + { + methodName = "UpdateMessage"; + return true; + } + + return false; + } + + static Task ReplaceAssignmentWithTypedCall( + Document document, + SyntaxNode root, + AssignmentExpressionSyntax assignment, + string methodName, + string messageType, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var memberAccess = (MemberAccessExpressionSyntax)assignment.Left; + var typeArgument = SyntaxFactory.ParseTypeName(messageType) + .WithAdditionalAnnotations(Simplifier.Annotation); + var typeArguments = SyntaxFactory.TypeArgumentList( + SyntaxFactory.SingletonSeparatedList(typeArgument)); + + var invocation = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + memberAccess.Expression, + SyntaxFactory.GenericName(SyntaxFactory.Identifier(methodName), typeArguments) + .WithTriviaFrom(memberAccess.Name)), + SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList(SyntaxFactory.Argument(assignment.Right)))) + .WithAdditionalAnnotations(Formatter.Annotation); + + return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(assignment, invocation))); + } + static ExpressionSyntax AddTypeArgumentToExpression(ExpressionSyntax expression, string messageType) { var typeArgument = SyntaxFactory.ParseTypeName(messageType) diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationAnalyzerTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationAnalyzerTests.cs index ba8fdbeb25d..669b1eaf181 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationAnalyzerTests.cs +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationAnalyzerTests.cs @@ -859,6 +859,198 @@ async Task Bar(IMessageSession session) return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); } + // ===== Mutator context setters ===== + + [Test] + public Task NSB0039_MutatorIncomingContext_DirectObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context) + { + [|context.Message = new MyMessage()|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MutatorOutgoingContext_DirectObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateOutgoingMessageContext context) + { + [|context.OutgoingMessage = new MyEvent()|]; + } + } + + class MyEvent : IEvent { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MutatorIncomingContext_ValueType() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context, MyValue message) + { + [|context.Message = message|]; + } + } + + struct MyValue : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0040_MutatorIncomingContext_VarObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context) + { + var message = new MyMessage(); + [|context.Message = message|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_MutatorOutgoingContext_VarObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateOutgoingMessageContext context) + { + var message = new MyEvent(); + [|context.OutgoingMessage = message|]; + } + } + + class MyEvent : IEvent { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_MutatorIncomingContext_SealedVariable() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context, MyMessage message) + { + [|context.Message = message|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_MutatorOutgoingContext_CreatedByMessageCreator() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateOutgoingMessageContext context, IMessageCreator creator) + { + [|context.OutgoingMessage = creator.CreateInstance()|]; + } + } + + class MyEvent : IEvent { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NoDiagnostic_MutatorContext_ObjectType() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context) + { + context.Message = new object(); + } + } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_MutatorContext_UnrelatedMessageProperty() + { + var source = + """ + using NServiceBus; + + class Foo + { + public object Message { get; set; } + + void Bar(Foo foo) + { + foo.Message = new MyMessage(); + } + } + + class MyMessage : IMessage { } + """; + return Assert(source); + } + // ===== NSB0041: Generic T == object ===== [Test] @@ -1003,6 +1195,158 @@ void Bar(IOutgoingLogicalMessageContext context, object message) return Assert(source, DiagnosticIds.GenericMessageTypeIsObject); } + // ===== UpdateMessageInstance on IIncomingLogicalMessageContext ===== + + [Test] + public Task NSB0039_UpdateMessageInstanceDirectObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + using System.Threading.Tasks; + + class Foo + { + void Bar(IIncomingLogicalMessageContext context) + { + [|context.UpdateMessageInstance(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0040_UpdateMessageInstanceVarObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class MyMessage : IMessage { } + + class Foo + { + void Bar(IIncomingLogicalMessageContext context) + { + var message = new MyMessage(); + [|context.UpdateMessageInstance(message)|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_UpdateMessageInstanceCreatedByMessageCreator() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IIncomingLogicalMessageContext context, IMessageCreator creator) + { + [|context.UpdateMessageInstance(creator.CreateInstance())|]; + } + } + + public interface IMyMessage { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_UpdateMessageInstanceSealedVariable() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IIncomingLogicalMessageContext context, MyMessage message) + { + [|context.UpdateMessageInstance(message)|]; + } + } + + public sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0041_GenericTIsObject_UpdateMessageInstance() + { + var source = + """ + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IIncomingLogicalMessageContext context, object message) + { + [|context.UpdateMessageInstance(message)|]; + } + } + """; + return Assert(source, DiagnosticIds.GenericMessageTypeIsObject); + } + + [Test] + public Task NoDiagnostic_UpdateMessageInstance_UnrelatedMethod() + { + var source = + """ + using NServiceBus; + + class Helper + { + public void UpdateMessageInstance(object message) { } + } + + class Foo + { + void Bar(Helper helper, MyMessage message) + { + helper.UpdateMessageInstance(message); + } + } + + class MyMessage : IMessage { } + """; + return Assert(source); + } + + [Test] + public Task NSB0039_TestableIncomingLogicalMessageContext_UpdateMessageInstance() + { + var source = + """ + using NServiceBus; + using NServiceBus.Testing; + + class Foo + { + void Bar(TestableIncomingLogicalMessageContext context) + { + [|context.UpdateMessageInstance(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return FakeMigrationTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + // ===== Method groups and delegates ===== [Test] diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationFixerTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationFixerTests.cs index 27e1fb13785..d9260e37460 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationFixerTests.cs +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationFixerTests.cs @@ -639,6 +639,158 @@ class MyMessage : IMessage { } return Assert(original, expected); } + [Test] + public Task UpdateMessageInstance() + { + var original = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IIncomingLogicalMessageContext context) + { + context.UpdateMessageInstance(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IIncomingLogicalMessageContext context) + { + context.UpdateMessageInstance(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task MutatorIncomingContext_Message() + { + var original = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context) + { + context.Message = new MyMessage(); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context) + { + context.UpdateMessageInstance(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task MutatorOutgoingContext_OutgoingMessage() + { + var original = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateOutgoingMessageContext context) + { + context.OutgoingMessage = new MyEvent(); + } + } + + class MyEvent : IEvent { } + """; + + var expected = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateOutgoingMessageContext context) + { + context.UpdateMessage(new MyEvent()); + } + } + + class MyEvent : IEvent { } + """; + + return Assert(original, expected); + } + + [Test] + public Task MutatorIncomingContext_MessageValueType() + { + var original = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context, MyValue message) + { + context.Message = message; + } + } + + struct MyValue : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context, MyValue message) + { + context.UpdateMessageInstance(message); + } + } + + struct MyValue : IMessage { } + """; + + return Assert(original, expected); + } + [Test] public Task MethodGroup_SessionSend() { diff --git a/src/NServiceBus.Core.Analyzer/MessagingMigrationAnalyzer.cs b/src/NServiceBus.Core.Analyzer/MessagingMigrationAnalyzer.cs index 6d7a0a82886..e58f09dcfb3 100644 --- a/src/NServiceBus.Core.Analyzer/MessagingMigrationAnalyzer.cs +++ b/src/NServiceBus.Core.Analyzer/MessagingMigrationAnalyzer.cs @@ -73,9 +73,101 @@ public override void Initialize(AnalysisContext context) startContext.RegisterOperationAction( operationContext => AnalyzeDelegateCreation(operationContext, knownTypes, severityConfiguration), OperationKind.DelegateCreation); + startContext.RegisterOperationAction( + operationContext => AnalyzeSimpleAssignment(operationContext, knownTypes, severityConfiguration), + OperationKind.SimpleAssignment); }); } + static void AnalyzeSimpleAssignment( + OperationAnalysisContext context, + KnownTypes knownTypes, + MigrationDiagnosticConfiguration severityConfiguration) + { + var assignment = (ISimpleAssignmentOperation)context.Operation; + if (assignment.Target is not IPropertyReferenceOperation + { + Instance: not null, + Property: { } property + }) + { + return; + } + + var replacementMethodName = ResolveMutatorReplacementMethod(property, knownTypes); + if (replacementMethodName is null) + { + return; + } + + var messageValue = UnwrapImplicitConversions(assignment.Value); + var messageType = messageValue.Type; + if (messageType is null || messageType.TypeKind == TypeKind.Dynamic || + messageValue.ConstantValue is { HasValue: true, Value: null }) + { + return; + } + + if (!messageType.CanBeReferencedByName) + { + return; + } + + // An object-typed assignment would be fixed to the generic overload with T = System.Object, + // which immediately violates NSB0041. Never offer a fixable NSB0039 for the object type. + if (messageType.SpecialType == SpecialType.System_Object) + { + return; + } + + // Mutator contexts preserve the previous logical type when the same instance is assigned + // again, mirroring UpdateMessage. Only direct creation and value types are provably safe. + if (IsRoutingEquivalent(messageValue, messageType, knownTypes.IMessageCreator, isUpdateMessage: true)) + { + if (!severityConfiguration.IsEnabled(context, assignment.Syntax.SyntaxTree, UseGenericTypeRule)) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + UseGenericTypeRule, + assignment.Syntax.GetLocation(), + ImmutableDictionary.Empty.Add( + MessageTypeProperty, + messageType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), + messageType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat))); + } + else + { + if (!severityConfiguration.IsEnabled(context, assignment.Syntax.SyntaxTree, RuntimeTypeMayDifferRule)) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + RuntimeTypeMayDifferRule, + assignment.Syntax.GetLocation(), + messageType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat))); + } + } + + static string? ResolveMutatorReplacementMethod(IPropertySymbol property, KnownTypes knownTypes) + { + if (property.Name == "Message" && + SymbolEqualityComparer.Default.Equals(property.ContainingType, knownTypes.MutateIncomingMessageContext)) + { + return "UpdateMessageInstance"; + } + + if (property.Name == "OutgoingMessage" && + SymbolEqualityComparer.Default.Equals(property.ContainingType, knownTypes.MutateOutgoingMessageContext)) + { + return "UpdateMessage"; + } + + return null; + } + static void AnalyzeDelegateCreation( OperationAnalysisContext context, KnownTypes knownTypes, @@ -130,7 +222,7 @@ static void AnalyzeDelegateCreation( // UpdateMessage reference types remain ambiguous because same-instance replacement can // preserve the previous logical type. Value-type method groups cannot bind here (CS0123). - var isRoutingEquivalent = declaration.Name == "UpdateMessage" + var isRoutingEquivalent = declaration.Name is "UpdateMessage" or "UpdateMessageInstance" ? messageType.IsValueType : IsRoutingEquivalentMessageType(messageType); if (isRoutingEquivalent) @@ -224,7 +316,7 @@ argument.Parameter is not null && return; } - var isUpdateMessage = declaration.Name == "UpdateMessage"; + var isUpdateMessage = declaration.Name is "UpdateMessage" or "UpdateMessageInstance"; var isStableVarObjectCreation = !isUpdateMessage && IsStableVarObjectCreation(messageValue, messageArgument, invocation, invocation.SemanticModel!); if (IsRoutingEquivalent(messageValue, messageType, knownTypes.IMessageCreator, isUpdateMessage) || @@ -601,6 +693,11 @@ static bool IsTargetMethod(IMethodSymbol method, KnownTypes knownTypes, out IMet return method.Name == "UpdateMessage"; } + if (SymbolEqualityComparer.Default.Equals(containingType, knownTypes.IIncomingLogicalMessageContext)) + { + return method.Name == "UpdateMessageInstance"; + } + return ImplementsKnownContractMember(method, knownTypes, out contractMember); } @@ -608,7 +705,7 @@ static bool IsTargetMethod(IMethodSymbol method, KnownTypes knownTypes, out IMet static bool ImplementsKnownContractMember(IMethodSymbol method, KnownTypes knownTypes, out IMethodSymbol? contractMember) { contractMember = null; - if (method.Name is not ("Send" or "Publish" or "Reply" or "UpdateMessage")) + if (method.Name is not ("Send" or "Publish" or "Reply" or "UpdateMessage" or "UpdateMessageInstance")) { return false; } @@ -660,7 +757,10 @@ sealed class KnownTypes INamedTypeSymbol messageProcessingContextExtensions, INamedTypeSymbol saga, INamedTypeSymbol outgoingLogicalMessageContext, - INamedTypeSymbol messageCreator) + INamedTypeSymbol incomingLogicalMessageContext, + INamedTypeSymbol messageCreator, + INamedTypeSymbol mutateIncomingMessageContext, + INamedTypeSymbol mutateOutgoingMessageContext) { IMessageSession = messageSession; IPipelineContext = pipelineContext; @@ -670,13 +770,17 @@ sealed class KnownTypes MessageProcessingContextExtensions = messageProcessingContextExtensions; Saga = saga; IOutgoingLogicalMessageContext = outgoingLogicalMessageContext; + IIncomingLogicalMessageContext = incomingLogicalMessageContext; IMessageCreator = messageCreator; + MutateIncomingMessageContext = mutateIncomingMessageContext; + MutateOutgoingMessageContext = mutateOutgoingMessageContext; ContractInterfaces = [ messageSession, pipelineContext, messageProcessingContext, - outgoingLogicalMessageContext + outgoingLogicalMessageContext, + incomingLogicalMessageContext ]; } @@ -688,7 +792,10 @@ sealed class KnownTypes public INamedTypeSymbol MessageProcessingContextExtensions { get; } public INamedTypeSymbol Saga { get; } public INamedTypeSymbol IOutgoingLogicalMessageContext { get; } + public INamedTypeSymbol IIncomingLogicalMessageContext { get; } public INamedTypeSymbol IMessageCreator { get; } + public INamedTypeSymbol MutateIncomingMessageContext { get; } + public INamedTypeSymbol MutateOutgoingMessageContext { get; } public ImmutableArray ContractInterfaces { get; } readonly ConcurrentDictionary implementsContractCache = new(SymbolEqualityComparer.Default); @@ -757,12 +864,17 @@ public static bool TryCreate(Compilation compilation, out KnownTypes knownTypes) var messageProcessingContextExtensions = compilation.GetTypeByMetadataName("NServiceBus.MessageProcessingContextExtensions"); var saga = compilation.GetTypeByMetadataName("NServiceBus.Saga"); var outgoingLogicalMessageContext = compilation.GetTypeByMetadataName("NServiceBus.Pipeline.IOutgoingLogicalMessageContext"); + var incomingLogicalMessageContext = compilation.GetTypeByMetadataName("NServiceBus.Pipeline.IIncomingLogicalMessageContext"); var messageCreator = compilation.GetTypeByMetadataName("NServiceBus.IMessageCreator"); + var mutateIncomingMessageContext = compilation.GetTypeByMetadataName("NServiceBus.MessageMutator.MutateIncomingMessageContext"); + var mutateOutgoingMessageContext = compilation.GetTypeByMetadataName("NServiceBus.MessageMutator.MutateOutgoingMessageContext"); if (messageSession is null || pipelineContext is null || messageProcessingContext is null || messageSessionExtensions is null || pipelineContextExtensions is null || messageProcessingContextExtensions is null || saga is null || - outgoingLogicalMessageContext is null || messageCreator is null) + outgoingLogicalMessageContext is null || messageCreator is null || + incomingLogicalMessageContext is null || + mutateIncomingMessageContext is null || mutateOutgoingMessageContext is null) { knownTypes = null!; return false; @@ -777,7 +889,10 @@ messageProcessingContextExtensions is null || saga is null || messageProcessingContextExtensions, saga, outgoingLogicalMessageContext, - messageCreator); + incomingLogicalMessageContext, + messageCreator, + mutateIncomingMessageContext, + mutateOutgoingMessageContext); return true; } } diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index b292515cc56..3b421c88eb9 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -1881,7 +1881,15 @@ namespace NServiceBus.MessageMutator public MutateIncomingMessageContext(object message, System.Collections.Generic.Dictionary headers, System.Threading.CancellationToken cancellationToken = default) { } public System.Threading.CancellationToken CancellationToken { get; } public System.Collections.Generic.Dictionary Headers { get; } + [set: System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] + [set: System.Obsolete("Use \'UpdateMessageInstance(T)\' or \'UpdateMessageInstance(object, Type)\' instea" + + "d. Will be treated as an error from version 11.0.0. Will be removed in version 1" + + "2.0.0.", false)] public object Message { get; set; } + public void UpdateMessageInstance(object newMessage, [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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType) { } + public void UpdateMessageInstance<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T newMessage) { } } public class MutateIncomingTransportMessageContext : NServiceBus.ICancellableContext { @@ -1895,9 +1903,16 @@ namespace NServiceBus.MessageMutator public MutateOutgoingMessageContext(object outgoingMessage, System.Collections.Generic.Dictionary outgoingHeaders, object? incomingMessage, System.Collections.Generic.IReadOnlyDictionary? incomingHeaders, System.Threading.CancellationToken cancellationToken = default) { } public System.Threading.CancellationToken CancellationToken { get; } public System.Collections.Generic.Dictionary OutgoingHeaders { get; } + [set: System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] + [set: System.Obsolete("Use \'UpdateMessage(T)\' or \'UpdateMessage(object, Type)\' instead. Will be treat" + + "ed as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] public object OutgoingMessage { get; set; } public bool TryGetIncomingHeaders([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out System.Collections.Generic.IReadOnlyDictionary? incomingHeaders) { } public bool TryGetIncomingMessage([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out object? incomingMessage) { } + public void UpdateMessage(object newMessage, [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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType) { } + public void UpdateMessage<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T newMessage) { } } public class MutateOutgoingTransportMessageContext : NServiceBus.ICancellableContext { @@ -2045,7 +2060,16 @@ namespace NServiceBus.Pipeline System.Collections.Generic.Dictionary Headers { get; } NServiceBus.Pipeline.LogicalMessage Message { get; } bool MessageHandled { get; set; } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] void UpdateMessageInstance(object newInstance); + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification="The default interface implementation preserves compatibility with third-party imp" + + "lementations. Built-in implementations override this method and preserve the dec" + + "lared message type.")] + void UpdateMessageInstance(object newInstance, [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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType); + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + void UpdateMessageInstance<[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.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T newInstance); } public interface IIncomingPhysicalMessageContext : NServiceBus.Extensibility.IExtendable, NServiceBus.ICancellableContext, NServiceBus.IMessageProcessingContext, NServiceBus.IPipelineContext, NServiceBus.Pipeline.IBehaviorContext, NServiceBus.Pipeline.IIncomingContext { diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt index 4f8f1f1378e..c835831f11f 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt @@ -2,9 +2,6 @@ The following trimming warnings are present in NServiceBus.Core. Changes that make this list longer should not be approved. ----- -src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs - IL2026: Using member 'NServiceBus.Pipeline.IOutgoingLogicalMessageContext.UpdateMessage(Object)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. When trimming is enabled, routing a message using its runtime type cannot be statically analyzed by the trimmer. Use the generic overload or, when the message type is not known at compile time, the overload accepting an explicit Type. - 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. diff --git a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs index 0fa1ce103f1..f4ca7e73f90 100644 --- a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs +++ b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs @@ -1,5 +1,6 @@ namespace NServiceBus.Core.Tests.MessageMutators.MutateInstanceMessage; +using System; using System.Threading.Tasks; using MessageMutator; using Microsoft.Extensions.DependencyInjection; @@ -126,15 +127,68 @@ public async Task When_mutator_modifies_the_body_should_update_the_body() Assert.That(context.UpdateMessageCalled, Is.True); } + [Test] + public async Task When_mutator_declares_a_message_type_should_use_the_explicit_type_overload() + { + var behavior = new MutateIncomingMessageBehavior([]); + + var context = new InterceptUpdateMessageIncomingLogicalMessageContext(); + + context.Services.AddTransient(sp => new MutatorWhichDeclaresAMessageType()); + + await behavior.Invoke(context, ctx => Task.CompletedTask); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.UpdateMessageObjCalled, Is.False); + Assert.That(context.UpdateMessageWithTypeCalled, Is.True); + Assert.That(context.DeclaredMessageType, Is.EqualTo(typeof(IMyMessage))); + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + } + } + + [Test] + public async Task When_mutator_uses_the_object_setter_should_use_the_object_overload() + { + var behavior = new MutateIncomingMessageBehavior([]); + + var context = new InterceptUpdateMessageIncomingLogicalMessageContext(); + + context.Services.AddTransient(sp => new MutatorWhichMutatesTheBody()); + + await behavior.Invoke(context, ctx => Task.CompletedTask); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.UpdateMessageObjCalled, Is.True); + Assert.That(context.UpdateMessageWithTypeCalled, Is.False); + } + } + class InterceptUpdateMessageIncomingLogicalMessageContext : TestableIncomingLogicalMessageContext { public bool UpdateMessageCalled { get; private set; } + public bool UpdateMessageObjCalled { get; private set; } + + public bool UpdateMessageWithTypeCalled { get; private set; } + + public Type DeclaredMessageType { get; private set; } + public override void UpdateMessageInstance(object newInstance) { base.UpdateMessageInstance(newInstance); UpdateMessageCalled = true; + UpdateMessageObjCalled = true; + } + + public override void UpdateMessageInstance(object newInstance, Type messageType) + { + base.UpdateMessageInstance(newInstance, messageType); + + UpdateMessageWithTypeCalled = true; + DeclaredMessageType = messageType; } } @@ -162,7 +216,19 @@ class MutatorWhichMutatesTheBody : IMutateIncomingMessages { public Task MutateIncoming(MutateIncomingMessageContext context) { +#pragma warning disable CS0618 // Deliberate coverage of the legacy runtime-type-routing setter until its removal context.Message = new object(); +#pragma warning restore CS0618 + + return Task.CompletedTask; + } + } + + class MutatorWhichDeclaresAMessageType : IMutateIncomingMessages + { + public Task MutateIncoming(MutateIncomingMessageContext context) + { + context.UpdateMessageInstance(new MyMessage()); return Task.CompletedTask; } @@ -178,4 +244,10 @@ public Task MutateIncoming(MutateIncomingMessageContext context) class TestMessage : IMessage { } + + interface IMyMessage : IMessage + { } + + class MyMessage : IMyMessage + { } } diff --git a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehaviorTests.cs b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehaviorTests.cs index b16ed6426c1..1161d2f18b1 100644 --- a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehaviorTests.cs +++ b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehaviorTests.cs @@ -1,5 +1,6 @@ namespace NServiceBus.Core.Tests.MessageMutators.MutateInstanceMessage; +using System; using System.Threading.Tasks; using MessageMutator; using Microsoft.Extensions.DependencyInjection; @@ -122,15 +123,68 @@ public async Task When_mutator_modifies_the_body_should_update_the_body() Assert.That(context.UpdateMessageCalled, Is.True); } + [Test] + public async Task When_mutator_declares_a_message_type_should_use_the_explicit_type_overload() + { + var behavior = new MutateOutgoingMessageBehavior([]); + + var context = new InterceptUpdateMessageOutgoingLogicalMessageContext(); + + context.Services.AddTransient(sp => new MutatorWhichDeclaresAMessageType()); + + await behavior.Invoke(context, ctx => Task.CompletedTask); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.UpdateMessageObjCalled, Is.False); + Assert.That(context.UpdateMessageWithTypeCalled, Is.True); + Assert.That(context.DeclaredMessageType, Is.EqualTo(typeof(IMyMessage))); + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + } + } + + [Test] + public async Task When_mutator_uses_the_object_setter_should_use_the_object_overload() + { + var behavior = new MutateOutgoingMessageBehavior([]); + + var context = new InterceptUpdateMessageOutgoingLogicalMessageContext(); + + context.Services.AddTransient(sp => new MutatorWhichMutatesTheBody()); + + await behavior.Invoke(context, ctx => Task.CompletedTask); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.UpdateMessageObjCalled, Is.True); + Assert.That(context.UpdateMessageWithTypeCalled, Is.False); + } + } + class InterceptUpdateMessageOutgoingLogicalMessageContext : TestableOutgoingLogicalMessageContext { public bool UpdateMessageCalled { get; private set; } + public bool UpdateMessageObjCalled { get; private set; } + + public bool UpdateMessageWithTypeCalled { get; private set; } + + public Type DeclaredMessageType { get; private set; } + public override void UpdateMessage(object newInstance) { base.UpdateMessage(newInstance); UpdateMessageCalled = true; + UpdateMessageObjCalled = true; + } + + public override void UpdateMessage(object newInstance, Type messageType) + { + base.UpdateMessage(newInstance, messageType); + + UpdateMessageWithTypeCalled = true; + DeclaredMessageType = messageType; } } @@ -166,9 +220,27 @@ class MutatorWhichMutatesTheBody : IMutateOutgoingMessages { public Task MutateOutgoing(MutateOutgoingMessageContext context) { +#pragma warning disable CS0618 // Deliberate coverage of the legacy runtime-type-routing setter until its removal context.OutgoingMessage = new object(); +#pragma warning restore CS0618 return Task.CompletedTask; } } + + class MutatorWhichDeclaresAMessageType : IMutateOutgoingMessages + { + public Task MutateOutgoing(MutateOutgoingMessageContext context) + { + context.UpdateMessage(new MyMessage()); + + return Task.CompletedTask; + } + } + + interface IMyMessage : IMessage + { } + + class MyMessage : IMyMessage + { } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/When_incoming_mutator_replaces_message_instance.cs b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/When_incoming_mutator_replaces_message_instance.cs index c1dc73df095..47317c70202 100644 --- a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/When_incoming_mutator_replaces_message_instance.cs +++ b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/When_incoming_mutator_replaces_message_instance.cs @@ -17,69 +17,62 @@ public class When_incoming_mutator_replaces_message_instance [Test] public async Task Should_recompute_metadata_for_the_replacement_instance_type() { - var registry = new MessageMetadataRegistry(); - registry.Initialize(new Conventions().IsMessageType, true); - registry.RegisterMessageTypes([typeof(OriginalMessage), typeof(ReplacementMessage)]); - - var context = CreateContext(registry, new ReplaceWithReplacementMessageMutator()); + var context = CreateContext(new ReplaceWithReplacementMessageMutator()); + var behavior = new MutateIncomingMessageBehavior([]); - await context.Behavior.Invoke(context.Context, ctx => Task.CompletedTask); + await behavior.Invoke(context, ctx => Task.CompletedTask); using (Assert.EnterMultipleScope()) { - Assert.That(context.Context.Message.Instance, Is.TypeOf()); - Assert.That(context.Context.Message.MessageType, Is.EqualTo(typeof(ReplacementMessage))); - Assert.That(context.Context.Message.Metadata.MessageType, Is.EqualTo(typeof(ReplacementMessage))); + Assert.That(context.Message.Instance, Is.TypeOf()); + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(ReplacementMessage))); + Assert.That(context.Message.Metadata.MessageType, Is.EqualTo(typeof(ReplacementMessage))); } } [Test] public async Task Should_keep_original_metadata_when_instance_is_not_replaced() { - var registry = new MessageMetadataRegistry(); - registry.Initialize(new Conventions().IsMessageType, true); - registry.RegisterMessageTypes([typeof(OriginalMessage), typeof(ReplacementMessage)]); - - var context = CreateContext(registry, new DoNothingMutator()); + var context = CreateContext(new DoNothingMutator()); + var behavior = new MutateIncomingMessageBehavior([]); - await context.Behavior.Invoke(context.Context, ctx => Task.CompletedTask); + await behavior.Invoke(context, ctx => Task.CompletedTask); using (Assert.EnterMultipleScope()) { - Assert.That(context.Context.Message.Instance, Is.TypeOf()); - Assert.That(context.Context.Message.MessageType, Is.EqualTo(typeof(OriginalMessage))); + Assert.That(context.Message.Instance, Is.TypeOf()); + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(OriginalMessage))); } } - static ContextFixture CreateContext(MessageMetadataRegistry registry, IMutateIncomingMessages mutator) + static IncomingLogicalMessageContext CreateContext(IMutateIncomingMessages mutator) { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + registry.RegisterMessageTypes([typeof(OriginalMessage), typeof(ReplacementMessage)]); + var services = new ServiceCollection(); services.AddSingleton(registry); services.AddSingleton(); services.AddSingleton(new TrimmingSafeMessageMapper()); + services.AddSingleton(mutator); IServiceProvider provider = services.BuildServiceProvider(); var parentContext = new TestableIncomingPhysicalMessageContext(); parentContext.Extensions.Set(provider); var logicalMessage = new LogicalMessage(registry.GetMessageMetadata(typeof(OriginalMessage)), new OriginalMessage()); - var context = new IncomingLogicalMessageContext(logicalMessage, parentContext); - var behavior = new MutateIncomingMessageBehavior([mutator]); - return new ContextFixture(context, behavior); - } - - class ContextFixture(IncomingLogicalMessageContext context, MutateIncomingMessageBehavior behavior) - { - public IncomingLogicalMessageContext Context { get; } = context; - public MutateIncomingMessageBehavior Behavior { get; } = behavior; + return new IncomingLogicalMessageContext(logicalMessage, parentContext); } class ReplaceWithReplacementMessageMutator : IMutateIncomingMessages { public Task MutateIncoming(MutateIncomingMessageContext context) { +#pragma warning disable CS0618 // Deliberate coverage of the legacy runtime-type-routing setter until its removal context.Message = new ReplacementMessage(); +#pragma warning restore CS0618 return Task.CompletedTask; } } diff --git a/src/NServiceBus.Core.Tests/Pipeline/Incoming/IncomingLogicalMessageContextTests.cs b/src/NServiceBus.Core.Tests/Pipeline/Incoming/IncomingLogicalMessageContextTests.cs new file mode 100644 index 00000000000..59e968fd5f1 --- /dev/null +++ b/src/NServiceBus.Core.Tests/Pipeline/Incoming/IncomingLogicalMessageContextTests.cs @@ -0,0 +1,109 @@ +namespace NServiceBus.Core.Tests.Pipeline.Incoming; + +using System; +using MessageInterfaces; +using MessageInterfaces.MessageMapper.Reflection; +using Microsoft.Extensions.DependencyInjection; +using NServiceBus.Pipeline; +using NUnit.Framework; +using Testing; +using Unicast.Messages; + +[TestFixture] +public class IncomingLogicalMessageContextTests +{ + [Test] + public void Updating_the_message_to_a_new_type_should_update_the_MessageType() + { + var context = CreateContext(typeof(MyDifferentMessage)); + + var differentMessage = new MyDifferentMessage(); + context.UpdateMessageInstance(differentMessage); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(MyDifferentMessage))); + Assert.That(context.Message.Instance, Is.SameAs(differentMessage)); + } + } + + [Test] + public void Updating_the_existing_instance_with_a_different_explicit_type_should_use_that_type() + { + var message = new MySubMessage(); + var context = CreateContext(typeof(MySubMessage), message); + + context.UpdateMessageInstance(message); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(MyDifferentMessage))); + Assert.That(context.Message.Instance, Is.SameAs(message)); + } + } + + [Test] + public void Updating_the_existing_instance_with_the_same_type_should_preserve_the_metadata() + { + var message = new MyDifferentMessage(); + var context = CreateContext(typeof(MyDifferentMessage), message); + + var metadataBefore = context.Message.Metadata; + + context.UpdateMessageInstance(message); + + Assert.That(context.Message.Metadata, Is.SameAs(metadataBefore)); + } + + [Test] + public void Updating_with_an_explicit_type_that_is_not_assignable_should_throw() + { + var context = CreateContext(typeof(MyDifferentMessage)); + + Assert.Throws(() => context.UpdateMessageInstance(new MyDifferentMessage(), typeof(string))); + } + + [Test] + public void Updating_with_a_null_instance_should_throw() + { + var context = CreateContext(typeof(MyDifferentMessage)); + + Assert.Throws(() => context.UpdateMessageInstance(null!, typeof(MyDifferentMessage))); + } + + [Test] + public void Updating_with_a_null_message_type_should_throw() + { + var context = CreateContext(typeof(MyDifferentMessage)); + + Assert.Throws(() => context.UpdateMessageInstance(new MyDifferentMessage(), null!)); + } + + static IncomingLogicalMessageContext CreateContext(Type messageType, object instance = null) + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + registry.RegisterMessageTypes([typeof(MyDifferentMessage), typeof(MySubMessage)]); + var services = new ServiceCollection(); + services.AddSingleton(registry); + services.AddSingleton(); + services.AddSingleton(new TrimmingSafeMessageMapper()); + IServiceProvider provider = services.BuildServiceProvider(); + + var parentContext = new TestableIncomingPhysicalMessageContext(); + parentContext.Extensions.Set(provider); + + instance ??= new MyDifferentMessage(); + + var logicalMessage = new LogicalMessage(registry.GetMessageMetadata(messageType), instance); + var context = new IncomingLogicalMessageContext(logicalMessage, parentContext); + + return context; + } + + class MyDifferentMessage : IMessage + { } + + class MySubMessage : MyDifferentMessage + { } +} diff --git a/src/NServiceBus.Core.Tests/TypedMessageInstanceOverloadsTests.cs b/src/NServiceBus.Core.Tests/TypedMessageInstanceOverloadsTests.cs index b72c5522885..9683dbd95ea 100644 --- a/src/NServiceBus.Core.Tests/TypedMessageInstanceOverloadsTests.cs +++ b/src/NServiceBus.Core.Tests/TypedMessageInstanceOverloadsTests.cs @@ -332,6 +332,66 @@ public void Testable_outgoing_context_explicit_type_preserves_declared_type_and_ Assert.That(context.Message.Instance, Is.SameAs(message)); } + [Test] + public void Testable_incoming_context_ordinary_call_uses_runtime_type() + { + var context = new TestableIncomingLogicalMessageContext(); + var message = (IMyMessage)new MyMessage(); + + context.UpdateMessageInstance(message); + + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(MyMessage))); + } + + [Test] + public void Testable_incoming_context_explicit_generic_call_uses_specified_type() + { + var context = new TestableIncomingLogicalMessageContext(); + var message = new MyMessage(); + + context.UpdateMessageInstance(message); + + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + } + + [Test] + public void Testable_incoming_context_explicit_type_validates_declared_type() + { + var context = new TestableIncomingLogicalMessageContext(); + object message = new MyMessage(); + + Assert.Throws(() => context.UpdateMessageInstance(message, typeof(MyOtherMessage))); + } + + [Test] + public void Testable_incoming_context_explicit_type_rejects_null_instance() + { + var context = new TestableIncomingLogicalMessageContext(); + + Assert.Throws(() => context.UpdateMessageInstance(null!, typeof(IMyMessage))); + } + + [Test] + public void Testable_incoming_context_explicit_type_rejects_null_message_type() + { + var context = new TestableIncomingLogicalMessageContext(); + var message = new MyMessage(); + + Assert.Throws(() => context.UpdateMessageInstance(message, null!)); + } + + [Test] + public void Testable_incoming_context_explicit_type_preserves_declared_type_and_instance() + { + var context = new TestableIncomingLogicalMessageContext(); + object message = new MyMessage(); + + context.UpdateMessageInstance(message, typeof(IMyMessage)); + + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + Assert.That(context.Message.Instance, Is.SameAs(message)); + } + [Test] public async Task Default_interface_fallback_Send_uses_object_overload() { diff --git a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehavior.cs b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehavior.cs index 6f1e1423048..ef9de36e969 100644 --- a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehavior.cs +++ b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehavior.cs @@ -4,6 +4,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using MessageMutator; using Microsoft.Extensions.DependencyInjection; @@ -46,11 +47,28 @@ await mutator.MutateIncoming(mutatorContext) if (mutatorContext.MessageInstanceChanged) { - context.UpdateMessageInstance(mutatorContext.Message); + UpdateMessageInstance(context, mutatorContext); } await next(context).ConfigureAwait(false); } + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", + Justification = "Path without compiler-known type can only be visited if MutateIncomingMessageContext.Message setter is used.")] +#pragma warning disable PS0015 // Multiple cancellable contexts are fine here + static void UpdateMessageInstance(IIncomingLogicalMessageContext context, MutateIncomingMessageContext mutatorContext) +#pragma warning restore PS0015 + { + if (mutatorContext.ReplacementMessageType != null) + { + context.UpdateMessageInstance(mutatorContext.Message, mutatorContext.ReplacementMessageType); + } + else + { + // Requires code path to use MutateIncomingMessageContext.Message which is marked as RequiresUnreferencedCode + context.UpdateMessageInstance(mutatorContext.Message); + } + } + volatile bool hasIncomingMessageMutators = true; } \ No newline at end of file diff --git a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs index 07f6b1ac709..3a69dfffb95 100644 --- a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs +++ b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs @@ -4,7 +4,9 @@ namespace NServiceBus.MessageMutator; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading; +using Particular.Obsoletes; /// /// Provides ways to mutate the outgoing message instance. @@ -29,6 +31,11 @@ public MutateIncomingMessageContext(object message, Dictionary h public object Message { get => message; + [ObsoleteMetadata(ReplacementTypeOrMember = "UpdateMessageInstance(T)", + TreatAsErrorFromVersion = "11", + RemoveInVersion = "12")] + [Obsolete("Use 'UpdateMessageInstance(T)' or 'UpdateMessageInstance(object, Type)' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] set { ArgumentNullException.ThrowIfNull(value); @@ -37,6 +44,28 @@ public object Message } } + /// + /// Replaces the current incoming message with the provided typed message instance. + /// + /// The type used to update the message. It determines the logical message type and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The replacement message instance. + public void UpdateMessageInstance<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newMessage) => UpdateMessageInstance(newMessage!, typeof(T)); + + /// + /// Replaces the current incoming message with the provided message instance and message type. The declared type determines the logical message type. + /// + /// The replacement message instance. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + public void UpdateMessageInstance(object newMessage, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + MessageTypeValidator.Validate(newMessage, messageType); + message = newMessage; + MessageInstanceChanged = true; + ReplacementMessageType = messageType; + } + /// /// The current incoming headers. /// @@ -50,4 +79,7 @@ public object Message object message; internal bool MessageInstanceChanged; + + [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] + internal Type? ReplacementMessageType; } \ No newline at end of file diff --git a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs index 8fedadcebe2..7e6551878ea 100644 --- a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs +++ b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs @@ -4,6 +4,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using MessageMutator; using Microsoft.Extensions.DependencyInjection; @@ -52,11 +53,28 @@ await mutator.MutateOutgoing(mutatorContext) if (mutatorContext.MessageInstanceChanged) { - context.UpdateMessage(mutatorContext.OutgoingMessage); + UpdateMessage(context, mutatorContext); } await next(context).ConfigureAwait(false); } + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", + Justification = "Path without compiler-known type can only be visited if MutateOutgoingMessageContext.OutgoingMessage setter is used.")] +#pragma warning disable PS0015 // Multiple cancellable contexts are fine here + static void UpdateMessage(IOutgoingLogicalMessageContext context, MutateOutgoingMessageContext mutatorContext) +#pragma warning restore PS0015 + { + if (mutatorContext.ReplacementMessageType != null) + { + context.UpdateMessage(mutatorContext.OutgoingMessage, mutatorContext.ReplacementMessageType); + } + else + { + // Requires code path to use MutateOutgoingMessageContext.OutgoingMessage which is marked as RequiresUnreferencedCode + context.UpdateMessage(mutatorContext.OutgoingMessage); + } + } + volatile bool hasOutgoingMessageMutators = true; } \ No newline at end of file diff --git a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs index 8eba75e3684..1804f38e185 100644 --- a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs +++ b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs @@ -6,6 +6,7 @@ namespace NServiceBus.MessageMutator; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Threading; +using Particular.Obsoletes; /// /// Provides ways to mutate the outgoing message instance. @@ -32,6 +33,11 @@ public MutateOutgoingMessageContext(object outgoingMessage, Dictionary outgoingMessage; + [ObsoleteMetadata(ReplacementTypeOrMember = "UpdateMessage(T)", + TreatAsErrorFromVersion = "11", + RemoveInVersion = "12")] + [Obsolete("Use 'UpdateMessage(T)' or 'UpdateMessage(object, Type)' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] set { ArgumentNullException.ThrowIfNull(value); @@ -40,6 +46,31 @@ public object OutgoingMessage } } + /// + /// Replaces the current outgoing message with the provided typed message instance. + /// + /// The type used to update the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The replacement message instance. + public void UpdateMessage<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newMessage) + { + UpdateMessage(newMessage!, typeof(T)); + } + + /// + /// Replaces the current outgoing message with the provided message instance and message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The replacement message instance. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + public void UpdateMessage(object newMessage, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + MessageTypeValidator.Validate(newMessage, messageType); + outgoingMessage = newMessage; + MessageInstanceChanged = true; + ReplacementMessageType = messageType; + } + /// /// The current outgoing headers. /// @@ -73,5 +104,11 @@ public bool TryGetIncomingHeaders([NotNullWhen(true)] out IReadOnlyDictionary + /// The declared logical message type of when a mutator supplied an explicit type, otherwise . + /// + [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] + internal Type? ReplacementMessageType { get; set; } + object outgoingMessage; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Pipeline/Incoming/IIncomingLogicalMessageContext.cs b/src/NServiceBus.Core/Pipeline/Incoming/IIncomingLogicalMessageContext.cs index 4af169ac758..f554a585482 100644 --- a/src/NServiceBus.Core/Pipeline/Incoming/IIncomingLogicalMessageContext.cs +++ b/src/NServiceBus.Core/Pipeline/Incoming/IIncomingLogicalMessageContext.cs @@ -2,7 +2,11 @@ namespace NServiceBus.Pipeline; +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Particular.Obsoletes; /// /// A context of behavior execution in logical message processing stage. @@ -25,8 +29,40 @@ public interface IIncomingLogicalMessageContext : IIncomingContext bool MessageHandled { get; set; } /// - /// Updates the message instance contained in . + /// Updates the message instance contained in . /// /// The new instance. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "UpdateMessageInstance(T)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] void UpdateMessageInstance(object newInstance); + + /// + /// Updates the message instance contained in while preserving the specified message type. + /// + /// The type used to update the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The replacement message instance. + [OverloadResolutionPriority(-1)] + void UpdateMessageInstance<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newInstance) + { + UpdateMessageInstance(newInstance!, typeof(T)); + } + + /// + /// Updates the message instance contained in with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The replacement message instance. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + /// + /// Third-party implementations that inherit this default implementation fall back to the object overload and route by the runtime type of . Override this method to preserve a declared that differs from the runtime type. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = MessageOperations.DefaultInterfaceTrimmingSuppressionJustification)] + void UpdateMessageInstance(object newInstance, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + MessageTypeValidator.Validate(newInstance, messageType); + UpdateMessageInstance(newInstance); + } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Pipeline/Incoming/IncomingLogicalMessageContext.cs b/src/NServiceBus.Core/Pipeline/Incoming/IncomingLogicalMessageContext.cs index 9ed9aee336f..147493cd987 100644 --- a/src/NServiceBus.Core/Pipeline/Incoming/IncomingLogicalMessageContext.cs +++ b/src/NServiceBus.Core/Pipeline/Incoming/IncomingLogicalMessageContext.cs @@ -4,7 +4,10 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using Microsoft.Extensions.DependencyInjection; +using Particular.Obsoletes; using Pipeline; class IncomingLogicalMessageContext : IncomingContext, IIncomingLogicalMessageContext @@ -28,6 +31,10 @@ public IncomingLogicalMessageContext(LogicalMessage logicalMessage, string messa public bool MessageHandled { get; set; } + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "UpdateMessageInstance(T)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public void UpdateMessageInstance(object newInstance) { ArgumentNullException.ThrowIfNull(newInstance); @@ -45,4 +52,41 @@ public void UpdateMessageInstance(object newInstance) Message.Metadata = newLogicalMessage.Metadata; } + + /// + /// Updates the message instance contained in while preserving the specified message type. + /// + /// The type used to update the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The replacement message instance. + [OverloadResolutionPriority(-1)] + public void UpdateMessageInstance<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newInstance) + => UpdateMessageInstance(newInstance!, typeof(T)); + + /// + /// Updates the message instance contained in with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The replacement message instance. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + public void UpdateMessageInstance(object newInstance, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + ArgumentNullException.ThrowIfNull(newInstance); + ArgumentNullException.ThrowIfNull(messageType); + MessageTypeValidator.Validate(newInstance, messageType); + + var sameInstance = ReferenceEquals(Message.Instance, newInstance); + + Message.Instance = newInstance; + + if (sameInstance && Message.Metadata.MessageType == messageType) + { + return; + } + + var factory = Builder.GetRequiredService(); + var newLogicalMessage = factory.Create(messageType, newInstance); + + Message.Metadata = newLogicalMessage.Metadata; + } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Pipeline/Outgoing/IOutgoingLogicalMessageContext.cs b/src/NServiceBus.Core/Pipeline/Outgoing/IOutgoingLogicalMessageContext.cs index 04edfafd28f..b39e249a71e 100644 --- a/src/NServiceBus.Core/Pipeline/Outgoing/IOutgoingLogicalMessageContext.cs +++ b/src/NServiceBus.Core/Pipeline/Outgoing/IOutgoingLogicalMessageContext.cs @@ -39,10 +39,7 @@ public interface IOutgoingLogicalMessageContext : IOutgoingContext /// The type used to update the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// The replacement message instance. [OverloadResolutionPriority(-1)] - void UpdateMessage<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newInstance) - { - UpdateMessage(newInstance!, typeof(T)); - } + void UpdateMessage<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newInstance) => UpdateMessage(newInstance!, typeof(T)); /// /// Updates the message instance with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. diff --git a/src/NServiceBus.Testing.Fakes/TestableIncomingLogicalMessageContext.cs b/src/NServiceBus.Testing.Fakes/TestableIncomingLogicalMessageContext.cs index 04716b85aeb..cd9fe1f780b 100644 --- a/src/NServiceBus.Testing.Fakes/TestableIncomingLogicalMessageContext.cs +++ b/src/NServiceBus.Testing.Fakes/TestableIncomingLogicalMessageContext.cs @@ -1,6 +1,9 @@ namespace NServiceBus.Testing; +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using Pipeline; using Unicast.Messages; @@ -35,8 +38,33 @@ public TestableIncomingLogicalMessageContext(IMessageCreator messageCreator = nu /// Updates the message instance contained in . /// /// The new instance. + [RequiresUnreferencedCode(DynamicMemberTypeAccess.RuntimeTypeRoutingTrimmingMessage)] public virtual void UpdateMessageInstance(object newInstance) { Message = new LogicalMessage(new MessageMetadata(newInstance.GetType()), newInstance); } + + /// + /// Updates the message instance contained in while preserving the specified message type. + /// + /// The type used to update the message. It determines the logical message type and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The replacement message instance. + [OverloadResolutionPriority(-1)] + public virtual void UpdateMessageInstance<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newInstance) + { + UpdateMessageInstance(newInstance!, typeof(T)); + } + + /// + /// Updates the message instance contained in with the specified message type. The declared type determines the logical message type. + /// + /// The replacement message instance. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + public virtual void UpdateMessageInstance(object newInstance, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + MessageTypeValidator.Validate(newInstance, messageType); + Message = new LogicalMessage(new MessageMetadata(messageType), newInstance); + } } \ No newline at end of file From ccc94cb30cbd8cccee93e224da010fa9d3f74386 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:40:11 +0000 Subject: [PATCH 63/79] Update Particular/run-tests-action action to v1.8.0 (#7930) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 167825a7d37..4c85ee93500 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,4 +42,4 @@ jobs: path: nugets/ retention-days: 7 - name: Run tests - uses: Particular/run-tests-action@v1.7.0 + uses: Particular/run-tests-action@v1.8.0 From 3a1d278bf363aec06abe4b1352c45828f5b26f18 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 09:51:01 +0000 Subject: [PATCH 64/79] Update dependency Microsoft.NET.Test.Sdk to 18.10.0 (#7935) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- .../NServiceBus.AcceptanceTests.csproj | 2 +- .../NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj | 2 +- src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj | 2 +- .../NServiceBus.Learning.AcceptanceTests.csproj | 2 +- .../NServiceBus.PersistenceTests.csproj | 2 +- .../NServiceBus.TransportTests.csproj | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj b/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj index 3413d11e2ba..d441d6ed055 100644 --- a/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj +++ b/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj index c67a5c7a001..67f40f30a04 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj @@ -17,7 +17,7 @@ - + diff --git a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj index c581ebb7e4f..e58bcf4439a 100644 --- a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj +++ b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj b/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj index 960de1aa004..265e867a88a 100644 --- a/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj +++ b/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj b/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj index c7faf593d5f..cc52f162b9a 100644 --- a/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj +++ b/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj b/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj index dc70a6fc9d1..93ea8f449cb 100644 --- a/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj +++ b/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj @@ -10,7 +10,7 @@ - + From 70666de2f5786a3b76d5f1aaa60b1414a7e0e98a Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 21:46:56 +0000 Subject: [PATCH 65/79] Update Particular/run-tests-action action to v1.9.0 (#7938) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c85ee93500..ed68845a1ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,4 +42,4 @@ jobs: path: nugets/ retention-days: 7 - name: Run tests - uses: Particular/run-tests-action@v1.8.0 + uses: Particular/run-tests-action@v1.9.0 From 65114c9fc7afd99219604cfb01a0827132760b39 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 10 Sep 2026 07:20:03 +0200 Subject: [PATCH 66/79] Remove pi session artifact (#7939) --- ..._01a0662e-95f5-7570-adde-5ca565b482e5.html | 4332 ----------------- 1 file changed, 4332 deletions(-) delete mode 100644 pi-session-2026-09-03T07-32-04-213Z_01a0662e-95f5-7570-adde-5ca565b482e5.html diff --git a/pi-session-2026-09-03T07-32-04-213Z_01a0662e-95f5-7570-adde-5ca565b482e5.html b/pi-session-2026-09-03T07-32-04-213Z_01a0662e-95f5-7570-adde-5ca565b482e5.html deleted file mode 100644 index 3851f4dcb13..00000000000 --- a/pi-session-2026-09-03T07-32-04-213Z_01a0662e-95f5-7570-adde-5ca565b482e5.html +++ /dev/null @@ -1,4332 +0,0 @@ - - - - - - Session Export - - - - - -
- - -
-
-
-
-
- -
-
- - - - - - - - - - - - - From 4f1fe2424ae0c8c8919424457ca57c61fccfa457 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Thu, 10 Sep 2026 12:10:31 +0200 Subject: [PATCH 67/79] Say that the cancellation token fired instead of printing an infinite limit as -0.001s (#7937) When Run is handed a cancellable token the Done limit is Timeout.InfiniteTimeSpan, and the timeout message rendered that as "The maximum time limit for this test(-0.001s) has been reached". --- src/NServiceBus.AcceptanceTesting/Support/ScenarioRunner.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/NServiceBus.AcceptanceTesting/Support/ScenarioRunner.cs b/src/NServiceBus.AcceptanceTesting/Support/ScenarioRunner.cs index 5d3c147238b..60f83121897 100644 --- a/src/NServiceBus.AcceptanceTesting/Support/ScenarioRunner.cs +++ b/src/NServiceBus.AcceptanceTesting/Support/ScenarioRunner.cs @@ -119,7 +119,9 @@ async Task PerformScenarios(ComponentRunner[] runners, CancellationToken cancell internal static string GenerateTestTimedOutMessage(TimeSpan maxTime) { var sb = new StringBuilder(); - sb.AppendLine($"The maximum time limit for this test({maxTime.TotalSeconds}s) has been reached"); + sb.AppendLine(maxTime == Timeout.InfiniteTimeSpan + ? "The cancellation token passed to the test was cancelled before the test completed" + : $"The maximum time limit for this test({maxTime.TotalSeconds}s) has been reached"); sb.AppendLine("----------------------------------------------------------------------------"); return sb.ToString(); } From d2b5ed9148b703a485ded8514b86ffb9fb0efc89 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 10 Sep 2026 12:36:24 +0200 Subject: [PATCH 68/79] Bound acceptance tests by the test's CancelAfter budget instead of the fixed 90 s Done limit- #5870 (#7940) Co-authored-by: Daniel Marbach --- .../ScenarioWithContext.cs | 7 ++++++- .../Support/ScenarioRunner.cs | 20 +++++++++++++----- ...lready_cancelled_token_is_passed_to_run.cs | 20 ++++++++++++++++++ .../When_scenario_exceeds_the_test_timeout.cs | 19 +++++++++++++++++ ...est_cancellation_token_is_passed_to_run.cs | 21 +++++++++++++++++++ 5 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 src/NServiceBus.AcceptanceTests/Core/ScenarioTimeout/When_an_already_cancelled_token_is_passed_to_run.cs create mode 100644 src/NServiceBus.AcceptanceTests/Core/ScenarioTimeout/When_scenario_exceeds_the_test_timeout.cs create mode 100644 src/NServiceBus.AcceptanceTests/Core/ScenarioTimeout/When_the_test_cancellation_token_is_passed_to_run.cs diff --git a/src/NServiceBus.AcceptanceTesting/ScenarioWithContext.cs b/src/NServiceBus.AcceptanceTesting/ScenarioWithContext.cs index 9812f9ec64f..4077f606c0a 100644 --- a/src/NServiceBus.AcceptanceTesting/ScenarioWithContext.cs +++ b/src/NServiceBus.AcceptanceTesting/ScenarioWithContext.cs @@ -21,6 +21,11 @@ public class ScenarioWithContext(Action initializer) : IScen public async Task Run(RunSettings settings, CancellationToken cancellationToken = default) { + if (!cancellationToken.CanBeCanceled) + { + cancellationToken = TestContext.CurrentContext.CancellationToken; + } + var scenarioContext = new TContext(); initializer(scenarioContext); @@ -160,7 +165,7 @@ public IScenarioWithEndpointBehavior Done(Func> f } catch (OperationCanceledException e) when (combinedDoneTokenSource.Token.IsCancellationRequested) { - throw new TimeoutException(GenerateTestTimedOutMessage(maxTime), e); + throw new TimeoutException(GenerateTestTimedOutMessage(maxTime, cancellationToken), e); } }); return this; diff --git a/src/NServiceBus.AcceptanceTesting/Support/ScenarioRunner.cs b/src/NServiceBus.AcceptanceTesting/Support/ScenarioRunner.cs index 60f83121897..67c8fe8b218 100644 --- a/src/NServiceBus.AcceptanceTesting/Support/ScenarioRunner.cs +++ b/src/NServiceBus.AcceptanceTesting/Support/ScenarioRunner.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using NServiceBus.Logging; +using NUnit.Framework.Internal; public class ScenarioRunner( RunDescriptor runDescriptor, @@ -92,7 +93,7 @@ async Task PerformScenarios(ComponentRunner[] runners, CancellationToken cancell } catch (OperationCanceledException e) { - throw new TimeoutException(GenerateTestTimedOutMessage(maxTime), e); + throw new TimeoutException(GenerateTestTimedOutMessage(maxTime, cancellationToken), e); } } @@ -116,12 +117,21 @@ async Task PerformScenarios(ComponentRunner[] runners, CancellationToken cancell } } - internal static string GenerateTestTimedOutMessage(TimeSpan maxTime) + internal static string GenerateTestTimedOutMessage(TimeSpan maxTime, CancellationToken cancellationToken = default) { var sb = new StringBuilder(); - sb.AppendLine(maxTime == Timeout.InfiniteTimeSpan - ? "The cancellation token passed to the test was cancelled before the test completed" - : $"The maximum time limit for this test({maxTime.TotalSeconds}s) has been reached"); + if (cancellationToken.CanBeCanceled && cancellationToken.IsCancellationRequested) + { + var timeoutMs = TestExecutionContext.CurrentContext.TestCaseTimeout; + sb.AppendLine(timeoutMs > 0 + ? $"The scenario did not complete within the test's timeout budget of {timeoutMs} ms" + : "The scenario did not complete before the cancellation token was cancelled"); + } + else + { + sb.AppendLine($"The maximum time limit for this test({maxTime.TotalSeconds}s) has been reached"); + } + sb.AppendLine("----------------------------------------------------------------------------"); return sb.ToString(); } diff --git a/src/NServiceBus.AcceptanceTests/Core/ScenarioTimeout/When_an_already_cancelled_token_is_passed_to_run.cs b/src/NServiceBus.AcceptanceTests/Core/ScenarioTimeout/When_an_already_cancelled_token_is_passed_to_run.cs new file mode 100644 index 00000000000..8cb19255186 --- /dev/null +++ b/src/NServiceBus.AcceptanceTests/Core/ScenarioTimeout/When_an_already_cancelled_token_is_passed_to_run.cs @@ -0,0 +1,20 @@ +namespace NServiceBus.AcceptanceTests.Core.ScenarioTimeout; + +using System; +using System.Threading; +using NServiceBus.AcceptanceTesting; +using NUnit.Framework; + +public class When_an_already_cancelled_token_is_passed_to_run : NServiceBusAcceptanceTest +{ + [Test] + public void Should_fail_with_a_timeout() + { + var exception = Assert.ThrowsAsync(() => Scenario.Define() + .Run(new CancellationToken(canceled: true))); + + Assert.That(exception.Message, Does.Contain("The scenario did not complete before the cancellation token was cancelled")); + } + + public class Context : ScenarioContext; +} diff --git a/src/NServiceBus.AcceptanceTests/Core/ScenarioTimeout/When_scenario_exceeds_the_test_timeout.cs b/src/NServiceBus.AcceptanceTests/Core/ScenarioTimeout/When_scenario_exceeds_the_test_timeout.cs new file mode 100644 index 00000000000..7c95de35701 --- /dev/null +++ b/src/NServiceBus.AcceptanceTests/Core/ScenarioTimeout/When_scenario_exceeds_the_test_timeout.cs @@ -0,0 +1,19 @@ +namespace NServiceBus.AcceptanceTests.Core.ScenarioTimeout; + +using System; +using NServiceBus.AcceptanceTesting; +using NUnit.Framework; + +public class When_scenario_exceeds_the_test_timeout : NServiceBusAcceptanceTest +{ + [Test, CancelAfter(250)] + public void Should_fail_with_the_test_timeout_budget() + { + var exception = Assert.ThrowsAsync(() => Scenario.Define() + .Run()); + + Assert.That(exception.Message, Does.Contain("timeout budget of 250 ms")); + } + + public class Context : ScenarioContext; +} diff --git a/src/NServiceBus.AcceptanceTests/Core/ScenarioTimeout/When_the_test_cancellation_token_is_passed_to_run.cs b/src/NServiceBus.AcceptanceTests/Core/ScenarioTimeout/When_the_test_cancellation_token_is_passed_to_run.cs new file mode 100644 index 00000000000..a5052f2044a --- /dev/null +++ b/src/NServiceBus.AcceptanceTests/Core/ScenarioTimeout/When_the_test_cancellation_token_is_passed_to_run.cs @@ -0,0 +1,21 @@ +namespace NServiceBus.AcceptanceTests.Core.ScenarioTimeout; + +using System; +using System.Threading; +using NServiceBus.AcceptanceTesting; +using NUnit.Framework; + +[CancelAfter(5_000)] +public class When_the_test_cancellation_token_is_passed_to_run : NServiceBusAcceptanceTest +{ + [Test, CancelAfter(250)] + public void Should_use_the_passed_token(CancellationToken cancellationToken = default) + { + var exception = Assert.ThrowsAsync(() => Scenario.Define() + .Run(cancellationToken)); + + Assert.That(exception.Message, Does.Contain("timeout budget of 250 ms")); + } + + public class Context : ScenarioContext; +} From 65ebb29f34d8268ea154835f93319e097ae5d549 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Fri, 11 Sep 2026 07:02:40 +0200 Subject: [PATCH 69/79] Support for trimming and AOT (#7929) * Update must reset the ReplacementType * Effective dynamic loading diagnostics * Add mutator coverage * Simplify registration to facilitate source gen registration under the hood * All public APIs that use those sources are properly annotated so we can suppress it unconditionally. * Enable trimming analyzers for now * Mark as trimmable * 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. * Remove unnecessary make generic type * Refactor JSON serialization logic and update suppression attributes for trimming * Suppress FileVersionRetriever because the code handles graceful fallback * MessageHandlerRegistry path invokes generic methods and therefore also requires dynamic code. The source generated path doesn't require that so suppress * KeyedServiceProviderAdapter suppressions * Grammar fix * Better explanation (hopefully) * Reword comment on provider adapter * IsAotCompatible * Fix approvals --------- Co-authored-by: Daniel Marbach --- .../AddHandlerInterceptorSuppressorTests.cs | 67 +++++++++ .../Helpers/MockTrimmingAnalyzer.cs | 25 +++- .../AddSagaInterceptorSuppressorTests.cs | 136 ++++++++++++++++++ .../AddHandlerInterceptor.Suppressor.cs | 16 ++- .../Sagas/AddSagaInterceptor.Suppressor.cs | 18 ++- .../SupressionIds.cs | 2 + .../API/TrimmabilityWarnings.cs | 130 ----------------- ...IApprovals.ApproveNServiceBus.approved.txt | 25 +++- ...s.ApproveTrimmabilityWarnings.approved.txt | 13 -- .../MutateIncomingMessageBehaviorTests.cs | 19 +++ .../MutateOutgoingMessageBehaviorTests.cs | 19 +++ .../TrimmedEndpointTests.cs | 4 +- src/NServiceBus.Core/EndpointConfiguration.cs | 2 +- src/NServiceBus.Core/EndpointCreator.cs | 17 ++- .../KeyedServiceProviderAdapter.cs | 10 ++ .../HostStartupDiagnosticsWriter.cs | 25 ++-- .../MutateIncomingMessageContext.cs | 2 + .../MutateOutgoingMessageContext.cs | 2 + src/NServiceBus.Core/NServiceBus.Core.csproj | 1 + .../Routing/AssemblyRouteSource.cs | 6 +- .../AssemblyPublisherSource.cs | 9 +- .../NamespacePublisherSource.cs | 10 +- .../Routing/NamespaceRouteSource.cs | 6 +- src/NServiceBus.Core/Sagas/SagaComponent.cs | 1 + src/NServiceBus.Core/Sagas/SagaMetadata.cs | 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 + .../MessageHandlerRegistrationExtensions.cs | 1 + .../Config/SagaRegistrationExtensions.cs | 1 + .../Unicast/MessageHandlerRegistry.cs | 17 ++- .../Utils/FileVersionRetriever.cs | 2 + .../Utils/Reflection/MethodInfoExtensions.cs | 6 + .../Utils/Reflection/TypeExtensionMethods.cs | 7 +- src/TrimmedEndpoint/Program.cs | 52 ++++++- 39 files changed, 461 insertions(+), 242 deletions(-) delete mode 100644 src/NServiceBus.Core.Tests/API/TrimmabilityWarnings.cs delete mode 100644 src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt rename src/NServiceBus.Core/{Utils/Reflection => Serializers/XML}/DelegateFactory.cs (98%) 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; } } From 9106cc6a59c03a32a1f95a22d62ddae70ad63329 Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Sat, 12 Sep 2026 11:03:40 -0500 Subject: [PATCH 70/79] Update dependency NUnit.Analyzers to 4.15.0 (#7941) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- .../NServiceBus.AcceptanceTesting.csproj | 2 +- .../NServiceBus.AcceptanceTests.csproj | 2 +- .../NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj | 2 +- src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj | 2 +- .../NServiceBus.Learning.AcceptanceTests.csproj | 2 +- .../NServiceBus.PersistenceTests.csproj | 2 +- .../NServiceBus.TransportTests.csproj | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/NServiceBus.AcceptanceTesting/NServiceBus.AcceptanceTesting.csproj b/src/NServiceBus.AcceptanceTesting/NServiceBus.AcceptanceTesting.csproj index 981014fc674..c15b5bd35de 100644 --- a/src/NServiceBus.AcceptanceTesting/NServiceBus.AcceptanceTesting.csproj +++ b/src/NServiceBus.AcceptanceTesting/NServiceBus.AcceptanceTesting.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj b/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj index d441d6ed055..bd0589ac424 100644 --- a/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj +++ b/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj index 67f40f30a04..d3b432bbaf2 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj index e58bcf4439a..5e269ba36ee 100644 --- a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj +++ b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj @@ -18,7 +18,7 @@ - + diff --git a/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj b/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj index 265e867a88a..fce77bcfcbe 100644 --- a/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj +++ b/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj b/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj index cc52f162b9a..c87b6d00f0a 100644 --- a/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj +++ b/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj b/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj index 93ea8f449cb..d9789f2f087 100644 --- a/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj +++ b/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj @@ -12,7 +12,7 @@ - + From d060eafc6a97bf181a2425b1905afc59c03fc22c Mon Sep 17 00:00:00 2001 From: "dependencyupdates[bot]" <218638057+dependencyupdates[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 17:11:53 +0000 Subject: [PATCH 71/79] Update dependency Microsoft.NET.Test.Sdk to 18.10.1 (#7942) Co-authored-by: dependencyupdates[bot] <218638057+dependencyupdates[bot]@users.noreply.github.com> --- .../NServiceBus.AcceptanceTests.csproj | 2 +- .../NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj | 2 +- src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj | 2 +- .../NServiceBus.Learning.AcceptanceTests.csproj | 2 +- .../NServiceBus.PersistenceTests.csproj | 2 +- .../NServiceBus.TransportTests.csproj | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj b/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj index bd0589ac424..a4802fd05e8 100644 --- a/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj +++ b/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj index d3b432bbaf2..c904edc451d 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj @@ -17,7 +17,7 @@ - + diff --git a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj index 5e269ba36ee..3815ed7e74f 100644 --- a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj +++ b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj b/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj index fce77bcfcbe..7879fbc1f04 100644 --- a/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj +++ b/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj b/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj index c87b6d00f0a..3084b595cd6 100644 --- a/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj +++ b/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj b/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj index d9789f2f087..a729570decd 100644 --- a/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj +++ b/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj @@ -10,7 +10,7 @@ - + From fb957825b101f1b269705d0a2a134ac25a4e114f Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Thu, 17 Sep 2026 02:32:34 +1000 Subject: [PATCH 72/79] Use nameof instead of Enum.ToString for the message intent header (#7944) Calling ToString() on an enum value boxes it, so every outgoing message allocated 24 bytes and paid roughly 12ns to produce a string that is known at compile time. nameof yields the identical string for free. MessageIntent.Send.ToString() 24.0 B/op 11.97 ns/op nameof(MessageIntent.Send) 0.0 B/op 1.45 ns/op ReplyConnector already did this; this brings the other four sites into line. The enum has no attributes that affect ToString and no duplicate values, so the header value on the wire is unchanged: both forms produce "Send" and "Publish". This is a small change in proportion - 24 bytes against roughly 5.4 KB for a full outgoing pipeline run, about 0.4% - so it is worth doing for being free and consistent rather than for moving the needle. --- .../Routing/Routers/MulticastPublishConnector.cs | 2 +- src/NServiceBus.Core/Routing/Routers/SendConnector.cs | 2 +- src/NServiceBus.Core/Routing/Routers/UnicastPublishConnector.cs | 2 +- .../SubscriptionMigrationMode/MigrationModePublishConnector.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/NServiceBus.Core/Routing/Routers/MulticastPublishConnector.cs b/src/NServiceBus.Core/Routing/Routers/MulticastPublishConnector.cs index 1931281364b..c95bf1e6295 100644 --- a/src/NServiceBus.Core/Routing/Routers/MulticastPublishConnector.cs +++ b/src/NServiceBus.Core/Routing/Routers/MulticastPublishConnector.cs @@ -9,7 +9,7 @@ class MulticastPublishConnector : StageConnector stage) { - context.Headers[Headers.MessageIntent] = MessageIntent.Publish.ToString(); + context.Headers[Headers.MessageIntent] = nameof(MessageIntent.Publish); var logicalMessageContext = this.CreateOutgoingLogicalMessageContext( context.Message, diff --git a/src/NServiceBus.Core/Routing/Routers/SendConnector.cs b/src/NServiceBus.Core/Routing/Routers/SendConnector.cs index fc913a85fa2..415755ee615 100644 --- a/src/NServiceBus.Core/Routing/Routers/SendConnector.cs +++ b/src/NServiceBus.Core/Routing/Routers/SendConnector.cs @@ -15,7 +15,7 @@ public SendConnector(UnicastSendRouter unicastSendRouter) public override async Task Invoke(IOutgoingSendContext context, Func stage) { var routingStrategy = unicastSendRouter.Route(context); - context.Headers[Headers.MessageIntent] = MessageIntent.Send.ToString(); + context.Headers[Headers.MessageIntent] = nameof(MessageIntent.Send); var logicalMessageContext = this.CreateOutgoingLogicalMessageContext(context.Message, new[] { routingStrategy }, context); try diff --git a/src/NServiceBus.Core/Routing/Routers/UnicastPublishConnector.cs b/src/NServiceBus.Core/Routing/Routers/UnicastPublishConnector.cs index 9b69eb49e73..9e854b3beee 100644 --- a/src/NServiceBus.Core/Routing/Routers/UnicastPublishConnector.cs +++ b/src/NServiceBus.Core/Routing/Routers/UnicastPublishConnector.cs @@ -26,7 +26,7 @@ public override async Task Invoke(IOutgoingPublishContext context, Func stage) { - context.Headers[Headers.MessageIntent] = MessageIntent.Publish.ToString(); + context.Headers[Headers.MessageIntent] = nameof(MessageIntent.Publish); var eventType = context.Message.MessageType; var addressLabels = await GetRoutingStrategies(context, eventType).ConfigureAwait(false); From 8c087385fadc1c142ed8065ee7114ce3d4ab25bb Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Thu, 17 Sep 2026 06:55:33 +1000 Subject: [PATCH 73/79] Format the wire date by hand instead of via a custom format string (#7945) ToWireFormattedString went through ToString("yyyy-MM-dd HH:mm:ss:ffffff Z"). A custom format string is reparsed on every call - DateTimeFormat.FormatCustomized walks it character by character calling ParseRepeatPattern - and that parsing dominates the cost. The layout here is fixed and ASCII, so the digits can be written straight into the string buffer instead. This follows what the framework does for its own standard date formats. DateTimeFormat .TryFormatO writes into fixed offsets of the destination span and takes each two digit field from a lookup table rather than dividing per digit; the same two techniques are used here. ToString(format) 80.0 B/op 111.06 ns/op string.Create 80.0 B/op 27.37 ns/op Roughly four times faster. The 80 bytes is the resulting string itself and is unchanged, so this is a CPU saving rather than an allocation one. For reference, an earlier revision of this change divided per digit instead of using the lookup table and measured 30.95 ns/op, so the table is worth about 12% on top - a small margin, kept because it matches how the framework formats dates and costs only a static table. ToDateTimeOffset directly below already parses by hand for the same reason, so this makes the two directions consistent. Verified byte identical to the previous implementation over 3,000,012 values: a random sweep of the whole representable tick range crossed with offsets from -14:00 to +14:00, plus DateTimeOffset.MinValue and MaxValue, the Unix epoch, a leap day, an offset that rolls the date across a boundary, a three digit year, and sub-microsecond ticks either side of the truncation boundary. Zero mismatches. Also round tripped 200,000 values back through ToDateTimeOffset with zero failures. This runs at least once per outgoing message for the TimeSent header, and again for DeliverAt on delayed sends. --- src/NServiceBus.Core/DateTimeOffsetHelper.cs | 52 +++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/src/NServiceBus.Core/DateTimeOffsetHelper.cs b/src/NServiceBus.Core/DateTimeOffsetHelper.cs index 9152aa795b3..3dcfee01b6a 100644 --- a/src/NServiceBus.Core/DateTimeOffsetHelper.cs +++ b/src/NServiceBus.Core/DateTimeOffsetHelper.cs @@ -1,7 +1,6 @@ namespace NServiceBus; using System; -using System.Globalization; /// /// Common date time extensions. @@ -13,9 +12,57 @@ public static class DateTimeOffsetHelper /// public static string ToWireFormattedString(DateTimeOffset dateTime) { - return dateTime.ToUniversalTime().ToString(format, CultureInfo.InvariantCulture); + // Formatted by hand rather than via ToString(format): a custom format string is reparsed on every + // call, and that parsing dominates the cost. The layout here is fixed and ASCII, so the digits can be + // written straight into the string buffer instead. This mirrors how the framework formats its own + // standard date formats (see DateTimeFormat.TryFormatO), including writing each two digit field from + // a lookup table rather than dividing per digit. ToDateTimeOffset below already parses by hand for + // the same reason. + return string.Create(FormatLength, dateTime.ToUniversalTime(), static (destination, utc) => + { + var (yearHigh, yearLow) = Math.DivRem((uint)utc.Year, 100); + WriteTwoDigits(destination[..2], yearHigh); + WriteTwoDigits(destination.Slice(2, 2), yearLow); + destination[4] = '-'; + WriteTwoDigits(destination.Slice(5, 2), (uint)utc.Month); + destination[7] = '-'; + WriteTwoDigits(destination.Slice(8, 2), (uint)utc.Day); + destination[10] = ' '; + WriteTwoDigits(destination.Slice(11, 2), (uint)utc.Hour); + destination[13] = ':'; + WriteTwoDigits(destination.Slice(14, 2), (uint)utc.Minute); + destination[16] = ':'; + WriteTwoDigits(destination.Slice(17, 2), (uint)utc.Second); + destination[19] = ':'; + + var microseconds = (uint)(utc.Ticks % TimeSpan.TicksPerSecond / 10); + var (microHigh, microRest) = Math.DivRem(microseconds, 10000); + var (microMid, microLow) = Math.DivRem(microRest, 100); + WriteTwoDigits(destination.Slice(20, 2), microHigh); + WriteTwoDigits(destination.Slice(22, 2), microMid); + WriteTwoDigits(destination.Slice(24, 2), microLow); + + destination[26] = ' '; + destination[27] = 'Z'; + }); + + // Both characters come from the table in one copy, which the JIT turns into a single 4 byte move. + static void WriteTwoDigits(Span destination, uint value) => TwoDigits.Slice((int)value * 2, 2).CopyTo(destination); } + // Every two digit value from "00" to "99", indexed by value * 2. Spanning a literal allocates nothing. + static ReadOnlySpan TwoDigits => + "00010203040506070809" + + "10111213141516171819" + + "20212223242526272829" + + "30313233343536373839" + + "40414243444546474849" + + "50515253545556575859" + + "60616263646566676869" + + "70717273747576777879" + + "80818283848586878889" + + "90919293949596979899"; + /// /// Converts a wire formatted from to a UTC /// . @@ -117,5 +164,6 @@ public static DateTimeOffset ToDateTimeOffset(string wireFormattedString) } const string format = "yyyy-MM-dd HH:mm:ss:ffffff Z"; + const int FormatLength = 28; // format.Length, as a constant usable by string.Create const string errorMessage = "String was not recognized as a valid DateTime."; } \ No newline at end of file From b078555881e7de9566f037c83c94736908679c94 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Fri, 18 Sep 2026 16:01:03 +1000 Subject: [PATCH 74/79] Skip outbox work in the receive pipeline when no outbox is configured (#7943) * Skip outbox work in the receive pipeline when no outbox is configured With no outbox configured the storage is a no-op, yet the receive connector still did all the outbox work on every message: it built an OutboxMessage and a TransportOperation[] for NoOpOutboxStorage.Store to discard, opened and committed a no-op transaction, and parked a placeholder NoOpOutboxTransaction in the context purely so the storage session could read it back. It also snapshotted PendingTransportOperations.Operations twice, and each read copies a ConcurrentStack. CompletableSynchronizedStorageSessionExtensions.Open now substitutes the no-op transaction when none is present, rather than requiring one via a hard Get(). Persisters already received exactly that instance when no outbox was configured, so the input to their TryOpen is unchanged. That frees the connector to skip the transaction entirely on the no-outbox path. Invoke is now a non-async dispatcher over InvokeWithOutbox and InvokeWithoutOutbox, so it has no state machine of its own and exactly one is boxed per message, sized for the path actually taken. Without an outbox, Get always returns null and SetAsDispatched does nothing, so the deduplication branch and both calls are unreachable and are gone from that path. The boxed state machine drops from 200 B to 104 B per message; the outbox path stays at 208 B. Measured on TransportReceiveToPhysicalMessageConnector.Invoke driving one incoming message, against master, back to back on the same machine (net10.0, i5-12600, BenchmarkDotNet ShortRun). Allocation is deterministic and reproduces exactly; the timings carry a few percent of run-to-run variance. outgoing outbox master this change 0 off 223.0 ns / 1.35 KB 204.6 ns / 1.30 KB 1 off 386.9 ns / 2.19 KB 322.4 ns / 1.95 KB 2 off 428.9 ns / 2.61 KB 344.4 ns / 2.30 KB 0 on 234.3 ns / 1.45 KB 228.1 ns / 1.45 KB 1 on 368.3 ns / 2.28 KB 356.0 ns / 2.16 KB 2 on 434.2 ns / 2.70 KB 410.3 ns / 2.58 KB The absolute figures include the harness's own per-iteration allocations, so only the deltas are meaningful. No configuration regresses; the outbox path also gains from the single Operations snapshot. Note that IOutboxTransaction is no longer present in the context when the outbox is disabled. Code reading it directly via Get() would now throw rather than receive the no-op instance. * Extract shared dispatch tail into single DispatchOperations helper Co-authored-by: danielmarbach <174258+danielmarbach@users.noreply.github.com> * Avoid extra state machine: share only synchronous dispatch instrumentation Co-authored-by: danielmarbach <174258+danielmarbach@users.noreply.github.com> * Use target type where it is obvious * Stop unnecessary wrapping * Switch to built-in logger --------- Co-authored-by: Daniel Marbach Co-authored-by: danielmarbach <174258+danielmarbach@users.noreply.github.com> --- ...rUsage.ApproveLogManagerUsage.approved.txt | 1 - ...tReceiveToPhysicalMessageConnectorTests.cs | 60 ++++++++- ...nsportReceiveToPhysicalMessageConnector.cs | 115 +++++++++++++----- .../Receiving/ReceiveComponent.cs | 10 +- .../Reliability/Outbox/NoOpOutboxStorage.cs | 2 +- .../Outbox/NoOpOutboxTransaction.cs | 3 + ...bleSynchronizedStorageSessionExtensions.cs | 10 +- 7 files changed, 162 insertions(+), 39 deletions(-) diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/LogManagerUsage.ApproveLogManagerUsage.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/LogManagerUsage.ApproveLogManagerUsage.approved.txt index d7a9399dad4..60420d9a588 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/LogManagerUsage.ApproveLogManagerUsage.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/LogManagerUsage.ApproveLogManagerUsage.approved.txt @@ -45,7 +45,6 @@ NServiceBus.SerializationFeature NServiceBus.SerializationSettingsExtensions NServiceBus.SerializeMessageConnector NServiceBus.SubscriptionReceiverBehavior -NServiceBus.TransportReceiveToPhysicalMessageConnector NServiceBus.Unicast.Messages.MessageMetadataRegistry NServiceBus.UnicastPublishRouter NServiceBus.Validations diff --git a/src/NServiceBus.Core.Tests/Reliability/Outbox/TransportReceiveToPhysicalMessageConnectorTests.cs b/src/NServiceBus.Core.Tests/Reliability/Outbox/TransportReceiveToPhysicalMessageConnectorTests.cs index 2e577b4c301..2f976ba4ac1 100644 --- a/src/NServiceBus.Core.Tests/Reliability/Outbox/TransportReceiveToPhysicalMessageConnectorTests.cs +++ b/src/NServiceBus.Core.Tests/Reliability/Outbox/TransportReceiveToPhysicalMessageConnectorTests.cs @@ -8,6 +8,7 @@ namespace NServiceBus.Core.Tests.Reliability.Outbox; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using Microsoft.Extensions.Logging.Abstractions; using NServiceBus.Outbox; using NServiceBus.Pipeline; using NServiceBus.Routing; @@ -174,6 +175,63 @@ public async Task Should_not_add_batch_dispatch_events_when_no_batched_messages( } } + [Test] + public async Task Should_store_outbox_message_when_outbox_is_enabled() + { + var context = CreateContext(fakeBatchPipeline, "id"); + + await Invoke(context, c => + { + c.Extensions.Get().Add( + new TransportOperation(new OutgoingMessage("out-1", [], Array.Empty()), new UnicastAddressTag("destination"))); + return Task.CompletedTask; + }); + + Assert.That(fakeOutbox.StoredMessage, Is.Not.Null); + var stored = fakeOutbox.StoredMessage!; + using (Assert.EnterMultipleScope()) + { + Assert.That(stored.MessageId, Is.EqualTo("id")); + Assert.That(stored.TransportOperations, Has.Length.EqualTo(1)); + Assert.That(stored.TransportOperations[0].Options!["Destination"], Is.EqualTo("destination")); + } + } + + [Test] + public async Task Should_still_dispatch_when_outbox_is_disabled() + { + var noOpBehavior = new TransportReceiveToPhysicalMessageConnector( + new NoOpOutboxStorage(), new IncomingPipelineMetrics(new TestMeterFactory(), "queue", "disc"), NullLogger.Instance); + + var context = CreateContext(fakeBatchPipeline, "id"); + + await noOpBehavior.Invoke(context, c => + { + c.Extensions.Get().AddRange([ + new TransportOperation(new OutgoingMessage("out-1", [], Array.Empty()), new UnicastAddressTag("destination")), + new TransportOperation(new OutgoingMessage("out-2", [], Array.Empty()), new MulticastAddressTag(typeof(MyEvent))) + ]); + return Task.CompletedTask; + }); + + Assert.That(fakeBatchPipeline.TransportOperations, Is.Not.Null); + var dispatched = fakeBatchPipeline.TransportOperations!.ToArray(); + Assert.That(dispatched, Has.Length.EqualTo(2)); + + var unicast = dispatched.Single(o => o.Message.MessageId == "out-1"); + var multicast = dispatched.Single(o => o.Message.MessageId == "out-2"); + using (Assert.EnterMultipleScope()) + { + Assert.That(((UnicastAddressTag)unicast.AddressTag).Destination, Is.EqualTo("destination")); + Assert.That(((MulticastAddressTag)multicast.AddressTag).MessageType, Is.EqualTo(typeof(MyEvent))); + + // With no outbox there is nothing to round-trip the address tag through, so the operations are + // dispatched without the routing strategy being serialized into their dispatch properties. + Assert.That(unicast.Properties.ContainsKey("Destination"), Is.False); + Assert.That(multicast.Properties.ContainsKey("EventType"), Is.False); + } + } + static TestableTransportReceiveContext CreateContext(FakeBatchPipeline pipeline, string messageId) { var context = new TestableTransportReceiveContext @@ -192,7 +250,7 @@ public void SetUp() fakeOutbox = new FakeOutboxStorage(); fakeBatchPipeline = new FakeBatchPipeline(); - behavior = new TransportReceiveToPhysicalMessageConnector(fakeOutbox, new IncomingPipelineMetrics(new TestMeterFactory(), "queue", "disc")); + behavior = new TransportReceiveToPhysicalMessageConnector(fakeOutbox, new IncomingPipelineMetrics(new TestMeterFactory(), "queue", "disc"), NullLogger.Instance); } Task Invoke(ITransportReceiveContext context, Func? next = null) => behavior.Invoke(context, next ?? (_ => Task.CompletedTask)); diff --git a/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs b/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs index 1256659e15a..496eddab70d 100644 --- a/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs +++ b/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs @@ -6,19 +6,30 @@ namespace NServiceBus; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; -using Logging; +using Microsoft.Extensions.Logging; using Outbox; using Pipeline; using Routing; using Transport; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; using TransportOperation = Outbox.TransportOperation; -class TransportReceiveToPhysicalMessageConnector( +partial class TransportReceiveToPhysicalMessageConnector( IOutboxStorage outboxStorage, - IncomingPipelineMetrics incomingPipelineMetrics) + IncomingPipelineMetrics incomingPipelineMetrics, + ILogger logger) : IStageForkConnector { - public async Task Invoke(ITransportReceiveContext context, Func next) + // When no outbox is configured the storage is a no-op that discards whatever it is handed, so building an + // OutboxMessage for it is pure waste on every message that dispatches anything. + readonly bool outboxEnabled = outboxStorage is not NoOpOutboxStorage; + + // Invoke is deliberately not async: it has no state machine of its own, so exactly one is boxed per + // message, sized for the path actually taken. + public Task Invoke(ITransportReceiveContext context, Func next) + => outboxEnabled ? InvokeWithOutbox(context, next) : InvokeWithoutOutbox(context, next); + + async Task InvokeWithOutbox(ITransportReceiveContext context, Func next) { var processingStartedAt = Stopwatch.GetTimestamp(); var messageId = context.Message.MessageId; @@ -26,6 +37,8 @@ public async Task Invoke(ITransportReceiveContext context, Func(); @@ -55,61 +70,98 @@ public async Task Invoke(ITransportReceiveContext context, Func 0) { - var batchDispatchContext = this.CreateBatchDispatchContext(pendingTransportOperations.Operations, physicalMessageContext); - - if (context.Extensions.TryGetRecordingIncomingPipelineActivity(out var activity)) - { - var activityTagsCollection = new ActivityTagsCollection { { "message-count", batchDispatchContext.Operations.Count } }; - activity.AddEvent(new ActivityEvent("Start dispatching", tags: activityTagsCollection)); - } - + var batchDispatchContext = this.CreateBatchDispatchContext(operations, physicalMessageContext); + var dispatchActivity = WriteStartDispatchingEvent(physicalMessageContext, operations.Length); await this.Fork(batchDispatchContext).ConfigureAwait(false); - activity?.AddEvent(new ActivityEvent("Finished dispatching")); + dispatchActivity?.AddEvent(new("Finished dispatching")); } await outboxStorage.SetAsDispatched(messageId, context.Extensions, context.CancellationToken).ConfigureAwait(false); - if (pendingTransportOperations.HasOperations || deduplicationEntry == null) + if (operations.Length > 0 || deduplicationEntry == null) { incomingPipelineMetrics.RecordCriticalTimeAndTotalProcessed(context); } } + async Task InvokeWithoutOutbox(ITransportReceiveContext context, Func next) + { + // Without an outbox there is nothing to deduplicate against: NoOpOutboxStorage.Get always returns null + // and SetAsDispatched does nothing, so only the fresh-processing path is reachable and neither call is + // made. No outbox transaction is parked in the context either; the storage session substitutes the + // no-op transaction when none is present. + var processingStartedAt = Stopwatch.GetTimestamp(); + var physicalMessageContext = this.CreateIncomingPhysicalMessageContext(context.Message, context); + + var pendingTransportOperations = new PendingTransportOperations(); + physicalMessageContext.Extensions.Set(pendingTransportOperations); + + await next(physicalMessageContext).ConfigureAwait(false); + + // Materialized once: PendingTransportOperations.Operations snapshots a ConcurrentStack on every access. + var operations = pendingTransportOperations.Operations; + + var elapsedTime = Stopwatch.GetElapsedTime(processingStartedAt); + incomingPipelineMetrics.RecordProcessingTime(context, elapsedTime); + + physicalMessageContext.Extensions.Remove(); + + if (operations.Length > 0) + { + var batchDispatchContext = this.CreateBatchDispatchContext(operations, physicalMessageContext); + var dispatchActivity = WriteStartDispatchingEvent(physicalMessageContext, operations.Length); + await this.Fork(batchDispatchContext).ConfigureAwait(false); + dispatchActivity?.AddEvent(new("Finished dispatching")); + } + + incomingPipelineMetrics.RecordCriticalTimeAndTotalProcessed(context); + } + + // Synchronous by design so the dispatch instrumentation lives in one place without adding an async state + // machine to either path; the Fork await stays in the callers. The activity is resolved from the physical + // message context; child context bags read through to their parent, so this sees the same activity as the + // root receive context. Returns the activity (or null) so the caller can emit the finished event after the fork. + static Activity? WriteStartDispatchingEvent(IIncomingPhysicalMessageContext physicalMessageContext, int operationCount) + { + if (!physicalMessageContext.Extensions.TryGetRecordingIncomingPipelineActivity(out var activity)) + { + return null; + } + + activity.AddEvent(new("Start dispatching", tags: new() { { "message-count", operationCount } })); + return activity; + } + static void ConvertToPendingOperations(OutboxMessage deduplicationEntry, PendingTransportOperations pendingTransportOperations) { foreach (var operation in deduplicationEntry.TransportOperations) { var message = new OutgoingMessage(operation.MessageId, operation.Headers, operation.Body); - pendingTransportOperations.Add( - new Transport.TransportOperation( - message, - DeserializeRoutingStrategy(operation.Options), - operation.Options, - DispatchConsistency.Isolated - )); + pendingTransportOperations.Add(new(message, DeserializeRoutingStrategy(operation.Options), operation.Options, DispatchConsistency.Isolated)); } } static TransportOperation[] ConvertToOutboxOperations(Transport.TransportOperation[] operations) { var transportOperations = new TransportOperation[operations.Length]; - var index = 0; - foreach (var operation in operations) + for (int index = 0; index < operations.Length; index++) { + var operation = operations[index]; SerializeRoutingStrategy(operation.AddressTag, operation.Properties); - transportOperations[index] = new TransportOperation(operation.Message.MessageId, operation.Properties, operation.Message.Body, operation.Message.Headers); - index++; + transportOperations[index] = new(operation.Message.MessageId, operation.Properties, operation.Message.Body, operation.Message.Headers); } + return transportOperations; } @@ -146,5 +198,6 @@ static AddressTag DeserializeRoutingStrategy(DispatchProperties? options) throw new Exception("Could not find routing strategy to deserialize"); } - static readonly ILog Log = LogManager.GetLogger(); + [LoggerMessage(LogLevel.Information, "Outbox duplicate detected for message '{MessageId}'. Skipping handler execution")] + partial void LogOutboxDuplicateDetectedForMessageMessageIdSkippingHandlerExecution(string messageId); } diff --git a/src/NServiceBus.Core/Receiving/ReceiveComponent.cs b/src/NServiceBus.Core/Receiving/ReceiveComponent.cs index 044664b4949..196f610a9ec 100644 --- a/src/NServiceBus.Core/Receiving/ReceiveComponent.cs +++ b/src/NServiceBus.Core/Receiving/ReceiveComponent.cs @@ -9,11 +9,13 @@ namespace NServiceBus; using System.Threading.Tasks; using Logging; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using MicrosoftLoggerFactory = Microsoft.Extensions.Logging.ILoggerFactory; using Outbox; using Pipeline; using Transport; using Unicast; +using ILoggerFactory = Logging.ILoggerFactory; partial class ReceiveComponent { @@ -64,15 +66,15 @@ public static ReceiveComponent Configure( return transport.Receivers[MainReceiverId].Subscriptions; }); - pipelineSettings.Register("TransportReceiveToPhysicalMessageProcessingConnector", b => + pipelineSettings.Register("TransportReceiveToPhysicalMessageProcessingConnector", static b => { var storage = b.GetService() ?? new NoOpOutboxStorage(); - return new TransportReceiveToPhysicalMessageConnector(storage, b.GetRequiredService()); + return new TransportReceiveToPhysicalMessageConnector(storage, b.GetRequiredService(), b.GetRequiredService>()); }, "Allows to abort processing the message"); - pipelineSettings.Register("LoadHandlersConnector", b => new LoadHandlersConnector(b.GetRequiredService(), hostingConfiguration.ActivityFactory), "Gets all the handlers to invoke from the MessageHandler registry based on the message type."); + pipelineSettings.Register("LoadHandlersConnector", sp => new LoadHandlersConnector(sp.GetRequiredService(), hostingConfiguration.ActivityFactory), "Gets all the handlers to invoke from the MessageHandler registry based on the message type."); - pipelineSettings.Register("InvokeHandlers", sp => new InvokeHandlerTerminator(sp.GetRequiredService()), "Calls the IHandleMessages.Handle(T)"); + pipelineSettings.Register("InvokeHandlers", static sp => new InvokeHandlerTerminator(sp.GetRequiredService()), "Calls the IHandleMessages.Handle(T)"); var handlerDiagnostics = new Dictionary>(); diff --git a/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxStorage.cs b/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxStorage.cs index 554ececc44e..ed5c6288979 100644 --- a/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxStorage.cs +++ b/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxStorage.cs @@ -18,5 +18,5 @@ class NoOpOutboxStorage : IOutboxStorage public Task BeginTransaction(ContextBag context, CancellationToken cancellationToken = default) => NoOutboxTransactionTask; static readonly Task NoOutboxMessageTask = Task.FromResult(null); - static readonly Task NoOutboxTransactionTask = Task.FromResult(new NoOpOutboxTransaction()); + static readonly Task NoOutboxTransactionTask = Task.FromResult(NoOpOutboxTransaction.Instance); } \ No newline at end of file diff --git a/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxTransaction.cs b/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxTransaction.cs index 872827db1db..d88103e3fa1 100644 --- a/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxTransaction.cs +++ b/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxTransaction.cs @@ -8,6 +8,9 @@ namespace NServiceBus; sealed class NoOpOutboxTransaction : IOutboxTransaction { + // Stateless, so a single instance can be shared by every message. + public static readonly NoOpOutboxTransaction Instance = new(); + public void Dispose() { } diff --git a/src/NServiceBus.Core/Reliability/SynchronizedStorage/CompletableSynchronizedStorageSessionExtensions.cs b/src/NServiceBus.Core/Reliability/SynchronizedStorage/CompletableSynchronizedStorageSessionExtensions.cs index cf8e139a931..954717d6957 100644 --- a/src/NServiceBus.Core/Reliability/SynchronizedStorage/CompletableSynchronizedStorageSessionExtensions.cs +++ b/src/NServiceBus.Core/Reliability/SynchronizedStorage/CompletableSynchronizedStorageSessionExtensions.cs @@ -2,6 +2,7 @@ namespace NServiceBus.Persistence; +using NServiceBus; using System.Threading; using System.Threading.Tasks; using Extensibility; @@ -23,7 +24,14 @@ public static class CompletableSynchronizedStorageSessionExtensions /// The context information. public static ValueTask Open(this ICompletableSynchronizedStorageSession session, IIncomingLogicalMessageContext context) { - var outboxTransaction = context.Extensions.Get(); + // An outbox transaction is only in the context when an outbox is configured. Substituting the no-op + // transaction keeps this contract non-nullable without the receive pipeline having to park a + // placeholder in the context on every message. + if (!context.Extensions.TryGet(out var outboxTransaction)) + { + outboxTransaction = NoOpOutboxTransaction.Instance; + } + var transportTransaction = context.Extensions.Get(); return session.Open(outboxTransaction, transportTransaction, context.Extensions, context.CancellationToken); } From 72af5c47decf9b48a54304697b4a42a56e64e3e7 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Sat, 26 Sep 2026 07:20:13 +0200 Subject: [PATCH 75/79] Add engineering context map and public context index (#7950) * Add engineering context map and public context index AGENTS.md is the workflow for finding and preserving design rationale and is identical across repositories; docs/README.md describes this repository, indexes the public sources, and lists the pull requests that record decisions; docs/decisions/README.md holds ADR guidance. * Address review: generic component wording, drop a fix from the decision list --------- Co-authored-by: Daniel Marbach --- AGENTS.md | 34 ++++++++++++++++++++++++++++++++ docs/README.md | 42 ++++++++++++++++++++++++++++++++++++++++ docs/decisions/README.md | 33 +++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 AGENTS.md create mode 100644 docs/README.md create mode 100644 docs/decisions/README.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000..70c1597998a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,34 @@ +# AGENTS.md + +This file is the map for people and coding agents working in this repository. What the repository is, how it is laid out, and where its design is explained are in `docs/README.md`. This file changes only when the workflow changes; decisions and rationale go in the sources it points to. + +## Engineering context + +Start here when investigating existing behavior or making a change whose rationale may need to outlive the pull request: + +- [Engineering context](docs/README.md) +- [Architecture and design decisions](docs/decisions/) +- [Contributing guidance](CONTRIBUTING.md) + +Prefer public records close to the implementation. Keep `docs/README.md` current when a canonical public source is added, replaced, or retired; update its links rather than copying rationale into the index. The index lists sources that explain why, with one clause each naming the question it answers, and points to existing indexes for how-to material instead of repeating them. This file describes the workflow, not repository facts. Read `docs/README.md` before answering a question about existing behavior or about where to record a decision, then answer from the index, the code, and pull requests, never from this file alone. + +1. Identify the decision area and the precise question, for example why a constraint exists, why a limit lives in one layer rather than another, or why an alternative was rejected. +2. Start with the public source linked by `docs/README.md`. Read the current public contract, relevant code, tests, and linked pull requests or ADRs. Follow relevant pointer comments as routing hints; the linked source remains canonical. A Git commit can identify a pull request number without containing its description: a squash-merge commit carries a trailing `(#NNNN)`, and a merge commit reads `Merge pull request #NNNN`. Retrieve the pull request with `gh pr view ` from inside the checkout, which identifies the repository. If the pull request cannot be retrieved, report that rather than infer its rationale. Public records control public behavior and contribution requirements. +3. Decide where the rationale for the current work belongs. Record it once and link to it from the other places. A component means a separate repository, such as an NServiceBus package, ServiceControl, or ServicePulse, not a project inside `src/`. + - Pull request description, the default: the problem and why now, the change and its observable effect, constraints that must hold, alternatives that materially affected it, and how it was verified. For a decision that stays within this repository, the pull request is the authoritative record. + - `docs/.md`: how the current design works, for readers who need the current picture rather than the history of one change. Update the relevant page when a change alters it. Do not create a page to restate one pull request. + - `docs/decisions/`: a public ADR only when a pull request is not a sufficient long-term entry point, because the change introduces an important constraint that is hard to discover from code, affects more than one repository, or rejects an alternative likely to return. Before deciding either way, check `docs/README.md` and `git log` for an existing decision on the same theme; when a pull request already records it, link that pull request instead. Follow the structure and quality bar in `docs/decisions/README.md`; an ADR that only justifies a choice already made is not worth keeping. + - Private context, only when step 4 provides an approved root: supporting context that cannot be public goes in an addendum under `$PARTICULAR_CONTEXT_ROOT/repositories//` linked to its public source; a decision that affects more than one repository and cannot be public goes under `$PARTICULAR_CONTEXT_ROOT/cross-component/`. Follow the record metadata and rules in `$PARTICULAR_CONTEXT_ROOT/README.md`. The public record still carries everything a contributor needs. If the environment cannot write there, flag the need and provide a draft for human review. + - Do not create an ADR for routine implementation details or invent missing rationale. +4. Check private context only when this environment explicitly provides `$PARTICULAR_CONTEXT_ROOT/repositories//README.md`, where `` is this repository's GitHub name as shown by `git remote get-url origin`. + - Confirm that with a file check such as `test -f "$PARTICULAR_CONTEXT_ROOT/repositories//README.md"` rather than assuming it from instructions. `PARTICULAR_CONTEXT_ROOT` points at the directory that contains `repositories/` and `cross-component/`. + - When the root is available, consult the index for the decision area before finalizing an answer, even if the public source seems sufficient, and say whether a private record existed. + - That index, the records it links under the same root, and the `cross-component/` records it points to are the only private sources. Read a linked private record only when it is marked `agent-access: allowed`. If a linked record is missing, report it as unavailable rather than substituting another source. + - Do not inspect a sibling `../Platform` checkout. Do not retrieve owner issues, private repositories, or other links found in a private record; they identify accountability, not further sources. + - Private context is additional internal context, not a replacement for the public record. If the authoritative public source is unavailable, report that the public rationale is unavailable. Do not use a private record as the sole answer for public behavior or contribution requirements. +5. If sources conflict, do not resolve the conflict by inference. Use the current implementation and public contract for external behavior, report the conflict, and ask the record owner when it affects the decision. +6. In the response or pull request, cite the records consulted, distinguish evidence from inference, and state when relevant private context was unavailable or unauthorized. Keep private locations, quotations, customer names, and other confidential details out of public artifacts such as pull request descriptions, code comments, and `docs/`; say that internal context was consulted instead. + +## Pointer comments + +A brief code comment may link to a canonical public source, such as a `docs/` file or an ADR under `docs/decisions/`, when the relevant rationale is not apparent from the surrounding code. It is a signpost, not a copy of the rationale: keep the durable explanation in the linked record. Do not use a comment to narrate obvious code, and do not restate a pull request or ADR in the comment body. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000000..bf42c2b87d3 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,42 @@ +# Engineering context + +## About this repository + +NServiceBus is the core library of the Particular Service Platform, producing the NServiceBus NuGet package. It provides the abstractions for transports and persistence, the message processing pipeline, sagas, the outbox, recoverability, serialization, and hosting integration. + +- `src/` — NServiceBus.Core, transport and persistence test doubles, acceptance tests, and samples-as-tests +- `.github/workflows/` — CI pipelines (build and test, code analysis, release, dependency updates) + +## Start here + +- [NServiceBus documentation](https://docs.particular.net/nservicebus/) — public documentation entry point +- [README.md](../README.md) — how to build NServiceBus locally +- [Contributing](https://docs.particular.net/platform/contributing) — contribution process +- [NServiceBus Quick Start](https://docs.particular.net/tutorials/quickstart/) — first tutorial for building with NServiceBus +- [Samples](https://docs.particular.net/samples/) — worked examples of NServiceBus features +- [Platform NuGet packages](https://docs.particular.net/nservicebus/platform-nuget-packages) — where to find every published package + +## Architecture and design + +This repository tracks no design pages yet. When one is added, it will be linked here as the source that explains why the repository is designed the way it is. + +## Decisions and rationale + +- [Architecture and design decisions](decisions/) + +### Decisions recorded in pull requests + +A pull request is listed here only when it is the canonical record for a decision area: it establishes a durable constraint or convention, or rejects an alternative likely to return, and no `docs/` file or ADR covers it. Bug fixes and routine changes are not listed; recover them from `git log` and `gh pr view`. + +- The trimming and NativeAOT support strategy spans multiple coordinated changes rather than one switch — [#7929](https://github.com/Particular/NServiceBus/pull/7929) +- Object-overload `Send`/`Publish`/`Reply` calls keep runtime-type routing by default; the trimming-safe path is opt-in through explicit generic or `Type` overloads — [#7889](https://github.com/Particular/NServiceBus/pull/7889) +- Message metadata resolves without reflection-based assembly scanning so it stays trimming-safe — [#7918](https://github.com/Particular/NServiceBus/pull/7918) +- Startup diagnostics sections carry explicit `JsonTypeInfo` metadata to avoid reflection-based serialization under NativeAOT — [#7882](https://github.com/Particular/NServiceBus/pull/7882) +- Out-of-slot logging is routed through a DI-registered ambient `AsyncLocal` factory instead of mutating `LogManager` global state — [#7758](https://github.com/Particular/NServiceBus/pull/7758) +- `ContextBag`/`BehaviorContext` store pipeline context values in a fixed-size inline array instead of a lazily allocated dictionary — [#7823](https://github.com/Particular/NServiceBus/pull/7823) +- `DispatchProperties`/`ReceiveProperties` keep well-known keys in dedicated fields instead of a plain `Dictionary` — [#7843](https://github.com/Particular/NServiceBus/pull/7843) +- Host id generation and the learning saga persister use an XxHash128-based `DeterministicGuid`, with the legacy MD5 path kept behind an `AppContext` switch until removal in v12 — [#7723](https://github.com/Particular/NServiceBus/pull/7723) +- OpenTelemetry baggage propagation through `DistributedContextPropagator` is gated behind an `AppContext` switch until v11 to keep rolling upgrades compatible — [#7825](https://github.com/Particular/NServiceBus/pull/7825) +- Trace-continuation behavior for delayed messages is configurable rather than fixed — [#7845](https://github.com/Particular/NServiceBus/pull/7845) + +Keep this index current when a canonical source is added, replaced, or retired; link, do not copy. diff --git a/docs/decisions/README.md b/docs/decisions/README.md new file mode 100644 index 00000000000..7ecb940b3a9 --- /dev/null +++ b/docs/decisions/README.md @@ -0,0 +1,33 @@ +# Architecture and design decisions + +For a single-component decision, the pull request that implements it remains authoritative by default. Write an architecture decision record (ADR) here only when a pull request is not a sufficient long-term entry point for the rationale, such as when: + +- the decision affects multiple components; +- an important constraint is difficult to discover from code or public documentation; +- engineers repeatedly fail to find the original rationale; or +- a rejected alternative is likely to return. + +## Naming + +Name each file `YYYY-MM-DD-short-title.md`, using the date the decision was made. + +## Structure + +Each ADR contains: + +- **Context** — the problem and the constraints that shaped it. +- **Decision** — what was decided. +- **Consequences** — the resulting behavior, tradeoffs, and any follow-up work. +- **Alternative approaches** — the alternatives considered and why they were rejected. + +Link each ADR to the pull request that implemented the decision. The ADR records the durable rationale; the pull request remains the record of the actual code change. + +## Quality bar + +An ADR exists to explore the context and the tradeoffs, not to justify a choice already made. Gregor Hohpe's [warning signs for misused ADRs](https://www.linkedin.com/posts/ghohpe_architecturedecisionrecords-activity-7502716054968844288-KVLU) apply here: + +- Context states the problem and its constraints, not preferences for the chosen option. +- Every downside listed under Consequences names a mitigation or an explicit acceptance, and quantifies the risk where it can be quantified. +- Alternative approaches are credible options someone could have argued for, not "do nothing"; identify the ones that represent points of leverage. + +An ADR that fails this bar is not worth keeping; record the decision in the pull request instead. From 8af7dbc7b9df8d77d397ae877d6be99baa651a83 Mon Sep 17 00:00:00 2001 From: Tomasz Masternak Date: Mon, 5 Oct 2026 13:13:25 +0200 Subject: [PATCH 76/79] rename `TryGetRecordingPipelineActivity` methods to `TryGetPipelineActivity` --- .../OpenTelemetry/ActivityExtensionsTests.cs | 6 +++--- .../OpenTelemetry/Tracing/ActivityExtensions.cs | 10 +++++----- .../Tracing/SubscribeDiagnosticsBehavior.cs | 2 +- .../Tracing/UnsubscribeDiagnosticsBehavior.cs | 2 +- ...ransportReceiveToPhysicalMessageConnector.cs | 17 +++++++++++++---- .../Outgoing/RoutingToDispatchConnector.cs | 2 +- 6 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/NServiceBus.Core.Tests/OpenTelemetry/ActivityExtensionsTests.cs b/src/NServiceBus.Core.Tests/OpenTelemetry/ActivityExtensionsTests.cs index a8880919f67..c4825b33064 100644 --- a/src/NServiceBus.Core.Tests/OpenTelemetry/ActivityExtensionsTests.cs +++ b/src/NServiceBus.Core.Tests/OpenTelemetry/ActivityExtensionsTests.cs @@ -16,7 +16,7 @@ public void TryGetRecordingPipelineActivity_should_return_false_when_key_not_fou var contextBag = new ContextBag(); using (Assert.EnterMultipleScope()) { - Assert.That(contextBag.TryGetRecordingOutgoingPipelineActivity(out var activity), Is.False); + Assert.That(contextBag.TryGetOutgoingPipelineActivity(out var activity), Is.False); Assert.That(activity, Is.Null); } } @@ -31,7 +31,7 @@ public void TryGetRecordingPipelineActivity_should_return_false_when_value_null( using (Assert.EnterMultipleScope()) { - Assert.That(contextBag.TryGetRecordingOutgoingPipelineActivity(out var activity), Is.False); + Assert.That(contextBag.TryGetOutgoingPipelineActivity(out var activity), Is.False); Assert.That(activity, Is.Null); } } @@ -50,7 +50,7 @@ public void TryGetRecordingPipelineActivity_should_return_false_when_not_recordi using (Assert.EnterMultipleScope()) { - Assert.That(contextBag.TryGetRecordingOutgoingPipelineActivity(out var activity), Is.True); + Assert.That(contextBag.TryGetOutgoingPipelineActivity(out var activity), Is.True); Assert.That(activity, Is.EqualTo(recordingActivity)); } } diff --git a/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityExtensions.cs b/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityExtensions.cs index 3f784962a9b..023cec6aadb 100644 --- a/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityExtensions.cs +++ b/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityExtensions.cs @@ -11,13 +11,13 @@ static class ActivityExtensions public const string OutgoingActivityKey = "NServiceBus.Diagnostics.Activity.Outgoing"; public const string IncomingActivityKey = "NServiceBus.Diagnostics.Activity.Incoming"; - public static bool TryGetRecordingOutgoingPipelineActivity(this ContextBag pipelineContext, [NotNullWhen(true)] out Activity? activity) - => pipelineContext.TryGetRecordingPipelineActivity(OutgoingActivityKey, out activity); + public static bool TryGetOutgoingPipelineActivity(this ContextBag pipelineContext, [NotNullWhen(true)] out Activity? activity) + => pipelineContext.TryGetPipelineActivity(OutgoingActivityKey, out activity); - public static bool TryGetRecordingIncomingPipelineActivity(this ContextBag pipelineContext, [NotNullWhen(true)] out Activity? activity) - => pipelineContext.TryGetRecordingPipelineActivity(IncomingActivityKey, out activity); + public static bool TryGetIncomingPipelineActivity(this ContextBag pipelineContext, [NotNullWhen(true)] out Activity? activity) + => pipelineContext.TryGetPipelineActivity(IncomingActivityKey, out activity); - static bool TryGetRecordingPipelineActivity(this ContextBag pipelineContext, string activityKey, [NotNullWhen(true)] out Activity? activity) + static bool TryGetPipelineActivity(this ContextBag pipelineContext, string activityKey, [NotNullWhen(true)] out Activity? activity) { if (Activity.Current is not null // Cheaper to check than searching the pipeline context to start with. If there is no ambient activity, there can't be an activity in the context. && pipelineContext.TryGet(activityKey, out activity) // Search activity in context bag diff --git a/src/NServiceBus.Core/OpenTelemetry/Tracing/SubscribeDiagnosticsBehavior.cs b/src/NServiceBus.Core/OpenTelemetry/Tracing/SubscribeDiagnosticsBehavior.cs index f5f531b6324..dfa025c6099 100644 --- a/src/NServiceBus.Core/OpenTelemetry/Tracing/SubscribeDiagnosticsBehavior.cs +++ b/src/NServiceBus.Core/OpenTelemetry/Tracing/SubscribeDiagnosticsBehavior.cs @@ -10,7 +10,7 @@ class SubscribeDiagnosticsBehavior : IBehavior next) { - if (context.Extensions.TryGetRecordingOutgoingPipelineActivity(out var activity)) + if (context.Extensions.TryGetOutgoingPipelineActivity(out var activity)) { activity.SetTag(ActivityTags.EventTypes, string.Join(",", (object[])context.EventTypes)); } diff --git a/src/NServiceBus.Core/OpenTelemetry/Tracing/UnsubscribeDiagnosticsBehavior.cs b/src/NServiceBus.Core/OpenTelemetry/Tracing/UnsubscribeDiagnosticsBehavior.cs index fd5f0936f24..93cc7433afe 100644 --- a/src/NServiceBus.Core/OpenTelemetry/Tracing/UnsubscribeDiagnosticsBehavior.cs +++ b/src/NServiceBus.Core/OpenTelemetry/Tracing/UnsubscribeDiagnosticsBehavior.cs @@ -10,7 +10,7 @@ class UnsubscribeDiagnosticsBehavior : IBehavior next) { - if (context.Extensions.TryGetRecordingOutgoingPipelineActivity(out var activity)) + if (context.Extensions.TryGetOutgoingPipelineActivity(out var activity)) { activity.SetTag(ActivityTags.EventTypes, context.EventType.FullName); } diff --git a/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs b/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs index 55d33debdd4..9c06a8c35a9 100644 --- a/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs +++ b/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs @@ -76,8 +76,8 @@ async Task InvokeWithOutbox(ITransportReceiveContext context, Func 0) { var batchDispatchContext = this.CreateBatchDispatchContext(operations, physicalMessageContext); - var dispatchActivity = WriteStartDispatchingEvent(physicalMessageContext, operations.Length); + + Activity? dispatchActivity = null; + if (!instrumentationOptions.EmitMessageDispatchingEvents) + { + physicalMessageContext.Extensions.TryGetIncomingPipelineActivity(out dispatchActivity); + } + + dispatchActivity?.AddEvent(new("Start dispatching", tags: new() { { "message-count", operations.Length } })); + await this.Fork(batchDispatchContext).ConfigureAwait(false); + dispatchActivity?.AddEvent(new("Finished dispatching")); } @@ -140,7 +149,7 @@ async Task InvokeWithoutOutbox(ITransportReceiveContext context, Func Date: Mon, 5 Oct 2026 14:11:30 +0200 Subject: [PATCH 77/79] =?UTF-8?q?=E2=9A=9C=EF=B8=8F=20Put=20the=20connecto?= =?UTF-8?q?r=20constructor=20arguments=20on=20separate=20lines?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/NServiceBus.Core/Receiving/ReceiveComponent.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/NServiceBus.Core/Receiving/ReceiveComponent.cs b/src/NServiceBus.Core/Receiving/ReceiveComponent.cs index a92b0465116..f25328126dc 100644 --- a/src/NServiceBus.Core/Receiving/ReceiveComponent.cs +++ b/src/NServiceBus.Core/Receiving/ReceiveComponent.cs @@ -69,7 +69,12 @@ public static ReceiveComponent Configure( pipelineSettings.Register("TransportReceiveToPhysicalMessageProcessingConnector", b => { var storage = b.GetService() ?? new NoOpOutboxStorage(); - return new TransportReceiveToPhysicalMessageConnector(storage, b.GetRequiredService(), hostingConfiguration.ActivityFactory.Options, b.GetRequiredService>()); + return new TransportReceiveToPhysicalMessageConnector( + storage, + b.GetRequiredService(), + hostingConfiguration.ActivityFactory.Options, + b.GetRequiredService>() + ); }, "Allows to abort processing the message"); pipelineSettings.Register("LoadHandlersConnector", sp => new LoadHandlersConnector(sp.GetRequiredService(), hostingConfiguration.ActivityFactory, sp.GetRequiredService()), "Gets all the handlers to invoke from the MessageHandler registry based on the message type."); @@ -195,7 +200,7 @@ public async Task Initialize( pipelineComponent, messageOperations, activityFactory - ); + ); await mainPump.Initialize( configuration.PushRuntimeSettings, From c43ada9662475dc8f0cd0176a03da096e362c902 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Mon, 5 Oct 2026 14:11:30 +0200 Subject: [PATCH 78/79] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Dispatch=20through?= =?UTF-8?q?=20one=20method=20in=20both=20outbox=20paths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inlined event gating in the with-outbox path had an inverted condition and duplicated the fork block. Both paths now call Dispatch, which forks directly when the dispatching events are off and goes through DispatchWithEvents when they are on. Removes the unused WriteStartDispatchingEvent helper. --- ...nsportReceiveToPhysicalMessageConnector.cs | 44 ++++++++----------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs b/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs index 9c06a8c35a9..8ba0e81163b 100644 --- a/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs +++ b/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs @@ -86,18 +86,7 @@ async Task InvokeWithOutbox(ITransportReceiveContext context, Func 0) { var batchDispatchContext = this.CreateBatchDispatchContext(operations, physicalMessageContext); - - Activity? dispatchActivity = null; - if (!instrumentationOptions.EmitMessageDispatchingEvents) - { - physicalMessageContext.Extensions.TryGetIncomingPipelineActivity(out dispatchActivity); - } - - dispatchActivity?.AddEvent(new("Start dispatching", tags: new() { { "message-count", operations.Length } })); - - await this.Fork(batchDispatchContext).ConfigureAwait(false); - - dispatchActivity?.AddEvent(new("Finished dispatching")); + await Dispatch(batchDispatchContext).ConfigureAwait(false); } await outboxStorage.SetAsDispatched(messageId, context.Extensions, context.CancellationToken).ConfigureAwait(false); @@ -133,29 +122,32 @@ async Task InvokeWithoutOutbox(ITransportReceiveContext context, Func 0) { var batchDispatchContext = this.CreateBatchDispatchContext(operations, physicalMessageContext); - var dispatchActivity = WriteStartDispatchingEvent(physicalMessageContext, operations.Length); - await this.Fork(batchDispatchContext).ConfigureAwait(false); - dispatchActivity?.AddEvent(new("Finished dispatching")); + await Dispatch(batchDispatchContext).ConfigureAwait(false); } pipelineMetrics.RecordCriticalTimeAndTotalProcessed(context); } - // Synchronous by design so the dispatch instrumentation lives in one place without adding an async state - // machine to either path; the Fork await stays in the callers. The activity is resolved from the physical - // message context; child context bags read through to their parent, so this sees the same activity as the - // root receive context. Returns the activity (or null) so the caller can emit the finished event after the fork. - // Returns null when the dispatching events are turned off so neither event is written. - Activity? WriteStartDispatchingEvent(IIncomingPhysicalMessageContext physicalMessageContext, int operationCount) + // Both paths dispatch through here so the event instrumentation lives in one place. The activity is resolved + // from the batch dispatch context; child context bags read through to their parent, so this sees the same + // activity as the root receive context. When the events are off (the pre-v11 default) this returns the Fork + // task directly and adds no state machine of its own. + Task Dispatch(IBatchDispatchContext batchDispatchContext) { - if (!instrumentationOptions.EmitMessageDispatchingEvents || - !physicalMessageContext.Extensions.TryGetIncomingPipelineActivity(out var activity)) + if (instrumentationOptions.EmitMessageDispatchingEvents && + batchDispatchContext.Extensions.TryGetIncomingPipelineActivity(out var activity)) { - return null; + return DispatchWithEvents(batchDispatchContext, activity); } - activity.AddEvent(new("Start dispatching", tags: new() { { "message-count", operationCount } })); - return activity; + return this.Fork(batchDispatchContext); + } + + async Task DispatchWithEvents(IBatchDispatchContext batchDispatchContext, Activity activity) + { + activity.AddEvent(new("Start dispatching", tags: new() { { "message-count", batchDispatchContext.Operations.Count } })); + await this.Fork(batchDispatchContext).ConfigureAwait(false); + activity.AddEvent(new("Finished dispatching")); } static void ConvertToPendingOperations(OutboxMessage deduplicationEntry, PendingTransportOperations pendingTransportOperations) From e96ef16bc0a9252f6f8252afcf43275dbf83da41 Mon Sep 17 00:00:00 2001 From: Ramon Smits Date: Mon, 5 Oct 2026 14:23:35 +0200 Subject: [PATCH 79/79] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Resolve=20the=20Acti?= =?UTF-8?q?vityFactory=20logger=20through=20the=20service=20provider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The factory is created before the container exists, so it cannot take an ILogger in its constructor. RecordError now receives the behavior context's service provider instead of the unused ContextBag and resolves ILogger on first use. The message is emitted through a LoggerMessage method. Removes the last LogManager usage added by the otel branch. --- ...gerUsage.ApproveLogManagerUsage.approved.txt | 1 - .../OpenTelemetry/Tracing/ActivityFactory.cs | 17 +++++++++++------ .../OpenTelemetry/Tracing/IActivityFactory.cs | 3 +-- .../Tracing/NoOpActivityFactory.cs | 3 +-- .../Pipeline/Incoming/LoadHandlersConnector.cs | 2 +- .../Pipeline/MainPipelineExecutor.cs | 2 +- .../Unicast/MessageOperations.cs | 10 +++++----- 7 files changed, 20 insertions(+), 18 deletions(-) diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/LogManagerUsage.ApproveLogManagerUsage.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/LogManagerUsage.ApproveLogManagerUsage.approved.txt index b46abffb5a1..fb3f7c59c76 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/LogManagerUsage.ApproveLogManagerUsage.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/LogManagerUsage.ApproveLogManagerUsage.approved.txt @@ -4,7 +4,6 @@ Microsoft.Extensions.Logging instead (see https://learn.microsoft.com/dotnet/cor Changes that make this list longer should only be approved when the type affects user-facing code or when DI is not available. The list should otherwise shrink over time. ----- -NServiceBus.ActivityFactory NServiceBus.AsyncFile NServiceBus.AuditConfigReader NServiceBus.AuditInvalidLicenseBehavior diff --git a/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityFactory.cs b/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityFactory.cs index 4669e372477..dec4c1374f1 100644 --- a/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityFactory.cs +++ b/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityFactory.cs @@ -6,12 +6,12 @@ namespace NServiceBus; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; -using Extensibility; -using Logging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Pipeline; using Transport; -sealed class ActivityFactory(InstrumentationOptions options) : IActivityFactory +sealed partial class ActivityFactory(InstrumentationOptions options) : IActivityFactory { public InstrumentationOptions Options { get; } = options; @@ -223,7 +223,7 @@ public void UpdateActivityFromRecoverabilityAction(Activity activity, Recoverabi } } - public void RecordError(Activity? activity, Exception exception, ContextBag context) + public void RecordError(Activity? activity, Exception exception, IServiceProvider serviceProvider) { if (activity == null) { @@ -239,7 +239,9 @@ public void RecordError(Activity? activity, Exception exception, ContextBag cont { if (Options.ExceptionRecordingMode == ExceptionRecordingMode.Logs) { - Logger.Error($"An exception occurred while executing '{activity.DisplayName}'.", exception); + // The factory is created before the container exists, so the logger is resolved on first use. + logger ??= serviceProvider.GetRequiredService>(); + LogExceptionWhileExecuting(logger, exception, activity.DisplayName); } else { @@ -257,5 +259,8 @@ public void RecordError(Activity? activity, Exception exception, ContextBag cont const string ExceptionRecordedFlag = "otel.exception.recorded"; - static readonly ILog Logger = LogManager.GetLogger(); + ILogger? logger; + + [LoggerMessage(LogLevel.Error, "An exception occurred while executing '{DisplayName}'.")] + static partial void LogExceptionWhileExecuting(ILogger logger, Exception exception, string displayName); } \ No newline at end of file diff --git a/src/NServiceBus.Core/OpenTelemetry/Tracing/IActivityFactory.cs b/src/NServiceBus.Core/OpenTelemetry/Tracing/IActivityFactory.cs index 44943a4ce04..edec8ea2b24 100644 --- a/src/NServiceBus.Core/OpenTelemetry/Tracing/IActivityFactory.cs +++ b/src/NServiceBus.Core/OpenTelemetry/Tracing/IActivityFactory.cs @@ -4,7 +4,6 @@ namespace NServiceBus; using System; using System.Diagnostics; -using Extensibility; using Pipeline; using Transport; @@ -16,5 +15,5 @@ interface IActivityFactory Activity? StartHandlerActivity(MessageHandler messageHandler); Activity? StartRecoverabilityActivity(ErrorContext context); void UpdateActivityFromRecoverabilityAction(Activity activity, RecoverabilityAction recoverabilityAction, string receiveAddress); - void RecordError(Activity? activity, Exception exception, ContextBag context); + void RecordError(Activity? activity, Exception exception, IServiceProvider serviceProvider); } \ No newline at end of file diff --git a/src/NServiceBus.Core/OpenTelemetry/Tracing/NoOpActivityFactory.cs b/src/NServiceBus.Core/OpenTelemetry/Tracing/NoOpActivityFactory.cs index 5a025496460..b161531fa7b 100644 --- a/src/NServiceBus.Core/OpenTelemetry/Tracing/NoOpActivityFactory.cs +++ b/src/NServiceBus.Core/OpenTelemetry/Tracing/NoOpActivityFactory.cs @@ -4,7 +4,6 @@ namespace NServiceBus; using System; using System.Diagnostics; -using Extensibility; using Pipeline; using Transport; @@ -24,7 +23,7 @@ public void UpdateActivityFromRecoverabilityAction(Activity activity, Recoverabi { } - public void RecordError(Activity? activity, Exception exception, ContextBag context) + public void RecordError(Activity? activity, Exception exception, IServiceProvider serviceProvider) { } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Pipeline/Incoming/LoadHandlersConnector.cs b/src/NServiceBus.Core/Pipeline/Incoming/LoadHandlersConnector.cs index e1385d74fef..91d46f88ebc 100644 --- a/src/NServiceBus.Core/Pipeline/Incoming/LoadHandlersConnector.cs +++ b/src/NServiceBus.Core/Pipeline/Incoming/LoadHandlersConnector.cs @@ -65,7 +65,7 @@ public override async Task Invoke(IIncomingLogicalMessageContext context, Func