Skip to content

MySql.AddDatabase creates database for resource #8912

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
Apr 29, 2025
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
3 changes: 3 additions & 0 deletions playground/mysql/MySql.ApiService/MySql.ApiService.http
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ DELETE {{HostAddress}}/catalog/4
Accept: application/json

###

GET {{HostAddress}}/myTestDb2/
Accept: application/json
12 changes: 12 additions & 0 deletions playground/mysql/MySql.ApiService/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

builder.Services.AddProblemDetails();
builder.AddMySqlDataSource("Catalog");
builder.AddKeyedMySqlDataSource("myTestDb2");

var app = builder.Build();

Expand Down Expand Up @@ -63,6 +64,17 @@ DELETE FROM catalog
return rows > 0 ? Results.NoContent() : Results.NotFound();
});

app.MapGet("/myTestDb2", async ([FromKeyedServices("myTestDb2")] MySqlConnection db) =>
{
const string sql = """
SELECT id, name
FROM example_table
""";

return await db.QueryAsync<ExampleTableItem>(sql);
});

app.Run();

public record CatalogItem(int Id, string Name, string Description, decimal Price);
public record ExampleTableItem(int Id, string Name);
27 changes: 23 additions & 4 deletions playground/mysql/MySqlDb.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,33 @@
var builder = DistributedApplication.CreateBuilder(args);

var catalogDbName = "catalog"; // MySql database & table names are case-sensitive on non-Windows.
var catalogDb = builder.AddMySql("mysql")
var mySql = builder.AddMySql("mysql")
.WithEnvironment("MYSQL_DATABASE", catalogDbName)
.WithBindMount("../MySql.ApiService/data", "/docker-entrypoint-initdb.d")
.WithPhpMyAdmin()
.AddDatabase(catalogDbName);
.WithPhpMyAdmin();

var catalogDb = mySql.AddDatabase(catalogDbName);

var myTestDb = mySql.AddDatabase("myTestDb");

var myTestDb2 = mySql.AddDatabase("myTestDb2").WithCreationScript($"""

CREATE DATABASE IF NOT EXISTS `myTestDb2`;

USE myTestDb2;

CREATE TABLE IF NOT EXISTS example_table (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL
);

INSERT INTO example_table (name) VALUES ('Example Name 1');
""");

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

builder.Build().Run();
97 changes: 95 additions & 2 deletions src/Aspire.Hosting.MySql/MySqlBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.MySql;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MySqlConnector;

namespace Aspire.Hosting;

Expand Down Expand Up @@ -48,6 +50,27 @@ public static IResourceBuilder<MySqlServerResource> AddMySql(this IDistributedAp
}
});

builder.Eventing.Subscribe<ResourceReadyEvent>(resource, async (@event, ct) =>
{
if (connectionString is null)
{
throw new DistributedApplicationException($"ResourceReadyEvent was published for the '{resource.Name}' resource but the connection string was null.");
}

using var sqlConnection = new MySqlConnection(connectionString);
await sqlConnection.OpenAsync(ct).ConfigureAwait(false);

if (sqlConnection.State != System.Data.ConnectionState.Open)
{
throw new InvalidOperationException($"Could not open connection to '{resource.Name}'");
}

foreach (var sqlDatabase in resource.DatabaseResources)
{
await CreateDatabaseAsync(sqlConnection, sqlDatabase, @event.Services, ct).ConfigureAwait(false);
}
});

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

