Skip to content

Commit 5be2f55

Browse files
authored
Use MySqlDataSource in AddMySql by default (#2096)
* Send a MySQL ping packet by default. Fixes #2031 Users can opt in to setting a command that will be executed on the server, but the default is now a more efficient ping. * Add AddMySql overload for MySqlDataSource. This pushes users towards good defaults: firstly registering a MySqlDataSource as a singleton in DI, then just calling .AddHealthChecks().AddMySql() with no other arguments to retrieve and use that same data source for health checks. * Add package README for HealthChecks.MySql. * Use new MySqlHealthCheckOptions API. There are two constructors: one to initialise with a MySqlDataSource, and one with a connection string. This helps enforce correct uasge of the API by ensuring that exactly one is set.
1 parent fb04fe8 commit 5be2f55

File tree

8 files changed

+241
-39
lines changed

8 files changed

+241
-39
lines changed

src/HealthChecks.MySql/DependencyInjection/MySqlHealthCheckBuilderExtensions.cs

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,50 @@ namespace Microsoft.Extensions.DependencyInjection;
1010
public static class MySqlHealthCheckBuilderExtensions
1111
{
1212
private const string NAME = "mysql";
13-
internal const string HEALTH_QUERY = "SELECT 1;";
13+
14+
/// <summary>
15+
/// Add a health check for MySQL databases.
16+
/// </summary>
17+
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
18+
/// <param name="dataSourceFactory">An optional factory to create the <see cref="MySqlDataSource"/>. By default, one will be retrieved from the service collection.</param>
19+
/// <param name="healthQuery">The optional query to be executed. If this is <c>null</c>, a MySQL "ping" packet will be sent to the server instead of a query.</param>
20+
/// <param name="configure">An optional action to allow additional MySQL specific configuration.</param>
21+
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'mysql' will be used for the name.</param>
22+
/// <param name="failureStatus">
23+
/// The <see cref="HealthStatus"/> that should be reported when the health check fails. Optional. If <c>null</c> then
24+
/// the default status of <see cref="HealthStatus.Unhealthy"/> will be reported.
25+
/// </param>
26+
/// <param name="tags">A list of tags that can be used to filter sets of health checks. Optional.</param>
27+
/// <param name="timeout">An optional <see cref="TimeSpan"/> representing the timeout of the check.</param>
28+
/// <returns>The specified <paramref name="builder"/>.</returns>
29+
public static IHealthChecksBuilder AddMySql(
30+
this IHealthChecksBuilder builder,
31+
Func<IServiceProvider, MySqlDataSource>? dataSourceFactory = null,
32+
string? healthQuery = null,
33+
Action<MySqlConnection>? configure = null,
34+
string? name = default,
35+
HealthStatus? failureStatus = default,
36+
IEnumerable<string>? tags = default,
37+
TimeSpan? timeout = default)
38+
{
39+
return builder.Add(new HealthCheckRegistration(
40+
name ?? NAME,
41+
sp => new MySqlHealthCheck(new(dataSourceFactory?.Invoke(sp) ?? sp.GetRequiredService<MySqlDataSource>())
42+
{
43+
CommandText = healthQuery,
44+
Configure = configure,
45+
}),
46+
failureStatus,
47+
tags,
48+
timeout));
49+
}
1450

1551
/// <summary>
1652
/// Add a health check for MySQL databases.
1753
/// </summary>
1854
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
1955
/// <param name="connectionString">The MySQL connection string to be used.</param>
20-
/// <param name="healthQuery">The query to be executed.</param>
56+
/// <param name="healthQuery">The optional query to be executed. If this is <c>null</c>, a MySQL "ping" packet will be sent to the server instead of a query.</param>
2157
/// <param name="configure">An optional action to allow additional MySQL specific configuration.</param>
2258
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'mysql' will be used for the name.</param>
2359
/// <param name="failureStatus">
@@ -30,22 +66,28 @@ public static class MySqlHealthCheckBuilderExtensions
3066
public static IHealthChecksBuilder AddMySql(
3167
this IHealthChecksBuilder builder,
3268
string connectionString,
33-
string healthQuery = HEALTH_QUERY,
69+
string? healthQuery = null,
3470
Action<MySqlConnection>? configure = null,
3571
string? name = default,
3672
HealthStatus? failureStatus = default,
3773
IEnumerable<string>? tags = default,
3874
TimeSpan? timeout = default)
3975
{
40-
return builder.AddMySql(_ => connectionString, healthQuery, configure, name, failureStatus, tags, timeout);
76+
Guard.ThrowIfNull(connectionString, throwOnEmptyString: true);
77+
78+
return builder.AddMySql(new(connectionString)
79+
{
80+
CommandText = healthQuery,
81+
Configure = configure
82+
}, name, failureStatus, tags, timeout);
4183
}
4284

4385
/// <summary>
4486
/// Add a health check for MySQL databases.
4587
/// </summary>
4688
/// <param name="builder">The <see cref="IHealthChecksBuilder"/>.</param>
4789
/// <param name="connectionStringFactory">A factory to build the MySQL connection string to use.</param>
48-
/// <param name="healthQuery">The query to be executed.</param>
90+
/// <param name="healthQuery">The optional query to be executed. If this is <c>null</c>, a MySQL "ping" packet will be sent to the server instead of a query.</param>
4991
/// <param name="configure">An optional action to allow additional MySQL specific configuration.</param>
5092
/// <param name="name">The health check name. Optional. If <c>null</c> the type name 'mysql' will be used for the name.</param>
5193
/// <param name="failureStatus">
@@ -58,7 +100,7 @@ public static IHealthChecksBuilder AddMySql(
58100
public static IHealthChecksBuilder AddMySql(
59101
this IHealthChecksBuilder builder,
60102
Func<IServiceProvider, string> connectionStringFactory,
61-
string healthQuery = HEALTH_QUERY,
103+
string? healthQuery = null,
62104
Action<MySqlConnection>? configure = null,
63105
string? name = default,
64106
HealthStatus? failureStatus = default,
@@ -69,16 +111,11 @@ public static IHealthChecksBuilder AddMySql(
69111

70112
return builder.Add(new HealthCheckRegistration(
71113
name ?? NAME,
72-
sp =>
114+
sp => new MySqlHealthCheck(new(connectionStringFactory(sp))
73115
{
74-
var options = new MySqlHealthCheckOptions
75-
{
76-
ConnectionString = connectionStringFactory(sp),
77-
CommandText = healthQuery,
78-
Configure = configure,
79-
};
80-
return new MySqlHealthCheck(options);
81-
},
116+
CommandText = healthQuery,
117+
Configure = configure,
118+
}),
82119
failureStatus,
83120
tags,
84121
timeout));

src/HealthChecks.MySql/MySqlHealthCheck.cs

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ public class MySqlHealthCheck : IHealthCheck
1212

1313
public MySqlHealthCheck(MySqlHealthCheckOptions options)
1414
{
15-
Guard.ThrowIfNull(options.ConnectionString, true);
16-
Guard.ThrowIfNull(options.CommandText, true);
15+
Guard.ThrowIfNull(options);
16+
if (options.DataSource is null && options.ConnectionString is null)
17+
throw new InvalidOperationException("One of options.DataSource or options.ConnectionString must be specified.");
18+
if (options.DataSource is not null && options.ConnectionString is not null)
19+
throw new InvalidOperationException("Only one of options.DataSource or options.ConnectionString must be specified.");
1720
_options = options;
1821
}
1922

@@ -22,18 +25,30 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
2225
{
2326
try
2427
{
25-
using var connection = new MySqlConnection(_options.ConnectionString);
28+
using var connection = _options.DataSource is not null ?
29+
_options.DataSource.CreateConnection() :
30+
new MySqlConnection(_options.ConnectionString);
2631

2732
_options.Configure?.Invoke(connection);
2833
await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
2934

30-
using var command = connection.CreateCommand();
31-
command.CommandText = _options.CommandText;
32-
object? result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
35+
if (_options.CommandText is { } commandText)
36+
{
37+
using var command = connection.CreateCommand();
38+
command.CommandText = _options.CommandText;
39+
object? result = await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false);
3340

34-
return _options.HealthCheckResultBuilder == null
35-
? HealthCheckResult.Healthy()
36-
: _options.HealthCheckResultBuilder(result);
41+
return _options.HealthCheckResultBuilder == null
42+
? HealthCheckResult.Healthy()
43+
: _options.HealthCheckResultBuilder(result);
44+
}
45+
else
46+
{
47+
var success = await connection.PingAsync(cancellationToken).ConfigureAwait(false);
48+
return _options.HealthCheckResultBuilder is null
49+
? (success ? HealthCheckResult.Healthy() : new HealthCheckResult(context.Registration.FailureStatus)) :
50+
_options.HealthCheckResultBuilder(success);
51+
}
3752
}
3853
catch (Exception ex)
3954
{

src/HealthChecks.MySql/MySqlHealthCheckOptions.cs

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using Microsoft.Extensions.DependencyInjection;
21
using Microsoft.Extensions.Diagnostics.HealthChecks;
32
using MySqlConnector;
43

@@ -8,16 +7,55 @@ namespace HealthChecks.MySql;
87
/// Options for <see cref="MySqlHealthCheck"/>.
98
/// </summary>
109
public class MySqlHealthCheckOptions
11-
{
10+
{
11+
/// <summary>
12+
/// Creates an instance of <see cref="MySqlHealthCheckOptions"/>.
13+
/// </summary>
14+
/// <param name="dataSource">The <see cref="MySqlDataSource" /> to be used.</param>
15+
/// <remarks>
16+
/// Depending on how the <see cref="MySqlDataSource" /> was configured, the connections it hands out may be pooled.
17+
/// That is why it should be the exact same <see cref="MySqlDataSource" /> that is used by other parts of your app.
18+
/// </remarks>
19+
public MySqlHealthCheckOptions(MySqlDataSource dataSource)
20+
{
21+
DataSource = Guard.ThrowIfNull(dataSource);
22+
}
23+
24+
/// <summary>
25+
/// Creates an instance of <see cref="MySqlHealthCheckOptions"/>.
26+
/// </summary>
27+
/// <param name="connectionString">The MySQL connection string to be used.</param>
28+
/// <remarks>
29+
/// <see cref="MySqlDataSource"/> supports additional configuration beyond the connection string, such as logging and naming pools for diagnostics.
30+
/// To specify a data source, use <see cref=" MySqlDataSourceBuilder"/> and the <see cref="MySqlHealthCheckOptions(MySqlDataSource)"/> constructor.
31+
/// </remarks>
32+
public MySqlHealthCheckOptions(string connectionString)
33+
{
34+
ConnectionString = Guard.ThrowIfNull(connectionString, throwOnEmptyString: true);
35+
}
36+
37+
/// <summary>
38+
/// The MySQL data source to be used.
39+
/// </summary>
40+
/// <remarks>
41+
/// Depending on how the <see cref="MySqlDataSource" /> was configured, the connections it hands out may be pooled.
42+
/// That is why it should be the exact same <see cref="MySqlDataSource" /> that is used by other parts of your app.
43+
/// </remarks>
44+
public MySqlDataSource? DataSource { get; }
45+
1246
/// <summary>
13-
/// The MySQL connection string to be used.
47+
/// The MySQL connection string to be used, if <see cref="DataSource"/> isn't set.
1448
/// </summary>
15-
public string ConnectionString { get; set; } = null!;
49+
/// <remarks>
50+
/// <see cref="MySqlDataSource"/> supports additional configuration beyond the connection string, such as logging and naming pools for diagnostics.
51+
/// To specify a data source, use <see cref=" MySqlDataSourceBuilder"/> and the <see cref="MySqlHealthCheckOptions(MySqlDataSource)"/> constructor.
52+
/// </remarks>
53+
public string? ConnectionString { get; }
1654

1755
/// <summary>
1856
/// The query to be executed.
1957
/// </summary>
20-
public string CommandText { get; set; } = MySqlHealthCheckBuilderExtensions.HEALTH_QUERY;
58+
public string? CommandText { get; set; }
2159

2260
/// <summary>
2361
/// An optional action executed before the connection is opened in the health check.

src/HealthChecks.MySql/README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
## MySQL Health Check
2+
3+
This health check verifies the ability to communicate with a MySQL Server.
4+
It uses the provided [MySqlDataSource](https://mysqlconnector.net/api/mysqlconnector/mysqldatasourcetype/) or a connection string to connect to the server.
5+
6+
### Defaults
7+
8+
By default, the `MySqlDataSource` instance is resolved from service provider.
9+
(This should be the same as the instance being used by the application; do not create a new `MySqlDataSource` just for the health check.)
10+
The health check will send a MySQL "ping" packet to the server to verify connectivity.
11+
12+
```csharp
13+
builder.Services
14+
.AddMySqlDataSource(builder.Configuration.GetConnectionString("mysql")) // using the MySqlConnector.DependencyInjection package
15+
.AddHealthChecks().AddMySql();
16+
```
17+
18+
### Connection String
19+
20+
You can also specify a connection string directly:
21+
22+
```csharp
23+
builder.Services.AddHealthChecks().AddMySql(connectionString: "Server=...;User Id=...;Password=...");
24+
```
25+
26+
This can be useful if you're not using `MySqlDataSource` in your application.
27+
28+
### Customization
29+
30+
You can additionally add the following parameters:
31+
32+
- `healthQuery`: A query to run against the server. If `null` (the default), the health check will send a MySQL "ping" packet to the server.
33+
- `configure`: An action to configure the `MySqlConnection` object. This is called after the `MySqlConnection` is created but before the connection is opened.
34+
- `name`: The health check name. The default is `mysql`.
35+
- `failureStatus`: The `HealthStatus` that should be reported when the health check fails. Default is `HealthStatus.Unhealthy`.
36+
- `tags`: A list of tags that can be used to filter sets of health checks.
37+
- `timeout`: A `System.TimeSpan` representing the timeout of the check.
38+
39+
```csharp
40+
builder.Services
41+
.AddMySqlDataSource(builder.Configuration.GetConnectionString("mysql"))
42+
.AddHealthChecks().AddMySql(
43+
healthQuery: "SELECT 1;",
44+
configure: conn => conn.ConnectTimeout = 3,
45+
name: "MySQL"
46+
);
47+
```
48+
49+
### Breaking changes
50+
51+
In previous versions, `MySqlHealthCheck` defaulted to testing connectivity by sending a `SELECT 1;` query to the server.
52+
It has been changed to send a more efficient "ping" packet instead.
53+
To restore the previous behavior, specify `healthQuery: "SELECT 1;"` when registering the health check.
54+
55+
While not a breaking change, it's now preferred to use `MySqlDataSource` instead of a connection string.
56+
This allows the health check to use the same connection pool as the rest of the application.
57+
This can be achieved by calling the `.AddMySql()` overload that has no required parameters.
58+
The health check assumes that a `MySqlDataSource` instance has been registered with the service provider and will retrieve it automatically.
59+

test/HealthChecks.MySql.Tests/DependencyInjection/RegistrationTests.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using MySqlConnector;
2+
13
namespace HealthChecks.MySql.Tests.DependencyInjection;
24

35
public class mysql_registration_should
@@ -18,6 +20,7 @@ public void add_health_check_when_properly_configured()
1820
registration.Name.ShouldBe("mysql");
1921
check.ShouldBeOfType<MySqlHealthCheck>();
2022
}
23+
2124
[Fact]
2225
public void add_named_health_check_when_properly_configured()
2326
{
@@ -34,4 +37,22 @@ public void add_named_health_check_when_properly_configured()
3437
registration.Name.ShouldBe("my-mysql-group");
3538
check.ShouldBeOfType<MySqlHealthCheck>();
3639
}
40+
41+
[Fact]
42+
public void add_health_check_for_data_source()
43+
{
44+
var services = new ServiceCollection();
45+
services
46+
.AddMySqlDataSource("Server=example")
47+
.AddHealthChecks().AddMySql();
48+
49+
using var serviceProvider = services.BuildServiceProvider();
50+
var options = serviceProvider.GetRequiredService<IOptions<HealthCheckServiceOptions>>();
51+
52+
var registration = options.Value.Registrations.First();
53+
var check = registration.Factory(serviceProvider);
54+
55+
registration.Name.ShouldBe("mysql");
56+
check.ShouldBeOfType<MySqlHealthCheck>();
57+
}
3758
}

test/HealthChecks.MySql.Tests/Functional/MySqlHealthCheckTests.cs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,39 @@
11
using System.Net;
2+
using MySqlConnector;
23

34
namespace HealthChecks.MySql.Tests.Functional;
45

56
public class mysql_healthcheck_should
67
{
78
[Fact]
8-
public async Task be_healthy_when_mysql_server_is_available()
9+
public async Task be_healthy_when_mysql_server_is_available_using_data_source()
10+
{
11+
var connectionString = "server=localhost;port=3306;database=information_schema;uid=root;password=Password12!";
12+
13+
var webHostBuilder = new WebHostBuilder()
14+
.ConfigureServices(services =>
15+
{
16+
services
17+
.AddMySqlDataSource(connectionString)
18+
.AddHealthChecks().AddMySql(tags: new string[] { "mysql" });
19+
})
20+
.Configure(app =>
21+
{
22+
app.UseHealthChecks("/health", new HealthCheckOptions
23+
{
24+
Predicate = r => r.Tags.Contains("mysql")
25+
});
26+
});
27+
28+
using var server = new TestServer(webHostBuilder);
29+
30+
using var response = await server.CreateRequest("/health").GetAsync();
31+
32+
response.StatusCode.ShouldBe(HttpStatusCode.OK);
33+
}
34+
35+
[Fact]
36+
public async Task be_healthy_when_mysql_server_is_available_using_connection_string()
937
{
1038
var connectionString = "server=localhost;port=3306;database=information_schema;uid=root;password=Password12!";
1139

@@ -64,10 +92,7 @@ public async Task be_unhealthy_when_mysql_server_is_unavailable_using_options()
6492
var webHostBuilder = new WebHostBuilder()
6593
.ConfigureServices(services =>
6694
{
67-
var mysqlOptions = new MySqlHealthCheckOptions
68-
{
69-
ConnectionString = connectionString
70-
};
95+
var mysqlOptions = new MySqlHealthCheckOptions(connectionString);
7196
services.AddHealthChecks()
7297
.AddMySql(mysqlOptions, tags: new string[] { "mysql" });
7398
})

test/HealthChecks.MySql.Tests/HealthChecks.MySql.Tests.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

3+
<ItemGroup>
4+
<PackageReference Include="MySqlConnector.DependencyInjection" Version="2.3.1" />
5+
</ItemGroup>
6+
37
<ItemGroup>
48
<ProjectReference Include="..\..\src\HealthChecks.MySql\HealthChecks.MySql.csproj" />
59
</ItemGroup>

0 commit comments

Comments
 (0)