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