Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion playground/mysql/MySqlDb.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@

builder.AddProject<Projects.MySql_ApiService>("apiservice")
.WithExternalHttpEndpoints()
.WithReference(catalogDb);
.WithReference(catalogDb).WaitFor(catalogDb);

builder.Build().Run();
4 changes: 4 additions & 0 deletions src/Aspire.Hosting.MySql/Aspire.Hosting.MySql.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="AspNetCore.HealthChecks.MySql" />
</ItemGroup>

<ItemGroup>
<!-- Required for PhpMyAdminConfigWriterHook -->
<InternalsVisibleTo Include="Aspire.Hosting.MySql.Tests" />
</ItemGroup>
Expand Down
55 changes: 53 additions & 2 deletions src/Aspire.Hosting.MySql/MySqlBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.MySql;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;

namespace Aspire.Hosting;

Expand All @@ -30,14 +31,47 @@ public static IResourceBuilder<MySqlServerResource> AddMySql(this IDistributedAp
var passwordParameter = password?.Resource ?? ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(builder, $"{name}-password");

var resource = new MySqlServerResource(name, passwordParameter);

string? connectionString = null;

builder.Eventing.Subscribe<ConnectionStringAvailableEvent>(resource, async (@event, ct) =>
{
connectionString = await resource.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);

if (connectionString == null)
{
throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{resource.Name}' resource but the connection string was null.");
}

var lookup = builder.Resources.OfType<MySqlDatabaseResource>().ToDictionary(d => d.Name);

foreach (var databaseName in resource.Databases)
{
if (!lookup.TryGetValue(databaseName.Key, out var databaseResource))
{
throw new DistributedApplicationException($"Database resource '{databaseName}' under SQL Server resource '{resource.Name}' was not found in the model.");
}

var connectionStringAvailableEvent = new ConnectionStringAvailableEvent(databaseResource, @event.Services);
await builder.Eventing.PublishAsync<ConnectionStringAvailableEvent>(connectionStringAvailableEvent, ct).ConfigureAwait(false);

var beforeResourceStartedEvent = new BeforeResourceStartedEvent(databaseResource, @event.Services);
await builder.Eventing.PublishAsync(beforeResourceStartedEvent, ct).ConfigureAwait(false);
Comment on lines +58 to +59
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is incorrect but we will do a pass.

}
});

var healthCheckKey = $"{name}_check";
builder.Services.AddHealthChecks().AddMySql(sp => connectionString ?? throw new InvalidOperationException("Connection string is unavailable"), name: healthCheckKey);

return builder.AddResource(resource)
.WithEndpoint(port: port, targetPort: 3306, name: MySqlServerResource.PrimaryEndpointName) // Internal port is always 3306.
.WithImage(MySqlContainerImageTags.Image, MySqlContainerImageTags.Tag)
.WithImageRegistry(MySqlContainerImageTags.Registry)
.WithEnvironment(context =>
{
context.EnvironmentVariables[PasswordEnvVarName] = resource.PasswordParameter;
});
})
.WithHealthCheck(healthCheckKey);
}

/// <summary>
Expand All @@ -57,7 +91,24 @@ public static IResourceBuilder<MySqlDatabaseResource> AddDatabase(this IResource

builder.Resource.AddDatabase(name, databaseName);
var mySqlDatabase = new MySqlDatabaseResource(name, databaseName, builder.Resource);
return builder.ApplicationBuilder.AddResource(mySqlDatabase);

string? connectionString = null;

builder.ApplicationBuilder.Eventing.Subscribe<ConnectionStringAvailableEvent>(mySqlDatabase, async (@event, ct) =>
{
connectionString = await mySqlDatabase.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);

if (connectionString == null)
{
throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{mySqlDatabase.Name}' resource but the connection string was null.");
}
});

var healthCheckKey = $"{name}_check";
builder.ApplicationBuilder.Services.AddHealthChecks().AddMySql(sp => connectionString!, name: healthCheckKey);

return builder.ApplicationBuilder.AddResource(mySqlDatabase)
.WithHealthCheck(healthCheckKey);
}

