-
Notifications
You must be signed in to change notification settings - Fork 328
/
MySqlConnection.cs
1120 lines (985 loc) · 49.9 KB
/
MySqlConnection.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net.Security;
using System.Net.Sockets;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using MySqlConnector.Core;
using MySqlConnector.Logging;
using MySqlConnector.Protocol.Payloads;
using MySqlConnector.Protocol.Serialization;
using MySqlConnector.Utilities;
namespace MySqlConnector;
#if !NET6_0_OR_GREATER
#pragma warning disable CA1822 // Mark members as static
#endif
/// <summary>
/// <see cref="MySqlConnection"/> represents a connection to a MySQL database.
/// </summary>
public sealed class MySqlConnection : DbConnection, ICloneable
{
public MySqlConnection()
: this(default)
{
}
public MySqlConnection(string? connectionString)
{
GC.SuppressFinalize(this);
m_connectionString = connectionString ?? "";
}
#pragma warning disable CA2012 // Safe because method completes synchronously
/// <summary>
/// Begins a database transaction.
/// </summary>
/// <returns>A <see cref="MySqlTransaction"/> representing the new database transaction.</returns>
/// <remarks>Transactions may not be nested.</remarks>
public new MySqlTransaction BeginTransaction() => BeginTransactionAsync(IsolationLevel.Unspecified, default, IOBehavior.Synchronous, default).GetAwaiter().GetResult();
/// <summary>
/// Begins a database transaction.
/// </summary>
/// <param name="isolationLevel">The <see cref="IsolationLevel"/> for the transaction.</param>
/// <returns>A <see cref="MySqlTransaction"/> representing the new database transaction.</returns>
/// <remarks>Transactions may not be nested.</remarks>
public new MySqlTransaction BeginTransaction(IsolationLevel isolationLevel) => BeginTransactionAsync(isolationLevel, default, IOBehavior.Synchronous, default).GetAwaiter().GetResult();
/// <summary>
/// Begins a database transaction.
/// </summary>
/// <param name="isolationLevel">The <see cref="IsolationLevel"/> for the transaction.</param>
/// <param name="isReadOnly">If <c>true</c>, changes to tables used in the transaction are prohibited; otherwise, they are permitted.</param>
/// <returns>A <see cref="MySqlTransaction"/> representing the new database transaction.</returns>
/// <remarks>Transactions may not be nested.</remarks>
public MySqlTransaction BeginTransaction(IsolationLevel isolationLevel, bool isReadOnly) => BeginTransactionAsync(isolationLevel, isReadOnly, IOBehavior.Synchronous, default).GetAwaiter().GetResult();
/// <summary>
/// Begins a database transaction.
/// </summary>
/// <param name="isolationLevel">The <see cref="IsolationLevel"/> for the transaction.</param>
/// <returns>A <see cref="MySqlTransaction"/> representing the new database transaction.</returns>
protected override DbTransaction BeginDbTransaction(IsolationLevel isolationLevel) => BeginTransactionAsync(isolationLevel, default, IOBehavior.Synchronous, default).GetAwaiter().GetResult();
#pragma warning restore CA2012
#if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
/// <summary>
/// Begins a database transaction asynchronously.
/// </summary>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A <see cref="Task{MySqlTransaction}"/> representing the new database transaction.</returns>
/// <remarks>Transactions may not be nested.</remarks>
public new ValueTask<MySqlTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default) => BeginTransactionAsync(IsolationLevel.Unspecified, default, AsyncIOBehavior, cancellationToken);
/// <summary>
/// Begins a database transaction asynchronously.
/// </summary>
/// <param name="isolationLevel">The <see cref="IsolationLevel"/> for the transaction.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A <see cref="Task{MySqlTransaction}"/> representing the new database transaction.</returns>
/// <remarks>Transactions may not be nested.</remarks>
public new ValueTask<MySqlTransaction> BeginTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken = default) => BeginTransactionAsync(isolationLevel, default, AsyncIOBehavior, cancellationToken);
/// <summary>
/// Begins a database transaction asynchronously.
/// </summary>
/// <param name="isolationLevel">The <see cref="IsolationLevel"/> for the transaction.</param>
/// <param name="isReadOnly">If <c>true</c>, changes to tables used in the transaction are prohibited; otherwise, they are permitted.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A <see cref="Task{MySqlTransaction}"/> representing the new database transaction.</returns>
/// <remarks>Transactions may not be nested.</remarks>
public ValueTask<MySqlTransaction> BeginTransactionAsync(IsolationLevel isolationLevel, bool isReadOnly, CancellationToken cancellationToken = default) => BeginTransactionAsync(isolationLevel, isReadOnly, AsyncIOBehavior, cancellationToken);
/// <summary>
/// Begins a database transaction asynchronously.
/// </summary>
/// <param name="isolationLevel">The <see cref="IsolationLevel"/> for the transaction.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A <see cref="ValueTask{DbTransaction}"/> representing the new database transaction.</returns>
protected override async ValueTask<DbTransaction> BeginDbTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken) =>
await BeginTransactionAsync(isolationLevel, default, AsyncIOBehavior, cancellationToken).ConfigureAwait(false);
#else
/// <summary>
/// Begins a database transaction asynchronously.
/// </summary>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A <see cref="Task{MySqlTransaction}"/> representing the new database transaction.</returns>
/// <remarks>Transactions may not be nested.</remarks>
public ValueTask<MySqlTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default) => BeginTransactionAsync(IsolationLevel.Unspecified, default, AsyncIOBehavior, cancellationToken);
/// <summary>
/// Begins a database transaction asynchronously.
/// </summary>
/// <param name="isolationLevel">The <see cref="IsolationLevel"/> for the transaction.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A <see cref="Task{MySqlTransaction}"/> representing the new database transaction.</returns>
/// <remarks>Transactions may not be nested.</remarks>
public ValueTask<MySqlTransaction> BeginTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken = default) => BeginTransactionAsync(isolationLevel, default, AsyncIOBehavior, cancellationToken);
/// <summary>
/// Begins a database transaction asynchronously.
/// </summary>
/// <param name="isolationLevel">The <see cref="IsolationLevel"/> for the transaction.</param>
/// <param name="isReadOnly">If <c>true</c>, changes to tables used in the transaction are prohibited; otherwise, they are permitted.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A <see cref="Task{MySqlTransaction}"/> representing the new database transaction.</returns>
/// <remarks>Transactions may not be nested.</remarks>
public ValueTask<MySqlTransaction> BeginTransactionAsync(IsolationLevel isolationLevel, bool isReadOnly, CancellationToken cancellationToken = default) => BeginTransactionAsync(isolationLevel, isReadOnly, AsyncIOBehavior, cancellationToken);
#endif
private async ValueTask<MySqlTransaction> BeginTransactionAsync(IsolationLevel isolationLevel, bool? isReadOnly, IOBehavior ioBehavior, CancellationToken cancellationToken)
{
if (State != ConnectionState.Open)
throw new InvalidOperationException("Connection is not open.");
if (CurrentTransaction is not null)
throw new InvalidOperationException("Transactions may not be nested.");
if (m_enlistedTransaction is not null)
throw new InvalidOperationException("Cannot begin a transaction when already enlisted in a transaction.");
var isolationLevelValue = isolationLevel switch
{
IsolationLevel.ReadUncommitted => "read uncommitted",
IsolationLevel.ReadCommitted => "read committed",
IsolationLevel.RepeatableRead => "repeatable read",
IsolationLevel.Serializable => "serializable",
IsolationLevel.Snapshot => "repeatable read",
// "In terms of the SQL:1992 transaction isolation levels, the default InnoDB level is REPEATABLE READ." - http://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-model.html
IsolationLevel.Unspecified => "repeatable read",
_ => throw new NotSupportedException("IsolationLevel.{0} is not supported.".FormatInvariant(isolationLevel)),
};
using (var cmd = new MySqlCommand($"set session transaction isolation level {isolationLevelValue};", this) { NoActivity = true })
{
await cmd.ExecuteNonQueryAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
var consistentSnapshotText = isolationLevel == IsolationLevel.Snapshot ? " with consistent snapshot" : "";
var readOnlyText = isReadOnly switch
{
true => " read only",
false => " read write",
null => "",
};
var separatorText = (consistentSnapshotText.Length == 0 || readOnlyText.Length == 0) ? "" : ",";
cmd.CommandText = $"start transaction{consistentSnapshotText}{separatorText}{readOnlyText};";
await cmd.ExecuteNonQueryAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
}
var transaction = new MySqlTransaction(this, isolationLevel);
CurrentTransaction = transaction;
return transaction;
}
public override void EnlistTransaction(System.Transactions.Transaction? transaction)
{
if (State != ConnectionState.Open)
throw new InvalidOperationException("Connection is not open.");
// ignore reenlistment of same connection in same transaction
if (m_enlistedTransaction?.Transaction.Equals(transaction) ?? false)
return;
if (m_enlistedTransaction is not null)
throw new MySqlException("Already enlisted in a Transaction.");
if (CurrentTransaction is not null)
throw new InvalidOperationException("Can't enlist in a Transaction when there is an active MySqlTransaction.");
if (transaction is not null)
{
var existingConnection = FindExistingEnlistedSession(transaction);
if (existingConnection is not null)
{
// can reuse the existing connection
CloseAsync(changeState: false, IOBehavior.Synchronous).GetAwaiter().GetResult();
TakeSessionFrom(existingConnection);
return;
}
else
{
m_enlistedTransaction = GetInitializedConnectionSettings().UseXaTransactions ?
(EnlistedTransactionBase)new XaEnlistedTransaction(transaction, this) :
new StandardEnlistedTransaction(transaction, this);
m_enlistedTransaction.Start();
lock (s_lock)
{
if (!s_transactionConnections.TryGetValue(transaction, out var enlistedTransactions))
s_transactionConnections[transaction] = enlistedTransactions = new();
enlistedTransactions.Add(m_enlistedTransaction);
}
}
}
}
internal void UnenlistTransaction()
{
var transaction = m_enlistedTransaction!.Transaction;
m_enlistedTransaction = null;
// find this connection in the list of connections associated with the transaction
bool? wasIdle = null;
lock (s_lock)
{
var enlistedTransactions = s_transactionConnections[transaction];
for (int i = 0; i < enlistedTransactions.Count; i++)
{
if (enlistedTransactions[i].Connection == this)
{
wasIdle = enlistedTransactions[i].IsIdle;
enlistedTransactions.RemoveAt(i);
break;
}
}
if (enlistedTransactions.Count == 0)
s_transactionConnections.Remove(transaction);
}
// if the connection was idle (i.e., the client already closed it), really close it now
if (wasIdle is null)
throw new InvalidOperationException("Didn't find transaction");
if (wasIdle.Value)
Close();
}
// If there is an idle (i.e., no client has it open) MySqlConnection thats part of 'transaction',
// returns it; otherwise, returns null. If a valid MySqlConnection is returned, the current connection
// has been stored in 's_transactionConnections' and the caller must call TakeSessionFrom to
// transfer its session to this MySqlConnection.
// Also performs validation checks to ensure that XA and non-XA transactions aren't being mixed.
private MySqlConnection? FindExistingEnlistedSession(System.Transactions.Transaction transaction)
{
var hasEnlistedTransactions = false;
var hasXaTransaction = false;
lock (s_lock)
{
if (s_transactionConnections.TryGetValue(transaction, out var enlistedTransactions))
{
hasEnlistedTransactions = true;
foreach (var enlistedTransaction in enlistedTransactions)
{
hasXaTransaction = enlistedTransaction.Connection.GetInitializedConnectionSettings().UseXaTransactions;
if (enlistedTransaction.IsIdle && enlistedTransaction.Connection.m_connectionString == m_connectionString)
{
var existingConnection = enlistedTransaction.Connection;
enlistedTransaction.Connection = this;
enlistedTransaction.IsIdle = false;
return existingConnection;
}
}
}
}
// no valid existing connection was found; verify that constraints aren't violated
if (GetInitializedConnectionSettings().UseXaTransactions)
{
if (hasEnlistedTransactions && !hasXaTransaction)
throw new NotSupportedException("Cannot start an XA transaction when there is an existing non-XA transaction.");
}
else if (hasEnlistedTransactions)
{
throw new NotSupportedException("Multiple simultaneous connections or connections with different connection strings inside the same transaction are not supported when UseXaTransactions=False.");
}
return null;
}
private void TakeSessionFrom(MySqlConnection other)
{
#if DEBUG
if (other is null)
throw new ArgumentNullException(nameof(other));
if (m_session is not null)
throw new InvalidOperationException("This connection must not have a session");
if (other.m_session is null)
throw new InvalidOperationException("Other connection must have a session");
if (m_enlistedTransaction is not null)
throw new InvalidOperationException("This connection must not have an enlisted transaction");
if (other.m_enlistedTransaction is null)
throw new InvalidOperationException("Other connection must have an enlisted transaction");
if (m_activeReader is not null)
throw new InvalidOperationException("This connection must not have an active reader");
if (other.m_activeReader is not null)
throw new InvalidOperationException("Other connection must not have an active reader");
#endif
m_session = other.m_session;
m_session!.OwningConnection = new(this);
other.m_session = null;
m_cachedProcedures = other.m_cachedProcedures;
other.m_cachedProcedures = null;
m_enlistedTransaction = other.m_enlistedTransaction;
other.m_enlistedTransaction = null;
}
public override void Close() => CloseAsync(changeState: true, IOBehavior.Synchronous).GetAwaiter().GetResult();
#if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
public override Task CloseAsync() => CloseAsync(changeState: true, SimpleAsyncIOBehavior);
#else
public Task CloseAsync() => CloseAsync(changeState: true, SimpleAsyncIOBehavior);
#endif
internal Task CloseAsync(IOBehavior ioBehavior) => CloseAsync(changeState: true, ioBehavior);
public override void ChangeDatabase(string databaseName) => ChangeDatabaseAsync(IOBehavior.Synchronous, databaseName, CancellationToken.None).GetAwaiter().GetResult();
#if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
public override Task ChangeDatabaseAsync(string databaseName, CancellationToken cancellationToken = default) => ChangeDatabaseAsync(AsyncIOBehavior, databaseName, cancellationToken);
#else
public Task ChangeDatabaseAsync(string databaseName, CancellationToken cancellationToken = default) => ChangeDatabaseAsync(AsyncIOBehavior, databaseName, cancellationToken);
#endif
private async Task ChangeDatabaseAsync(IOBehavior ioBehavior, string databaseName, CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(databaseName))
throw new ArgumentException("Database name is not valid.", nameof(databaseName));
if (State != ConnectionState.Open)
throw new InvalidOperationException("Connection is not open.");
using (var initDatabasePayload = InitDatabasePayload.Create(databaseName))
await m_session!.SendAsync(initDatabasePayload, ioBehavior, cancellationToken).ConfigureAwait(false);
var payload = await m_session.ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
OkPayload.Create(payload.Span, m_session.SupportsDeprecateEof, m_session.SupportsSessionTrack);
m_session.DatabaseOverride = databaseName;
}
public new MySqlCommand CreateCommand() => (MySqlCommand)base.CreateCommand();
#pragma warning disable CA2012 // Safe because method completes synchronously
public bool Ping() => PingAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
#pragma warning restore CA2012
public Task<bool> PingAsync(CancellationToken cancellationToken = default) => PingAsync(SimpleAsyncIOBehavior, cancellationToken).AsTask();
private async ValueTask<bool> PingAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
{
if (m_session is null)
return false;
try
{
if (await m_session.TryPingAsync(logInfo: true, ioBehavior, cancellationToken).ConfigureAwait(false))
return true;
}
catch (InvalidOperationException)
{
}
SetState(ConnectionState.Closed);
return false;
}
public override void Open() => OpenAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
public override Task OpenAsync(CancellationToken cancellationToken) => OpenAsync(default, cancellationToken);
internal async Task OpenAsync(IOBehavior? ioBehavior, CancellationToken cancellationToken)
{
VerifyNotDisposed();
cancellationToken.ThrowIfCancellationRequested();
if (State != ConnectionState.Closed)
throw new InvalidOperationException("Cannot Open when State is {0}.".FormatInvariant(State));
using var activity = ActivitySourceHelper.StartActivity(ActivitySourceHelper.OpenActivityName);
try
{
var openStartTickCount = Environment.TickCount;
SetState(ConnectionState.Connecting);
var pool = ConnectionPool.GetPool(m_connectionString);
m_connectionSettings ??= pool?.ConnectionSettings ?? new ConnectionSettings(new MySqlConnectionStringBuilder(m_connectionString));
// check if there is an open session (in the current transaction) that can be adopted
if (m_connectionSettings.AutoEnlist && System.Transactions.Transaction.Current is not null)
{
var existingConnection = FindExistingEnlistedSession(System.Transactions.Transaction.Current);
if (existingConnection is not null)
{
TakeSessionFrom(existingConnection);
CopyActivityTags(m_session!, activity);
m_hasBeenOpened = true;
SetState(ConnectionState.Open);
return;
}
}
try
{
m_session = await CreateSessionAsync(pool, openStartTickCount, ioBehavior, cancellationToken).ConfigureAwait(false);
CopyActivityTags(m_session, activity);
m_hasBeenOpened = true;
SetState(ConnectionState.Open);
}
catch (OperationCanceledException)
{
SetState(ConnectionState.Closed);
throw;
}
catch (MySqlException)
{
SetState(ConnectionState.Closed);
cancellationToken.ThrowIfCancellationRequested();
throw;
}
catch (SocketException)
{
SetState(ConnectionState.Closed);
throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Unable to connect to any of the specified MySQL hosts.");
}
if (m_connectionSettings.AutoEnlist && System.Transactions.Transaction.Current is not null)
EnlistTransaction(System.Transactions.Transaction.Current);
activity?.SetSuccess();
}
catch (Exception ex) when (activity is { IsAllDataRequested: true })
{
// none of the other activity tags may have been set (depending on when the exception was thrown), so make sure at least the connection string is added, for diagnostics
if (m_connectionSettings?.ConnectionStringBuilder is { } connectionStringBuilder)
activity.SetTag(ActivitySourceHelper.DatabaseConnectionStringTagName, connectionStringBuilder.GetConnectionString(connectionStringBuilder.PersistSecurityInfo));
activity.SetException(ex);
throw;
}
static void CopyActivityTags(ServerSession session, Activity? activity)
{
if (activity is { IsAllDataRequested: true })
{
foreach (var tag in session.ActivityTags)
activity.SetTag(tag.Key, tag.Value);
}
}
}
/// <summary>
/// Resets the session state of the current open connection; this clears temporary tables and user-defined variables.
/// </summary>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A <c>ValueTask</c> representing the asynchronous operation.</returns>
/// <remarks>This is an optional feature of the MySQL protocol and may not be supported by all servers.
/// It's known to be supported by MySQL Server 5.7.3 (and later) and MariaDB 10.2.4 (and later).
/// Other MySQL-compatible servers or proxies may not support this command.</remarks>
#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
public async ValueTask ResetConnectionAsync(CancellationToken cancellationToken = default)
#else
public async Task ResetConnectionAsync(CancellationToken cancellationToken = default)
#endif
{
var session = Session;
Log.Debug("Session{0} resetting connection", session.Id);
await session.SendAsync(ResetConnectionPayload.Instance, AsyncIOBehavior, cancellationToken).ConfigureAwait(false);
var payload = await session.ReceiveReplyAsync(AsyncIOBehavior, cancellationToken).ConfigureAwait(false);
OkPayload.Create(payload.Span, session.SupportsDeprecateEof, session.SupportsSessionTrack);
}
[AllowNull]
public override string ConnectionString
{
get
{
if (!m_hasBeenOpened)
return m_connectionString;
var connectionStringBuilder = GetConnectionSettings().ConnectionStringBuilder;
return connectionStringBuilder.GetConnectionString(connectionStringBuilder.PersistSecurityInfo);
}
set
{
if (m_connectionState == ConnectionState.Open)
throw new InvalidOperationException("Cannot change the connection string on an open connection.");
m_hasBeenOpened = false;
m_connectionString = value ?? "";
m_connectionSettings = null;
}
}
public override string Database => m_session?.DatabaseOverride ?? GetConnectionSettings().Database;
public override ConnectionState State => m_connectionState;
public override string DataSource => GetConnectionSettings().ConnectionStringBuilder.Server;
public override string ServerVersion => Session.ServerVersion.OriginalString;
/// <summary>
/// The connection ID from MySQL Server.
/// </summary>
public int ServerThread => Session.ConnectionId;
/// <summary>
/// Gets or sets the delegate used to provide client certificates for connecting to a server.
/// </summary>
/// <remarks>The provided <see cref="X509CertificateCollection"/> should be filled with the client certificate(s) needed to connect to the server.</remarks>
#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
public Func<X509CertificateCollection, ValueTask>? ProvideClientCertificatesCallback { get; set; }
#else
public Func<X509CertificateCollection, Task>? ProvideClientCertificatesCallback { get; set; }
#endif
/// <summary>
/// Gets or sets the delegate used to generate a password for new database connections.
/// </summary>
/// <remarks>
/// <para>This delegate is executed when a new database connection is opened that requires a password. Due to
/// connection pooling, this delegate is only executed when a new physical connection is established with a database
/// server, not when a connection is retrieved from the pool.</para>
/// <para>The <see cref="MySqlConnectionStringBuilder.Password"/> option takes precedence over this
/// delegate if it is specified.</para>
/// <para>Using this delegate can make more efficient use of connection pooling for servers that require
/// frequently-changing passwords or authentication tokens. Changing the password in the connection string
/// will create unique connection pools; this delegate allows a single connection pool to use multiple passwords.</para>
/// </remarks>
public Func<MySqlProvidePasswordContext, string>? ProvidePasswordCallback { get; set; }
/// <summary>
/// Gets or sets the delegate used to verify that the server's certificate is valid.
/// </summary>
/// <remarks><see cref="MySqlConnectionStringBuilder.SslMode"/> must be set to <see cref="MySqlSslMode.Preferred"/>
/// or <see cref="MySqlSslMode.Required"/> in order for this delegate to be invoked. See the documentation for
/// <see cref="RemoteCertificateValidationCallback"/> for more information on the values passed to this delegate.</remarks>
public RemoteCertificateValidationCallback? RemoteCertificateValidationCallback { get; set; }
/// <summary>
/// Clears the connection pool that <paramref name="connection"/> belongs to.
/// </summary>
/// <param name="connection">The <see cref="MySqlConnection"/> whose connection pool will be cleared.</param>
public static void ClearPool(MySqlConnection connection) => ClearPoolAsync(connection, IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
/// <summary>
/// Asynchronously clears the connection pool that <paramref name="connection"/> belongs to.
/// </summary>
/// <param name="connection">The <see cref="MySqlConnection"/> whose connection pool will be cleared.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task ClearPoolAsync(MySqlConnection connection, CancellationToken cancellationToken = default) => ClearPoolAsync(connection, connection.AsyncIOBehavior, cancellationToken);
/// <summary>
/// Clears all connection pools.
/// </summary>
public static void ClearAllPools() => ConnectionPool.ClearPoolsAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
/// <summary>
/// Asynchronously clears all connection pools.
/// </summary>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static Task ClearAllPoolsAsync(CancellationToken cancellationToken = default) => ConnectionPool.ClearPoolsAsync(IOBehavior.Asynchronous, cancellationToken);
private static async Task ClearPoolAsync(MySqlConnection connection, IOBehavior ioBehavior, CancellationToken cancellationToken)
{
if (connection is null)
throw new ArgumentNullException(nameof(connection));
var pool = ConnectionPool.GetPool(connection.m_connectionString);
if (pool is not null)
await pool.ClearAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
}
protected override DbCommand CreateDbCommand() => new MySqlCommand(this, null);
protected override DbProviderFactory DbProviderFactory => MySqlConnectorFactory.Instance;
#pragma warning disable CA2012 // Safe because method completes synchronously
/// <summary>
/// Returns schema information for the data source of this <see cref="MySqlConnection"/>.
/// </summary>
/// <returns>A <see cref="DataTable"/> containing schema information.</returns>
public override DataTable GetSchema() => GetSchemaProvider().GetSchemaAsync(IOBehavior.Synchronous, "MetaDataCollections", default, default).GetAwaiter().GetResult();
/// <summary>
/// Returns schema information for the data source of this <see cref="MySqlConnection"/>.
/// </summary>
/// <param name="collectionName">The name of the schema to return.</param>
/// <returns>A <see cref="DataTable"/> containing schema information.</returns>
public override DataTable GetSchema(string collectionName) => GetSchemaProvider().GetSchemaAsync(IOBehavior.Synchronous, collectionName, default, default).GetAwaiter().GetResult();
/// <summary>
/// Returns schema information for the data source of this <see cref="MySqlConnection"/>.
/// </summary>
/// <param name="collectionName">The name of the schema to return.</param>
/// <param name="restrictionValues">The restrictions to apply to the schema.</param>
/// <returns>A <see cref="DataTable"/> containing schema information.</returns>
public override DataTable GetSchema(string collectionName, string?[] restrictionValues) => GetSchemaProvider().GetSchemaAsync(IOBehavior.Synchronous, collectionName, restrictionValues, default).GetAwaiter().GetResult();
#pragma warning restore CA2012
/// <summary>
/// Asynchronously returns schema information for the data source of this <see cref="MySqlConnection"/>.
/// </summary>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A <see cref="Task{DataTable}"/> containing schema information.</returns>
/// <remarks>The proposed ADO.NET API that this is based on is not finalized; this API may change in the future.</remarks>
#if NET5_0_OR_GREATER
public override Task<DataTable> GetSchemaAsync(CancellationToken cancellationToken = default)
#else
public Task<DataTable> GetSchemaAsync(CancellationToken cancellationToken = default)
#endif
=> GetSchemaProvider().GetSchemaAsync(AsyncIOBehavior, "MetaDataCollections", default, cancellationToken).AsTask();
/// <summary>
/// Asynchronously returns schema information for the data source of this <see cref="MySqlConnection"/>.
/// </summary>
/// <param name="collectionName">The name of the schema to return.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A <see cref="Task{DataTable}"/> containing schema information.</returns>
/// <remarks>The proposed ADO.NET API that this is based on is not finalized; this API may change in the future.</remarks>
#if NET5_0_OR_GREATER
public override Task<DataTable> GetSchemaAsync(string collectionName, CancellationToken cancellationToken = default)
#else
public Task<DataTable> GetSchemaAsync(string collectionName, CancellationToken cancellationToken = default)
#endif
=> GetSchemaProvider().GetSchemaAsync(AsyncIOBehavior, collectionName, default, cancellationToken).AsTask();
/// <summary>
/// Asynchronously returns schema information for the data source of this <see cref="MySqlConnection"/>.
/// </summary>
/// <param name="collectionName">The name of the schema to return.</param>
/// <param name="restrictionValues">The restrictions to apply to the schema.</param>
/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
/// <returns>A <see cref="Task{DataTable}"/> containing schema information.</returns>
/// <remarks>The proposed ADO.NET API that this is based on is not finalized; this API may change in the future.</remarks>
#if NET5_0_OR_GREATER
public override Task<DataTable> GetSchemaAsync(string collectionName, string?[] restrictionValues, CancellationToken cancellationToken = default)
#else
public Task<DataTable> GetSchemaAsync(string collectionName, string?[] restrictionValues, CancellationToken cancellationToken = default)
#endif
=> GetSchemaProvider().GetSchemaAsync(AsyncIOBehavior, collectionName, restrictionValues, cancellationToken).AsTask();
private SchemaProvider GetSchemaProvider() => m_schemaProvider ??= new(this);
/// <summary>
/// Gets the time (in seconds) to wait while trying to establish a connection
/// before terminating the attempt and generating an error. This value
/// is controlled by <see cref="MySqlConnectionStringBuilder.ConnectionTimeout"/>,
/// which defaults to 15 seconds.
/// </summary>
public override int ConnectionTimeout => GetConnectionSettings().ConnectionTimeout;
public event MySqlInfoMessageEventHandler? InfoMessage;
/// <summary>
/// Creates a <see cref="MySqlBatch"/> object for executing batched commands.
/// </summary>
#if NET6_0_OR_GREATER
public new MySqlBatch CreateBatch() => new(this);
protected override DbBatch CreateDbBatch() => CreateBatch();
public override bool CanCreateBatch => true;
#else
public MySqlBatch CreateBatch() => new(this);
public bool CanCreateBatch => true;
#endif
protected override void Dispose(bool disposing)
{
try
{
if (disposing)
CloseAsync(changeState: true, IOBehavior.Synchronous).GetAwaiter().GetResult();
}
finally
{
m_isDisposed = true;
base.Dispose(disposing);
}
}
#if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
public override async ValueTask DisposeAsync()
#else
public async Task DisposeAsync()
#endif
{
try
{
await CloseAsync(changeState: true, SimpleAsyncIOBehavior).ConfigureAwait(false);
}
finally
{
m_isDisposed = true;
}
}
public MySqlConnection Clone() => new(m_connectionString, m_hasBeenOpened)
{
ProvideClientCertificatesCallback = ProvideClientCertificatesCallback,
ProvidePasswordCallback = ProvidePasswordCallback,
RemoteCertificateValidationCallback = RemoteCertificateValidationCallback,
};
object ICloneable.Clone() => Clone();
/// <summary>
/// Returns an unopened copy of this connection with a new connection string. If the <c>Password</c>
/// in <paramref name="connectionString"/> is not set, the password from this connection will be used.
/// This allows creating a new connection with the same security information while changing other options,
/// such as database or pooling.
/// </summary>
/// <param name="connectionString">The new connection string to be used.</param>
/// <returns>A new <see cref="MySqlConnection"/> with different connection string options but
/// the same password as this connection (unless overridden by <paramref name="connectionString"/>).</returns>
public MySqlConnection CloneWith(string connectionString)
{
var newBuilder = new MySqlConnectionStringBuilder(connectionString ?? throw new ArgumentNullException(nameof(connectionString)));
var currentBuilder = new MySqlConnectionStringBuilder(m_connectionString);
var shouldCopyPassword = newBuilder.Password.Length == 0 && (!newBuilder.PersistSecurityInfo || currentBuilder.PersistSecurityInfo);
if (shouldCopyPassword)
newBuilder.Password = currentBuilder.Password;
return new MySqlConnection(newBuilder.ConnectionString, m_hasBeenOpened && shouldCopyPassword && !currentBuilder.PersistSecurityInfo)
{
ProvideClientCertificatesCallback = ProvideClientCertificatesCallback,
ProvidePasswordCallback = ProvidePasswordCallback,
RemoteCertificateValidationCallback = RemoteCertificateValidationCallback,
};
}
internal ServerSession Session
{
get
{
VerifyNotDisposed();
if (m_session is null || State != ConnectionState.Open)
throw new InvalidOperationException("Connection must be Open; current state is {0}".FormatInvariant(State));
return m_session;
}
}
internal void SetSessionFailed(Exception exception) => m_session!.SetFailed(exception);
internal void Cancel(ICancellableCommand command, int commandId, bool isCancel)
{
if (m_session?.Id is not string sessionId || State != ConnectionState.Open || m_session?.TryStartCancel(command) is not true)
{
Log.Trace("Ignoring cancellation for closed connection or invalid CommandId {0}", commandId);
return;
}
Log.Debug("CommandId {0} for Session{1} has been canceled via {2}.", commandId, sessionId, isCancel ? "Cancel()" : "command timeout");
try
{
// open a dedicated connection to the server to kill the active query
var csb = new MySqlConnectionStringBuilder(m_connectionString)
{
AutoEnlist = false,
Pooling = false,
};
if (m_session?.IPAddress is { } ipAddress)
csb.Server = ipAddress.ToString();
var cancellationTimeout = GetConnectionSettings().CancellationTimeout;
csb.ConnectionTimeout = cancellationTimeout < 1 ? 3u : (uint) cancellationTimeout;
using var connection = CloneWith(csb.ConnectionString);
connection.Open();
using var killCommand = new MySqlCommand("KILL QUERY {0}".FormatInvariant(command.Connection!.ServerThread), connection);
killCommand.CommandTimeout = cancellationTimeout < 1 ? 3 : cancellationTimeout;
m_session?.DoCancel(command, killCommand);
}
catch (InvalidOperationException ex)
{
// ignore a rare race condition where the connection is open at the beginning of the method, but closed by the time
// KILL QUERY is executed: https://github.com/mysql-net/MySqlConnector/issues/1002
Log.Info(ex, "Session{0} ignoring cancellation for closed connection.", sessionId);
m_session?.AbortCancel(command);
}
catch (MySqlException ex)
{
// cancelling the query failed; setting the state back to 'Querying' will allow another call to 'Cancel' to try again
Log.Info(ex, "Session{0} cancelling CommandId {1} failed", sessionId, command.CommandId);
m_session?.AbortCancel(command);
}
}
internal async Task<CachedProcedure?> GetCachedProcedure(string name, bool revalidateMissing, IOBehavior ioBehavior, CancellationToken cancellationToken)
{
if (Log.IsTraceEnabled())
Log.Trace("Session{0} getting cached procedure Name={1}", m_session!.Id, name);
if (State != ConnectionState.Open)
throw new InvalidOperationException("Connection is not open.");
var cachedProcedures = m_session!.Pool?.GetProcedureCache() ?? m_cachedProcedures;
if (cachedProcedures is null)
{
Log.Info("Session{0} pool Pool{1} doesn't have a shared procedure cache; procedure will only be cached on this connection", m_session.Id, m_session.Pool?.Id);
cachedProcedures = m_cachedProcedures = new();
}
var normalized = NormalizedSchema.MustNormalize(name, Database);
if (string.IsNullOrEmpty(normalized.Schema))
{
Log.Info("Session{0} couldn't normalize Database={1} Name={2}; not caching procedure", m_session.Id, Database, name);
return null;
}
CachedProcedure? cachedProcedure;
bool foundProcedure;
lock (cachedProcedures)
foundProcedure = cachedProcedures.TryGetValue(normalized.FullyQualified, out cachedProcedure);
if (!foundProcedure || (cachedProcedure is null && revalidateMissing))
{
cachedProcedure = await CachedProcedure.FillAsync(ioBehavior, this, normalized.Schema!, normalized.Component!, cancellationToken).ConfigureAwait(false);
if (Log.IsInfoEnabled())
{
if (cachedProcedure is null)
Log.Info("Session{0} failed to cache procedure Schema={1} Component={2}", m_session.Id, normalized.Schema, normalized.Component);
else
Log.Trace("Session{0} caching procedure Schema={1} Component={2}", m_session.Id, normalized.Schema, normalized.Component);
}
int count;
lock (cachedProcedures)
{
cachedProcedures[normalized.FullyQualified] = cachedProcedure;
count = cachedProcedures.Count;
}
if (Log.IsTraceEnabled())
Log.Trace("Session{0} procedure cache Count={1}", m_session.Id, count);
}
if (Log.IsInfoEnabled())
{
if (cachedProcedure is null)
Log.Info("Session{0} did not find cached procedure Schema={1} Component={2}", m_session.Id, normalized.Schema, normalized.Component);
else
Log.Trace("Session{0} returning cached procedure Schema={1} Component={2}", m_session.Id, normalized.Schema, normalized.Component);
}
return cachedProcedure;
}
internal MySqlTransaction? CurrentTransaction { get; set; }
internal bool AllowLoadLocalInfile => GetInitializedConnectionSettings().AllowLoadLocalInfile;
internal bool AllowUserVariables => GetInitializedConnectionSettings().AllowUserVariables;
internal bool AllowZeroDateTime => GetInitializedConnectionSettings().AllowZeroDateTime;
internal bool ConvertZeroDateTime => GetInitializedConnectionSettings().ConvertZeroDateTime;
internal DateTimeKind DateTimeKind => GetInitializedConnectionSettings().DateTimeKind;
internal int DefaultCommandTimeout => GetConnectionSettings().DefaultCommandTimeout;
internal MySqlGuidFormat GuidFormat => GetInitializedConnectionSettings().GuidFormat;
internal bool IgnoreCommandTransaction => GetInitializedConnectionSettings().IgnoreCommandTransaction || m_enlistedTransaction is StandardEnlistedTransaction;
internal bool IgnorePrepare => GetInitializedConnectionSettings().IgnorePrepare;
internal bool NoBackslashEscapes => GetInitializedConnectionSettings().NoBackslashEscapes;
internal bool TreatTinyAsBoolean => GetInitializedConnectionSettings().TreatTinyAsBoolean;
internal IOBehavior AsyncIOBehavior => GetConnectionSettings().ForceSynchronous ? IOBehavior.Synchronous : IOBehavior.Asynchronous;
// Defaults to IOBehavior.Synchronous if the connection hasn't been opened yet; only use if it's a no-op for a closed connection.
internal IOBehavior SimpleAsyncIOBehavior => (m_connectionSettings?.ForceSynchronous ?? false) ? IOBehavior.Synchronous : IOBehavior.Asynchronous;
internal MySqlSslMode SslMode => GetInitializedConnectionSettings().SslMode;
internal int? ActiveCommandId => m_session?.ActiveCommandId;
internal bool HasActiveReader => m_activeReader is not null;
internal void SetActiveReader(MySqlDataReader dataReader)
{
if (dataReader is null)
throw new ArgumentNullException(nameof(dataReader));
if (m_activeReader is not null)
throw new InvalidOperationException("Can't replace active reader.");
m_activeReader = dataReader;
}
internal void FinishQuerying(bool hasWarnings)
{
m_session!.FinishQuerying();
m_activeReader = null;
if (hasWarnings && InfoMessage is not null)
{
var errors = new List<MySqlError>();
using (var command = new MySqlCommand("SHOW WARNINGS;", this))
{
command.Transaction = CurrentTransaction;
using var reader = command.ExecuteReader();
while (reader.Read())
errors.Add(new(reader.GetString(0), reader.GetInt32(1), reader.GetString(2)));
}
InfoMessage(this, new MySqlInfoMessageEventArgs(errors));
}
}
private async ValueTask<ServerSession> CreateSessionAsync(ConnectionPool? pool, int startTickCount, IOBehavior? ioBehavior, CancellationToken cancellationToken)
{
var connectionSettings = GetInitializedConnectionSettings();
var actualIOBehavior = ioBehavior ?? (connectionSettings.ForceSynchronous ? IOBehavior.Synchronous : IOBehavior.Asynchronous);
CancellationTokenSource? timeoutSource = null;
CancellationTokenSource? linkedSource = null;
try
{
// the cancellation token for connection is controlled by 'cancellationToken' (if it can be cancelled), ConnectionTimeout
// (from the connection string, if non-zero), or a combination of both
if (connectionSettings.ConnectionTimeout != 0)
timeoutSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(Math.Max(1, connectionSettings.ConnectionTimeoutMilliseconds - unchecked(Environment.TickCount - startTickCount))));
if (cancellationToken.CanBeCanceled && timeoutSource is not null)
linkedSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutSource.Token);
var connectToken = linkedSource?.Token ?? timeoutSource?.Token ?? cancellationToken;
// get existing session from the pool if possible
if (pool is not null)
{
// this returns an open session
return await pool.GetSessionAsync(this, startTickCount, actualIOBehavior, connectToken).ConfigureAwait(false);
}
else
{
// only "fail over" and "random" load balancers supported without connection pooling
var loadBalancer = connectionSettings.LoadBalance == MySqlLoadBalance.Random && connectionSettings.HostNames!.Count > 1 ?
RandomLoadBalancer.Instance : FailOverLoadBalancer.Instance;
var session = new ServerSession();
session.OwningConnection = new WeakReference<MySqlConnection>(this);
Log.Debug("Created new non-pooled Session{0}", session.Id);
await session.ConnectAsync(connectionSettings, this, startTickCount, loadBalancer, actualIOBehavior, connectToken).ConfigureAwait(false);
return session;
}
}
catch (OperationCanceledException) when (timeoutSource?.IsCancellationRequested ?? false)
{
var messageSuffix = (pool?.IsEmpty ?? false) ? " All pooled connections are in use." : "";
throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Connect Timeout expired." + messageSuffix);
}
catch (MySqlException ex) when ((timeoutSource?.IsCancellationRequested ?? false) || (ex.ErrorCode == MySqlErrorCode.CommandTimeoutExpired))
{
throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Connect Timeout expired.", ex);
}
finally
{
linkedSource?.Dispose();
timeoutSource?.Dispose();
}
}
internal bool SslIsEncrypted => m_session!.SslIsEncrypted;
internal bool SslIsSigned => m_session!.SslIsSigned;
internal bool SslIsAuthenticated => m_session!.SslIsAuthenticated;
internal bool SslIsMutuallyAuthenticated => m_session!.SslIsMutuallyAuthenticated;
internal SslProtocols SslProtocol => m_session!.SslProtocol;
internal void SetState(ConnectionState newState)
{
if (m_connectionState != newState)
{
var previousState = m_connectionState;
m_connectionState = newState;
var eventArgs =
previousState == ConnectionState.Closed && newState == ConnectionState.Connecting ? s_stateChangeClosedConnecting :
previousState == ConnectionState.Connecting && newState == ConnectionState.Open ? s_stateChangeConnectingOpen :
previousState == ConnectionState.Open && newState == ConnectionState.Closed ? s_stateChangeOpenClosed :
new(previousState, newState);
OnStateChange(eventArgs);
}
}
private MySqlConnection(string connectionString, bool hasBeenOpened)
: this(connectionString)
{
m_hasBeenOpened = hasBeenOpened;
}
private void VerifyNotDisposed()
{
if (m_isDisposed)
throw new ObjectDisposedException(GetType().Name);
}
private async Task CloseAsync(bool changeState, IOBehavior ioBehavior)
{
// check fast path
if (m_activeReader is null &&
CurrentTransaction is null &&
m_enlistedTransaction is null &&
(m_connectionSettings?.Pooling ?? false))
{
m_cachedProcedures = null;
if (m_session is not null)
{
await m_session.ReturnToPoolAsync(ioBehavior, this).ConfigureAwait(false);