From abb3d0b9fa2591e258622427816203ba9a85c427 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Tue, 14 Nov 2023 16:05:05 -0800 Subject: [PATCH 01/11] Add Aspire.MySqlConnector. Fixes #786 Using the component name "Aspire.MySqlConnector" to leave the door open for "Aspire.MySql.Data" (based on the MySql.Data library) in the future. --- Aspire.sln | 7 + Directory.Packages.props | 2 + .../Aspire.MySqlConnector.csproj | 24 +++ .../AspireMySqlConnectorExtensions.cs | 141 ++++++++++++++++++ .../ConfigurationSchema.json | 58 +++++++ .../MySqlConnectorCommon.cs | 14 ++ .../MySqlConnectorSettings.cs | 33 ++++ .../Aspire.MySqlConnector/README.md | 92 ++++++++++++ src/Components/Telemetry.md | 20 +++ 9 files changed, 391 insertions(+) create mode 100644 src/Components/Aspire.MySqlConnector/Aspire.MySqlConnector.csproj create mode 100644 src/Components/Aspire.MySqlConnector/AspireMySqlConnectorExtensions.cs create mode 100644 src/Components/Aspire.MySqlConnector/ConfigurationSchema.json create mode 100644 src/Components/Aspire.MySqlConnector/MySqlConnectorCommon.cs create mode 100644 src/Components/Aspire.MySqlConnector/MySqlConnectorSettings.cs create mode 100644 src/Components/Aspire.MySqlConnector/README.md diff --git a/Aspire.sln b/Aspire.sln index f48ad76f5a..1daf4ea13d 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -152,6 +152,8 @@ 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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -406,6 +408,10 @@ 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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -477,6 +483,7 @@ 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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6DCEDFEC-988E-4CB3-B45B-191EB5086E0C} diff --git a/Directory.Packages.props b/Directory.Packages.props index de33cba0b4..d94ef61457 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -38,6 +38,7 @@ + @@ -73,6 +74,7 @@ + diff --git a/src/Components/Aspire.MySqlConnector/Aspire.MySqlConnector.csproj b/src/Components/Aspire.MySqlConnector/Aspire.MySqlConnector.csproj new file mode 100644 index 0000000000..5b5f3317a9 --- /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 0000000000..bc8065cf71 --- /dev/null +++ b/src/Components/Aspire.MySqlConnector/AspireMySqlConnectorExtensions.cs @@ -0,0 +1,141 @@ +// 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; +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:MySql"; + + /// + /// 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:MySql" 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:MySql:{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(MySqlConnectorCommon.AddMySqlMetrics); + } + } + + private static void RegisterMySqlServices(this IHostApplicationBuilder builder, MySqlConnectorSettings settings, string configurationSectionName, string connectionName, object? serviceKey) + { + if (serviceKey is null) + { + // delay validating the ConnectionString until the DataSource is requested. This ensures an exception doesn't happen until a Logger is established. + builder.Services.AddMySqlDataSource(settings.ConnectionString ?? string.Empty, dataSourceBuilder => + { + ValidateConnection(); + }); + } + else + { + // Currently MySqlConnector does not support Keyed DI Registration, so we implement it on our own. + // Register a MySqlDataSource factory method, based on https://github.com/mysql-net/MySqlConnector/blob/d895afc013a5849d33a123a7061442e2cbb9ce76/src/MySqlConnector.DependencyInjection/MySqlConnectorServiceCollectionExtensions.cs#L57-L60 + builder.Services.AddKeyedSingleton(serviceKey, (serviceProvider, _) => + { + ValidateConnection(); + + var dataSourceBuilder = new MySqlDataSourceBuilder(settings.ConnectionString); + dataSourceBuilder.UseLoggerFactory(serviceProvider.GetService()); + return dataSourceBuilder.Build(); + }); + // Common Services, based on https://github.com/mysql-net/MySqlConnector/blob/d895afc013a5849d33a123a7061442e2cbb9ce76/src/MySqlConnector.DependencyInjection/MySqlConnectorServiceCollectionExtensions.cs#L64-L70 + // They let the users resolve MySqlConnection directly. + builder.Services.AddKeyedSingleton(serviceKey, static (serviceProvider, key) => serviceProvider.GetRequiredKeyedService(key)); + builder.Services.AddKeyedTransient(serviceKey, static (serviceProvider, key) => serviceProvider.GetRequiredKeyedService(key).CreateConnection()); + builder.Services.AddKeyedTransient(serviceKey, static (serviceProvider, key) => serviceProvider.GetRequiredKeyedService(key)); + } + + 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 0000000000..d6ee6684a3 --- /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": { + "MySql": { + "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/MySqlConnectorCommon.cs b/src/Components/Aspire.MySqlConnector/MySqlConnectorCommon.cs new file mode 100644 index 0000000000..3c98bedf77 --- /dev/null +++ b/src/Components/Aspire.MySqlConnector/MySqlConnectorCommon.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using OpenTelemetry.Metrics; + +internal static class MySqlConnectorCommon +{ + public static void AddMySqlMetrics(MeterProviderBuilder meterProviderBuilder) + { + meterProviderBuilder + .AddMeter("MySqlConnector") + .AddView("db.client.connections.create_time", new HistogramConfiguration()); + } +} diff --git a/src/Components/Aspire.MySqlConnector/MySqlConnectorSettings.cs b/src/Components/Aspire.MySqlConnector/MySqlConnectorSettings.cs new file mode 100644 index 0000000000..d82447ae84 --- /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 0000000000..02d4652815 --- /dev/null +++ b/src/Components/Aspire.MySqlConnector/README.md @@ -0,0 +1,92 @@ +# 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": "Host=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:MySql` 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); +``` + +## 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/Telemetry.md b/src/Components/Telemetry.md index b81469c780..0a8a13d0ae 100644 --- a/src/Components/Telemetry.md +++ b/src/Components/Telemetry.md @@ -128,6 +128,26 @@ 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.usage" + Aspire.Npgsql: - Log categories: - "Npgsql.Command" From 0c3f02098666402690399d311d80846864db52f3 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Tue, 14 Nov 2023 18:36:39 -0800 Subject: [PATCH 02/11] Rename config section to Aspire:MySqlConnector. --- .../AspireMySqlConnectorExtensions.cs | 8 ++++---- src/Components/Aspire.MySqlConnector/README.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Components/Aspire.MySqlConnector/AspireMySqlConnectorExtensions.cs b/src/Components/Aspire.MySqlConnector/AspireMySqlConnectorExtensions.cs index bc8065cf71..908074c2af 100644 --- a/src/Components/Aspire.MySqlConnector/AspireMySqlConnectorExtensions.cs +++ b/src/Components/Aspire.MySqlConnector/AspireMySqlConnectorExtensions.cs @@ -18,7 +18,7 @@ namespace Microsoft.Extensions.Hosting; /// public static class AspireMySqlConnectorExtensions { - private const string DefaultConfigSectionName = "Aspire:MySql"; + private const string DefaultConfigSectionName = "Aspire:MySqlConnector"; /// /// Registers service for connecting MySQL database with MySqlConnector client. @@ -27,7 +27,7 @@ public static class AspireMySqlConnectorExtensions /// 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:MySql" section. + /// 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) @@ -40,7 +40,7 @@ public static void AddMySqlDataSource(this IHostApplicationBuilder builder, stri /// 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:MySql:{name}" section. + /// Reads the configuration from "Aspire:MySqlConnector:{name}" section. /// Thrown when or is null. /// Thrown if mandatory is empty. /// Thrown when mandatory is not provided. @@ -68,7 +68,7 @@ private static void AddMySqlDataSource(IHostApplicationBuilder builder, string c builder.RegisterMySqlServices(settings, configurationSectionName, connectionName, serviceKey); - // Same as SqlClient connection pooling is on by default and can be handled with connection string + // 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) { diff --git a/src/Components/Aspire.MySqlConnector/README.md b/src/Components/Aspire.MySqlConnector/README.md index 02d4652815..3dd8a57228 100644 --- a/src/Components/Aspire.MySqlConnector/README.md +++ b/src/Components/Aspire.MySqlConnector/README.md @@ -61,7 +61,7 @@ See the [ConnectionString documentation](https://mysqlconnector.net/connection-o ### 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:MySql` key. Example `appsettings.json` that configures some of the options: +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 { From 1deac05fc8d29d367682246038b38a830f803724 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Tue, 14 Nov 2023 18:47:28 -0800 Subject: [PATCH 03/11] Inline AddMeter call. --- .../AspireMySqlConnectorExtensions.cs | 5 ++++- .../Aspire.MySqlConnector/MySqlConnectorCommon.cs | 14 -------------- 2 files changed, 4 insertions(+), 15 deletions(-) delete mode 100644 src/Components/Aspire.MySqlConnector/MySqlConnectorCommon.cs diff --git a/src/Components/Aspire.MySqlConnector/AspireMySqlConnectorExtensions.cs b/src/Components/Aspire.MySqlConnector/AspireMySqlConnectorExtensions.cs index 908074c2af..a63367cdab 100644 --- a/src/Components/Aspire.MySqlConnector/AspireMySqlConnectorExtensions.cs +++ b/src/Components/Aspire.MySqlConnector/AspireMySqlConnectorExtensions.cs @@ -97,7 +97,10 @@ private static void AddMySqlDataSource(IHostApplicationBuilder builder, string c if (settings.Metrics) { builder.Services.AddOpenTelemetry() - .WithMetrics(MySqlConnectorCommon.AddMySqlMetrics); + .WithMetrics(meterProviderBuilder => + { + meterProviderBuilder.AddMeter("MySqlConnector"); + }); } } diff --git a/src/Components/Aspire.MySqlConnector/MySqlConnectorCommon.cs b/src/Components/Aspire.MySqlConnector/MySqlConnectorCommon.cs deleted file mode 100644 index 3c98bedf77..0000000000 --- a/src/Components/Aspire.MySqlConnector/MySqlConnectorCommon.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using OpenTelemetry.Metrics; - -internal static class MySqlConnectorCommon -{ - public static void AddMySqlMetrics(MeterProviderBuilder meterProviderBuilder) - { - meterProviderBuilder - .AddMeter("MySqlConnector") - .AddView("db.client.connections.create_time", new HistogramConfiguration()); - } -} From b6a5e0eac4ea6cf67163a6dadf5e70a7d74bb762 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Tue, 14 Nov 2023 19:29:10 -0800 Subject: [PATCH 04/11] Add AppHost for MySQL. --- src/Aspire.Hosting/MySql/IMySqlResource.cs | 11 +++ .../MySql/MySqlBuilderExtensions.cs | 81 +++++++++++++++++++ .../MySql/MySqlConnectionResource.cs | 20 +++++ .../MySql/MySqlContainerResource.cs | 31 +++++++ .../MySql/MySqlDatabaseResource.cs | 30 +++++++ .../Aspire.MySqlConnector/README.md | 17 ++++ 6 files changed, 190 insertions(+) create mode 100644 src/Aspire.Hosting/MySql/IMySqlResource.cs create mode 100644 src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs create mode 100644 src/Aspire.Hosting/MySql/MySqlConnectionResource.cs create mode 100644 src/Aspire.Hosting/MySql/MySqlContainerResource.cs create mode 100644 src/Aspire.Hosting/MySql/MySqlDatabaseResource.cs diff --git a/src/Aspire.Hosting/MySql/IMySqlResource.cs b/src/Aspire.Hosting/MySql/IMySqlResource.cs new file mode 100644 index 0000000000..c9001d4e28 --- /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 0000000000..027db93cb9 --- /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 0000000000..9e37e6d43d --- /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 0000000000..74c66d99c0 --- /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 "Host=host;Port=port;Username=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 = $"Host={allocatedEndpoint.Address};Port={allocatedEndpoint.Port};Username=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 0000000000..dcbef3b1f4 --- /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/README.md b/src/Components/Aspire.MySqlConnector/README.md index 3dd8a57228..217ccbb1a3 100644 --- a/src/Components/Aspire.MySqlConnector/README.md +++ b/src/Components/Aspire.MySqlConnector/README.md @@ -82,6 +82,23 @@ Also you can pass the `Action configureSettings` delegat 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/ From 3255804eb406439fe7576c9140b5f5d43f3cfd42 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Tue, 14 Nov 2023 21:27:43 -0800 Subject: [PATCH 05/11] Rename config section to Aspire:MySqlConnector. --- src/Components/Aspire.MySqlConnector/ConfigurationSchema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Components/Aspire.MySqlConnector/ConfigurationSchema.json b/src/Components/Aspire.MySqlConnector/ConfigurationSchema.json index d6ee6684a3..cf7dceba64 100644 --- a/src/Components/Aspire.MySqlConnector/ConfigurationSchema.json +++ b/src/Components/Aspire.MySqlConnector/ConfigurationSchema.json @@ -27,7 +27,7 @@ "Aspire": { "type": "object", "properties": { - "MySql": { + "MySqlConnector": { "type": "object", "properties": { "ConnectionString": { From 5b16736bf5353635ce196506134fbc3df7a21646 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Tue, 14 Nov 2023 21:28:37 -0800 Subject: [PATCH 06/11] Add Aspire.MySqlConnector.Tests. --- Aspire.sln | 7 + .../Aspire.MySqlConnector/README.md | 2 +- .../Aspire.MySqlConnector.Tests.csproj | 12 ++ .../AspireMySqlConnectorExtensionsTests.cs | 104 +++++++++++ .../ConfigurationTests.cs | 25 +++ .../ConformanceTests.cs | 162 ++++++++++++++++++ 6 files changed, 311 insertions(+), 1 deletion(-) create mode 100644 tests/Aspire.MySqlConnector.Tests/Aspire.MySqlConnector.Tests.csproj create mode 100644 tests/Aspire.MySqlConnector.Tests/AspireMySqlConnectorExtensionsTests.cs create mode 100644 tests/Aspire.MySqlConnector.Tests/ConfigurationTests.cs create mode 100644 tests/Aspire.MySqlConnector.Tests/ConformanceTests.cs diff --git a/Aspire.sln b/Aspire.sln index 1daf4ea13d..b7547f986f 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -154,6 +154,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.RabbitMQ.Client.Test 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 @@ -412,6 +414,10 @@ Global {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 @@ -484,6 +490,7 @@ Global {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/src/Components/Aspire.MySqlConnector/README.md b/src/Components/Aspire.MySqlConnector/README.md index 217ccbb1a3..7dbc7db221 100644 --- a/src/Components/Aspire.MySqlConnector/README.md +++ b/src/Components/Aspire.MySqlConnector/README.md @@ -52,7 +52,7 @@ And then the connection string will be retrieved from the `ConnectionStrings` co ```json { "ConnectionStrings": { - "myConnection": "Host=mysql;Database=test" + "myConnection": "Server=mysql;Database=test" } } ``` 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 0000000000..ab93f67996 --- /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 0000000000..148da3e1c0 --- /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 0000000000..62bb546041 --- /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 0000000000..380c924f49 --- /dev/null +++ b/tests/Aspire.MySqlConnector.Tests/ConformanceTests.cs @@ -0,0 +1,162 @@ +// 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": { + "Npgsql": { + "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; + } +} From 9f85ccf02e2db724f989e49d34596be086bda12d Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Wed, 15 Nov 2023 15:40:01 -0800 Subject: [PATCH 07/11] Use MySqlConnector conventions for connection string options. --- src/Aspire.Hosting/MySql/MySqlContainerResource.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Aspire.Hosting/MySql/MySqlContainerResource.cs b/src/Aspire.Hosting/MySql/MySqlContainerResource.cs index 74c66d99c0..4b5765b5f0 100644 --- a/src/Aspire.Hosting/MySql/MySqlContainerResource.cs +++ b/src/Aspire.Hosting/MySql/MySqlContainerResource.cs @@ -15,7 +15,7 @@ public class MySqlContainerResource(string name, string password) : ContainerRes /// /// Gets the connection string for the MySQL server. /// - /// A connection string for the MySQL server in the form "Host=host;Port=port;Username=root;Password=password". + /// 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)) @@ -25,7 +25,7 @@ public class MySqlContainerResource(string name, string password) : ContainerRes var allocatedEndpoint = allocatedEndpoints.Single(); // We should only have one endpoint for MySQL. - var connectionString = $"Host={allocatedEndpoint.Address};Port={allocatedEndpoint.Port};Username=root;Password={Password};"; + var connectionString = $"Server={allocatedEndpoint.Address};Port={allocatedEndpoint.Port};User ID=root;Password={Password};"; return connectionString; } } From 68077a27ca3f04061c4396b7c56e8dc4691494fa Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Wed, 15 Nov 2023 15:43:19 -0800 Subject: [PATCH 08/11] Remove extraneous configuration level in JSON. --- .../Aspire.MySqlConnector.Tests/ConformanceTests.cs | 10 ++++------ tests/Aspire.Npgsql.Tests/ConformanceTests.cs | 12 +++++------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/tests/Aspire.MySqlConnector.Tests/ConformanceTests.cs b/tests/Aspire.MySqlConnector.Tests/ConformanceTests.cs index 380c924f49..305eb71f99 100644 --- a/tests/Aspire.MySqlConnector.Tests/ConformanceTests.cs +++ b/tests/Aspire.MySqlConnector.Tests/ConformanceTests.cs @@ -41,12 +41,10 @@ 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 } } } From b1c30a9e31b33ca789c709e15211319017488d34 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Wed, 15 Nov 2023 15:47:45 -0800 Subject: [PATCH 09/11] Add MySqlConnector to component progress documentation. All requirements are met except for public API review. --- src/Components/Aspire_Components_Progress.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Components/Aspire_Components_Progress.md b/src/Components/Aspire_Components_Progress.md index bbafadd54e..427a1ac1c6 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: From a64653f4c4ef8a3902b3fbff04b936a64a7695ae Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Thu, 16 Nov 2023 17:55:26 -0800 Subject: [PATCH 10/11] Update MySqlConnector to 2.3.1. This allows us to use the built-in support for keyed services. --- Directory.Packages.props | 2 +- .../AspireMySqlConnectorExtensions.cs | 16 ++-------------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 679a152691..877c50482c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -74,7 +74,7 @@ - + diff --git a/src/Components/Aspire.MySqlConnector/AspireMySqlConnectorExtensions.cs b/src/Components/Aspire.MySqlConnector/AspireMySqlConnectorExtensions.cs index a63367cdab..6bd05a09a3 100644 --- a/src/Components/Aspire.MySqlConnector/AspireMySqlConnectorExtensions.cs +++ b/src/Components/Aspire.MySqlConnector/AspireMySqlConnectorExtensions.cs @@ -1,7 +1,6 @@ // 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; using Aspire.MySqlConnector; using HealthChecks.MySql; @@ -108,7 +107,6 @@ private static void RegisterMySqlServices(this IHostApplicationBuilder builder, { if (serviceKey is null) { - // delay validating the ConnectionString until the DataSource is requested. This ensures an exception doesn't happen until a Logger is established. builder.Services.AddMySqlDataSource(settings.ConnectionString ?? string.Empty, dataSourceBuilder => { ValidateConnection(); @@ -116,23 +114,13 @@ private static void RegisterMySqlServices(this IHostApplicationBuilder builder, } else { - // Currently MySqlConnector does not support Keyed DI Registration, so we implement it on our own. - // Register a MySqlDataSource factory method, based on https://github.com/mysql-net/MySqlConnector/blob/d895afc013a5849d33a123a7061442e2cbb9ce76/src/MySqlConnector.DependencyInjection/MySqlConnectorServiceCollectionExtensions.cs#L57-L60 - builder.Services.AddKeyedSingleton(serviceKey, (serviceProvider, _) => + builder.Services.AddKeyedMySqlDataSource(serviceKey, settings.ConnectionString ?? string.Empty, dataSourceBuilder => { ValidateConnection(); - - var dataSourceBuilder = new MySqlDataSourceBuilder(settings.ConnectionString); - dataSourceBuilder.UseLoggerFactory(serviceProvider.GetService()); - return dataSourceBuilder.Build(); }); - // Common Services, based on https://github.com/mysql-net/MySqlConnector/blob/d895afc013a5849d33a123a7061442e2cbb9ce76/src/MySqlConnector.DependencyInjection/MySqlConnectorServiceCollectionExtensions.cs#L64-L70 - // They let the users resolve MySqlConnection directly. - builder.Services.AddKeyedSingleton(serviceKey, static (serviceProvider, key) => serviceProvider.GetRequiredKeyedService(key)); - builder.Services.AddKeyedTransient(serviceKey, static (serviceProvider, key) => serviceProvider.GetRequiredKeyedService(key).CreateConnection()); - builder.Services.AddKeyedTransient(serviceKey, static (serviceProvider, key) => serviceProvider.GetRequiredKeyedService(key)); } + // 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)) From ec14198a63efc4feb6cad5d399b0bd5720429dc6 Mon Sep 17 00:00:00 2001 From: Bradley Grainger Date: Fri, 17 Nov 2023 10:07:14 -0800 Subject: [PATCH 11/11] Add new counter in MySqlConnector 2.3.1. --- src/Components/Telemetry.md | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Components/Telemetry.md b/src/Components/Telemetry.md index 0a8a13d0ae..d55e0b7543 100644 --- a/src/Components/Telemetry.md +++ b/src/Components/Telemetry.md @@ -146,6 +146,7 @@ Aspire.MySqlConnector: - "db.client.connections.idle.min" - "db.client.connections.max" - "db.client.connections.pending_requests" + - "db.client.connections.timeouts" - "db.client.connections.usage" Aspire.Npgsql: