using System.Reflection; using Microsoft.Extensions.Logging; namespace GB5Shared.Hosting; public sealed record DiscoveredAssembly(Assembly Assembly, string FileName); public sealed record DiscoveredModule(IModule Module, Assembly OwnerAssembly, string DisplayName); /// /// Finds every GB5 module hosted in this process with no hardcoded list anywhere. A module is /// any public, parameterless-constructible class implementing , found by /// scanning every candidate .dll physically present next to the Host's own executable. Only /// assemblies the Host's .csproj actually references via <ProjectReference> end up copied /// there at publish time, so adding a new module to a Host requires just that one .csproj edit — /// nothing in Program.cs needs to change. /// public static class ModuleDiscovery { /// /// Every GB5 module assembly, without exception, ends in one of these three suffixes /// (CRMSL/CRMBLL/CRMDAL, PayRollSL/PayRollBLL/PayRollDAL, ...). Restricting discovery to this /// naming convention means we never call Assembly.LoadFrom on incidental third-party or legacy /// .NET Framework dependency DLLs that happen to sit in the same output folder (e.g. /// DigestAuthenticationOnWCF.dll, System.Data.SqlClient.dll) - those can reference types that /// don't exist in .NET 9 (old WCF-era assemblies), and merely LOADING them is enough to make /// FastEndpoints' own internal endpoint scan crash the whole host when it later enumerates /// every loaded assembly - excluding them from o.Assemblies alone is not sufficient once they're /// already loaded into the process. /// private static readonly string[] ModuleAssemblySuffixes = ["SL", "BLL", "DAL"]; public static (List Assemblies, List Modules) ScanBaseDirectory(ILogger logger) { var assemblies = new List(); var modules = new List(); var seenModuleTypes = new HashSet(); foreach (var dllPath in Directory.EnumerateFiles(AppContext.BaseDirectory, "*.dll")) { var fileName = Path.GetFileNameWithoutExtension(dllPath); if (!ModuleAssemblySuffixes.Any(suffix => fileName.EndsWith(suffix, StringComparison.Ordinal))) continue; // not a GB5 module assembly by naming convention — never load it at all Assembly assembly; try { assembly = Assembly.LoadFrom(dllPath); } catch { continue; // native/resource-only/non-.NET dll sitting in the output folder — not a candidate } assemblies.Add(new DiscoveredAssembly(assembly, fileName)); Type[] types; try { types = assembly.GetTypes(); } catch (ReflectionTypeLoadException ex) { types = ex.Types.Where(t => t is not null).Cast().ToArray()!; } catch { continue; } foreach (var type in types) { if (type.IsAbstract || type.IsInterface || !typeof(IModule).IsAssignableFrom(type)) continue; if (type.GetConstructor(Type.EmptyTypes) is null) continue; // IModule implementations are parameterless by existing convention if (!seenModuleTypes.Add(type)) continue; IModule instance; try { instance = (IModule)Activator.CreateInstance(type)!; } catch (Exception ex) { logger.LogError(ex, "Module type '{Type}' could not be instantiated - skipped.", type.FullName); continue; } modules.Add(new DiscoveredModule(instance, assembly, instance.DisplayName)); } } return (assemblies, modules); } }