Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AssemblyScanner doesn't scan message assemblies that reference Message Interfaces #7091

Merged
merged 3 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -320,6 +321,7 @@ static AssemblyScanner CreateDefaultAssemblyScanner(DynamicAssembly coreAssembly
new AssemblyScanner(DynamicAssembly.TestAssemblyDirectory)
{
CoreAssemblyName = coreAssembly.DynamicName,
MessageInterfacesAssemblyName = null,
ScanAppDomainAssemblies = true,
ScanFileSystemAssemblies = true,
ThrowExceptions = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
@@ -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");
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Binary file not shown.
Binary file not shown.
17 changes: 15 additions & 2 deletions src/NServiceBus.Core/Hosting/Helpers/AssemblyScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ internal AssemblyScanner(Assembly assemblyToScan)

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

internal string MessageInterfacesAssemblyName { get; set; } = NServiceBusMessageInterfacesAssemblyName;

internal IReadOnlyCollection<string> AssembliesToSkip
{
set => assembliesToSkip = new HashSet<string>(value.Select(RemoveExtension), StringComparer.OrdinalIgnoreCase);
Expand Down Expand Up @@ -224,7 +226,8 @@ bool ScanAssembly(Assembly assembly, Dictionary<string, bool> processed)

processed[assembly.FullName] = false;

if (assembly.GetName().Name == CoreAssemblyName)
var assemblyName = assembly.GetName();
if (IsCoreOrMessageInterfaceAssembly(assemblyName))
{
return processed[assembly.FullName] = true;
}
Expand Down Expand Up @@ -409,7 +412,7 @@ bool ShouldScanDependencies(Assembly assembly)

var assemblyName = assembly.GetName();

if (assemblyName.Name == CoreAssemblyName)
if (IsCoreOrMessageInterfaceAssembly(assemblyName))
{
return false;
}
Expand All @@ -427,13 +430,23 @@ 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;
string baseDirectoryToScan;
HashSet<Type> typesToSkip = new();
HashSet<string> assembliesToSkip = new(StringComparer.OrdinalIgnoreCase);
const string NServiceBusCoreAssemblyName = "NServiceBus.Core";
const string NServiceBusMessageInterfacesAssemblyName = "NServiceBus.MessageInterfaces";

static readonly string[] FileSearchPatternsToUse =
{
Expand Down