Skip to content

Commit 0125252

Browse files
committed
Implement MySqlDataSource. Fixes #1208
1 parent d0f9f35 commit 0125252

File tree

5 files changed

+255
-2
lines changed

5 files changed

+255
-2
lines changed

src/MySqlConnector/Core/ConnectionPool.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,22 @@ private async ValueTask<ServerSession> ConnectSessionAsync(MySqlConnection conne
425425
return session;
426426
}
427427

428+
public static ConnectionPool? CreatePool(string connectionString)
429+
{
430+
// parse connection string and check for 'Pooling' setting; return 'null' if pooling is disabled
431+
var connectionStringBuilder = new MySqlConnectionStringBuilder(connectionString);
432+
if (!connectionStringBuilder.Pooling)
433+
{
434+
return null;
435+
}
436+
437+
// force a new pool to be created, ignoring the cache
438+
var connectionSettings = new ConnectionSettings(connectionStringBuilder);
439+
var pool = new ConnectionPool(connectionSettings);
440+
pool.StartReaperTask();
441+
return pool;
442+
}
443+
428444
public static ConnectionPool? GetPool(string connectionString)
429445
{
430446
// check single-entry MRU cache for this exact connection string; most applications have just one

src/MySqlConnector/MySqlConnection.cs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ namespace MySqlConnector;
2323
public sealed class MySqlConnection : DbConnection, ICloneable
2424
{
2525
public MySqlConnection()
26-
: this(default)
26+
: this("")
2727
{
2828
}
2929

@@ -33,6 +33,14 @@ public MySqlConnection(string? connectionString)
3333
m_connectionString = connectionString ?? "";
3434
}
3535

36+
#if NET7_0_OR_GREATER
37+
internal MySqlConnection(MySqlDataSource dataSource)
38+
: this(dataSource.ConnectionString)
39+
{
40+
m_dataSource = dataSource;
41+
}
42+
#endif
43+
3644
#pragma warning disable CA2012 // Safe because method completes synchronously
3745
/// <summary>
3846
/// Begins a database transaction.
@@ -388,7 +396,11 @@ internal async Task OpenAsync(IOBehavior? ioBehavior, CancellationToken cancella
388396

389397
SetState(ConnectionState.Connecting);
390398

391-
var pool = ConnectionPool.GetPool(m_connectionString);
399+
var pool =
400+
#if NET7_0_OR_GREATER
401+
m_dataSource?.Pool ??
402+
#endif
403+
ConnectionPool.GetPool(m_connectionString);
392404
m_connectionSettings ??= pool?.ConnectionSettings ?? new ConnectionSettings(new MySqlConnectionStringBuilder(m_connectionString));
393405

394406
// check if there is an open session (in the current transaction) that can be adopted
@@ -1105,6 +1117,9 @@ private ConnectionSettings GetConnectionSettings() =>
11051117
private static readonly object s_lock = new();
11061118
private static readonly Dictionary<System.Transactions.Transaction, List<EnlistedTransactionBase>> s_transactionConnections = new();
11071119

1120+
#if NET7_0_OR_GREATER
1121+
private readonly MySqlDataSource? m_dataSource;
1122+
#endif
11081123
private string m_connectionString;
11091124
private ConnectionSettings? m_connectionSettings;
11101125
private ServerSession? m_session;

src/MySqlConnector/MySqlConnectorFactory.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,14 @@ public sealed class MySqlConnectorFactory : DbProviderFactory
8787
#endif
8888
#pragma warning restore CA1822 // Mark members as static
8989

90+
#if NET7_0_OR_GREATER
91+
/// <summary>
92+
/// Creates a new <see cref="MySqlDataSource"/> object.
93+
/// </summary>
94+
/// <param name="connectionString">The connection string.</param>
95+
public override DbDataSource CreateDataSource(string connectionString) => new MySqlDataSource(connectionString);
96+
#endif
97+
9098
private MySqlConnectorFactory()
9199
{
92100
}

src/MySqlConnector/MySqlDataSource.cs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#if NET7_0_OR_GREATER
2+
using MySqlConnector.Core;
3+
using MySqlConnector.Logging;
4+
using MySqlConnector.Protocol.Serialization;
5+
6+
namespace MySqlConnector;
7+
8+
/// <summary>
9+
/// <see cref="MySqlDataSource"/> implements a MySQL data source which can be used to obtain open connections, and against which commands can be executed directly.
10+
/// </summary>
11+
public sealed class MySqlDataSource : DbDataSource
12+
{
13+
/// <summary>
14+
/// Initializes a new instance of the <see cref="MySqlDataSource"/> class.
15+
/// </summary>
16+
/// <param name="connectionString">The connection string for the MySQL Server. This parameter is required.</param>
17+
/// <exception cref="ArgumentNullException">Thrown if <paramref name="connectionString"/> is <c>null</c>.</exception>
18+
public MySqlDataSource(string connectionString)
19+
{
20+
m_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
21+
Pool = ConnectionPool.CreatePool(m_connectionString);
22+
m_id = Interlocked.Increment(ref s_lastId);
23+
m_logArguments = new object?[2] { m_id, null };
24+
if (Pool is not null)
25+
{
26+
m_logArguments[1] = Pool.Id;
27+
Log.Info("DataSource{0} created with Pool {1}", m_logArguments);
28+
}
29+
else
30+
{
31+
Log.Info("DataSource{0} created with no pool", m_logArguments);
32+
}
33+
}
34+
35+
/// <summary>
36+
/// Creates a new <see cref="MySqlConnection"/> that can connect to the database represented by this <see cref="MySqlDataSource"/>.
37+
/// </summary>
38+
/// <remarks>
39+
/// <para>The connection must be opened before it can be used.</para>
40+
/// <para>It is the responsibility of the caller to properly dispose the connection returned by this method. Failure to do so may result in a connection leak.</para>
41+
/// </remarks>
42+
public new MySqlConnection CreateConnection() => (MySqlConnection) base.CreateConnection();
43+
44+
/// <summary>
45+
/// Returns a new, open <see cref="MySqlConnection"/> to the database represented by this <see cref="MySqlDataSource"/>.
46+
/// </summary>
47+
/// <remarks>
48+
/// <para>The returned connection is already open, and is ready for immediate use.</para>
49+
/// <para>It is the responsibility of the caller to properly dispose the connection returned by this method. Failure to do so may result in a connection leak.</para>
50+
/// </remarks>
51+
public new MySqlConnection OpenConnection() => (MySqlConnection) base.OpenConnection();
52+
53+
/// <summary>
54+
/// Asynchronously returns a new, open <see cref="MySqlConnection"/> to the database represented by this <see cref="MySqlDataSource"/>.
55+
/// </summary>
56+
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
57+
/// <remarks>
58+
/// <para>The returned connection is already open, and is ready for immediate use.</para>
59+
/// <para>It is the responsibility of the caller to properly dispose the connection returned by this method. Failure to do so may result in a connection leak.</para>
60+
/// </remarks>
61+
public new async ValueTask<MySqlConnection> OpenConnectionAsync(CancellationToken cancellationToken = default) =>
62+
(MySqlConnection) await base.OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
63+
64+
/// <summary>
65+
/// Gets the connection string of the database represented by this <see cref="MySqlDataSource"/>.
66+
/// </summary>
67+
public override string ConnectionString => m_connectionString;
68+
69+
protected override DbConnection CreateDbConnection()
70+
{
71+
if (m_isDisposed)
72+
throw new ObjectDisposedException(nameof(MySqlDataSource));
73+
return new MySqlConnection(this);
74+
}
75+
76+
protected override void Dispose(bool disposing)
77+
{
78+
try
79+
{
80+
if (disposing)
81+
DisposeAsync(IOBehavior.Synchronous).GetAwaiter().GetResult();
82+
}
83+
finally
84+
{
85+
base.Dispose(disposing);
86+
}
87+
}
88+
89+
protected override ValueTask DisposeAsyncCore() => DisposeAsync(IOBehavior.Asynchronous);
90+
91+
private async ValueTask DisposeAsync(IOBehavior ioBehavior)
92+
{
93+
if (Pool is not null)
94+
await Pool.ClearAsync(ioBehavior, default).ConfigureAwait(false);
95+
m_isDisposed = true;
96+
}
97+
98+
internal ConnectionPool? Pool { get; }
99+
100+
private static readonly IMySqlConnectorLogger Log = MySqlConnectorLogManager.CreateLogger(nameof(MySqlDataSource));
101+
private static int s_lastId;
102+
103+
private readonly int m_id;
104+
private readonly object?[] m_logArguments;
105+
private readonly string m_connectionString;
106+
private bool m_isDisposed;
107+
}
108+
#endif
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#if !BASELINE
2+
#if NET7_0_OR_GREATER
3+
using System;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using System.Text;
7+
using System.Threading.Tasks;
8+
9+
namespace SideBySide;
10+
11+
public class MySqlDataSourceTests : IClassFixture<DatabaseFixture>
12+
{
13+
public MySqlDataSourceTests(DatabaseFixture _)
14+
{
15+
}
16+
17+
[Fact]
18+
public void CreateConnectionConnectionString()
19+
{
20+
var connectionString = AppConfig.ConnectionString;
21+
using var dbSource = new MySqlDataSource(connectionString);
22+
using var connection = dbSource.CreateConnection();
23+
Assert.Equal(ConnectionState.Closed, connection.State);
24+
Assert.Equal(connectionString, connection.ConnectionString);
25+
}
26+
27+
[Fact]
28+
public void OpenConnection()
29+
{
30+
using var dbSource = new MySqlDataSource(AppConfig.ConnectionString);
31+
using var connection = dbSource.OpenConnection();
32+
Assert.Equal(ConnectionState.Open, connection.State);
33+
}
34+
35+
[Fact]
36+
public async Task OpenConnectionAsync()
37+
{
38+
using var dbSource = new MySqlDataSource(AppConfig.ConnectionString);
39+
using var connection = await dbSource.OpenConnectionAsync();
40+
Assert.Equal(ConnectionState.Open, connection.State);
41+
}
42+
43+
[Fact]
44+
public void OpenConnectionReusesConnection()
45+
{
46+
using var dbSource = new MySqlDataSource(AppConfig.ConnectionString);
47+
48+
int serverThread;
49+
using (var connection = dbSource.OpenConnection())
50+
{
51+
serverThread = connection.ServerThread;
52+
}
53+
54+
using (var connection = dbSource.OpenConnection())
55+
{
56+
Assert.Equal(serverThread, connection.ServerThread);
57+
}
58+
}
59+
60+
[Fact]
61+
public void MultipleDataSourcesHaveDifferentPools()
62+
{
63+
using var dbSource1 = new MySqlDataSource(AppConfig.ConnectionString);
64+
using var dbSource2 = new MySqlDataSource(AppConfig.ConnectionString);
65+
66+
int serverThread;
67+
using (var connection = dbSource1.OpenConnection())
68+
{
69+
serverThread = connection.ServerThread;
70+
}
71+
72+
using (var connection = dbSource2.OpenConnection())
73+
{
74+
Assert.NotEqual(serverThread, connection.ServerThread);
75+
}
76+
}
77+
78+
[Fact]
79+
public void NonPoolingDataSourceDoesNotReuseConnections()
80+
{
81+
var csb = AppConfig.CreateConnectionStringBuilder();
82+
csb.Pooling = false;
83+
using var dbSource = new MySqlDataSource(csb.ConnectionString);
84+
85+
int serverThread;
86+
using (var connection = dbSource.OpenConnection())
87+
{
88+
serverThread = connection.ServerThread;
89+
}
90+
91+
using (var connection = dbSource.OpenConnection())
92+
{
93+
Assert.NotEqual(serverThread, connection.ServerThread);
94+
}
95+
}
96+
97+
[Fact]
98+
public void CreateFromDbFactory()
99+
{
100+
using var dbSource = MySqlConnectorFactory.Instance.CreateDataSource(AppConfig.ConnectionString);
101+
Assert.IsType<MySqlDataSource>(dbSource);
102+
Assert.Equal(AppConfig.ConnectionString, dbSource.ConnectionString);
103+
}
104+
}
105+
#endif
106+
#endif

0 commit comments

Comments
 (0)