using EventSubscriber.Application; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace Projection.ReadModel; public static class ServiceCollectionExtensions { /// Register the projection against Postgres. public static IServiceCollection AddProjectionReadModel(this IServiceCollection services, string connectionString) => services.AddDbContext(o => o.UseNpgsql(connectionString)); /// Register the write-side ports (projector store + notification log) used by the Event Subscriber. public static IServiceCollection AddProjectionWriteSide(this IServiceCollection services) { services.AddScoped(); services.AddScoped(); services.AddScoped(); return services; } // A fixed application-scoped key for the migration advisory lock (any stable 64-bit constant). private const long MigrationAdvisoryLockKey = 727501; /// Apply any pending EF migrations. Called once on service start so a fresh stack /// reaches a usable schema without a manual migration step (DoD: compose up reaches green). /// /// The Event Subscriber and the projection-api share this DB and both migrate on start. EF's /// migrations-history lock is released between individual migrations, so with more than one pending /// migration two migrators can interleave and one re-applies a migration the other just did /// ("column already exists"). Hold a Postgres session pg_advisory_lock across the whole /// sequence so it runs exactly once; the second migrator then finds nothing pending. public static async Task MigrateProjectionAsync(this IServiceProvider services, CancellationToken ct = default) { await using var scope = services.CreateAsyncScope(); var db = scope.ServiceProvider.GetRequiredService(); var connection = db.Database.GetDbConnection(); await connection.OpenAsync(ct); try { await ExecuteAsync(connection, $"SELECT pg_advisory_lock({MigrationAdvisoryLockKey})", ct); try { await db.Database.MigrateAsync(ct); } finally { await ExecuteAsync(connection, $"SELECT pg_advisory_unlock({MigrationAdvisoryLockKey})", ct); } } finally { await connection.CloseAsync(); } } private static async Task ExecuteAsync(System.Data.Common.DbConnection connection, string sql, CancellationToken ct) { await using var command = connection.CreateCommand(); command.CommandText = sql; await command.ExecuteNonQueryAsync(ct); } }