using BomLocalService.Services.Interfaces; using BomLocalService.Services.Interfaces.Registration; using BomLocalService.Services.Scraping; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using System.Reflection; namespace BomLocalService.Extensions; public static class ServiceCollectionExtensions { /// /// Configures CORS with settings from configuration. /// Configuration values come from appsettings.json (defaults) and can be overridden via environment variables. /// Environment variables use double underscore for nested keys (e.g., CORS__ALLOWEDORIGINS). /// public static IServiceCollection AddCorsConfiguration(this IServiceCollection services, IConfiguration configuration) { var corsOrigins = configuration.GetValue("Cors:AllowedOrigins") ?? throw new InvalidOperationException("Cors:AllowedOrigins configuration is required. Set it in appsettings.json or via CORS__ALLOWEDORIGINS environment variable."); var corsMethods = configuration.GetValue("Cors:AllowedMethods") ?? throw new InvalidOperationException("Cors:AllowedMethods configuration is required. Set it in appsettings.json or via CORS__ALLOWEDMETHODS environment variable."); var corsHeaders = configuration.GetValue("Cors:AllowedHeaders") ?? throw new InvalidOperationException("Cors:AllowedHeaders configuration is required. Set it in appsettings.json or via CORS__ALLOWEDHEADERS environment variable."); // For bool, check if the key exists in configuration (GetValue returns false if not found, which is ambiguous) var corsAllowCredentialsKey = configuration["Cors:AllowCredentials"]; if (corsAllowCredentialsKey == null) { throw new InvalidOperationException("Cors:AllowCredentials configuration is required. Set it in appsettings.json or via CORS__ALLOWCREDENTIALS environment variable."); } var corsAllowCredentials = configuration.GetValue("Cors:AllowCredentials"); services.AddCors(options => { options.AddDefaultPolicy(policy => { if (corsOrigins == "*") { policy.AllowAnyOrigin(); } else { // Split comma-separated origins var origins = corsOrigins.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); policy.WithOrigins(origins); } // Split comma-separated methods var methods = corsMethods.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); policy.WithMethods(methods); // Split comma-separated headers or allow all if (corsHeaders == "*") { policy.AllowAnyHeader(); } else { var headers = corsHeaders.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); policy.WithHeaders(headers); } if (corsAllowCredentials) { policy.AllowCredentials(); } }); }); return services; } /// /// Scans the assembly for services implementing registration interfaces and registers them automatically. /// Services are registered as their concrete interface (that inherits from registration interface). /// Properly handles generic interfaces by using concrete interfaces that hide the generics. /// public static IServiceCollection ScanAndRegisterServices(this IServiceCollection services, Assembly assembly) { var registeredTypes = new HashSet(); // Track registered types to avoid duplicates // Get all types from the assembly with improved filtering var types = GetRegisterableTypes(assembly); // Register Singleton services RegisterServicesByLifetime( services, types, registeredTypes); // Register Scoped services RegisterServicesByLifetime( services, types, registeredTypes); // Register Transient services RegisterServicesByLifetime( services, types, registeredTypes); // Register Hosted Services RegisterHostedServices(services, types, registeredTypes); return services; } /// /// Gets all types that should be considered for registration. /// Filters out abstract classes, interfaces, generic type definitions, nested private types, /// compiler-generated types, and types from excluded namespaces. /// private static List GetRegisterableTypes(Assembly assembly) { return assembly.GetTypes() .Where(t => // Must be a class (not interface, struct, enum, etc.) t.IsClass // Must be concrete (not abstract) && !t.IsAbstract // Must not be a generic type definition (but closed generics are OK) && !t.IsGenericTypeDefinition // Must be public (or nested public in a public type) && (t.IsPublic || (t.IsNestedPublic && t.DeclaringType?.IsPublic == true)) // Must not be compiler-generated (e.g., async state machines, iterator classes) && !t.IsDefined(typeof(System.Runtime.CompilerServices.CompilerGeneratedAttribute), inherit: false) // Must not be a nested private/internal type && !(t.IsNested && !t.IsNestedPublic) // Exclude test namespaces if any (optional - adjust as needed) && !t.Namespace?.StartsWith("BomLocalService.Tests", StringComparison.Ordinal) == true ) .ToList(); } private static void RegisterServicesByLifetime( IServiceCollection services, List types, HashSet registeredTypes) where TRegistrationInterface : class { foreach (var implementationType in types) { // Skip if already registered if (registeredTypes.Contains(implementationType)) continue; // Get all interfaces implemented by this type var allInterfaces = implementationType.GetInterfaces().ToList(); // Find interfaces that inherit from TRegistrationInterface (but not the registration interface itself) var registrationInterfaces = allInterfaces .Where(i => typeof(TRegistrationInterface).IsAssignableFrom(i) && i != typeof(TRegistrationInterface) && !IsGenericTypeDefinition(i)) .ToList(); if (registrationInterfaces.Count == 0) continue; // For each registration interface, find the most specific concrete interface // Priority: concrete interfaces (like IRadarScrapingWorkflow) over generic interfaces (like IWorkflow) foreach (var registrationInterface in registrationInterfaces) { // Skip if this is a generic interface definition (we prefer concrete interfaces) if (registrationInterface.IsGenericTypeDefinition) continue; // Check if this interface is already registered to avoid duplicates var alreadyRegistered = services.Any(s => s.ServiceType == registrationInterface && s.ImplementationType == implementationType); if (alreadyRegistered) continue; // Determine lifetime based on registration interface var lifetime = GetLifetime(); // Register the service by interface if (lifetime == ServiceLifetime.Singleton) { services.AddSingleton(registrationInterface, implementationType); // Also register as concrete type for direct resolution (e.g., for ScrapingStepRegistry) services.AddSingleton(implementationType); } else if (lifetime == ServiceLifetime.Scoped) { services.AddScoped(registrationInterface, implementationType); services.AddScoped(implementationType); } else if (lifetime == ServiceLifetime.Transient) { services.AddTransient(registrationInterface, implementationType); services.AddTransient(implementationType); } registeredTypes.Add(implementationType); } } } private static bool IsGenericTypeDefinition(Type type) { return type.IsGenericTypeDefinition || (type.IsGenericType && type.GetGenericTypeDefinition() == type); } private static ServiceLifetime GetLifetime() where TRegistrationInterface : class { if (typeof(ISingletonService).IsAssignableFrom(typeof(TRegistrationInterface))) return ServiceLifetime.Singleton; if (typeof(IScopedService).IsAssignableFrom(typeof(TRegistrationInterface))) return ServiceLifetime.Scoped; if (typeof(ITransientService).IsAssignableFrom(typeof(TRegistrationInterface))) return ServiceLifetime.Transient; throw new InvalidOperationException($"Unknown registration interface: {typeof(TRegistrationInterface).Name}"); } private static void RegisterHostedServices( IServiceCollection services, List types, HashSet registeredTypes) { foreach (var type in types) { // Skip if already registered if (registeredTypes.Contains(type)) continue; // Check if it's a BackgroundService that implements IHostedServiceRegistration if (typeof(Microsoft.Extensions.Hosting.BackgroundService).IsAssignableFrom(type) && typeof(IHostedServiceRegistration).IsAssignableFrom(type)) { // Check if already registered var alreadyRegistered = services.Any(s => s.ServiceType == typeof(Microsoft.Extensions.Hosting.IHostedService) && s.ImplementationType == type); if (!alreadyRegistered) { // Use AddHostedService via reflection - this is the recommended way // It properly handles lifecycle management for hosted services // AddHostedService is generic, so we need to call it via reflection var addHostedServiceMethod = typeof(Microsoft.Extensions.DependencyInjection.ServiceCollectionHostedServiceExtensions) .GetMethod(nameof(Microsoft.Extensions.DependencyInjection.ServiceCollectionHostedServiceExtensions.AddHostedService), new[] { typeof(IServiceCollection) })! .MakeGenericMethod(type); addHostedServiceMethod.Invoke(null, new object[] { services }); registeredTypes.Add(type); } } } } }