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 <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
using System.Data;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace ZA.CoreService.ESBCertificateManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Stellt sicher, dass optionale Schema-Patches auf bestehenden Datenbanken
|
||||
/// angewendet wurden (z. B. SonicContainer.CertificateCheckUrl).
|
||||
/// </summary>
|
||||
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<bool> 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<bool> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<DeploymentTarget> 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);
|
||||
|
||||
@@ -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<ContainerOption> 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(
|
||||
|
||||
Reference in New Issue
Block a user