Skip to content

Commit 9a818b6

Browse files
committed
MySql.AddDatabase creates database for resource
Resolves #8294
1 parent 81710b9 commit 9a818b6

File tree

8 files changed

+417
-12
lines changed

8 files changed

+417
-12
lines changed

playground/mysql/MySql.ApiService/MySql.ApiService.http

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@ DELETE {{HostAddress}}/catalog/4
2424
Accept: application/json
2525

2626
###
27+
28+
GET {{HostAddress}}/myTestDb2/
29+
Accept: application/json

playground/mysql/MySql.ApiService/Program.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
builder.Services.AddProblemDetails();
1414
builder.AddMySqlDataSource("Catalog");
15+
builder.AddKeyedMySqlDataSource("myTestDb2");
1516

1617
var app = builder.Build();
1718

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

67+
app.MapGet("/myTestDb2", async ([FromKeyedServices("myTestDb2")] MySqlConnection db) =>
68+
{
69+
const string sql = """
70+
SELECT id, name
71+
FROM example_table
72+
""";
73+
74+
return await db.QueryAsync<ExampleTableItem>(sql);
75+
});
76+
6677
app.Run();
6778

6879
public record CatalogItem(int Id, string Name, string Description, decimal Price);
80+
public record ExampleTableItem(int Id, string Name);

playground/mysql/MySqlDb.AppHost/Program.cs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,33 @@
44
var builder = DistributedApplication.CreateBuilder(args);
55

66
var catalogDbName = "catalog"; // MySql database & table names are case-sensitive on non-Windows.
7-
var catalogDb = builder.AddMySql("mysql")
7+
var mySql = builder.AddMySql("mysql")
88
.WithEnvironment("MYSQL_DATABASE", catalogDbName)
99
.WithBindMount("../MySql.ApiService/data", "/docker-entrypoint-initdb.d")
10-
.WithPhpMyAdmin()
11-
.AddDatabase(catalogDbName);
10+
.WithPhpMyAdmin();
11+
12+
var catalogDb = mySql.AddDatabase(catalogDbName);
13+
14+
var myTestDb = mySql.AddDatabase("myTestDb");
15+
16+
var myTestDb2 = mySql.AddDatabase("myTestDb2").WithCreationScript($"""
17+
18+
CREATE DATABASE IF NOT EXISTS `myTestDb2`;
19+
20+
USE myTestDb2;
21+
22+
CREATE TABLE IF NOT EXISTS example_table (
23+
id INT AUTO_INCREMENT PRIMARY KEY,
24+
name VARCHAR(255) NOT NULL
25+
);
26+
27+
INSERT INTO example_table (name) VALUES ('Example Name 1');
28+
""");
1229

1330
builder.AddProject<Projects.MySql_ApiService>("apiservice")
1431
.WithExternalHttpEndpoints()
15-
.WithReference(catalogDb).WaitFor(catalogDb);
32+
.WithReference(catalogDb).WaitFor(catalogDb)
33+
.WithReference(myTestDb).WaitFor(myTestDb)
34+
.WithReference(myTestDb2).WaitFor(myTestDb2);
1635

1736
builder.Build().Run();

src/Aspire.Hosting.MySql/MySqlBuilderExtensions.cs

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
using Aspire.Hosting.ApplicationModel;
66
using Aspire.Hosting.MySql;
77
using Microsoft.Extensions.DependencyInjection;
8+
using Microsoft.Extensions.Logging;
9+
using MySqlConnector;
810

911
namespace Aspire.Hosting;
1012

@@ -48,6 +50,27 @@ public static IResourceBuilder<MySqlServerResource> AddMySql(this IDistributedAp
4850
}
4951
});
5052

