diff --git a/src/NServiceBus.Core.Tests/AssemblyScanner/When_exclusion_predicate_is_used.cs b/src/NServiceBus.Core.Tests/AssemblyScanner/When_exclusion_predicate_is_used.cs index 3e9d939e081..be2313775c4 100644 --- a/src/NServiceBus.Core.Tests/AssemblyScanner/When_exclusion_predicate_is_used.cs +++ b/src/NServiceBus.Core.Tests/AssemblyScanner/When_exclusion_predicate_is_used.cs @@ -9,7 +9,6 @@ [TestFixture] public class When_exclusion_predicate_is_used { - [Test] public void No_files_explicitly_excluded_are_returned() { @@ -21,7 +20,7 @@ public void No_files_explicitly_excluded_are_returned() }, ScanAppDomainAssemblies = false } - .GetScannableAssemblies(); + .GetScannableAssemblies(); var skippedFiles = results.SkippedFiles; var explicitlySkippedDll = skippedFiles.FirstOrDefault(s => s.FilePath.Contains("dotNet.dll")); diff --git a/src/NServiceBus.Core.Tests/AssemblyScanner/When_validating_assemblies.cs b/src/NServiceBus.Core.Tests/AssemblyScanner/When_validating_assemblies.cs index 09eb7d03f06..829c86a41e4 100644 --- a/src/NServiceBus.Core.Tests/AssemblyScanner/When_validating_assemblies.cs +++ b/src/NServiceBus.Core.Tests/AssemblyScanner/When_validating_assemblies.cs @@ -10,12 +10,11 @@ public class When_validating_assemblies [Test] public void Should_not_validate_system_assemblies() { - var validator = new AssemblyValidator(); var systemAssemblies = AppDomain.CurrentDomain.GetAssemblies().Where(a => a.FullName.Contains("System")); foreach (var assembly in systemAssemblies) { - validator.ValidateAssemblyFile(assembly.Location, out var shouldLoad, out var reason); + AssemblyValidator.ValidateAssemblyFile(assembly.Location, out var shouldLoad, out var reason); Assert.IsFalse(shouldLoad, $"Should not validate {assembly.FullName}"); Assert.That(reason == "File is a .NET runtime assembly."); @@ -25,9 +24,7 @@ public void Should_not_validate_system_assemblies() [Test] public void Should_validate_NServiceBus_Core_assembly() { - var validator = new AssemblyValidator(); - - validator.ValidateAssemblyFile(typeof(EndpointConfiguration).Assembly.Location, out var shouldLoad, out var reason); + AssemblyValidator.ValidateAssemblyFile(typeof(EndpointConfiguration).Assembly.Location, out var shouldLoad, out var reason); Assert.IsTrue(shouldLoad); Assert.That(reason == "File is a .NET assembly."); @@ -36,9 +33,7 @@ public void Should_validate_NServiceBus_Core_assembly() [Test] public void Should_validate_non_system_assemblies() { - var validator = new AssemblyValidator(); - - validator.ValidateAssemblyFile(GetType().Assembly.Location, out var shouldLoad, out var reason); + AssemblyValidator.ValidateAssemblyFile(GetType().Assembly.Location, out var shouldLoad, out var reason); Assert.IsTrue(shouldLoad); Assert.That(reason == "File is a .NET assembly."); diff --git a/src/NServiceBus.Core.Tests/Config/When_scanning_assemblies.cs b/src/NServiceBus.Core.Tests/Config/When_scanning_assemblies.cs index 1f215a34faf..d74e554a110 100644 --- a/src/NServiceBus.Core.Tests/Config/When_scanning_assemblies.cs +++ b/src/NServiceBus.Core.Tests/Config/When_scanning_assemblies.cs @@ -49,7 +49,7 @@ IEnumerable GetAssembliesInDirectory(string path, params string[] asse if (assembliesToSkip != null) { - assemblyScanner.AssembliesToSkip = assembliesToSkip.ToList(); + assemblyScanner.AssembliesToSkip = assembliesToSkip; } return assemblyScanner .GetScannableAssemblies() diff --git a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanner.cs b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanner.cs index fc704b154da..4e67d73bb98 100644 --- a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanner.cs +++ b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanner.cs @@ -51,7 +51,17 @@ internal AssemblyScanner(Assembly assemblyToScan) /// public bool ScanFileSystemAssemblies { get; set; } = true; - internal string CoreAssemblyName { get; set; } = NServicebusCoreAssemblyName; + internal string CoreAssemblyName { get; set; } = NServiceBusCoreAssemblyName; + + internal IReadOnlyCollection AssembliesToSkip + { + set => assembliesToSkip = new HashSet(value.Select(Path.GetFileNameWithoutExtension), StringComparer.OrdinalIgnoreCase); + } + + internal IReadOnlyCollection TypesToSkip + { + set => typesToSkip = new HashSet(value); + } internal string AdditionalAssemblyScanningPath { get; set; } @@ -156,7 +166,7 @@ bool TryLoadScannableAssembly(string assemblyPath, AssemblyScannerResults result return false; } - assemblyValidator.ValidateAssemblyFile(assemblyPath, out var shouldLoad, out var reason); + AssemblyValidator.ValidateAssemblyFile(assemblyPath, out var shouldLoad, out var reason); if (!shouldLoad) { @@ -248,7 +258,7 @@ internal static string FormatReflectionTypeLoadException(string fileName, Reflec { var sb = new StringBuilder($"Could not enumerate all types for '{fileName}'."); - if (!e.LoaderExceptions.Any()) + if (e.LoaderExceptions.Length == 0) { sb.NewLine($"Exception message: {e}"); return sb.ToString(); @@ -260,7 +270,7 @@ internal static string FormatReflectionTypeLoadException(string fileName, Reflec foreach (var ex in e.LoaderExceptions) { - if (ex is FileLoadException loadException && loadException.FileName != null) + if (ex is FileLoadException { FileName: not null } loadException) { if (!files.Contains(loadException.FileName)) { @@ -297,65 +307,43 @@ static List ScanDirectoryForAssemblyFiles(string directoryToScan, bool var searchOption = scanNestedDirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; foreach (var searchPattern in FileSearchPatternsToUse) { - foreach (var info in baseDir.GetFiles(searchPattern, searchOption)) - { - fileInfo.Add(info); - } + fileInfo.AddRange(baseDir.GetFiles(searchPattern, searchOption)); } return fileInfo; } - bool IsExcluded(string assemblyNameOrFileName) - { - var isExplicitlyExcluded = AssembliesToSkip.Any(excluded => IsMatch(excluded, assemblyNameOrFileName)); - if (isExplicitlyExcluded) - { - return true; - } - - var isExcludedByDefault = DefaultAssemblyExclusions.Any(exclusion => IsMatch(exclusion, assemblyNameOrFileName)); - if (isExcludedByDefault) - { - return true; - } - - return false; - } - - static bool IsMatch(string expression1, string expression2) - { - return DistillLowerAssemblyName(expression1) == DistillLowerAssemblyName(expression2); - } + bool IsExcluded(string assemblyNameOrFileNameWithoutExtension) => + assembliesToSkip.Contains(assemblyNameOrFileNameWithoutExtension) || + DefaultAssemblyExclusions.Contains(assemblyNameOrFileNameWithoutExtension); - bool IsAllowedType(Type type) + // The parameter and return types of this method are deliberately using the most concrete types + // to avoid unnecessary allocations + List FilterAllowedTypes(Type[] types) { - return type != null && - !type.IsValueType && - !IsCompilerGenerated(type) && - !TypesToSkip.Contains(type); - } - - static bool IsCompilerGenerated(Type type) - { - return type.GetCustomAttribute(false) != null; - } - - static string DistillLowerAssemblyName(string assemblyOrFileName) - { - var lowerAssemblyName = assemblyOrFileName.ToLowerInvariant(); - if (lowerAssemblyName.EndsWith(".dll") || lowerAssemblyName.EndsWith(".exe")) + // assume the majority of types will be allowed to preallocate the list + var allowedTypes = new List(types.Length); + foreach (var typeToAdd in types) { - lowerAssemblyName = lowerAssemblyName.Substring(0, lowerAssemblyName.Length - 4); + if (IsAllowedType(typeToAdd)) + { + allowedTypes.Add(typeToAdd); + } } - return lowerAssemblyName; + return allowedTypes; } + bool IsAllowedType(Type type) => + type is { IsValueType: false } && + Attribute.GetCustomAttribute(type, typeof(CompilerGeneratedAttribute), false) == null && + !typesToSkip.Contains(type); + void AddTypesToResult(Assembly assembly, AssemblyScannerResults results) { try { //will throw if assembly cannot be loaded - results.Types.AddRange(assembly.GetTypes().Where(IsAllowedType)); + var types = assembly.GetTypes(); + results.Types.AddRange(FilterAllowedTypes(types)); } catch (ReflectionTypeLoadException e) { @@ -368,7 +356,7 @@ void AddTypesToResult(Assembly assembly, AssemblyScannerResults results) } LogManager.GetLogger().Warn(errorMessage); - results.Types.AddRange(e.Types.Where(IsAllowedType)); + results.Types.AddRange(FilterAllowedTypes(e.Types)); } results.Assemblies.Add(assembly); } @@ -387,26 +375,21 @@ bool ShouldScanDependencies(Assembly assembly) return false; } - if (AssemblyValidator.IsRuntimeAssembly(assemblyName.GetPublicKeyToken())) - { - return false; - } - - if (IsExcluded(assemblyName.Name)) + if (AssemblyValidator.IsRuntimeAssembly(assemblyName)) { return false; } - return true; + // AssemblyName.Name is without the file extension + return !IsExcluded(assemblyName.Name); } - readonly AssemblyValidator assemblyValidator = new(); - internal List AssembliesToSkip = new(); internal bool ScanNestedDirectories; - internal List TypesToSkip = new(); readonly Assembly assemblyToScan; readonly string baseDirectoryToScan; - const string NServicebusCoreAssemblyName = "NServiceBus.Core"; + HashSet typesToSkip = new(); + HashSet assembliesToSkip = new(StringComparer.OrdinalIgnoreCase); + const string NServiceBusCoreAssemblyName = "NServiceBus.Core"; static readonly string[] FileSearchPatternsToUse = { @@ -414,8 +397,7 @@ bool ShouldScanDependencies(Assembly assembly) "*.exe" }; - //TODO: delete when we make message scanning lazy #1617 - static readonly string[] DefaultAssemblyExclusions = + static readonly HashSet DefaultAssemblyExclusions = new(StringComparer.OrdinalIgnoreCase) { // NSB Build-Dependencies "nunit", diff --git a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScannerConfiguration.cs b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScannerConfiguration.cs index 5089637a5c3..2596cfc64f4 100644 --- a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScannerConfiguration.cs +++ b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScannerConfiguration.cs @@ -68,7 +68,7 @@ public void ExcludeTypes(params Type[] types) ExcludedTypes.AddRange(types); } - internal List ExcludedAssemblies { get; } = new List(0); - internal List ExcludedTypes { get; } = new List(0); + internal List ExcludedAssemblies { get; } = new(0); + internal List ExcludedTypes { get; } = new(0); } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningComponent.cs b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningComponent.cs index b1156e2d1f3..573d11a29c6 100644 --- a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningComponent.cs +++ b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningComponent.cs @@ -3,9 +3,9 @@ using System; using System.Collections.Generic; using System.Linq; - using Settings; - using Hosting.Helpers; using System.Reflection; + using Hosting.Helpers; + using Settings; class AssemblyScanningComponent { @@ -67,19 +67,13 @@ public static AssemblyScanningComponent Initialize(Configuration configuration, return new AssemblyScanningComponent(availableTypes); } - AssemblyScanningComponent(List availableTypes) - { - AvailableTypes = availableTypes; - } + AssemblyScanningComponent(List availableTypes) => AvailableTypes = availableTypes; public List AvailableTypes { get; } public class Configuration { - public Configuration(SettingsHolder settings) - { - this.settings = settings; - } + public Configuration(SettingsHolder settings) => this.settings = settings; public List UserProvidedTypes { get; set; } @@ -87,10 +81,7 @@ public Configuration(SettingsHolder settings) public IList AvailableTypes => settings.Get>(TypesToScanSettingsKey); - public void SetDefaultAvailableTypes(IList scannedTypes) - { - settings.SetDefault(TypesToScanSettingsKey, scannedTypes); - } + public void SetDefaultAvailableTypes(IList scannedTypes) => settings.SetDefault(TypesToScanSettingsKey, scannedTypes); readonly SettingsHolder settings; diff --git a/src/NServiceBus.Core/Hosting/Helpers/AssemblyValidator.cs b/src/NServiceBus.Core/Hosting/Helpers/AssemblyValidator.cs index ae028fd192f..de0fc3ab63b 100644 --- a/src/NServiceBus.Core/Hosting/Helpers/AssemblyValidator.cs +++ b/src/NServiceBus.Core/Hosting/Helpers/AssemblyValidator.cs @@ -2,79 +2,75 @@ { using System; using System.IO; + using System.Reflection; using System.Reflection.Metadata; using System.Reflection.PortableExecutable; using System.Security.Cryptography; - class AssemblyValidator + static class AssemblyValidator { - public void ValidateAssemblyFile(string assemblyPath, out bool shouldLoad, out string reason) + public static void ValidateAssemblyFile(string assemblyPath, out bool shouldLoad, out string reason) { - using (var stream = File.OpenRead(assemblyPath)) - using (var file = new PEReader(stream)) + using var stream = File.OpenRead(assemblyPath); + using var file = new PEReader(stream); + var hasMetadata = false; + + try { - var hasMetadata = false; + hasMetadata = file.HasMetadata; + } + catch (BadImageFormatException) { } - try - { - hasMetadata = file.HasMetadata; - } - catch (BadImageFormatException) { } + if (!hasMetadata) + { + shouldLoad = false; + reason = "File is not a .NET assembly."; + return; + } - if (!hasMetadata) - { - shouldLoad = false; - reason = "File is not a .NET assembly."; - return; - } + var reader = file.GetMetadataReader(); + var assemblyDefinition = reader.GetAssemblyDefinition(); - var reader = file.GetMetadataReader(); - var assemblyDefinition = reader.GetAssemblyDefinition(); + if (!assemblyDefinition.PublicKey.IsNil) + { + var publicKey = reader.GetBlobBytes(assemblyDefinition.PublicKey); + var publicKeyToken = GetPublicKeyToken(publicKey); - if (!assemblyDefinition.PublicKey.IsNil) + if (IsRuntimeAssembly(publicKeyToken)) { - var publicKey = reader.GetBlobBytes(assemblyDefinition.PublicKey); - var publicKeyToken = GetPublicKeyToken(publicKey); - - if (IsRuntimeAssembly(publicKeyToken)) - { - shouldLoad = false; - reason = "File is a .NET runtime assembly."; - return; - } + shouldLoad = false; + reason = "File is a .NET runtime assembly."; + return; } - - shouldLoad = true; - reason = "File is a .NET assembly."; } + + shouldLoad = true; + reason = "File is a .NET assembly."; } - static byte[] GetPublicKeyToken(byte[] publicKey) + public static bool IsRuntimeAssembly(AssemblyName assemblyName) { - using (var sha1 = SHA1.Create()) - { - var hash = sha1.ComputeHash(publicKey); - var publicKeyToken = new byte[8]; - - for (var i = 0; i < 8; i++) - { - publicKeyToken[i] = hash[hash.Length - (i + 1)]; - } - - return publicKeyToken; - } + var tokenString = Convert.ToHexString(assemblyName.GetPublicKeyToken() ?? Array.Empty()); + return IsRuntimeAssembly(tokenString); } - public static bool IsRuntimeAssembly(byte[] publicKeyToken) + static string GetPublicKeyToken(byte[] publicKey) { - var tokenString = BitConverter.ToString(publicKeyToken).Replace("-", string.Empty).ToLowerInvariant(); + using var sha1 = SHA1.Create(); + Span publicKeyToken = stackalloc byte[20]; + // returns false when the destination doesn't have enough space + _ = sha1.TryComputeHash(publicKey, publicKeyToken, out _); + Span lastEightBytes = publicKeyToken.Slice(publicKeyToken.Length - 8, 8); + lastEightBytes.Reverse(); + return Convert.ToHexString(lastEightBytes); + } - return tokenString switch + static bool IsRuntimeAssembly(string tokenString) => + tokenString switch { // Microsoft tokens - "b77a5c561934e089" or "7cec85d7bea7798e" or "b03f5f7f11d50a3a" or "31bf3856ad364e35" or "cc7b13ffcd2ddd51" or "adb9793829ddae60" or "7e34167dcc6d6d8c" or "23ec7fc2d6eaa4a5" => true, + "B77A5C561934E089" or "7CEC85D7BEA7798E" or "B03F5F7F11D50A3A" or "31BF3856AD364E35" or "CC7B13FFCD2DDD51" or "ADB9793829DDAE60" or "7E34167DCC6D6D8C" or "23EC7FC2D6EAA4A5" => true, _ => false, }; - } } }