178 lines
7.8 KiB
C#
178 lines
7.8 KiB
C#
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;
|
|
}
|
|
}
|