-
Notifications
You must be signed in to change notification settings - Fork 129
Add ability to skip .dacpac deployment #896
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+340
−3
Merged
Changes from 2 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
59ac68b
Add ability to skip deployment if metadata in the target database ind…
ErikEJ cc8b455
Merge branch 'main' into issue-860
ErikEJ 417244d
fix typo
ErikEJ b5ad35c
fix typo
ErikEJ 11bb1f9
fix DI
ErikEJ 983361f
Rename class
ErikEJ b1da6cf
Review feedback
ErikEJ 5972c2d
PR suggestions
ErikEJ e68a24e
exit early
ErikEJ bfe058d
preserve extended properties
ErikEJ 7dbdfee
fix checksum! (after focused smoke testing)
ErikEJ c60dccc
Include predeploy.sql and postdeploy.sql
ErikEJ a01ab4f
fix name
ErikEJ ac5f8eb
PR updates
ErikEJ 68d6d05
fix comment
ErikEJ 872ea99
Merge branch 'main' into issue-860
ErikEJ File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
139 changes: 139 additions & 0 deletions
139
src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacDeploySkipper.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
using Microsoft.Data.SqlClient; | ||
using Microsoft.Extensions.Logging; | ||
using System.Data; | ||
using System.Security.Cryptography; | ||
|
||
namespace CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects; | ||
|
||
internal class DacpacDeploySkipper : IDacpacDeploySkipper | ||
{ | ||
public async Task<string?> CheckIfDeployedAsync(string dacpacPath, string targetConnectionString, ILogger deploymentSkipLogger, CancellationToken cancellationToken) | ||
{ | ||
var targetDatabaseName = GetDatabaseName(targetConnectionString); | ||
|
||
var dacpacId = GetStringChecksum(dacpacPath); | ||
ErikEJ marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
var dacpacChecksum = await GetChecksumAsync(dacpacPath); | ||
|
||
using (var testConnection = new SqlConnection(targetConnectionString)) | ||
{ | ||
try | ||
{ | ||
// Try to connect to the target database to see it exists and fail fast if it does not. | ||
await testConnection.OpenAsync(SqlConnectionOverrides.OpenWithoutRetry, cancellationToken); | ||
} | ||
catch (Exception ex) when (ex is InvalidOperationException || ex is SqlException) | ||
{ | ||
deploymentSkipLogger.LogInformation("Target database {TargetDatabase} is not available.", targetDatabaseName); | ||
ErikEJ marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
return dacpacChecksum; | ||
} | ||
} | ||
|
||
using (var connection = new SqlConnection(targetConnectionString)) | ||
{ | ||
await connection.OpenAsync(SqlConnectionOverrides.OpenWithoutRetry, cancellationToken); | ||
ErikEJ marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
var deployed = await CheckExtendedPropertyAsync(connection, dacpacId, dacpacChecksum, cancellationToken); | ||
|
||
if (deployed) | ||
{ | ||
deploymentSkipLogger.LogInformation("The .dacpac with checksum {DacpacChecksum} has already been deployed to database {TargetDatabaseName}.", dacpacChecksum, targetDatabaseName); | ||
return null; | ||
} | ||
|
||
deploymentSkipLogger.LogInformation("The .dacpac with checksum {DacpacChecksum} has not been deployed to database {TargetDatabaseName}.", dacpacChecksum, targetDatabaseName); | ||
|
||
return dacpacChecksum; | ||
} | ||
} | ||
|
||
public async Task SetChecksumAsync(string dacpacPath, string targetConnectionString, string dacpacChecksum, ILogger deploymentSkipLogger, CancellationToken cancellationToken) | ||
{ | ||
var targetDatabaseName = GetDatabaseName(targetConnectionString); | ||
|
||
var dacpacId = GetStringChecksum(dacpacPath); | ||
|
||
using (var connection = new SqlConnection(targetConnectionString)) | ||
{ | ||
await connection.OpenAsync(SqlConnectionOverrides.OpenWithoutRetry, cancellationToken); | ||
|
||
await UpdateExtendedPropertyAsync(connection, dacpacId, dacpacChecksum, cancellationToken); | ||
|
||
deploymentSkipLogger.LogInformation("The .dacpac with checksum {DacpacChecksum} has been registered in database {TargetDatabaseName}.", dacpacChecksum, targetDatabaseName); | ||
} | ||
} | ||
|
||
private static string GetDatabaseName(string connectionString) | ||
{ | ||
var builder = new SqlConnectionStringBuilder(connectionString); | ||
return builder.InitialCatalog; | ||
} | ||
|
||
private static async Task<string> GetChecksumAsync(string file) | ||
{ | ||
using var stream = File.OpenRead(file); | ||
using var sha = SHA256.Create(); | ||
var checksum = await sha.ComputeHashAsync(stream); | ||
return BitConverter.ToString(checksum).Replace("-", string.Empty); | ||
} | ||
|
||
private static string GetStringChecksum(string text) | ||
{ | ||
var bytes = System.Text.Encoding.UTF8.GetBytes(text); | ||
using var sha = SHA256.Create(); | ||
var checksum = sha.ComputeHash(bytes); | ||
return BitConverter.ToString(checksum).Replace("-", string.Empty); | ||
} | ||
|
||
private static async Task<bool> CheckExtendedPropertyAsync(SqlConnection connection, string dacpacId, string dacpacChecksum, CancellationToken cancellationToken) | ||
{ | ||
var command = new SqlCommand( | ||
@$"SELECT CAST(1 AS BIT) FROM fn_listextendedproperty(NULL, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT) | ||
WHERE [value] = @Expected | ||
AND [name] = @dacpacId;", | ||
connection); | ||
|
||
command.Parameters.AddRange(GetParameters(dacpacChecksum, dacpacId)); | ||
|
||
var result = await command.ExecuteScalarAsync(cancellationToken); | ||
|
||
return result == null ? false : (bool)result; | ||
} | ||
|
||
private static async Task UpdateExtendedPropertyAsync(SqlConnection connection, string dacpacId, string dacpacChecksum, CancellationToken cancellationToken) | ||
{ | ||
var command = new SqlCommand($@" | ||
IF EXISTS | ||
( | ||
SELECT 1 FROM fn_listextendedproperty(null, default, default, default, default, default, default) | ||
WHERE [name] = @dacpacId | ||
) | ||
BEGIN | ||
EXEC sp_updateextendedproperty @name = @dacpacId, @value = @Expected; | ||
END | ||
ELSE | ||
BEGIN | ||
EXEC sp_addextendedproperty @name = @dacpacId, @value = @Expected; | ||
END;", | ||
connection); | ||
|
||
command.Parameters.AddRange(GetParameters(dacpacChecksum, dacpacId)); | ||
|
||
await command.ExecuteNonQueryAsync(cancellationToken); | ||
} | ||
|
||
private static SqlParameter[] GetParameters(string dacpacChecksum, string dacpacId) | ||
{ | ||
return | ||
[ | ||
new SqlParameter("@Expected", SqlDbType.VarChar) | ||
{ | ||
Value = dacpacChecksum | ||
}, | ||
new SqlParameter("@dacpacId", SqlDbType.NVarChar, 128) | ||
{ | ||
Value = dacpacId | ||
}, | ||
]; | ||
} | ||
} |
8 changes: 8 additions & 0 deletions
8
src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacSkipWhenDeployedAnnotation.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
namespace Aspire.Hosting.ApplicationModel; | ||
|
||
/// <summary> | ||
/// Represents a metadata annotation that specifies that .dacpac deployment should be skipped if metadata in the target database indicates that the .dacpac has already been deployed in it's current state. | ||
ErikEJ marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// </summary> | ||
public sealed class DacpacSkipWhenDeployedAnnotation : IResourceAnnotation | ||
{ | ||
} |
30 changes: 30 additions & 0 deletions
30
src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/IDacpacDeploySkipper.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects; | ||
|
||
/// <summary> | ||
/// Abstracts the check of the .dacpac file already having been deployed to the target SQL Server database. | ||
/// </summary> | ||
internal interface IDacpacDeploySkipper | ||
{ | ||
/// <summary> | ||
/// Checks if the <paramref name="dacpacPath">.dacpac</paramref> file has already been deployed to the specified <paramref name="targetConnectionString">SQL Server.</paramref> | ||
/// </summary> | ||
/// <param name="dacpacPath">Path to the .dacpac file to deploy.</param> | ||
/// <param name="targetConnectionString">Connection string to the SQL Server.</param> | ||
/// <param name="deploymentSkipLogger">An <see cref="ILogger" /> to write the log to.</param> | ||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the deployment operation.</param> | ||
/// <returns>the checksum calculated for the .dacpac if it has not been deployed, otherwise null</returns> | ||
Task<string?> CheckIfDeployedAsync(string dacpacPath, string targetConnectionString, ILogger deploymentSkipLogger, CancellationToken cancellationToken); | ||
|
||
/// <summary> | ||
/// Sets the checksum extended property on the target database to indicate that the <paramref name="dacpacPath"> .dacpac</paramref> file has been deployed. | ||
ErikEJ marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
/// </summary> | ||
/// <param name="dacpacPath">Path to the .dacpac file to deploy.</param> | ||
/// <param name="targetConnectionString">Connection string to the SQL Server.</param> | ||
/// <param name="dacpacChecksum">Checksum for the .dacpac </param> | ||
/// <param name="deploymentSkipLogger">An <see cref="ILogger" /> to write the log to.</param> | ||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the deployment operation.</param> | ||
/// <returns></returns> | ||
Task SetChecksumAsync(string dacpacPath, string targetConnectionString, string dacpacChecksum, ILogger deploymentSkipLogger, CancellationToken cancellationToken); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.