From 59ac68bf2bb39e57cb4f4aaeab7b0166991723b6 Mon Sep 17 00:00:00 2001 From: Erik Ejlskov Jensen Date: Mon, 13 Oct 2025 08:38:34 +0200 Subject: [PATCH 01/14] Add ability to skip deployment if metadata in the target database indicates that the database has already been deployed fixes #860 var builder = DistributedApplication.CreateBuilder(args); var server = builder.AddSqlServer("sql") .WithDataVolume("sampledata") .WithLifetime(ContainerLifetime.Persistent); var database = server.AddDatabase("TargetDatabase"); var sdkProject = builder.AddSqlProject("sdk-project") .WithSkipWhenDeployed() .WithReference(database); builder.Build().Run(); --- .../SdkProject/SdkProject.csproj | 2 +- .../DacpacDeploySkipper.cs | 139 ++++++++++++++++++ .../DacpacSkipWhenDeployedAnnotation.cs | 8 + .../IDacpacDeploySkipper.cs | 30 ++++ .../README.md | 27 +++- .../SqlProjectBuilderExtensions.cs | 25 ++++ .../SqlProjectPublishService.cs | 22 ++- .../AddSqlPackageTests.cs | 42 ++++++ .../AddSqlProjectTests.cs | 22 +++ 9 files changed, 314 insertions(+), 3 deletions(-) create mode 100644 src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacDeploySkipper.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacSkipWhenDeployedAnnotation.cs create mode 100644 src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/IDacpacDeploySkipper.cs diff --git a/examples/sql-database-projects/SdkProject/SdkProject.csproj b/examples/sql-database-projects/SdkProject/SdkProject.csproj index d0ba19757..9bd8dbf27 100644 --- a/examples/sql-database-projects/SdkProject/SdkProject.csproj +++ b/examples/sql-database-projects/SdkProject/SdkProject.csproj @@ -1,4 +1,4 @@ - + $(DefaultTargetFramework) Sql150 diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacDeploySkipper.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacDeploySkipper.cs new file mode 100644 index 000000000..be5229e86 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacDeploySkipper.cs @@ -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 CheckIfDeployedAsync(string dacpacPath, string targetConnectionString, ILogger deploymentSkipLogger, CancellationToken cancellationToken) + { + var targetDatabaseName = GetDatabaseName(targetConnectionString); + + var dacpacId = GetStringChecksum(dacpacPath); + + 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); + return dacpacChecksum; + } + } + + using (var connection = new SqlConnection(targetConnectionString)) + { + await connection.OpenAsync(SqlConnectionOverrides.OpenWithoutRetry, cancellationToken); + + 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 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 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 + }, + ]; + } +} diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacSkipWhenDeployedAnnotation.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacSkipWhenDeployedAnnotation.cs new file mode 100644 index 000000000..0a551acd6 --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacSkipWhenDeployedAnnotation.cs @@ -0,0 +1,8 @@ +namespace Aspire.Hosting.ApplicationModel; + +/// +/// 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. +/// +public sealed class DacpacSkipWhenDeployedAnnotation : IResourceAnnotation +{ +} diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/IDacpacDeploySkipper.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/IDacpacDeploySkipper.cs new file mode 100644 index 000000000..0f672f1dc --- /dev/null +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/IDacpacDeploySkipper.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Logging; + +namespace CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects; + +/// +/// Abstracts the check of the .dacpac file already having been deployed to the target SQL Server database. +/// +internal interface IDacpacDeploySkipper +{ + /// + /// Checks if the .dacpac file has already been deployed to the specified SQL Server. + /// + /// Path to the .dacpac file to deploy. + /// Connection string to the SQL Server. + /// An to write the log to. + /// A that can be used to cancel the deployment operation. + /// the checksum calculated for the .dacpac if it has not been deployed, otherwise null + Task CheckIfDeployedAsync(string dacpacPath, string targetConnectionString, ILogger deploymentSkipLogger, CancellationToken cancellationToken); + + /// + /// Sets the checksum extended property on the target database to indicate that the .dacpac file has been deployed. + /// + /// Path to the .dacpac file to deploy. + /// Connection string to the SQL Server. + /// Checksum for the .dacpac + /// An to write the log to. + /// A that can be used to cancel the deployment operation. + /// + Task SetChecksumAsync(string dacpacPath, string targetConnectionString, string dacpacChecksum, ILogger deploymentSkipLogger, CancellationToken cancellationToken); +} diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/README.md b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/README.md index 09e9ffd07..eadb79523 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/README.md +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/README.md @@ -1,7 +1,9 @@ # CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects library + This package provides [.NET Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview) integration for SQL Server Database Projects. It allows you to publish SQL Database Projects as part of your .NET Aspire AppHost projects. It currently works with both [MSBuild.Sdk.SqlProj](https://github.com/rr-wfm/MSBuild.Sdk.SqlProj) and [Microsoft.Build.Sql](https://github.com/microsoft/DacFx) (aka .sqlprojx) based projects. ## Usage + To use this package, install it into your .NET Aspire AppHost project: ```bash @@ -33,6 +35,7 @@ builder.Build().Run(); Now when you run your .NET Aspire AppHost project you will see the SQL Database Project being published to the specified SQL Server. ## Local .dacpac file support + If you are sourcing your .dacpac file from somewhere other than a project reference, you can also specify the path to the .dacpac file directly: ```csharp @@ -49,6 +52,7 @@ builder.Build().Run(); ``` ## Support for existing SQL Server + Instead of using the `AddSqlServer` method to use a SQL Server container, you can specify a connection string to an existing server: ```csharp @@ -64,6 +68,7 @@ builder.Build().Run(); ``` ## Deployment options support + Define options that affect the behavior of package deployment. ```csharp @@ -77,4 +82,24 @@ builder.AddSqlProject("mysqlproj") .WithReference(sql); builder.Build().Run(); -``` \ No newline at end of file +``` + +## Ability to skip deployment + +You can use the `WithSkipWhenDeployed` method to avoid re-deploying your SQL Database Project if no changes have been made. This is useful in scenarios where the SQL container database is persisted to permanent disk and will significantly improve the .NET Aspire AppHost project startup time. + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var server = builder.AddSqlServer("sql") + .WithDataVolume("testdata") + .WithLifetime(ContainerLifetime.Persistent); + +var database = server.AddDatabase("test"); + +var sdkProject = builder.AddSqlProject("mysqlproj") + .WithSkipWhenDeployed() + .WithReference(database); + +builder.Build().Run(); +``` diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectBuilderExtensions.cs index 7d36bff2c..1898a0c52 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectBuilderExtensions.cs @@ -114,6 +114,29 @@ internal static IResourceBuilder InternalWithDacpac(this I return builder.WithAnnotation(new DacpacMetadataAnnotation(dacpacPath)); } + /// + /// 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. + /// + /// An representing the SQL Server Database project. + /// An that can be used to further customize the resource. + public static IResourceBuilder WithSkipWhenDeployed(this IResourceBuilder builder) + => InternalWithSkipWhenDeployed(builder); + + /// + /// 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. + /// + /// An representing the SQL Server Database project. + /// An that can be used to further customize the resource. + public static IResourceBuilder> WithSkipWhenDeloyed(this IResourceBuilder> builder) + where TPackage : IPackageMetadata => InternalWithSkipWhenDeployed(builder); + + + internal static IResourceBuilder InternalWithSkipWhenDeployed(this IResourceBuilder builder) + where TResource : IResourceWithDacpac + { + return builder.WithAnnotation(new DacpacSkipWhenDeployedAnnotation()); + } + /// /// Adds a delegate annotation for configuring dacpac deployment options to the . /// @@ -218,6 +241,8 @@ internal static IResourceBuilder InternalWithReference(thi where TResource : IResourceWithDacpac { builder.ApplicationBuilder.Services.TryAddSingleton(); + builder.ApplicationBuilder.Services.TryAddSingleton(); + builder.ApplicationBuilder.Services.TryAddSingleton(); builder.ApplicationBuilder.Services.TryAddSingleton(); builder.WithParentRelationship(target.Resource); diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs index 0cefd8b19..63e489ec8 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs @@ -5,7 +5,7 @@ namespace CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects; -internal class SqlProjectPublishService(IDacpacDeployer deployer, IHostEnvironment hostEnvironment, ResourceLoggerService resourceLoggerService, ResourceNotificationService resourceNotificationService, IDistributedApplicationEventing eventing, IServiceProvider serviceProvider) +internal class SqlProjectPublishService(IDacpacDeployer deployer, IDacpacDeploySkipper deploySkipper, IHostEnvironment hostEnvironment, ResourceLoggerService resourceLoggerService, ResourceNotificationService resourceNotificationService, IDistributedApplicationEventing eventing, IServiceProvider serviceProvider) { public async Task PublishSqlProject(IResourceWithDacpac resource, IResourceWithConnectionString target, string? targetDatabaseName, CancellationToken cancellationToken) { @@ -42,11 +42,31 @@ await resourceNotificationService.PublishUpdateAsync(resource, return; } + string? checksum = null; + + if (resource.HasAnnotationOfType()) + { + var result = await deploySkipper.CheckIfDeployedAsync(dacpacPath, connectionString, logger, cancellationToken); + if (string.IsNullOrEmpty(result)) + { + await resourceNotificationService.PublishUpdateAsync(resource, + state => state with { State = new ResourceStateSnapshot(KnownResourceStates.Finished, KnownResourceStateStyles.Success) }); + return; + } + + checksum = result; + } + await resourceNotificationService.PublishUpdateAsync(resource, state => state with { State = new ResourceStateSnapshot("Publishing", KnownResourceStateStyles.Info) }); deployer.Deploy(dacpacPath, options, connectionString, targetDatabaseName, logger, cancellationToken); + if (resource.HasAnnotationOfType() && !string.IsNullOrEmpty(checksum)) + { + await deploySkipper.SetChecksumAsync(dacpacPath, connectionString, checksum!, logger, cancellationToken); + } + await resourceNotificationService.PublishUpdateAsync(resource, state => state with { State = new ResourceStateSnapshot(KnownResourceStates.Finished, KnownResourceStateStyles.Success) }); diff --git a/tests/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests/AddSqlPackageTests.cs b/tests/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests/AddSqlPackageTests.cs index 6d12ab027..5962546f9 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests/AddSqlPackageTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests/AddSqlPackageTests.cs @@ -116,4 +116,46 @@ public void AddSqlPackage_WithDeploymentOptions_FromFile() var options = ((IResourceWithDacpac)sqlProjectResource).GetDacpacDeployOptions(); Assert.False(options.BlockOnPossibleDataLoss); } + + [Fact] + public void AddSqlPackage_WithExplicitStart() + { + // Arrange + var appBuilder = DistributedApplication.CreateBuilder(); + var targetDatabase = appBuilder.AddSqlServer("sql").AddDatabase("test"); + appBuilder.AddSqlPackage("chinook") + .WithReference(targetDatabase) + .WithExplicitStart(); + + // Act + using var app = appBuilder.Build(); + var appModel = app.Services.GetRequiredService(); + + // Assert + var sqlProjectResource = Assert.Single(appModel.Resources.OfType>()); + Assert.Equal("chinook", sqlProjectResource.Name); + + Assert.True(sqlProjectResource.HasAnnotationOfType()); + } + + [Fact] + public void AddSqlPackage_WithSkipWhenDeployed() + { + // Arrange + var appBuilder = DistributedApplication.CreateBuilder(); + var targetDatabase = appBuilder.AddSqlServer("sql").AddDatabase("test"); + appBuilder.AddSqlPackage("chinook") + .WithReference(targetDatabase) + .WithSkipWhenDeloyed(); + + // Act + using var app = appBuilder.Build(); + var appModel = app.Services.GetRequiredService(); + + // Assert + var sqlProjectResource = Assert.Single(appModel.Resources.OfType>()); + Assert.Equal("chinook", sqlProjectResource.Name); + + Assert.True(sqlProjectResource.HasAnnotationOfType()); + } } \ No newline at end of file diff --git a/tests/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests/AddSqlProjectTests.cs b/tests/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests/AddSqlProjectTests.cs index 4cb105448..e5b822bd0 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests/AddSqlProjectTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests/AddSqlProjectTests.cs @@ -158,6 +158,7 @@ public void WithReference_AddsRequiredServices() // Assert Assert.Single(app.Services.GetServices()); Assert.Single(app.Services.GetServices()); + Assert.Single(app.Services.GetServices()); } [Fact] @@ -180,4 +181,25 @@ public void AddSqlProject_WithExplicitStart() Assert.True(sqlProjectResource.HasAnnotationOfType()); } + + [Fact] + public void AddSqlProject_WithSkipWhenDeployed() + { + // Arrange + var appBuilder = DistributedApplication.CreateBuilder(); + var targetDatabase = appBuilder.AddSqlServer("sql").AddDatabase("test"); + appBuilder.AddSqlProject("MySqlProject") + .WithReference(targetDatabase) + .WithSkipWhenDeployed(); + + // Act + using var app = appBuilder.Build(); + var appModel = app.Services.GetRequiredService(); + + // Assert + var sqlProjectResource = Assert.Single(appModel.Resources.OfType()); + Assert.Equal("MySqlProject", sqlProjectResource.Name); + + Assert.True(sqlProjectResource.HasAnnotationOfType()); + } } From 417244d14fd79242e9dea4967d75fd2c055f32fa Mon Sep 17 00:00:00 2001 From: Erik Ejlskov Jensen Date: Mon, 13 Oct 2025 08:46:45 +0200 Subject: [PATCH 02/14] fix typo --- .../SqlProjectBuilderExtensions.cs | 2 +- .../AddSqlPackageTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectBuilderExtensions.cs index 1898a0c52..0e3e6740e 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectBuilderExtensions.cs @@ -127,7 +127,7 @@ public static IResourceBuilder WithSkipWhenDeployed(this IRe /// /// An representing the SQL Server Database project. /// An that can be used to further customize the resource. - public static IResourceBuilder> WithSkipWhenDeloyed(this IResourceBuilder> builder) + public static IResourceBuilder> WithSkipWhenDeployed(this IResourceBuilder> builder) where TPackage : IPackageMetadata => InternalWithSkipWhenDeployed(builder); diff --git a/tests/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests/AddSqlPackageTests.cs b/tests/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests/AddSqlPackageTests.cs index 5962546f9..c594ed63f 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests/AddSqlPackageTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests/AddSqlPackageTests.cs @@ -146,7 +146,7 @@ public void AddSqlPackage_WithSkipWhenDeployed() var targetDatabase = appBuilder.AddSqlServer("sql").AddDatabase("test"); appBuilder.AddSqlPackage("chinook") .WithReference(targetDatabase) - .WithSkipWhenDeloyed(); + .WithSkipWhenDeployed(); // Act using var app = appBuilder.Build(); From b5ad35cd1416bcdeb328a4b12213cc5da3266a81 Mon Sep 17 00:00:00 2001 From: Erik Ejlskov Jensen Date: Mon, 13 Oct 2025 08:48:52 +0200 Subject: [PATCH 03/14] fix typo --- .../SqlProjectBuilderExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectBuilderExtensions.cs index 0e3e6740e..a3f55263c 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectBuilderExtensions.cs @@ -115,7 +115,7 @@ internal static IResourceBuilder InternalWithDacpac(this I } /// - /// 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. + /// Specifies that .dacpac deployment should be skipped if metadata in the target database indicates that the .dacpac has already been deployed in its current state. /// /// An representing the SQL Server Database project. /// An that can be used to further customize the resource. @@ -123,7 +123,7 @@ public static IResourceBuilder WithSkipWhenDeployed(this IRe => InternalWithSkipWhenDeployed(builder); /// - /// 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. + /// Specifies that .dacpac deployment should be skipped if metadata in the target database indicates that the .dacpac has already been deployed in its current state. /// /// An representing the SQL Server Database project. /// An that can be used to further customize the resource. From 11bb1f963fc329d1343e580ff45c6172ae5c6ab2 Mon Sep 17 00:00:00 2001 From: Erik Ejlskov Jensen Date: Mon, 13 Oct 2025 09:10:58 +0200 Subject: [PATCH 04/14] fix DI --- .../SqlProjectBuilderExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectBuilderExtensions.cs index a3f55263c..ed5f37c64 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectBuilderExtensions.cs @@ -242,7 +242,6 @@ internal static IResourceBuilder InternalWithReference(thi { builder.ApplicationBuilder.Services.TryAddSingleton(); builder.ApplicationBuilder.Services.TryAddSingleton(); - builder.ApplicationBuilder.Services.TryAddSingleton(); builder.ApplicationBuilder.Services.TryAddSingleton(); builder.WithParentRelationship(target.Resource); From 983361f9129d52b26ecf521ea775b4b5acd414a9 Mon Sep 17 00:00:00 2001 From: Erik Ejlskov Jensen Date: Tue, 14 Oct 2025 10:40:19 +0200 Subject: [PATCH 05/14] Rename class --- .../{DacpacDeploySkipper.cs => DacpacChecksumService.cs} | 2 +- .../{IDacpacDeploySkipper.cs => IDacpacChecksumService.cs} | 2 +- .../SqlProjectBuilderExtensions.cs | 2 +- .../SqlProjectPublishService.cs | 2 +- .../AddSqlProjectTests.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/{DacpacDeploySkipper.cs => DacpacChecksumService.cs} (98%) rename src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/{IDacpacDeploySkipper.cs => IDacpacChecksumService.cs} (97%) diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacDeploySkipper.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacChecksumService.cs similarity index 98% rename from src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacDeploySkipper.cs rename to src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacChecksumService.cs index be5229e86..f44c0fb20 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacDeploySkipper.cs +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacChecksumService.cs @@ -5,7 +5,7 @@ namespace CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects; -internal class DacpacDeploySkipper : IDacpacDeploySkipper +internal class DacpacChecksumService : IDacpacChecksumService { public async Task CheckIfDeployedAsync(string dacpacPath, string targetConnectionString, ILogger deploymentSkipLogger, CancellationToken cancellationToken) { diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/IDacpacDeploySkipper.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/IDacpacChecksumService.cs similarity index 97% rename from src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/IDacpacDeploySkipper.cs rename to src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/IDacpacChecksumService.cs index 0f672f1dc..042b76c98 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/IDacpacDeploySkipper.cs +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/IDacpacChecksumService.cs @@ -5,7 +5,7 @@ namespace CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects; /// /// Abstracts the check of the .dacpac file already having been deployed to the target SQL Server database. /// -internal interface IDacpacDeploySkipper +internal interface IDacpacChecksumService { /// /// Checks if the .dacpac file has already been deployed to the specified SQL Server. diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectBuilderExtensions.cs index ed5f37c64..3aa0c29dd 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectBuilderExtensions.cs @@ -241,7 +241,7 @@ internal static IResourceBuilder InternalWithReference(thi where TResource : IResourceWithDacpac { builder.ApplicationBuilder.Services.TryAddSingleton(); - builder.ApplicationBuilder.Services.TryAddSingleton(); + builder.ApplicationBuilder.Services.TryAddSingleton(); builder.ApplicationBuilder.Services.TryAddSingleton(); builder.WithParentRelationship(target.Resource); diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs index 63e489ec8..1dfc901c1 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs @@ -5,7 +5,7 @@ namespace CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects; -internal class SqlProjectPublishService(IDacpacDeployer deployer, IDacpacDeploySkipper deploySkipper, IHostEnvironment hostEnvironment, ResourceLoggerService resourceLoggerService, ResourceNotificationService resourceNotificationService, IDistributedApplicationEventing eventing, IServiceProvider serviceProvider) +internal class SqlProjectPublishService(IDacpacDeployer deployer, IDacpacChecksumService deploySkipper, IHostEnvironment hostEnvironment, ResourceLoggerService resourceLoggerService, ResourceNotificationService resourceNotificationService, IDistributedApplicationEventing eventing, IServiceProvider serviceProvider) { public async Task PublishSqlProject(IResourceWithDacpac resource, IResourceWithConnectionString target, string? targetDatabaseName, CancellationToken cancellationToken) { diff --git a/tests/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests/AddSqlProjectTests.cs b/tests/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests/AddSqlProjectTests.cs index e5b822bd0..4a0d14b30 100644 --- a/tests/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests/AddSqlProjectTests.cs +++ b/tests/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests/AddSqlProjectTests.cs @@ -158,7 +158,7 @@ public void WithReference_AddsRequiredServices() // Assert Assert.Single(app.Services.GetServices()); Assert.Single(app.Services.GetServices()); - Assert.Single(app.Services.GetServices()); + Assert.Single(app.Services.GetServices()); } [Fact] From b1da6cf41a25ada23da0ab0996369b7d62b3e523 Mon Sep 17 00:00:00 2001 From: Erik Ejlskov Jensen Date: Tue, 14 Oct 2025 10:49:17 +0200 Subject: [PATCH 06/14] Review feedback --- .../DacpacChecksumService.cs | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacChecksumService.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacChecksumService.cs index f44c0fb20..961cb3311 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacChecksumService.cs +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacChecksumService.cs @@ -11,29 +11,24 @@ internal class DacpacChecksumService : IDacpacChecksumService { var targetDatabaseName = GetDatabaseName(targetConnectionString); - var dacpacId = GetStringChecksum(dacpacPath); + var dacpacPathChecksum = GetStringChecksum(dacpacPath); var dacpacChecksum = await GetChecksumAsync(dacpacPath); - using (var testConnection = new SqlConnection(targetConnectionString)) + using (var connection = 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); + await connection.OpenAsync(SqlConnectionOverrides.OpenWithoutRetry, cancellationToken); } catch (Exception ex) when (ex is InvalidOperationException || ex is SqlException) { - deploymentSkipLogger.LogInformation("Target database {TargetDatabase} is not available.", targetDatabaseName); + deploymentSkipLogger.LogWarning(ex, "Target database {TargetDatabase} is not available.", targetDatabaseName); return dacpacChecksum; } - } - - using (var connection = new SqlConnection(targetConnectionString)) - { - await connection.OpenAsync(SqlConnectionOverrides.OpenWithoutRetry, cancellationToken); - - var deployed = await CheckExtendedPropertyAsync(connection, dacpacId, dacpacChecksum, cancellationToken); + + var deployed = await CheckExtendedPropertyAsync(connection, dacpacPathChecksum, dacpacChecksum, cancellationToken); if (deployed) { @@ -51,13 +46,13 @@ public async Task SetChecksumAsync(string dacpacPath, string targetConnectionStr { var targetDatabaseName = GetDatabaseName(targetConnectionString); - var dacpacId = GetStringChecksum(dacpacPath); + var dacpacPathChecksum = GetStringChecksum(dacpacPath); using (var connection = new SqlConnection(targetConnectionString)) { await connection.OpenAsync(SqlConnectionOverrides.OpenWithoutRetry, cancellationToken); - await UpdateExtendedPropertyAsync(connection, dacpacId, dacpacChecksum, cancellationToken); + await UpdateExtendedPropertyAsync(connection, dacpacPathChecksum, dacpacChecksum, cancellationToken); deploymentSkipLogger.LogInformation("The .dacpac with checksum {DacpacChecksum} has been registered in database {TargetDatabaseName}.", dacpacChecksum, targetDatabaseName); } @@ -85,7 +80,7 @@ private static string GetStringChecksum(string text) return BitConverter.ToString(checksum).Replace("-", string.Empty); } - private static async Task CheckExtendedPropertyAsync(SqlConnection connection, string dacpacId, string dacpacChecksum, CancellationToken cancellationToken) + private static async Task CheckExtendedPropertyAsync(SqlConnection connection, string dacpacPathChecksum, string dacpacChecksum, CancellationToken cancellationToken) { var command = new SqlCommand( @$"SELECT CAST(1 AS BIT) FROM fn_listextendedproperty(NULL, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT) @@ -93,14 +88,14 @@ private static async Task CheckExtendedPropertyAsync(SqlConnection connect AND [name] = @dacpacId;", connection); - command.Parameters.AddRange(GetParameters(dacpacChecksum, dacpacId)); + command.Parameters.AddRange(GetParameters(dacpacChecksum, dacpacPathChecksum)); 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) + private static async Task UpdateExtendedPropertyAsync(SqlConnection connection, string dacpacPathChecksum, string dacpacChecksum, CancellationToken cancellationToken) { var command = new SqlCommand($@" IF EXISTS @@ -117,12 +112,12 @@ SELECT 1 FROM fn_listextendedproperty(null, default, default, default, default, END;", connection); - command.Parameters.AddRange(GetParameters(dacpacChecksum, dacpacId)); + command.Parameters.AddRange(GetParameters(dacpacChecksum, dacpacPathChecksum)); await command.ExecuteNonQueryAsync(cancellationToken); } - private static SqlParameter[] GetParameters(string dacpacChecksum, string dacpacId) + private static SqlParameter[] GetParameters(string dacpacChecksum, string dacpacPathChecksum) { return [ @@ -132,7 +127,7 @@ private static SqlParameter[] GetParameters(string dacpacChecksum, string dacpac }, new SqlParameter("@dacpacId", SqlDbType.NVarChar, 128) { - Value = dacpacId + Value = dacpacPathChecksum }, ]; } From 5972c2d46790f61a9c1672c88df30077eca83c3e Mon Sep 17 00:00:00 2001 From: Erik Ejlskov Jensen Date: Tue, 14 Oct 2025 10:55:38 +0200 Subject: [PATCH 07/14] PR suggestions --- .../IDacpacChecksumService.cs | 2 +- .../SqlProjectPublishService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/IDacpacChecksumService.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/IDacpacChecksumService.cs index 042b76c98..30a29c020 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/IDacpacChecksumService.cs +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/IDacpacChecksumService.cs @@ -18,7 +18,7 @@ internal interface IDacpacChecksumService Task CheckIfDeployedAsync(string dacpacPath, string targetConnectionString, ILogger deploymentSkipLogger, CancellationToken cancellationToken); /// - /// Sets the checksum extended property on the target database to indicate that the .dacpac file has been deployed. + /// Sets the checksum extended property on the target database to indicate that the .dacpac file has been deployed. /// /// Path to the .dacpac file to deploy. /// Connection string to the SQL Server. diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs index 1dfc901c1..fbee1e5cd 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs @@ -64,7 +64,7 @@ await resourceNotificationService.PublishUpdateAsync(resource, if (resource.HasAnnotationOfType() && !string.IsNullOrEmpty(checksum)) { - await deploySkipper.SetChecksumAsync(dacpacPath, connectionString, checksum!, logger, cancellationToken); + await deploySkipper.SetChecksumAsync(dacpacPath, connectionString, checksum, logger, cancellationToken); } await resourceNotificationService.PublishUpdateAsync(resource, From e68a24e0718e1ed680fe6ae3fbfa93778c42d379 Mon Sep 17 00:00:00 2001 From: Erik Ejlskov Jensen Date: Tue, 14 Oct 2025 11:18:27 +0200 Subject: [PATCH 08/14] exit early --- .../SqlProjectPublishService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs index fbee1e5cd..f31f003d0 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs @@ -62,7 +62,7 @@ await resourceNotificationService.PublishUpdateAsync(resource, deployer.Deploy(dacpacPath, options, connectionString, targetDatabaseName, logger, cancellationToken); - if (resource.HasAnnotationOfType() && !string.IsNullOrEmpty(checksum)) + if (!string.IsNullOrEmpty(checksum) && resource.HasAnnotationOfType()) { await deploySkipper.SetChecksumAsync(dacpacPath, connectionString, checksum, logger, cancellationToken); } From bfe058d90e8171d71bc5064bc971ccd1e974ec2d Mon Sep 17 00:00:00 2001 From: Erik Ejlskov Jensen Date: Tue, 14 Oct 2025 14:38:52 +0200 Subject: [PATCH 09/14] preserve extended properties --- .../SqlProjectPublishService.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs index f31f003d0..c7f28d31f 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs @@ -46,6 +46,8 @@ await resourceNotificationService.PublishUpdateAsync(resource, if (resource.HasAnnotationOfType()) { + options.DropExtendedPropertiesNotInSource = false; + var result = await deploySkipper.CheckIfDeployedAsync(dacpacPath, connectionString, logger, cancellationToken); if (string.IsNullOrEmpty(result)) { From 7dbdfeeb36ccdd73633841347e90ec23478cf756 Mon Sep 17 00:00:00 2001 From: Erik Ejlskov Jensen Date: Tue, 14 Oct 2025 16:56:32 +0200 Subject: [PATCH 10/14] fix checksum! (after focused smoke testing) --- .../DacpacChecksumService.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacChecksumService.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacChecksumService.cs index 961cb3311..775195b3d 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacChecksumService.cs +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacChecksumService.cs @@ -66,9 +66,24 @@ private static string GetDatabaseName(string connectionString) private static async Task GetChecksumAsync(string file) { - using var stream = File.OpenRead(file); + var output = Path.Join(Path.GetTempPath(), Path.GetRandomFileName()); + + System.IO.Compression.ZipFile.ExtractToDirectory(file, output); + + using var stream = File.OpenRead(Path.Join(output, "model.xml")); using var sha = SHA256.Create(); var checksum = await sha.ComputeHashAsync(stream); + + // Clean up the extracted files + try + { + Directory.Delete(output, true); + } + catch + { + // Ignore any errors during cleanup + } + return BitConverter.ToString(checksum).Replace("-", string.Empty); } From c60dccc4f55d0c5ce368dfa6636a3d530afb7d62 Mon Sep 17 00:00:00 2001 From: Erik Ejlskov Jensen Date: Wed, 15 Oct 2025 12:12:12 +0200 Subject: [PATCH 11/14] Include predeploy.sql and postdeploy.sql --- .../DacpacChecksumService.cs | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacChecksumService.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacChecksumService.cs index 775195b3d..317162d49 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacChecksumService.cs +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacChecksumService.cs @@ -70,9 +70,26 @@ private static async Task GetChecksumAsync(string file) System.IO.Compression.ZipFile.ExtractToDirectory(file, output); - using var stream = File.OpenRead(Path.Join(output, "model.xml")); + var bytes = await File.ReadAllBytesAsync(Path.Join(output, "model.xml")); + + var predeployPath = Path.Join(output, "predeploy.sql"); + + if (File.Exists(predeployPath)) + { + var predeployBytes = await File.ReadAllBytesAsync(predeployPath); + bytes = bytes.Concat(predeployBytes).ToArray(); + } + + var postdeployPath = Path.Join(output, "postdeploy.sql"); + + if (File.Exists(postdeployPath)) + { + var postdeployBytes = await File.ReadAllBytesAsync(postdeployPath); + bytes = bytes.Concat(postdeployBytes).ToArray(); + } + using var sha = SHA256.Create(); - var checksum = await sha.ComputeHashAsync(stream); + var checksum = sha.ComputeHash(bytes); // Clean up the extracted files try From a01ab4f6f6235118ba4f2a20e7dc1a5bfcc48cd7 Mon Sep 17 00:00:00 2001 From: Erik Ejlskov Jensen Date: Mon, 20 Oct 2025 14:16:47 +0200 Subject: [PATCH 12/14] fix name --- .../SqlProjectPublishService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs index c7f28d31f..1ef7486ae 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs @@ -5,7 +5,7 @@ namespace CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects; -internal class SqlProjectPublishService(IDacpacDeployer deployer, IDacpacChecksumService deploySkipper, IHostEnvironment hostEnvironment, ResourceLoggerService resourceLoggerService, ResourceNotificationService resourceNotificationService, IDistributedApplicationEventing eventing, IServiceProvider serviceProvider) +internal class SqlProjectPublishService(IDacpacDeployer deployer, IDacpacChecksumService dacpacChecksumService, IHostEnvironment hostEnvironment, ResourceLoggerService resourceLoggerService, ResourceNotificationService resourceNotificationService, IDistributedApplicationEventing eventing, IServiceProvider serviceProvider) { public async Task PublishSqlProject(IResourceWithDacpac resource, IResourceWithConnectionString target, string? targetDatabaseName, CancellationToken cancellationToken) { @@ -48,7 +48,7 @@ await resourceNotificationService.PublishUpdateAsync(resource, { options.DropExtendedPropertiesNotInSource = false; - var result = await deploySkipper.CheckIfDeployedAsync(dacpacPath, connectionString, logger, cancellationToken); + var result = await dacpacChecksumService.CheckIfDeployedAsync(dacpacPath, connectionString, logger, cancellationToken); if (string.IsNullOrEmpty(result)) { await resourceNotificationService.PublishUpdateAsync(resource, @@ -66,7 +66,7 @@ await resourceNotificationService.PublishUpdateAsync(resource, if (!string.IsNullOrEmpty(checksum) && resource.HasAnnotationOfType()) { - await deploySkipper.SetChecksumAsync(dacpacPath, connectionString, checksum, logger, cancellationToken); + await dacpacChecksumService.SetChecksumAsync(dacpacPath, connectionString, checksum, logger, cancellationToken); } await resourceNotificationService.PublishUpdateAsync(resource, From ac5f8eb1f0df27ca5bb11ccfb36207bc225097d6 Mon Sep 17 00:00:00 2001 From: Erik Ejlskov Jensen Date: Mon, 20 Oct 2025 14:29:19 +0200 Subject: [PATCH 13/14] PR updates --- .../DacpacChecksumService.cs | 58 +++++++++---------- .../IDacpacChecksumService.cs | 6 +- .../SqlProjectPublishService.cs | 2 +- 3 files changed, 32 insertions(+), 34 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacChecksumService.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacChecksumService.cs index 317162d49..30a244433 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacChecksumService.cs +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacChecksumService.cs @@ -7,55 +7,53 @@ namespace CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects; internal class DacpacChecksumService : IDacpacChecksumService { - public async Task CheckIfDeployedAsync(string dacpacPath, string targetConnectionString, ILogger deploymentSkipLogger, CancellationToken cancellationToken) + public async Task CheckIfDeployedAsync(string dacpacPath, string targetConnectionString, ILogger logger, CancellationToken cancellationToken) { var targetDatabaseName = GetDatabaseName(targetConnectionString); var dacpacPathChecksum = GetStringChecksum(dacpacPath); var dacpacChecksum = await GetChecksumAsync(dacpacPath); - - using (var connection = new SqlConnection(targetConnectionString)) + + using var connection = new SqlConnection(targetConnectionString); + + try { - try - { - // Try to connect to the target database to see it exists and fail fast if it does not. - await connection.OpenAsync(SqlConnectionOverrides.OpenWithoutRetry, cancellationToken); - } - catch (Exception ex) when (ex is InvalidOperationException || ex is SqlException) - { - deploymentSkipLogger.LogWarning(ex, "Target database {TargetDatabase} is not available.", targetDatabaseName); - return dacpacChecksum; - } + // Try to connect to the target database to see it exists and fail fast if it does not. + await connection.OpenAsync(SqlConnectionOverrides.OpenWithoutRetry, cancellationToken); + } + catch (Exception ex) when (ex is InvalidOperationException || ex is SqlException) + { + logger.LogWarning(ex, "Target database {TargetDatabase} is not available.", targetDatabaseName); + return dacpacChecksum; + } - var deployed = await CheckExtendedPropertyAsync(connection, dacpacPathChecksum, dacpacChecksum, cancellationToken); + var deployed = await CheckExtendedPropertyAsync(connection, dacpacPathChecksum, dacpacChecksum, cancellationToken); - if (deployed) - { - deploymentSkipLogger.LogInformation("The .dacpac with checksum {DacpacChecksum} has already been deployed to database {TargetDatabaseName}.", dacpacChecksum, targetDatabaseName); - return null; - } + if (deployed) + { + logger.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); + logger.LogInformation("The .dacpac with checksum {DacpacChecksum} has not been deployed to database {TargetDatabaseName}.", dacpacChecksum, targetDatabaseName); - return dacpacChecksum; - } + return dacpacChecksum; } - public async Task SetChecksumAsync(string dacpacPath, string targetConnectionString, string dacpacChecksum, ILogger deploymentSkipLogger, CancellationToken cancellationToken) + public async Task SetChecksumAsync(string dacpacPath, string targetConnectionString, string dacpacChecksum, ILogger logger, CancellationToken cancellationToken) { var targetDatabaseName = GetDatabaseName(targetConnectionString); var dacpacPathChecksum = GetStringChecksum(dacpacPath); - - using (var connection = new SqlConnection(targetConnectionString)) - { - await connection.OpenAsync(SqlConnectionOverrides.OpenWithoutRetry, cancellationToken); + + using var connection = new SqlConnection(targetConnectionString); + + await connection.OpenAsync(SqlConnectionOverrides.OpenWithoutRetry, cancellationToken); - await UpdateExtendedPropertyAsync(connection, dacpacPathChecksum, dacpacChecksum, cancellationToken); + await UpdateExtendedPropertyAsync(connection, dacpacPathChecksum, dacpacChecksum, cancellationToken); - deploymentSkipLogger.LogInformation("The .dacpac with checksum {DacpacChecksum} has been registered in database {TargetDatabaseName}.", dacpacChecksum, targetDatabaseName); - } + logger.LogInformation("The .dacpac with checksum {DacpacChecksum} has been registered in database {TargetDatabaseName}.", dacpacChecksum, targetDatabaseName); } private static string GetDatabaseName(string connectionString) diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/IDacpacChecksumService.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/IDacpacChecksumService.cs index 30a29c020..f8da75353 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/IDacpacChecksumService.cs +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/IDacpacChecksumService.cs @@ -8,7 +8,7 @@ namespace CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects; internal interface IDacpacChecksumService { /// - /// Checks if the .dacpac file has already been deployed to the specified SQL Server. + /// Checks if the file has already been deployed to the specified /// /// Path to the .dacpac file to deploy. /// Connection string to the SQL Server. @@ -18,13 +18,13 @@ internal interface IDacpacChecksumService Task CheckIfDeployedAsync(string dacpacPath, string targetConnectionString, ILogger deploymentSkipLogger, CancellationToken cancellationToken); /// - /// Sets the checksum extended property on the target database to indicate that the .dacpac file has been deployed. + /// Sets the checksum extended property on the target database to indicate that the file has been deployed. /// /// Path to the .dacpac file to deploy. /// Connection string to the SQL Server. /// Checksum for the .dacpac /// An to write the log to. /// A that can be used to cancel the deployment operation. - /// + /// A task that represents the asynchronous operation. Task SetChecksumAsync(string dacpacPath, string targetConnectionString, string dacpacChecksum, ILogger deploymentSkipLogger, CancellationToken cancellationToken); } diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs index 1ef7486ae..cd87c8b97 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectPublishService.cs @@ -49,7 +49,7 @@ await resourceNotificationService.PublishUpdateAsync(resource, options.DropExtendedPropertiesNotInSource = false; var result = await dacpacChecksumService.CheckIfDeployedAsync(dacpacPath, connectionString, logger, cancellationToken); - if (string.IsNullOrEmpty(result)) + if (result is null) { await resourceNotificationService.PublishUpdateAsync(resource, state => state with { State = new ResourceStateSnapshot(KnownResourceStates.Finished, KnownResourceStateStyles.Success) }); From 68d6d05e015d441468aaf9de1811f571eaf45b72 Mon Sep 17 00:00:00 2001 From: Erik Ejlskov Jensen Date: Mon, 20 Oct 2025 15:17:24 +0200 Subject: [PATCH 14/14] fix comment --- .../DacpacChecksumService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacChecksumService.cs b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacChecksumService.cs index 30a244433..3771fabcc 100644 --- a/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacChecksumService.cs +++ b/src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/DacpacChecksumService.cs @@ -19,7 +19,7 @@ internal class DacpacChecksumService : IDacpacChecksumService try { - // Try to connect to the target database to see it exists and fail fast if it does not. + // Try to connect to the target database to see if it exists and fail fast if it does not. await connection.OpenAsync(SqlConnectionOverrides.OpenWithoutRetry, cancellationToken); } catch (Exception ex) when (ex is InvalidOperationException || ex is SqlException)