diff --git a/Aspire.sln b/Aspire.sln
index f48ad76f5ab..b7547f986f8 100644
--- a/Aspire.sln
+++ b/Aspire.sln
@@ -152,6 +152,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.RabbitMQ.Client", "s
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.RabbitMQ.Client.Tests", "tests\Aspire.RabbitMQ.Client.Tests\Aspire.RabbitMQ.Client.Tests.csproj", "{165411FE-755E-4869-A756-F87F455860AC}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.MySqlConnector", "src\Components\Aspire.MySqlConnector\Aspire.MySqlConnector.csproj", "{CA283D7F-EB95-4353-B196-C409965D2B42}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.MySqlConnector.Tests", "tests\Aspire.MySqlConnector.Tests\Aspire.MySqlConnector.Tests.csproj", "{C8079F06-304F-49B1-A0C1-45AA3782A923}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -406,6 +410,14 @@ Global
{165411FE-755E-4869-A756-F87F455860AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{165411FE-755E-4869-A756-F87F455860AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{165411FE-755E-4869-A756-F87F455860AC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CA283D7F-EB95-4353-B196-C409965D2B42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CA283D7F-EB95-4353-B196-C409965D2B42}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CA283D7F-EB95-4353-B196-C409965D2B42}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CA283D7F-EB95-4353-B196-C409965D2B42}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C8079F06-304F-49B1-A0C1-45AA3782A923}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C8079F06-304F-49B1-A0C1-45AA3782A923}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C8079F06-304F-49B1-A0C1-45AA3782A923}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C8079F06-304F-49B1-A0C1-45AA3782A923}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -477,6 +489,8 @@ Global
{A84C4EE3-2601-4804-BCDC-E9948E164A22} = {A68BA1A5-1604-433D-9778-DC0199831C2A}
{4D8A92AB-4E77-4965-AD8E-8E206DCE66A4} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2}
{165411FE-755E-4869-A756-F87F455860AC} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
+ {CA283D7F-EB95-4353-B196-C409965D2B42} = {27381127-6C45-4B4C-8F18-41FF48DFE4B2}
+ {C8079F06-304F-49B1-A0C1-45AA3782A923} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6DCEDFEC-988E-4CB3-B45B-191EB5086E0C}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 4ac4bc34d87..0fc8e753757 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -35,6 +35,7 @@
+
@@ -70,6 +71,7 @@
+
diff --git a/src/Aspire.Hosting/MySql/IMySqlResource.cs b/src/Aspire.Hosting/MySql/IMySqlResource.cs
new file mode 100644
index 00000000000..c9001d4e281
--- /dev/null
+++ b/src/Aspire.Hosting/MySql/IMySqlResource.cs
@@ -0,0 +1,11 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Hosting.ApplicationModel;
+
+///
+/// Represents a MySQL resource that requires a connection string.
+///
+public interface IMySqlResource : IResourceWithConnectionString
+{
+}
diff --git a/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs b/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs
new file mode 100644
index 00000000000..027db93cb92
--- /dev/null
+++ b/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs
@@ -0,0 +1,81 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Net.Sockets;
+using System.Text.Json;
+using Aspire.Hosting.ApplicationModel;
+
+namespace Aspire.Hosting;
+
+///
+/// Provides extension methods for adding MySQL resources to an .
+///
+public static class MySqlBuilderExtensions
+{
+ private const string PasswordEnvVarName = "MYSQL_ROOT_PASSWORD";
+
+ ///
+ /// Adds a MySQL container to the application model. The default image is "mysql" and the tag is "latest".
+ ///
+ /// The .
+ /// The name of the resource. This name will be used as the connection string name when referenced in a dependency.
+ /// The host port for MySQL.
+ /// The password for the MySQL root user. Defaults to a random password.
+ /// A reference to the .
+ public static IResourceBuilder AddMySqlContainer(this IDistributedApplicationBuilder builder, string name, int? port = null, string? password = null)
+ {
+ password ??= Guid.NewGuid().ToString("N");
+ var mySqlContainer = new MySqlContainerResource(name, password);
+ return builder.AddResource(mySqlContainer)
+ .WithAnnotation(new ManifestPublishingCallbackAnnotation(WriteMySqlContainerToManifest))
+ .WithAnnotation(new ServiceBindingAnnotation(ProtocolType.Tcp, port: port, containerPort: 3306)) // Internal port is always 3306.
+ .WithAnnotation(new ContainerImageAnnotation { Image = "mysql", Tag = "latest" })
+ .WithEnvironment(PasswordEnvVarName, () => mySqlContainer.Password);
+ }
+
+ ///
+ /// Adds a MySQL connection to the application model. Connection strings can also be read from the connection string section of the configuration using the name of the resource.
+ ///
+ /// The .
+ /// The name of the resource. This name will be used as the connection string name when referenced in a dependency.
+ /// The MySQL connection string (optional).
+ /// A reference to the .
+ public static IResourceBuilder AddMySqlConnection(this IDistributedApplicationBuilder builder, string name, string? connectionString = null)
+ {
+ var mySqlConnection = new MySqlConnectionResource(name, connectionString);
+
+ return builder.AddResource(mySqlConnection)
+ .WithAnnotation(new ManifestPublishingCallbackAnnotation((json) => WriteMySqlConnectionToManifest(json, mySqlConnection)));
+ }
+
+ ///
+ /// Adds a MySQL database to the application model.
+ ///
+ /// The MySQL server resource builder.
+ /// The name of the resource. This name will be used as the connection string name when referenced in a dependency.
+ /// A reference to the .
+ public static IResourceBuilder AddDatabase(this IResourceBuilder builder, string name)
+ {
+ var mySqlDatabase = new MySqlDatabaseResource(name, builder.Resource);
+ return builder.ApplicationBuilder.AddResource(mySqlDatabase)
+ .WithAnnotation(new ManifestPublishingCallbackAnnotation(
+ (json) => WriteMySqlDatabaseToManifest(json, mySqlDatabase)));
+ }
+
+ private static void WriteMySqlConnectionToManifest(Utf8JsonWriter jsonWriter, MySqlConnectionResource mySqlConnection)
+ {
+ jsonWriter.WriteString("type", "mysql.connection.v0");
+ jsonWriter.WriteString("connectionString", mySqlConnection.GetConnectionString());
+ }
+
+ private static void WriteMySqlContainerToManifest(Utf8JsonWriter jsonWriter)
+ {
+ jsonWriter.WriteString("type", "mysql.server.v0");
+ }
+
+ private static void WriteMySqlDatabaseToManifest(Utf8JsonWriter json, MySqlDatabaseResource mySqlDatabase)
+ {
+ json.WriteString("type", "mysql.database.v0");
+ json.WriteString("parent", mySqlDatabase.Parent.Name);
+ }
+}
diff --git a/src/Aspire.Hosting/MySql/MySqlConnectionResource.cs b/src/Aspire.Hosting/MySql/MySqlConnectionResource.cs
new file mode 100644
index 00000000000..9e37e6d43d6
--- /dev/null
+++ b/src/Aspire.Hosting/MySql/MySqlConnectionResource.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Hosting.ApplicationModel;
+
+///
+/// A resource that represents a MySQL connection.
+///
+/// The name of the resource.
+/// The MySQL connection string.
+public class MySqlConnectionResource(string name, string? connectionString) : Resource(name), IMySqlResource
+{
+ private readonly string? _connectionString = connectionString;
+
+ ///
+ /// Gets the connection string for the MySQL server.
+ ///
+ /// The specified connection string.
+ public string? GetConnectionString() => _connectionString;
+}
diff --git a/src/Aspire.Hosting/MySql/MySqlContainerResource.cs b/src/Aspire.Hosting/MySql/MySqlContainerResource.cs
new file mode 100644
index 00000000000..4b5765b5f0b
--- /dev/null
+++ b/src/Aspire.Hosting/MySql/MySqlContainerResource.cs
@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Hosting.ApplicationModel;
+
+///
+/// A resource that represents a MySQL container.
+///
+/// The name of the resource.
+/// The MySQL server root password.
+public class MySqlContainerResource(string name, string password) : ContainerResource(name), IMySqlResource
+{
+ public string Password { get; } = password;
+
+ ///
+ /// Gets the connection string for the MySQL server.
+ ///
+ /// A connection string for the MySQL server in the form "Server=host;Port=port;User ID=root;Password=password".
+ public string? GetConnectionString()
+ {
+ if (!this.TryGetAllocatedEndPoints(out var allocatedEndpoints))
+ {
+ throw new DistributedApplicationException("Expected allocated endpoints!");
+ }
+
+ var allocatedEndpoint = allocatedEndpoints.Single(); // We should only have one endpoint for MySQL.
+
+ var connectionString = $"Server={allocatedEndpoint.Address};Port={allocatedEndpoint.Port};User ID=root;Password={Password};";
+ return connectionString;
+ }
+}
diff --git a/src/Aspire.Hosting/MySql/MySqlDatabaseResource.cs b/src/Aspire.Hosting/MySql/MySqlDatabaseResource.cs
new file mode 100644
index 00000000000..dcbef3b1f4b
--- /dev/null
+++ b/src/Aspire.Hosting/MySql/MySqlDatabaseResource.cs
@@ -0,0 +1,30 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Hosting.ApplicationModel;
+
+///
+/// A resource that represents a MySQL database. This is a child resource of a .
+///
+/// The name of the resource.
+/// The MySQL server resource associated with this database.
+public class MySqlDatabaseResource(string name, MySqlContainerResource mySqlContainer) : Resource(name), IMySqlResource, IResourceWithParent
+{
+ public MySqlContainerResource Parent { get; } = mySqlContainer;
+
+ ///
+ /// Gets the connection string for the MySQL database.
+ ///
+ /// A connection string for the MySQL database.
+ public string? GetConnectionString()
+ {
+ if (Parent.GetConnectionString() is { } connectionString)
+ {
+ return $"{connectionString}Database={Name}";
+ }
+ else
+ {
+ throw new DistributedApplicationException("Parent resource connection string was null.");
+ }
+ }
+}
diff --git a/src/Components/Aspire.MySqlConnector/Aspire.MySqlConnector.csproj b/src/Components/Aspire.MySqlConnector/Aspire.MySqlConnector.csproj
new file mode 100644
index 00000000000..5b5f3317a99
--- /dev/null
+++ b/src/Components/Aspire.MySqlConnector/Aspire.MySqlConnector.csproj
@@ -0,0 +1,24 @@
+
+
+
+ $(NetCurrent)
+ true
+ $(ComponentDatabasePackageTags) mysqlconnector mysql sql
+ A MySQL client that integrates with Aspire, including health checks, metrics, logging, and telemetry.
+ $(SharedDir)SQL_256x.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Components/Aspire.MySqlConnector/AspireMySqlConnectorExtensions.cs b/src/Components/Aspire.MySqlConnector/AspireMySqlConnectorExtensions.cs
new file mode 100644
index 00000000000..6bd05a09a30
--- /dev/null
+++ b/src/Components/Aspire.MySqlConnector/AspireMySqlConnectorExtensions.cs
@@ -0,0 +1,132 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire;
+using Aspire.MySqlConnector;
+using HealthChecks.MySql;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Logging;
+using MySqlConnector;
+
+namespace Microsoft.Extensions.Hosting;
+
+///
+/// Extension methods for connecting MySQL database with MySqlConnector client
+///
+public static class AspireMySqlConnectorExtensions
+{
+ private const string DefaultConfigSectionName = "Aspire:MySqlConnector";
+
+ ///
+ /// Registers service for connecting MySQL database with MySqlConnector client.
+ /// Configures health check, logging and telemetry for the MySqlConnector client.
+ ///
+ /// The to read config from and add services to.
+ /// A name used to retrieve the connection string from the ConnectionStrings configuration section.
+ /// An optional delegate that can be used for customizing options. It's invoked after the settings are read from the configuration.
+ /// Reads the configuration from "Aspire:MySqlConnector" section.
+ /// Thrown if mandatory is null.
+ /// Thrown when mandatory is not provided.
+ public static void AddMySqlDataSource(this IHostApplicationBuilder builder, string connectionName, Action? configureSettings = null)
+ => AddMySqlDataSource(builder, DefaultConfigSectionName, configureSettings, connectionName, serviceKey: null);
+
+ ///
+ /// Registers as a keyed service for given for connecting MySQL database with MySqlConnector client.
+ /// Configures health check, logging and telemetry for the MySqlConnector client.
+ ///
+ /// The to read config from and add services to.
+ /// The name of the component, which is used as the of the service and also to retrieve the connection string from the ConnectionStrings configuration section.
+ /// An optional method that can be used for customizing options. It's invoked after the settings are read from the configuration.
+ /// Reads the configuration from "Aspire:MySqlConnector:{name}" section.
+ /// Thrown when or is null.
+ /// Thrown if mandatory is empty.
+ /// Thrown when mandatory is not provided.
+ public static void AddKeyedMySqlDataSource(this IHostApplicationBuilder builder, string name, Action? configureSettings = null)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(name);
+
+ AddMySqlDataSource(builder, $"{DefaultConfigSectionName}:{name}", configureSettings, connectionName: name, serviceKey: name);
+ }
+
+ private static void AddMySqlDataSource(IHostApplicationBuilder builder, string configurationSectionName,
+ Action? configureSettings, string connectionName, object? serviceKey)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+
+ MySqlConnectorSettings settings = new();
+ builder.Configuration.GetSection(configurationSectionName).Bind(settings);
+
+ if (builder.Configuration.GetConnectionString(connectionName) is string connectionString)
+ {
+ settings.ConnectionString = connectionString;
+ }
+
+ configureSettings?.Invoke(settings);
+
+ builder.RegisterMySqlServices(settings, configurationSectionName, connectionName, serviceKey);
+
+ // Same as SqlClient connection pooling is on by default and can be handled with connection string
+ // https://mysqlconnector.net/connection-options/#Pooling
+ if (settings.HealthChecks)
+ {
+ builder.TryAddHealthCheck(new HealthCheckRegistration(
+ serviceKey is null ? "MySql" : $"MySql_{connectionName}",
+ sp => new MySqlHealthCheck(new MySqlHealthCheckOptions()
+ {
+ ConnectionString = serviceKey is null
+ ? sp.GetRequiredService().ConnectionString
+ : sp.GetRequiredKeyedService(serviceKey).ConnectionString
+ }),
+ failureStatus: default,
+ tags: default,
+ timeout: default));
+ }
+
+ if (settings.Tracing)
+ {
+ builder.Services.AddOpenTelemetry()
+ .WithTracing(tracerProviderBuilder =>
+ {
+ tracerProviderBuilder.AddSource("MySqlConnector");
+ });
+ }
+
+ if (settings.Metrics)
+ {
+ builder.Services.AddOpenTelemetry()
+ .WithMetrics(meterProviderBuilder =>
+ {
+ meterProviderBuilder.AddMeter("MySqlConnector");
+ });
+ }
+ }
+
+ private static void RegisterMySqlServices(this IHostApplicationBuilder builder, MySqlConnectorSettings settings, string configurationSectionName, string connectionName, object? serviceKey)
+ {
+ if (serviceKey is null)
+ {
+ builder.Services.AddMySqlDataSource(settings.ConnectionString ?? string.Empty, dataSourceBuilder =>
+ {
+ ValidateConnection();
+ });
+ }
+ else
+ {
+ builder.Services.AddKeyedMySqlDataSource(serviceKey, settings.ConnectionString ?? string.Empty, dataSourceBuilder =>
+ {
+ ValidateConnection();
+ });
+ }
+
+ // delay validating the ConnectionString until the DataSource is requested. This ensures an exception doesn't happen until a Logger is established.
+ void ValidateConnection()
+ {
+ if (string.IsNullOrEmpty(settings.ConnectionString))
+ {
+ throw new InvalidOperationException($"ConnectionString is missing. It should be provided in 'ConnectionStrings:{connectionName}' or under the 'ConnectionString' key in '{configurationSectionName}' configuration section.");
+ }
+ }
+ }
+}
diff --git a/src/Components/Aspire.MySqlConnector/ConfigurationSchema.json b/src/Components/Aspire.MySqlConnector/ConfigurationSchema.json
new file mode 100644
index 00000000000..cf7dceba64f
--- /dev/null
+++ b/src/Components/Aspire.MySqlConnector/ConfigurationSchema.json
@@ -0,0 +1,58 @@
+{
+ "definitions": {
+ "logLevel": {
+ "properties": {
+ "MySqlConnector": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "MySqlConnector.ConnectionPool": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "MySqlConnector.MySqlBulkCopy": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "MySqlConnector.MySqlCommand": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "MySqlConnector.MySqlConnection": {
+ "$ref": "#/definitions/logLevelThreshold"
+ },
+ "MySqlConnector.MySqlDataSource": {
+ "$ref": "#/definitions/logLevelThreshold"
+ }
+ }
+ }
+ },
+ "properties": {
+ "Aspire": {
+ "type": "object",
+ "properties": {
+ "MySqlConnector": {
+ "type": "object",
+ "properties": {
+ "ConnectionString": {
+ "type": "string",
+ "description": "Gets or sets the connection string of the MySQL database to connect to."
+ },
+ "HealthChecks": {
+ "type": "boolean",
+ "description": "Gets or sets a boolean value that indicates whether the database health check is enabled or not.",
+ "default": true
+ },
+ "Tracing": {
+ "type": "boolean",
+ "description": "Gets or sets a boolean value that indicates whether the OpenTelemetry tracing is enabled or not.",
+ "default": true
+ },
+ "Metrics": {
+ "type": "boolean",
+ "description": "Gets or sets a boolean value that indicates whether the OpenTelemetry metrics are enabled or not.",
+ "default": true
+ }
+ }
+ }
+ }
+ }
+ },
+ "type": "object"
+}
diff --git a/src/Components/Aspire.MySqlConnector/MySqlConnectorSettings.cs b/src/Components/Aspire.MySqlConnector/MySqlConnectorSettings.cs
new file mode 100644
index 00000000000..d82447ae846
--- /dev/null
+++ b/src/Components/Aspire.MySqlConnector/MySqlConnectorSettings.cs
@@ -0,0 +1,33 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.MySqlConnector;
+
+///
+/// Provides the client configuration settings for connecting to a MySQL database using MySqlConnector.
+///
+public sealed class MySqlConnectorSettings
+{
+ ///
+ /// The connection string of the MySQL database to connect to.
+ ///
+ public string? ConnectionString { get; set; }
+
+ ///
+ /// Gets or sets a boolean value that indicates whether the database health check is enabled or not.
+ /// Enabled by default.
+ ///
+ public bool HealthChecks { get; set; } = true;
+
+ ///
+ /// Gets or sets a boolean value that indicates whether the Open Telemetry tracing is enabled or not.
+ /// Enabled by default.
+ ///
+ public bool Tracing { get; set; } = true;
+
+ ///
+ /// Gets or sets a boolean value that indicates whether the Open Telemetry metrics are enabled or not.
+ /// Enabled by default.
+ ///
+ public bool Metrics { get; set; } = true;
+}
diff --git a/src/Components/Aspire.MySqlConnector/README.md b/src/Components/Aspire.MySqlConnector/README.md
new file mode 100644
index 00000000000..7dbc7db221d
--- /dev/null
+++ b/src/Components/Aspire.MySqlConnector/README.md
@@ -0,0 +1,109 @@
+# Aspire.MySqlConnector library
+
+Registers [MySqlDataSource](https://mysqlconnector.net/api/mysqlconnector/mysqldatasourcetype/) in the DI container for connecting MySQL database. Enables corresponding health check, metrics, logging and telemetry.
+
+## Getting started
+
+### Prerequisites
+
+- MySQL database and connection string for accessing the database.
+
+### Install the package
+
+Install the .NET Aspire MySQL library with [NuGet](https://www.nuget.org):
+
+```dotnetcli
+dotnet add package Aspire.MySqlConnector
+```
+
+## Usage example
+
+In the _Program.cs_ file of your project, call the `AddMyDataSource` extension method to register a `MySqlDataSource` for use via the dependency injection container. The method takes a connection name parameter.
+
+```csharp
+builder.AddMySqlDataSource("server=mysql;user id=myuser;password=mypass");
+```
+
+You can then retrieve a `MySqlConnection` instance using dependency injection. For example, to retrieve a connection from a Web API controller:
+
+```csharp
+private readonly MySqlConnection _connection;
+
+public ProductsController(MySqlConnection connection)
+{
+ _connection = connection;
+}
+```
+
+## Configuration
+
+The .NET Aspire MySQL component provides multiple options to configure the database connection based on the requirements and conventions of your project.
+
+### Use a connection string
+
+When using a connection string from the `ConnectionStrings` configuration section, you can provide the name of the connection string when calling `builder.AddMySqlDataSource()`:
+
+```csharp
+builder.AddMySqlDataSource("myConnection");
+```
+
+And then the connection string will be retrieved from the `ConnectionStrings` configuration section:
+
+```json
+{
+ "ConnectionStrings": {
+ "myConnection": "Server=mysql;Database=test"
+ }
+}
+```
+
+See the [ConnectionString documentation](https://mysqlconnector.net/connection-options/) for more information on how to format this connection string.
+
+### Use configuration providers
+
+The .NET Aspire MySQL component supports [Microsoft.Extensions.Configuration](https://learn.microsoft.com/dotnet/api/microsoft.extensions.configuration). It loads the `MySqlConnectorSettings` from configuration by using the `Aspire:MySqlConnector` key. Example `appsettings.json` that configures some of the options:
+
+```json
+{
+ "Aspire": {
+ "MySql": {
+ "HealthChecks": false,
+ "Tracing": false
+ }
+ }
+}
+```
+
+### Use inline delegates
+
+Also you can pass the `Action configureSettings` delegate to set up some or all the options inline, for example to disable health checks from code:
+
+```csharp
+ builder.AddMySqlDataSource("mysql", settings => settings.HealthChecks = false);
+```
+
+## AppHost extensions
+
+In your AppHost project, register a MySQL container and consume the connection using the following methods:
+
+```csharp
+var mysqldb = builder.AddMySqlContainer("mysql").AddDatabase("mysqldb");
+
+var myService = builder.AddProject()
+ .WithReference(mysqldb);
+```
+
+The `WithReference` method configures a connection in the `MyService` project named `mysqldb`. In the _Program.cs_ file of `MyService`, the database connection can be consumed using:
+
+```csharp
+builder.AddMySqlDataSource("mysqldb");
+```
+
+## Additional documentation
+
+* https://mysqlconnector.net/tutorials/basic-api/
+* https://github.com/dotnet/aspire/tree/main/src/Components/README.md
+
+## Feedback & contributing
+
+https://github.com/dotnet/aspire
diff --git a/src/Components/Aspire_Components_Progress.md b/src/Components/Aspire_Components_Progress.md
index bbafadd54e4..427a1ac1c66 100644
--- a/src/Components/Aspire_Components_Progress.md
+++ b/src/Components/Aspire_Components_Progress.md
@@ -20,6 +20,7 @@ As part of the .NET Aspire November preview, we want to include a set of .NET As
| StackExchange.Redis.DistributedCaching | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ❌ | ✅ |
| StackExchange.Redis.OutputCaching | ✅ | ✅ | N/A | ✅ | ✅ | ✅ | ❌ | ✅ |
| RabbitMQ | ✅ | ✅ | ✅ | ✅ | | | ❌ | ✅ |
+| MySqlConnector | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Nomenclature used in the table above:
diff --git a/src/Components/Telemetry.md b/src/Components/Telemetry.md
index b81469c7800..d55e0b7543d 100644
--- a/src/Components/Telemetry.md
+++ b/src/Components/Telemetry.md
@@ -128,6 +128,27 @@ Aspire.Microsoft.EntityFrameworkCore.SqlServer:
- "ec_Microsoft_EntityFramew_total_optimistic_concurrency_failures"
- "ec_Microsoft_EntityF_optimistic_concurrency_failures_per_second"
+Aspire.MySqlConnector:
+- Log categories:
+ - "MySqlConnector.ConnectionPool"
+ - "MySqlConnector.MySqlBulkCopy"
+ - "MySqlConnector.MySqlCommand"
+ - "MySqlConnector.MySqlConnection"
+ - "MySqlConnector.MySqlDataSource"
+- Activity source names:
+ - "MySqlConnector"
+- Metric names:
+ - "MySqlConnector":
+ - "db.client.connections.create_time"
+ - "db.client.connections.use_time"
+ - "db.client.connections.wait_time"
+ - "db.client.connections.idle.max"
+ - "db.client.connections.idle.min"
+ - "db.client.connections.max"
+ - "db.client.connections.pending_requests"
+ - "db.client.connections.timeouts"
+ - "db.client.connections.usage"
+
Aspire.Npgsql:
- Log categories:
- "Npgsql.Command"
diff --git a/tests/Aspire.MySqlConnector.Tests/Aspire.MySqlConnector.Tests.csproj b/tests/Aspire.MySqlConnector.Tests/Aspire.MySqlConnector.Tests.csproj
new file mode 100644
index 00000000000..ab93f67996d
--- /dev/null
+++ b/tests/Aspire.MySqlConnector.Tests/Aspire.MySqlConnector.Tests.csproj
@@ -0,0 +1,12 @@
+
+
+
+ $(NetCurrent)
+
+
+
+
+
+
+
+
diff --git a/tests/Aspire.MySqlConnector.Tests/AspireMySqlConnectorExtensionsTests.cs b/tests/Aspire.MySqlConnector.Tests/AspireMySqlConnectorExtensionsTests.cs
new file mode 100644
index 00000000000..148da3e1c03
--- /dev/null
+++ b/tests/Aspire.MySqlConnector.Tests/AspireMySqlConnectorExtensionsTests.cs
@@ -0,0 +1,104 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using MySqlConnector;
+using Xunit;
+
+namespace Aspire.MySqlConnector.Tests;
+
+public class AspireMySqlConnectorExtensionsTests
+{
+ private const string ConnectionString = "Server=localhost;Database=test_aspire_mysql";
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ReadsFromConnectionStringsCorrectly(bool useKeyed)
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair("ConnectionStrings:mysql", ConnectionString)
+ ]);
+
+ if (useKeyed)
+ {
+ builder.AddKeyedMySqlDataSource("mysql");
+ }
+ else
+ {
+ builder.AddMySqlDataSource("mysql");
+ }
+
+ var host = builder.Build();
+ var dataSource = useKeyed ?
+ host.Services.GetRequiredKeyedService("mysql") :
+ host.Services.GetRequiredService();
+
+ Assert.Equal(ConnectionString, dataSource.ConnectionString);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ConnectionStringCanBeSetInCode(bool useKeyed)
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair("ConnectionStrings:mysql", "unused")
+ ]);
+
+ static void SetConnectionString(MySqlConnectorSettings settings) => settings.ConnectionString = ConnectionString;
+ if (useKeyed)
+ {
+ builder.AddKeyedMySqlDataSource("mysql", SetConnectionString);
+ }
+ else
+ {
+ builder.AddMySqlDataSource("mysql", SetConnectionString);
+ }
+
+ var host = builder.Build();
+ var dataSource = useKeyed ?
+ host.Services.GetRequiredKeyedService("mysql") :
+ host.Services.GetRequiredService();
+
+ Assert.Equal(ConnectionString, dataSource.ConnectionString);
+ // the connection string from config should not be used since code set it explicitly
+ Assert.DoesNotContain("unused", dataSource.ConnectionString);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void ConnectionNameWinsOverConfigSection(bool useKeyed)
+ {
+ var builder = Host.CreateEmptyApplicationBuilder(null);
+
+ var key = useKeyed ? "mysql" : null;
+ builder.Configuration.AddInMemoryCollection([
+ new KeyValuePair(ConformanceTests.CreateConfigKey("Aspire:MySqlConnector", key, "ConnectionString"), "unused"),
+ new KeyValuePair("ConnectionStrings:mysql", ConnectionString)
+ ]);
+
+ if (useKeyed)
+ {
+ builder.AddKeyedMySqlDataSource("mysql");
+ }
+ else
+ {
+ builder.AddMySqlDataSource("mysql");
+ }
+
+ var host = builder.Build();
+ var dataSource = useKeyed ?
+ host.Services.GetRequiredKeyedService("mysql") :
+ host.Services.GetRequiredService();
+
+ Assert.Equal(ConnectionString, dataSource.ConnectionString);
+ // the connection string from config should not be used since it was found in ConnectionStrings
+ Assert.DoesNotContain("unused", dataSource.ConnectionString);
+ }
+}
diff --git a/tests/Aspire.MySqlConnector.Tests/ConfigurationTests.cs b/tests/Aspire.MySqlConnector.Tests/ConfigurationTests.cs
new file mode 100644
index 00000000000..62bb5460412
--- /dev/null
+++ b/tests/Aspire.MySqlConnector.Tests/ConfigurationTests.cs
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace Aspire.MySqlConnector.Tests;
+
+public class ConfigurationTests
+{
+ [Fact]
+ public void ConnectionStringIsNullByDefault()
+ => Assert.Null(new MySqlConnectorSettings().ConnectionString);
+
+ [Fact]
+ public void HealthCheckIsEnabledByDefault()
+ => Assert.True(new MySqlConnectorSettings().HealthChecks);
+
+ [Fact]
+ public void TracingIsEnabledByDefault()
+ => Assert.True(new MySqlConnectorSettings().Tracing);
+
+ [Fact]
+ public void MetricsAreEnabledByDefault()
+ => Assert.True(new MySqlConnectorSettings().Metrics);
+}
diff --git a/tests/Aspire.MySqlConnector.Tests/ConformanceTests.cs b/tests/Aspire.MySqlConnector.Tests/ConformanceTests.cs
new file mode 100644
index 00000000000..305eb71f998
--- /dev/null
+++ b/tests/Aspire.MySqlConnector.Tests/ConformanceTests.cs
@@ -0,0 +1,160 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Data.Common;
+using Aspire.Components.ConformanceTests;
+using Microsoft.DotNet.RemoteExecutor;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using MySqlConnector;
+using Xunit;
+
+namespace Aspire.MySqlConnector.Tests;
+
+public class ConformanceTests : ConformanceTests
+{
+ private const string ConnectionSting = "Host=localhost;Database=test_aspire_mysql;Username=root;Password=password";
+
+ private static readonly Lazy s_canConnectToServer = new(GetCanConnect);
+
+ protected override ServiceLifetime ServiceLifetime => ServiceLifetime.Singleton;
+
+ // https://github.com/mysql-net/MySqlConnector/blob/d895afc013a5849d33a123a7061442e2cbb9ce76/src/MySqlConnector/Utilities/ActivitySourceHelper.cs#L61
+ protected override string ActivitySourceName => "MySqlConnector";
+
+ protected override string[] RequiredLogCategories => [
+ "MySqlConnector.ConnectionPool",
+ "MySqlConnector.MySqlBulkCopy",
+ "MySqlConnector.MySqlCommand",
+ "MySqlConnector.MySqlConnection",
+ "MySqlConnector.MySqlDataSource",
+ ];
+
+ protected override bool SupportsKeyedRegistrations => true;
+
+ protected override bool CanConnectToServer => s_canConnectToServer.Value;
+
+ protected override string JsonSchemaPath => "src/Components/Aspire.MySqlConnector/ConfigurationSchema.json";
+
+ protected override string ValidJsonConfig => """
+ {
+ "Aspire": {
+ "MySqlConnector": {
+ "ConnectionString": "YOUR_CONNECTION_STRING",
+ "HealthChecks": false,
+ "Tracing": true,
+ "Metrics": true
+ }
+ }
+ }
+ """;
+
+ protected override (string json, string error)[] InvalidJsonToErrorMessage => new[]
+ {
+ ("""{"Aspire": { "MySqlConnector":{ "Metrics": 0}}}""", "Value is \"integer\" but should be \"boolean\""),
+ ("""{"Aspire": { "MySqlConnector":{ "ConnectionString": "Con", "HealthChecks": "false"}}}""", "Value is \"string\" but should be \"boolean\"")
+ };
+
+ protected override void PopulateConfiguration(ConfigurationManager configuration, string? key = null)
+ => configuration.AddInMemoryCollection(new KeyValuePair[1]
+ {
+ new KeyValuePair(CreateConfigKey("Aspire:MySqlConnector", key, "ConnectionString"), ConnectionSting)
+ });
+
+ protected override void RegisterComponent(HostApplicationBuilder builder, Action? configure = null, string? key = null)
+ {
+ if (key is null)
+ {
+ builder.AddMySqlDataSource("mysql", configure);
+ }
+ else
+ {
+ builder.AddKeyedMySqlDataSource(key, configure);
+ }
+ }
+
+ protected override void SetHealthCheck(MySqlConnectorSettings options, bool enabled)
+ => options.HealthChecks = enabled;
+
+ protected override void SetTracing(MySqlConnectorSettings options, bool enabled)
+ => options.Tracing = enabled;
+
+ protected override void SetMetrics(MySqlConnectorSettings options, bool enabled)
+ => options.Metrics = enabled;
+
+ protected override void TriggerActivity(MySqlDataSource service)
+ {
+ using MySqlConnection connection = service.CreateConnection();
+ connection.Open();
+ using MySqlCommand command = connection.CreateCommand();
+ command.CommandText = "Select 1;";
+ command.ExecuteScalar();
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("key")]
+ public void BothDataSourceAndConnectionCanBeResolved(string? key)
+ {
+ using IHost host = CreateHostWithComponent(key: key);
+
+ MySqlDataSource? mySqlDataSource = Resolve();
+ DbDataSource? dbDataSource = Resolve();
+ MySqlConnection? mySqlConnection = Resolve();
+ DbConnection? dbConnection = Resolve();
+
+ Assert.NotNull(mySqlDataSource);
+ Assert.Same(mySqlDataSource, dbDataSource);
+
+ Assert.NotNull(mySqlConnection);
+ Assert.NotNull(dbConnection);
+
+ Assert.Equal(dbConnection.ConnectionString, mySqlConnection.ConnectionString);
+ Assert.Equal(mySqlDataSource.ConnectionString, mySqlConnection.ConnectionString);
+
+ T? Resolve() => key is null ? host.Services.GetService() : host.Services.GetKeyedService(key);
+ }
+
+ [ConditionalFact]
+ public void TracingEnablesTheRightActivitySource()
+ {
+ SkipIfCanNotConnectToServer();
+
+ RemoteExecutor.Invoke(() => ActivitySourceTest(key: null)).Dispose();
+ }
+
+ [ConditionalFact]
+ public void TracingEnablesTheRightActivitySource_Keyed()
+ {
+ SkipIfCanNotConnectToServer();
+
+ RemoteExecutor.Invoke(() => ActivitySourceTest(key: "key")).Dispose();
+ }
+
+ private static bool GetCanConnect()
+ {
+ using MySqlConnection connection = new(ConnectionSting);
+
+ try
+ {
+ // clear the database from the connection string so we can create it
+ var builder = new MySqlConnectionStringBuilder(connection.ConnectionString);
+ string dbName = connection.Database;
+ builder.Database = null;
+
+ using var noDatabaseConnection = new MySqlConnection(builder.ConnectionString);
+
+ noDatabaseConnection.Open();
+
+ using var cmd = new MySqlCommand($"CREATE DATABASE IF NOT EXISTS `{dbName}`", noDatabaseConnection);
+ cmd.ExecuteNonQuery();
+ }
+ catch (Exception)
+ {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/tests/Aspire.Npgsql.Tests/ConformanceTests.cs b/tests/Aspire.Npgsql.Tests/ConformanceTests.cs
index 4544b8f8c31..1c66f6f3832 100644
--- a/tests/Aspire.Npgsql.Tests/ConformanceTests.cs
+++ b/tests/Aspire.Npgsql.Tests/ConformanceTests.cs
@@ -42,13 +42,11 @@ public class ConformanceTests : ConformanceTests """
{
"Aspire": {
- "PostgreSql": {
- "Npgsql": {
- "ConnectionString": "YOUR_CONNECTION_STRING",
- "HealthChecks": false,
- "Tracing": true,
- "Metrics": true
- }
+ "Npgsql": {
+ "ConnectionString": "YOUR_CONNECTION_STRING",
+ "HealthChecks": false,
+ "Tracing": true,
+ "Metrics": true
}
}
}