Skip to content

Commit 71fe44b

Browse files
Daniel MarbachdanielmarbachPhilBastianbording
authored
AssemblyScanner doesn't scan message assemblies that reference Message Interfaces (#7081) (#7089)
* Add a test to verify the messages referencing core are scanned * Failing test for messages that reference message interfaces * Unify in one test due avoid assembly loading issues * Cleanup * AssemblyScanner should scan assemblies that reference the message interfaces assembly to make sure messages using those interfaces can be discovered and do not act like unobtrusive messages * Extract into method with a huge comment and inline hints * Add a type forwarding test as a safety net * Update src/NServiceBus.Core.Tests/AssemblyScanner/When_using_type_forwarding.cs Co-authored-by: Phil Bastian <[email protected]> * Apply suggestions from code review Co-authored-by: Brandon Ording <[email protected]> * string.Equals Co-authored-by: Brandon Ording <[email protected]> --------- Co-authored-by: danielmarbach <[email protected]> Co-authored-by: Phil Bastian <[email protected]> Co-authored-by: Brandon Ording <[email protected]> (cherry picked from commit 3a7c74c)
1 parent 37ad2ee commit 71fe44b

File tree

6 files changed

+90
-5
lines changed

6 files changed

+90
-5
lines changed

src/NServiceBus.Core.Tests/AssemblyScanner/AssemblyScanningComponentTests.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ public void Should_initialize_scanner_with_custom_path_when_provided()
1414
var settingsHolder = new SettingsHolder();
1515
settingsHolder.Set(new HostingComponent.Settings(settingsHolder));
1616

17-
var configuration = new AssemblyScanningComponent.Configuration(settingsHolder);
18-
19-
configuration.AssemblyScannerConfiguration.AdditionalAssemblyScanningPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "TestDlls", "Nested", "Subfolder");
17+
var configuration = new AssemblyScanningComponent.Configuration(settingsHolder)
18+
{
19+
AssemblyScannerConfiguration = { AdditionalAssemblyScanningPath = Path.Combine(TestContext.CurrentContext.TestDirectory, "TestDlls", "Nested", "Subfolder") }
20+
};
2021

