Skip to content

Commit 79b247c

Browse files
authored
Add support for deploying dacpacs from referenced NuGet packages (#334)
* WIP Signed-off-by: Jonathan Mezach <[email protected]> * Put metadata on PackageVersion element for CPM Signed-off-by: Jonathan Mezach <[email protected]> * Allow specifying a relative path within package Signed-off-by: Jonathan Mezach <[email protected]> * Generate package version into package metadata Signed-off-by: Jonathan Mezach <[email protected]> * Refactor to simplify Move targets file into package Signed-off-by: Jonathan Mezach <[email protected]> * Fix resolving of IsAspirePackageResource Signed-off-by: Jonathan Mezach <[email protected]> * Fix broken test Signed-off-by: Jonathan Mezach <[email protected]> * Add initial tests for package based SQL Database Projects Signed-off-by: Jonathan Mezach <[email protected]> * Make sure we include MSBuild logic in packae Signed-off-by: Jonathan Mezach <[email protected]> * Attempt to make tests work on Windows Signed-off-by: Jonathan Mezach <[email protected]> * Replace MS package with a more acurate sample package Add integration test Signed-off-by: Jonathan Mezach <[email protected]> * Make tests more robust cross-platform Signed-off-by: Jonathan Mezach <[email protected]> * Resolve PR comments Signed-off-by: Jonathan Mezach <[email protected]> * Separate out project and package based resource Signed-off-by: Jonathan Mezach <[email protected]> --------- Signed-off-by: Jonathan Mezach <[email protected]>
1 parent f29652c commit 79b247c

17 files changed

+392
-42
lines changed

Directory.Packages.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
<PackageVersion Include="EventStore.Client.Extensions.OpenTelemetry" Version="23.3.7" />
5454
<PackageVersion Include="EventStore.Client.Grpc.Streams" Version="23.3.7" />
5555
<PackageVersion Include="MassTransit.RabbitMQ" Version="8.3.2" />
56+
<PackageVersion Include="ErikEJ.Dacpac.Chinook" Version="1.0.0" />
5657
<!-- Build dependencies -->
5758
<PackageVersion Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="3.3.4" />
5859
<!-- Testcontainers packages -->

examples/sql-database-projects/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.AppHost/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.AppHost.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@
1212

1313
<ItemGroup>
1414
<PackageReference Include="Aspire.Hosting.AppHost" />
15+
<PackageReference Include="ErikEJ.Dacpac.Chinook" IsAspirePackageResource="true" />
1516
</ItemGroup>
1617

1718
<ItemGroup>
1819
<ProjectReference Include="..\..\..\src\CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects\CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.csproj" IsAspireProjectResource="False" />
1920
<ProjectReference Include="..\SdkProject\SdkProject.csproj" />
2021
</ItemGroup>
2122

23+
<Import Project="..\..\..\src\CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects\build\CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.targets" />
24+
2225
</Project>

examples/sql-database-projects/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.AppHost/Program.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@
66
builder.AddSqlProject<Projects.SdkProject>("sdk-project")
77
.WithReference(server);
88

9+
builder.AddSqlPackage<Packages.ErikEJ_Dacpac_Chinook>("chinook")
10+
.WithReference(server);
11+
912
builder.Build().Run();

src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
<PackageReference Include="Microsoft.SqlServer.DacFx" />
1515
</ItemGroup>
1616

17+
<ItemGroup>
18+
<None Include="**/*.props;**/*.targets" Pack="true" PackagePath="%(RecursiveDir)%(Filename)%(Extension)" />
19+
</ItemGroup>
20+
1721
<ItemGroup>
1822
<InternalsVisibleTo Include="CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects.Tests" />
1923
</ItemGroup>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using Aspire.Hosting.ApplicationModel;
2+
3+
namespace Aspire.Hosting;
4+
5+
/// <summary>
6+
/// Represents metadata for a referenced NuGet package.
7+
/// </summary>
8+
public interface IPackageMetadata : IResourceAnnotation
9+
{
10+
/// <summary>
11+
/// Gets the unique identifier of the package.
12+
/// </summary>
13+
string PackageId { get; }
14+
15+
/// <summary>
16+
/// Gets the version of the package.
17+
/// </summary>
18+
Version PackageVersion { get; }
19+
20+
/// <summary>
21+
/// Gets the physical location on disk of the package.
22+
/// </summary>
23+
string PackagePath { get; }
24+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using Microsoft.SqlServer.Dac;
2+
3+
namespace Aspire.Hosting.ApplicationModel;
4+
5+
/// <summary>
6+
/// Represents a resource that produces a .dacpac file.
7+
/// </summary>
8+
public interface IResourceWithDacpac : IResource, IResourceWithWaitSupport
9+
{
10+
internal string GetDacpacPath();
11+
internal DacDeployOptions GetDacpacDeployOptions();
12+
}

src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/PublicAPI.Unshipped.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,14 @@ Aspire.Hosting.ApplicationModel.ConfigureDacDeployOptionsAnnotation.ConfigureDac
44
Aspire.Hosting.ApplicationModel.ConfigureDacDeployOptionsAnnotation.ConfigureDeploymentOptions.get -> System.Action<Microsoft.SqlServer.Dac.DacDeployOptions!>!
55
Aspire.Hosting.ApplicationModel.ConfigureDacDeployOptionsAnnotation.ConfigureDeploymentOptions.init -> void
66
static Aspire.Hosting.SqlProjectBuilderExtensions.WithConfigureDacDeployOptions(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.SqlProjectResource!>! builder, System.Action<Microsoft.SqlServer.Dac.DacDeployOptions!>! configureDeploymentOptions) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.SqlProjectResource!>!
7+
Aspire.Hosting.IPackageMetadata
8+
Aspire.Hosting.IPackageMetadata.PackageId.get -> string!
9+
Aspire.Hosting.IPackageMetadata.PackageVersion.get -> System.Version!
10+
Aspire.Hosting.IPackageMetadata.PackagePath.get -> string!
11+
Aspire.Hosting.ApplicationModel.IResourceWithDacpac
12+
Aspire.Hosting.ApplicationModel.SqlPackageResource<TPackage>
13+
Aspire.Hosting.ApplicationModel.SqlPackageResource<TPackage>.SqlPackageResource(string! name) -> void
14+
static Aspire.Hosting.SqlProjectBuilderExtensions.AddSqlPackage<TPackage>(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.SqlPackageResource<TPackage>!>!
15+
static Aspire.Hosting.SqlProjectBuilderExtensions.WithReference<TPackage>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.SqlPackageResource<TPackage>!>! builder, Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.SqlServerDatabaseResource!>! target) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.SqlPackageResource<TPackage>!>!
16+
static Aspire.Hosting.SqlProjectBuilderExtensions.WithDacpac<TPackage>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.SqlPackageResource<TPackage>!>! builder, string! dacpacPath) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.SqlPackageResource<TPackage>!>!
17+
static Aspire.Hosting.SqlProjectBuilderExtensions.WithConfigureDacDeployOptions<TPackage>(this Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.SqlPackageResource<TPackage>!>! builder, System.Action<Microsoft.SqlServer.Dac.DacDeployOptions!>! configureDeploymentOptions) -> Aspire.Hosting.ApplicationModel.IResourceBuilder<Aspire.Hosting.ApplicationModel.SqlPackageResource<TPackage>!>!
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using Microsoft.SqlServer.Dac;
2+
3+
namespace Aspire.Hosting.ApplicationModel;
4+
5+
/// <summary>
6+
/// Represents a SQL Server Database package resource.
7+
/// </summary>
8+
/// <typeparam name="TPackage">Type that represents the package that contains the .dacpac file.</typeparam>
9+
/// <param name="name">Name of the resource.</param>
10+
public sealed class SqlPackageResource<TPackage>(string name) : Resource(name), IResourceWithWaitSupport, IResourceWithDacpac
11+
where TPackage : IPackageMetadata
12+
{
13+
string IResourceWithDacpac.GetDacpacPath()
14+
{
15+
if (this.TryGetLastAnnotation<IPackageMetadata>(out var packageMetadata))
16+
{
17+
var packagePath = packageMetadata.PackagePath;
18+
if (this.TryGetLastAnnotation<DacpacMetadataAnnotation>(out var relativeDacpacMetadata))
19+
{
20+
return Path.Combine(packagePath, relativeDacpacMetadata.DacpacPath);;
21+
}
22+
else
23+
{
24+
return Path.Combine(packagePath, "tools", packageMetadata.PackageId + ".dacpac");
25+
}
26+
}
27+
28+
if (this.TryGetLastAnnotation<DacpacMetadataAnnotation>(out var dacpacMetadata))
29+
{
30+
return dacpacMetadata.DacpacPath;
31+
}
32+
33+
throw new InvalidOperationException($"Unable to locate SQL Server Database project package for resource {Name}.");
34+
}
35+
36+
DacDeployOptions IResourceWithDacpac.GetDacpacDeployOptions()
37+
{
38+
var options = new DacDeployOptions();
39+
40+
if (this.TryGetLastAnnotation<ConfigureDacDeployOptionsAnnotation>(out var configureAnnotation))
41+
{
42+
configureAnnotation.ConfigureDeploymentOptions(options);
43+
}
44+
45+
return options;
46+
}
47+
}

src/CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects/SqlProjectBuilderExtensions.cs

Lines changed: 80 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ static SqlProjectBuilderExtensions()
3030
/// <param name="builder">An <see cref="IDistributedApplicationBuilder"/> instance to add the SQL Server Database project to.</param>
3131
/// <param name="name">Name of the resource.</param>
3232
/// <returns>An <see cref="IResourceBuilder{T}"/> that can be used to further customize the resource.</returns>
33-
public static IResourceBuilder<SqlProjectResource> AddSqlProject<TProject>(this IDistributedApplicationBuilder builder, [ResourceName]string name)
33+
public static IResourceBuilder<SqlProjectResource> AddSqlProject<TProject>(this IDistributedApplicationBuilder builder, [ResourceName] string name)
3434
where TProject : IProjectMetadata, new()
3535
{
3636
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
@@ -46,14 +46,46 @@ public static IResourceBuilder<SqlProjectResource> AddSqlProject<TProject>(this
4646
/// <param name="builder">An <see cref="IDistributedApplicationBuilder"/> instance to add the SQL Server Database project to.</param>
4747
/// <param name="name">Name of the resource.</param>
4848
/// <returns>An <see cref="IResourceBuilder{T}"/> that can be used to further customize the resource.</returns>
49-
public static IResourceBuilder<SqlProjectResource> AddSqlProject(this IDistributedApplicationBuilder builder, [ResourceName]string name)
49+
public static IResourceBuilder<SqlProjectResource> AddSqlProject(this IDistributedApplicationBuilder builder, [ResourceName] string name)
5050
{
5151
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
5252
ArgumentNullException.ThrowIfNull(name, nameof(name));
5353

5454
var resource = new SqlProjectResource(name);
5555

5656
return builder.AddResource(resource)
57+
.WithInitialState(new CustomResourceSnapshot
58+
{
59+
Properties = [],
60+
ResourceType = "SqlProject",
61+
State = new ResourceStateSnapshot("Pending", KnownResourceStateStyles.Info)
62+
})
63+
.ExcludeFromManifest();
64+
}
65+
66+
/// <summary>
67+
/// Adds a SQL Server Database Project resource to the application based on a referenced NuGet package.
68+
/// </summary>
69+
/// <typeparam name="TPackage">Type that represents the NuGet package that contains the .dacpac file.</typeparam>
70+
/// <param name="builder">An <see cref="IDistributedApplicationBuilder"/> instance to add the SQL Server Database project to.</param>
71+
/// <param name="name">Name of the resource.</param>
72+
/// <returns>Am <see cref="IResourceBuilder{T}"/> that can be used to further customize the resource.</returns>
73+
public static IResourceBuilder<SqlPackageResource<TPackage>> AddSqlPackage<TPackage>(this IDistributedApplicationBuilder builder, [ResourceName] string name)
74+
where TPackage : IPackageMetadata, new()
75+
{
76+
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
77+
ArgumentNullException.ThrowIfNull(name, nameof(name));
78+
79+
var resource = new SqlPackageResource<TPackage>(name);
80+
81+
return builder.AddResource(resource)
82+
.WithAnnotation(new TPackage())
83+
.WithInitialState(new CustomResourceSnapshot
84+
{
85+
Properties = [],
86+
ResourceType = "SqlPackage",
87+
State = new ResourceStateSnapshot("Pending", KnownResourceStateStyles.Info)
88+
})
5789
.ExcludeFromManifest();
5890
}
5991

@@ -63,13 +95,22 @@ public static IResourceBuilder<SqlProjectResource> AddSqlProject(this IDistribut
6395
/// <param name="builder">An <see cref="IResourceBuilder{T}"/> representing the SQL Server Database project.</param>
6496
/// <param name="dacpacPath">Path to the .dacpac file.</param>
6597
/// <returns>An <see cref="IResourceBuilder{T}"/> that can be used to further customize the resource.</returns>
66-
public static IResourceBuilder<SqlProjectResource> WithDacpac(this IResourceBuilder<SqlProjectResource> builder, string dacpacPath)
67-
{
68-
if (!Path.IsPathRooted(dacpacPath))
69-
{
70-
dacpacPath = Path.Combine(builder.ApplicationBuilder.AppHostDirectory, dacpacPath);
71-
}
98+
public static IResourceBuilder<SqlProjectResource> WithDacpac(this IResourceBuilder<SqlProjectResource> builder, string dacpacPath)
99+
=> InternalWithDacpac(builder, dacpacPath);
100+
101+
/// <summary>
102+
/// Specifies the path to the .dacpac file.
103+
/// </summary>
104+
/// <param name="builder">An <see cref="IResourceBuilder{T}"/> representing the SQL Server Database project.</param>
105+
/// <param name="dacpacPath">Path to the .dacpac file.</param>
106+
/// <returns>An <see cref="IResourceBuilder{T}"/> that can be used to further customize the resource.</returns>
107+
public static IResourceBuilder<SqlPackageResource<TPackage>> WithDacpac<TPackage>(this IResourceBuilder<SqlPackageResource<TPackage>> builder, string dacpacPath)
108+
where TPackage : IPackageMetadata => InternalWithDacpac(builder, dacpacPath);
72109

110+
111+
internal static IResourceBuilder<TResource> InternalWithDacpac<TResource>(this IResourceBuilder<TResource> builder, string dacpacPath)
112+
where TResource : IResourceWithDacpac
113+
{
73114
return builder.WithAnnotation(new DacpacMetadataAnnotation(dacpacPath));
74115
}
75116

@@ -80,6 +121,19 @@ public static IResourceBuilder<SqlProjectResource> WithDacpac(this IResourceBuil
80121
/// <param name="configureDeploymentOptions">The delegate for configuring dacpac deployment options</param>
81122
/// <returns>An <see cref="IResourceBuilder{T}"/> that can be used to further customize the resource.</returns>
82123
public static IResourceBuilder<SqlProjectResource> WithConfigureDacDeployOptions(this IResourceBuilder<SqlProjectResource> builder, Action<DacDeployOptions> configureDeploymentOptions)
124+
=> InternalWithConfigureDacDeployOptions(builder, configureDeploymentOptions);
125+
126+
/// <summary>
127+
/// Adds a delegate annotation for configuring dacpac deployment options to the <see cref="SqlProjectResource"/>.
128+
/// </summary>
129+
/// <param name="builder">An <see cref="IResourceBuilder{T}"/> representing the SQL Server Database project.</param>
130+
/// <param name="configureDeploymentOptions">The delegate for configuring dacpac deployment options</param>
131+
/// <returns>An <see cref="IResourceBuilder{T}"/> that can be used to further customize the resource.</returns>
132+
public static IResourceBuilder<SqlPackageResource<TPackage>> WithConfigureDacDeployOptions<TPackage>(this IResourceBuilder<SqlPackageResource<TPackage>> builder, Action<DacDeployOptions> configureDeploymentOptions)
133+
where TPackage : IPackageMetadata => InternalWithConfigureDacDeployOptions(builder, configureDeploymentOptions);
134+
135+
internal static IResourceBuilder<TResource> InternalWithConfigureDacDeployOptions<TResource>(this IResourceBuilder<TResource> builder, Action<DacDeployOptions> configureDeploymentOptions)
136+
where TResource : IResourceWithDacpac
83137
{
84138
ArgumentNullException.ThrowIfNull(builder, nameof(builder));
85139
ArgumentNullException.ThrowIfNull(configureDeploymentOptions);
@@ -95,7 +149,23 @@ public static IResourceBuilder<SqlProjectResource> WithConfigureDacDeployOptions
95149
/// <param name="target">An <see cref="IResourceBuilder{T}"/> representing the target <see cref="SqlServerDatabaseResource"/> to publish the SQL Server Database project to.</param>
96150
/// <returns>An <see cref="IResourceBuilder{T}"/> that can be used to further customize the resource.</returns>
97151
public static IResourceBuilder<SqlProjectResource> WithReference(
98-
this IResourceBuilder<SqlProjectResource> builder, IResourceBuilder<SqlServerDatabaseResource> target)
152+
this IResourceBuilder<SqlProjectResource> builder, IResourceBuilder<SqlServerDatabaseResource> target) => InternalWithReference(builder, target);
153+
154+
/// <summary>
155+
/// Publishes the SQL Server Database project to the target <see cref="SqlServerDatabaseResource"/>.
156+
/// </summary>
157+
/// <param name="builder">An <see cref="IResourceBuilder{T}"/> representing the SQL Server Database project to publish.</param>
158+
/// <param name="target">An <see cref="IResourceBuilder{T}"/> representing the target <see cref="SqlServerDatabaseResource"/> to publish the SQL Server Database project to.</param>
159+
/// <returns>An <see cref="IResourceBuilder{T}"/> that can be used to further customize the resource.</returns>
160+
public static IResourceBuilder<SqlPackageResource<TPackage>> WithReference<TPackage>(
161+
this IResourceBuilder<SqlPackageResource<TPackage>> builder, IResourceBuilder<SqlServerDatabaseResource> target)
162+
where TPackage : IPackageMetadata
163+
{
164+
return InternalWithReference(builder, target);
165+
}
166+
167+
internal static IResourceBuilder<TResource> InternalWithReference<TResource>(this IResourceBuilder<TResource> builder, IResourceBuilder<SqlServerDatabaseResource> target)
168+
where TResource : IResourceWithDacpac
99169
{
100170
builder.ApplicationBuilder.Services.TryAddSingleton<IDacpacDeployer, DacpacDeployer>();
101171
builder.ApplicationBuilder.Services.TryAddSingleton<SqlProjectPublishService>();
@@ -108,20 +178,13 @@ public static IResourceBuilder<SqlProjectResource> WithReference(
108178

109179
builder.WaitFor(target);
110180

111-
builder.WithInitialState(new CustomResourceSnapshot
112-
{
113-
Properties = [],
114-
ResourceType = "SqlProject",
115-
State = new ResourceStateSnapshot("Pending", KnownResourceStateStyles.Info)
116-
});
117-
118181
builder.WithCommand("redeploy", "Redeploy", async (context) =>
119182
{
120183
var service = context.ServiceProvider.GetRequiredService<SqlProjectPublishService>();
121184
await service.PublishSqlProject(builder.Resource, target.Resource, context.CancellationToken);
122185
return new ExecuteCommandResult { Success = true };
123186
}, updateState: (context) => context.ResourceSnapshot?.State?.Text == KnownResourceStates.Finished ? ResourceCommandState.Enabled : ResourceCommandState.Disabled,
124-
displayDescription: "Redeploys the SQL Server Database project to the target database.",
187+
displayDescription: "Redeploys the SQL Server Database to the target database.",
125188
iconName: "ArrowReset",
126189
iconVariant: IconVariant.Filled,
127190
isHighlighted: true);

0 commit comments

Comments
 (0)