Skip to content

Commit d8cdef5

Browse files
committed
Implement CloneWith. Fixes #736
1 parent deb2bde commit d8cdef5

File tree

2 files changed

+94
-5
lines changed

2 files changed

+94
-5
lines changed

src/MySqlConnector/MySql.Data.MySqlClient/MySqlConnection.cs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public sealed class MySqlConnection : DbConnection
2121
#endif
2222
{
2323
public MySqlConnection()
24-
: this("")
24+
: this(default)
2525
{
2626
}
2727

@@ -447,12 +447,31 @@ public override async ValueTask DisposeAsync()
447447
}
448448
}
449449

450-
public MySqlConnection Clone() => new MySqlConnection(this);
450+
public MySqlConnection Clone() => new MySqlConnection(m_connectionString, m_hasBeenOpened);
451451

452452
#if !NETSTANDARD1_3
453453
object ICloneable.Clone() => Clone();
454454
#endif
455455

456+
/// <summary>
457+
/// Returns an unopened copy of this connection with a new connection string. If the <c>Password</c>
458+
/// in <paramref name="connectionString"/> is not set, the password from this connection will be used.
459+
/// This allows creating a new connection with the same security information while changing other options,
460+
/// such as database or pooling.
461+
/// </summary>
462+
/// <param name="connectionString">The new connection string to be used.</param>
463+
/// <returns>A new <see cref="MySqlConnection"/> with different connection string options but
464+
/// the same password as this connection (unless overridden by <paramref name="connectionString"/>).</returns>
465+
public MySqlConnection CloneWith(string connectionString)
466+
{
467+
var newBuilder = new MySqlConnectionStringBuilder(connectionString ?? throw new ArgumentNullException(nameof(connectionString)));
468+
var currentBuilder = new MySqlConnectionStringBuilder(m_connectionString);
469+
var shouldCopyPassword = newBuilder.Password.Length == 0 && (!newBuilder.PersistSecurityInfo || currentBuilder.PersistSecurityInfo);
470+
if (shouldCopyPassword)
471+
newBuilder.Password = currentBuilder.Password;
472+
return new MySqlConnection(newBuilder.ConnectionString, m_hasBeenOpened && shouldCopyPassword && !currentBuilder.PersistSecurityInfo);
473+
}
474+
456475
internal ServerSession Session
457476
{
458477
get
@@ -675,10 +694,10 @@ internal void SetState(ConnectionState newState)
675694
}
676695
}
677696

678-
private MySqlConnection(MySqlConnection other)
679-
: this(other.m_connectionString)
697+
private MySqlConnection(string connectionString, bool hasBeenOpened)
698+
: this(connectionString)
680699
{
681-
m_hasBeenOpened = other.m_hasBeenOpened;
700+
m_hasBeenOpened = hasBeenOpened;
682701
}
683702

684703
private void VerifyNotDisposed()

tests/SideBySide/ConnectionTests.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,5 +172,75 @@ public void CloneDoesNotDisclosePassword()
172172
Assert.Equal(connection.ConnectionString, connection2.ConnectionString);
173173
Assert.DoesNotContain("password", connection2.ConnectionString, StringComparison.OrdinalIgnoreCase);
174174
}
175+
176+
#if !BASELINE
177+
[Theory]
178+
[InlineData(false)]
179+
[InlineData(true)]
180+
public void CloneWithUsesNewConnectionString(bool openConnection)
181+
{
182+
using var connection = new MySqlConnection(AppConfig.ConnectionString);
183+
if (openConnection)
184+
connection.Open();
185+
using var connection2 = connection.CloneWith("user=root;password=pass;server=example.com;database=test");
186+
Assert.Equal("User Id=root;Password=pass;Server=example.com;Database=test", connection2.ConnectionString);
187+
}
188+
189+
[Fact]
190+
public void CloneWithUsesExistingPassword()
191+
{
192+
using var connection = new MySqlConnection(AppConfig.ConnectionString);
193+
var newConnectionString = "user=root;server=example.com;database=test";
194+
using var connection2 = connection.CloneWith(newConnectionString);
195+
196+
var builder = new MySqlConnectionStringBuilder(newConnectionString);
197+
builder.Password = AppConfig.CreateConnectionStringBuilder().Password;
198+
Assert.Equal(builder.ConnectionString, connection2.ConnectionString);
199+
}
200+
201+
[Theory]
202+
[InlineData(false)]
203+
[InlineData(true)]
204+
public void CloneWithDoesNotDiscloseExistingPassword(bool persistSecurityInfo)
205+
{
206+
using var connection = new MySqlConnection(AppConfig.ConnectionString);
207+
connection.Open();
208+
209+
var newConnectionString = "user=root;server=example.com;database=test;Persist Security Info=" + persistSecurityInfo;
210+
using var connection2 = connection.CloneWith(newConnectionString);
211+
212+
var builder = new MySqlConnectionStringBuilder(newConnectionString);
213+
Assert.Equal(builder.ConnectionString, connection2.ConnectionString);
214+
}
215+
216+
[Theory]
217+
[InlineData(false)]
218+
[InlineData(true)]
219+
public void CloneWithDoesDiscloseExistingPasswordIfPersistSecurityInfo(bool persistSecurityInfo)
220+
{
221+
using var connection = new MySqlConnection(AppConfig.ConnectionString + ";Persist Security Info=true");
222+
connection.Open();
223+
224+
var newConnectionString = "user=root;server=example.com;database=test;Persist Security Info=" + persistSecurityInfo;
225+
using var connection2 = connection.CloneWith(newConnectionString);
226+
227+
var builder = new MySqlConnectionStringBuilder(newConnectionString);
228+
builder.Password = AppConfig.CreateConnectionStringBuilder().Password;
229+
Assert.Equal(builder.ConnectionString, connection2.ConnectionString);
230+
}
231+
232+
[Fact]
233+
public void CloneWithCopiesExistingPassword()
234+
{
235+
using var connection = new MySqlConnection(AppConfig.ConnectionString);
236+
connection.Open();
237+
238+
var builder = AppConfig.CreateConnectionStringBuilder();
239+
builder.Password = "";
240+
using var connection2 = connection.CloneWith(builder.ConnectionString);
241+
connection2.Open();
242+
Assert.Equal(ConnectionState.Open, connection2.State);
243+
}
244+
#endif
175245
}
176246
}

0 commit comments

Comments
 (0)