Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/MigrationHelper.cs
T
Joseph Doherty 7b0b9c7365 refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj,
namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated.
ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated.
SQL roles/logins, LDAP domains, CLI command name, and CLI config dir
(~/.scadalink → ~/.scadabridge) also renamed.

Build green; 5 Host.Tests fail awaiting SQL login rename in next commit.
Pre-existing StaleTagMonitor timing flakes unchanged.

Rename script committed at tools/rename-to-scadabridge.sh.
2026-05-28 09:37:45 -04:00

95 lines
3.9 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
/// <summary>
/// Provides environment-aware migration behavior for the ScadaBridge configuration database.
/// </summary>
public static class MigrationHelper
{
/// <summary>
/// Applies pending migrations (development mode) or validates schema version (production mode).
/// </summary>
/// <param name="dbContext">The database context to migrate or validate.</param>
/// <param name="isDevelopment">When true, auto-applies migrations. When false, validates schema version matches.</param>
/// <param name="logger">Optional logger for readiness-wait diagnostics.</param>
/// <param name="cancellationToken">Cancellation token.</param>
public static async Task ApplyOrValidateMigrationsAsync(
ScadaBridgeDbContext 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(
ScadaBridgeDbContext 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);
}
}