53+
builder.Eventing.Subscribe<ResourceReadyEvent>(resource, async (@event, ct) =>
54+
{
55+
if (connectionString is null)
56+
{
57+
throw new DistributedApplicationException($"ResourceReadyEvent was published for the '{resource.Name}' resource but the connection string was null.");
58+
}
59+
60+
using var sqlConnection = new MySqlConnection(connectionString);
61+
await sqlConnection.OpenAsync(ct).ConfigureAwait(false);
62+
63+
if (sqlConnection.State != System.Data.ConnectionState.Open)
64+
{
65+
throw new InvalidOperationException($"Could not open connection to '{resource.Name}'");
66+
}
67+
68+
foreach (var sqlDatabase in resource.DatabaseResources)
69+
{
70+
await CreateDatabaseAsync(sqlConnection, sqlDatabase, @event.Services, ct).ConfigureAwait(false);
71+
}
72+
});
73+
5174
var healthCheckKey = $"{name}_check";
5275
builder.Services.AddHealthChecks().AddMySql(sp => connectionString ?? throw new InvalidOperationException("Connection string is unavailable"), name: healthCheckKey);
5376

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

80-
builder.Resource.AddDatabase(name, databaseName);
81103
var mySqlDatabase = new MySqlDatabaseResource(name, databaseName, builder.Resource);
82-
return builder.ApplicationBuilder.AddResource(mySqlDatabase);
104+
105+
builder.Resource.AddDatabase(mySqlDatabase);
106+
107+
string? connectionString = null;
108+
109+
builder.ApplicationBuilder.Eventing.Subscribe<ConnectionStringAvailableEvent>(mySqlDatabase, async (@event, ct) =>
110+
{
111+
connectionString = await mySqlDatabase.ConnectionStringExpression.GetValueAsync(ct).ConfigureAwait(false);
112+
113+
if (connectionString is null)
114+
{
115+
throw new DistributedApplicationException($"ConnectionStringAvailableEvent was published for the '{name}' resource but the connection string was null.");
116+
}
117+
});
118+
119+
var healthCheckKey = $"{name}_check";
120+
builder.ApplicationBuilder.Services.AddHealthChecks().AddMySql(sp => connectionString ?? throw new InvalidOperationException("Connection string is unavailable"), name: healthCheckKey);
121+
122+
return builder.ApplicationBuilder
123+
.AddResource(mySqlDatabase)
124+
.WithHealthCheck(healthCheckKey);
125+
}
126+
127+
private static async Task CreateDatabaseAsync(MySqlConnection sqlConnection, MySqlDatabaseResource sqlDatabase, IServiceProvider serviceProvider, CancellationToken ct)
128+
{
129+
var logger = serviceProvider.GetRequiredService<ResourceLoggerService>().GetLogger(sqlDatabase.Parent);
130+
131+
logger.LogDebug("Creating database '{DatabaseName}'", sqlDatabase.DatabaseName);
132+
133+
try
134+
{
135+
var scriptAnnotation = sqlDatabase.Annotations.OfType<MySqlCreateDatabaseScriptAnnotation>().LastOrDefault();
136+
137+
if (scriptAnnotation?.Script is null)
138+
{
139+
var quotedDatabaseIdentifier = new MySqlCommandBuilder().QuoteIdentifier(sqlDatabase.DatabaseName);
140+
using var command = sqlConnection.CreateCommand();
141+
command.CommandText = $"CREATE DATABASE IF NOT EXISTS {quotedDatabaseIdentifier};";
142+
await command.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
143+
}
144+
else
145+
{
146+
using var command = sqlConnection.CreateCommand();
147+
command.CommandText = scriptAnnotation.Script;
148+
await command.ExecuteNonQueryAsync(ct).ConfigureAwait(false);
149+
}
150+
151+
logger.LogDebug("Database '{DatabaseName}' created successfully", sqlDatabase.DatabaseName);
152+
}
153+
catch (Exception e)
154+
{
155+
logger.LogError(e, "Failed to create database '{DatabaseName}'", sqlDatabase.DatabaseName);
156+
}
157+
}
158+
159+
/// <summary>
160+
/// Defines the SQL script used to create the database.
161+
/// </summary>
162+
/// <param name="builder">The builder for the <see cref="MySqlDatabaseResource"/>.</param>
163+
/// <param name="script">The SQL script used to create the database.</param>
164+
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
165+
/// <remarks>
166+
/// <value>Default script is <code>CREATE DATABASE IF NOT EXISTS `QUOTED_DATABASE_NAME`;</code></value>
167+
/// </remarks>
168+
public static IResourceBuilder<MySqlDatabaseResource> WithCreationScript(this IResourceBuilder<MySqlDatabaseResource> builder, string script)
169+
{
170+
ArgumentNullException.ThrowIfNull(builder);
171+
ArgumentNullException.ThrowIfNull(script);
172+
173+
builder.WithAnnotation(new MySqlCreateDatabaseScriptAnnotation(script));
174+
175+
return builder;
83176
}
84177

