From 0a449727ad5e92a69668863822cb529ea7e31b26 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 3 Aug 2026 13:24:20 +0200 Subject: [PATCH] Fix invalid CertificateCheckUrl column errors on existing databases. Auto-migrate SonicContainer schema on startup, rename legacy typo columns, and use fallback SQL when the column is not yet present. Co-authored-by: Cursor --- .../Database/05_AddCertificateCheckUrl.sql | 35 +++- .../Program.cs | 13 ++ .../Services/DatabaseSchemaPatcher.cs | 126 ++++++++++++++ .../Services/DeployTargetRepository.cs | 67 +++++++- .../Services/SonicSetupRepository.cs | 157 ++++++++++++++---- 5 files changed, 362 insertions(+), 36 deletions(-) create mode 100644 ZA.CoreService.ESBCertificateManager/Services/DatabaseSchemaPatcher.cs diff --git a/ZA.CoreService.ESBCertificateManager/Database/05_AddCertificateCheckUrl.sql b/ZA.CoreService.ESBCertificateManager/Database/05_AddCertificateCheckUrl.sql index 96adcab..0edd640 100644 --- a/ZA.CoreService.ESBCertificateManager/Database/05_AddCertificateCheckUrl.sql +++ b/ZA.CoreService.ESBCertificateManager/Database/05_AddCertificateCheckUrl.sql @@ -12,7 +12,38 @@ GO IF COL_LENGTH(N'dbo.SonicContainer', N'CertificateCheckUrl') IS NULL BEGIN - ALTER TABLE [dbo].[SonicContainer] - ADD [CertificateCheckUrl] [nvarchar](500) NULL; + IF COL_LENGTH(N'dbo.SonicContainer', N'vertifiaktechecurl') IS NOT NULL + BEGIN + EXEC sp_rename + N'dbo.SonicContainer.vertifiaktechecurl', + N'CertificateCheckUrl', + N'COLUMN'; + END + ELSE IF COL_LENGTH(N'dbo.SonicContainer', N'VerifyCertificateCheckCurl') IS NOT NULL + BEGIN + EXEC sp_rename + N'dbo.SonicContainer.VerifyCertificateCheckCurl', + N'CertificateCheckUrl', + N'COLUMN'; + END + ELSE IF COL_LENGTH(N'dbo.SonicContainer', N'CertificateCheckCurl') IS NOT NULL + BEGIN + EXEC sp_rename + N'dbo.SonicContainer.CertificateCheckCurl', + N'CertificateCheckUrl', + N'COLUMN'; + END + ELSE IF COL_LENGTH(N'dbo.SonicContainer', N'ZertifikatCheckCurl') IS NOT NULL + BEGIN + EXEC sp_rename + N'dbo.SonicContainer.ZertifikatCheckCurl', + N'CertificateCheckUrl', + N'COLUMN'; + END + ELSE + BEGIN + ALTER TABLE [dbo].[SonicContainer] + ADD [CertificateCheckUrl] [nvarchar](500) NULL; + END END; GO diff --git a/ZA.CoreService.ESBCertificateManager/Program.cs b/ZA.CoreService.ESBCertificateManager/Program.cs index dfef8e0..137486d 100644 --- a/ZA.CoreService.ESBCertificateManager/Program.cs +++ b/ZA.CoreService.ESBCertificateManager/Program.cs @@ -67,6 +67,19 @@ internal static class Program } } + try + { + DatabaseSchemaPatcher + .EnsureCertificateCheckUrlColumnAsync( + settings.Database.ConnectionString) + .GetAwaiter() + .GetResult(); + } + catch + { + // Ohne ALTER-Rechte weiter starten; Repositories nutzen Fallback-SQL. + } + Application.Run( new Form1()); } diff --git a/ZA.CoreService.ESBCertificateManager/Services/DatabaseSchemaPatcher.cs b/ZA.CoreService.ESBCertificateManager/Services/DatabaseSchemaPatcher.cs new file mode 100644 index 0000000..8a6666c --- /dev/null +++ b/ZA.CoreService.ESBCertificateManager/Services/DatabaseSchemaPatcher.cs @@ -0,0 +1,126 @@ +using System.Data; +using Microsoft.Data.SqlClient; + +namespace ZA.CoreService.ESBCertificateManager.Services; + +/// +/// Stellt sicher, dass optionale Schema-Patches auf bestehenden Datenbanken +/// angewendet wurden (z. B. SonicContainer.CertificateCheckUrl). +/// +public static class DatabaseSchemaPatcher +{ + private static readonly string[] LegacyCertificateCheckUrlColumns = + [ + "vertifiaktechecurl", + "VerifyCertificateCheckCurl", + "CertificateCheckCurl", + "ZertifikatCheckCurl" + ]; + + private static bool? _hasCertificateCheckUrlColumn; + + public static async Task EnsureCertificateCheckUrlColumnAsync( + string connectionString, + CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(connectionString)) + { + return; + } + + await using SqlConnection connection = new(connectionString); + await connection.OpenAsync(cancellationToken); + + if (await ColumnExistsAsync( + connection, + "CertificateCheckUrl", + cancellationToken)) + { + _hasCertificateCheckUrlColumn = true; + return; + } + + foreach (string legacyColumn in LegacyCertificateCheckUrlColumns) + { + if (!await ColumnExistsAsync( + connection, + legacyColumn, + cancellationToken)) + { + continue; + } + + await using SqlCommand renameCommand = new( + """ + EXEC sp_rename + N'dbo.SonicContainer.LEGACY_COLUMN', + N'CertificateCheckUrl', + N'COLUMN'; + """ + .Replace( + "LEGACY_COLUMN", + legacyColumn, + StringComparison.Ordinal), + connection); + + await renameCommand.ExecuteNonQueryAsync(cancellationToken); + _hasCertificateCheckUrlColumn = true; + return; + } + + const string addColumnSql = """ + ALTER TABLE [dbo].[SonicContainer] + ADD [CertificateCheckUrl] [nvarchar](500) NULL; + """; + + await using SqlCommand addCommand = new(addColumnSql, connection); + await addCommand.ExecuteNonQueryAsync(cancellationToken); + _hasCertificateCheckUrlColumn = true; + } + + public static async Task HasCertificateCheckUrlColumnAsync( + string connectionString, + CancellationToken cancellationToken = default) + { + if (_hasCertificateCheckUrlColumn is bool cached) + { + return cached; + } + + if (string.IsNullOrWhiteSpace(connectionString)) + { + return false; + } + + await using SqlConnection connection = new(connectionString); + await connection.OpenAsync(cancellationToken); + + _hasCertificateCheckUrlColumn = await ColumnExistsAsync( + connection, + "CertificateCheckUrl", + cancellationToken); + + return _hasCertificateCheckUrlColumn.Value; + } + + private static async Task ColumnExistsAsync( + SqlConnection connection, + string columnName, + CancellationToken cancellationToken) + { + await using SqlCommand command = new( + "SELECT COL_LENGTH(N'dbo.SonicContainer', @ColumnName);", + connection); + + command.Parameters.Add( + new SqlParameter("@ColumnName", SqlDbType.NVarChar, 128) + { + Value = columnName + }); + + object? result = + await command.ExecuteScalarAsync(cancellationToken); + + return result is not null and not DBNull; + } +} diff --git a/ZA.CoreService.ESBCertificateManager/Services/DeployTargetRepository.cs b/ZA.CoreService.ESBCertificateManager/Services/DeployTargetRepository.cs index 6b0cfdf..aff4962 100644 --- a/ZA.CoreService.ESBCertificateManager/Services/DeployTargetRepository.cs +++ b/ZA.CoreService.ESBCertificateManager/Services/DeployTargetRepository.cs @@ -31,7 +31,7 @@ public sealed class DeploymentTargetRepository nameof(environmentCode)); } - const string sql = """ + const string sqlWithCheckUrl = """ SELECT target.[CertificateTargetId], target.[TargetDirectory], @@ -75,11 +75,63 @@ public sealed class DeploymentTargetRepository target.[TargetFileName]; """; + const string sqlWithoutCheckUrl = """ + SELECT + target.[CertificateTargetId], + target.[TargetDirectory], + target.[TargetFileName], + target.[BackupEnabled], + target.[BackupDirectoryName], + target.[IsActive] AS [TargetIsActive], + + container.[SonicContainerId], + container.[ContainerName], + container.[ContainerDisplayName], + container.[RestartTimeoutSeconds], + + connection.[ConnectionName], + + environment.[EnvironmentCode], + environment.[SortOrder] AS [EnvironmentSortOrder], + + company.[CompanyCode] + FROM [dbo].[CertificateTarget] AS target + INNER JOIN [dbo].[SonicContainer] AS container + ON container.[SonicContainerId] = target.[SonicContainerId] + INNER JOIN [dbo].[SonicConnection] AS connection + ON connection.[SonicConnectionId] = container.[SonicConnectionId] + INNER JOIN [dbo].[Environment] AS environment + ON environment.[EnvironmentId] = connection.[EnvironmentId] + INNER JOIN [dbo].[Company] AS company + ON company.[CompanyId] = container.[CompanyId] + WHERE target.[IsActive] = 1 + AND container.[IsActive] = 1 + AND connection.[IsActive] = 1 + AND company.[IsActive] = 1 + AND environment.[IsActive] = 1 + AND environment.[EnvironmentCode] = @EnvironmentCode + ORDER BY + company.[CompanyCode], + environment.[SortOrder], + container.[ContainerName], + target.[TargetDirectory], + target.[TargetFileName]; + """; + List targets = []; await using SqlConnection connection = new(_connectionString); await connection.OpenAsync(cancellationToken); + bool hasCertificateCheckUrl = + await DatabaseSchemaPatcher.HasCertificateCheckUrlColumnAsync( + _connectionString, + cancellationToken); + + string sql = hasCertificateCheckUrl + ? sqlWithCheckUrl + : sqlWithoutCheckUrl; + await using SqlCommand command = new(sql, connection); command.Parameters.Add( @@ -120,8 +172,9 @@ public sealed class DeploymentTargetRepository int containerDisplayNameOrdinal = reader.GetOrdinal("ContainerDisplayName"); - int certificateCheckUrlOrdinal = - reader.GetOrdinal("CertificateCheckUrl"); + int? certificateCheckUrlOrdinal = hasCertificateCheckUrl + ? reader.GetOrdinal("CertificateCheckUrl") + : null; int restartTimeoutOrdinal = reader.GetOrdinal("RestartTimeoutSeconds"); @@ -149,9 +202,11 @@ public sealed class DeploymentTargetRepository containerDisplayNameOrdinal); string? certificateCheckUrl = - ReadNullableString( - reader, - certificateCheckUrlOrdinal); + certificateCheckUrlOrdinal is int checkUrlOrdinal + ? ReadNullableString( + reader, + checkUrlOrdinal) + : null; string companyCode = reader.GetString(companyCodeOrdinal); diff --git a/ZA.CoreService.ESBCertificateManager/Services/SonicSetupRepository.cs b/ZA.CoreService.ESBCertificateManager/Services/SonicSetupRepository.cs index 94e5f76..76f5855 100644 --- a/ZA.CoreService.ESBCertificateManager/Services/SonicSetupRepository.cs +++ b/ZA.CoreService.ESBCertificateManager/Services/SonicSetupRepository.cs @@ -878,7 +878,7 @@ public sealed class SonicSetupRepository nameof(environmentCode)); } - const string sql = """ + const string sqlWithCheckUrl = """ SELECT container.[SonicContainerId], container.[SonicConnectionId], @@ -913,6 +913,40 @@ public sealed class SonicSetupRepository container.[ContainerName]; """; + const string sqlWithoutCheckUrl = """ + SELECT + container.[SonicContainerId], + container.[SonicConnectionId], + container.[CompanyId], + container.[ContainerName], + container.[ContainerDisplayName], + container.[RestartTimeoutSeconds], + company.[CompanyCode], + connection.[ConnectionName] + FROM [dbo].[SonicContainer] AS container + INNER JOIN [dbo].[Company] AS company + ON company.[CompanyId] = container.[CompanyId] + INNER JOIN [dbo].[SonicConnection] AS connection + ON connection.[SonicConnectionId] = + container.[SonicConnectionId] + INNER JOIN [dbo].[Environment] AS environment + ON environment.[EnvironmentId] = + connection.[EnvironmentId] + WHERE container.[IsActive] = 1 + AND company.[IsActive] = 1 + AND connection.[IsActive] = 1 + AND environment.[IsActive] = 1 + AND environment.[EnvironmentCode] = @EnvironmentCode + AND + ( + @SonicConnectionId IS NULL + OR container.[SonicConnectionId] = @SonicConnectionId + ) + ORDER BY + company.[CompanyCode], + container.[ContainerName]; + """; + List containers = []; await using SqlConnection connection = @@ -920,6 +954,15 @@ public sealed class SonicSetupRepository await connection.OpenAsync(cancellationToken); + bool hasCertificateCheckUrl = + await DatabaseSchemaPatcher.HasCertificateCheckUrlColumnAsync( + _connectionString, + cancellationToken); + + string sql = hasCertificateCheckUrl + ? sqlWithCheckUrl + : sqlWithoutCheckUrl; + await using SqlCommand command = new(sql, connection); @@ -952,8 +995,9 @@ public sealed class SonicSetupRepository int displayOrdinal = reader.GetOrdinal("ContainerDisplayName"); - int checkUrlOrdinal = - reader.GetOrdinal("CertificateCheckUrl"); + int? checkUrlOrdinal = hasCertificateCheckUrl + ? reader.GetOrdinal("CertificateCheckUrl") + : null; containers.Add( new ContainerOption @@ -980,9 +1024,10 @@ public sealed class SonicSetupRepository : reader.GetString(displayOrdinal), CertificateCheckUrl = - reader.IsDBNull(checkUrlOrdinal) - ? null - : reader.GetString(checkUrlOrdinal), + checkUrlOrdinal is int urlOrdinal + && !reader.IsDBNull(urlOrdinal) + ? reader.GetString(urlOrdinal) + : null, RestartTimeoutSeconds = reader.GetInt32( @@ -1328,7 +1373,13 @@ public sealed class SonicSetupRepository string? checkUrl = TlsEndpointProbeService.NormalizeCheckUrl(certificateCheckUrl); - const string sql = """ + bool hasCertificateCheckUrl = + await DatabaseSchemaPatcher.HasCertificateCheckUrlColumnAsync( + _connectionString, + cancellationToken); + + string sql = hasCertificateCheckUrl + ? """ INSERT INTO [dbo].[SonicContainer] ( [CompanyId], @@ -1354,6 +1405,31 @@ public sealed class SonicSetupRepository SYSUTCDATETIME(), SUSER_SNAME() ); + """ + : """ + INSERT INTO [dbo].[SonicContainer] + ( + [CompanyId], + [SonicConnectionId], + [ContainerName], + [ContainerDisplayName], + [RestartTimeoutSeconds], + [IsActive], + [CreationDateTime], + [CreatedBy] + ) + OUTPUT INSERTED.[SonicContainerId] + VALUES + ( + @CompanyId, + @SonicConnectionId, + @ContainerName, + @ContainerDisplayName, + @RestartTimeoutSeconds, + 1, + SYSUTCDATETIME(), + SUSER_SNAME() + ); """; await using SqlConnection connection = @@ -1396,16 +1472,19 @@ public sealed class SonicSetupRepository : display }); - command.Parameters.Add( - new SqlParameter( - "@CertificateCheckUrl", - SqlDbType.NVarChar, - 500) - { - Value = checkUrl is null - ? DBNull.Value - : checkUrl - }); + if (hasCertificateCheckUrl) + { + command.Parameters.Add( + new SqlParameter( + "@CertificateCheckUrl", + SqlDbType.NVarChar, + 500) + { + Value = checkUrl is null + ? DBNull.Value + : checkUrl + }); + } command.Parameters.Add( new SqlParameter( @@ -1751,7 +1830,13 @@ public sealed class SonicSetupRepository string? checkUrl = TlsEndpointProbeService.NormalizeCheckUrl(certificateCheckUrl); - const string sql = """ + bool hasCertificateCheckUrl = + await DatabaseSchemaPatcher.HasCertificateCheckUrlColumnAsync( + _connectionString, + cancellationToken); + + string sql = hasCertificateCheckUrl + ? """ UPDATE [dbo].[SonicContainer] SET [CompanyId] = @CompanyId, @@ -1764,6 +1849,19 @@ public sealed class SonicSetupRepository [ModifiedBy] = SUSER_SNAME() WHERE [SonicContainerId] = @SonicContainerId AND [IsActive] = 1; + """ + : """ + UPDATE [dbo].[SonicContainer] + SET + [CompanyId] = @CompanyId, + [SonicConnectionId] = @SonicConnectionId, + [ContainerName] = @ContainerName, + [ContainerDisplayName] = @ContainerDisplayName, + [RestartTimeoutSeconds] = @RestartTimeoutSeconds, + [ModifiedDateTime] = SYSUTCDATETIME(), + [ModifiedBy] = SUSER_SNAME() + WHERE [SonicContainerId] = @SonicContainerId + AND [IsActive] = 1; """; await using SqlConnection connection = @@ -1812,16 +1910,19 @@ public sealed class SonicSetupRepository : display }); - command.Parameters.Add( - new SqlParameter( - "@CertificateCheckUrl", - SqlDbType.NVarChar, - 500) - { - Value = checkUrl is null - ? DBNull.Value - : checkUrl - }); + if (hasCertificateCheckUrl) + { + command.Parameters.Add( + new SqlParameter( + "@CertificateCheckUrl", + SqlDbType.NVarChar, + 500) + { + Value = checkUrl is null + ? DBNull.Value + : checkUrl + }); + } command.Parameters.Add( new SqlParameter(