using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Prefab.Data; using Prefab.Data.Seeder; using Prefab.Endpoints; namespace Prefab.Module; /// /// Provides extension methods for registering and configuring Prefab modules and endpoint registrars within an ASP.NET /// Core application. /// /// The Extensions class contains methods intended to be called from the application's Program.cs file to /// integrate Prefab modules into the application's dependency injection and request pipeline. These methods enable /// modular composition by discovering and registering implementations of IModule and IEndpointRegistrar from /// application dependencies. This approach allows modules to participate in both the application's build and /// configuration phases, supporting extensibility and separation of concerns. public static class Extensions { /// /// Adds Prefab modules, endpoint registrars, and related services to the specified WebApplicationBuilder for /// modular application composition. /// /// This method scans application dependencies for types implementing IModule and /// IEndpointRegistrar, registers them as singletons, and invokes their Build methods to allow modules to /// participate in application setup. Call this method early in the application's startup configuration to ensure /// all modules are properly registered and initialized. /// The WebApplicationBuilder to which Prefab modules and services will be added. Cannot be null. /// The same WebApplicationBuilder instance, configured with Prefab modules and services. public static WebApplicationBuilder AddPrefab(this WebApplicationBuilder builder) { builder.Services.AddPrefab(builder.Configuration); // Discover endpoint registrars through Scrutor so modules can contribute HTTP endpoints. builder.Services.Scan(scan => scan .FromApplicationDependencies(assembly => assembly.GetName().Name?.StartsWith(nameof(Prefab)) == true) .AddClasses(classes => classes.AssignableTo(typeof(IEndpointRegistrar))) .As() .WithSingletonLifetime()); // Discover modules via Scrutor and register them as singletons so they can participate in Build/Configure. builder.Services.Scan(scan => scan .FromApplicationDependencies(assembly => assembly.GetName().Name?.StartsWith(nameof(Prefab)) == true) .AddClasses(classes => classes.AssignableTo()) .As() .WithSingletonLifetime()); var modules = new List(); // Build a temporary provider so DI can instantiate modules before the real host is built. // Disposes it immediately but keeps the instances so Build/Configure run on the same objects. using (var provider = builder.Services.BuildServiceProvider(new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true })) { modules.AddRange(provider.GetServices()); } foreach (var module in modules) { module.Build(builder); } builder.Services.RemoveAll(); foreach (var module in modules) { builder.Services.AddSingleton(module); } builder.Services.AddPrefabHandlers(); return builder; } /// /// Configures the application by invoking all registered modules and mapping endpoints using registered endpoint /// registrars. /// /// This method discovers and executes all services implementing and from the application's dependency injection container. Each module's configuration /// and endpoint mapping logic will be applied in the order they are registered. /// The instance to configure. Cannot be null. /// The same instance, enabling method chaining. public static WebApplication UsePrefab(this WebApplication app) { using var scope = app.Services.CreateScope(); var seederTypes = scope.ServiceProvider.GetServices() .Select(seeder => seeder.GetType()) .ToList(); foreach (var type in seederTypes) { var mutexName = $"Global\\PrefabSeeder_{type.FullName}"; var mutex = new Mutex(initiallyOwned: false, mutexName, out _); var acquired = false; try { acquired = mutex.WaitOne(0); } catch (AbandonedMutexException) { acquired = true; } if (!acquired) { mutex.Dispose(); continue; } Prefab.Data.Seeder.Extensions.Channel.Writer.TryWrite(async (_, cancellationToken) => { using var workerScope = app.Services.CreateScope(); var seeder = (ISeeder)workerScope.ServiceProvider.GetRequiredService(type); try { await seeder.Execute(workerScope.ServiceProvider, cancellationToken); } finally { mutex.ReleaseMutex(); mutex.Dispose(); } }); } // Allow each module to run its Configure step foreach (var module in app.Services.GetServices()) { module.Configure(app); } // Allow registered endpoint registrars to map HTTP endpoints. foreach (var registrar in app.Services.GetServices()) { registrar.MapEndpoints(app); } return app; } /// /// Registers all interfaces implemented by the specified DbContext type that derive from IPrefabDb as scoped /// services in the dependency injection container. /// /// Each interface derived from IPrefabDb that is implemented by the specified DbContext type is /// registered as a scoped service, resolving to the same DbContext instance. This ensures that all such interfaces /// share the same context and change tracking within a request scope. /// The IServiceCollection to add the service registrations to. /// The type of the DbContext whose IPrefabDb-derived interfaces will be registered. Must implement one or more /// interfaces that derive from IPrefabDb. /// The IServiceCollection instance with the added service registrations. This enables method chaining. public static IServiceCollection AddModuleDbInterfaces(this IServiceCollection services, Type dbContextType) { var dbContextInterfaces = dbContextType.GetInterfaces() .Where(i => typeof(IPrefabDb).IsAssignableFrom(i)) //.Where(i => typeof(IPrefabDb).IsAssignableFrom(i) || i == typeof(ISagaDb) || i == typeof(IQueueDb)) .ToList(); foreach (var dbContextInterface in dbContextInterfaces) { // resolve module interfaces to the same DbContext instance that was registered // via AddDbContext so that all changes are tracked by a single context services.TryAdd(new ServiceDescriptor( dbContextInterface, sp => sp.GetRequiredService(dbContextType), ServiceLifetime.Scoped)); } return services; } }