diff --git a/src/NServiceBus.Core.Tests/AssemblyScanner/AssemblyScannerTests.cs b/src/NServiceBus.Core.Tests/AssemblyScanner/AssemblyScannerTests.cs index 3775c4a16b5..d66d562fa94 100644 --- a/src/NServiceBus.Core.Tests/AssemblyScanner/AssemblyScannerTests.cs +++ b/src/NServiceBus.Core.Tests/AssemblyScanner/AssemblyScannerTests.cs @@ -96,7 +96,8 @@ public void Assemblies_which_reference_older_core_version_are_included() { ThrowExceptions = false, ScanAppDomainAssemblies = false, - CoreAssemblyName = busAssemblyV2.Name + CoreAssemblyName = busAssemblyV2.Name, + MessageInterfacesAssemblyName = null }; var result = scanner.GetScannableAssemblies(); @@ -320,6 +321,7 @@ static AssemblyScanner CreateDefaultAssemblyScanner(DynamicAssembly coreAssembly new AssemblyScanner(DynamicAssembly.TestAssemblyDirectory) { CoreAssemblyName = coreAssembly.DynamicName, + MessageInterfacesAssemblyName = null, ScanAppDomainAssemblies = true, ScanFileSystemAssemblies = true, ThrowExceptions = true diff --git a/src/NServiceBus.Core.Tests/AssemblyScanner/AssemblyScanningComponentTests.cs b/src/NServiceBus.Core.Tests/AssemblyScanner/AssemblyScanningComponentTests.cs index 250f79d3a9f..f81de917958 100644 --- a/src/NServiceBus.Core.Tests/AssemblyScanner/AssemblyScanningComponentTests.cs +++ b/src/NServiceBus.Core.Tests/AssemblyScanner/AssemblyScanningComponentTests.cs @@ -14,9 +14,10 @@ public void Should_initialize_scanner_with_custom_path_when_provided() var settingsHolder = new SettingsHolder(); settingsHolder.Set(new HostingComponent.Settings(settingsHolder)); - var configuration = new AssemblyScanningComponent.Configuration(settingsHolder); - - configuration.AssemblyScannerConfiguration.AdditionalAssemblyScanningPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "TestDlls", "Nested", "Subfolder"); + var configuration = new AssemblyScanningComponent.Configuration(settingsHolder) + { + AssemblyScannerConfiguration = { AdditionalAssemblyScanningPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "TestDlls", "Nested", "Subfolder") } + }; var component = AssemblyScanningComponent.Initialize(configuration, settingsHolder); diff --git a/src/NServiceBus.Core.Tests/AssemblyScanner/When_directory_with_messages_referencing_core_or_interfaces_is_scanned.cs b/src/NServiceBus.Core.Tests/AssemblyScanner/When_directory_with_messages_referencing_core_or_interfaces_is_scanned.cs new file mode 100644 index 00000000000..f3f07ed23d0 --- /dev/null +++ b/src/NServiceBus.Core.Tests/AssemblyScanner/When_directory_with_messages_referencing_core_or_interfaces_is_scanned.cs @@ -0,0 +1,23 @@ +namespace NServiceBus.Core.Tests.AssemblyScanner; + +using System.IO; +using System.Linq; +using Hosting.Helpers; +using NUnit.Framework; + +[TestFixture] +public class When_directory_with_messages_referencing_core_or_interfaces_is_scanned +{ + [Test] + public void Assemblies_should_be_scanned() + { + var scanner = + new AssemblyScanner(Path.Combine(TestContext.CurrentContext.TestDirectory, "TestDlls", "Messages")); + + var result = scanner.GetScannableAssemblies(); + var assemblyFullNames = result.Assemblies.Select(a => a.GetName().Name).ToList(); + + CollectionAssert.Contains(assemblyFullNames, "Messages.Referencing.Core"); + CollectionAssert.Contains(assemblyFullNames, "Messages.Referencing.MessageInterfaces"); + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/AssemblyScanner/When_using_type_forwarding.cs b/src/NServiceBus.Core.Tests/AssemblyScanner/When_using_type_forwarding.cs new file mode 100644 index 00000000000..a40703d89e5 --- /dev/null +++ b/src/NServiceBus.Core.Tests/AssemblyScanner/When_using_type_forwarding.cs @@ -0,0 +1,43 @@ +namespace NServiceBus.Core.Tests.AssemblyScanner; + +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using Hosting.Helpers; +using NUnit.Framework; + +[TestFixture] +public class When_using_type_forwarding +{ + // This test is not perfect since it relies on existing binaries to covered assembly scanning scenarios. Since we + // already use those though the idea of this test is to make sure that the assembly scanner is able to scan all + // assemblies that have a type forwarding rule within the core assembly. This might turn out to be a broad assumption + // in the future, and we might have to explicitly remove some but in the meantime this test would have covered us + // when we moved ICommand, IEvent and IMessages to the message interfaces assembly. + [Test] + public void Should_scan_assemblies_indicated_by_the_forwarding_metadata() + { + using var fs = File.OpenRead(typeof(AssemblyScanner).Assembly.Location); + using var peReader = new PEReader(fs); + var metadataReader = peReader.GetMetadataReader(); + + // Exported types only contains a small subset of types, so it's safe to enumerate all of them + var assemblyNamesOfForwardedTypes = metadataReader.ExportedTypes + .Select(exportedTypeHandle => metadataReader.GetExportedType(exportedTypeHandle)) + .Where(exportedType => exportedType.IsForwarder) + .Select(exportedType => (AssemblyReferenceHandle)exportedType.Implementation) + .Select(assemblyReferenceHandle => metadataReader.GetAssemblyReference(assemblyReferenceHandle)) + .Select(assemblyReference => metadataReader.GetString(assemblyReference.Name)) + .Where(assemblyName => assemblyName.StartsWith("NServiceBus") || assemblyName.StartsWith("Particular")) + .Distinct() + .ToList(); + + var scanner = new AssemblyScanner(Path.Combine(TestContext.CurrentContext.TestDirectory, "TestDlls")); + + var result = scanner.GetScannableAssemblies(); + var assemblyFullNames = result.Assemblies.Select(a => a.GetName().Name).ToList(); + + CollectionAssert.IsSubsetOf(assemblyNamesOfForwardedTypes, assemblyFullNames); + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/TestDlls/Messages/Messages.Referencing.Core.dll b/src/NServiceBus.Core.Tests/TestDlls/Messages/Messages.Referencing.Core.dll new file mode 100644 index 00000000000..ec17106ef11 Binary files /dev/null and b/src/NServiceBus.Core.Tests/TestDlls/Messages/Messages.Referencing.Core.dll differ diff --git a/src/NServiceBus.Core.Tests/TestDlls/Messages/Messages.Referencing.MessageInterfaces.dll b/src/NServiceBus.Core.Tests/TestDlls/Messages/Messages.Referencing.MessageInterfaces.dll new file mode 100644 index 00000000000..4b42adeb279 Binary files /dev/null and b/src/NServiceBus.Core.Tests/TestDlls/Messages/Messages.Referencing.MessageInterfaces.dll differ diff --git a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanner.cs b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanner.cs index dac4d62ebbc..c6754aa5033 100644 --- a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanner.cs +++ b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanner.cs @@ -55,6 +55,8 @@ internal AssemblyScanner(Assembly assemblyToScan) internal string CoreAssemblyName { get; set; } = NServiceBusCoreAssemblyName; + internal string MessageInterfacesAssemblyName { get; set; } = NServiceBusMessageInterfacesAssemblyName; + internal IReadOnlyCollection AssembliesToSkip { set => assembliesToSkip = new HashSet(value.Select(RemoveExtension), StringComparer.OrdinalIgnoreCase); @@ -224,7 +226,8 @@ bool ScanAssembly(Assembly assembly, Dictionary processed) processed[assembly.FullName] = false; - if (assembly.GetName().Name == CoreAssemblyName) + var assemblyName = assembly.GetName(); + if (IsCoreOrMessageInterfaceAssembly(assemblyName)) { return processed[assembly.FullName] = true; } @@ -409,7 +412,7 @@ bool ShouldScanDependencies(Assembly assembly) var assemblyName = assembly.GetName(); - if (assemblyName.Name == CoreAssemblyName) + if (IsCoreOrMessageInterfaceAssembly(assemblyName)) { return false; } @@ -427,6 +430,15 @@ bool ShouldScanDependencies(Assembly assembly) return true; } + // We are deliberately checking here against the MessageInterfaces assembly name because + // the command, event, and message interfaces have been moved there by using type forwarding. + // While it would be possible to read the type forwarding information from the assembly, that imposes + // some performance overhead, and we don't expect that the assembly name will change nor that we will add many + // more type forwarding cases. Should that be the case we might want to revisit the idea of reading the metadata + // information from the assembly. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + bool IsCoreOrMessageInterfaceAssembly(AssemblyName assemblyName) => string.Equals(assemblyName.Name, CoreAssemblyName, StringComparison.Ordinal) || string.Equals(assemblyName.Name, MessageInterfacesAssemblyName, StringComparison.Ordinal); + AssemblyValidator assemblyValidator = new AssemblyValidator(); internal bool ScanNestedDirectories; Assembly assemblyToScan; @@ -434,6 +446,7 @@ bool ShouldScanDependencies(Assembly assembly) HashSet typesToSkip = new(); HashSet assembliesToSkip = new(StringComparer.OrdinalIgnoreCase); const string NServiceBusCoreAssemblyName = "NServiceBus.Core"; + const string NServiceBusMessageInterfacesAssemblyName = "NServiceBus.MessageInterfaces"; static readonly string[] FileSearchPatternsToUse = {