/// <summary>
Expand Down
97 changes: 97 additions & 0 deletions tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@

using System.Data;
using Aspire.Components.Common.Tests;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Tests.Utils;
using Aspire.Hosting.Utils;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using MySqlConnector;
using Polly;
Expand All @@ -22,6 +24,101 @@ public class MySqlFunctionalTests(ITestOutputHelper testOutputHelper)
{
private static readonly Predicate<string> s_mySqlReadyText = log => log.Contains("ready for connections") && log.Contains("port: 3306");

[Fact]
[RequiresDocker]
public async Task VerifyWaitForOnMySqlBlocksDependentResources()
{
var cts = new CancellationTokenSource(TimeSpan.FromMinutes(3));
using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);

var healthCheckTcs = new TaskCompletionSource<HealthCheckResult>();
builder.Services.AddHealthChecks().AddAsyncCheck("blocking_check", () =>
{
return healthCheckTcs.Task;
});

var resource = builder.AddMySql("resource")
.WithHealthCheck("blocking_check");

var dependentResource = builder.AddMySql("dependentresource")
.WaitFor(resource);

using var app = builder.Build();

var pendingStart = app.StartAsync(cts.Token);

var rns = app.Services.GetRequiredService<ResourceNotificationService>();

await rns.WaitForResourceAsync(resource.Resource.Name, KnownResourceStates.Running, cts.Token);

await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Waiting, cts.Token);

healthCheckTcs.SetResult(HealthCheckResult.Healthy());

await rns.WaitForResourceAsync(resource.Resource.Name, (re => re.Snapshot.HealthStatus == HealthStatus.Healthy), cts.Token);

await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Running, cts.Token);

await pendingStart;

await app.StopAsync();
}

[Fact]
[RequiresDocker]
public async Task VerifyWaitForOnMySqlDatabaseBlocksDependentResources()
{
var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);

var healthCheckTcs = new TaskCompletionSource<HealthCheckResult>();
builder.Services.AddHealthChecks().AddAsyncCheck("blocking_check", () =>
{
return healthCheckTcs.Task;
});

var resource = builder.AddMySql("resource")
.WithHealthCheck("blocking_check");

var db = resource.AddDatabase("db");

var dependentResource = builder.AddMySql("dependentresource")
.WaitFor(db);

using var app = builder.Build();

var pendingStart = app.StartAsync(cts.Token);

var rns = app.Services.GetRequiredService<ResourceNotificationService>();

await rns.WaitForResourceAsync(resource.Resource.Name, KnownResourceStates.Running, cts.Token);

await rns.WaitForResourceAsync(db.Resource.Name, KnownResourceStates.Running, cts.Token);

await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Waiting, cts.Token);

healthCheckTcs.SetResult(HealthCheckResult.Healthy());

await rns.WaitForResourceAsync(resource.Resource.Name, (re => re.Snapshot.HealthStatus == HealthStatus.Healthy), cts.Token);

// Create the database.
var connectionString = await resource.Resource.ConnectionStringExpression.GetValueAsync(cts.Token);
using var connection = new MySqlConnection(connectionString);
await connection.OpenAsync(cts.Token);

var command = connection.CreateCommand();
command.CommandText = "CREATE DATABASE db;";
await command.ExecuteNonQueryAsync(cts.Token);

await rns.WaitForResourceAsync(db.Resource.Name, re => re.Snapshot.HealthStatus == HealthStatus.Healthy, cts.Token);

await rns.WaitForResourceAsync(dependentResource.Resource.Name, KnownResourceStates.Running, cts.Token);

await pendingStart;

await app.StopAsync();
}

[Fact]
[RequiresDocker]
public async Task VerifyMySqlResource()
Expand Down
2 changes: 1 addition & 1 deletion tests/Aspire.Hosting.Redis.Tests/RedisFunctionalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public async Task VerifyWaitForOnRedisBlocksDependentResources()
});

var redis = builder.AddRedis("redis")
.WithHealthCheck("blocking_check");
.WithHealthCheck("blocking_check");

var dependentResource = builder.AddRedis("dependentresource")
.WaitFor(redis); // Just using another redis instance as a dependent resource.
Expand Down