Skip to content

Commit

Permalink
Refactor to increase testability
Browse files Browse the repository at this point in the history
Signed-off-by: Jonathan Mezach <[email protected]>
  • Loading branch information
jmezach committed Aug 2, 2024
1 parent f1c887a commit f69da0a
Show file tree
Hide file tree
Showing 9 changed files with 98 additions and 12 deletions.
19 changes: 19 additions & 0 deletions src/MSBuild.Sdk.SqlProj.Aspire/DacpacDeployer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Microsoft.Extensions.Logging;
using Microsoft.SqlServer.Dac;

namespace MSBuild.Sdk.SqlProj.Aspire;

/// <summary>
/// Provides the actual implementation of the <see cref="IDacpacDeployer"/> interface.
/// </summary>
internal class DacpacDeployer : IDacpacDeployer
{
/// <inheritdoc cref="IDacpacDeployer.Deploy(string, string, string, ILogger, CancellationToken)">
public void Deploy(string dacpacPath, string targetConnectionString, string targetDatabaseName, ILogger deploymentLogger, CancellationToken cancellationToken)
{
var dacPackage = DacPackage.Load(dacpacPath, DacSchemaModelStorageType.Memory);
var dacServices = new DacServices(targetConnectionString);
dacServices.Message += (sender, args) => deploymentLogger.LogInformation(args.Message.ToString());
dacServices.Deploy(dacPackage, targetDatabaseName, true, new DacDeployOptions(), cancellationToken);
}
}
20 changes: 20 additions & 0 deletions src/MSBuild.Sdk.SqlProj.Aspire/IDacpacDeployer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Microsoft.Extensions.Logging;

namespace MSBuild.Sdk.SqlProj.Aspire;

/// <summary>
/// Abstracts the actual deployment of the .dacpac file to a SQL Server database.
/// </summary>
public interface IDacpacDeployer
{
/// <summary>
/// Deployes the provided <paramref name="dacpacPath">.dacpac</paramref> file to the specified <paramref name="targetConnectionString">SQL Server</paramref>
/// using the provided <paramref name="targetDatabaseName">database name</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="targetDatabaseName">Name of the target database to deploy to.</param>
/// <param name="deploymentLogger">An <see cref="ILogger"> to write the deployment log to.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the deployment operation.</param>
void Deploy(string dacpacPath, string targetConnectionString, string targetDatabaseName, ILogger deploymentLogger, CancellationToken cancellationToken);
}
17 changes: 11 additions & 6 deletions src/MSBuild.Sdk.SqlProj.Aspire/PublishSqlProjectLifecycleHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ namespace MSBuild.Sdk.SqlProj.Aspire;

