using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace ScadaLink.ConfigurationDatabase; /// /// Provides environment-aware migration behavior for the ScadaLink configuration database. /// public static class MigrationHelper { /// /// Applies pending migrations (development mode) or validates schema version (production mode). /// /// The database context to migrate or validate. /// When true, auto-applies migrations. When false, validates schema version matches. /// Optional logger for readiness-wait diagnostics. /// Cancellation token. public static async Task ApplyOrValidateMigrationsAsync( ScadaLinkDbContext dbContext, bool isDevelopment, ILogger? logger = null, CancellationToken cancellationToken = default) { // Wait for the target database to accept connections before invoking MigrateAsync. // On a fresh MSSQL container, user databases recover asynchronously after the server // starts accepting connections — DB_ID(@dbName) returns null until recovery completes. // Without this wait, MigrateAsync sees the database as missing and falls through to // CREATE DATABASE, which fails for non-privileged app logins. await WaitForDatabaseReadyAsync(dbContext, logger, cancellationToken); if (isDevelopment) { await dbContext.Database.MigrateAsync(cancellationToken); } else { var pendingMigrations = await dbContext.Database.GetPendingMigrationsAsync(cancellationToken); var pending = pendingMigrations.ToList(); if (pending.Count > 0) { throw new InvalidOperationException( $"Database schema is out of date. {pending.Count} pending migration(s): {string.Join(", ", pending)}. " + "Apply migrations using 'dotnet ef database update' or the generated SQL scripts before starting in production mode."); } } } private static async Task WaitForDatabaseReadyAsync( ScadaLinkDbContext dbContext, ILogger? logger, CancellationToken cancellationToken) { var timeout = TimeSpan.FromSeconds(60); var pollInterval = TimeSpan.FromSeconds(2); var deadline = DateTimeOffset.UtcNow + timeout; var attempt = 0; Exception? lastException = null; while (DateTimeOffset.UtcNow < deadline) { attempt++; try { if (await dbContext.Database.CanConnectAsync(cancellationToken)) { if (attempt > 1) { logger?.LogInformation( "Configuration database ready after {Attempt} attempt(s).", attempt); } return; } logger?.LogDebug( "Configuration database not yet reachable (attempt {Attempt}).", attempt); } catch (Exception ex) { lastException = ex; logger?.LogDebug(ex, "Configuration database not yet reachable (attempt {Attempt}).", attempt); } await Task.Delay(pollInterval, cancellationToken); } throw new InvalidOperationException( $"Configuration database not ready after {timeout.TotalSeconds:N0}s ({attempt} attempts). " + "Verify SQL Server is running and the configuration database exists and is attached.", lastException); } }