Expand Down Expand Up @@ -77,9 +100,79 @@ public static IResourceBuilder<MySqlDatabaseResource> AddDatabase(this IResource
// Use the resource name as the database name if it's not provided
databaseName ??= name;

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

builder.Resource.AddDatabase(mySqlDatabase);

string? connectionString = null;

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

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

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

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

private static async Task CreateDatabaseAsync(MySqlConnection sqlConnection, MySqlDatabaseResource sqlDatabase, IServiceProvider serviceProvider, CancellationToken ct)
{
var logger = serviceProvider.GetRequiredService<ResourceLoggerService>().GetLogger(sqlDatabase.Parent);

logger.LogDebug("Creating database '{DatabaseName}'", sqlDatabase.DatabaseName);

try
{
var scriptAnnotation = sqlDatabase.Annotations.OfType<MySqlCreateDatabaseScriptAnnotation>().LastOrDefault();

if (scriptAnnotation?.Script is null)
{
var quotedDatabaseIdentifier = new MySqlCommandBuilder().QuoteIdentifier(sqlDatabase.DatabaseName);
using var command = sqlConnection.CreateCommand();
command.CommandText = $"CREATE DATABASE IF NOT EXISTS {quotedDatabaseIdentifier};";
await command.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
}
else
{
using var command = sqlConnection.CreateCommand();
command.CommandText = scriptAnnotation.Script;
await command.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
}

logger.LogDebug("Database '{DatabaseName}' created successfully", sqlDatabase.DatabaseName);
}
catch (Exception e)
{
logger.LogError(e, "Failed to create database '{DatabaseName}'", sqlDatabase.DatabaseName);
}
}

/// <summary>
/// Defines the SQL script used to create the database.
/// </summary>
/// <param name="builder">The builder for the <see cref="MySqlDatabaseResource"/>.</param>
/// <param name="script">The SQL script used to create the database.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
/// <remarks>
/// <value>Default script is <code>CREATE DATABASE IF NOT EXISTS `QUOTED_DATABASE_NAME`;</code></value>
/// </remarks>
public static IResourceBuilder<MySqlDatabaseResource> WithCreationScript(this IResourceBuilder<MySqlDatabaseResource> builder, string script)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(script);

builder.WithAnnotation(new MySqlCreateDatabaseScriptAnnotation(script));

return builder;
}

/// <summary>
Expand Down
27 changes: 27 additions & 0 deletions src/Aspire.Hosting.MySql/MySqlCreateDatabaseScriptAnnotation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Aspire.Hosting.ApplicationModel;

namespace Aspire.Hosting;

/// <summary>
/// Represents an annotation for defining a script to create a database in MySql.
/// </summary>
internal sealed class MySqlCreateDatabaseScriptAnnotation : IResourceAnnotation
{
/// <summary>
/// Initializes a new instance of the <see cref="MySqlCreateDatabaseScriptAnnotation"/> class.
/// </summary>
/// <param name="script">The script used to create the database.</param>
public MySqlCreateDatabaseScriptAnnotation(string script)
{
ArgumentNullException.ThrowIfNull(script);
Script = script;
}

/// <summary>
/// Gets the script used to create the database.
/// </summary>
public string Script { get; }
}
14 changes: 12 additions & 2 deletions src/Aspire.Hosting.MySql/MySqlDatabaseResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using MySqlConnector;

namespace Aspire.Hosting.ApplicationModel;

Expand All @@ -23,9 +24,18 @@ public class MySqlDatabaseResource(string name, string databaseName, MySqlServer
/// <summary>
/// Gets the connection string expression for the MySQL database.
/// </summary>
public ReferenceExpression ConnectionStringExpression =>
ReferenceExpression.Create($"{Parent};Database={DatabaseName}");
public ReferenceExpression ConnectionStringExpression
{
get
{
var connectionStringBuilder = new MySqlConnectionStringBuilder
{
["Database"] = DatabaseName
};

return ReferenceExpression.Create($"{Parent};{connectionStringBuilder.ToString()}");
}
}
/// <summary>
/// Gets the database name.
/// </summary>
Expand Down
12 changes: 8 additions & 4 deletions src/Aspire.Hosting.MySql/MySqlServerResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ public class MySqlServerResource : ContainerResource, IResourceWithConnectionStr
{
internal static string PrimaryEndpointName => "tcp";

private readonly Dictionary<string, string> _databases = new(StringComparers.ResourceName);
private readonly List<MySqlDatabaseResource> _databaseResources = [];

/// <summary>
/// Initializes a new instance of the <see cref="MySqlServerResource"/> class.
/// </summary>
Expand Down Expand Up @@ -40,15 +43,16 @@ public MySqlServerResource(string name, ParameterResource password) : base(name)
ReferenceExpression.Create(
$"Server={PrimaryEndpoint.Property(EndpointProperty.Host)};Port={PrimaryEndpoint.Property(EndpointProperty.Port)};User ID=root;Password={PasswordParameter}");

private readonly Dictionary<string, string> _databases = new Dictionary<string, string>(StringComparers.ResourceName);

/// <summary>
/// A dictionary where the key is the resource name and the value is the database name.
/// </summary>
public IReadOnlyDictionary<string, string> Databases => _databases;

internal void AddDatabase(string name, string databaseName)
internal IReadOnlyList<MySqlDatabaseResource> DatabaseResources => _databaseResources;

internal void AddDatabase(MySqlDatabaseResource database)
{
_databases.TryAdd(name, databaseName);
_databases.TryAdd(database.Name, database.DatabaseName);
_databaseResources.Add(database);
}
}
Loading
Loading