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