Skip to content

Add support for DacDeployOption from a publish profile #729

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
merged 3 commits into from
Jun 18, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions examples/sql-database-projects/SdkProject/Database.publish.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<IncludeCompositeObjects>False</IncludeCompositeObjects>
<TargetDatabaseName>Database</TargetDatabaseName>
<DeployScriptFileName>Database.sql</DeployScriptFileName>
<BlockOnPossibleDataLoss>True</BlockOnPossibleDataLoss>
<ProfileVersionNumber>1</ProfileVersionNumber>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// Represents a metadata annotation that specifies dacpac deployment options.
/// </summary>
/// <param name="OptionsPath">path to deployment options xml file</param>
public record DacDeployOptionsAnnotation(string OptionsPath) : IResourceAnnotation
{
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,19 @@ DacDeployOptions IResourceWithDacpac.GetDacpacDeployOptions()
{
var options = new DacDeployOptions();

if (this.TryGetLastAnnotation<DacDeployOptionsAnnotation>(out var optionsAnnotation))
{
var profile = DacProfile.Load(optionsAnnotation.OptionsPath);

if (profile == null)
{
throw new InvalidOperationException($"Unable to load DacProfile from path {optionsAnnotation.OptionsPath} for resource {Name}.");
}

options = profile.DeployOptions;
return options;
}

if (this.TryGetLastAnnotation<ConfigureDacDeployOptionsAnnotation>(out var configureAnnotation))
{
configureAnnotation.ConfigureDeploymentOptions(options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,34 @@ internal static IResourceBuilder<TResource> InternalWithConfigureDacDeployOption
.WithAnnotation(new ConfigureDacDeployOptionsAnnotation(configureDeploymentOptions));
}

/// <summary>
/// Adds a path to a publish profile for configuring dacpac deployment options to the <see cref="SqlProjectResource"/>.
/// </summary>
/// <param name="builder">An <see cref="IResourceBuilder{T}"/> representing the SQL Server Database project.</param>
/// <param name="optionsPath">Path to the publish profile xml file</param>
/// <returns>An <see cref="IResourceBuilder{T}"/> that can be used to further customize the resource.</returns>
public static IResourceBuilder<SqlProjectResource> WithDacDeployOptions(this IResourceBuilder<SqlProjectResource> builder, string optionsPath)
=> InternalWithDacDeployOptions(builder, optionsPath);

/// <summary>
/// Adds a path to a publish profile for configuring dacpac deployment options to the <see cref="SqlProjectResource"/>.
/// </summary>
/// <param name="builder">An <see cref="IResourceBuilder{T}"/> representing the SQL Server Database project.</param>
/// <param name="optionsPath">Path to the publish profile xml file</param>
/// <returns>An <see cref="IResourceBuilder{T}"/> that can be used to further customize the resource.</returns>
public static IResourceBuilder<SqlPackageResource<TPackage>> WithDacDeployOptions<TPackage>(this IResourceBuilder<SqlPackageResource<TPackage>> builder, string optionsPath)
where TPackage : IPackageMetadata => InternalWithDacDeployOptions(builder, optionsPath);

internal static IResourceBuilder<TResource> InternalWithDacDeployOptions<TResource>(this IResourceBuilder<TResource> builder, string optionsPath)
where TResource : IResourceWithDacpac
{
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
ArgumentNullException.ThrowIfNull(optionsPath);

return builder
.WithAnnotation(new DacDeployOptionsAnnotation(optionsPath));
}

/// <summary>
/// Publishes the SQL Server Database project to the target <see cref="SqlServerDatabaseResource"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ string IResourceWithDacpac.GetDacpacPath()

var project = projectCollection.LoadProject(projectPath);

// .sqlprojx has a SqlTargetPath property, so try that first
// Microsoft.Build.Sql .sqlproj has a SqlTargetPath property, so try that first
var targetPath = project.GetPropertyValue("SqlTargetPath");
if (string.IsNullOrWhiteSpace(targetPath))
{
Expand All @@ -45,6 +45,19 @@ DacDeployOptions IResourceWithDacpac.GetDacpacDeployOptions()
{
var options = new DacDeployOptions();

if (this.TryGetLastAnnotation<DacDeployOptionsAnnotation>(out var optionsAnnotation))
{
var profile = DacProfile.Load(optionsAnnotation.OptionsPath);

if (profile == null)
{
throw new InvalidOperationException($"Unable to load DacProfile from path {optionsAnnotation.OptionsPath} for resource {Name}.");
}

options = profile.DeployOptions;
return options;
}

if (this.TryGetLastAnnotation<ConfigureDacDeployOptionsAnnotation>(out var configureAnnotation))
{
configureAnnotation.ConfigureDeploymentOptions(options);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Aspire.Hosting;
using Microsoft.SqlServer.Dac;
using System.Configuration;

namespace CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests;

Expand Down Expand Up @@ -91,4 +92,29 @@ public void AddSqlPackage_WithDeploymentOptions()
var options = ((IResourceWithDacpac)sqlProjectResource).GetDacpacDeployOptions();
Assert.True(options.IncludeCompositeObjects);
}

[Fact]
public void AddSqlPackage_WithDeploymentOptions_FromFile()
{
// Arrange
var appBuilder = DistributedApplication.CreateBuilder();

var optionsPath = "Database.publish.xml";

appBuilder.AddSqlPackage<TestPackage>("chinook").WithDacDeployOptions(optionsPath);

// Act
using var app = appBuilder.Build();
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();

// Assert
var sqlProjectResource = Assert.Single(appModel.Resources.OfType<SqlPackageResource<TestPackage>>());
Assert.Equal("chinook", sqlProjectResource.Name);

Assert.True(sqlProjectResource.TryGetLastAnnotation(out DacDeployOptionsAnnotation? dacDeployOptionsAnnotation));
Assert.Equal(optionsPath, dacDeployOptionsAnnotation.OptionsPath);

var options = ((IResourceWithDacpac)sqlProjectResource).GetDacpacDeployOptions();
Assert.False(options.BlockOnPossibleDataLoss);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,55 @@ public void AddSqlProject_WithDeploymentOptions()
Assert.True(options.IncludeCompositeObjects);
}

[Fact]
public void AddSqlProject_WithDeploymentOptions_FromFile_NonExisting()
{
// Arrange
var appBuilder = DistributedApplication.CreateBuilder();

var optionsPath = "/folder/project.publish.xml";

appBuilder.AddSqlProject("MySqlProject").WithDacDeployOptions(optionsPath);

// Act
using var app = appBuilder.Build();
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();

// Assert
var sqlProjectResource = Assert.Single(appModel.Resources.OfType<SqlProjectResource>());
Assert.Equal("MySqlProject", sqlProjectResource.Name);

Assert.True(sqlProjectResource.TryGetLastAnnotation(out DacDeployOptionsAnnotation? dacDeployOptionsAnnotation));
Assert.Equal(optionsPath, dacDeployOptionsAnnotation.OptionsPath);

Assert.Throws<DirectoryNotFoundException>(() => ((IResourceWithDacpac)sqlProjectResource).GetDacpacDeployOptions());
}

[Fact]
public void AddSqlProject_WithDeploymentOptions_FromFile()
{
// Arrange
var appBuilder = DistributedApplication.CreateBuilder();

var optionsPath = "Database.publish.xml";

appBuilder.AddSqlProject("MySqlProject").WithDacDeployOptions(optionsPath);

// Act
using var app = appBuilder.Build();
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();

// Assert
var sqlProjectResource = Assert.Single(appModel.Resources.OfType<SqlProjectResource>());
Assert.Equal("MySqlProject", sqlProjectResource.Name);

Assert.True(sqlProjectResource.TryGetLastAnnotation(out DacDeployOptionsAnnotation? dacDeployOptionsAnnotation));
Assert.Equal(optionsPath, dacDeployOptionsAnnotation.OptionsPath);

var options = ((IResourceWithDacpac)sqlProjectResource).GetDacpacDeployOptions();
Assert.False(options.BlockOnPossibleDataLoss);
}

[Fact]
public void WithReference_AddsRequiredServices()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,10 @@
<ProjectReference Include="..\..\examples\sql-database-projects\CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.AppHost\CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.AppHost.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="Database.publish.xml">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<TargetDatabaseName>Database</TargetDatabaseName>
<DeployScriptFileName>Database.sql</DeployScriptFileName>
<BlockOnPossibleDataLoss>False</BlockOnPossibleDataLoss>
<ProfileVersionNumber>1</ProfileVersionNumber>
</PropertyGroup>
</Project>
Loading