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);
}
}