public class PublishSqlProjectLifecycleHook : IDistributedApplicationLifecycleHook
{
private readonly IDacpacDeployer _deployer;
private readonly ResourceLoggerService _resourceLoggerService;
private readonly ResourceNotificationService _resourceNotificationService;

public PublishSqlProjectLifecycleHook(ResourceLoggerService resourceLoggerService,
public PublishSqlProjectLifecycleHook(IDacpacDeployer deployer, ResourceLoggerService resourceLoggerService,
ResourceNotificationService resourceNotificationService)
{
_deployer = deployer ?? throw new ArgumentNullException(nameof(deployer));
_resourceLoggerService = resourceLoggerService ?? throw new ArgumentNullException(nameof(resourceLoggerService));
_resourceNotificationService = resourceNotificationService ?? throw new ArgumentNullException(nameof(resourceNotificationService));
}
Expand All @@ -35,17 +37,20 @@ await _resourceNotificationService.PublishUpdateAsync(sqlProject,
var targetDatabaseResourceName = sqlProject.Annotations.OfType<TargetDatabaseResourceAnnotation>().Single().TargetDatabaseResourceName;
var targetDatabaseResource = application.Resources.OfType<SqlServerDatabaseResource>().Single(r => r.Name == targetDatabaseResourceName);
var connectionString = await targetDatabaseResource.ConnectionStringExpression.GetValueAsync(cancellationToken);
if (connectionString == null)
{
logger.LogError("Failed to retrieve connection string for target database {TargetDatabaseResourceName}.", targetDatabaseResourceName);
await _resourceNotificationService.PublishUpdateAsync(sqlProject,
state => state with { State = new ResourceStateSnapshot("Failed", KnownResourceStateStyles.Error) });
continue;
}

await _resourceNotificationService.PublishUpdateAsync(sqlProject,
state => state with { State = new ResourceStateSnapshot("Publishing", KnownResourceStateStyles.Info) });

try
{
var dacServices = new DacServices(connectionString);
dacServices.Message += (sender, args) => logger.LogInformation(args.Message.ToString());

var dacpacPackage = DacPackage.Load(dacpacPath, DacSchemaModelStorageType.Memory);
dacServices.Deploy(dacpacPackage, targetDatabaseResource.Name, true, new DacDeployOptions(), cancellationToken);
_deployer.Deploy(dacpacPath, connectionString, targetDatabaseResource.DatabaseName, logger, cancellationToken);

await _resourceNotificationService.PublishUpdateAsync(sqlProject,
state => state with { State = new ResourceStateSnapshot("Published", KnownResourceStateStyles.Success) });
Expand Down
3 changes: 3 additions & 0 deletions src/MSBuild.Sdk.SqlProj.Aspire/SqlProjectBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Lifecycle;
using Microsoft.Build.Locator;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using MSBuild.Sdk.SqlProj.Aspire;

namespace Aspire.Hosting;
Expand Down Expand Up @@ -66,6 +68,7 @@ public static IResourceBuilder<SqlProjectResource> FromDacpac(this IResourceBuil
public static IResourceBuilder<SqlProjectResource> PublishTo(
this IResourceBuilder<SqlProjectResource> builder, IResourceBuilder<SqlServerDatabaseResource> target)
{
builder.ApplicationBuilder.Services.TryAddSingleton<IDacpacDeployer, DacpacDeployer>();
builder.ApplicationBuilder.Services.TryAddLifecycleHook<PublishSqlProjectLifecycleHook>();
builder.WithAnnotation(new TargetDatabaseResourceAnnotation(target.Resource.Name), ResourceAnnotationMutationBehavior.Replace);
return builder;
Expand Down
3 changes: 2 additions & 1 deletion src/MSBuild.Sdk.SqlProj.Aspire/SqlProjectResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ public string GetDacpacPath()
if (projectMetadata != null)
{
var projectPath = projectMetadata.ProjectPath;
var project = new Project(projectPath);
using var projectCollection = new ProjectCollection();
var project = projectCollection.LoadProject(projectPath);
return project.GetPropertyValue("TargetPath");
}

Expand Down
5 changes: 0 additions & 5 deletions test/MSBuild.Sdk.SqlProj.Aspire.Tests/AddSqlProjectTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,4 @@ public void PublishTo_AddsLifecycleHook()
var lifecycleHooks = app.Services.GetServices<IDistributedApplicationLifecycleHook>();
Assert.Single(lifecycleHooks.OfType<PublishSqlProjectLifecycleHook>());
}

private class TestProject : IProjectMetadata
{
public string ProjectPath { get; } = "../../../../TestProject/TestProject.csproj";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<PackageReference Include="Aspire.Hosting.Testing" Version="8.1.0" />
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Include="xunit" Version="2.9.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Aspire.Hosting;
using Aspire.Hosting.Lifecycle;
using Microsoft.Extensions.Logging;
using NSubstitute;

namespace MSBuild.Sdk.SqlProj.Aspire.Tests;

public class PublishSqlProjectLifecycleHookTests
{
[Fact]
public async Task AfterResourcesCreatedAsync_PublishesDacpacToTargetDatabase()
{
// Arrange
var dacDeployerMock = Substitute.For<IDacpacDeployer>();
var appBuilder = DistributedApplication.CreateBuilder();
appBuilder.Services.AddSingleton(dacDeployerMock);

var targetDatabase = appBuilder.AddSqlServer("sql")
.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 1433)) // Simulated endpoint
.AddDatabase("test");
appBuilder.AddSqlProject<TestProject>("MySqlProject")
.PublishTo(targetDatabase);

// Act
using var app = appBuilder.Build();
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
var lifecycleHook = Assert.Single(app.Services.GetServices<IDistributedApplicationLifecycleHook>().OfType<PublishSqlProjectLifecycleHook>());
await lifecycleHook.AfterResourcesCreatedAsync(appModel, CancellationToken.None);

// Assert
var expectedPath = Path.GetFullPath(Path.Combine(appBuilder.AppHostDirectory, "../../../../TestProject/bin/Debug/netstandard2.0/TestProject.dacpac"));
dacDeployerMock.Received().Deploy(Arg.Is(expectedPath), Arg.Any<string>(), Arg.Is("test"), Arg.Any<ILogger>(), Arg.Any<CancellationToken>());
}
}
8 changes: 8 additions & 0 deletions test/MSBuild.Sdk.SqlProj.Aspire.Tests/TestProject.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Aspire.Hosting;

namespace MSBuild.Sdk.SqlProj.Aspire.Tests;

internal class TestProject : IProjectMetadata
{
public string ProjectPath { get; } = "../../../../TestProject/TestProject.csproj";
}

0 comments on commit f69da0a

Please sign in to comment.