2122
var component = AssemblyScanningComponent.Initialize(configuration, settingsHolder);
2223

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace NServiceBus.Core.Tests.AssemblyScanner;
2+
3+
using System.IO;
4+
using System.Linq;
5+
using Hosting.Helpers;
6+
using NUnit.Framework;
7+
8+
[TestFixture]
9+
public class When_directory_with_messages_referencing_core_or_interfaces_is_scanned
10+
{
11+
[Test]
12+
public void Assemblies_should_be_scanned()
13+
{
14+
var scanner = new AssemblyScanner(Path.Combine(TestContext.CurrentContext.TestDirectory, "TestDlls", "Messages"))
15+
{
16+
ScanAppDomainAssemblies = false
17+
};
18+
19+
var result = scanner.GetScannableAssemblies();
20+
var assemblyFullNames = result.Assemblies.Select(a => a.GetName().Name).ToList();
21+
22+
CollectionAssert.Contains(assemblyFullNames, "Messages.Referencing.Core");
23+
CollectionAssert.Contains(assemblyFullNames, "Messages.Referencing.MessageInterfaces");
24+
}
25+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
namespace NServiceBus.Core.Tests.AssemblyScanner;
2+
3+
using System.IO;
4+
using System.Linq;
5+
using System.Reflection.Metadata;
6+
using System.Reflection.PortableExecutable;
7+
using Hosting.Helpers;
8+
using NUnit.Framework;
9+
10+
[TestFixture]
11+
public class When_using_type_forwarding
12+
{
13+
// This test is not perfect since it relies on existing binaries to covered assembly scanning scenarios. Since we
14+
// already use those though the idea of this test is to make sure that the assembly scanner is able to scan all
15+
// assemblies that have a type forwarding rule within the core assembly. This might turn out to be a broad assumption
16+
// in the future, and we might have to explicitly remove some but in the meantime this test would have covered us
17+
// when we moved ICommand, IEvent and IMessages to the message interfaces assembly.
18+
[Test]
19+
public void Should_scan_assemblies_indicated_by_the_forwarding_metadata()
20+
{
21+
using var fs = File.OpenRead(typeof(AssemblyScanner).Assembly.Location);
22+
using var peReader = new PEReader(fs);
23+
var metadataReader = peReader.GetMetadataReader();
24+
25+
// Exported types only contains a small subset of types, so it's safe to enumerate all of them
26+
var assemblyNamesOfForwardedTypes = metadataReader.ExportedTypes
27+
.Select(exportedTypeHandle => metadataReader.GetExportedType(exportedTypeHandle))
28+
.Where(exportedType => exportedType.IsForwarder)
29+
.Select(exportedType => (AssemblyReferenceHandle)exportedType.Implementation)
30+
.Select(assemblyReferenceHandle => metadataReader.GetAssemblyReference(assemblyReferenceHandle))
31+
.Select(assemblyReference => metadataReader.GetString(assemblyReference.Name))
32+
.Where(assemblyName => assemblyName.StartsWith("NServiceBus") || assemblyName.StartsWith("Particular"))
33+
.Distinct()
34+
.ToList();
35+
36+
var scanner = new AssemblyScanner(Path.Combine(TestContext.CurrentContext.TestDirectory, "TestDlls"))
37+
{
38+
ScanAppDomainAssemblies = false
39+
};
40+
41+
var result = scanner.GetScannableAssemblies();
42+
var assemblyFullNames = result.Assemblies.Select(a => a.GetName().Name).ToList();
43+
44+
CollectionAssert.IsSubsetOf(assemblyNamesOfForwardedTypes, assemblyFullNames);
45+
}
46+
}
4 KB
Binary file not shown.
4.5 KB
Binary file not shown.

src/NServiceBus.Core/Hosting/Helpers/AssemblyScanner.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ internal AssemblyScanner(Assembly assemblyToScan)
5353

5454
internal string CoreAssemblyName { get; set; } = NServiceBusCoreAssemblyName;
5555

56+
internal string MessageInterfacesAssemblyName { get; set; } = NServiceBusMessageInterfacesAssemblyName;
57+
5658
internal IReadOnlyCollection<string> AssembliesToSkip
5759
{
5860
set => assembliesToSkip = new HashSet<string>(value.Select(RemoveExtension), StringComparer.OrdinalIgnoreCase);
@@ -219,7 +221,8 @@ bool ScanAssembly(Assembly assembly, Dictionary<string, bool> processed)
219221

220222
processed[assembly.FullName] = false;
221223

222-
if (assembly.GetName().Name == CoreAssemblyName)
224+
var assemblyName = assembly.GetName();
225+
if (IsCoreOrMessageInterfaceAssembly(assemblyName))
223226
{
224227
return processed[assembly.FullName] = true;
225228
}
@@ -373,7 +376,7 @@ bool ShouldScanDependencies(Assembly assembly)
373376

374377
var assemblyName = assembly.GetName();
375378

376-
if (assemblyName.Name == CoreAssemblyName)
379+
if (IsCoreOrMessageInterfaceAssembly(assemblyName))
377380
{
378381
return false;
379382
}
@@ -387,12 +390,22 @@ bool ShouldScanDependencies(Assembly assembly)
387390
return !IsExcluded(assemblyName.Name);
388391
}
389392

393+
// We are deliberately checking here against the MessageInterfaces assembly name because
394+
// the command, event, and message interfaces have been moved there by using type forwarding.
395+
// While it would be possible to read the type forwarding information from the assembly, that imposes
396+
// some performance overhead, and we don't expect that the assembly name will change nor that we will add many
397+
// more type forwarding cases. Should that be the case we might want to revisit the idea of reading the metadata
398+
// information from the assembly.
399+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
400+
bool IsCoreOrMessageInterfaceAssembly(AssemblyName assemblyName) => string.Equals(assemblyName.Name, CoreAssemblyName, StringComparison.Ordinal) || string.Equals(assemblyName.Name, MessageInterfacesAssemblyName, StringComparison.Ordinal);
401+
390402
internal bool ScanNestedDirectories;
391403
readonly Assembly assemblyToScan;
392404
readonly string baseDirectoryToScan;
393405
HashSet<Type> typesToSkip = [];
394406
HashSet<string> assembliesToSkip = new(StringComparer.OrdinalIgnoreCase);
395407
const string NServiceBusCoreAssemblyName = "NServiceBus.Core";
408+
const string NServiceBusMessageInterfacesAssemblyName = "NServiceBus.MessageInterfaces";
396409

397410
static readonly string[] FileSearchPatternsToUse =
398411
{

0 commit comments

Comments
 (0)