//----------------------------------------------------------------------- // // Copyright (c) aliasvault. All rights reserved. // Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information. // //----------------------------------------------------------------------- namespace AliasServerDb.Configuration; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Npgsql; /// /// Database configuration class. /// public static class DatabaseConfiguration { /// /// Configures SQLite for use with Entity Framework Core. /// /// The IServiceCollection to add the DbContext to. /// The IConfiguration to use for the connection string. /// The IServiceCollection for method chaining. public static IServiceCollection AddAliasVaultDatabaseConfiguration(this IServiceCollection services, IConfiguration configuration) { // Check for environment variables first, then fall back to configuration var connectionString = Environment.GetEnvironmentVariable("ConnectionStrings__AliasServerDbContext"); var dbProvider = Environment.GetEnvironmentVariable("DatabaseProvider")?.ToLower() ?? configuration.GetValue("DatabaseProvider")?.ToLower() ?? "postgresql"; // Create a new configuration if we have environment-provided values if (!string.IsNullOrEmpty(connectionString)) { var configDictionary = new Dictionary { ["ConnectionStrings:AliasServerDbContext"] = connectionString, ["DatabaseProvider"] = dbProvider, }; var configurationBuilder = new ConfigurationBuilder() .AddInMemoryCollection(configDictionary); // Only add the original configuration after our environment variables // This ensures environment variables take precedence configurationBuilder.AddConfiguration(configuration).Build(); } // Add custom DbContextFactory registration which supports multiple database providers // NOTE: previously we looked at the "dbProvider" flag for which factory to initiate, // but as we dropped support for SQLite we now just have this one database provider. services.AddSingleton(); // Updated DbContextFactory registration services.AddDbContextFactory((sp, options) => { var factory = sp.GetRequiredService(); factory.ConfigureDbContextOptions(options); }); // Add scoped DbContext registration based on the factory services.AddScoped(sp => { var factory = sp.GetRequiredService(); return factory.CreateDbContext(); }); return services; } /// /// Waits for the database to be ready by checking if all migrations have been applied. /// This is useful for services that should not run migrations themselves but need to wait /// for another service (typically the API) to complete migrations first. /// /// The database context to check. /// Optional logger for diagnostics. /// Maximum time to wait in seconds (default: 60). /// Interval between checks in milliseconds (default: 2000). /// A task representing the asynchronous operation. public static async Task WaitForDatabaseReadyAsync(this DbContext context, ILogger? logger = null, int timeoutSeconds = 60, int checkIntervalMs = 2000) { var timeout = DateTime.UtcNow.AddSeconds(timeoutSeconds); var attempt = 0; while (DateTime.UtcNow < timeout) { attempt++; try { // First check if database is accessible var canConnect = await context.Database.CanConnectAsync(); if (!canConnect) { logger?.LogInformation( "Database not yet accessible. Attempt {Attempt}. Waiting {Interval}ms...", attempt, checkIntervalMs); await Task.Delay(checkIntervalMs); continue; } // Check if migrations history table exists to avoid PostgreSQL logging errors var connection = context.Database.GetDbConnection(); await using var command = connection.CreateCommand(); command.CommandText = "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = '__EFMigrationsHistory')"; if (connection.State != System.Data.ConnectionState.Open) { await connection.OpenAsync(); } var tableExists = (bool)(await command.ExecuteScalarAsync() ?? false); if (!tableExists) { logger?.LogInformation( "Database accessible but migrations not yet started. Attempt {Attempt}. Waiting {Interval}ms...", attempt, checkIntervalMs); await Task.Delay(checkIntervalMs); continue; } // Now safe to check pending migrations without PostgreSQL logging errors var pendingMigrations = await context.Database.GetPendingMigrationsAsync(); if (!pendingMigrations.Any()) { logger?.LogInformation("Database is ready. All migrations have been applied."); return; } logger?.LogInformation( "Waiting for database migrations to complete. {PendingCount} migrations pending. Attempt {Attempt}.", pendingMigrations.Count(), attempt); } catch (Exception ex) { logger?.LogWarning( ex, "Error checking database status. Attempt {Attempt}. Waiting {Interval}ms before retry...", attempt, checkIntervalMs); } await Task.Delay(checkIntervalMs); } throw new TimeoutException($"Database did not become ready within {timeoutSeconds} seconds. Migrations may not have completed."); } }