This commit is contained in:
2025-10-27 17:39:18 -04:00
commit 31f723bea4
1579 changed files with 642409 additions and 0 deletions

177
Prefab/Module/Extensions.cs Normal file
View File

@@ -0,0 +1,177 @@
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;
/// <summary>
/// Provides extension methods for registering and configuring Prefab modules and endpoint registrars within an ASP.NET
/// Core application.
/// </summary>
/// <remarks>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.</remarks>
public static class Extensions
{
/// <summary>
/// Adds Prefab modules, endpoint registrars, and related services to the specified WebApplicationBuilder for
/// modular application composition.
/// </summary>
/// <remarks>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.</remarks>
/// <param name="builder">The WebApplicationBuilder to which Prefab modules and services will be added. Cannot be null.</param>
/// <returns>The same WebApplicationBuilder instance, configured with Prefab modules and services.</returns>
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<IEndpointRegistrar>()
.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<IModule>())
.As<IModule>()
.WithSingletonLifetime());
var modules = new List<IModule>();
// 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<IModule>());
}
foreach (var module in modules)
{
module.Build(builder);
}
builder.Services.RemoveAll<IModule>();
foreach (var module in modules)
{
builder.Services.AddSingleton(module);
}
builder.Services.AddPrefabHandlers();
return builder;
}
/// <summary>
/// Configures the application by invoking all registered modules and mapping endpoints using registered endpoint
/// registrars.
/// </summary>
/// <remarks>This method discovers and executes all services implementing <see cref="IModule"/> and <see
/// cref="IEndpointRegistrar"/> from the application's dependency injection container. Each module's configuration
/// and endpoint mapping logic will be applied in the order they are registered.</remarks>
/// <param name="app">The <see cref="WebApplication"/> instance to configure. Cannot be null.</param>
/// <returns>The same <see cref="WebApplication"/> instance, enabling method chaining.</returns>
public static WebApplication UsePrefab(this WebApplication app)
{
using var scope = app.Services.CreateScope();
var seederTypes = scope.ServiceProvider.GetServices<ISeeder>()
.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<IModule>())
{
module.Configure(app);
}
// Allow registered endpoint registrars to map HTTP endpoints.
foreach (var registrar in app.Services.GetServices<IEndpointRegistrar>())
{
registrar.MapEndpoints(app);
}
return app;
}
/// <summary>
/// Registers all interfaces implemented by the specified DbContext type that derive from IPrefabDb as scoped
/// services in the dependency injection container.
/// </summary>
/// <remarks>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.</remarks>
/// <param name="services">The IServiceCollection to add the service registrations to.</param>
/// <param name="dbContextType">The type of the DbContext whose IPrefabDb-derived interfaces will be registered. Must implement one or more
/// interfaces that derive from IPrefabDb.</param>
/// <returns>The IServiceCollection instance with the added service registrations. This enables method chaining.</returns>
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;
}
}