85178
/// <summary>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Hosting.ApplicationModel;
5+
6+
namespace Aspire.Hosting;
7+
8+
/// <summary>
9+
/// Represents an annotation for defining a script to create a database in MySql.
10+
/// </summary>
11+
internal sealed class MySqlCreateDatabaseScriptAnnotation : IResourceAnnotation
12+
{
13+
/// <summary>
14+
/// Initializes a new instance of the <see cref="MySqlCreateDatabaseScriptAnnotation"/> class.
15+
/// </summary>
16+
/// <param name="script">The script used to create the database.</param>
17+
public MySqlCreateDatabaseScriptAnnotation(string script)
18+
{
19+
ArgumentNullException.ThrowIfNull(script);
20+
Script = script;
21+
}
22+
23+
/// <summary>
24+
/// Gets the script used to create the database.
25+
/// </summary>
26+
public string Script { get; }
27+
}

src/Aspire.Hosting.MySql/MySqlDatabaseResource.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Diagnostics.CodeAnalysis;
55
using System.Runtime.CompilerServices;
6+
using MySqlConnector;
67

78
namespace Aspire.Hosting.ApplicationModel;
89

@@ -23,9 +24,18 @@ public class MySqlDatabaseResource(string name, string databaseName, MySqlServer
2324
/// <summary>
2425
/// Gets the connection string expression for the MySQL database.
2526
/// </summary>
26-
public ReferenceExpression ConnectionStringExpression =>
27-
ReferenceExpression.Create($"{Parent};Database={DatabaseName}");
27+
public ReferenceExpression ConnectionStringExpression
28+
{
29+
get
30+
{
31+
var connectionStringBuilder = new MySqlConnectionStringBuilder
32+
{
33+
["Database"] = DatabaseName
34+
};
2835

36+
return ReferenceExpression.Create($"{Parent};{connectionStringBuilder.ToString()}");
37+
}
38+
}
2939
/// <summary>
3040
/// Gets the database name.
3141
/// </summary>

src/Aspire.Hosting.MySql/MySqlServerResource.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ public class MySqlServerResource : ContainerResource, IResourceWithConnectionStr
1010
{
1111
internal static string PrimaryEndpointName => "tcp";
1212

13+
private readonly Dictionary<string, string> _databases = new(StringComparers.ResourceName);
14+
private readonly List<MySqlDatabaseResource> _databaseResources = [];
15+
1316
/// <summary>
1417
/// Initializes a new instance of the <see cref="MySqlServerResource"/> class.
1518
/// </summary>
@@ -40,15 +43,16 @@ public MySqlServerResource(string name, ParameterResource password) : base(name)
4043
ReferenceExpression.Create(
4144
$"Server={PrimaryEndpoint.Property(EndpointProperty.Host)};Port={PrimaryEndpoint.Property(EndpointProperty.Port)};User ID=root;Password={PasswordParameter}");
4245

43-
private readonly Dictionary<string, string> _databases = new Dictionary<string, string>(StringComparers.ResourceName);
44-
4546
/// <summary>
4647
/// A dictionary where the key is the resource name and the value is the database name.
4748
/// </summary>
4849
public IReadOnlyDictionary<string, string> Databases => _databases;
4950

50-
internal void AddDatabase(string name, string databaseName)
51+
internal IReadOnlyList<MySqlDatabaseResource> DatabaseResources => _databaseResources;
52+
53+
internal void AddDatabase(MySqlDatabaseResource database)
5154
{
52-
_databases.TryAdd(name, databaseName);
55+
_databases.TryAdd(database.Name, database.DatabaseName);
56+
_databaseResources.Add(database);
5357
}
5458
}

0 commit comments

Comments
 (0)