diff --git a/.ci/build-steps.yml b/.ci/build-steps.yml
index b4eef39cc..b3d6d0895 100644
--- a/.ci/build-steps.yml
+++ b/.ci/build-steps.yml
@@ -6,9 +6,10 @@ steps:
     contents: 'config.json'
     targetFolder: 'tests/IntegrationTests'
 - task: UseDotNet@2
-  displayName: 'Install .NET Core'
+  displayName: 'Install .NET'
   inputs:
     version: $(DotNetCoreSdkVersion)
+    includePreviewVersions: true
 
 - task: DotNetCoreCLI@2
   displayName: 'Restore packages'
@@ -26,58 +27,63 @@ steps:
   displayName: 'Publish MySqlConnector.Tests'
   inputs:
     command: 'publish'
-    arguments: '-c Release -f net7.0 --no-build tests/MySqlConnector.Tests/MySqlConnector.Tests.csproj'
+    arguments: '-c Release -f net9.0 --no-build tests/MySqlConnector.Tests/MySqlConnector.Tests.csproj'
     publishWebProjects: false
     zipAfterPublish: false
-- task: PublishPipelineArtifact@0
+- task: PublishPipelineArtifact@1
   inputs:
-    artifactName: 'MySqlConnector.Tests-7.0-$(Agent.OS)'
-    targetPath: 'tests/MySqlConnector.Tests/bin/Release/net7.0/publish'
+    artifact: 'MySqlConnector.Tests-9.0-$(Agent.OS)'
+    targetPath: 'artifacts/publish/MySqlConnector.Tests/release_net9.0'
+    publishLocation: 'pipeline'
 
 - task: DotNetCoreCLI@2
   displayName: 'Publish Conformance.Tests'
   inputs:
     command: 'publish'
-    arguments: '-c Release -f net7.0 --no-build tests/Conformance.Tests/Conformance.Tests.csproj'
+    arguments: '-c Release -f net9.0 --no-build tests/Conformance.Tests/Conformance.Tests.csproj'
     publishWebProjects: false
     zipAfterPublish: false
-- task: PublishPipelineArtifact@0
+- task: PublishPipelineArtifact@1
   inputs:
-    artifactName: 'Conformance.Tests-7.0-$(Agent.OS)'
-    targetPath: 'tests/Conformance.Tests/bin/Release/net7.0/publish'
+    artifact: 'Conformance.Tests-9.0-$(Agent.OS)'
+    targetPath: 'artifacts/publish/Conformance.Tests/release_net9.0'
+    publishLocation: 'pipeline'
 
 - task: DotNetCoreCLI@2
-  displayName: 'Publish IntegrationTests (3.1)'
+  displayName: 'Publish MySqlConnector.DependencyInjection.Tests'
   inputs:
     command: 'publish'
-    arguments: '-c Release -f netcoreapp3.1 --no-build tests/IntegrationTests/IntegrationTests.csproj'
+    arguments: '-c Release -f net9.0 --no-build tests/MySqlConnector.DependencyInjection.Tests/MySqlConnector.DependencyInjection.Tests.csproj'
     publishWebProjects: false
     zipAfterPublish: false
-- task: PublishPipelineArtifact@0
+- task: PublishPipelineArtifact@1
   inputs:
-    artifactName: 'IntegrationTests-netcoreapp3.1-$(Agent.OS)'
-    targetPath: 'tests/IntegrationTests/bin/Release/netcoreapp3.1/publish'
+    artifact: 'MySqlConnector.DependencyInjection.Tests-9.0-$(Agent.OS)'
+    targetPath: 'artifacts/publish/MySqlConnector.DependencyInjection.Tests/release_net9.0'
+    publishLocation: 'pipeline'
 
 - task: DotNetCoreCLI@2
-  displayName: 'Publish IntegrationTests (7.0)'
+  displayName: 'Publish IntegrationTests (9.0)'
   inputs:
     command: 'publish'
-    arguments: '-c Release -f net7.0 --no-build tests/IntegrationTests/IntegrationTests.csproj'
+    arguments: '-c Release -f net9.0 --no-build tests/IntegrationTests/IntegrationTests.csproj'
     publishWebProjects: false
     zipAfterPublish: false
-- task: PublishPipelineArtifact@0
+- task: PublishPipelineArtifact@1
   inputs:
-    artifactName: 'IntegrationTests-net7.0-$(Agent.OS)'
-    targetPath: 'tests/IntegrationTests/bin/Release/net7.0/publish'
+    artifact: 'IntegrationTests-net9.0-$(Agent.OS)'
+    targetPath: 'artifacts/publish/IntegrationTests/release_net9.0'
+    publishLocation: 'pipeline'
 
 - task: DotNetCoreCLI@2
-  displayName: 'Publish IntegrationTests (6.0)'
+  displayName: 'Publish IntegrationTests (8.0)'
   inputs:
     command: 'publish'
-    arguments: '-c Release -f net6.0 --no-build tests/IntegrationTests/IntegrationTests.csproj'
+    arguments: '-c Release -f net8.0 --no-build tests/IntegrationTests/IntegrationTests.csproj'
     publishWebProjects: false
     zipAfterPublish: false
-- task: PublishPipelineArtifact@0
+- task: PublishPipelineArtifact@1
   inputs:
-    artifactName: 'IntegrationTests-net6.0-$(Agent.OS)'
-    targetPath: 'tests/IntegrationTests/bin/Release/net6.0/publish'
+    artifact: 'IntegrationTests-net8.0-$(Agent.OS)'
+    targetPath: 'artifacts/publish/IntegrationTests/release_net8.0'
+    publishLocation: 'pipeline'
diff --git a/.ci/config/config.compression+ssl.json b/.ci/config/config.compression+ssl.json
index 206c84969..e0bf3504d 100644
--- a/.ci/config/config.compression+ssl.json
+++ b/.ci/config/config.compression+ssl.json
@@ -1,12 +1,12 @@
 {
     "Data": {
       "ConnectionString": "server=127.0.0.1;user id=ssltest;password=test;port=3306;database=mysqltest;ssl mode=required;use compression=true;DefaultCommandTimeout=3600",
-      "SocketPath": "./../../../../../.ci/run/mysql/mysqld.sock",
+      "SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
       "PasswordlessUser": "no_password",
       "SecondaryDatabase": "testdb2",
-      "UnsupportedFeatures": "RsaEncryption,CachingSha2Password,Tls12,Tls13,UuidToBin",
-      "MySqlBulkLoaderLocalCsvFile": "../../../../TestData/LoadData_UTF8_BOM_Unix.CSV",
-      "MySqlBulkLoaderLocalTsvFile": "../../../../TestData/LoadData_UTF8_BOM_Unix.TSV",
-      "CertificatesPath": "../../../../../.ci/server/certs"
+      "UnsupportedFeatures": "CachingSha2Password,Redirection,RsaEncryption,Tls12,Tls13,TlsFingerprintValidation,UuidToBin,Vector",
+      "MySqlBulkLoaderLocalCsvFile": "../../../TestData/LoadData_UTF8_BOM_Unix.CSV",
+      "MySqlBulkLoaderLocalTsvFile": "../../../TestData/LoadData_UTF8_BOM_Unix.TSV",
+      "CertificatesPath": "../../../../.ci/server/certs"
     }
 }
diff --git a/.ci/config/config.compression.json b/.ci/config/config.compression.json
index afd6f6bee..fd312878b 100644
--- a/.ci/config/config.compression.json
+++ b/.ci/config/config.compression.json
@@ -1,11 +1,11 @@
 {
     "Data": {
-      "ConnectionString": "server=127.0.0.1;user id=mysqltest;password=test;port=3306;database=mysqltest;ssl mode=none;UseCompression=true;DefaultCommandTimeout=3600",
-      "SocketPath": "./../../../../../.ci/run/mysql/mysqld.sock",
+      "ConnectionString": "server=127.0.0.1;user id=mysqltest;password=test;port=3306;database=mysqltest;ssl mode=none;UseCompression=true;DefaultCommandTimeout=3600;AllowPublicKeyRetrieval=true",
+      "SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
       "PasswordlessUser": "no_password",
       "SecondaryDatabase": "testdb2",
-      "UnsupportedFeatures": "Ed25519,RsaEncryption,CachingSha2Password,QueryAttributes,Tls12,Tls13,UnixDomainSocket,UuidToBin",
-      "MySqlBulkLoaderLocalCsvFile": "../../../../TestData/LoadData_UTF8_BOM_Unix.CSV",
-      "MySqlBulkLoaderLocalTsvFile": "../../../../TestData/LoadData_UTF8_BOM_Unix.TSV"
+      "UnsupportedFeatures": "Ed25519,QueryAttributes,ParsecAuthentication,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,Vector,ZeroDateTime",
+      "MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV",
+      "MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV"
     }
 }
diff --git a/.ci/config/config.json b/.ci/config/config.json
index b0332563d..67eb66c79 100644
--- a/.ci/config/config.json
+++ b/.ci/config/config.json
@@ -1,11 +1,11 @@
 {
     "Data": {
-      "ConnectionString": "server=127.0.0.1;user id=mysqltest;password=test;port=3306;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600",
-      "SocketPath": "./../../../../../.ci/run/mysql/mysqld.sock",
+      "ConnectionString": "server=127.0.0.1;user id=mysqltest;password=test;port=3306;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600;AllowPublicKeyRetrieval=true",
+      "SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
       "PasswordlessUser": "no_password",
       "SecondaryDatabase": "testdb2",
-      "UnsupportedFeatures": "Ed25519,RsaEncryption,CachingSha2Password,QueryAttributes,Tls12,Tls13,UnixDomainSocket,UuidToBin",
-      "MySqlBulkLoaderLocalCsvFile": "../../../../TestData/LoadData_UTF8_BOM_Unix.CSV",
-      "MySqlBulkLoaderLocalTsvFile": "../../../../TestData/LoadData_UTF8_BOM_Unix.TSV"
+      "UnsupportedFeatures": "Ed25519,QueryAttributes,ParsecAuthentication,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,Vector,ZeroDateTime",
+      "MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV",
+      "MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV"
     }
 }
diff --git a/.ci/config/config.ssl.json b/.ci/config/config.ssl.json
index d2132e290..e136a243d 100644
--- a/.ci/config/config.ssl.json
+++ b/.ci/config/config.ssl.json
@@ -1,12 +1,12 @@
 {
     "Data": {
       "ConnectionString": "server=127.0.0.1;user id=ssltest;password=test;port=3306;database=mysqltest;ssl mode=required;certificate file=../../../../../.ci/server/certs/ssl-client.pfx;DefaultCommandTimeout=3600",
-      "SocketPath": "./../../../../../.ci/run/mysql/mysqld.sock",
+      "SocketPath": "./../../../../.ci/run/mysql/mysqld.sock",
       "PasswordlessUser": "no_password",
       "SecondaryDatabase": "testdb2",
-      "UnsupportedFeatures": "RsaEncryption,CachingSha2Password,Tls12,Tls13,UuidToBin",
-      "MySqlBulkLoaderLocalCsvFile": "../../../../TestData/LoadData_UTF8_BOM_Unix.CSV",
-      "MySqlBulkLoaderLocalTsvFile": "../../../../TestData/LoadData_UTF8_BOM_Unix.TSV",
-      "CertificatesPath": "../../../../../.ci/server/certs"
+      "UnsupportedFeatures": "CachingSha2Password,Redirection,RsaEncryption,Tls12,Tls13,TlsFingerprintValidation,UuidToBin,Vector",
+      "MySqlBulkLoaderLocalCsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.CSV",
+      "MySqlBulkLoaderLocalTsvFile": "../../../../tests/TestData/LoadData_UTF8_BOM_Unix.TSV",
+      "CertificatesPath": "../../../../.ci/server/certs"
     }
 }
diff --git a/.ci/config/config.uds+ssl.json b/.ci/config/config.uds+ssl.json
index 26d67f019..eb754da59 100644
--- a/.ci/config/config.uds+ssl.json
+++ b/.ci/config/config.uds+ssl.json
@@ -1,11 +1,11 @@
 {
     "Data": {
-      "ConnectionString": "server=./../../../../../.ci/mysqld/mysqld.sock;user id=ssltest;password=test;database=mysqltest;ssl mode=required;DefaultCommandTimeout=3600",
+      "ConnectionString": "server=./../../../../.ci/mysqld/mysqld.sock;user id=ssltest;password=test;database=mysqltest;ssl mode=required;DefaultCommandTimeout=3600",
       "PasswordlessUser": "no_password",
       "SecondaryDatabase": "testdb2",
       "UnsupportedFeatures": "None",
-      "MySqlBulkLoaderLocalCsvFile": "../../../../TestData/LoadData_UTF8_BOM_Unix.CSV",
-      "MySqlBulkLoaderLocalTsvFile": "../../../../TestData/LoadData_UTF8_BOM_Unix.TSV",
-      "CertificatesPath": "../../../../../.ci/server/certs"
+      "MySqlBulkLoaderLocalCsvFile": "../../../TestData/LoadData_UTF8_BOM_Unix.CSV",
+      "MySqlBulkLoaderLocalTsvFile": "../../../TestData/LoadData_UTF8_BOM_Unix.TSV",
+      "CertificatesPath": "../../../../.ci/server/certs"
     }
 }
diff --git a/.ci/config/config.uds.json b/.ci/config/config.uds.json
index d3e90c036..6b209d734 100644
--- a/.ci/config/config.uds.json
+++ b/.ci/config/config.uds.json
@@ -1,10 +1,10 @@
 {
     "Data": {
-      "ConnectionString": "server=./../../../../../.ci/run/mysql/mysqld.sock;user id=mysqltest;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600",
+      "ConnectionString": "server=./../../../../.ci/run/mysql/mysqld.sock;user id=mysqltest;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600",
       "PasswordlessUser": "no_password",
       "SecondaryDatabase": "testdb2",
       "UnsupportedFeatures": "None",
-      "MySqlBulkLoaderLocalCsvFile": "../../../../TestData/LoadData_UTF8_BOM_Unix.CSV",
-      "MySqlBulkLoaderLocalTsvFile": "../../../../TestData/LoadData_UTF8_BOM_Unix.TSV"
+      "MySqlBulkLoaderLocalCsvFile": "../../../TestData/LoadData_UTF8_BOM_Unix.CSV",
+      "MySqlBulkLoaderLocalTsvFile": "../../../TestData/LoadData_UTF8_BOM_Unix.TSV"
     }
 }
diff --git a/.ci/conformance-test-steps.yml b/.ci/conformance-test-steps.yml
index 28d38cbc0..87cdf035e 100644
--- a/.ci/conformance-test-steps.yml
+++ b/.ci/conformance-test-steps.yml
@@ -7,17 +7,18 @@ steps:
   condition: always()
   inputs:
     contents: '**/*.trx'
-- task: DownloadPipelineArtifact@0
+- task: DownloadPipelineArtifact@2
   condition: always()
   inputs:
-    artifactName: 'Conformance.Tests-7.0-$(Agent.OS)'
-    targetPath: '$(Build.BinariesDirectory)/7.0'
+    buildType: 'current'
+    artifactName: 'Conformance.Tests-9.0-$(Agent.OS)'
+    targetPath: '$(Build.BinariesDirectory)/9.0'
 - task: DotNetCoreCLI@2
   displayName: 'Conformance Tests'
   inputs:
     command: 'custom'
     custom: 'vstest'
-    arguments: '$(Build.BinariesDirectory)/7.0/Conformance.Tests.dll /logger:trx'
+    arguments: '$(Build.BinariesDirectory)/9.0/Conformance.Tests.dll /logger:trx'
   env:
     CONNECTION_STRING: ${{ parameters.connectionString }}
 - task: PublishTestResults@2
diff --git a/.ci/docker-run.sh b/.ci/docker-run.sh
index f698394bf..08e4efead 100755
--- a/.ci/docker-run.sh
+++ b/.ci/docker-run.sh
@@ -23,12 +23,13 @@ IMAGE=$1
 PORT=$2
 OMIT_FEATURES=$3
 MYSQL_EXTRA=
+MYSQL=mysql
 
-if [ "$IMAGE" == "mysql:8.0" ]; then
-  MYSQL_EXTRA='--default-authentication-plugin=mysql_native_password'
-fi
 if [[ "$IMAGE" == mariadb* ]]; then
-  MYSQL_EXTRA='--in-predicate-conversion-threshold=100000'
+  MYSQL_EXTRA='--in-predicate-conversion-threshold=100000 --plugin-maturity=beta'
+fi
+if [ "$IMAGE" == "mariadb:11.4" ] || [ "$IMAGE" == "mariadb:11.8" ]; then
+  MYSQL='mariadb'
 fi
 
 sudo mkdir -p run/mysql
@@ -42,7 +43,7 @@ docker run -d \
 	-e MYSQL_ROOT_PASSWORD='test' \
 	--tmpfs /var/lib/mysql \
 	$IMAGE \
-  --log-bin-trust-function-creators=1 \
+  --disable-log-bin \
   --local-infile=1 \
   --secure-file-priv=/var/tmp \
   --max-connections=250 \
@@ -52,40 +53,46 @@ for i in `seq 1 120`; do
 	# wait for mysql to come up
 	sleep 1
 	echo "Testing if container is responding"
-	docker exec mysql mysql -uroot -ptest -e "SELECT 1" >/dev/null 2>&1
+	docker exec mysql $MYSQL -uroot -ptest -e "SELECT 1" >/dev/null 2>&1
 	if [ $? -ne 0 ]; then continue; fi
 
 	# try running the init script
 	echo "Creating mysqltest user"
-	docker exec mysql bash -c 'mysql -uroot -ptest < /etc/mysql/conf.d/init.sql'
+	docker exec mysql bash -c "$MYSQL -uroot -ptest < /etc/mysql/conf.d/init.sql"
 	if [ $? -ne 0 ]; then continue; fi
 
 	if [[ $OMIT_FEATURES != *"Sha256Password"* ]]; then
 		echo "Creating sha256_password user"
-	 	docker exec mysql bash -c 'mysql -uroot -ptest < /etc/mysql/conf.d/init_sha256.sql'
+	 	docker exec mysql bash -c "$MYSQL -uroot -ptest < /etc/mysql/conf.d/init_sha256.sql"
 		if [ $? -ne 0 ]; then exit $?; fi
 	fi
 
 	if [[ $OMIT_FEATURES != *"CachingSha2Password"* ]]; then
 		echo "Creating caching_sha2_password user"
-		docker exec mysql bash -c 'mysql -uroot -ptest < /etc/mysql/conf.d/init_caching_sha2.sql'
+		docker exec mysql bash -c "$MYSQL -uroot -ptest < /etc/mysql/conf.d/init_caching_sha2.sql"
 		if [ $? -ne 0 ]; then exit $?; fi
 	fi
 
 	if [[ $OMIT_FEATURES != *"Ed25519"* ]]; then
 		echo "Installing auth_ed25519 component"
-		docker exec mysql bash -c 'mysql -uroot -ptest < /etc/mysql/conf.d/init_ed25519.sql'
+		docker exec mysql bash -c "$MYSQL -uroot -ptest < /etc/mysql/conf.d/init_ed25519.sql"
+		if [ $? -ne 0 ]; then exit $?; fi
+	fi
+
+	if [[ $OMIT_FEATURES != *"ParsecAuthentication"* ]]; then
+		echo "Installing auth_parsec component"
+		docker exec mysql bash -c "$MYSQL -uroot -ptest < /etc/mysql/conf.d/init_parsec.sql"
 		if [ $? -ne 0 ]; then exit $?; fi
 	fi
 
 	if [[ $OMIT_FEATURES != *"QueryAttributes"* ]]; then
 		echo "Installing query_attributes component"
-		docker exec mysql mysql -uroot -ptest -e "INSTALL COMPONENT 'file://component_query_attributes';"
+		docker exec mysql $MYSQL -uroot -ptest -e "INSTALL COMPONENT 'file://component_query_attributes';"
 		if [ $? -ne 0 ]; then exit $?; fi
 	fi
 
 	# exit if successful
-	docker exec mysql mysql -ussltest -ptest \
+	docker exec mysql $MYSQL -ussltest -ptest \
 		--ssl-mode=REQUIRED \
 		--ssl-ca=/etc/mysql/conf.d/certs/ssl-ca-cert.pem \
 		--ssl-cert=/etc/mysql/conf.d/certs/ssl-client-cert.pem \
@@ -93,7 +100,7 @@ for i in `seq 1 120`; do
 		-e "SELECT 1"
 	if [ $? -ne 0 ]; then
 		# mariadb uses --ssl=TRUE instead of --ssl-mode=REQUIRED
-		docker exec mysql mysql -ussltest -ptest \
+		docker exec mysql $MYSQL -ussltest -ptest \
 			--ssl=TRUE \
 			--ssl-ca=/etc/mysql/conf.d/certs/ssl-ca-cert.pem \
 			--ssl-cert=/etc/mysql/conf.d/certs/ssl-client-cert.pem \
diff --git a/.ci/integration-test-steps.yml b/.ci/integration-test-steps.yml
index c4c4d3135..f45f7ac9c 100644
--- a/.ci/integration-test-steps.yml
+++ b/.ci/integration-test-steps.yml
@@ -10,9 +10,10 @@ steps:
   condition: always()
   inputs:
     contents: '**/*.trx'
-- task: DownloadPipelineArtifact@0
+- task: DownloadPipelineArtifact@2
   condition: always()
   inputs:
+    buildType: 'current'
     artifactName: ${{ format('IntegrationTests-{0}-$(Agent.OS)', parameters.platform) }}
     targetPath: ${{ format('$(Build.BinariesDirectory)/{0}', parameters.platform) }}
 - task: DotNetCoreCLI@2
@@ -35,4 +36,4 @@ steps:
     testResultsFormat: VSTest
     testResultsFiles: '**/*.trx'
     testRunTitle: ${{ format('{0}, $(Agent.OS), {1}, {2}', parameters.image, parameters.platform, parameters.description) }}
-    failTaskOnFailedTests: true
\ No newline at end of file
+    failTaskOnFailedTests: true
diff --git a/.ci/integration-tests-steps.yml b/.ci/integration-tests-steps.yml
index b22e0f007..db26b23ed 100644
--- a/.ci/integration-tests-steps.yml
+++ b/.ci/integration-tests-steps.yml
@@ -5,58 +5,52 @@ parameters:
   DotNetCoreSdkVersion: ''
 
 steps:
+- task: Docker@2
+  displayName: Log in to Docker
+  inputs:
+    command: login
+    containerRegistry: DockerHub
 - bash: ${{ format('.ci/docker-run.sh {0} 3300 {1}', parameters.image, parameters.unsupportedFeatures) }}
   displayName: 'Start Docker container'
 - task: UseDotNet@2
-  displayName: 'Install .NET Core 3.1'
+  displayName: 'Install .NET 8.0'
   inputs:
-    version: 3.1.x
+    version: 8.0.x
     packageType: runtime
 - task: UseDotNet@2
-  displayName: 'Install .NET Core 6.0'
-  inputs:
-    version: 6.0.x
-    packageType: runtime
-- task: UseDotNet@2
-  displayName: 'Install .NET Core'
+  displayName: 'Install .NET'
   inputs:
     version: $(DotNetCoreSdkVersion)
+    includePreviewVersions: true
 - template: 'conformance-test-steps.yml'
   parameters:
     image: ${{ parameters.image }}
-    connectionString: 'server=localhost;port=3300;user id=mysqltest;password=test;database=conformance;ssl mode=none;DefaultCommandTimeout=3600'
-- template: 'integration-test-steps.yml'
-  parameters:
-    image: ${{ parameters.image }}
-    unsupportedFeatures: ${{ parameters.unsupportedFeatures }}
-    connectionString: 'server=localhost;port=3300;user id=mysqltest;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600'
-    platform: 'netcoreapp3.1'
-    description: 'No SSL'
+    connectionString: 'server=localhost;port=3300;user id=mysqltest;password=test;database=conformance;ssl mode=none;DefaultCommandTimeout=3600;${{ parameters.connectionStringExtra }}'
 - template: 'integration-test-steps.yml'
   parameters:
     image: ${{ parameters.image }}
     unsupportedFeatures: ${{ parameters.unsupportedFeatures }}
     connectionString: server=localhost;port=3300;user id=mysqltest;password=test;database=mysqltest;ssl mode=required;DefaultCommandTimeout=3600;certificate file=$(Build.Repository.LocalPath)/.ci/server/certs/ssl-client.pfx;${{ parameters.connectionStringExtra }}
-    platform: 'net7.0'
+    platform: 'net8.0'
     description: 'SSL'
 - template: 'integration-test-steps.yml'
   parameters:
     image: ${{ parameters.image }}
     unsupportedFeatures: ${{ parameters.unsupportedFeatures }}
-    connectionString: 'server=localhost;port=3300;user id=mysqltest;password=test;database=mysqltest;ssl mode=none;UseCompression=True;DefaultCommandTimeout=3600'
-    platform: 'net6.0'
+    connectionString: 'server=localhost;port=3300;user id=mysqltest;password=test;database=mysqltest;ssl mode=none;UseCompression=True;DefaultCommandTimeout=3600;${{ parameters.connectionStringExtra }}'
+    platform: 'net8.0'
     description: 'Compression'
 - template: 'integration-test-steps.yml'
   parameters:
     image: ${{ parameters.image }}
     unsupportedFeatures: ${{ parameters.unsupportedFeatures }}
-    connectionString: 'server=localhost;port=3300;user id=mysqltest;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600'
-    platform: 'net6.0'
+    connectionString: 'server=localhost;port=3300;user id=mysqltest;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600;${{ parameters.connectionStringExtra }}'
+    platform: 'net9.0'
     description: 'No SSL'
 - template: 'integration-test-steps.yml'
   parameters:
     image: ${{ parameters.image }}
     unsupportedFeatures: ${{ parameters.unsupportedFeatures }}
     connectionString: server=localhost;port=3300;user id=mysqltest;password=test;database=mysqltest;ssl mode=required;DefaultCommandTimeout=3600;certificate file=$(Build.Repository.LocalPath)/.ci/server/certs/ssl-client.pfx;${{ parameters.connectionStringExtra }}
-    platform: 'net6.0'
-    description: 'SSL'
\ No newline at end of file
+    platform: 'net9.0'
+    description: 'SSL'
diff --git a/.ci/mysqlconnector-tests-steps.yml b/.ci/mysqlconnector-tests-steps.yml
index 6e25b9f90..0ddc01fc9 100644
--- a/.ci/mysqlconnector-tests-steps.yml
+++ b/.ci/mysqlconnector-tests-steps.yml
@@ -1,11 +1,13 @@
 steps:
 - task: UseDotNet@2
-  displayName: 'Install .NET Core'
+  displayName: 'Install .NET'
   inputs:
     version: $(DotNetCoreSdkVersion)
-- task: DownloadPipelineArtifact@0
+    includePreviewVersions: true
+- task: DownloadPipelineArtifact@2
   inputs:
-    artifactName: 'MySqlConnector.Tests-7.0-$(Agent.OS)'
+    buildType: 'current'
+    artifactName: 'MySqlConnector.Tests-9.0-$(Agent.OS)'
     targetPath: $(System.DefaultWorkingDirectory)
 - task: DotNetCoreCLI@2
   displayName: 'Run MySqlConnector.Tests'
@@ -13,8 +15,19 @@ steps:
     command: 'custom'
     custom: 'vstest'
     arguments: 'MySqlConnector.Tests.dll /logger:trx'
+- task: DownloadPipelineArtifact@2
+  inputs:
+    buildType: 'current'
+    artifactName: 'MySqlConnector.DependencyInjection.Tests-9.0-$(Agent.OS)'
+    targetPath: $(System.DefaultWorkingDirectory)
+- task: DotNetCoreCLI@2
+  displayName: 'Run MySqlConnector.DependencyInjection.Tests'
+  inputs:
+    command: 'custom'
+    custom: 'vstest'
+    arguments: 'MySqlConnector.DependencyInjection.Tests.dll /logger:trx'
 - task: PublishTestResults@2
   inputs:
     testResultsFormat: VSTest
     testResultsFiles: '**/*.trx'
-    testRunTitle: 'MySqlConnector.Tests-7.0-$(Agent.OS)'
\ No newline at end of file
+    testRunTitle: 'MySqlConnector.Tests-9.0-$(Agent.OS)'
diff --git a/.ci/server/init_ed25519.sql b/.ci/server/init_ed25519.sql
index a36b23fe4..18b3c0678 100644
--- a/.ci/server/init_ed25519.sql
+++ b/.ci/server/init_ed25519.sql
@@ -1,3 +1,6 @@
 INSTALL SONAME 'auth_ed25519';
 CREATE USER 'ed25519user'@'%' IDENTIFIED VIA ed25519 USING PASSWORD('Ed255!9');
-GRANT ALL PRIVILEGES ON *.* TO 'ed25519user'@'%';
\ No newline at end of file
+GRANT ALL PRIVILEGES ON *.* TO 'ed25519user'@'%';
+
+CREATE USER 'multiAuthUser'@'%' IDENTIFIED VIA ed25519 USING PASSWORD('Ed255!9') OR mysql_native_password as password("secret");
+GRANT ALL PRIVILEGES ON *.* TO 'multiAuthUser'@'%';
diff --git a/.ci/server/init_parsec.sql b/.ci/server/init_parsec.sql
new file mode 100644
index 000000000..85c8d6894
--- /dev/null
+++ b/.ci/server/init_parsec.sql
@@ -0,0 +1,3 @@
+INSTALL SONAME 'auth_parsec';
+CREATE USER 'parsec-user'@'%' IDENTIFIED via parsec using PASSWORD('P@rs3c-Pa55');
+GRANT ALL PRIVILEGES ON *.* TO 'parsec-user'@'%';
diff --git a/.ci/test.ps1 b/.ci/test.ps1
index 75d4a0c90..7d595e35e 100644
--- a/.ci/test.ps1
+++ b/.ci/test.ps1
@@ -23,6 +23,12 @@ if ($LASTEXITCODE -ne 0){
     exit $LASTEXITCODE;
 }
 popd
+pushd tests\MySqlConnector.DependencyInjection.Tests
+dotnet test -c Release
+if ($LASTEXITCODE -ne 0){
+    exit $LASTEXITCODE;
+}
+popd
 
 pushd .\tests\IntegrationTests
 
@@ -32,14 +38,14 @@ dotnet test -c Release -f net462
 if ($LASTEXITCODE -ne 0){
     exit $LASTEXITCODE;
 }
-dotnet test -c Release -f net7.0
+dotnet test -c Release -f net9.0
 if ($LASTEXITCODE -ne 0){
     exit $LASTEXITCODE;
 }
 
 echo "Executing integration tests with Compression, No SSL"
 Copy-Item -Force ..\..\.ci\config\config.compression.json config.json
-dotnet test -c Release -f netcoreapp3.1
+dotnet test -c Release -f net8.0
 if ($LASTEXITCODE -ne 0){
     exit $LASTEXITCODE;
 }
diff --git a/.editorconfig b/.editorconfig
index cca51b0ea..4a511abec 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -16,13 +16,16 @@ indent_style = tab
 indent_size = 4
 
 # changes from VS2017 defaults
+csharp_style_expression_bodied_local_functions = true:suggestion
 csharp_style_expression_bodied_methods = true:suggestion
 csharp_style_expression_bodied_constructors = true:suggestion
 csharp_style_expression_bodied_operators = true:suggestion
+csharp_style_namespace_declarations = file_scoped
 csharp_prefer_braces = false:none
-csharp_indent_switch_labels = false
+csharp_indent_switch_labels = true
 csharp_space_after_cast = true
 csharp_preserve_single_line_statements = false
+dotnet_style_prefer_conditional_expression_over_return = false:suggestion
 
 # use VS2017 defaults, but make them warnings (instead of none)
 dotnet_style_qualification_for_field = false:warning
@@ -79,16 +82,19 @@ dotnet_diagnostic.CA1508.severity = silent # Avoid dead conditional code.
 dotnet_diagnostic.CA1510.severity = none # Use ArgumentNullException.ThrowIfNull.
 dotnet_diagnostic.CA1513.severity = none # Use ObjectDisposedException.ThrowIf.
 dotnet_diagnostic.CA1849.severity = none # Call async methods when in an async method.
-dotnet_diagnostic.CA1859.severity = silent # Use concrete return types for performance.
 dotnet_diagnostic.CA2000.severity = none # Use recommended Dispose pattern.
 dotnet_diagnostic.CA2100.severity = none # Review SQL queries for security vulnerabilities.
 dotnet_diagnostic.CA2213.severity = silent # Disposable fields should be disposed.
 dotnet_diagnostic.CA5398.severity = none # Avoid hardcoded SslProtocols values.
+dotnet_diagnostic.IDE0044.severity = error # Make fields readonly.
+dotnet_diagnostic.IDE0045.severity = suggestion # Use conditional expression for assignment.
+dotnet_diagnostic.IDE0058.severity = none # Remove unnecessary expression value.
+dotnet_diagnostic.IDE0059.severity = none # Remove unnecessary value assignment.
+dotnet_diagnostic.IDE0065.severity = error # Using directives must be placed outside of a namespace declaration.
+dotnet_diagnostic.IDE0072.severity = silent # Add missing cases to switch expression.
 dotnet_diagnostic.SA1003.severity = none # Operator must not be followed by whitespace.
 dotnet_diagnostic.SA1008.severity = none # Opening parenthesis must not be preceded by a space.
 dotnet_diagnostic.SA1009.severity = none # Closing parenthesis must not be followed by a space.
-dotnet_diagnostic.SA1010.severity = silent # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3503
-dotnet_diagnostic.SA1011.severity = silent # https://github.com/DotNetAnalyzers/StyleCopAnalyzers/issues/3503
 dotnet_diagnostic.SA1027.severity = none # Tabs must not be used.
 dotnet_diagnostic.SA1101.severity = none # Prefix local calls with this.
 dotnet_diagnostic.SA1116.severity = none # Split parameters should start on line after declaration.
@@ -109,6 +115,7 @@ dotnet_diagnostic.SA1500.severity = none # Braces for multi-line statements shou
 dotnet_diagnostic.SA1503.severity = none # Braces should not be omitted.
 dotnet_diagnostic.SA1513.severity = none # Closing brace should be followed by blank line.
 dotnet_diagnostic.SA1516.severity = none # Elements should be separated by blank line.
+dotnet_diagnostic.SA1519.severity = none # Braces should not be omitted from multi-line child statement
 dotnet_diagnostic.SA1600.severity = none # Elements should be documented.
 dotnet_diagnostic.SA1601.severity = none # Partial elements should be documented.
 dotnet_diagnostic.SA1602.severity = none # Enumeration items should be documented.
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 000000000..a79c209a5
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,25 @@
+# AI Instructions
+
+## Project Context
+
+MySqlConnector is a high-performance ADO.NET Data Provider for MySQL.
+It implements the MySQL protocol and handles authentication, connection pooling, and data access.
+
+## Key Conventions
+
+### Code Style
+- follow Microsoft's C# coding conventions with the exception that field names are prefixed with `m_` and are specified at the end of the class definition
+- use C# nullable reference types and implicit usings
+- XML documentation required for public APIs
+- use tabs for indentation in *.cs files; use two spaces for indentation in XML and JSON files, including *.csproj files
+- follow the rules from `.editorconfig`
+- implement async methods using the IOBehavior pattern: sync and async methods delegate to a common implementation that takes an IOBehavior parameter and handles both I/O types
+
+### Performance
+- minimize allocations in hot paths
+- use Span-based APIs wherever possible
+
+### Testing
+- Test files should be located in corresponding test directories. Place unit tests in tests/MySqlConnector.Tests. Place integration tests in tests/IntegrationTests.
+- Test names should be descriptive and follow the pattern `CamelCaseShortDescription`.
+- Use xUnit for tests.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 000000000..b9995083e
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,57 @@
+name: "Continuous Integration"
+
+on:
+  push:
+    branches: [ master, test ]
+
+jobs:
+  component-detection:
+    name: Component Detection
+    runs-on: ubuntu-latest
+    permissions:
+      contents: write
+
+    steps:
+    - name: Check out repository
+      uses: actions/checkout@v4
+
+    - name: Set up .NET
+      uses: actions/setup-dotnet@v4
+
+    - name: Restore
+      run: dotnet restore
+
+    - name: Build
+      run: dotnet build --configuration Release --no-restore
+
+    - name: Component detection
+      uses: advanced-security/component-detection-dependency-submission-action@v0.1.0
+
+  native-aot:
+    name: Native AOT
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        tfm: [ 'net8.0', 'net9.0' ]
+    services:
+      mysql:
+        image: mysql:9.4
+        env:
+          MYSQL_DATABASE: testdb
+          MYSQL_ROOT_PASSWORD: pass
+        ports:
+          - 3306:3306
+          # Before continuing, verify the mysql container is reachable from the ubuntu host
+        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
+    steps:
+      - name: Check out repository
+        uses: actions/checkout@v4
+
+      - name: Set up .NET
+        uses: actions/setup-dotnet@v4
+
+      - name: Publish
+        run: dotnet publish -c Release -r linux-x64 -f ${{ matrix.tfm }} tests/MySqlConnector.NativeAot.Tests/MySqlConnector.NativeAot.Tests.csproj
+
+      - name: Test Native AOT
+        run: ./artifacts/publish/MySqlConnector.NativeAot.Tests/release_${{ matrix.tfm }}_linux-x64/MySqlConnector.NativeAot.Tests
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index cd7d2c236..241a74d82 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -23,26 +23,20 @@ jobs:
         language: [ 'csharp' ]
 
     steps:
-    - name: Checkout repository
-      uses: actions/checkout@v3
+    - name: Check out
+      uses: actions/checkout@v4
 
-    - name: Set up .NET 7.0
-      uses: actions/setup-dotnet@v3
-      with:
-        dotnet-version: 7.x
-        dotnet-quality: preview
+    - name: Set up .NET
+      uses: actions/setup-dotnet@v4
 
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@v2
+      uses: github/codeql-action/init@v3
       with:
         languages: ${{ matrix.language }}
-        # If you wish to specify custom queries, you can do so here or in a config file.
-        # By default, queries listed here will override any specified in a config file.
-        # Prefix the list here with "+" to use these queries and those in the config file.
-        # queries: ./path/to/local/query, your-org/your-repo/queries@main
+        queries: security-extended
 
-    - name: Autobuild
-      uses: github/codeql-action/autobuild@v2
+    - name: Build
+      uses: github/codeql-action/autobuild@v3
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v2
+      uses: github/codeql-action/analyze@v3
diff --git a/.gitignore b/.gitignore
index b58dd54e1..c40f33ac0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
 # ignore output folders
 BenchmarkDotNet.Artifacts/
+artifacts/
 bin/
 build/
 obj/
@@ -10,6 +11,7 @@ project.lock.json
 _ReSharper.*
 TestResult.xml
 Thumbs.db
+.aider*
 *.csproj.user
 *.DotSettings.user
 .idea
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 0bf1718e7..663c360cb 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -6,5 +6,6 @@
 	// Controls if the editor will insert spaces for tabs. Accepted values:  "auto", true, false. If set to "auto", the value will be guessed when a file is opened.
 	"editor.insertSpaces": false,
 
-	"files.trimTrailingWhitespace": true
+	"files.trimTrailingWhitespace": true,
+	"dotnet.defaultSolution": "MySqlConnector.sln"
 }
\ No newline at end of file
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 000000000..10bca8b48
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,23 @@
+<Project>
+
+  <PropertyGroup>
+    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+    <LangVersion>preview</LangVersion>
+    <AccelerateBuildsInVisualStudio>true</AccelerateBuildsInVisualStudio>
+    <NoWarn>$(NoWarn);1591;CA1708;CA1835;CA2215;CA5397;NU5105;SYSLIB0039</NoWarn>
+    <ArtifactsPath>$(MSBuildThisFileDirectory)artifacts</ArtifactsPath>
+    <NuGetAudit>true</NuGetAudit>
+    <NuGetAuditLevel>low</NuGetAuditLevel>
+    <NuGetAuditMode>all</NuGetAuditMode>
+  </PropertyGroup>
+
+  <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+    <RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
+  </PropertyGroup>
+
+  <PropertyGroup Condition=" '$(GITHUB_ACTIONS)' == 'true' OR '$(APPVEYOR)' == 'True' OR '$(TF_BUILD)' == 'True' ">
+    <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
+    <Deterministic>true</Deterministic>
+  </PropertyGroup>
+
+</Project>
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 000000000..c66ca1f40
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,37 @@
+<Project>
+  <PropertyGroup>
+    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
+    <CentralPackageTransitivePinningEnabled>true</CentralPackageTransitivePinningEnabled>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageVersion Include="AdoNet.Specification.Tests" Version="2.0.0-beta.2" />
+    <PackageVersion Include="Dapper.StrongName" Version="2.1.35" />
+    <PackageVersion Include="IndexRange" Version="1.0.3" />
+    <PackageVersion Include="log4net" Version="2.0.17" />
+    <PackageVersion Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="8.0.16" />
+    <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
+    <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
+    <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.1" />
+    <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
+    <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
+    <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.2" />
+    <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
+    <PackageVersion Include="MySql.Data" Version="9.4.0" />
+    <PackageVersion Include="NLog" Version="4.7.15" />
+    <PackageVersion Include="Serilog" Version="2.12.0" />
+    <PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="8.0.1" />
+    <PackageVersion Include="System.Text.Json" Version="9.0.1" />
+    <PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
+    <PackageVersion Include="xunit" Version="2.9.3" />
+    <PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
+    <PackageVersion Include="YamlDotNet" Version="16.3.0" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <GlobalPackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
+    <GlobalPackageReference Include="MinVer" Version="6.0.0" />
+    <GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
+  </ItemGroup>
+
+</Project>
diff --git a/LICENSE b/LICENSE
index 11f3e7c69..7fad5192d 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2016-2023 Bradley Grainger
+Copyright (c) 2016-2024 Bradley Grainger
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/MySqlConnector.sln b/MySqlConnector.sln
deleted file mode 100644
index 9b4d24e70..000000000
--- a/MySqlConnector.sln
+++ /dev/null
@@ -1,83 +0,0 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.3.32611.2
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MySqlConnector", "src\MySqlConnector\MySqlConnector.csproj", "{F82378AF-274E-4FBA-8E45-27126D607B85}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MySqlConnector.Tests", "tests\MySqlConnector.Tests\MySqlConnector.Tests.csproj", "{5E4FF768-D007-4D66-8220-AE789CC614C2}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTests", "tests\IntegrationTests\IntegrationTests.csproj", "{407C2AC9-0CCA-4D6B-8698-362976D97730}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Conformance.Tests", "tests\Conformance.Tests\Conformance.Tests.csproj", "{CC0DA702-43E8-471E-9320-F36685C540A1}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MySqlConnector.Logging.log4net", "src\MySqlConnector.Logging.log4net\MySqlConnector.Logging.log4net.csproj", "{A15647B8-FA3F-4536-BF4E-4F93F2FBFC75}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MySqlConnector.Logging.Microsoft.Extensions.Logging", "src\MySqlConnector.Logging.Microsoft.Extensions.Logging\MySqlConnector.Logging.Microsoft.Extensions.Logging.csproj", "{6A3F1732-F874-463E-9BB8-21690E7B8ED0}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MySqlConnector.Logging.Serilog", "src\MySqlConnector.Logging.Serilog\MySqlConnector.Logging.Serilog.csproj", "{38806B85-6526-4A81-9905-45D3411B0AE2}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MySqlConnector.Logging.NLog", "src\MySqlConnector.Logging.NLog\MySqlConnector.Logging.NLog.csproj", "{92015BEE-563A-4595-9243-0510D2B8767F}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MySqlConnector.Authentication.Ed25519", "src\MySqlConnector.Authentication.Ed25519\MySqlConnector.Authentication.Ed25519.csproj", "{5DB4FA2E-910B-47CE-B467-F6852104D567}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{D0EDA522-FB29-4C44-B4EC-0D3AF72C016D}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SchemaCollectionGenerator", "tools\SchemaCollectionGenerator\SchemaCollectionGenerator.csproj", "{2C8FA08E-755D-454D-B98E-10A122673ABE}"
-EndProject
-Global
-	GlobalSection(SolutionConfigurationPlatforms) = preSolution
-		Debug|Any CPU = Debug|Any CPU
-		Release|Any CPU = Release|Any CPU
-	EndGlobalSection
-	GlobalSection(ProjectConfigurationPlatforms) = postSolution
-		{F82378AF-274E-4FBA-8E45-27126D607B85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{F82378AF-274E-4FBA-8E45-27126D607B85}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{F82378AF-274E-4FBA-8E45-27126D607B85}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{F82378AF-274E-4FBA-8E45-27126D607B85}.Release|Any CPU.Build.0 = Release|Any CPU
-		{5E4FF768-D007-4D66-8220-AE789CC614C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{5E4FF768-D007-4D66-8220-AE789CC614C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{5E4FF768-D007-4D66-8220-AE789CC614C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{5E4FF768-D007-4D66-8220-AE789CC614C2}.Release|Any CPU.Build.0 = Release|Any CPU
-		{407C2AC9-0CCA-4D6B-8698-362976D97730}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{407C2AC9-0CCA-4D6B-8698-362976D97730}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{407C2AC9-0CCA-4D6B-8698-362976D97730}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{407C2AC9-0CCA-4D6B-8698-362976D97730}.Release|Any CPU.Build.0 = Release|Any CPU
-		{CC0DA702-43E8-471E-9320-F36685C540A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{CC0DA702-43E8-471E-9320-F36685C540A1}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{CC0DA702-43E8-471E-9320-F36685C540A1}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{CC0DA702-43E8-471E-9320-F36685C540A1}.Release|Any CPU.Build.0 = Release|Any CPU
-		{A15647B8-FA3F-4536-BF4E-4F93F2FBFC75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{A15647B8-FA3F-4536-BF4E-4F93F2FBFC75}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{A15647B8-FA3F-4536-BF4E-4F93F2FBFC75}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{A15647B8-FA3F-4536-BF4E-4F93F2FBFC75}.Release|Any CPU.Build.0 = Release|Any CPU
-		{6A3F1732-F874-463E-9BB8-21690E7B8ED0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{6A3F1732-F874-463E-9BB8-21690E7B8ED0}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{6A3F1732-F874-463E-9BB8-21690E7B8ED0}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{6A3F1732-F874-463E-9BB8-21690E7B8ED0}.Release|Any CPU.Build.0 = Release|Any CPU
-		{38806B85-6526-4A81-9905-45D3411B0AE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{38806B85-6526-4A81-9905-45D3411B0AE2}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{38806B85-6526-4A81-9905-45D3411B0AE2}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{38806B85-6526-4A81-9905-45D3411B0AE2}.Release|Any CPU.Build.0 = Release|Any CPU
-		{92015BEE-563A-4595-9243-0510D2B8767F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{92015BEE-563A-4595-9243-0510D2B8767F}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{92015BEE-563A-4595-9243-0510D2B8767F}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{92015BEE-563A-4595-9243-0510D2B8767F}.Release|Any CPU.Build.0 = Release|Any CPU
-		{5DB4FA2E-910B-47CE-B467-F6852104D567}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{5DB4FA2E-910B-47CE-B467-F6852104D567}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{5DB4FA2E-910B-47CE-B467-F6852104D567}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{5DB4FA2E-910B-47CE-B467-F6852104D567}.Release|Any CPU.Build.0 = Release|Any CPU
-		{2C8FA08E-755D-454D-B98E-10A122673ABE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{2C8FA08E-755D-454D-B98E-10A122673ABE}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{2C8FA08E-755D-454D-B98E-10A122673ABE}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{2C8FA08E-755D-454D-B98E-10A122673ABE}.Release|Any CPU.Build.0 = Release|Any CPU
-	EndGlobalSection
-	GlobalSection(SolutionProperties) = preSolution
-		HideSolutionNode = FALSE
-	EndGlobalSection
-	GlobalSection(NestedProjects) = preSolution
-		{2C8FA08E-755D-454D-B98E-10A122673ABE} = {D0EDA522-FB29-4C44-B4EC-0D3AF72C016D}
-	EndGlobalSection
-	GlobalSection(ExtensibilityGlobals) = postSolution
-		SolutionGuid = {4784EA55-5DE0-404F-8BEC-E14C202F01FA}
-	EndGlobalSection
-EndGlobal
diff --git a/MySqlConnector.slnx b/MySqlConnector.slnx
new file mode 100644
index 000000000..1c13b6d95
--- /dev/null
+++ b/MySqlConnector.slnx
@@ -0,0 +1,17 @@
+<Solution>
+  <Folder Name="/tools/">
+    <Project Path="tools/SchemaCollectionGenerator/SchemaCollectionGenerator.csproj" />
+  </Folder>
+  <Project Path="src/MySqlConnector.Authentication.Ed25519/MySqlConnector.Authentication.Ed25519.csproj" />
+  <Project Path="src/MySqlConnector.DependencyInjection/MySqlConnector.DependencyInjection.csproj" />
+  <Project Path="src/MySqlConnector.Logging.log4net/MySqlConnector.Logging.log4net.csproj" />
+  <Project Path="src/MySqlConnector.Logging.Microsoft.Extensions.Logging/MySqlConnector.Logging.Microsoft.Extensions.Logging.csproj" />
+  <Project Path="src/MySqlConnector.Logging.NLog/MySqlConnector.Logging.NLog.csproj" />
+  <Project Path="src/MySqlConnector.Logging.Serilog/MySqlConnector.Logging.Serilog.csproj" />
+  <Project Path="src/MySqlConnector/MySqlConnector.csproj" />
+  <Project Path="tests/Conformance.Tests/Conformance.Tests.csproj" />
+  <Project Path="tests/IntegrationTests/IntegrationTests.csproj" />
+  <Project Path="tests/MySqlConnector.DependencyInjection.Tests/MySqlConnector.DependencyInjection.Tests.csproj" />
+  <Project Path="tests/MySqlConnector.NativeAot.Tests/MySqlConnector.NativeAot.Tests.csproj" />
+  <Project Path="tests/MySqlConnector.Tests/MySqlConnector.Tests.csproj" />
+</Solution>
diff --git a/NuGet.config b/NuGet.config
index 6873eb959..4d736c19e 100644
--- a/NuGet.config
+++ b/NuGet.config
@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <configuration>
   <packageSources>
+    <clear />
     <add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
   </packageSources>
 </configuration>
diff --git a/README.md b/README.md
index d9706ca5a..a3da17a0e 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,10 @@
-# Async MySQL Connector for .NET and .NET Core
+# MySQL Connector for .NET and .NET Core
+
+[![NuGet](https://img.shields.io/nuget/vpre/MySqlConnector.svg)](https://www.nuget.org/packages/MySqlConnector/)
 
 This is an [ADO.NET](https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/) data
-provider for [MySQL](https://www.mysql.com/). It provides implementations of
+provider for [MySQL](https://www.mysql.com/) and other compatible servers including [MariaDB](https://www.mariadb.org).
+It provides implementations of
 `DbConnection`, `DbCommand`, `DbDataReader`, `DbTransaction`—the classes
 needed to query and update databases from managed code.
 
@@ -9,35 +12,39 @@ Complete documentation is available at the [MySqlConnector Documentation Website
 
 ## Why Use This Library?
 
-### Async Support
-
-This library implements true asynchronous I/O for database operations, without blocking
-(or using `Task.Run` to run synchronous methods on a background thread). This greatly
-improves the throughput of a web server that performs database operations.
-
 ### Performance
 
 This library outperforms MySQL Connector/NET (`MySql.Data`) on benchmarks:
 
-![Benchmark](https://files.logoscdn.com/v1/files/63673908/assets/13928411/content.png?signature=MVHBZxDfB0J-0Pueja8NtvuLD9A)
+![Benchmark](https://files.logoscdn.com/v1/assets/15435916/optimized)
 
-(Client: MySqlConnector 2.2.0, Ubuntu 20.04, .NET 7.0; Server: Azure Database for MySQL 8.0.28, TLS 1.2)
+(Client: MySqlConnector 2.3.1, MySql.Data 8.2.0, Ubuntu 23.04, .NET 8.0; Server: Azure Database for MySQL 8.0.34, TLS 1.2)
+
+### Server Compatibility
+
+This library is compatible with [many MySQL-compatible servers](https://mysqlconnector.net/#server-compatibility), including MySQL 5.5 and newer and MariaDB 10.x and newer.
+MySql.Data [only supports MySQL Server](https://bugs.mysql.com/bug.php?id=109331).
 
 ### Bug Fixes
 
 This library [fixes dozens of outstanding bugs](https://mysqlconnector.net/tutorials/migrating-from-connector-net/#fixed-bugs) in Connector/NET.
 
+### Cutting Edge
+
+This library implements the latest ADO.NET APIs, from async (introduced in .NET Framework 4.5), through `DbBatch` (.NET 6.0) and `DbDataSource` (.NET 7.0).
+
 ### License
 
 This library is [MIT-licensed](LICENSE) and may be freely distributed with commercial software.
 Commercial software that uses Connector/NET may have to purchase a [commercial license](https://www.mysql.com/about/legal/licensing/oem/)
 from Oracle.
 
-## ORMs
+## Related Projects
 
 This library is compatible with popular .NET ORMs including:
 
 * [Dapper](https://dapperlib.github.io/Dapper/) ([GitHub](https://github.com/DapperLib/Dapper), [NuGet](https://www.nuget.org/packages/Dapper))
+* [Faithlife.Data](https://faithlife.github.io/FaithlifeData/) ([GitHub](https://github.com/Faithlife/FaithlifeData), [NuGet](https://www.nuget.org/packages/Faithlife.Data))
 * [FreeSql](https://freesql.net/) ([GitHub](https://github.com/dotnetcore/FreeSql), [NuGet](https://www.nuget.org/packages/FreeSql.Provider.MySqlConnector/))
 * [LINQ to DB](https://linq2db.github.io/) ([GitHub](https://github.com/linq2db/linq2db), [NuGet](https://www.nuget.org/packages/linq2db.MySqlConnector)) including ClickHouse support
 * [NHibernate](https://nhibernate.info/) ([GitHub](https://github.com/nhibernate/NHibernate.MySqlConnector), [NuGet](https://www.nuget.org/packages/NHibernate.Driver.MySqlConnector))
@@ -51,15 +58,18 @@ For Entity Framework support, use:
 
 * Pomelo.EntityFrameworkCore.MySql ([GitHub](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql), [NuGet](https://www.nuget.org/packages/Pomelo.EntityFrameworkCore.MySql))
 
+For ASP.NET Core health checks, use:
+
+* AspNetCore.HealthChecks.MySql ([GitHub](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks), [NuGet](https://www.nuget.org/packages/AspNetCore.HealthChecks.MySql))
+
 ## Build Status
 
-Appveyor | Azure Pipelines | NuGet
---- | --- | ---
-[![AppVeyor](https://img.shields.io/appveyor/ci/mysqlnet/mysqlconnector/master.svg)](https://ci.appveyor.com/project/mysqlnet/mysqlconnector) | [![Azure Pipelines](https://dev.azure.com/mysqlnet/MySqlConnector/_apis/build/status/mysql-net.MySqlConnector?branchName=master)](https://dev.azure.com/mysqlnet/MySqlConnector/_build/latest?definitionId=2&branchName=master) | [![NuGet](https://img.shields.io/nuget/vpre/MySqlConnector.svg)](https://www.nuget.org/packages/MySqlConnector/)
+[![AppVeyor](https://img.shields.io/appveyor/ci/mysqlnet/mysqlconnector/master.svg)](https://ci.appveyor.com/project/mysqlnet/mysqlconnector)
+[![Azure Pipelines](https://dev.azure.com/mysqlnet/MySqlConnector/_apis/build/status/mysql-net.MySqlConnector?branchName=master)](https://dev.azure.com/mysqlnet/MySqlConnector/_build/latest?definitionId=2&branchName=master)
 
 ## Building
 
-Install the latest [.NET Core](https://www.microsoft.com/net/core).
+Install the latest [.NET](https://dotnet.microsoft.com/en-us/download).
 
 To build and run the tests, clone the repo and execute:
 
@@ -97,7 +107,10 @@ If you'd like to contribute to MySqlConnector, please read our [contributing gui
 
 Development of MySqlConnector is supported by:
 
+[![Devolutions](https://webdevolutions.blob.core.windows.net/images/projects/devolutions/logos/devolutions-icon-shadow.svg)](https://devolutions.net/)
+
+[Devolutions](https://devolutions.net/)
+
 [![Faithlife](https://files.logoscdn.com/v1/files/4319104/content.svg?signature=3szVb3XmOfYMAxIv-LmuNYL_290)](https://faithlife.com/about)
 
 [Faithlife](https://faithlife.com/about) ([View jobs](https://faithlife.com/careers))
-
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 000000000..ccc859d04
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,27 @@
+# Security Policy
+
+## Supported Versions
+
+| Version | Supported          |
+| ------- | ------------------ |
+| 2.4.0   | :white_check_mark: |
+| 2.3.7   | :white_check_mark: |
+| 2.2.7   | :x:                |
+| 2.1.13  | :x:                |
+| 2.0.0   | :x:                |
+| < 2.0   | :x:                |
+
+
+## Reporting a Vulnerability
+
+You can [report a vulnerability through GitHub](https://github.com/mysql-net/MySqlConnector/security).
+
+Your report will be acknowledged within 5 days and you will receive a more detailed response outlining the next steps in handling your submission.
+
+Please include the information listed below to help better understand the nature and scope of the possible issue:
+
+  * Type of issue (e.g. SQL injection, data leakage, etc.)
+  * Full paths of the files related to the issue
+  * Step by step instructions to reproduce the issue
+  * Proof of concept or exploit code (if possible)
+  * Impact of the issue
diff --git a/appveyor.yml b/appveyor.yml
index d5850814c..ef905dcaa 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -1,6 +1,4 @@
 image: Visual Studio 2022
-services:
-- mysql
 environment:
   global:
     DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
@@ -9,36 +7,28 @@ cache:
   - '%USERPROFILE%\.nuget\packages -> **\*.csproj'
 install:
   - ps: Invoke-WebRequest -Uri "https://dot.net/v1/dotnet-install.ps1" -OutFile "install-dotnet.ps1"
-  - ps: .\install-dotnet.ps1 -Channel 3.1 -InstallDir "dotnetcli"
-  - ps: .\install-dotnet.ps1 -Channel 5.0 -InstallDir "dotnetcli"
-  - ps: .\install-dotnet.ps1 -Channel 7.0 -InstallDir "dotnetcli"
+  - ps: .\install-dotnet.ps1 -Channel 6.0 -InstallDir "dotnetcli"
+  - ps: .\install-dotnet.ps1 -Channel 8.0 -InstallDir "dotnetcli"
+  - ps: .\install-dotnet.ps1 -Channel 9.0 -InstallDir "dotnetcli"
 build_script:
 - dotnet --info
 before_test:
+- ps: Start-Service MySQL80
 - cmd: |-
-    "C:\Program Files\MySQL\MySQL Server 5.7\bin\mysql.exe" -u root --password=Password12! < .ci\server\init.sql
-    "C:\Program Files\MySQL\MySQL Server 5.7\bin\mysql.exe" -u root --password=Password12! < .ci\server\init_sha256.sql
+    "C:\Program Files\MySQL\MySQL Server 8.0\bin\mysql.exe" -u root --password=Password12! -e "SET GLOBAL log_bin_trust_function_creators = 1;"
+    "C:\Program Files\MySQL\MySQL Server 8.0\bin\mysql.exe" -u root --password=Password12! -e "SET GLOBAL local_infile = 1;"
+    "C:\Program Files\MySQL\MySQL Server 8.0\bin\mysql.exe" -u root --password=Password12! -e "SET GLOBAL max_connections = 250;"
+    "C:\Program Files\MySQL\MySQL Server 8.0\bin\mysql.exe" -u root --password=Password12! < .ci\server\init.sql
+    "C:\Program Files\MySQL\MySQL Server 8.0\bin\mysql.exe" -u root --password=Password12! < .ci\server\init_sha256.sql
+    "C:\Program Files\MySQL\MySQL Server 8.0\bin\mysql.exe" -u root --password=Password12! < .ci\server\init_caching_sha2.sql
 test_script:
 - ps: .\.ci\test.ps1
 after_test:
 - cmd: |-
-    dotnet pack src\MySqlConnector\MySqlConnector.csproj -c Release
-    dotnet pack src\MySqlConnector.Authentication.Ed25519\MySqlConnector.Authentication.Ed25519.csproj -c Release
-    dotnet pack src\MySqlConnector.Logging.log4net\MySqlConnector.Logging.log4net.csproj -c Release
-    dotnet pack src\MySqlConnector.Logging.Microsoft.Extensions.Logging\MySqlConnector.Logging.Microsoft.Extensions.Logging.csproj -c Release
-    dotnet pack src\MySqlConnector.Logging.NLog\MySqlConnector.Logging.NLog.csproj -c Release
-    dotnet pack src\MySqlConnector.Logging.Serilog\MySqlConnector.Logging.Serilog.csproj -c Release
+    dotnet pack -c Release
 notifications:
 - provider: Slack
   incoming_webhook:
     secure: SRJ5fYQE4tvelyqeX3Lkv0gXta3O2pl4/+wNaqmqmcFkmYBe+U31T5YCGhipBOVhHBIZLevihOJZ78sFVJrmAFV5bHgtX/VPIKEXN+7ytis=
 artifacts:
-- path: src\*\bin\Release\*.nupkg
-deploy:
-- provider: NuGet
-  api_key:
-    secure: SCNhz02UXcSKv6UVgAlhbzGb0u7PAQF4w1kx+cCGw1tm95QNhy2yiKLsLjjQ0j7l
-  artifact: /.*MySqlConnector[0-9.]+(-(alpha|beta|rc)\.?[0-9.]+)?nupkg/
-  skip_symbols: true
-  on:
-    appveyor_repo_tag: true
+- path: artifacts\package\release\*.nupkg
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index b80fcb6ca..e579d4de3 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -1,37 +1,26 @@
 variables:
-  DotNetCoreSdkVersion: '7.x'
+  DotNetCoreSdkVersion: '9.x'
   NUGET_PACKAGES: '$(Pipeline.Workspace)/.nuget/packages'
 
 jobs:
 - job: linux_build
   displayName: 'Linux Build'
   pool:
-    vmimage: 'ubuntu-20.04'
+    vmimage: 'ubuntu-24.04'
   steps:
   - template: '.ci/build-steps.yml'
 
 - job: windows_build
   displayName: 'Windows Build'
   pool:
-    vmimage: 'windows-latest'
+    vmimage: 'windows-2022'
   steps:
   - template: '.ci/build-steps.yml'
-  - task: DotNetCoreCLI@2
-    displayName: 'Publish IntegrationTests (net472)'
-    inputs:
-      command: 'publish'
-      arguments: '-c Release -f net472 tests/IntegrationTests/IntegrationTests.csproj'
-      publishWebProjects: false
-      zipAfterPublish: false
-  - task: PublishPipelineArtifact@0
-    inputs:
-      artifactName: 'IntegrationTests-net472-$(Agent.OS)'
-      targetPath: 'tests/IntegrationTests/bin/Release/net472/publish'
 
 - job: windows_mysql_data
   displayName: 'MySql.Data Tests'
   pool:
-    vmimage: 'windows-2019'
+    vmimage: 'windows-2022'
   steps:
   - template: '.ci/install-mysql-windows.yml'
   - task: CopyFiles@2
@@ -41,13 +30,14 @@ jobs:
       contents: 'config.json'
       targetFolder: 'tests/IntegrationTests'
   - task: UseDotNet@2
-    displayName: 'Install .NET Core 6.0'
+    displayName: 'Install .NET 8.0'
     inputs:
-      version: '6.0.x'
+      version: '8.0.x'
   - task: UseDotNet@2
-    displayName: 'Install .NET Core'
+    displayName: 'Install .NET'
     inputs:
       version: $(DotNetCoreSdkVersion)
+      includePreviewVersions: true
   - task: DotNetCoreCLI@2
     displayName: 'MySql.Data unit tests'
     inputs:
@@ -61,7 +51,7 @@ jobs:
       arguments: 'tests\IntegrationTests\IntegrationTests.csproj -c MySqlData'
       testRunTitle: 'MySql.Data integration tests'
     env:
-      DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,StreamingResults,UnixDomainSocket'
+      DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,ParsecAuthentication,StreamingResults,TlsFingerprintValidation,UnixDomainSocket,Vector'
       DATA__CONNECTIONSTRING: 'server=localhost;port=3306;user id=root;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600'
       DATA__CERTIFICATESPATH: '$(Build.Repository.LocalPath)\.ci\server\certs\'
       DATA__MYSQLBULKLOADERLOCALCSVFILE: '$(Build.Repository.LocalPath)\tests\TestData\LoadData_UTF8_BOM_Unix.CSV'
@@ -79,7 +69,7 @@ jobs:
   dependsOn: linux_build
   displayName: 'Linux Unit Tests'
   pool:
-    vmimage: 'ubuntu-20.04'
+    vmimage: 'ubuntu-24.04'
   steps:
   - template: '.ci/mysqlconnector-tests-steps.yml'
 
@@ -87,14 +77,9 @@ jobs:
   dependsOn: windows_build
   displayName: 'Windows Tests'
   pool:
-    vmimage: 'windows-2019'
+    vmimage: 'windows-2022'
   steps:
   - template: '.ci/install-mysql-windows.yml'
-  - task: UseDotNet@2
-    displayName: 'Install .NET Core 3.1'
-    inputs:
-      version: 3.1.x
-      packageType: runtime
   - template: '.ci/mysqlconnector-tests-steps.yml'
   - template: '.ci/conformance-test-steps.yml'
     parameters:
@@ -104,13 +89,14 @@ jobs:
 - job: windows_integration_tests_1
   displayName: 'Windows Integration Tests (Part 1)'
   pool:
-    vmimage: 'windows-2019'
+    vmimage: 'windows-2022'
   steps:
   - template: '.ci/install-mysql-windows.yml'
   - task: UseDotNet@2
-    displayName: 'Install .NET Core'
+    displayName: 'Install .NET'
     inputs:
       version: $(DotNetCoreSdkVersion)
+      includePreviewVersions: true
   - task: CopyFiles@2
     displayName: 'Copy config.json'
     inputs:
@@ -121,95 +107,95 @@ jobs:
     displayName: 'Remove target frameworks'
     inputs:
       targetType: 'inline'
-      script: '((Get-Content .\tests\IntegrationTests\IntegrationTests.csproj -Raw) -replace(''<TargetFrameworks>.*</TargetFrameworks>'', ''<TargetFrameworks>net472;net7.0</TargetFrameworks>'')) | Set-Content .\tests\IntegrationTests\IntegrationTests.csproj'
+      script: '((Get-Content .\tests\IntegrationTests\IntegrationTests.csproj -Raw) -replace(''<TargetFrameworks>.*</TargetFrameworks>'', ''<TargetFrameworks>net481;net9.0</TargetFrameworks>'')) | Set-Content .\tests\IntegrationTests\IntegrationTests.csproj'
   - task: DotNetCoreCLI@2
     displayName: 'Restore packages'
     inputs:
       command: 'restore'
   - task: DotNetCoreCLI@2
-    displayName: 'Integration tests (net472/net7.0)'
+    displayName: 'Integration tests (net481/net9.0)'
     inputs:
       command: 'test'
       projects: 'tests/IntegrationTests/IntegrationTests.csproj'
-      arguments: '-c Release --no-restore'
-      testRunTitle: ${{ format('{0}, $(Agent.OS), {1}, {2}', 'mysql:8.0', 'net472/net7.0', 'No SSL') }}
+      arguments: '-c Release --no-restore -p:TestTfmsInParallel=false'
+      testRunTitle: ${{ format('{0}, $(Agent.OS), {1}, {2}', 'mysql:8.0', 'net481/net9.0', 'No SSL') }}
     env:
-      DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,StreamingResults,Tls11,UnixDomainSocket'
+      DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,ParsecAuthentication,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,Vector'
       DATA__CONNECTIONSTRING: 'server=localhost;port=3306;user id=mysqltest;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600;AllowPublicKeyRetrieval=True;UseCompression=True'
 
 - job: windows_integration_tests_2
   displayName: 'Windows Integration Tests (Part 2)'
   pool:
-    vmimage: 'windows-2019'
+    vmimage: 'windows-2022'
   steps:
   - template: '.ci/install-mysql-windows.yml'
   - task: UseDotNet@2
-    displayName: 'Install .NET Core 3.1'
-    inputs:
-      version: 3.1.x
-      packageType: runtime
-  - task: UseDotNet@2
-    displayName: 'Install .NET Core 6.0'
+    displayName: 'Install .NET 8.0'
     inputs:
-      version: 6.0.x
+      version: 8.0.x
       packageType: runtime
   - task: UseDotNet@2
-    displayName: 'Install .NET Core'
+    displayName: 'Install .NET'
     inputs:
       version: $(DotNetCoreSdkVersion)
+      includePreviewVersions: true
   - script: copy .ci\config\config.json tests\IntegrationTests\config.json
     displayName: 'Copy config.json'
   - task: PowerShell@2
     displayName: 'Remove target frameworks'
     inputs:
       targetType: 'inline'
-      script: '((Get-Content .\tests\IntegrationTests\IntegrationTests.csproj -Raw) -replace(''<TargetFrameworks>.*</TargetFrameworks>'', ''<TargetFrameworks>netcoreapp3.1;net6.0</TargetFrameworks>'')) | Set-Content .\tests\IntegrationTests\IntegrationTests.csproj'
+      script: '((Get-Content .\tests\IntegrationTests\IntegrationTests.csproj -Raw) -replace(''<TargetFrameworks>.*</TargetFrameworks>'', ''<TargetFrameworks>net8.0</TargetFrameworks>'')) | Set-Content .\tests\IntegrationTests\IntegrationTests.csproj'
   - task: DotNetCoreCLI@2
     displayName: 'Restore packages'
     inputs:
       command: 'restore'
   - task: DotNetCoreCLI@2
-    displayName: 'Integration tests (netcoreapp3.1/net6.0)'
+    displayName: 'Integration tests (net8.0)'
     inputs:
       command: 'test'
       projects: 'tests/IntegrationTests/IntegrationTests.csproj'
-      arguments: '-c Release --no-restore'
-      testRunTitle: ${{ format('{0}, $(Agent.OS), {1}, {2}', 'mysql:8.0', 'netcoreapp3.1/net6.0', 'No SSL') }}
+      arguments: '-c Release --no-restore -p:TestTfmsInParallel=false'
+      testRunTitle: ${{ format('{0}, $(Agent.OS), {1}, {2}', 'mysql:8.0', 'net8.0', 'No SSL') }}
     env:
-      DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,StreamingResults,Tls11,UnixDomainSocket'
+      DATA__UNSUPPORTEDFEATURES: 'Ed25519,QueryAttributes,ParsecAuthentication,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,UnixDomainSocket,Vector'
       DATA__CONNECTIONSTRING: 'server=localhost;port=3306;user id=mysqltest;password=test;database=mysqltest;ssl mode=none;DefaultCommandTimeout=3600;AllowPublicKeyRetrieval=True'
 
 - job: linux_integration_tests
   dependsOn: linux_build
   displayName: 'Linux Tests'
   pool:
-    vmimage: 'ubuntu-20.04'
+    vmimage: 'ubuntu-24.04'
   strategy:
     matrix:
-      'MySQL 5.6':
-        image: 'mysql:5.6'
-        connectionStringExtra: ''
-        unsupportedFeatures: 'CachingSha2Password,Ed25519,Json,LargePackets,QueryAttributes,ResetConnection,SessionTrack,Sha256Password,Tls11,Tls13,UuidToBin'
-      'MySQL 5.7':
-        image: 'mysql:5.7'
-        connectionStringExtra: 'Tls Cipher Suites=TLS_RSA_WITH_AES_128_CBC_SHA256,TLS_DHE_RSA_WITH_AES_128_CBC_SHA256'
-        unsupportedFeatures: 'CachingSha2Password,Ed25519,QueryAttributes,Tls11,Tls13,UuidToBin'
       'MySQL 8.0':
         image: 'mysql:8.0'
-        connectionStringExtra: ''
-        unsupportedFeatures: 'Ed25519,StreamingResults,Tls11,ZeroDateTime'
-      'MariaDB 10.5':
-        image: 'mariadb:10.5'
-        connectionStringExtra: ''
-        unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin'
+        connectionStringExtra: 'AllowPublicKeyRetrieval=True'
+        unsupportedFeatures: 'Ed25519,ParsecAuthentication,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,Vector,ZeroDateTime'
+      'MySQL 8.4':
+        image: 'mysql:8.4'
+        connectionStringExtra: 'AllowPublicKeyRetrieval=True'
+        unsupportedFeatures: 'Ed25519,ParsecAuthentication,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,Vector,ZeroDateTime'
+      'MySQL 9.4':
+        image: 'mysql:9.4'
+        connectionStringExtra: 'AllowPublicKeyRetrieval=True'
+        unsupportedFeatures: 'Ed25519,ParsecAuthentication,Redirection,StreamingResults,Tls11,TlsFingerprintValidation,Vector,ZeroDateTime'
       'MariaDB 10.6':
         image: 'mariadb:10.6'
         connectionStringExtra: ''
-        unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin'
-      'MariaDB 10.10':
-        image: 'mariadb:10.10'
+        unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,ParsecAuthentication,Redirection,Sha256Password,Tls11,TlsFingerprintValidation,UuidToBin,Vector'
+      'MariaDB 10.11':
+        image: 'mariadb:10.11'
+        connectionStringExtra: ''
+        unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,ParsecAuthentication,Redirection,Sha256Password,Tls11,TlsFingerprintValidation,UuidToBin,Vector'
+      'MariaDB 11.4':
+        image: 'mariadb:11.4'
+        connectionStringExtra: ''
+        unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,ParsecAuthentication,Redirection,Sha256Password,Tls11,UuidToBin,Vector'
+      'MariaDB 11.8':
+        image: 'mariadb:11.8'
         connectionStringExtra: ''
-        unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Sha256Password,Tls11,UuidToBin'
+        unsupportedFeatures: 'CachingSha2Password,CancelSleepSuccessfully,Json,RoundDateTime,QueryAttributes,Redirection,Sha256Password,Tls11,UuidToBin,VectorType'
   steps:
   - template: '.ci/integration-tests-steps.yml'
     parameters:
diff --git a/contrib/NewRelic/README.md b/contrib/NewRelic/README.md
deleted file mode 100644
index 2d9dececa..000000000
--- a/contrib/NewRelic/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# New Relic Agent
-
-MySqlConnector support is available in the New Relic Agent [version 8.32.0.0](https://docs.newrelic.com/docs/release-notes/agent-release-notes/net-release-notes/net-agent-83200) or later.
diff --git a/docs/.gitignore b/docs/.gitignore
index c75eeccc8..644896509 100644
--- a/docs/.gitignore
+++ b/docs/.gitignore
@@ -1 +1,2 @@
 /public
+.hugo_build.lock
diff --git a/docs/assets/css/site.css b/docs/assets/css/site.css
new file mode 100644
index 000000000..b9b8d87dc
--- /dev/null
+++ b/docs/assets/css/site.css
@@ -0,0 +1,3185 @@
+@charset "UTF-8";
+/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
+/* Document
+   ========================================================================== */
+/**
+ * 1. Correct the line height in all browsers.
+ * 2. Prevent adjustments of font size after orientation changes in iOS.
+ */
+html {
+  line-height: 1.15; /* 1 */
+  -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/* Sections
+   ========================================================================== */
+/**
+ * Remove the margin in all browsers.
+ */
+body {
+  margin: 0;
+}
+
+/**
+ * Render the `main` element consistently in IE.
+ */
+main {
+  display: block;
+}
+
+/**
+ * Correct the font size and margin on `h1` elements within `section` and
+ * `article` contexts in Chrome, Firefox, and Safari.
+ */
+h1 {
+  font-size: 2em;
+  margin: 0.67em 0;
+}
+
+/* Grouping content
+   ========================================================================== */
+/**
+ * 1. Add the correct box sizing in Firefox.
+ * 2. Show the overflow in Edge and IE.
+ */
+hr {
+  box-sizing: content-box; /* 1 */
+  height: 0; /* 1 */
+  overflow: visible; /* 2 */
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+pre {
+  font-family: monospace, monospace; /* 1 */
+  font-size: 1em; /* 2 */
+  overflow-x: auto;
+}
+
+/* Text-level semantics
+   ========================================================================== */
+/**
+ * Remove the gray background on active links in IE 10.
+ */
+a {
+  background-color: transparent;
+}
+
+/**
+ * 1. Remove the bottom border in Chrome 57-
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+ */
+abbr[title] {
+  border-bottom: none; /* 1 */
+  text-decoration: underline; /* 2 */
+  text-decoration: underline dotted; /* 2 */
+}
+
+/**
+ * Add the correct font weight in Chrome, Edge, and Safari.
+ */
+b,
+strong {
+  font-weight: bolder;
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+code,
+kbd,
+samp {
+  font-family: monospace, monospace; /* 1 */
+  font-size: 1em; /* 2 */
+}
+
+/**
+ * Add the correct font size in all browsers.
+ */
+small {
+  font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` elements from affecting the line height in
+ * all browsers.
+ */
+sub,
+sup {
+  font-size: 75%;
+  line-height: 0;
+  position: relative;
+  vertical-align: baseline;
+}
+
+sub {
+  bottom: -0.25em;
+}
+
+sup {
+  top: -0.5em;
+}
+
+/* Embedded content
+   ========================================================================== */
+/**
+ * Remove the border on images inside links in IE 10.
+ */
+img {
+  border-style: none;
+}
+
+/* Forms
+   ========================================================================== */
+/**
+ * 1. Change the font styles in all browsers.
+ * 2. Remove the margin in Firefox and Safari.
+ */
+button,
+input,
+optgroup,
+select,
+textarea {
+  font-family: inherit; /* 1 */
+  font-size: 100%; /* 1 */
+  line-height: 1.15; /* 1 */
+  margin: 0; /* 2 */
+}
+
+/**
+ * Show the overflow in IE.
+ * 1. Show the overflow in Edge.
+ */
+button,
+input {
+  /* 1 */
+  overflow: visible;
+}
+
+/**
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.
+ * 1. Remove the inheritance of text transform in Firefox.
+ */
+button,
+select {
+  /* 1 */
+  text-transform: none;
+}
+
+/**
+ * Correct the inability to style clickable types in iOS and Safari.
+ */
+button,
+[type=button],
+[type=reset],
+[type=submit] {
+  -webkit-appearance: button;
+}
+
+/**
+ * Remove the inner border and padding in Firefox.
+ */
+button::-moz-focus-inner,
+[type=button]::-moz-focus-inner,
+[type=reset]::-moz-focus-inner,
+[type=submit]::-moz-focus-inner {
+  border-style: none;
+  padding: 0;
+}
+
+/**
+ * Restore the focus styles unset by the previous rule.
+ */
+button:-moz-focusring,
+[type=button]:-moz-focusring,
+[type=reset]:-moz-focusring,
+[type=submit]:-moz-focusring {
+  outline: 1px dotted ButtonText;
+}
+
+/**
+ * Remove the default vertical scrollbar in IE 10+.
+ */
+textarea {
+  overflow: auto;
+}
+
+/**
+ * 1. Add the correct box sizing in IE 10.
+ * 2. Remove the padding in IE 10.
+ */
+[type=checkbox],
+[type=radio] {
+  box-sizing: border-box; /* 1 */
+  padding: 0; /* 2 */
+}
+
+/**
+ * Correct the cursor style of increment and decrement buttons in Chrome.
+ */
+[type=number]::-webkit-inner-spin-button,
+[type=number]::-webkit-outer-spin-button {
+  height: auto;
+}
+
+/**
+ * 1. Correct the odd appearance in Chrome and Safari.
+ * 2. Correct the outline style in Safari.
+ */
+[type=search] {
+  -webkit-appearance: textfield; /* 1 */
+  outline-offset: -2px; /* 2 */
+}
+
+/**
+ * Remove the inner padding in Chrome and Safari on macOS.
+ */
+[type=search]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+
+/**
+ * 1. Correct the inability to style clickable types in iOS and Safari.
+ * 2. Change font properties to `inherit` in Safari.
+ */
+::-webkit-file-upload-button {
+  -webkit-appearance: button; /* 1 */
+  font: inherit; /* 2 */
+}
+
+/* Misc
+   ========================================================================== */
+/**
+ * Add the correct display in IE 10+.
+ */
+template {
+  display: none;
+}
+
+/**
+ * Add the correct display in IE 10.
+ */
+[hidden] {
+  display: none;
+}
+
+* {
+  box-sizing: border-box;
+}
+
+::selection {
+  color: #fff;
+  background: #0969da;
+}
+
+html {
+  font-size: 14px !important;
+  scroll-behavior: smooth;
+}
+@media (min-width: 31.25rem) {
+  html {
+    font-size: 16px !important;
+  }
+}
+
+body {
+  font-family: system-ui, -apple-system, blinkmacsystemfont, "Segoe UI", roboto, "Helvetica Neue", arial, sans-serif;
+  font-size: inherit;
+  line-height: 1.4;
+  color: #5c5962;
+  background-color: #fff;
+  overflow-wrap: break-word;
+}
+
+ol,
+ul,
+dl,
+pre,
+address,
+blockquote,
+table,
+div,
+hr,
+form,
+fieldset,
+noscript .table-wrapper {
+  margin-top: 0;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+#toctitle {
+  margin-top: 0;
+  margin-bottom: 1em;
+  font-weight: 500;
+  line-height: 1.25;
+  color: #27262b;
+}
+
+p {
+  margin-top: 1em;
+  margin-bottom: 1em;
+}
+
+a {
+  color: #0969da;
+  text-decoration: none;
+}
+
+a:not([class]) {
+  text-decoration: underline;
+  text-decoration-color: #eeebee;
+  text-underline-offset: 2px;
+}
+a:not([class]):hover {
+  text-decoration-color: rgba(9, 105, 218, 0.45);
+}
+
+code {
+  font-family: Inter,InterVariable,Verdana,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";
+  font-size: 0.9em;
+  line-height: 1.4;
+}
+
+figure,
+pre {
+  margin: 16px 0;
+}
+
+li {
+  margin: 0.25em 0;
+}
+
+img {
+  max-width: 100%;
+  height: auto;
+}
+
+hr {
+  height: 1px;
+  padding: 0;
+  margin: 2rem 0;
+  background-color: #eeebee;
+  border: 0;
+}
+
+blockquote {
+  margin: 10px 0;
+  margin-block-start: 0;
+  margin-inline-start: 0;
+  padding-left: 15px;
+  border-left: 3px solid #eeebee;
+}
+
+.side-bar {
+  z-index: 0;
+  display: flex;
+  flex-wrap: wrap;
+  background-color: #f5f6fa;
+}
+@media (min-width: 50rem) {
+  .side-bar {
+    flex-flow: column nowrap;
+    position: fixed;
+    width: 248px;
+    height: 100%;
+    border-right: 1px solid #eeebee;
+    align-items: flex-end;
+  }
+}
+@media (min-width: 66.5rem) {
+  .side-bar {
+    width: calc((100% - 1064px) / 2 + 264px);
+    min-width: 264px;
+  }
+}
+
+@media (min-width: 50rem) {
+  .main {
+    position: relative;
+    max-width: 800px;
+    margin-left: 248px;
+  }
+}
+@media (min-width: 66.5rem) {
+  .main {
+    margin-left: calc(
+      (100% - 1064px) / 2 + 264px
+    );
+  }
+}
+
+.main-content-wrap {
+  padding-right: 1rem;
+  padding-left: 1rem;
+  padding-top: 1rem;
+  padding-bottom: 1rem;
+}
+@media (min-width: 50rem) {
+  .main-content-wrap {
+    padding-right: 2rem;
+    padding-left: 2rem;
+  }
+}
+@media (min-width: 50rem) {
+  .main-content-wrap {
+    padding-top: 2rem;
+    padding-bottom: 2rem;
+  }
+}
+
+.main-header {
+  z-index: 0;
+  display: none;
+  background-color: #f5f6fa;
+}
+@media (min-width: 50rem) {
+  .main-header {
+    display: flex;
+    justify-content: space-between;
+    height: 60px;
+    background-color: #fff;
+    border-bottom: 1px solid #eeebee;
+  }
+}
+.main-header.nav-open {
+  display: block;
+}
+@media (min-width: 50rem) {
+  .main-header.nav-open {
+    display: flex;
+  }
+}
+
+.site-nav,
+.site-header,
+.site-footer {
+  width: 100%;
+}
+@media (min-width: 66.5rem) {
+  .site-nav,
+  .site-header,
+  .site-footer {
+    width: 264px;
+  }
+}
+
+.site-nav {
+  display: none;
+}
+.site-nav.nav-open {
+  display: block;
+}
+@media (min-width: 50rem) {
+  .site-nav {
+    display: block;
+    padding-top: 3rem;
+    padding-bottom: 1rem;
+    overflow-y: auto;
+    flex: 1 1 auto;
+  }
+}
+
+.site-header {
+  display: flex;
+  min-height: 60px;
+  align-items: center;
+}
+@media (min-width: 50rem) {
+  .site-header {
+    height: 60px;
+    max-height: 60px;
+    border-bottom: 1px solid #eeebee;
+  }
+}
+
+.site-title {
+  padding-right: 1rem;
+  padding-left: 1rem;
+  flex-grow: 1;
+  display: flex;
+  height: 100%;
+  align-items: center;
+  padding-top: 0.75rem;
+  padding-bottom: 0.75rem;
+  color: #27262b;
+  font-size: 18px !important;
+}
+@media (min-width: 50rem) {
+  .site-title {
+    padding-right: 2rem;
+    padding-left: 2rem;
+  }
+}
+@media (min-width: 31.25rem) {
+  .site-title {
+    font-size: 24px !important;
+    line-height: 1.25;
+  }
+}
+@media (min-width: 50rem) {
+  .site-title {
+    padding-top: 0.5rem;
+    padding-bottom: 0.5rem;
+  }
+}
+
+.site-button {
+  display: flex;
+  height: 100%;
+  padding: 1rem;
+  align-items: center;
+}
+
+@media (min-width: 50rem) {
+  .site-header .site-button {
+    display: none;
+  }
+}
+.site-title:hover {
+  background-image: linear-gradient(-90deg, #ebedf5 0%, rgba(235, 237, 245, 0.8) 80%, rgba(235, 237, 245, 0) 100%);
+}
+
+.site-button:hover {
+  background-image: linear-gradient(-90deg, #ebedf5 0%, rgba(235, 237, 245, 0.8) 100%);
+}
+
+body {
+  position: relative;
+  padding-bottom: 4rem;
+  overflow-y: scroll;
+}
+@media (min-width: 50rem) {
+  body {
+    position: static;
+    padding-bottom: 0;
+  }
+}
+
+.icon {
+  width: 1.5rem;
+  height: 1.5rem;
+  color: #0969da;
+}
+
+.main-content {
+  line-height: 1.6;
+}
+.main-content ol,
+.main-content ul,
+.main-content dl,
+.main-content pre,
+.main-content address,
+.main-content blockquote,
+.main-content .table-wrapper {
+  margin-top: 0.5em;
+}
+.main-content a {
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.main-content ul,
+.main-content ol {
+  padding-left: 1.5em;
+}
+.main-content li .highlight {
+  margin-top: 0.25rem;
+}
+.main-content ol {
+  list-style-type: none;
+  counter-reset: step-counter;
+}
+.main-content ol > li {
+  position: relative;
+}
+.main-content ol > li::before {
+  position: absolute;
+  top: 0.2em;
+  left: -1.6em;
+  color: #959396;
+  content: counter(step-counter);
+  counter-increment: step-counter;
+  font-size: 12px !important;
+}
+@media (min-width: 31.25rem) {
+  .main-content ol > li::before {
+    font-size: 14px !important;
+  }
+}
+@media (min-width: 31.25rem) {
+  .main-content ol > li::before {
+    top: 0.11em;
+  }
+}
+.main-content ol > li ol {
+  counter-reset: sub-counter;
+}
+.main-content ol > li ol > li::before {
+  content: counter(sub-counter, lower-alpha);
+  counter-increment: sub-counter;
+}
+.main-content ul {
+  list-style: none;
+}
+.main-content ul > li::before {
+  position: absolute;
+  margin-left: -1.4em;
+  color: #959396;
+  content: "•";
+}
+.main-content .task-list-item::before {
+  content: "";
+}
+.main-content .task-list-item-checkbox {
+  margin-right: 0.6em;
+  margin-left: -1.4em;
+}
+.main-content hr + * {
+  margin-top: 0;
+}
+.main-content h1:first-of-type {
+  margin-top: 0.5em;
+}
+.main-content dl {
+  display: grid;
+  grid-template: auto/10em 1fr;
+}
+.main-content dt,
+.main-content dd {
+  margin: 0.25em 0;
+}
+.main-content dt {
+  grid-column: 1;
+  font-weight: 500;
+  text-align: right;
+}
+.main-content dt::after {
+  content: ":";
+}
+.main-content dd {
+  grid-column: 2;
+  margin-bottom: 0;
+  margin-left: 1em;
+}
+.main-content dd blockquote:first-child,
+.main-content dd div:first-child,
+.main-content dd dl:first-child,
+.main-content dd dt:first-child,
+.main-content dd h1:first-child,
+.main-content dd h2:first-child,
+.main-content dd h3:first-child,
+.main-content dd h4:first-child,
+.main-content dd h5:first-child,
+.main-content dd h6:first-child,
+.main-content dd li:first-child,
+.main-content dd ol:first-child,
+.main-content dd p:first-child,
+.main-content dd pre:first-child,
+.main-content dd table:first-child,
+.main-content dd ul:first-child,
+.main-content dd .table-wrapper:first-child {
+  margin-top: 0;
+}
+.main-content dd dl:first-child dt:first-child,
+.main-content dd dl:first-child dd:nth-child(2),
+.main-content ol dl:first-child dt:first-child,
+.main-content ol dl:first-child dd:nth-child(2),
+.main-content ul dl:first-child dt:first-child,
+.main-content ul dl:first-child dd:nth-child(2) {
+  margin-top: 0;
+}
+.main-content .anchor-heading {
+  position: absolute;
+  right: -1rem;
+  width: 1.5rem;
+  height: 100%;
+  padding-right: 0.25rem;
+  padding-left: 0.25rem;
+  overflow: visible;
+}
+@media (min-width: 50rem) {
+  .main-content .anchor-heading {
+    right: auto;
+    left: -1.5rem;
+  }
+}
+.main-content .anchor-heading svg {
+  display: inline-block;
+  width: 100%;
+  height: 100%;
+  color: #0969da;
+  visibility: hidden;
+}
+.main-content .anchor-heading:hover svg,
+.main-content .anchor-heading:focus svg,
+.main-content h1:hover > .anchor-heading svg,
+.main-content h2:hover > .anchor-heading svg,
+.main-content h3:hover > .anchor-heading svg,
+.main-content h4:hover > .anchor-heading svg,
+.main-content h5:hover > .anchor-heading svg,
+.main-content h6:hover > .anchor-heading svg {
+  visibility: visible;
+}
+.main-content summary {
+  cursor: pointer;
+}
+.main-content h1,
+.main-content h2,
+.main-content h3,
+.main-content h4,
+.main-content h5,
+.main-content h6,
+.main-content #toctitle {
+  position: relative;
+  margin-top: 1.5em;
+  margin-bottom: 0.25em;
+}
+.main-content h1 + table,
+.main-content h1 + .table-wrapper,
+.main-content h1 + .code-example,
+.main-content h1 + .highlighter-rouge,
+.main-content h1 + .sectionbody .listingblock,
+.main-content h2 + table,
+.main-content h2 + .table-wrapper,
+.main-content h2 + .code-example,
+.main-content h2 + .highlighter-rouge,
+.main-content h2 + .sectionbody .listingblock,
+.main-content h3 + table,
+.main-content h3 + .table-wrapper,
+.main-content h3 + .code-example,
+.main-content h3 + .highlighter-rouge,
+.main-content h3 + .sectionbody .listingblock,
+.main-content h4 + table,
+.main-content h4 + .table-wrapper,
+.main-content h4 + .code-example,
+.main-content h4 + .highlighter-rouge,
+.main-content h4 + .sectionbody .listingblock,
+.main-content h5 + table,
+.main-content h5 + .table-wrapper,
+.main-content h5 + .code-example,
+.main-content h5 + .highlighter-rouge,
+.main-content h5 + .sectionbody .listingblock,
+.main-content h6 + table,
+.main-content h6 + .table-wrapper,
+.main-content h6 + .code-example,
+.main-content h6 + .highlighter-rouge,
+.main-content h6 + .sectionbody .listingblock,
+.main-content #toctitle + table,
+.main-content #toctitle + .table-wrapper,
+.main-content #toctitle + .code-example,
+.main-content #toctitle + .highlighter-rouge,
+.main-content #toctitle + .sectionbody .listingblock {
+  margin-top: 1em;
+}
+.main-content h1 + p:not(.label),
+.main-content h2 + p:not(.label),
+.main-content h3 + p:not(.label),
+.main-content h4 + p:not(.label),
+.main-content h5 + p:not(.label),
+.main-content h6 + p:not(.label),
+.main-content #toctitle + p:not(.label) {
+  margin-top: 0;
+}
+.main-content > h1:first-child,
+.main-content > h2:first-child,
+.main-content > h3:first-child,
+.main-content > h4:first-child,
+.main-content > h5:first-child,
+.main-content > h6:first-child,
+.main-content > .sect1:first-child > h2,
+.main-content > .sect2:first-child > h3,
+.main-content > .sect3:first-child > h4,
+.main-content > .sect4:first-child > h5,
+.main-content > .sect5:first-child > h6 {
+  margin-top: 0.5rem;
+}
+
+.nav-list {
+  padding: 0;
+  margin-top: 0;
+  margin-bottom: 0;
+  list-style: none;
+}
+.nav-list .nav-list-item {
+  font-size: 14px !important;
+  position: relative;
+  margin: 0;
+}
+@media (min-width: 31.25rem) {
+  .nav-list .nav-list-item {
+    font-size: 16px !important;
+  }
+}
+@media (min-width: 50rem) {
+  .nav-list .nav-list-item {
+    font-size: 12px !important;
+  }
+}
+@media (min-width: 50rem) and (min-width: 31.25rem) {
+  .nav-list .nav-list-item {
+    font-size: 14px !important;
+  }
+}
+.nav-list .nav-list-item .nav-list-link {
+  display: block;
+  min-height: 3rem;
+  padding-top: 0.25rem;
+  padding-bottom: 0.25rem;
+  line-height: 2.5rem;
+  padding-right: 3rem;
+  padding-left: 1rem;
+}
+@media (min-width: 50rem) {
+  .nav-list .nav-list-item .nav-list-link {
+    min-height: 2rem;
+    line-height: 1.5rem;
+    padding-right: 2rem;
+    padding-left: 2rem;
+  }
+}
+.nav-list .nav-list-item .nav-list-link.external > svg {
+  width: 1rem;
+  height: 1rem;
+  vertical-align: text-bottom;
+}
+.nav-list .nav-list-item .nav-list-link.active {
+  font-weight: 600;
+  text-decoration: none;
+}
+.nav-list .nav-list-item .nav-list-link:hover, .nav-list .nav-list-item .nav-list-link.active {
+  background-image: linear-gradient(-90deg, #ebedf5 0%, rgba(235, 237, 245, 0.8) 80%, rgba(235, 237, 245, 0) 100%);
+}
+.nav-list .nav-list-item .nav-list-expander {
+  position: absolute;
+  right: 0;
+  width: 3rem;
+  height: 3rem;
+  padding: 0.75rem;
+  color: #0969da;
+}
+@media (min-width: 50rem) {
+  .nav-list .nav-list-item .nav-list-expander {
+    width: 2rem;
+    height: 2rem;
+    padding: 0.5rem;
+  }
+}
+.nav-list .nav-list-item .nav-list-expander:hover {
+  background-image: linear-gradient(-90deg, #ebedf5 0%, rgba(235, 237, 245, 0.8) 100%);
+}
+.nav-list .nav-list-item .nav-list-expander svg {
+  transform: rotate(90deg);
+}
+.nav-list .nav-list-item > .nav-list {
+  display: none;
+  padding-left: 0.75rem;
+  list-style: none;
+}
+.nav-list .nav-list-item > .nav-list .nav-list-item {
+  position: relative;
+}
+.nav-list .nav-list-item > .nav-list .nav-list-item .nav-list-link {
+  color: #5c5962;
+}
+.nav-list .nav-list-item > .nav-list .nav-list-item .nav-list-expander {
+  color: #5c5962;
+}
+.nav-list .nav-list-item.active > .nav-list-expander svg {
+  transform: rotate(-90deg);
+}
+.nav-list .nav-list-item.active > .nav-list {
+  display: block;
+}
+
+.nav-category {
+  padding: 0.5rem 1rem;
+  font-weight: 600;
+  text-align: start;
+  text-transform: uppercase;
+  border-bottom: 1px solid #eeebee;
+  font-size: 11px !important;
+}
+@media (min-width: 31.25rem) {
+  .nav-category {
+    font-size: 12px !important;
+  }
+}
+@media (min-width: 50rem) {
+  .nav-category {
+    padding: 0.5rem 2rem;
+    margin-top: 1rem;
+    text-align: start;
+  }
+  .nav-category:first-child {
+    margin-top: 0;
+  }
+}
+
+.nav-list.nav-category-list > .nav-list-item {
+  margin: 0;
+}
+.nav-list.nav-category-list > .nav-list-item > .nav-list {
+  padding: 0;
+}
+.nav-list.nav-category-list > .nav-list-item > .nav-list > .nav-list-item > .nav-list-link {
+  color: #0969da;
+}
+.nav-list.nav-category-list > .nav-list-item > .nav-list > .nav-list-item > .nav-list-expander {
+  color: #0969da;
+}
+
+.aux-nav {
+  height: 100%;
+  overflow-x: auto;
+  font-size: 11px !important;
+}
+@media (min-width: 31.25rem) {
+  .aux-nav {
+    font-size: 12px !important;
+  }
+}
+.aux-nav .aux-nav-list {
+  display: flex;
+  height: 100%;
+  padding: 0;
+  margin: 0;
+  list-style: none;
+}
+.aux-nav .aux-nav-list-item {
+  display: inline-block;
+  height: 100%;
+  padding: 0;
+  margin: 0;
+}
+@media (min-width: 50rem) {
+  .aux-nav {
+    padding-right: 1rem;
+  }
+}
+
+@media (min-width: 50rem) {
+  .breadcrumb-nav {
+    margin-top: -1rem;
+  }
+}
+
+.breadcrumb-nav-list {
+  padding-left: 0;
+  margin-bottom: 0.75rem;
+  list-style: none;
+}
+
+.breadcrumb-nav-list-item {
+  display: table-cell;
+  font-size: 11px !important;
+}
+@media (min-width: 31.25rem) {
+  .breadcrumb-nav-list-item {
+    font-size: 12px !important;
+  }
+}
+.breadcrumb-nav-list-item::before {
+  display: none;
+}
+.breadcrumb-nav-list-item::after {
+  display: inline-block;
+  margin-right: 0.5rem;
+  margin-left: 0.5rem;
+  color: #959396;
+  content: "/";
+}
+.breadcrumb-nav-list-item:last-child::after {
+  content: "";
+}
+
+h1,
+.text-alpha {
+  font-size: 32px !important;
+  line-height: 1.25;
+  font-weight: 300;
+}
+@media (min-width: 31.25rem) {
+  h1,
+  .text-alpha {
+    font-size: 36px !important;
+  }
+}
+
+h2,
+.text-beta,
+#toctitle {
+  font-size: 18px !important;
+}
+@media (min-width: 31.25rem) {
+  h2,
+  .text-beta,
+  #toctitle {
+    font-size: 24px !important;
+    line-height: 1.25;
+  }
+}
+
+h3,
+.text-gamma {
+  font-size: 16px !important;
+}
+@media (min-width: 31.25rem) {
+  h3,
+  .text-gamma {
+    font-size: 18px !important;
+  }
+}
+
+h4,
+.text-delta {
+  font-size: 11px !important;
+  font-weight: 400;
+  text-transform: uppercase;
+  letter-spacing: 0.1em;
+}
+@media (min-width: 31.25rem) {
+  h4,
+  .text-delta {
+    font-size: 12px !important;
+  }
+}
+
+h4 code {
+  text-transform: none;
+}
+
+h5,
+.text-epsilon {
+  font-size: 12px !important;
+}
+@media (min-width: 31.25rem) {
+  h5,
+  .text-epsilon {
+    font-size: 14px !important;
+  }
+}
+
+h6,
+.text-zeta {
+  font-size: 11px !important;
+}
+@media (min-width: 31.25rem) {
+  h6,
+  .text-zeta {
+    font-size: 12px !important;
+  }
+}
+
+.text-small {
+  font-size: 11px !important;
+}
+@media (min-width: 31.25rem) {
+  .text-small {
+    font-size: 12px !important;
+  }
+}
+
+.text-mono {
+  font-family: "SFMono-Regular", menlo, consolas, monospace !important;
+}
+
+.text-left {
+  text-align: left !important;
+}
+
+.text-center {
+  text-align: center !important;
+}
+
+.text-right {
+  text-align: right !important;
+}
+
+.label,
+.label-blue {
+  display: inline-block;
+  padding: 0.16em 0.56em;
+  margin-right: 0.5rem;
+  margin-left: 0.5rem;
+  color: #fff;
+  text-transform: uppercase;
+  vertical-align: middle;
+  background-color: #2869e6;
+  font-size: 11px !important;
+  border-radius: 12px;
+}
+@media (min-width: 31.25rem) {
+  .label,
+  .label-blue {
+    font-size: 12px !important;
+  }
+}
+
+.label-green {
+  background-color: #009c7b;
+}
+
+.label-purple {
+  background-color: #5e41d0;
+}
+
+.label-red {
+  background-color: #e94c4c;
+}
+
+.label-yellow {
+  color: #44434d;
+  background-color: #f7d12e;
+}
+
+.table-wrapper {
+  display: block;
+  width: 100%;
+  max-width: 100%;
+  margin-bottom: 1.5rem;
+  overflow-x: auto;
+  border-radius: 4px;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12), 0 3px 10px rgba(0, 0, 0, 0.08);
+}
+
+table {
+  display: table;
+  min-width: 100%;
+  border-collapse: separate;
+}
+
+th,
+td {
+  font-size: 12px !important;
+  min-width: 120px;
+  padding: 0.5rem 0.75rem;
+  background-color: #fff;
+  border-bottom: 1px solid rgba(238, 235, 238, 0.5);
+  border-left: 1px solid #eeebee;
+}
+@media (min-width: 31.25rem) {
+  th,
+  td {
+    font-size: 14px !important;
+  }
+}
+th:first-of-type,
+td:first-of-type {
+  border-left: 0;
+}
+
+tbody tr:last-of-type th,
+tbody tr:last-of-type td {
+  border-bottom: 0;
+}
+tbody tr:last-of-type td {
+  padding-bottom: 0.75rem;
+}
+
+thead th {
+  border-bottom: 1px solid #eeebee;
+}
+
+:not(pre, figure) > code {
+  padding: 0.2em 0.15em;
+  font-weight: 400;
+  background-color: #f5f6fa;
+  border: 1px solid #eeebee;
+  border-radius: 4px;
+}
+
+a:visited code {
+  border-color: #eeebee;
+}
+
+div.highlight,
+div.listingblock > div.content,
+figure.highlight {
+  margin-top: 0;
+  margin-bottom: 0.75rem;
+  background-color: #f5f6fa;
+  border-radius: 4px;
+  box-shadow: none;
+  -webkit-overflow-scrolling: touch;
+  position: relative;
+  padding: 0;
+}
+div.highlight > button,
+div.listingblock > div.content > button,
+figure.highlight > button {
+  width: 0.75rem;
+  opacity: 0;
+  position: absolute;
+  top: 0;
+  right: 0;
+  border: 0.75rem solid #f5f6fa;
+  background-color: #f5f6fa;
+  color: #5c5962;
+  box-sizing: content-box;
+}
+div.highlight > button svg,
+div.listingblock > div.content > button svg,
+figure.highlight > button svg {
+  fill: #5c5962;
+}
+div.highlight > button:active,
+div.listingblock > div.content > button:active,
+figure.highlight > button:active {
+  text-decoration: none;
+  outline: none;
+  opacity: 1;
+}
+div.highlight > button:focus,
+div.listingblock > div.content > button:focus,
+figure.highlight > button:focus {
+  opacity: 1;
+}
+div.highlight:hover > button,
+div.listingblock > div.content:hover > button,
+figure.highlight:hover > button {
+  cursor: copy;
+  opacity: 1;
+}
+
+div.highlight div.highlight,
+div.listingblock div.highlight {
+  overflow-x: auto;
+  padding: 0.75rem;
+  margin: 0;
+  border: 0;
+}
+div.highlight pre.highlight,
+div.highlight code,
+div.listingblock pre.highlight,
+div.listingblock code {
+  padding: 0;
+  margin: 0;
+  border: 0;
+}
+
+figure.highlight pre,
+figure.highlight :not(pre) > code {
+  overflow-x: auto;
+  padding: 0.75rem;
+  margin: 0;
+  border: 0;
+}
+
+.highlight .table-wrapper {
+  padding: 0.75rem 0;
+  margin: 0;
+  border: 0;
+  box-shadow: none;
+}
+.highlight .table-wrapper td,
+.highlight .table-wrapper pre {
+  font-size: 11px !important;
+  min-width: 0;
+  padding: 0;
+  background-color: #f5f6fa;
+  border: 0;
+}
+@media (min-width: 31.25rem) {
+  .highlight .table-wrapper td,
+  .highlight .table-wrapper pre {
+    font-size: 12px !important;
+  }
+}
+.highlight .table-wrapper td.gl {
+  width: 1em;
+  padding-right: 0.75rem;
+  padding-left: 0.75rem;
+}
+.highlight .table-wrapper pre {
+  margin: 0;
+  line-height: 2;
+}
+
+.code-example,
+.listingblock > .title {
+  padding: 0.75rem;
+  margin-bottom: 0.75rem;
+  overflow: auto;
+  border: 1px solid #eeebee;
+  border-radius: 4px;
+}
+.code-example + .highlighter-rouge,
+.code-example + .sectionbody .listingblock,
+.code-example + .content,
+.code-example + figure.highlight,
+.listingblock > .title + .highlighter-rouge,
+.listingblock > .title + .sectionbody .listingblock,
+.listingblock > .title + .content,
+.listingblock > .title + figure.highlight {
+  position: relative;
+  margin-top: -1rem;
+  border-right: 1px solid #eeebee;
+  border-bottom: 1px solid #eeebee;
+  border-left: 1px solid #eeebee;
+  border-top-left-radius: 0;
+  border-top-right-radius: 0;
+}
+
+code.language-mermaid {
+  padding: 0;
+  background-color: inherit;
+  border: 0;
+}
+
+.text-grey-dk-000 {
+  color: #959396 !important;
+}
+
+.text-grey-dk-100 {
+  color: #5c5962 !important;
+}
+
+.text-grey-dk-200 {
+  color: #44434d !important;
+}
+
+.text-grey-dk-250 {
+  color: #302d36 !important;
+}
+
+.text-grey-dk-300 {
+  color: #27262b !important;
+}
+
+.text-grey-lt-000 {
+  color: #f5f6fa !important;
+}
+
+.text-grey-lt-100 {
+  color: #eeebee !important;
+}
+
+.text-grey-lt-200 {
+  color: #ecebed !important;
+}
+
+.text-grey-lt-300 {
+  color: #e6e1e8 !important;
+}
+
+.text-blue-000 {
+  color: #2c84fa !important;
+}
+
+.text-blue-100 {
+  color: #2869e6 !important;
+}
+
+.text-blue-200 {
+  color: #264caf !important;
+}
+
+.text-blue-300 {
+  color: #183385 !important;
+}
+
+.text-green-000 {
+  color: #41d693 !important;
+}
+
+.text-green-100 {
+  color: #11b584 !important;
+}
+
+.text-green-200 {
+  color: #009c7b !important;
+}
+
+.text-green-300 {
+  color: #026e57 !important;
+}
+
+.text-purple-000 {
+  color: #7253ed !important;
+}
+
+.text-purple-100 {
+  color: #5e41d0 !important;
+}
+
+.text-purple-200 {
+  color: #4e26af !important;
+}
+
+.text-purple-300 {
+  color: #381885 !important;
+}
+
+.text-yellow-000 {
+  color: #ffeb82 !important;
+}
+
+.text-yellow-100 {
+  color: #fadf50 !important;
+}
+
+.text-yellow-200 {
+  color: #f7d12e !important;
+}
+
+.text-yellow-300 {
+  color: #e7af06 !important;
+}
+
+.text-red-000 {
+  color: #f77e7e !important;
+}
+
+.text-red-100 {
+  color: #f96e65 !important;
+}
+
+.text-red-200 {
+  color: #e94c4c !important;
+}
+
+.text-red-300 {
+  color: #dd2e2e !important;
+}
+
+.bg-grey-dk-000 {
+  background-color: #959396 !important;
+}
+
+.bg-grey-dk-100 {
+  background-color: #5c5962 !important;
+}
+
+.bg-grey-dk-200 {
+  background-color: #44434d !important;
+}
+
+.bg-grey-dk-250 {
+  background-color: #302d36 !important;
+}
+
+.bg-grey-dk-300 {
+  background-color: #27262b !important;
+}
+
+.bg-grey-lt-000 {
+  background-color: #f5f6fa !important;
+}
+
+.bg-grey-lt-100 {
+  background-color: #eeebee !important;
+}
+
+.bg-grey-lt-200 {
+  background-color: #ecebed !important;
+}
+
+.bg-grey-lt-300 {
+  background-color: #e6e1e8 !important;
+}
+
+.bg-blue-000 {
+  background-color: #2c84fa !important;
+}
+
+.bg-blue-100 {
+  background-color: #2869e6 !important;
+}
+
+.bg-blue-200 {
+  background-color: #264caf !important;
+}
+
+.bg-blue-300 {
+  background-color: #183385 !important;
+}
+
+.bg-green-000 {
+  background-color: #41d693 !important;
+}
+
+.bg-green-100 {
+  background-color: #11b584 !important;
+}
+
+.bg-green-200 {
+  background-color: #009c7b !important;
+}
+
+.bg-green-300 {
+  background-color: #026e57 !important;
+}
+
+.bg-purple-000 {
+  background-color: #7253ed !important;
+}
+
+.bg-purple-100 {
+  background-color: #5e41d0 !important;
+}
+
+.bg-purple-200 {
+  background-color: #4e26af !important;
+}
+
+.bg-purple-300 {
+  background-color: #381885 !important;
+}
+
+.bg-yellow-000 {
+  background-color: #ffeb82 !important;
+}
+
+.bg-yellow-100 {
+  background-color: #fadf50 !important;
+}
+
+.bg-yellow-200 {
+  background-color: #f7d12e !important;
+}
+
+.bg-yellow-300 {
+  background-color: #e7af06 !important;
+}
+
+.bg-red-000 {
+  background-color: #f77e7e !important;
+}
+
+.bg-red-100 {
+  background-color: #f96e65 !important;
+}
+
+.bg-red-200 {
+  background-color: #e94c4c !important;
+}
+
+.bg-red-300 {
+  background-color: #dd2e2e !important;
+}
+
+.d-block {
+  display: block !important;
+}
+
+.d-flex {
+  display: flex !important;
+}
+
+.d-inline {
+  display: inline !important;
+}
+
+.d-inline-block {
+  display: inline-block !important;
+}
+
+.d-none {
+  display: none !important;
+}
+
+@media (min-width: 20rem) {
+  .d-xs-block {
+    display: block !important;
+  }
+  .d-xs-flex {
+    display: flex !important;
+  }
+  .d-xs-inline {
+    display: inline !important;
+  }
+  .d-xs-inline-block {
+    display: inline-block !important;
+  }
+  .d-xs-none {
+    display: none !important;
+  }
+}
+@media (min-width: 20rem) {
+  .d-xs-block {
+    display: block !important;
+  }
+  .d-xs-flex {
+    display: flex !important;
+  }
+  .d-xs-inline {
+    display: inline !important;
+  }
+  .d-xs-inline-block {
+    display: inline-block !important;
+  }
+  .d-xs-none {
+    display: none !important;
+  }
+}
+@media (min-width: 20rem) {
+  .d-xs-block {
+    display: block !important;
+  }
+  .d-xs-flex {
+    display: flex !important;
+  }
+  .d-xs-inline {
+    display: inline !important;
+  }
+  .d-xs-inline-block {
+    display: inline-block !important;
+  }
+  .d-xs-none {
+    display: none !important;
+  }
+}
+@media (min-width: 20rem) {
+  .d-xs-block {
+    display: block !important;
+  }
+  .d-xs-flex {
+    display: flex !important;
+  }
+  .d-xs-inline {
+    display: inline !important;
+  }
+  .d-xs-inline-block {
+    display: inline-block !important;
+  }
+  .d-xs-none {
+    display: none !important;
+  }
+}
+@media (min-width: 20rem) {
+  .d-xs-block {
+    display: block !important;
+  }
+  .d-xs-flex {
+    display: flex !important;
+  }
+  .d-xs-inline {
+    display: inline !important;
+  }
+  .d-xs-inline-block {
+    display: inline-block !important;
+  }
+  .d-xs-none {
+    display: none !important;
+  }
+}
+@media (min-width: 20rem) {
+  .d-xs-block {
+    display: block !important;
+  }
+  .d-xs-flex {
+    display: flex !important;
+  }
+  .d-xs-inline {
+    display: inline !important;
+  }
+  .d-xs-inline-block {
+    display: inline-block !important;
+  }
+  .d-xs-none {
+    display: none !important;
+  }
+}
+@media (min-width: 20rem) {
+  .d-xs-block {
+    display: block !important;
+  }
+  .d-xs-flex {
+    display: flex !important;
+  }
+  .d-xs-inline {
+    display: inline !important;
+  }
+  .d-xs-inline-block {
+    display: inline-block !important;
+  }
+  .d-xs-none {
+    display: none !important;
+  }
+}
+@media (min-width: 20rem) {
+  .d-xs-block {
+    display: block !important;
+  }
+  .d-xs-flex {
+    display: flex !important;
+  }
+  .d-xs-inline {
+    display: inline !important;
+  }
+  .d-xs-inline-block {
+    display: inline-block !important;
+  }
+  .d-xs-none {
+    display: none !important;
+  }
+}
+@media (min-width: 20rem) {
+  .d-xs-block {
+    display: block !important;
+  }
+  .d-xs-flex {
+    display: flex !important;
+  }
+  .d-xs-inline {
+    display: inline !important;
+  }
+  .d-xs-inline-block {
+    display: inline-block !important;
+  }
+  .d-xs-none {
+    display: none !important;
+  }
+}
+@media (min-width: 20rem) {
+  .d-xs-block {
+    display: block !important;
+  }
+  .d-xs-flex {
+    display: flex !important;
+  }
+  .d-xs-inline {
+    display: inline !important;
+  }
+  .d-xs-inline-block {
+    display: inline-block !important;
+  }
+  .d-xs-none {
+    display: none !important;
+  }
+}
+@media (min-width: 20rem) {
+  .d-xs-block {
+    display: block !important;
+  }
+  .d-xs-flex {
+    display: flex !important;
+  }
+  .d-xs-inline {
+    display: inline !important;
+  }
+  .d-xs-inline-block {
+    display: inline-block !important;
+  }
+  .d-xs-none {
+    display: none !important;
+  }
+}
+@media (min-width: 31.25rem) {
+  .d-sm-block {
+    display: block !important;
+  }
+  .d-sm-flex {
+    display: flex !important;
+  }
+  .d-sm-inline {
+    display: inline !important;
+  }
+  .d-sm-inline-block {
+    display: inline-block !important;
+  }
+  .d-sm-none {
+    display: none !important;
+  }
+}
+@media (min-width: 31.25rem) {
+  .d-sm-block {
+    display: block !important;
+  }
+  .d-sm-flex {
+    display: flex !important;
+  }
+  .d-sm-inline {
+    display: inline !important;
+  }
+  .d-sm-inline-block {
+    display: inline-block !important;
+  }
+  .d-sm-none {
+    display: none !important;
+  }
+}
+@media (min-width: 31.25rem) {
+  .d-sm-block {
+    display: block !important;
+  }
+  .d-sm-flex {
+    display: flex !important;
+  }
+  .d-sm-inline {
+    display: inline !important;
+  }
+  .d-sm-inline-block {
+    display: inline-block !important;
+  }
+  .d-sm-none {
+    display: none !important;
+  }
+}
+@media (min-width: 31.25rem) {
+  .d-sm-block {
+    display: block !important;
+  }
+  .d-sm-flex {
+    display: flex !important;
+  }
+  .d-sm-inline {
+    display: inline !important;
+  }
+  .d-sm-inline-block {
+    display: inline-block !important;
+  }
+  .d-sm-none {
+    display: none !important;
+  }
+}
+@media (min-width: 31.25rem) {
+  .d-sm-block {
+    display: block !important;
+  }
+  .d-sm-flex {
+    display: flex !important;
+  }
+  .d-sm-inline {
+    display: inline !important;
+  }
+  .d-sm-inline-block {
+    display: inline-block !important;
+  }
+  .d-sm-none {
+    display: none !important;
+  }
+}
+@media (min-width: 31.25rem) {
+  .d-sm-block {
+    display: block !important;
+  }
+  .d-sm-flex {
+    display: flex !important;
+  }
+  .d-sm-inline {
+    display: inline !important;
+  }
+  .d-sm-inline-block {
+    display: inline-block !important;
+  }
+  .d-sm-none {
+    display: none !important;
+  }
+}
+@media (min-width: 31.25rem) {
+  .d-sm-block {
+    display: block !important;
+  }
+  .d-sm-flex {
+    display: flex !important;
+  }
+  .d-sm-inline {
+    display: inline !important;
+  }
+  .d-sm-inline-block {
+    display: inline-block !important;
+  }
+  .d-sm-none {
+    display: none !important;
+  }
+}
+@media (min-width: 31.25rem) {
+  .d-sm-block {
+    display: block !important;
+  }
+  .d-sm-flex {
+    display: flex !important;
+  }
+  .d-sm-inline {
+    display: inline !important;
+  }
+  .d-sm-inline-block {
+    display: inline-block !important;
+  }
+  .d-sm-none {
+    display: none !important;
+  }
+}
+@media (min-width: 31.25rem) {
+  .d-sm-block {
+    display: block !important;
+  }
+  .d-sm-flex {
+    display: flex !important;
+  }
+  .d-sm-inline {
+    display: inline !important;
+  }
+  .d-sm-inline-block {
+    display: inline-block !important;
+  }
+  .d-sm-none {
+    display: none !important;
+  }
+}
+@media (min-width: 31.25rem) {
+  .d-sm-block {
+    display: block !important;
+  }
+  .d-sm-flex {
+    display: flex !important;
+  }
+  .d-sm-inline {
+    display: inline !important;
+  }
+  .d-sm-inline-block {
+    display: inline-block !important;
+  }
+  .d-sm-none {
+    display: none !important;
+  }
+}
+@media (min-width: 31.25rem) {
+  .d-sm-block {
+    display: block !important;
+  }
+  .d-sm-flex {
+    display: flex !important;
+  }
+  .d-sm-inline {
+    display: inline !important;
+  }
+  .d-sm-inline-block {
+    display: inline-block !important;
+  }
+  .d-sm-none {
+    display: none !important;
+  }
+}
+@media (min-width: 50rem) {
+  .d-md-block {
+    display: block !important;
+  }
+  .d-md-flex {
+    display: flex !important;
+  }
+  .d-md-inline {
+    display: inline !important;
+  }
+  .d-md-inline-block {
+    display: inline-block !important;
+  }
+  .d-md-none {
+    display: none !important;
+  }
+}
+@media (min-width: 50rem) {
+  .d-md-block {
+    display: block !important;
+  }
+  .d-md-flex {
+    display: flex !important;
+  }
+  .d-md-inline {
+    display: inline !important;
+  }
+  .d-md-inline-block {
+    display: inline-block !important;
+  }
+  .d-md-none {
+    display: none !important;
+  }
+}
+@media (min-width: 50rem) {
+  .d-md-block {
+    display: block !important;
+  }
+  .d-md-flex {
+    display: flex !important;
+  }
+  .d-md-inline {
+    display: inline !important;
+  }
+  .d-md-inline-block {
+    display: inline-block !important;
+  }
+  .d-md-none {
+    display: none !important;
+  }
+}
+@media (min-width: 50rem) {
+  .d-md-block {
+    display: block !important;
+  }
+  .d-md-flex {
+    display: flex !important;
+  }
+  .d-md-inline {
+    display: inline !important;
+  }
+  .d-md-inline-block {
+    display: inline-block !important;
+  }
+  .d-md-none {
+    display: none !important;
+  }
+}
+@media (min-width: 50rem) {
+  .d-md-block {
+    display: block !important;
+  }
+  .d-md-flex {
+    display: flex !important;
+  }
+  .d-md-inline {
+    display: inline !important;
+  }
+  .d-md-inline-block {
+    display: inline-block !important;
+  }
+  .d-md-none {
+    display: none !important;
+  }
+}
+@media (min-width: 50rem) {
+  .d-md-block {
+    display: block !important;
+  }
+  .d-md-flex {
+    display: flex !important;
+  }
+  .d-md-inline {
+    display: inline !important;
+  }
+  .d-md-inline-block {
+    display: inline-block !important;
+  }
+  .d-md-none {
+    display: none !important;
+  }
+}
+@media (min-width: 50rem) {
+  .d-md-block {
+    display: block !important;
+  }
+  .d-md-flex {
+    display: flex !important;
+  }
+  .d-md-inline {
+    display: inline !important;
+  }
+  .d-md-inline-block {
+    display: inline-block !important;
+  }
+  .d-md-none {
+    display: none !important;
+  }
+}
+@media (min-width: 50rem) {
+  .d-md-block {
+    display: block !important;
+  }
+  .d-md-flex {
+    display: flex !important;
+  }
+  .d-md-inline {
+    display: inline !important;
+  }
+  .d-md-inline-block {
+    display: inline-block !important;
+  }
+  .d-md-none {
+    display: none !important;
+  }
+}
+@media (min-width: 50rem) {
+  .d-md-block {
+    display: block !important;
+  }
+  .d-md-flex {
+    display: flex !important;
+  }
+  .d-md-inline {
+    display: inline !important;
+  }
+  .d-md-inline-block {
+    display: inline-block !important;
+  }
+  .d-md-none {
+    display: none !important;
+  }
+}
+@media (min-width: 50rem) {
+  .d-md-block {
+    display: block !important;
+  }
+  .d-md-flex {
+    display: flex !important;
+  }
+  .d-md-inline {
+    display: inline !important;
+  }
+  .d-md-inline-block {
+    display: inline-block !important;
+  }
+  .d-md-none {
+    display: none !important;
+  }
+}
+@media (min-width: 50rem) {
+  .d-md-block {
+    display: block !important;
+  }
+  .d-md-flex {
+    display: flex !important;
+  }
+  .d-md-inline {
+    display: inline !important;
+  }
+  .d-md-inline-block {
+    display: inline-block !important;
+  }
+  .d-md-none {
+    display: none !important;
+  }
+}
+@media (min-width: 66.5rem) {
+  .d-lg-block {
+    display: block !important;
+  }
+  .d-lg-flex {
+    display: flex !important;
+  }
+  .d-lg-inline {
+    display: inline !important;
+  }
+  .d-lg-inline-block {
+    display: inline-block !important;
+  }
+  .d-lg-none {
+    display: none !important;
+  }
+}
+@media (min-width: 66.5rem) {
+  .d-lg-block {
+    display: block !important;
+  }
+  .d-lg-flex {
+    display: flex !important;
+  }
+  .d-lg-inline {
+    display: inline !important;
+  }
+  .d-lg-inline-block {
+    display: inline-block !important;
+  }
+  .d-lg-none {
+    display: none !important;
+  }
+}
+@media (min-width: 66.5rem) {
+  .d-lg-block {
+    display: block !important;
+  }
+  .d-lg-flex {
+    display: flex !important;
+  }
+  .d-lg-inline {
+    display: inline !important;
+  }
+  .d-lg-inline-block {
+    display: inline-block !important;
+  }
+  .d-lg-none {
+    display: none !important;
+  }
+}
+@media (min-width: 66.5rem) {
+  .d-lg-block {
+    display: block !important;
+  }
+  .d-lg-flex {
+    display: flex !important;
+  }
+  .d-lg-inline {
+    display: inline !important;
+  }
+  .d-lg-inline-block {
+    display: inline-block !important;
+  }
+  .d-lg-none {
+    display: none !important;
+  }
+}
+@media (min-width: 66.5rem) {
+  .d-lg-block {
+    display: block !important;
+  }
+  .d-lg-flex {
+    display: flex !important;
+  }
+  .d-lg-inline {
+    display: inline !important;
+  }
+  .d-lg-inline-block {
+    display: inline-block !important;
+  }
+  .d-lg-none {
+    display: none !important;
+  }
+}
+@media (min-width: 66.5rem) {
+  .d-lg-block {
+    display: block !important;
+  }
+  .d-lg-flex {
+    display: flex !important;
+  }
+  .d-lg-inline {
+    display: inline !important;
+  }
+  .d-lg-inline-block {
+    display: inline-block !important;
+  }
+  .d-lg-none {
+    display: none !important;
+  }
+}
+@media (min-width: 66.5rem) {
+  .d-lg-block {
+    display: block !important;
+  }
+  .d-lg-flex {
+    display: flex !important;
+  }
+  .d-lg-inline {
+    display: inline !important;
+  }
+  .d-lg-inline-block {
+    display: inline-block !important;
+  }
+  .d-lg-none {
+    display: none !important;
+  }
+}
+@media (min-width: 66.5rem) {
+  .d-lg-block {
+    display: block !important;
+  }
+  .d-lg-flex {
+    display: flex !important;
+  }
+  .d-lg-inline {
+    display: inline !important;
+  }
+  .d-lg-inline-block {
+    display: inline-block !important;
+  }
+  .d-lg-none {
+    display: none !important;
+  }
+}
+@media (min-width: 66.5rem) {
+  .d-lg-block {
+    display: block !important;
+  }
+  .d-lg-flex {
+    display: flex !important;
+  }
+  .d-lg-inline {
+    display: inline !important;
+  }
+  .d-lg-inline-block {
+    display: inline-block !important;
+  }
+  .d-lg-none {
+    display: none !important;
+  }
+}
+@media (min-width: 66.5rem) {
+  .d-lg-block {
+    display: block !important;
+  }
+  .d-lg-flex {
+    display: flex !important;
+  }
+  .d-lg-inline {
+    display: inline !important;
+  }
+  .d-lg-inline-block {
+    display: inline-block !important;
+  }
+  .d-lg-none {
+    display: none !important;
+  }
+}
+@media (min-width: 66.5rem) {
+  .d-lg-block {
+    display: block !important;
+  }
+  .d-lg-flex {
+    display: flex !important;
+  }
+  .d-lg-inline {
+    display: inline !important;
+  }
+  .d-lg-inline-block {
+    display: inline-block !important;
+  }
+  .d-lg-none {
+    display: none !important;
+  }
+}
+@media (min-width: 87.5rem) {
+  .d-xl-block {
+    display: block !important;
+  }
+  .d-xl-flex {
+    display: flex !important;
+  }
+  .d-xl-inline {
+    display: inline !important;
+  }
+  .d-xl-inline-block {
+    display: inline-block !important;
+  }
+  .d-xl-none {
+    display: none !important;
+  }
+}
+@media (min-width: 87.5rem) {
+  .d-xl-block {
+    display: block !important;
+  }
+  .d-xl-flex {
+    display: flex !important;
+  }
+  .d-xl-inline {
+    display: inline !important;
+  }
+  .d-xl-inline-block {
+    display: inline-block !important;
+  }
+  .d-xl-none {
+    display: none !important;
+  }
+}
+@media (min-width: 87.5rem) {
+  .d-xl-block {
+    display: block !important;
+  }
+  .d-xl-flex {
+    display: flex !important;
+  }
+  .d-xl-inline {
+    display: inline !important;
+  }
+  .d-xl-inline-block {
+    display: inline-block !important;
+  }
+  .d-xl-none {
+    display: none !important;
+  }
+}
+@media (min-width: 87.5rem) {
+  .d-xl-block {
+    display: block !important;
+  }
+  .d-xl-flex {
+    display: flex !important;
+  }
+  .d-xl-inline {
+    display: inline !important;
+  }
+  .d-xl-inline-block {
+    display: inline-block !important;
+  }
+  .d-xl-none {
+    display: none !important;
+  }
+}
+@media (min-width: 87.5rem) {
+  .d-xl-block {
+    display: block !important;
+  }
+  .d-xl-flex {
+    display: flex !important;
+  }
+  .d-xl-inline {
+    display: inline !important;
+  }
+  .d-xl-inline-block {
+    display: inline-block !important;
+  }
+  .d-xl-none {
+    display: none !important;
+  }
+}
+@media (min-width: 87.5rem) {
+  .d-xl-block {
+    display: block !important;
+  }
+  .d-xl-flex {
+    display: flex !important;
+  }
+  .d-xl-inline {
+    display: inline !important;
+  }
+  .d-xl-inline-block {
+    display: inline-block !important;
+  }
+  .d-xl-none {
+    display: none !important;
+  }
+}
+@media (min-width: 87.5rem) {
+  .d-xl-block {
+    display: block !important;
+  }
+  .d-xl-flex {
+    display: flex !important;
+  }
+  .d-xl-inline {
+    display: inline !important;
+  }
+  .d-xl-inline-block {
+    display: inline-block !important;
+  }
+  .d-xl-none {
+    display: none !important;
+  }
+}
+@media (min-width: 87.5rem) {
+  .d-xl-block {
+    display: block !important;
+  }
+  .d-xl-flex {
+    display: flex !important;
+  }
+  .d-xl-inline {
+    display: inline !important;
+  }
+  .d-xl-inline-block {
+    display: inline-block !important;
+  }
+  .d-xl-none {
+    display: none !important;
+  }
+}
+@media (min-width: 87.5rem) {
+  .d-xl-block {
+    display: block !important;
+  }
+  .d-xl-flex {
+    display: flex !important;
+  }
+  .d-xl-inline {
+    display: inline !important;
+  }
+  .d-xl-inline-block {
+    display: inline-block !important;
+  }
+  .d-xl-none {
+    display: none !important;
+  }
+}
+@media (min-width: 87.5rem) {
+  .d-xl-block {
+    display: block !important;
+  }
+  .d-xl-flex {
+    display: flex !important;
+  }
+  .d-xl-inline {
+    display: inline !important;
+  }
+  .d-xl-inline-block {
+    display: inline-block !important;
+  }
+  .d-xl-none {
+    display: none !important;
+  }
+}
+@media (min-width: 87.5rem) {
+  .d-xl-block {
+    display: block !important;
+  }
+  .d-xl-flex {
+    display: flex !important;
+  }
+  .d-xl-inline {
+    display: inline !important;
+  }
+  .d-xl-inline-block {
+    display: inline-block !important;
+  }
+  .d-xl-none {
+    display: none !important;
+  }
+}
+.float-left {
+  float: left !important;
+}
+
+.float-right {
+  float: right !important;
+}
+
+.flex-justify-start {
+  justify-content: flex-start !important;
+}
+
+.flex-justify-end {
+  justify-content: flex-end !important;
+}
+
+.flex-justify-between {
+  justify-content: space-between !important;
+}
+
+.flex-justify-around {
+  justify-content: space-around !important;
+}
+
+.v-align-baseline {
+  vertical-align: baseline !important;
+}
+
+.v-align-bottom {
+  vertical-align: bottom !important;
+}
+
+.v-align-middle {
+  vertical-align: middle !important;
+}
+
+.v-align-text-bottom {
+  vertical-align: text-bottom !important;
+}
+
+.v-align-text-top {
+  vertical-align: text-top !important;
+}
+
+.v-align-top {
+  vertical-align: top !important;
+}
+
+.fs-1 {
+  font-size: 9px !important;
+}
+@media (min-width: 31.25rem) {
+  .fs-1 {
+    font-size: 10px !important;
+  }
+}
+
+.fs-2 {
+  font-size: 11px !important;
+}
+@media (min-width: 31.25rem) {
+  .fs-2 {
+    font-size: 12px !important;
+  }
+}
+
+.fs-3 {
+  font-size: 12px !important;
+}
+@media (min-width: 31.25rem) {
+  .fs-3 {
+    font-size: 14px !important;
+  }
+}
+
+.fs-4 {
+  font-size: 14px !important;
+}
+@media (min-width: 31.25rem) {
+  .fs-4 {
+    font-size: 16px !important;
+  }
+}
+
+.fs-5 {
+  font-size: 16px !important;
+}
+@media (min-width: 31.25rem) {
+  .fs-5 {
+    font-size: 18px !important;
+  }
+}
+
+.fs-6 {
+  font-size: 18px !important;
+}
+@media (min-width: 31.25rem) {
+  .fs-6 {
+    font-size: 24px !important;
+    line-height: 1.25;
+  }
+}
+
+.fs-7 {
+  font-size: 24px !important;
+  line-height: 1.25;
+}
+@media (min-width: 31.25rem) {
+  .fs-7 {
+    font-size: 32px !important;
+  }
+}
+
+.fs-8 {
+  font-size: 32px !important;
+  line-height: 1.25;
+}
+@media (min-width: 31.25rem) {
+  .fs-8 {
+    font-size: 36px !important;
+  }
+}
+
+.fs-9 {
+  font-size: 36px !important;
+  line-height: 1.25;
+}
+@media (min-width: 31.25rem) {
+  .fs-9 {
+    font-size: 42px !important;
+  }
+}
+
+.fs-10 {
+  font-size: 42px !important;
+  line-height: 1.25;
+}
+@media (min-width: 31.25rem) {
+  .fs-10 {
+    font-size: 48px !important;
+  }
+}
+
+.fw-300 {
+  font-weight: 300 !important;
+}
+
+.fw-400 {
+  font-weight: 400 !important;
+}
+
+.fw-500 {
+  font-weight: 500 !important;
+}
+
+.fw-700 {
+  font-weight: 700 !important;
+}
+
+.lh-0 {
+  line-height: 0 !important;
+}
+
+.lh-default {
+  line-height: 1.4;
+}
+
+.lh-tight {
+  line-height: 1.25;
+}
+
+.ls-5 {
+  letter-spacing: 0.05em !important;
+}
+
+.ls-10 {
+  letter-spacing: 0.1em !important;
+}
+
+.ls-0 {
+  letter-spacing: 0 !important;
+}
+
+.text-uppercase {
+  text-transform: uppercase !important;
+}
+
+.list-style-none {
+  padding: 0 !important;
+  margin: 0 !important;
+  list-style: none !important;
+}
+.list-style-none li::before {
+  display: none !important;
+}
+
+@media print {
+  .site-footer,
+  .site-button,
+  #edit-this-page,
+  #back-to-top,
+  .site-nav,
+  .main-header {
+    display: none !important;
+  }
+  .side-bar {
+    width: 100%;
+    height: auto;
+    border-right: 0 !important;
+  }
+  .site-header {
+    border-bottom: 1px solid #eeebee;
+  }
+  .site-title {
+    font-size: 16px !important;
+    font-weight: 700 !important;
+  }
+  .text-small {
+    font-size: 8pt !important;
+  }
+  pre.highlight {
+    border: 1px solid #eeebee;
+  }
+  .main {
+    max-width: none;
+    margin-left: 0;
+  }
+}
+a.skip-to-main {
+  left: -999px;
+  position: absolute;
+  top: auto;
+  width: 1px;
+  height: 1px;
+  overflow: hidden;
+  z-index: -999;
+}
+
+a.skip-to-main:focus,
+a.skip-to-main:active {
+  color: #0969da;
+  background-color: #fff;
+  left: auto;
+  top: auto;
+  width: 30%;
+  height: auto;
+  overflow: auto;
+  margin: 10px 35%;
+  padding: 5px;
+  border-radius: 15px;
+  border: 4px solid #5e41d0;
+  text-align: center;
+  font-size: 1.2em;
+  z-index: 999;
+}
+
+div.opaque {
+  background-color: #fff;
+}
+
+p.highlight, blockquote.highlight {
+  background: rgba(255, 235, 130, 0.2);
+  border-left: 4px solid #e7af06;
+  border-radius: 4px;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12), 0 3px 10px rgba(0, 0, 0, 0.08);
+  padding: 0.8rem;
+}
+p.highlight > .highlight-title, blockquote.highlight > .highlight-title {
+  color: #e7af06;
+  display: block;
+  font-weight: bold;
+  text-transform: uppercase;
+  font-size: 0.75em;
+  padding-bottom: 0.125rem;
+}
+
+p.highlight-title, blockquote.highlight-title {
+  background: rgba(255, 235, 130, 0.2);
+  border-left: 4px solid #e7af06;
+  border-radius: 4px;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12), 0 3px 10px rgba(0, 0, 0, 0.08);
+  padding: 0.8rem;
+}
+p.highlight-title > p:first-child, blockquote.highlight-title > p:first-child {
+  margin-top: 0;
+  margin-bottom: 0;
+  color: #e7af06;
+  display: block;
+  font-weight: bold;
+  text-transform: uppercase;
+  font-size: 0.75em;
+  padding-bottom: 0.125rem;
+}
+
+blockquote.highlight {
+  margin-left: 0;
+  margin-right: 0;
+}
+blockquote.highlight > p:first-child {
+  margin-top: 0;
+}
+blockquote.highlight > p:last-child {
+  margin-bottom: 0;
+}
+
+blockquote.highlight-title {
+  margin-left: 0;
+  margin-right: 0;
+}
+blockquote.highlight-title > p:nth-child(2) {
+  margin-top: 0;
+}
+blockquote.highlight-title > p:last-child {
+  margin-bottom: 0;
+}
+
+p.important, blockquote.important {
+  background: rgba(44, 132, 250, 0.2);
+  border-left: 4px solid #183385;
+  border-radius: 4px;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12), 0 3px 10px rgba(0, 0, 0, 0.08);
+  padding: 0.8rem;
+}
+p.important::before, blockquote.important::before {
+  color: #183385;
+  content: "Important";
+  display: block;
+  font-weight: bold;
+  text-transform: uppercase;
+  font-size: 0.75em;
+  padding-bottom: 0.125rem;
+}
+p.important > .important-title, blockquote.important > .important-title {
+  color: #183385;
+  display: block;
+  font-weight: bold;
+  text-transform: uppercase;
+  font-size: 0.75em;
+  padding-bottom: 0.125rem;
+}
+
+p.important-title, blockquote.important-title {
+  background: rgba(44, 132, 250, 0.2);
+  border-left: 4px solid #183385;
+  border-radius: 4px;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12), 0 3px 10px rgba(0, 0, 0, 0.08);
+  padding: 0.8rem;
+}
+p.important-title > p:first-child, blockquote.important-title > p:first-child {
+  margin-top: 0;
+  margin-bottom: 0;
+  color: #183385;
+  display: block;
+  font-weight: bold;
+  text-transform: uppercase;
+  font-size: 0.75em;
+  padding-bottom: 0.125rem;
+}
+
+blockquote.important {
+  margin-left: 0;
+  margin-right: 0;
+}
+blockquote.important > p:first-child {
+  margin-top: 0;
+}
+blockquote.important > p:last-child {
+  margin-bottom: 0;
+}
+
+blockquote.important-title {
+  margin-left: 0;
+  margin-right: 0;
+}
+blockquote.important-title > p:nth-child(2) {
+  margin-top: 0;
+}
+blockquote.important-title > p:last-child {
+  margin-bottom: 0;
+}
+
+p.new, blockquote.new {
+  background: rgba(65, 214, 147, 0.2);
+  border-left: 4px solid #026e57;
+  border-radius: 4px;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12), 0 3px 10px rgba(0, 0, 0, 0.08);
+  padding: 0.8rem;
+}
+p.new::before, blockquote.new::before {
+  color: #026e57;
+  content: "New";
+  display: block;
+  font-weight: bold;
+  text-transform: uppercase;
+  font-size: 0.75em;
+  padding-bottom: 0.125rem;
+}
+p.new > .new-title, blockquote.new > .new-title {
+  color: #026e57;
+  display: block;
+  font-weight: bold;
+  text-transform: uppercase;
+  font-size: 0.75em;
+  padding-bottom: 0.125rem;
+}
+
+p.new-title, blockquote.new-title {
+  background: rgba(65, 214, 147, 0.2);
+  border-left: 4px solid #026e57;
+  border-radius: 4px;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12), 0 3px 10px rgba(0, 0, 0, 0.08);
+  padding: 0.8rem;
+}
+p.new-title > p:first-child, blockquote.new-title > p:first-child {
+  margin-top: 0;
+  margin-bottom: 0;
+  color: #026e57;
+  display: block;
+  font-weight: bold;
+  text-transform: uppercase;
+  font-size: 0.75em;
+  padding-bottom: 0.125rem;
+}
+
+blockquote.new {
+  margin-left: 0;
+  margin-right: 0;
+}
+blockquote.new > p:first-child {
+  margin-top: 0;
+}
+blockquote.new > p:last-child {
+  margin-bottom: 0;
+}
+
+blockquote.new-title {
+  margin-left: 0;
+  margin-right: 0;
+}
+blockquote.new-title > p:nth-child(2) {
+  margin-top: 0;
+}
+blockquote.new-title > p:last-child {
+  margin-bottom: 0;
+}
+
+p.note, blockquote.note {
+  background: rgba(114, 83, 237, 0.2);
+  border-left: 4px solid #381885;
+  border-radius: 4px;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12), 0 3px 10px rgba(0, 0, 0, 0.08);
+  padding: 0.8rem;
+}
+p.note::before, blockquote.note::before {
+  color: #381885;
+  content: "Note";
+  display: block;
+  font-weight: bold;
+  text-transform: uppercase;
+  font-size: 0.75em;
+  padding-bottom: 0.125rem;
+}
+p.note > .note-title, blockquote.note > .note-title {
+  color: #381885;
+  display: block;
+  font-weight: bold;
+  text-transform: uppercase;
+  font-size: 0.75em;
+  padding-bottom: 0.125rem;
+}
+
+p.note-title, blockquote.note-title {
+  background: rgba(114, 83, 237, 0.2);
+  border-left: 4px solid #381885;
+  border-radius: 4px;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12), 0 3px 10px rgba(0, 0, 0, 0.08);
+  padding: 0.8rem;
+}
+p.note-title > p:first-child, blockquote.note-title > p:first-child {
+  margin-top: 0;
+  margin-bottom: 0;
+  color: #381885;
+  display: block;
+  font-weight: bold;
+  text-transform: uppercase;
+  font-size: 0.75em;
+  padding-bottom: 0.125rem;
+}
+
+blockquote.note {
+  margin-left: 0;
+  margin-right: 0;
+}
+blockquote.note > p:first-child {
+  margin-top: 0;
+}
+blockquote.note > p:last-child {
+  margin-bottom: 0;
+}
+
+blockquote.note-title {
+  margin-left: 0;
+  margin-right: 0;
+}
+blockquote.note-title > p:nth-child(2) {
+  margin-top: 0;
+}
+blockquote.note-title > p:last-child {
+  margin-bottom: 0;
+}
+
+p.warning, blockquote.warning {
+  background: rgba(247, 126, 126, 0.2);
+  border-left: 4px solid #dd2e2e;
+  border-radius: 4px;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12), 0 3px 10px rgba(0, 0, 0, 0.08);
+  padding: 0.8rem;
+}
+p.warning::before, blockquote.warning::before {
+  color: #dd2e2e;
+  content: "Warning";
+  display: block;
+  font-weight: bold;
+  text-transform: uppercase;
+  font-size: 0.75em;
+  padding-bottom: 0.125rem;
+}
+p.warning > .warning-title, blockquote.warning > .warning-title {
+  color: #dd2e2e;
+  display: block;
+  font-weight: bold;
+  text-transform: uppercase;
+  font-size: 0.75em;
+  padding-bottom: 0.125rem;
+}
+
+p.warning-title, blockquote.warning-title {
+  background: rgba(247, 126, 126, 0.2);
+  border-left: 4px solid #dd2e2e;
+  border-radius: 4px;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12), 0 3px 10px rgba(0, 0, 0, 0.08);
+  padding: 0.8rem;
+}
+p.warning-title > p:first-child, blockquote.warning-title > p:first-child {
+  margin-top: 0;
+  margin-bottom: 0;
+  color: #dd2e2e;
+  display: block;
+  font-weight: bold;
+  text-transform: uppercase;
+  font-size: 0.75em;
+  padding-bottom: 0.125rem;
+}
+
+blockquote.warning {
+  margin-left: 0;
+  margin-right: 0;
+}
+blockquote.warning > p:first-child {
+  margin-top: 0;
+}
+blockquote.warning > p:last-child {
+  margin-bottom: 0;
+}
+
+blockquote.warning-title {
+  margin-left: 0;
+  margin-right: 0;
+}
+blockquote.warning-title > p:nth-child(2) {
+  margin-top: 0;
+}
+blockquote.warning-title > p:last-child {
+  margin-bottom: 0;
+}
+
+/* Background */ .bg { background-color: #f5f6fa; }
+/* PreWrapper */ .chroma { background-color: #f5f6fa; }
+pre.chroma { border: 1px solid #eeebee; }
+/* Other */ .chroma .x {  }
+/* Error */ .chroma .err {  }
+/* CodeLine */ .chroma .cl {  }
+/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit }
+/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
+/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
+/* LineHighlight */ .chroma .hl { background-color: #ffffcc }
+/* LineNumbersTable */ .chroma .lnt { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
+/* LineNumbers */ .chroma .ln { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
+/* Line */ .chroma .line { display: flex; }
+/* Keyword */ .chroma .k { color: #0000ff }
+/* KeywordConstant */ .chroma .kc { color: #0000ff }
+/* KeywordDeclaration */ .chroma .kd { color: #0000ff }
+/* KeywordNamespace */ .chroma .kn { color: #0000ff }
+/* KeywordPseudo */ .chroma .kp { color: #0000ff }
+/* KeywordReserved */ .chroma .kr { color: #0000ff }
+/* KeywordType */ .chroma .kt { color: #2b91af }
+/* Name */ .chroma .n {  }
+/* NameAttribute */ .chroma .na {  }
+/* NameBuiltin */ .chroma .nb {  }
+/* NameBuiltinPseudo */ .chroma .bp {  }
+/* NameClass */ .chroma .nc { color: #2b91af }
+/* NameConstant */ .chroma .no {  }
+/* NameDecorator */ .chroma .nd {  }
+/* NameEntity */ .chroma .ni {  }
+/* NameException */ .chroma .ne {  }
+/* NameFunction */ .chroma .nf {  }
+/* NameFunctionMagic */ .chroma .fm {  }
+/* NameLabel */ .chroma .nl {  }
+/* NameNamespace */ .chroma .nn {  }
+/* NameOther */ .chroma .nx {  }
+/* NameProperty */ .chroma .py {  }
+/* NameTag */ .chroma .nt {  }
+/* NameVariable */ .chroma .nv {  }
+/* NameVariableClass */ .chroma .vc {  }
+/* NameVariableGlobal */ .chroma .vg {  }
+/* NameVariableInstance */ .chroma .vi {  }
+/* NameVariableMagic */ .chroma .vm {  }
+/* Literal */ .chroma .l {  }
+/* LiteralDate */ .chroma .ld {  }
+/* LiteralString */ .chroma .s { color: #a31515 }
+/* LiteralStringAffix */ .chroma .sa { color: #a31515 }
+/* LiteralStringBacktick */ .chroma .sb { color: #a31515 }
+/* LiteralStringChar */ .chroma .sc { color: #a31515 }
+/* LiteralStringDelimiter */ .chroma .dl { color: #a31515 }
+/* LiteralStringDoc */ .chroma .sd { color: #a31515 }
+/* LiteralStringDouble */ .chroma .s2 { color: #a31515 }
+/* LiteralStringEscape */ .chroma .se { color: #a31515 }
+/* LiteralStringHeredoc */ .chroma .sh { color: #a31515 }
+/* LiteralStringInterpol */ .chroma .si { color: #a31515 }
+/* LiteralStringOther */ .chroma .sx { color: #a31515 }
+/* LiteralStringRegex */ .chroma .sr { color: #a31515 }
+/* LiteralStringSingle */ .chroma .s1 { color: #a31515 }
+/* LiteralStringSymbol */ .chroma .ss { color: #a31515 }
+/* LiteralNumber */ .chroma .m {  }
+/* LiteralNumberBin */ .chroma .mb {  }
+/* LiteralNumberFloat */ .chroma .mf {  }
+/* LiteralNumberHex */ .chroma .mh {  }
+/* LiteralNumberInteger */ .chroma .mi {  }
+/* LiteralNumberIntegerLong */ .chroma .il {  }
+/* LiteralNumberOct */ .chroma .mo {  }
+/* Operator */ .chroma .o {  }
+/* OperatorWord */ .chroma .ow { color: #0000ff }
+/* Punctuation */ .chroma .p {  }
+/* Comment */ .chroma .c { color: #008000 }
+/* CommentHashbang */ .chroma .ch { color: #008000 }
+/* CommentMultiline */ .chroma .cm { color: #008000 }
+/* CommentSingle */ .chroma .c1 { color: #008000 }
+/* CommentSpecial */ .chroma .cs { color: #008000 }
+/* CommentPreproc */ .chroma .cp { color: #0000ff }
+/* CommentPreprocFile */ .chroma .cpf { color: #0000ff }
+/* Generic */ .chroma .g {  }
+/* GenericDeleted */ .chroma .gd {  }
+/* GenericEmph */ .chroma .ge { font-style: italic }
+/* GenericError */ .chroma .gr {  }
+/* GenericHeading */ .chroma .gh { font-weight: bold }
+/* GenericInserted */ .chroma .gi {  }
+/* GenericOutput */ .chroma .go {  }
+/* GenericPrompt */ .chroma .gp { font-weight: bold }
+/* GenericStrong */ .chroma .gs { font-weight: bold }
+/* GenericSubheading */ .chroma .gu { font-weight: bold }
+/* GenericTraceback */ .chroma .gt {  }
+/* GenericUnderline */ .chroma .gl {  }
+/* TextWhitespace */ .chroma .w {  }
diff --git a/docs/assets/js/site.js b/docs/assets/js/site.js
new file mode 100644
index 000000000..15f60c152
--- /dev/null
+++ b/docs/assets/js/site.js
@@ -0,0 +1,111 @@
+(function (jtd, undefined) {
+
+// Event handling
+
+jtd.addEvent = function(el, type, handler) {
+  if (el.attachEvent) el.attachEvent('on'+type, handler); else el.addEventListener(type, handler);
+}
+jtd.removeEvent = function(el, type, handler) {
+  if (el.detachEvent) el.detachEvent('on'+type, handler); else el.removeEventListener(type, handler);
+}
+jtd.onReady = function(ready) {
+  // in case the document is already rendered
+  if (document.readyState!='loading') ready();
+  // modern browsers
+  else if (document.addEventListener) document.addEventListener('DOMContentLoaded', ready);
+  // IE <= 8
+  else document.attachEvent('onreadystatechange', function(){
+      if (document.readyState=='complete') ready();
+  });
+}
+
+// Show/hide mobile menu
+
+function initNav() {
+  jtd.addEvent(document, 'click', function(e){
+    var target = e.target;
+    while (target && !(target.classList && target.classList.contains('nav-list-expander'))) {
+      target = target.parentNode;
+    }
+    if (target) {
+      e.preventDefault();
+      target.parentNode.classList.toggle('active');
+    }
+  });
+
+  const siteNav = document.getElementById('site-nav');
+  const mainHeader = document.getElementById('main-header');
+  const menuButton = document.getElementById('menu-button');
+
+  jtd.addEvent(menuButton, 'click', function(e){
+    e.preventDefault();
+
+    if (menuButton.classList.toggle('nav-open')) {
+      siteNav.classList.add('nav-open');
+      mainHeader.classList.add('nav-open');
+    } else {
+      siteNav.classList.remove('nav-open');
+      mainHeader.classList.remove('nav-open');
+    }
+  });
+}
+
+// Scroll site-nav to ensure the link to the current page is visible
+
+function scrollNav() {
+  const href = document.location.pathname;
+  const siteNav = document.getElementById('site-nav');
+  const targetLink = siteNav.querySelector('a[href="' + href + '"], a[href="' + href + '/"]');
+  if(targetLink){
+    const rect = targetLink.getBoundingClientRect();
+    siteNav.scrollBy(0, rect.top - 3*rect.height);
+  }
+}
+
+// Document ready
+
+jtd.onReady(function(){
+  initNav();
+  scrollNav();
+});
+
+// Copy button on code
+
+jtd.onReady(function(){
+
+  var codeBlocks = document.querySelectorAll('div.highlight, div.listingblock, figure.highlight');
+
+  // note: the SVG svg-copied and svg-copy is only loaded as a Jekyll include if site.enable_copy_code_button is true; see _includes/icons/icons.html
+  var svgCopied =  '<svg viewBox="0 0 24 24" class="copy-icon"><use xlink:href="#svg-copied"></use></svg>';
+  var svgCopy =  '<svg viewBox="0 0 24 24" class="copy-icon"><use xlink:href="#svg-copy"></use></svg>';
+
+  codeBlocks.forEach(codeBlock => {
+    var copyButton = document.createElement('button');
+    var timeout = null;
+    copyButton.type = 'button';
+    copyButton.ariaLabel = 'Copy code to clipboard';
+    copyButton.innerHTML = svgCopy;
+    codeBlock.append(copyButton);
+
+    copyButton.addEventListener('click', function () {
+      if(timeout === null) {
+        var code = codeBlock.querySelector('pre:not(.lineno)').innerText;
+        window.navigator.clipboard.writeText(code);
+
+        copyButton.innerHTML = svgCopied;
+
+        var timeoutSetting = 4000;
+
+        timeout = setTimeout(function () {
+          copyButton.innerHTML = svgCopy;
+          timeout = null;
+        }, timeoutSetting);
+      }
+    });
+  });
+
+});
+
+})(window.jtd = window.jtd || {});
+
+
diff --git a/docs/config.toml b/docs/config.toml
index eb34b24ce..bde8f3da3 100644
--- a/docs/config.toml
+++ b/docs/config.toml
@@ -1,9 +1,10 @@
-title = "High-Performance Async MySQL Driver for .NET"
+title = "High-Performance .NET MySQL Driver"
 baseurl = "https://mysqlconnector.net/"
 MetaDataFormat = "yaml"
 pluralizeListTitles = false
 pygmentsCodeFences = true
 pygmentsStyle = "friendly"
+disablePathToLower = true
 
 [blackfriday]
   plainIDAnchors = true
@@ -13,8 +14,12 @@ pygmentsStyle = "friendly"
     [markup.goldmark.renderer]
       unsafe = true
 
+  [markup.highlight]
+    noClasses=false
+    style = 'vs'
+
 [params]
-  description = "Documentation for MySqlConnector: an Async MySQL Driver for .NET and .NET Core"
+  description = "MySqlConnector is a high-performance, asynchronous C# ADO.NET driver for MySQL Server, MariaDB, Amazon Aurora, Azure Database for MySQL, Google Cloud SQL for MySQL, Percona Server and more."
   author = "MySqlConnector Authors"
 
 [taxonomies]
@@ -26,17 +31,22 @@ pygmentsStyle = "friendly"
     identifier = "getting started"
     pre = "<i class='fa fa-road'></i>"
     weight = 20
-[[menu.main]]
-    name = "Tutorials"
-    identifier = "tutorials"
-    pre = "<i class='fa fa-lightbulb-o'></i>"
-    weight = 40
 [[menu.main]]
     name = "API"
     identifier = "api"
     pre = "<i class='fa fa-file-text'></i>"
     url = "/api/MySqlConnectorAssembly/"
-    weight = 35
+    weight = 30
+[[menu.main]]
+    name = "Diagnostics"
+    identifier = "diagnostics"
+    pre = "<i class='fa fa-road'></i>"
+    weight = 40
+[[menu.main]]
+    name = "Tutorials"
+    identifier = "tutorials"
+    pre = "<i class='fa fa-lightbulb-o'></i>"
+    weight = 50
 [[menu.main]]
     name = "Troubleshooting"
     identifier = "troubleshooting"
diff --git a/docs/content/api/MySqlConnector.Authentication/IAuthenticationPlugin2/CreatePasswordHash.md b/docs/content/api/MySqlConnector.Authentication/IAuthenticationPlugin2/CreatePasswordHash.md
new file mode 100644
index 000000000..9bf5f1b62
--- /dev/null
+++ b/docs/content/api/MySqlConnector.Authentication/IAuthenticationPlugin2/CreatePasswordHash.md
@@ -0,0 +1,28 @@
+---
+title: IAuthenticationPlugin2.CreatePasswordHash method
+---
+
+# IAuthenticationPlugin2.CreatePasswordHash method
+
+Hashes the client's password (e.g., for TLS certificate fingerprint verification).
+
+```csharp
+public byte[] CreatePasswordHash(string password, ReadOnlySpan<byte> authenticationData)
+```
+
+| parameter | description |
+| --- | --- |
+| password | The client's password. |
+| authenticationData | The authentication data supplied by the server; this is the |
+
+## Return Value
+
+The authentication-method-specific hash of the client's password.
+
+## See Also
+
+* interface [IAuthenticationPlugin2](../../IAuthenticationPlugin2Type/)
+* namespace [MySqlConnector.Authentication](../../IAuthenticationPlugin2Type/)
+* assembly [MySqlConnector](../../../MySqlConnectorAssembly/)
+
+<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector.Authentication/IAuthenticationPlugin2Type.md b/docs/content/api/MySqlConnector.Authentication/IAuthenticationPlugin2Type.md
new file mode 100644
index 000000000..033bc42cc
--- /dev/null
+++ b/docs/content/api/MySqlConnector.Authentication/IAuthenticationPlugin2Type.md
@@ -0,0 +1,25 @@
+---
+title: IAuthenticationPlugin2
+---
+
+# IAuthenticationPlugin2 interface
+
+[`IAuthenticationPlugin2`](../IAuthenticationPlugin2Type/) is an extension to [`IAuthenticationPlugin`](../IAuthenticationPluginType/) that returns a hash of the client's password.
+
+```csharp
+public interface IAuthenticationPlugin2 : IAuthenticationPlugin
+```
+
+## Members
+
+| name | description |
+| --- | --- |
+| [CreatePasswordHash](../IAuthenticationPlugin2/CreatePasswordHash/)(…) | Hashes the client's password (e.g., for TLS certificate fingerprint verification). |
+
+## See Also
+
+* interface [IAuthenticationPlugin](../IAuthenticationPluginType/)
+* namespace [MySqlConnector.Authentication](../../MySqlConnectorAuthenticationNamespace/)
+* assembly [MySqlConnector](../../MySqlConnectorAssembly/)
+
+<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector.Logging/IMySqlConnectorLogger/Log.md b/docs/content/api/MySqlConnector.Logging/IMySqlConnectorLogger/Log.md
index 2660f6679..159d5d2a1 100644
--- a/docs/content/api/MySqlConnector.Logging/IMySqlConnectorLogger/Log.md
+++ b/docs/content/api/MySqlConnector.Logging/IMySqlConnectorLogger/Log.md
@@ -7,8 +7,8 @@ title: IMySqlConnectorLogger.Log method
 Writes a log message to the target.
 
 ```csharp
-public void Log(MySqlConnectorLogLevel level, string message, object[] args = null, 
-    Exception exception = null)
+public void Log(MySqlConnectorLogLevel level, string message, object?[]? args = null, 
+    Exception? exception = null)
 ```
 
 | parameter | description |
diff --git a/docs/content/api/MySqlConnector.Logging/NoOpLogger/Log.md b/docs/content/api/MySqlConnector.Logging/NoOpLogger/Log.md
index 92ee93e66..4ce6bb9ef 100644
--- a/docs/content/api/MySqlConnector.Logging/NoOpLogger/Log.md
+++ b/docs/content/api/MySqlConnector.Logging/NoOpLogger/Log.md
@@ -7,8 +7,8 @@ title: NoOpLogger.Log method
 Ignores the specified log message.
 
 ```csharp
-public void Log(MySqlConnectorLogLevel level, string message, object[] args = null, 
-    Exception exception = null)
+public void Log(MySqlConnectorLogLevel level, string message, object?[]? args = null, 
+    Exception? exception = null)
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlAttribute/MySqlAttribute.md b/docs/content/api/MySqlConnector/MySqlAttribute/MySqlAttribute.md
index 917fc4ac8..06f9f3be9 100644
--- a/docs/content/api/MySqlConnector/MySqlAttribute/MySqlAttribute.md
+++ b/docs/content/api/MySqlConnector/MySqlAttribute/MySqlAttribute.md
@@ -20,12 +20,16 @@ public MySqlAttribute()
 
 # MySqlAttribute constructor (2 of 2)
 
-Initializes a new [`MySqlAttribute`](../../MySqlAttributeType/) with the specified attribute name and value.
+[`MySqlAttribute`](../../MySqlAttributeType/) represents an attribute that can be sent with a MySQL query.
 
 ```csharp
-public MySqlAttribute(string attributeName, object value)
+public MySqlAttribute(string attributeName, object? value)
 ```
 
+## Remarks
+
+See [Query Attributes](https://dev.mysql.com/doc/refman/8.0/en/query-attributes.html) for information on using query attributes.
+
 ## See Also
 
 * class [MySqlAttribute](../../MySqlAttributeType/)
diff --git a/docs/content/api/MySqlConnector/MySqlAttribute/Value.md b/docs/content/api/MySqlConnector/MySqlAttribute/Value.md
index 3c733c04c..36ff07b24 100644
--- a/docs/content/api/MySqlConnector/MySqlAttribute/Value.md
+++ b/docs/content/api/MySqlConnector/MySqlAttribute/Value.md
@@ -7,7 +7,7 @@ title: MySqlAttribute.Value property
 Gets or sets the attribute value.
 
 ```csharp
-public object Value { get; set; }
+public object? Value { get; set; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlAttributeCollection/SetAttribute.md b/docs/content/api/MySqlConnector/MySqlAttributeCollection/SetAttribute.md
index b8ce434db..a63742c28 100644
--- a/docs/content/api/MySqlConnector/MySqlAttributeCollection/SetAttribute.md
+++ b/docs/content/api/MySqlConnector/MySqlAttributeCollection/SetAttribute.md
@@ -7,7 +7,7 @@ title: MySqlAttributeCollection.SetAttribute method
 Sets the attribute with the specified name to the given value, overwriting it if it already exists.
 
 ```csharp
-public void SetAttribute(string attributeName, object value)
+public void SetAttribute(string attributeName, object? value)
 ```
 
 | parameter | description |
diff --git a/docs/content/api/MySqlConnector/MySqlAttributeType.md b/docs/content/api/MySqlConnector/MySqlAttributeType.md
index d389b9981..bcf4a8184 100644
--- a/docs/content/api/MySqlConnector/MySqlAttributeType.md
+++ b/docs/content/api/MySqlConnector/MySqlAttributeType.md
@@ -15,7 +15,7 @@ public sealed class MySqlAttribute : ICloneable
 | name | description |
 | --- | --- |
 | [MySqlAttribute](../MySqlAttribute/MySqlAttribute/)() | Initializes a new [`MySqlAttribute`](../MySqlAttributeType/). |
-| [MySqlAttribute](../MySqlAttribute/MySqlAttribute/)(…) | Initializes a new [`MySqlAttribute`](../MySqlAttributeType/) with the specified attribute name and value. |
+| [MySqlAttribute](../MySqlAttribute/MySqlAttribute/)(…) | [`MySqlAttribute`](../MySqlAttributeType/) represents an attribute that can be sent with a MySQL query. |
 | [AttributeName](../MySqlAttribute/AttributeName/) { get; set; } | Gets or sets the attribute name. |
 | [Value](../MySqlAttribute/Value/) { get; set; } | Gets or sets the attribute value. |
 | [Clone](../MySqlAttribute/Clone/)() | Returns a new [`MySqlAttribute`](../MySqlAttributeType/) with the same property values as this instance. |
diff --git a/docs/content/api/MySqlConnector/MySqlBatch/Connection.md b/docs/content/api/MySqlConnector/MySqlBatch/Connection.md
index 103f0e90f..3cbe0547d 100644
--- a/docs/content/api/MySqlConnector/MySqlBatch/Connection.md
+++ b/docs/content/api/MySqlConnector/MySqlBatch/Connection.md
@@ -5,7 +5,7 @@ title: MySqlBatch.Connection property
 # MySqlBatch.Connection property
 
 ```csharp
-public MySqlConnection Connection { get; set; }
+public MySqlConnection? Connection { get; set; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlBatch/DbConnection.md b/docs/content/api/MySqlConnector/MySqlBatch/DbConnection.md
index 045708f79..d3bd41404 100644
--- a/docs/content/api/MySqlConnector/MySqlBatch/DbConnection.md
+++ b/docs/content/api/MySqlConnector/MySqlBatch/DbConnection.md
@@ -5,7 +5,7 @@ title: MySqlBatch.DbConnection property
 # MySqlBatch.DbConnection property
 
 ```csharp
-protected override DbConnection DbConnection { get; set; }
+protected override DbConnection? DbConnection { get; set; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlBatch/DbTransaction.md b/docs/content/api/MySqlConnector/MySqlBatch/DbTransaction.md
index 93fe5b4fc..923e20a4e 100644
--- a/docs/content/api/MySqlConnector/MySqlBatch/DbTransaction.md
+++ b/docs/content/api/MySqlConnector/MySqlBatch/DbTransaction.md
@@ -5,7 +5,7 @@ title: MySqlBatch.DbTransaction property
 # MySqlBatch.DbTransaction property
 
 ```csharp
-protected override DbTransaction DbTransaction { get; set; }
+protected override DbTransaction? DbTransaction { get; set; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlBatch/ExecuteNonQueryAsync.md b/docs/content/api/MySqlConnector/MySqlBatch/ExecuteNonQueryAsync.md
index 4b7e741ef..0d1935d27 100644
--- a/docs/content/api/MySqlConnector/MySqlBatch/ExecuteNonQueryAsync.md
+++ b/docs/content/api/MySqlConnector/MySqlBatch/ExecuteNonQueryAsync.md
@@ -5,8 +5,7 @@ title: MySqlBatch.ExecuteNonQueryAsync method
 # MySqlBatch.ExecuteNonQueryAsync method
 
 ```csharp
-public override Task<int> ExecuteNonQueryAsync(
-    CancellationToken cancellationToken = default(CancellationToken))
+public override Task<int> ExecuteNonQueryAsync(CancellationToken cancellationToken = default)
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlBatch/ExecuteReaderAsync.md b/docs/content/api/MySqlConnector/MySqlBatch/ExecuteReaderAsync.md
index 4d6951ffd..761061ba1 100644
--- a/docs/content/api/MySqlConnector/MySqlBatch/ExecuteReaderAsync.md
+++ b/docs/content/api/MySqlConnector/MySqlBatch/ExecuteReaderAsync.md
@@ -7,8 +7,7 @@ title: MySqlBatch.ExecuteReaderAsync method
 Executes all the commands in the batch, returning a [`MySqlDataReader`](../../MySqlDataReaderType/) that can iterate over the result sets. If multiple resultsets are returned, use [`NextResultAsync`](../../MySqlDataReader/NextResultAsync/) to access them.
 
 ```csharp
-public Task<MySqlDataReader> ExecuteReaderAsync(
-    CancellationToken cancellationToken = default(CancellationToken))
+public Task<MySqlDataReader> ExecuteReaderAsync(CancellationToken cancellationToken = default)
 ```
 
 | parameter | description |
diff --git a/docs/content/api/MySqlConnector/MySqlBatch/ExecuteScalar.md b/docs/content/api/MySqlConnector/MySqlBatch/ExecuteScalar.md
index 4b01057e9..a0361049b 100644
--- a/docs/content/api/MySqlConnector/MySqlBatch/ExecuteScalar.md
+++ b/docs/content/api/MySqlConnector/MySqlBatch/ExecuteScalar.md
@@ -5,7 +5,7 @@ title: MySqlBatch.ExecuteScalar method
 # MySqlBatch.ExecuteScalar method
 
 ```csharp
-public override object ExecuteScalar()
+public override object? ExecuteScalar()
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlBatch/ExecuteScalarAsync.md b/docs/content/api/MySqlConnector/MySqlBatch/ExecuteScalarAsync.md
index e39d8b2dc..368b3df70 100644
--- a/docs/content/api/MySqlConnector/MySqlBatch/ExecuteScalarAsync.md
+++ b/docs/content/api/MySqlConnector/MySqlBatch/ExecuteScalarAsync.md
@@ -5,8 +5,7 @@ title: MySqlBatch.ExecuteScalarAsync method
 # MySqlBatch.ExecuteScalarAsync method
 
 ```csharp
-public override Task<object> ExecuteScalarAsync(
-    CancellationToken cancellationToken = default(CancellationToken))
+public override Task<object?> ExecuteScalarAsync(CancellationToken cancellationToken = default)
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlBatch/MySqlBatch.md b/docs/content/api/MySqlConnector/MySqlBatch/MySqlBatch.md
index 63339b2b6..a8c026cdb 100644
--- a/docs/content/api/MySqlConnector/MySqlBatch/MySqlBatch.md
+++ b/docs/content/api/MySqlConnector/MySqlBatch/MySqlBatch.md
@@ -23,7 +23,7 @@ public MySqlBatch()
 Initializes a new [`MySqlBatch`](../../MySqlBatchType/) object, setting the [`Connection`](../Connection/) and [`Transaction`](../Transaction/) if specified.
 
 ```csharp
-public MySqlBatch(MySqlConnection connection = null, MySqlTransaction transaction = null)
+public MySqlBatch(MySqlConnection? connection = null, MySqlTransaction? transaction = null)
 ```
 
 | parameter | description |
diff --git a/docs/content/api/MySqlConnector/MySqlBatch/PrepareAsync.md b/docs/content/api/MySqlConnector/MySqlBatch/PrepareAsync.md
index f6192b653..e53c2c1f8 100644
--- a/docs/content/api/MySqlConnector/MySqlBatch/PrepareAsync.md
+++ b/docs/content/api/MySqlConnector/MySqlBatch/PrepareAsync.md
@@ -5,7 +5,7 @@ title: MySqlBatch.PrepareAsync method
 # MySqlBatch.PrepareAsync method
 
 ```csharp
-public override Task PrepareAsync(CancellationToken cancellationToken = default(CancellationToken))
+public override Task PrepareAsync(CancellationToken cancellationToken = default)
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlBatch/Transaction.md b/docs/content/api/MySqlConnector/MySqlBatch/Transaction.md
index 81ef352af..4f24205d3 100644
--- a/docs/content/api/MySqlConnector/MySqlBatch/Transaction.md
+++ b/docs/content/api/MySqlConnector/MySqlBatch/Transaction.md
@@ -5,7 +5,7 @@ title: MySqlBatch.Transaction property
 # MySqlBatch.Transaction property
 
 ```csharp
-public MySqlTransaction Transaction { get; set; }
+public MySqlTransaction? Transaction { get; set; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlBatchCommand/CanCreateParameter.md b/docs/content/api/MySqlConnector/MySqlBatchCommand/CanCreateParameter.md
new file mode 100644
index 000000000..1714a54b6
--- /dev/null
+++ b/docs/content/api/MySqlConnector/MySqlBatchCommand/CanCreateParameter.md
@@ -0,0 +1,17 @@
+---
+title: MySqlBatchCommand.CanCreateParameter property
+---
+
+# MySqlBatchCommand.CanCreateParameter property
+
+```csharp
+public override bool CanCreateParameter { get; }
+```
+
+## See Also
+
+* class [MySqlBatchCommand](../../MySqlBatchCommandType/)
+* namespace [MySqlConnector](../../MySqlBatchCommandType/)
+* assembly [MySqlConnector](../../../MySqlConnectorAssembly/)
+
+<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector/MySqlBatchCommand/CreateParameter.md b/docs/content/api/MySqlConnector/MySqlBatchCommand/CreateParameter.md
new file mode 100644
index 000000000..722edbfbe
--- /dev/null
+++ b/docs/content/api/MySqlConnector/MySqlBatchCommand/CreateParameter.md
@@ -0,0 +1,17 @@
+---
+title: MySqlBatchCommand.CreateParameter method
+---
+
+# MySqlBatchCommand.CreateParameter method
+
+```csharp
+public override DbParameter CreateParameter()
+```
+
+## See Also
+
+* class [MySqlBatchCommand](../../MySqlBatchCommandType/)
+* namespace [MySqlConnector](../../MySqlBatchCommandType/)
+* assembly [MySqlConnector](../../../MySqlConnectorAssembly/)
+
+<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector/MySqlBatchCommand/MySqlBatchCommand.md b/docs/content/api/MySqlConnector/MySqlBatchCommand/MySqlBatchCommand.md
index 7dd155319..8b012fdbe 100644
--- a/docs/content/api/MySqlConnector/MySqlBatchCommand/MySqlBatchCommand.md
+++ b/docs/content/api/MySqlConnector/MySqlBatchCommand/MySqlBatchCommand.md
@@ -21,7 +21,7 @@ public MySqlBatchCommand()
 # MySqlBatchCommand constructor (2 of 2)
 
 ```csharp
-public MySqlBatchCommand(string commandText)
+public MySqlBatchCommand(string? commandText)
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlBatchCommandType.md b/docs/content/api/MySqlConnector/MySqlBatchCommandType.md
index 2eb882a9e..0bc897502 100644
--- a/docs/content/api/MySqlConnector/MySqlBatchCommandType.md
+++ b/docs/content/api/MySqlConnector/MySqlBatchCommandType.md
@@ -14,10 +14,12 @@ public sealed class MySqlBatchCommand : DbBatchCommand
 | --- | --- |
 | [MySqlBatchCommand](../MySqlBatchCommand/MySqlBatchCommand/)() | The default constructor. |
 | [MySqlBatchCommand](../MySqlBatchCommand/MySqlBatchCommand/)(…) |  |
+| override [CanCreateParameter](../MySqlBatchCommand/CanCreateParameter/) { get; } |  |
 | override [CommandText](../MySqlBatchCommand/CommandText/) { get; set; } |  |
 | override [CommandType](../MySqlBatchCommand/CommandType/) { get; set; } |  |
 | [Parameters](../MySqlBatchCommand/Parameters/) { get; } |  |
 | override [RecordsAffected](../MySqlBatchCommand/RecordsAffected/) { get; } |  |
+| override [CreateParameter](../MySqlBatchCommand/CreateParameter/)() |  |
 
 ## Protected Members
 
diff --git a/docs/content/api/MySqlConnector/MySqlBulkCopy/DestinationTableName.md b/docs/content/api/MySqlConnector/MySqlBulkCopy/DestinationTableName.md
index bdca57bb3..135e76ed1 100644
--- a/docs/content/api/MySqlConnector/MySqlBulkCopy/DestinationTableName.md
+++ b/docs/content/api/MySqlConnector/MySqlBulkCopy/DestinationTableName.md
@@ -7,7 +7,7 @@ title: MySqlBulkCopy.DestinationTableName property
 The name of the table to insert rows into.
 
 ```csharp
-public string DestinationTableName { get; set; }
+public string? DestinationTableName { get; set; }
 ```
 
 ## Remarks
diff --git a/docs/content/api/MySqlConnector/MySqlBulkCopy/MySqlBulkCopy.md b/docs/content/api/MySqlConnector/MySqlBulkCopy/MySqlBulkCopy.md
index cf4bf7248..1d0101b8a 100644
--- a/docs/content/api/MySqlConnector/MySqlBulkCopy/MySqlBulkCopy.md
+++ b/docs/content/api/MySqlConnector/MySqlBulkCopy/MySqlBulkCopy.md
@@ -7,7 +7,7 @@ title: MySqlBulkCopy constructor
 Initializes a [`MySqlBulkCopy`](../../MySqlBulkCopyType/) object with the specified connection, and optionally the active transaction.
 
 ```csharp
-public MySqlBulkCopy(MySqlConnection connection, MySqlTransaction transaction = null)
+public MySqlBulkCopy(MySqlConnection connection, MySqlTransaction? transaction = null)
 ```
 
 | parameter | description |
diff --git a/docs/content/api/MySqlConnector/MySqlBulkCopy/MySqlRowsCopied.md b/docs/content/api/MySqlConnector/MySqlBulkCopy/MySqlRowsCopied.md
index e478807f4..66429cfec 100644
--- a/docs/content/api/MySqlConnector/MySqlBulkCopy/MySqlRowsCopied.md
+++ b/docs/content/api/MySqlConnector/MySqlBulkCopy/MySqlRowsCopied.md
@@ -7,7 +7,7 @@ title: MySqlBulkCopy.MySqlRowsCopied event
 This event is raised every time that the number of rows specified by the [`NotifyAfter`](../NotifyAfter/) property have been processed.
 
 ```csharp
-public event MySqlRowsCopiedEventHandler MySqlRowsCopied;
+public event MySqlRowsCopiedEventHandler? MySqlRowsCopied;
 ```
 
 ## Remarks
diff --git a/docs/content/api/MySqlConnector/MySqlBulkCopy/WriteToServerAsync.md b/docs/content/api/MySqlConnector/MySqlBulkCopy/WriteToServerAsync.md
index 3055fc5ad..0b5c4569f 100644
--- a/docs/content/api/MySqlConnector/MySqlBulkCopy/WriteToServerAsync.md
+++ b/docs/content/api/MySqlConnector/MySqlBulkCopy/WriteToServerAsync.md
@@ -8,7 +8,7 @@ Asynchronously copies all rows in the supplied DataTable to the destination tabl
 
 ```csharp
 public ValueTask<MySqlBulkCopyResult> WriteToServerAsync(DataTable dataTable, 
-    CancellationToken cancellationToken = default(CancellationToken))
+    CancellationToken cancellationToken = default)
 ```
 
 | parameter | description |
@@ -35,7 +35,7 @@ Asynchronously copies all rows in the supplied IDataReader to the destination ta
 
 ```csharp
 public ValueTask<MySqlBulkCopyResult> WriteToServerAsync(IDataReader dataReader, 
-    CancellationToken cancellationToken = default(CancellationToken))
+    CancellationToken cancellationToken = default)
 ```
 
 | parameter | description |
@@ -62,7 +62,7 @@ Asynchronously copies all rows in the supplied sequence of DataRow objects to th
 
 ```csharp
 public ValueTask<MySqlBulkCopyResult> WriteToServerAsync(IEnumerable<DataRow> dataRows, 
-    int columnCount, CancellationToken cancellationToken = default(CancellationToken))
+    int columnCount, CancellationToken cancellationToken = default)
 ```
 
 | parameter | description |
diff --git a/docs/content/api/MySqlConnector/MySqlBulkCopyColumnMapping/Expression.md b/docs/content/api/MySqlConnector/MySqlBulkCopyColumnMapping/Expression.md
index 42be87106..5e17a1407 100644
--- a/docs/content/api/MySqlConnector/MySqlBulkCopyColumnMapping/Expression.md
+++ b/docs/content/api/MySqlConnector/MySqlBulkCopyColumnMapping/Expression.md
@@ -7,7 +7,7 @@ title: MySqlBulkCopyColumnMapping.Expression property
 An optional expression for setting a destination column. To use an expression, the [`DestinationColumn`](../DestinationColumn/) should be set to the name of a user-defined variable and this expression should set a column using that variable.
 
 ```csharp
-public string Expression { get; set; }
+public string? Expression { get; set; }
 ```
 
 ## Remarks
diff --git a/docs/content/api/MySqlConnector/MySqlBulkCopyColumnMapping/MySqlBulkCopyColumnMapping.md b/docs/content/api/MySqlConnector/MySqlBulkCopyColumnMapping/MySqlBulkCopyColumnMapping.md
index 5e394d20a..ae4bc3092 100644
--- a/docs/content/api/MySqlConnector/MySqlBulkCopyColumnMapping/MySqlBulkCopyColumnMapping.md
+++ b/docs/content/api/MySqlConnector/MySqlBulkCopyColumnMapping/MySqlBulkCopyColumnMapping.md
@@ -20,11 +20,33 @@ public MySqlBulkCopyColumnMapping()
 
 # MySqlBulkCopyColumnMapping constructor (2 of 2)
 
-Initializes [`MySqlBulkCopyColumnMapping`](../../MySqlBulkCopyColumnMappingType/) to the specified values.
+Use [`MySqlBulkCopyColumnMapping`](../../MySqlBulkCopyColumnMappingType/) to specify how to map columns in the source data to columns in the destination table when using [`MySqlBulkCopy`](../../MySqlBulkCopyType/).
+
+Set [`SourceOrdinal`](../SourceOrdinal/) to the zero-based index of the source column to map. Set [`DestinationColumn`](../DestinationColumn/) to either the name of a column in the destination table, or the name of a user-defined variable. If a user-defined variable, you can use [`Expression`](../Expression/) to specify a MySQL expression that assigns its value to destination column.
+
+Source columns that don't have an entry in [`ColumnMappings`](../../MySqlBulkCopy/ColumnMappings/) will be ignored (unless the [`ColumnMappings`](../../MySqlBulkCopy/ColumnMappings/) collection is empty, in which case all columns will be mapped one-to-one).
+
+MySqlConnector will transmit all binary data as hex, so any expression that operates on it must decode it with the `UNHEX` function first. (This will be performed automatically if no [`Expression`](../Expression/) is specified, but will be necessary to specify manually for more complex expressions.)
+
+Example code:
+
+```csharp
+new MySqlBulkCopyColumnMapping
+{
+    SourceOrdinal = 2,
+    DestinationColumn = "user_name",
+},
+new MySqlBulkCopyColumnMapping
+{
+    SourceOrdinal = 0,
+    DestinationColumn = "@tmp",
+    Expression = "column_value = @tmp * 2",
+},
+```
 
 ```csharp
 public MySqlBulkCopyColumnMapping(int sourceOrdinal, string destinationColumn, 
-    string expression = null)
+    string? expression = null)
 ```
 
 | parameter | description |
diff --git a/docs/content/api/MySqlConnector/MySqlBulkCopyColumnMappingType.md b/docs/content/api/MySqlConnector/MySqlBulkCopyColumnMappingType.md
index cdcc716a1..2bce2888c 100644
--- a/docs/content/api/MySqlConnector/MySqlBulkCopyColumnMappingType.md
+++ b/docs/content/api/MySqlConnector/MySqlBulkCopyColumnMappingType.md
@@ -32,12 +32,18 @@ new MySqlBulkCopyColumnMapping
 public sealed class MySqlBulkCopyColumnMapping
 ```
 
+| parameter | description |
+| --- | --- |
+| sourceOrdinal | The zero-based ordinal position of the source column. |
+| destinationColumn | The name of the destination column. |
+| expression | The optional expression to be used to set the destination column. |
+
 ## Public Members
 
 | name | description |
 | --- | --- |
 | [MySqlBulkCopyColumnMapping](../MySqlBulkCopyColumnMapping/MySqlBulkCopyColumnMapping/)() | Initializes [`MySqlBulkCopyColumnMapping`](../MySqlBulkCopyColumnMappingType/) with the default values. |
-| [MySqlBulkCopyColumnMapping](../MySqlBulkCopyColumnMapping/MySqlBulkCopyColumnMapping/)(…) | Initializes [`MySqlBulkCopyColumnMapping`](../MySqlBulkCopyColumnMappingType/) to the specified values. |
+| [MySqlBulkCopyColumnMapping](../MySqlBulkCopyColumnMapping/MySqlBulkCopyColumnMapping/)(…) | Use [`MySqlBulkCopyColumnMapping`](../MySqlBulkCopyColumnMappingType/) to specify how to map columns in the source data to columns in the destination table when using [`MySqlBulkCopy`](../MySqlBulkCopyType/). |
 | [DestinationColumn](../MySqlBulkCopyColumnMapping/DestinationColumn/) { get; set; } | The name of the destination column to copy to. To use an expression, this should be the name of a unique user-defined variable. |
 | [Expression](../MySqlBulkCopyColumnMapping/Expression/) { get; set; } | An optional expression for setting a destination column. To use an expression, the [`DestinationColumn`](../MySqlBulkCopyColumnMapping/DestinationColumn/) should be set to the name of a user-defined variable and this expression should set a column using that variable. |
 | [SourceOrdinal](../MySqlBulkCopyColumnMapping/SourceOrdinal/) { get; set; } | The zero-based ordinal position of the source column to map from. |
diff --git a/docs/content/api/MySqlConnector/MySqlBulkLoader/CharacterSet.md b/docs/content/api/MySqlConnector/MySqlBulkLoader/CharacterSet.md
index e914d8a7b..3b16471d0 100644
--- a/docs/content/api/MySqlConnector/MySqlBulkLoader/CharacterSet.md
+++ b/docs/content/api/MySqlConnector/MySqlBulkLoader/CharacterSet.md
@@ -7,7 +7,7 @@ title: MySqlBulkLoader.CharacterSet property
 (Optional) The character set of the source data. By default, the database's character set is used.
 
 ```csharp
-public string CharacterSet { get; set; }
+public string? CharacterSet { get; set; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlBulkLoader/FieldTerminator.md b/docs/content/api/MySqlConnector/MySqlBulkLoader/FieldTerminator.md
index bbe29423d..33f540ecf 100644
--- a/docs/content/api/MySqlConnector/MySqlBulkLoader/FieldTerminator.md
+++ b/docs/content/api/MySqlConnector/MySqlBulkLoader/FieldTerminator.md
@@ -7,7 +7,7 @@ title: MySqlBulkLoader.FieldTerminator property
 (Optional) The string fields are terminated with.
 
 ```csharp
-public string FieldTerminator { get; set; }
+public string? FieldTerminator { get; set; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlBulkLoader/FileName.md b/docs/content/api/MySqlConnector/MySqlBulkLoader/FileName.md
index d741de5c6..4c20a251a 100644
--- a/docs/content/api/MySqlConnector/MySqlBulkLoader/FileName.md
+++ b/docs/content/api/MySqlConnector/MySqlBulkLoader/FileName.md
@@ -7,7 +7,7 @@ title: MySqlBulkLoader.FileName property
 The name of the local (if [`Local`](../Local/) is `true`) or remote (otherwise) file to load. Either this or [`SourceStream`](../SourceStream/) must be set.
 
 ```csharp
-public string FileName { get; set; }
+public string? FileName { get; set; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlBulkLoader/LinePrefix.md b/docs/content/api/MySqlConnector/MySqlBulkLoader/LinePrefix.md
index 88b4593ef..d924dd7a5 100644
--- a/docs/content/api/MySqlConnector/MySqlBulkLoader/LinePrefix.md
+++ b/docs/content/api/MySqlConnector/MySqlBulkLoader/LinePrefix.md
@@ -7,7 +7,7 @@ title: MySqlBulkLoader.LinePrefix property
 (Optional) A prefix in each line that should be skipped when loading.
 
 ```csharp
-public string LinePrefix { get; set; }
+public string? LinePrefix { get; set; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlBulkLoader/LineTerminator.md b/docs/content/api/MySqlConnector/MySqlBulkLoader/LineTerminator.md
index 61d9aa1df..f387c13ef 100644
--- a/docs/content/api/MySqlConnector/MySqlBulkLoader/LineTerminator.md
+++ b/docs/content/api/MySqlConnector/MySqlBulkLoader/LineTerminator.md
@@ -7,7 +7,7 @@ title: MySqlBulkLoader.LineTerminator property
 (Optional) The string lines are terminated with.
 
 ```csharp
-public string LineTerminator { get; set; }
+public string? LineTerminator { get; set; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlBulkLoader/SourceStream.md b/docs/content/api/MySqlConnector/MySqlBulkLoader/SourceStream.md
index e9c884c88..58b3efcf4 100644
--- a/docs/content/api/MySqlConnector/MySqlBulkLoader/SourceStream.md
+++ b/docs/content/api/MySqlConnector/MySqlBulkLoader/SourceStream.md
@@ -7,7 +7,7 @@ title: MySqlBulkLoader.SourceStream property
 A Stream containing the data to load. Either this or [`FileName`](../FileName/) must be set. The [`Local`](../Local/) property must be `true` if this is set.
 
 ```csharp
-public Stream SourceStream { get; set; }
+public Stream? SourceStream { get; set; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlBulkLoader/TableName.md b/docs/content/api/MySqlConnector/MySqlBulkLoader/TableName.md
index cafb0a522..2190e201a 100644
--- a/docs/content/api/MySqlConnector/MySqlBulkLoader/TableName.md
+++ b/docs/content/api/MySqlConnector/MySqlBulkLoader/TableName.md
@@ -7,7 +7,7 @@ title: MySqlBulkLoader.TableName property
 The name of the table to load into. If this is a reserved word or contains spaces, it must be quoted.
 
 ```csharp
-public string TableName { get; set; }
+public string? TableName { get; set; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlCommand/Connection.md b/docs/content/api/MySqlConnector/MySqlCommand/Connection.md
index 97fb24da5..d22ae3ef5 100644
--- a/docs/content/api/MySqlConnector/MySqlCommand/Connection.md
+++ b/docs/content/api/MySqlConnector/MySqlCommand/Connection.md
@@ -5,7 +5,7 @@ title: MySqlCommand.Connection property
 # MySqlCommand.Connection property
 
 ```csharp
-public MySqlConnection Connection { get; set; }
+public MySqlConnection? Connection { get; set; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlCommand/DbConnection.md b/docs/content/api/MySqlConnector/MySqlCommand/DbConnection.md
index ce67ba1a4..a244c7e50 100644
--- a/docs/content/api/MySqlConnector/MySqlCommand/DbConnection.md
+++ b/docs/content/api/MySqlConnector/MySqlCommand/DbConnection.md
@@ -5,7 +5,7 @@ title: MySqlCommand.DbConnection property
 # MySqlCommand.DbConnection property
 
 ```csharp
-protected override DbConnection DbConnection { get; set; }
+protected override DbConnection? DbConnection { get; set; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlCommand/DbTransaction.md b/docs/content/api/MySqlConnector/MySqlCommand/DbTransaction.md
index b7e71644f..4edb70974 100644
--- a/docs/content/api/MySqlConnector/MySqlCommand/DbTransaction.md
+++ b/docs/content/api/MySqlConnector/MySqlCommand/DbTransaction.md
@@ -5,7 +5,7 @@ title: MySqlCommand.DbTransaction property
 # MySqlCommand.DbTransaction property
 
 ```csharp
-protected override DbTransaction DbTransaction { get; set; }
+protected override DbTransaction? DbTransaction { get; set; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlCommand/ExecuteReaderAsync.md b/docs/content/api/MySqlConnector/MySqlCommand/ExecuteReaderAsync.md
index 11f888c5c..4e875b087 100644
--- a/docs/content/api/MySqlConnector/MySqlCommand/ExecuteReaderAsync.md
+++ b/docs/content/api/MySqlConnector/MySqlCommand/ExecuteReaderAsync.md
@@ -5,8 +5,7 @@ title: MySqlCommand.ExecuteReaderAsync methods
 # MySqlCommand.ExecuteReaderAsync method (1 of 2)
 
 ```csharp
-public Task<MySqlDataReader> ExecuteReaderAsync(
-    CancellationToken cancellationToken = default(CancellationToken))
+public Task<MySqlDataReader> ExecuteReaderAsync(CancellationToken cancellationToken = default)
 ```
 
 ## See Also
@@ -22,7 +21,7 @@ public Task<MySqlDataReader> ExecuteReaderAsync(
 
 ```csharp
 public Task<MySqlDataReader> ExecuteReaderAsync(CommandBehavior behavior, 
-    CancellationToken cancellationToken = default(CancellationToken))
+    CancellationToken cancellationToken = default)
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlCommand/ExecuteScalar.md b/docs/content/api/MySqlConnector/MySqlCommand/ExecuteScalar.md
index e8a03ae5e..02bd3e2fd 100644
--- a/docs/content/api/MySqlConnector/MySqlCommand/ExecuteScalar.md
+++ b/docs/content/api/MySqlConnector/MySqlCommand/ExecuteScalar.md
@@ -5,7 +5,7 @@ title: MySqlCommand.ExecuteScalar method
 # MySqlCommand.ExecuteScalar method
 
 ```csharp
-public override object ExecuteScalar()
+public override object? ExecuteScalar()
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlCommand/ExecuteScalarAsync.md b/docs/content/api/MySqlConnector/MySqlCommand/ExecuteScalarAsync.md
index 524921777..bb4ddf153 100644
--- a/docs/content/api/MySqlConnector/MySqlCommand/ExecuteScalarAsync.md
+++ b/docs/content/api/MySqlConnector/MySqlCommand/ExecuteScalarAsync.md
@@ -5,7 +5,7 @@ title: MySqlCommand.ExecuteScalarAsync method
 # MySqlCommand.ExecuteScalarAsync method
 
 ```csharp
-public override Task<object> ExecuteScalarAsync(CancellationToken cancellationToken)
+public override Task<object?> ExecuteScalarAsync(CancellationToken cancellationToken)
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlCommand/MySqlCommand.md b/docs/content/api/MySqlConnector/MySqlCommand/MySqlCommand.md
index 6a179e7bf..8227af70b 100644
--- a/docs/content/api/MySqlConnector/MySqlCommand/MySqlCommand.md
+++ b/docs/content/api/MySqlConnector/MySqlCommand/MySqlCommand.md
@@ -23,7 +23,7 @@ public MySqlCommand()
 Initializes a new instance of the [`MySqlCommand`](../../MySqlCommandType/) class, setting [`CommandText`](../CommandText/) to *commandText*.
 
 ```csharp
-public MySqlCommand(string commandText)
+public MySqlCommand(string? commandText)
 ```
 
 | parameter | description |
@@ -43,7 +43,7 @@ public MySqlCommand(string commandText)
 Initializes a new instance of the [`MySqlCommand`](../../MySqlCommandType/) class with the specified [`MySqlConnection`](../../MySqlConnectionType/) and [`MySqlTransaction`](../../MySqlTransactionType/).
 
 ```csharp
-public MySqlCommand(MySqlConnection connection, MySqlTransaction transaction)
+public MySqlCommand(MySqlConnection? connection, MySqlTransaction? transaction)
 ```
 
 | parameter | description |
@@ -66,7 +66,7 @@ public MySqlCommand(MySqlConnection connection, MySqlTransaction transaction)
 Initializes a new instance of the [`MySqlCommand`](../../MySqlCommandType/) class with the specified command text and [`MySqlConnection`](../../MySqlConnectionType/).
 
 ```csharp
-public MySqlCommand(string commandText, MySqlConnection connection)
+public MySqlCommand(string? commandText, MySqlConnection? connection)
 ```
 
 | parameter | description |
@@ -88,7 +88,7 @@ public MySqlCommand(string commandText, MySqlConnection connection)
 Initializes a new instance of the [`MySqlCommand`](../../MySqlCommandType/) class with the specified command text,[`MySqlConnection`](../../MySqlConnectionType/), and [`MySqlTransaction`](../../MySqlTransactionType/).
 
 ```csharp
-public MySqlCommand(string commandText, MySqlConnection connection, MySqlTransaction transaction)
+public MySqlCommand(string? commandText, MySqlConnection? connection, MySqlTransaction? transaction)
 ```
 
 | parameter | description |
diff --git a/docs/content/api/MySqlConnector/MySqlCommand/PrepareAsync.md b/docs/content/api/MySqlConnector/MySqlCommand/PrepareAsync.md
index 68ca471a2..0d63851e5 100644
--- a/docs/content/api/MySqlConnector/MySqlCommand/PrepareAsync.md
+++ b/docs/content/api/MySqlConnector/MySqlCommand/PrepareAsync.md
@@ -5,7 +5,7 @@ title: MySqlCommand.PrepareAsync method
 # MySqlCommand.PrepareAsync method
 
 ```csharp
-public override Task PrepareAsync(CancellationToken cancellationToken = default(CancellationToken))
+public override Task PrepareAsync(CancellationToken cancellationToken = default)
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlCommand/Transaction.md b/docs/content/api/MySqlConnector/MySqlCommand/Transaction.md
index 813d74246..d77b5d59c 100644
--- a/docs/content/api/MySqlConnector/MySqlCommand/Transaction.md
+++ b/docs/content/api/MySqlConnector/MySqlCommand/Transaction.md
@@ -5,7 +5,7 @@ title: MySqlCommand.Transaction property
 # MySqlCommand.Transaction property
 
 ```csharp
-public MySqlTransaction Transaction { get; set; }
+public MySqlTransaction? Transaction { get; set; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlCommandBuilder/DataAdapter.md b/docs/content/api/MySqlConnector/MySqlCommandBuilder/DataAdapter.md
index 048031ae4..866776c7e 100644
--- a/docs/content/api/MySqlConnector/MySqlCommandBuilder/DataAdapter.md
+++ b/docs/content/api/MySqlConnector/MySqlCommandBuilder/DataAdapter.md
@@ -5,7 +5,7 @@ title: MySqlCommandBuilder.DataAdapter property
 # MySqlCommandBuilder.DataAdapter property
 
 ```csharp
-public MySqlDataAdapter DataAdapter { get; set; }
+public MySqlDataAdapter? DataAdapter { get; set; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlConnection/BeginTransactionAsync.md b/docs/content/api/MySqlConnector/MySqlConnection/BeginTransactionAsync.md
index 06dfb80e9..120a3b3fb 100644
--- a/docs/content/api/MySqlConnector/MySqlConnection/BeginTransactionAsync.md
+++ b/docs/content/api/MySqlConnector/MySqlConnection/BeginTransactionAsync.md
@@ -8,7 +8,7 @@ Begins a database transaction asynchronously.
 
 ```csharp
 public ValueTask<MySqlTransaction> BeginTransactionAsync(
-    CancellationToken cancellationToken = default(CancellationToken))
+    CancellationToken cancellationToken = default)
 ```
 
 | parameter | description |
@@ -38,7 +38,7 @@ Begins a database transaction asynchronously.
 
 ```csharp
 public ValueTask<MySqlTransaction> BeginTransactionAsync(IsolationLevel isolationLevel, 
-    CancellationToken cancellationToken = default(CancellationToken))
+    CancellationToken cancellationToken = default)
 ```
 
 | parameter | description |
@@ -69,7 +69,7 @@ Begins a database transaction asynchronously.
 
 ```csharp
 public ValueTask<MySqlTransaction> BeginTransactionAsync(IsolationLevel isolationLevel, 
-    bool isReadOnly, CancellationToken cancellationToken = default(CancellationToken))
+    bool isReadOnly, CancellationToken cancellationToken = default)
 ```
 
 | parameter | description |
diff --git a/docs/content/api/MySqlConnector/MySqlConnection/ChangeDatabaseAsync.md b/docs/content/api/MySqlConnector/MySqlConnection/ChangeDatabaseAsync.md
index 7ea38ab30..1c0820fa3 100644
--- a/docs/content/api/MySqlConnector/MySqlConnection/ChangeDatabaseAsync.md
+++ b/docs/content/api/MySqlConnector/MySqlConnection/ChangeDatabaseAsync.md
@@ -6,7 +6,7 @@ title: MySqlConnection.ChangeDatabaseAsync method
 
 ```csharp
 public override Task ChangeDatabaseAsync(string databaseName, 
-    CancellationToken cancellationToken = default(CancellationToken))
+    CancellationToken cancellationToken = default)
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlConnection/ClearAllPoolsAsync.md b/docs/content/api/MySqlConnector/MySqlConnection/ClearAllPoolsAsync.md
index 1ef70be08..8239a16d2 100644
--- a/docs/content/api/MySqlConnector/MySqlConnection/ClearAllPoolsAsync.md
+++ b/docs/content/api/MySqlConnector/MySqlConnection/ClearAllPoolsAsync.md
@@ -7,8 +7,7 @@ title: MySqlConnection.ClearAllPoolsAsync method
 Asynchronously clears all connection pools.
 
 ```csharp
-public static Task ClearAllPoolsAsync(
-    CancellationToken cancellationToken = default(CancellationToken))
+public static Task ClearAllPoolsAsync(CancellationToken cancellationToken = default)
 ```
 
 | parameter | description |
diff --git a/docs/content/api/MySqlConnector/MySqlConnection/ClearPoolAsync.md b/docs/content/api/MySqlConnector/MySqlConnection/ClearPoolAsync.md
index 49fdcb698..d981c83cb 100644
--- a/docs/content/api/MySqlConnector/MySqlConnection/ClearPoolAsync.md
+++ b/docs/content/api/MySqlConnector/MySqlConnection/ClearPoolAsync.md
@@ -8,7 +8,7 @@ Asynchronously clears the connection pool that *connection* belongs to.
 
 ```csharp
 public static Task ClearPoolAsync(MySqlConnection connection, 
-    CancellationToken cancellationToken = default(CancellationToken))
+    CancellationToken cancellationToken = default)
 ```
 
 | parameter | description |
diff --git a/docs/content/api/MySqlConnector/MySqlConnection/EnlistTransaction.md b/docs/content/api/MySqlConnector/MySqlConnection/EnlistTransaction.md
index cd452486d..68de061e9 100644
--- a/docs/content/api/MySqlConnector/MySqlConnection/EnlistTransaction.md
+++ b/docs/content/api/MySqlConnector/MySqlConnection/EnlistTransaction.md
@@ -5,7 +5,7 @@ title: MySqlConnection.EnlistTransaction method
 # MySqlConnection.EnlistTransaction method
 
 ```csharp
-public override void EnlistTransaction(Transaction transaction)
+public override void EnlistTransaction(Transaction? transaction)
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlConnection/GetSchema.md b/docs/content/api/MySqlConnector/MySqlConnection/GetSchema.md
index e11ceff1a..c7f9b218e 100644
--- a/docs/content/api/MySqlConnector/MySqlConnection/GetSchema.md
+++ b/docs/content/api/MySqlConnector/MySqlConnection/GetSchema.md
@@ -32,7 +32,7 @@ public override DataTable GetSchema(string collectionName)
 
 | parameter | description |
 | --- | --- |
-| collectionName | The name of the schema to return. |
+| collectionName | The name of the schema to return. See [Supported Schema Collections](https://mysqlconnector.net/overview/schema-collections/) for the list of supported schema names. |
 
 ## Return Value
 
@@ -51,12 +51,12 @@ A DataTable containing schema information.
 Returns schema information for the data source of this [`MySqlConnection`](../../MySqlConnectionType/).
 
 ```csharp
-public override DataTable GetSchema(string collectionName, string[] restrictionValues)
+public override DataTable GetSchema(string collectionName, string?[] restrictionValues)
 ```
 
 | parameter | description |
 | --- | --- |
-| collectionName | The name of the schema to return. |
+| collectionName | The name of the schema to return. See [Supported Schema Collections](https://mysqlconnector.net/overview/schema-collections/) for the list of supported schema names. |
 | restrictionValues | The restrictions to apply to the schema. |
 
 ## Return Value
diff --git a/docs/content/api/MySqlConnector/MySqlConnection/GetSchemaAsync.md b/docs/content/api/MySqlConnector/MySqlConnection/GetSchemaAsync.md
index 5686a8f30..e0d33cf3b 100644
--- a/docs/content/api/MySqlConnector/MySqlConnection/GetSchemaAsync.md
+++ b/docs/content/api/MySqlConnector/MySqlConnection/GetSchemaAsync.md
@@ -7,8 +7,7 @@ title: MySqlConnection.GetSchemaAsync methods
 Asynchronously returns schema information for the data source of this [`MySqlConnection`](../../MySqlConnectionType/).
 
 ```csharp
-public override Task<DataTable> GetSchemaAsync(
-    CancellationToken cancellationToken = default(CancellationToken))
+public override Task<DataTable> GetSchemaAsync(CancellationToken cancellationToken = default)
 ```
 
 | parameter | description |
@@ -37,7 +36,7 @@ Asynchronously returns schema information for the data source of this [`MySqlCon
 
 ```csharp
 public override Task<DataTable> GetSchemaAsync(string collectionName, 
-    CancellationToken cancellationToken = default(CancellationToken))
+    CancellationToken cancellationToken = default)
 ```
 
 | parameter | description |
@@ -66,8 +65,8 @@ The proposed ADO.NET API that this is based on is not finalized; this API may ch
 Asynchronously returns schema information for the data source of this [`MySqlConnection`](../../MySqlConnectionType/).
 
 ```csharp
-public override Task<DataTable> GetSchemaAsync(string collectionName, string[] restrictionValues, 
-    CancellationToken cancellationToken = default(CancellationToken))
+public override Task<DataTable> GetSchemaAsync(string collectionName, string?[] restrictionValues, 
+    CancellationToken cancellationToken = default)
 ```
 
 | parameter | description |
diff --git a/docs/content/api/MySqlConnector/MySqlConnection/InfoMessage.md b/docs/content/api/MySqlConnector/MySqlConnection/InfoMessage.md
index a564dc7f1..f91351abf 100644
--- a/docs/content/api/MySqlConnector/MySqlConnection/InfoMessage.md
+++ b/docs/content/api/MySqlConnector/MySqlConnection/InfoMessage.md
@@ -5,7 +5,7 @@ title: MySqlConnection.InfoMessage event
 # MySqlConnection.InfoMessage event
 
 ```csharp
-public event MySqlInfoMessageEventHandler InfoMessage;
+public event MySqlInfoMessageEventHandler? InfoMessage;
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlConnection/MySqlConnection.md b/docs/content/api/MySqlConnector/MySqlConnection/MySqlConnection.md
index 957ca9465..cb1cb1aa1 100644
--- a/docs/content/api/MySqlConnector/MySqlConnection/MySqlConnection.md
+++ b/docs/content/api/MySqlConnector/MySqlConnection/MySqlConnection.md
@@ -21,7 +21,7 @@ public MySqlConnection()
 # MySqlConnection constructor (2 of 2)
 
 ```csharp
-public MySqlConnection(string connectionString)
+public MySqlConnection(string? connectionString)
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlConnection/PingAsync.md b/docs/content/api/MySqlConnector/MySqlConnection/PingAsync.md
index 2ab121b7f..4f052efd9 100644
--- a/docs/content/api/MySqlConnector/MySqlConnection/PingAsync.md
+++ b/docs/content/api/MySqlConnector/MySqlConnection/PingAsync.md
@@ -5,7 +5,7 @@ title: MySqlConnection.PingAsync method
 # MySqlConnection.PingAsync method
 
 ```csharp
-public Task<bool> PingAsync(CancellationToken cancellationToken = default(CancellationToken))
+public Task<bool> PingAsync(CancellationToken cancellationToken = default)
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlConnection/ProvideClientCertificatesCallback.md b/docs/content/api/MySqlConnector/MySqlConnection/ProvideClientCertificatesCallback.md
index ead598e30..e95403b5a 100644
--- a/docs/content/api/MySqlConnector/MySqlConnection/ProvideClientCertificatesCallback.md
+++ b/docs/content/api/MySqlConnector/MySqlConnection/ProvideClientCertificatesCallback.md
@@ -7,7 +7,7 @@ title: MySqlConnection.ProvideClientCertificatesCallback property
 Gets or sets the delegate used to provide client certificates for connecting to a server.
 
 ```csharp
-public Func<X509CertificateCollection, ValueTask> ProvideClientCertificatesCallback { get; set; }
+public Func<X509CertificateCollection, ValueTask>? ProvideClientCertificatesCallback { get; set; }
 ```
 
 ## Remarks
diff --git a/docs/content/api/MySqlConnector/MySqlConnection/ProvidePasswordCallback.md b/docs/content/api/MySqlConnector/MySqlConnection/ProvidePasswordCallback.md
index 091767e55..78750de7a 100644
--- a/docs/content/api/MySqlConnector/MySqlConnection/ProvidePasswordCallback.md
+++ b/docs/content/api/MySqlConnector/MySqlConnection/ProvidePasswordCallback.md
@@ -7,7 +7,7 @@ title: MySqlConnection.ProvidePasswordCallback property
 Gets or sets the delegate used to generate a password for new database connections.
 
 ```csharp
-public Func<MySqlProvidePasswordContext, string> ProvidePasswordCallback { get; set; }
+public Func<MySqlProvidePasswordContext, string>? ProvidePasswordCallback { get; set; }
 ```
 
 ## Remarks
diff --git a/docs/content/api/MySqlConnector/MySqlConnection/RemoteCertificateValidationCallback.md b/docs/content/api/MySqlConnector/MySqlConnection/RemoteCertificateValidationCallback.md
index 880ba2f04..08366c6bc 100644
--- a/docs/content/api/MySqlConnector/MySqlConnection/RemoteCertificateValidationCallback.md
+++ b/docs/content/api/MySqlConnector/MySqlConnection/RemoteCertificateValidationCallback.md
@@ -7,7 +7,7 @@ title: MySqlConnection.RemoteCertificateValidationCallback property
 Gets or sets the delegate used to verify that the server's certificate is valid.
 
 ```csharp
-public RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get; set; }
+public RemoteCertificateValidationCallback? RemoteCertificateValidationCallback { get; set; }
 ```
 
 ## Remarks
diff --git a/docs/content/api/MySqlConnector/MySqlConnection/ResetConnectionAsync.md b/docs/content/api/MySqlConnector/MySqlConnection/ResetConnectionAsync.md
index 2fb89a0ad..a78428752 100644
--- a/docs/content/api/MySqlConnector/MySqlConnection/ResetConnectionAsync.md
+++ b/docs/content/api/MySqlConnector/MySqlConnection/ResetConnectionAsync.md
@@ -7,8 +7,7 @@ title: MySqlConnection.ResetConnectionAsync method
 Resets the session state of the current open connection; this clears temporary tables and user-defined variables.
 
 ```csharp
-public ValueTask ResetConnectionAsync(
-    CancellationToken cancellationToken = default(CancellationToken))
+public ValueTask ResetConnectionAsync(CancellationToken cancellationToken = default)
 ```
 
 | parameter | description |
diff --git a/docs/content/api/MySqlConnector/MySqlConnectionOpenedCallbackType.md b/docs/content/api/MySqlConnector/MySqlConnectionOpenedCallbackType.md
new file mode 100644
index 000000000..7091e0a2e
--- /dev/null
+++ b/docs/content/api/MySqlConnector/MySqlConnectionOpenedCallbackType.md
@@ -0,0 +1,29 @@
+---
+title: MySqlConnectionOpenedCallback
+---
+
+# MySqlConnectionOpenedCallback delegate
+
+A callback that is invoked when a new [`MySqlConnection`](../MySqlConnectionType/) is opened.
+
+```csharp
+public delegate ValueTask MySqlConnectionOpenedCallback(MySqlConnectionOpenedContext context, 
+    CancellationToken cancellationToken);
+```
+
+| parameter | description |
+| --- | --- |
+| context | A [`MySqlConnectionOpenedContext`](../MySqlConnectionOpenedContextType/) giving information about the connection being opened. |
+| cancellationToken | A CancellationToken that can be used to cancel the asynchronous operation. |
+
+## Return Value
+
+A ValueTask representing the result of the possibly-asynchronous operation.
+
+## See Also
+
+* class [MySqlConnectionOpenedContext](../MySqlConnectionOpenedContextType/)
+* namespace [MySqlConnector](../../MySqlConnectorNamespace/)
+* assembly [MySqlConnector](../../MySqlConnectorAssembly/)
+
+<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector/MySqlConnectionOpenedConditionsType.md b/docs/content/api/MySqlConnector/MySqlConnectionOpenedConditionsType.md
new file mode 100644
index 000000000..ca177011f
--- /dev/null
+++ b/docs/content/api/MySqlConnector/MySqlConnectionOpenedConditionsType.md
@@ -0,0 +1,27 @@
+---
+title: MySqlConnectionOpenedConditions
+---
+
+# MySqlConnectionOpenedConditions enumeration
+
+Bitflags giving the conditions under which a connection was opened.
+
+```csharp
+[Flags]
+public enum MySqlConnectionOpenedConditions
+```
+
+## Values
+
+| name | value | description |
+| --- | --- | --- |
+| None | `0x0` | No specific conditions apply. This value may be used when an existing pooled connection is reused without being reset. |
+| New | `0x1` | A new physical connection to a MySQL Server was opened. This value is mutually exclusive with Reset. |
+| Reset | `0x2` | An existing pooled connection to a MySQL Server was reset. This value is mutually exclusive with New. |
+
+## See Also
+
+* namespace [MySqlConnector](../../MySqlConnectorNamespace/)
+* assembly [MySqlConnector](../../MySqlConnectorAssembly/)
+
+<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector/MySqlConnectionOpenedContext/Conditions.md b/docs/content/api/MySqlConnector/MySqlConnectionOpenedContext/Conditions.md
new file mode 100644
index 000000000..a62434adc
--- /dev/null
+++ b/docs/content/api/MySqlConnector/MySqlConnectionOpenedContext/Conditions.md
@@ -0,0 +1,20 @@
+---
+title: MySqlConnectionOpenedContext.Conditions property
+---
+
+# MySqlConnectionOpenedContext.Conditions property
+
+Bitflags giving the conditions under which a connection was opened.
+
+```csharp
+public MySqlConnectionOpenedConditions Conditions { get; }
+```
+
+## See Also
+
+* enum [MySqlConnectionOpenedConditions](../../MySqlConnectionOpenedConditionsType/)
+* class [MySqlConnectionOpenedContext](../../MySqlConnectionOpenedContextType/)
+* namespace [MySqlConnector](../../MySqlConnectionOpenedContextType/)
+* assembly [MySqlConnector](../../../MySqlConnectorAssembly/)
+
+<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector/MySqlConnectionOpenedContext/Connection.md b/docs/content/api/MySqlConnector/MySqlConnectionOpenedContext/Connection.md
new file mode 100644
index 000000000..70af5af5f
--- /dev/null
+++ b/docs/content/api/MySqlConnector/MySqlConnectionOpenedContext/Connection.md
@@ -0,0 +1,20 @@
+---
+title: MySqlConnectionOpenedContext.Connection property
+---
+
+# MySqlConnectionOpenedContext.Connection property
+
+The [`MySqlConnection`](../../MySqlConnectionType/) that was opened.
+
+```csharp
+public MySqlConnection Connection { get; }
+```
+
+## See Also
+
+* class [MySqlConnection](../../MySqlConnectionType/)
+* class [MySqlConnectionOpenedContext](../../MySqlConnectionOpenedContextType/)
+* namespace [MySqlConnector](../../MySqlConnectionOpenedContextType/)
+* assembly [MySqlConnector](../../../MySqlConnectorAssembly/)
+
+<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector/MySqlConnectionOpenedContextType.md b/docs/content/api/MySqlConnector/MySqlConnectionOpenedContextType.md
new file mode 100644
index 000000000..bf4cf0a59
--- /dev/null
+++ b/docs/content/api/MySqlConnector/MySqlConnectionOpenedContextType.md
@@ -0,0 +1,25 @@
+---
+title: MySqlConnectionOpenedContext
+---
+
+# MySqlConnectionOpenedContext class
+
+Contains information passed to [`MySqlConnectionOpenedCallback`](../MySqlConnectionOpenedCallbackType/) when a new [`MySqlConnection`](../MySqlConnectionType/) is opened.
+
+```csharp
+public sealed class MySqlConnectionOpenedContext
+```
+
+## Public Members
+
+| name | description |
+| --- | --- |
+| [Conditions](../MySqlConnectionOpenedContext/Conditions/) { get; } | Bitflags giving the conditions under which a connection was opened. |
+| [Connection](../MySqlConnectionOpenedContext/Connection/) { get; } | The [`MySqlConnection`](../MySqlConnectionType/) that was opened. |
+
+## See Also
+
+* namespace [MySqlConnector](../../MySqlConnectorNamespace/)
+* assembly [MySqlConnector](../../MySqlConnectorAssembly/)
+
+<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector/MySqlDataAdapter/CreateRowUpdatedEvent.md b/docs/content/api/MySqlConnector/MySqlDataAdapter/CreateRowUpdatedEvent.md
index 906472015..9076ec09c 100644
--- a/docs/content/api/MySqlConnector/MySqlDataAdapter/CreateRowUpdatedEvent.md
+++ b/docs/content/api/MySqlConnector/MySqlDataAdapter/CreateRowUpdatedEvent.md
@@ -5,7 +5,7 @@ title: MySqlDataAdapter.CreateRowUpdatedEvent method
 # MySqlDataAdapter.CreateRowUpdatedEvent method
 
 ```csharp
-protected override RowUpdatedEventArgs CreateRowUpdatedEvent(DataRow dataRow, IDbCommand command, 
+protected override RowUpdatedEventArgs CreateRowUpdatedEvent(DataRow dataRow, IDbCommand? command, 
     StatementType statementType, DataTableMapping tableMapping)
 ```
 
diff --git a/docs/content/api/MySqlConnector/MySqlDataAdapter/CreateRowUpdatingEvent.md b/docs/content/api/MySqlConnector/MySqlDataAdapter/CreateRowUpdatingEvent.md
index 08fd9e814..e63ede119 100644
--- a/docs/content/api/MySqlConnector/MySqlDataAdapter/CreateRowUpdatingEvent.md
+++ b/docs/content/api/MySqlConnector/MySqlDataAdapter/CreateRowUpdatingEvent.md
@@ -5,8 +5,8 @@ title: MySqlDataAdapter.CreateRowUpdatingEvent method
 # MySqlDataAdapter.CreateRowUpdatingEvent method
 
 ```csharp
-protected override RowUpdatingEventArgs CreateRowUpdatingEvent(DataRow dataRow, IDbCommand command, 
-    StatementType statementType, DataTableMapping tableMapping)
+protected override RowUpdatingEventArgs CreateRowUpdatingEvent(DataRow dataRow, 
+    IDbCommand? command, StatementType statementType, DataTableMapping tableMapping)
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlDataAdapter/DeleteCommand.md b/docs/content/api/MySqlConnector/MySqlDataAdapter/DeleteCommand.md
index c0a6b71ce..7f9bc22e4 100644
--- a/docs/content/api/MySqlConnector/MySqlDataAdapter/DeleteCommand.md
+++ b/docs/content/api/MySqlConnector/MySqlDataAdapter/DeleteCommand.md
@@ -5,7 +5,7 @@ title: MySqlDataAdapter.DeleteCommand property
 # MySqlDataAdapter.DeleteCommand property
 
 ```csharp
-public MySqlCommand DeleteCommand { get; set; }
+public MySqlCommand? DeleteCommand { get; set; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlDataAdapter/InsertCommand.md b/docs/content/api/MySqlConnector/MySqlDataAdapter/InsertCommand.md
index 93c209a25..21219b482 100644
--- a/docs/content/api/MySqlConnector/MySqlDataAdapter/InsertCommand.md
+++ b/docs/content/api/MySqlConnector/MySqlDataAdapter/InsertCommand.md
@@ -5,7 +5,7 @@ title: MySqlDataAdapter.InsertCommand property
 # MySqlDataAdapter.InsertCommand property
 
 ```csharp
-public MySqlCommand InsertCommand { get; set; }
+public MySqlCommand? InsertCommand { get; set; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlDataAdapter/RowUpdated.md b/docs/content/api/MySqlConnector/MySqlDataAdapter/RowUpdated.md
index 82ac73734..9d935180b 100644
--- a/docs/content/api/MySqlConnector/MySqlDataAdapter/RowUpdated.md
+++ b/docs/content/api/MySqlConnector/MySqlDataAdapter/RowUpdated.md
@@ -5,7 +5,7 @@ title: MySqlDataAdapter.RowUpdated event
 # MySqlDataAdapter.RowUpdated event
 
 ```csharp
-public event MySqlRowUpdatedEventHandler RowUpdated;
+public event MySqlRowUpdatedEventHandler? RowUpdated;
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlDataAdapter/RowUpdating.md b/docs/content/api/MySqlConnector/MySqlDataAdapter/RowUpdating.md
index ffba0ecca..bffe8ab2d 100644
--- a/docs/content/api/MySqlConnector/MySqlDataAdapter/RowUpdating.md
+++ b/docs/content/api/MySqlConnector/MySqlDataAdapter/RowUpdating.md
@@ -5,7 +5,7 @@ title: MySqlDataAdapter.RowUpdating event
 # MySqlDataAdapter.RowUpdating event
 
 ```csharp
-public event MySqlRowUpdatingEventHandler RowUpdating;
+public event MySqlRowUpdatingEventHandler? RowUpdating;
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlDataAdapter/SelectCommand.md b/docs/content/api/MySqlConnector/MySqlDataAdapter/SelectCommand.md
index 5235be275..83494c40a 100644
--- a/docs/content/api/MySqlConnector/MySqlDataAdapter/SelectCommand.md
+++ b/docs/content/api/MySqlConnector/MySqlDataAdapter/SelectCommand.md
@@ -5,7 +5,7 @@ title: MySqlDataAdapter.SelectCommand property
 # MySqlDataAdapter.SelectCommand property
 
 ```csharp
-public MySqlCommand SelectCommand { get; set; }
+public MySqlCommand? SelectCommand { get; set; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlDataAdapter/UpdateCommand.md b/docs/content/api/MySqlConnector/MySqlDataAdapter/UpdateCommand.md
index 7cd16a8f4..5be3729dd 100644
--- a/docs/content/api/MySqlConnector/MySqlDataAdapter/UpdateCommand.md
+++ b/docs/content/api/MySqlConnector/MySqlDataAdapter/UpdateCommand.md
@@ -5,7 +5,7 @@ title: MySqlDataAdapter.UpdateCommand property
 # MySqlDataAdapter.UpdateCommand property
 
 ```csharp
-public MySqlCommand UpdateCommand { get; set; }
+public MySqlCommand? UpdateCommand { get; set; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlDataReader/GetBytes.md b/docs/content/api/MySqlConnector/MySqlDataReader/GetBytes.md
index c9bd4bb4f..900426e76 100644
--- a/docs/content/api/MySqlConnector/MySqlDataReader/GetBytes.md
+++ b/docs/content/api/MySqlConnector/MySqlDataReader/GetBytes.md
@@ -5,7 +5,7 @@ title: MySqlDataReader.GetBytes methods
 # MySqlDataReader.GetBytes method (1 of 2)
 
 ```csharp
-public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, 
+public override long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOffset, 
     int length)
 ```
 
@@ -20,7 +20,7 @@ public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int b
 # MySqlDataReader.GetBytes method (2 of 2)
 
 ```csharp
-public long GetBytes(string name, long dataOffset, byte[] buffer, int bufferOffset, int length)
+public long GetBytes(string name, long dataOffset, byte[]? buffer, int bufferOffset, int length)
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlDataReader/GetChars.md b/docs/content/api/MySqlConnector/MySqlDataReader/GetChars.md
index ac652e534..3bf9914f6 100644
--- a/docs/content/api/MySqlConnector/MySqlDataReader/GetChars.md
+++ b/docs/content/api/MySqlConnector/MySqlDataReader/GetChars.md
@@ -5,7 +5,7 @@ title: MySqlDataReader.GetChars method
 # MySqlDataReader.GetChars method
 
 ```csharp
-public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, 
+public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int bufferOffset, 
     int length)
 ```
 
diff --git a/docs/content/api/MySqlConnector/MySqlDataReader/GetColumnSchemaAsync.md b/docs/content/api/MySqlConnector/MySqlDataReader/GetColumnSchemaAsync.md
index bfb54bcde..7145365c4 100644
--- a/docs/content/api/MySqlConnector/MySqlDataReader/GetColumnSchemaAsync.md
+++ b/docs/content/api/MySqlConnector/MySqlDataReader/GetColumnSchemaAsync.md
@@ -8,7 +8,7 @@ Returns metadata about the columns in the result set.
 
 ```csharp
 public override Task<ReadOnlyCollection<DbColumn>> GetColumnSchemaAsync(
-    CancellationToken cancellationToken = default(CancellationToken))
+    CancellationToken cancellationToken = default)
 ```
 
 | parameter | description |
diff --git a/docs/content/api/MySqlConnector/MySqlDataReader/GetSchemaTable.md b/docs/content/api/MySqlConnector/MySqlDataReader/GetSchemaTable.md
index b5c825ecb..daffb65bb 100644
--- a/docs/content/api/MySqlConnector/MySqlDataReader/GetSchemaTable.md
+++ b/docs/content/api/MySqlConnector/MySqlDataReader/GetSchemaTable.md
@@ -7,7 +7,7 @@ title: MySqlDataReader.GetSchemaTable method
 Returns a DataTable that contains metadata about the columns in the result set.
 
 ```csharp
-public override DataTable GetSchemaTable()
+public override DataTable? GetSchemaTable()
 ```
 
 ## Return Value
diff --git a/docs/content/api/MySqlConnector/MySqlDataReader/GetSchemaTableAsync.md b/docs/content/api/MySqlConnector/MySqlDataReader/GetSchemaTableAsync.md
index 153de15a4..05a27e481 100644
--- a/docs/content/api/MySqlConnector/MySqlDataReader/GetSchemaTableAsync.md
+++ b/docs/content/api/MySqlConnector/MySqlDataReader/GetSchemaTableAsync.md
@@ -7,8 +7,7 @@ title: MySqlDataReader.GetSchemaTableAsync method
 Returns a DataTable that contains metadata about the columns in the result set.
 
 ```csharp
-public override Task<DataTable> GetSchemaTableAsync(
-    CancellationToken cancellationToken = default(CancellationToken))
+public override Task<DataTable?> GetSchemaTableAsync(CancellationToken cancellationToken = default)
 ```
 
 | parameter | description |
diff --git a/docs/content/api/MySqlConnector/MySqlDataSource/OpenConnectionAsync.md b/docs/content/api/MySqlConnector/MySqlDataSource/OpenConnectionAsync.md
index 8edadf2eb..9b20554ef 100644
--- a/docs/content/api/MySqlConnector/MySqlDataSource/OpenConnectionAsync.md
+++ b/docs/content/api/MySqlConnector/MySqlDataSource/OpenConnectionAsync.md
@@ -7,8 +7,7 @@ title: MySqlDataSource.OpenConnectionAsync method
 Asynchronously returns a new, open [`MySqlConnection`](../../MySqlConnectionType/) to the database represented by this [`MySqlDataSource`](../../MySqlDataSourceType/).
 
 ```csharp
-public ValueTask<MySqlConnection> OpenConnectionAsync(
-    CancellationToken cancellationToken = default(CancellationToken))
+public ValueTask<MySqlConnection> OpenConnectionAsync(CancellationToken cancellationToken = default)
 ```
 
 | parameter | description |
diff --git a/docs/content/api/MySqlConnector/MySqlDataSource/Password.md b/docs/content/api/MySqlConnector/MySqlDataSource/Password.md
new file mode 100644
index 000000000..159c461cf
--- /dev/null
+++ b/docs/content/api/MySqlConnector/MySqlDataSource/Password.md
@@ -0,0 +1,25 @@
+---
+title: MySqlDataSource.Password property
+---
+
+# MySqlDataSource.Password property
+
+Sets the password that will be used by the next [`MySqlConnection`](../../MySqlConnectionType/) created from this [`MySqlDataSource`](../../MySqlDataSourceType/).
+
+```csharp
+public string Password { set; }
+```
+
+## Remarks
+
+This can be used to update the password for database servers that periodically rotate authentication tokens, without affecting connection pooling. The [`Password`](../../MySqlConnectionStringBuilder/Password/) property must not be specified in order for this field to be used.
+
+Consider using [`UsePeriodicPasswordProvider`](../../MySqlDataSourceBuilder/UsePeriodicPasswordProvider/) instead.
+
+## See Also
+
+* class [MySqlDataSource](../../MySqlDataSourceType/)
+* namespace [MySqlConnector](../../MySqlDataSourceType/)
+* assembly [MySqlConnector](../../../MySqlConnectorAssembly/)
+
+<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/Build.md b/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/Build.md
new file mode 100644
index 000000000..e8352a4e2
--- /dev/null
+++ b/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/Build.md
@@ -0,0 +1,24 @@
+---
+title: MySqlDataSourceBuilder.Build method
+---
+
+# MySqlDataSourceBuilder.Build method
+
+Builds a [`MySqlDataSource`](../../MySqlDataSourceType/) which is ready for use.
+
+```csharp
+public MySqlDataSource Build()
+```
+
+## Return Value
+
+A new [`MySqlDataSource`](../../MySqlDataSourceType/) with the settings configured through this [`MySqlDataSourceBuilder`](../../MySqlDataSourceBuilderType/).
+
+## See Also
+
+* class [MySqlDataSource](../../MySqlDataSourceType/)
+* class [MySqlDataSourceBuilder](../../MySqlDataSourceBuilderType/)
+* namespace [MySqlConnector](../../MySqlDataSourceBuilderType/)
+* assembly [MySqlConnector](../../../MySqlConnectorAssembly/)
+
+<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/ConnectionStringBuilder.md b/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/ConnectionStringBuilder.md
new file mode 100644
index 000000000..8884808c5
--- /dev/null
+++ b/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/ConnectionStringBuilder.md
@@ -0,0 +1,20 @@
+---
+title: MySqlDataSourceBuilder.ConnectionStringBuilder property
+---
+
+# MySqlDataSourceBuilder.ConnectionStringBuilder property
+
+A [`MySqlConnectionStringBuilder`](../../MySqlConnectionStringBuilderType/) that can be used to configure the connection string on this [`MySqlDataSourceBuilder`](../../MySqlDataSourceBuilderType/).
+
+```csharp
+public MySqlConnectionStringBuilder ConnectionStringBuilder { get; }
+```
+
+## See Also
+
+* class [MySqlConnectionStringBuilder](../../MySqlConnectionStringBuilderType/)
+* class [MySqlDataSourceBuilder](../../MySqlDataSourceBuilderType/)
+* namespace [MySqlConnector](../../MySqlDataSourceBuilderType/)
+* assembly [MySqlConnector](../../../MySqlConnectorAssembly/)
+
+<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/MySqlDataSourceBuilder.md b/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/MySqlDataSourceBuilder.md
new file mode 100644
index 000000000..f3f30d761
--- /dev/null
+++ b/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/MySqlDataSourceBuilder.md
@@ -0,0 +1,23 @@
+---
+title: MySqlDataSourceBuilder constructor
+---
+
+# MySqlDataSourceBuilder constructor
+
+Initializes a new [`MySqlDataSourceBuilder`](../../MySqlDataSourceBuilderType/) with the specified connection string.
+
+```csharp
+public MySqlDataSourceBuilder(string? connectionString = null)
+```
+
+| parameter | description |
+| --- | --- |
+| connectionString | The optional connection string to use. |
+
+## See Also
+
+* class [MySqlDataSourceBuilder](../../MySqlDataSourceBuilderType/)
+* namespace [MySqlConnector](../../MySqlDataSourceBuilderType/)
+* assembly [MySqlConnector](../../../MySqlConnectorAssembly/)
+
+<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/UseClientCertificatesCallback.md b/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/UseClientCertificatesCallback.md
new file mode 100644
index 000000000..d0fd09905
--- /dev/null
+++ b/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/UseClientCertificatesCallback.md
@@ -0,0 +1,28 @@
+---
+title: MySqlDataSourceBuilder.UseClientCertificatesCallback method
+---
+
+# MySqlDataSourceBuilder.UseClientCertificatesCallback method
+
+Sets the callback used to provide client certificates for connecting to a server.
+
+```csharp
+public MySqlDataSourceBuilder UseClientCertificatesCallback(
+    Func<X509CertificateCollection, ValueTask>? callback)
+```
+
+| parameter | description |
+| --- | --- |
+| callback | The callback that will provide client certificates. The X509CertificateCollection provided to the callback should be filled with the client certificate(s) needed to connect to the server. |
+
+## Return Value
+
+This builder, so that method calls can be chained.
+
+## See Also
+
+* class [MySqlDataSourceBuilder](../../MySqlDataSourceBuilderType/)
+* namespace [MySqlConnector](../../MySqlDataSourceBuilderType/)
+* assembly [MySqlConnector](../../../MySqlConnectorAssembly/)
+
+<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/UseConnectionOpenedCallback.md b/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/UseConnectionOpenedCallback.md
new file mode 100644
index 000000000..f0a71550d
--- /dev/null
+++ b/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/UseConnectionOpenedCallback.md
@@ -0,0 +1,28 @@
+---
+title: MySqlDataSourceBuilder.UseConnectionOpenedCallback method
+---
+
+# MySqlDataSourceBuilder.UseConnectionOpenedCallback method
+
+Adds a callback that is invoked when a new [`MySqlConnection`](../../MySqlConnectionType/) is opened.
+
+```csharp
+public MySqlDataSourceBuilder UseConnectionOpenedCallback(MySqlConnectionOpenedCallback callback)
+```
+
+| parameter | description |
+| --- | --- |
+| callback | The callback to invoke. |
+
+## Return Value
+
+This builder, so that method calls can be chained.
+
+## See Also
+
+* delegate [MySqlConnectionOpenedCallback](../../MySqlConnectionOpenedCallbackType/)
+* class [MySqlDataSourceBuilder](../../MySqlDataSourceBuilderType/)
+* namespace [MySqlConnector](../../MySqlDataSourceBuilderType/)
+* assembly [MySqlConnector](../../../MySqlConnectorAssembly/)
+
+<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/UseLoggerFactory.md b/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/UseLoggerFactory.md
new file mode 100644
index 000000000..019d7e9c9
--- /dev/null
+++ b/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/UseLoggerFactory.md
@@ -0,0 +1,27 @@
+---
+title: MySqlDataSourceBuilder.UseLoggerFactory method
+---
+
+# MySqlDataSourceBuilder.UseLoggerFactory method
+
+Sets the ILoggerFactory that will be used for logging.
+
+```csharp
+public MySqlDataSourceBuilder UseLoggerFactory(ILoggerFactory? loggerFactory)
+```
+
+| parameter | description |
+| --- | --- |
+| loggerFactory | The logger factory. |
+
+## Return Value
+
+This builder, so that method calls can be chained.
+
+## See Also
+
+* class [MySqlDataSourceBuilder](../../MySqlDataSourceBuilderType/)
+* namespace [MySqlConnector](../../MySqlDataSourceBuilderType/)
+* assembly [MySqlConnector](../../../MySqlConnectorAssembly/)
+
+<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/UseName.md b/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/UseName.md
new file mode 100644
index 000000000..0ffc7716b
--- /dev/null
+++ b/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/UseName.md
@@ -0,0 +1,31 @@
+---
+title: MySqlDataSourceBuilder.UseName method
+---
+
+# MySqlDataSourceBuilder.UseName method
+
+Sets the name of the [`MySqlDataSource`](../../MySqlDataSourceType/) that will be created.
+
+```csharp
+public MySqlDataSourceBuilder UseName(string? name)
+```
+
+| parameter | description |
+| --- | --- |
+| name | The data source name. |
+
+## Return Value
+
+This builder, so that method calls can be chained.
+
+## Remarks
+
+The connection pool name is used to set the `program_name` connection attribute (which is visible to some diagnostic tools) and the `pool.name` tag supplied with [connection pool metrics](https://mysqlconnector.net/diagnostics/metrics/).
+
+## See Also
+
+* class [MySqlDataSourceBuilder](../../MySqlDataSourceBuilderType/)
+* namespace [MySqlConnector](../../MySqlDataSourceBuilderType/)
+* assembly [MySqlConnector](../../../MySqlConnectorAssembly/)
+
+<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/UsePeriodicPasswordProvider.md b/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/UsePeriodicPasswordProvider.md
new file mode 100644
index 000000000..028339557
--- /dev/null
+++ b/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/UsePeriodicPasswordProvider.md
@@ -0,0 +1,32 @@
+---
+title: MySqlDataSourceBuilder.UsePeriodicPasswordProvider method
+---
+
+# MySqlDataSourceBuilder.UsePeriodicPasswordProvider method
+
+Configures a periodic password provider, which is automatically called by the data source at some regular interval. This is the recommended way to fetch a rotating access token.
+
+```csharp
+public MySqlDataSourceBuilder UsePeriodicPasswordProvider(
+    Func<MySqlProvidePasswordContext, CancellationToken, ValueTask<string>>? passwordProvider, 
+    TimeSpan successRefreshInterval, TimeSpan failureRefreshInterval)
+```
+
+| parameter | description |
+| --- | --- |
+| passwordProvider | A callback which returns the password to be used by any new MySQL connections that are made. |
+| successRefreshInterval | How long to cache the password before re-invoking the callback. |
+| failureRefreshInterval | How long to wait before re-invoking the callback on failure. This should typically be much shorter than *successRefreshInterval*. |
+
+## Return Value
+
+This builder, so that method calls can be chained.
+
+## See Also
+
+* class [MySqlProvidePasswordContext](../../MySqlProvidePasswordContextType/)
+* class [MySqlDataSourceBuilder](../../MySqlDataSourceBuilderType/)
+* namespace [MySqlConnector](../../MySqlDataSourceBuilderType/)
+* assembly [MySqlConnector](../../../MySqlConnectorAssembly/)
+
+<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/UseRemoteCertificateValidationCallback.md b/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/UseRemoteCertificateValidationCallback.md
new file mode 100644
index 000000000..c82769a02
--- /dev/null
+++ b/docs/content/api/MySqlConnector/MySqlDataSourceBuilder/UseRemoteCertificateValidationCallback.md
@@ -0,0 +1,32 @@
+---
+title: MySqlDataSourceBuilder.UseRemoteCertificateValidationCallback method
+---
+
+# MySqlDataSourceBuilder.UseRemoteCertificateValidationCallback method
+
+Sets the callback used to verify that the server's certificate is valid.
+
+```csharp
+public MySqlDataSourceBuilder UseRemoteCertificateValidationCallback(
+    RemoteCertificateValidationCallback callback)
+```
+
+| parameter | description |
+| --- | --- |
+| callback | The callback used to verify that the server's certificate is valid. |
+
+## Return Value
+
+This builder, so that method calls can be chained.
+
+## Remarks
+
+[`SslMode`](../../MySqlConnectionStringBuilder/SslMode/) must be set to Preferred or Required in order for this delegate to be invoked. See the documentation for RemoteCertificateValidationCallback for more information on the values passed to this delegate.
+
+## See Also
+
+* class [MySqlDataSourceBuilder](../../MySqlDataSourceBuilderType/)
+* namespace [MySqlConnector](../../MySqlDataSourceBuilderType/)
+* assembly [MySqlConnector](../../../MySqlConnectorAssembly/)
+
+<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector/MySqlDataSourceBuilderType.md b/docs/content/api/MySqlConnector/MySqlDataSourceBuilderType.md
new file mode 100644
index 000000000..e70fccbd3
--- /dev/null
+++ b/docs/content/api/MySqlConnector/MySqlDataSourceBuilderType.md
@@ -0,0 +1,32 @@
+---
+title: MySqlDataSourceBuilder
+---
+
+# MySqlDataSourceBuilder class
+
+[`MySqlDataSourceBuilder`](../MySqlDataSourceBuilderType/) provides an API for configuring and creating a [`MySqlDataSource`](../MySqlDataSourceType/), from which [`MySqlConnection`](../MySqlConnectionType/) objects can be obtained.
+
+```csharp
+public sealed class MySqlDataSourceBuilder
+```
+
+## Public Members
+
+| name | description |
+| --- | --- |
+| [MySqlDataSourceBuilder](../MySqlDataSourceBuilder/MySqlDataSourceBuilder/)(…) | Initializes a new [`MySqlDataSourceBuilder`](../MySqlDataSourceBuilderType/) with the specified connection string. |
+| [ConnectionStringBuilder](../MySqlDataSourceBuilder/ConnectionStringBuilder/) { get; } | A [`MySqlConnectionStringBuilder`](../MySqlConnectionStringBuilderType/) that can be used to configure the connection string on this [`MySqlDataSourceBuilder`](../MySqlDataSourceBuilderType/). |
+| [Build](../MySqlDataSourceBuilder/Build/)() | Builds a [`MySqlDataSource`](../MySqlDataSourceType/) which is ready for use. |
+| [UseClientCertificatesCallback](../MySqlDataSourceBuilder/UseClientCertificatesCallback/)(…) | Sets the callback used to provide client certificates for connecting to a server. |
+| [UseConnectionOpenedCallback](../MySqlDataSourceBuilder/UseConnectionOpenedCallback/)(…) | Adds a callback that is invoked when a new [`MySqlConnection`](../MySqlConnectionType/) is opened. |
+| [UseLoggerFactory](../MySqlDataSourceBuilder/UseLoggerFactory/)(…) | Sets the ILoggerFactory that will be used for logging. |
+| [UseName](../MySqlDataSourceBuilder/UseName/)(…) | Sets the name of the [`MySqlDataSource`](../MySqlDataSourceType/) that will be created. |
+| [UsePeriodicPasswordProvider](../MySqlDataSourceBuilder/UsePeriodicPasswordProvider/)(…) | Configures a periodic password provider, which is automatically called by the data source at some regular interval. This is the recommended way to fetch a rotating access token. |
+| [UseRemoteCertificateValidationCallback](../MySqlDataSourceBuilder/UseRemoteCertificateValidationCallback/)(…) | Sets the callback used to verify that the server's certificate is valid. |
+
+## See Also
+
+* namespace [MySqlConnector](../../MySqlConnectorNamespace/)
+* assembly [MySqlConnector](../../MySqlConnectorAssembly/)
+
+<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector/MySqlDataSourceType.md b/docs/content/api/MySqlConnector/MySqlDataSourceType.md
index 29211c5f4..439631a43 100644
--- a/docs/content/api/MySqlConnector/MySqlDataSourceType.md
+++ b/docs/content/api/MySqlConnector/MySqlDataSourceType.md
@@ -4,7 +4,7 @@ title: MySqlDataSource
 
 # MySqlDataSource class
 
-[`MySqlDataSource`](../MySqlDataSourceType/) implements a MySQL data source which can be used to obtain open connections, and against which commands can be executed directly.
+[`MySqlDataSource`](../MySqlDataSourceType/) implements a MySQL data source which can be used to obtain open connections.
 
 ```csharp
 public sealed class MySqlDataSource : DbDataSource
@@ -16,6 +16,7 @@ public sealed class MySqlDataSource : DbDataSource
 | --- | --- |
 | [MySqlDataSource](../MySqlDataSource/MySqlDataSource/)(…) | Initializes a new instance of the [`MySqlDataSource`](../MySqlDataSourceType/) class. |
 | override [ConnectionString](../MySqlDataSource/ConnectionString/) { get; } | Gets the connection string of the database represented by this [`MySqlDataSource`](../MySqlDataSourceType/). |
+| [Password](../MySqlDataSource/Password/) { set; } | Sets the password that will be used by the next [`MySqlConnection`](../MySqlConnectionType/) created from this [`MySqlDataSource`](../MySqlDataSourceType/). |
 | [CreateConnection](../MySqlDataSource/CreateConnection/)() | Creates a new [`MySqlConnection`](../MySqlConnectionType/) that can connect to the database represented by this [`MySqlDataSource`](../MySqlDataSourceType/). |
 | [OpenConnection](../MySqlDataSource/OpenConnection/)() | Returns a new, open [`MySqlConnection`](../MySqlConnectionType/) to the database represented by this [`MySqlDataSource`](../MySqlDataSourceType/). |
 | [OpenConnectionAsync](../MySqlDataSource/OpenConnectionAsync/)(…) | Asynchronously returns a new, open [`MySqlConnection`](../MySqlConnectionType/) to the database represented by this [`MySqlDataSource`](../MySqlDataSourceType/). |
diff --git a/docs/content/api/MySqlConnector/MySqlDateTime/Equals.md b/docs/content/api/MySqlConnector/MySqlDateTime/Equals.md
index eb2360bbd..c106c21fe 100644
--- a/docs/content/api/MySqlConnector/MySqlDateTime/Equals.md
+++ b/docs/content/api/MySqlConnector/MySqlDateTime/Equals.md
@@ -7,7 +7,7 @@ title: MySqlDateTime.Equals method
 Returns `true` if this [`MySqlDateTime`](../../MySqlDateTimeType/) is equal to *obj*.
 
 ```csharp
-public override bool Equals(object obj)
+public override bool Equals(object? obj)
 ```
 
 | parameter | description |
diff --git a/docs/content/api/MySqlConnector/MySqlDateTime/MySqlDateTime.md b/docs/content/api/MySqlConnector/MySqlDateTime/MySqlDateTime.md
index ba8829993..a43698a6d 100644
--- a/docs/content/api/MySqlConnector/MySqlDateTime/MySqlDateTime.md
+++ b/docs/content/api/MySqlConnector/MySqlDateTime/MySqlDateTime.md
@@ -44,7 +44,7 @@ public MySqlDateTime(MySqlDateTime other)
 
 # MySqlDateTime constructor (3 of 3)
 
-Initializes a new instance of [`MySqlDateTime`](../../MySqlDateTimeType/).
+Represents a MySQL date/time value. This type can be used to store `DATETIME` values such as `0000-00-00` that can be stored in MySQL (when [`AllowZeroDateTime`](../../MySqlConnectionStringBuilder/AllowZeroDateTime/) is true) but can't be stored in a DateTime value.
 
 ```csharp
 public MySqlDateTime(int year, int month, int day, int hour, int minute, int second, 
diff --git a/docs/content/api/MySqlConnector/MySqlDateTimeType.md b/docs/content/api/MySqlConnector/MySqlDateTimeType.md
index 957ea7e5a..f6de9366f 100644
--- a/docs/content/api/MySqlConnector/MySqlDateTimeType.md
+++ b/docs/content/api/MySqlConnector/MySqlDateTimeType.md
@@ -11,11 +11,21 @@ public struct MySqlDateTime : IComparable, IComparable<MySqlDateTime>, IConverti
     IEquatable<MySqlDateTime>
 ```
 
+| parameter | description |
+| --- | --- |
+| year | The year. |
+| month | The (one-based) month. |
+| day | The (one-based) day of the month. |
+| hour | The hour. |
+| minute | The minute. |
+| second | The second. |
+| microsecond | The microsecond. |
+
 ## Public Members
 
 | name | description |
 | --- | --- |
-| [MySqlDateTime](../MySqlDateTime/MySqlDateTime/)(…) | Initializes a new instance of [`MySqlDateTime`](../MySqlDateTimeType/). (3 constructors) |
+| [MySqlDateTime](../MySqlDateTime/MySqlDateTime/)(…) | Represents a MySQL date/time value. This type can be used to store `DATETIME` values such as `0000-00-00` that can be stored in MySQL (when [`AllowZeroDateTime`](../MySqlConnectionStringBuilder/AllowZeroDateTime/) is true) but can't be stored in a DateTime value. (3 constructors) |
 | [Day](../MySqlDateTime/Day/) { get; set; } | Gets or sets the day of the month. |
 | [Hour](../MySqlDateTime/Hour/) { get; set; } | Gets or sets the hour. |
 | [IsValidDateTime](../MySqlDateTime/IsValidDateTime/) { get; } | Returns `true` if this value is a valid DateTime. |
diff --git a/docs/content/api/MySqlConnector/MySqlEndOfStreamException/ExpectedByteCount.md b/docs/content/api/MySqlConnector/MySqlEndOfStreamException/ExpectedByteCount.md
new file mode 100644
index 000000000..aaf2b111a
--- /dev/null
+++ b/docs/content/api/MySqlConnector/MySqlEndOfStreamException/ExpectedByteCount.md
@@ -0,0 +1,17 @@
+---
+title: MySqlEndOfStreamException.ExpectedByteCount property
+---
+
+# MySqlEndOfStreamException.ExpectedByteCount property
+
+```csharp
+public int ExpectedByteCount { get; }
+```
+
+## See Also
+
+* class [MySqlEndOfStreamException](../../MySqlEndOfStreamExceptionType/)
+* namespace [MySqlConnector](../../MySqlEndOfStreamExceptionType/)
+* assembly [MySqlConnector](../../../MySqlConnectorAssembly/)
+
+<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector/MySqlEndOfStreamException/ReadByteCount.md b/docs/content/api/MySqlConnector/MySqlEndOfStreamException/ReadByteCount.md
new file mode 100644
index 000000000..37b51437f
--- /dev/null
+++ b/docs/content/api/MySqlConnector/MySqlEndOfStreamException/ReadByteCount.md
@@ -0,0 +1,17 @@
+---
+title: MySqlEndOfStreamException.ReadByteCount property
+---
+
+# MySqlEndOfStreamException.ReadByteCount property
+
+```csharp
+public int ReadByteCount { get; }
+```
+
+## See Also
+
+* class [MySqlEndOfStreamException](../../MySqlEndOfStreamExceptionType/)
+* namespace [MySqlConnector](../../MySqlEndOfStreamExceptionType/)
+* assembly [MySqlConnector](../../../MySqlConnectorAssembly/)
+
+<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector/MySqlEndOfStreamExceptionType.md b/docs/content/api/MySqlConnector/MySqlEndOfStreamExceptionType.md
new file mode 100644
index 000000000..a73fc79c4
--- /dev/null
+++ b/docs/content/api/MySqlConnector/MySqlEndOfStreamExceptionType.md
@@ -0,0 +1,23 @@
+---
+title: MySqlEndOfStreamException
+---
+
+# MySqlEndOfStreamException class
+
+```csharp
+public sealed class MySqlEndOfStreamException : EndOfStreamException
+```
+
+## Public Members
+
+| name | description |
+| --- | --- |
+| [ExpectedByteCount](../MySqlEndOfStreamException/ExpectedByteCount/) { get; } |  |
+| [ReadByteCount](../MySqlEndOfStreamException/ReadByteCount/) { get; } |  |
+
+## See Also
+
+* namespace [MySqlConnector](../../MySqlConnectorNamespace/)
+* assembly [MySqlConnector](../../MySqlConnectorAssembly/)
+
+<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector/MySqlException/GetObjectData.md b/docs/content/api/MySqlConnector/MySqlException/GetObjectData.md
deleted file mode 100644
index 0c7e55da9..000000000
--- a/docs/content/api/MySqlConnector/MySqlException/GetObjectData.md
+++ /dev/null
@@ -1,24 +0,0 @@
----
-title: MySqlException.GetObjectData method
----
-
-# MySqlException.GetObjectData method
-
-Sets the SerializationInfo with information about the exception.
-
-```csharp
-public override void GetObjectData(SerializationInfo info, StreamingContext context)
-```
-
-| parameter | description |
-| --- | --- |
-| info | The SerializationInfo that will be set. |
-| context | The context. |
-
-## See Also
-
-* class [MySqlException](../../MySqlExceptionType/)
-* namespace [MySqlConnector](../../MySqlExceptionType/)
-* assembly [MySqlConnector](../../../MySqlConnectorAssembly/)
-
-<!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnector/MySqlException/SqlState.md b/docs/content/api/MySqlConnector/MySqlException/SqlState.md
index 0ce40043a..06c5dbc0b 100644
--- a/docs/content/api/MySqlConnector/MySqlException/SqlState.md
+++ b/docs/content/api/MySqlConnector/MySqlException/SqlState.md
@@ -7,7 +7,7 @@ title: MySqlException.SqlState property
 A `SQLSTATE` code identifying the kind of error.
 
 ```csharp
-public override string SqlState { get; }
+public override string? SqlState { get; }
 ```
 
 ## Remarks
diff --git a/docs/content/api/MySqlConnector/MySqlExceptionType.md b/docs/content/api/MySqlConnector/MySqlExceptionType.md
index bf1fab6ba..a079e5179 100644
--- a/docs/content/api/MySqlConnector/MySqlExceptionType.md
+++ b/docs/content/api/MySqlConnector/MySqlExceptionType.md
@@ -19,7 +19,6 @@ public sealed class MySqlException : DbException
 | override [IsTransient](../MySqlException/IsTransient/) { get; } | Returns `true` if this exception could indicate a transient error condition (that could succeed if retried); otherwise, `false`. |
 | [Number](../MySqlException/Number/) { get; } | A [`MySqlErrorCode`](../MySqlErrorCodeType/) value identifying the kind of error. Prefer to use the [`ErrorCode`](../MySqlException/ErrorCode/) property. |
 | override [SqlState](../MySqlException/SqlState/) { get; } | A `SQLSTATE` code identifying the kind of error. |
-| override [GetObjectData](../MySqlException/GetObjectData/)(…) | Sets the SerializationInfo with information about the exception. |
 
 ## See Also
 
diff --git a/docs/content/api/MySqlConnector/MySqlParameter/MySqlParameter.md b/docs/content/api/MySqlConnector/MySqlParameter/MySqlParameter.md
index 4d0b97c85..bdab728b6 100644
--- a/docs/content/api/MySqlConnector/MySqlParameter/MySqlParameter.md
+++ b/docs/content/api/MySqlConnector/MySqlParameter/MySqlParameter.md
@@ -36,7 +36,7 @@ public MySqlParameter(string name, MySqlDbType mySqlDbType)
 # MySqlParameter constructor (3 of 6)
 
 ```csharp
-public MySqlParameter(string name, object value)
+public MySqlParameter(string? name, object? value)
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlParameter/Value.md b/docs/content/api/MySqlConnector/MySqlParameter/Value.md
index 9858c9890..71785123c 100644
--- a/docs/content/api/MySqlConnector/MySqlParameter/Value.md
+++ b/docs/content/api/MySqlConnector/MySqlParameter/Value.md
@@ -5,7 +5,7 @@ title: MySqlParameter.Value property
 # MySqlParameter.Value property
 
 ```csharp
-public override object Value { get; set; }
+public override object? Value { get; set; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlParameterCollection/AddWithValue.md b/docs/content/api/MySqlConnector/MySqlParameterCollection/AddWithValue.md
index a7caf9fe1..9b3b806ce 100644
--- a/docs/content/api/MySqlConnector/MySqlParameterCollection/AddWithValue.md
+++ b/docs/content/api/MySqlConnector/MySqlParameterCollection/AddWithValue.md
@@ -5,7 +5,7 @@ title: MySqlParameterCollection.AddWithValue method
 # MySqlParameterCollection.AddWithValue method
 
 ```csharp
-public MySqlParameter AddWithValue(string parameterName, object value)
+public MySqlParameter AddWithValue(string parameterName, object? value)
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlRowUpdatedEventArgs/Command.md b/docs/content/api/MySqlConnector/MySqlRowUpdatedEventArgs/Command.md
index d0907f3ca..3a57e4d1b 100644
--- a/docs/content/api/MySqlConnector/MySqlRowUpdatedEventArgs/Command.md
+++ b/docs/content/api/MySqlConnector/MySqlRowUpdatedEventArgs/Command.md
@@ -5,7 +5,7 @@ title: MySqlRowUpdatedEventArgs.Command property
 # MySqlRowUpdatedEventArgs.Command property
 
 ```csharp
-public MySqlCommand Command { get; }
+public MySqlCommand? Command { get; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlRowUpdatedEventArgs/MySqlRowUpdatedEventArgs.md b/docs/content/api/MySqlConnector/MySqlRowUpdatedEventArgs/MySqlRowUpdatedEventArgs.md
index 4ef67b3b0..8ff21dd06 100644
--- a/docs/content/api/MySqlConnector/MySqlRowUpdatedEventArgs/MySqlRowUpdatedEventArgs.md
+++ b/docs/content/api/MySqlConnector/MySqlRowUpdatedEventArgs/MySqlRowUpdatedEventArgs.md
@@ -5,7 +5,7 @@ title: MySqlRowUpdatedEventArgs constructor
 # MySqlRowUpdatedEventArgs constructor
 
 ```csharp
-public MySqlRowUpdatedEventArgs(DataRow row, IDbCommand command, StatementType statementType, 
+public MySqlRowUpdatedEventArgs(DataRow row, IDbCommand? command, StatementType statementType, 
     DataTableMapping tableMapping)
 ```
 
diff --git a/docs/content/api/MySqlConnector/MySqlRowUpdatingEventArgs/Command.md b/docs/content/api/MySqlConnector/MySqlRowUpdatingEventArgs/Command.md
index dceb0c842..41d506a55 100644
--- a/docs/content/api/MySqlConnector/MySqlRowUpdatingEventArgs/Command.md
+++ b/docs/content/api/MySqlConnector/MySqlRowUpdatingEventArgs/Command.md
@@ -5,7 +5,7 @@ title: MySqlRowUpdatingEventArgs.Command property
 # MySqlRowUpdatingEventArgs.Command property
 
 ```csharp
-public MySqlCommand Command { get; }
+public MySqlCommand? Command { get; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlRowUpdatingEventArgs/MySqlRowUpdatingEventArgs.md b/docs/content/api/MySqlConnector/MySqlRowUpdatingEventArgs/MySqlRowUpdatingEventArgs.md
index 8d1e1b384..31d49b4b0 100644
--- a/docs/content/api/MySqlConnector/MySqlRowUpdatingEventArgs/MySqlRowUpdatingEventArgs.md
+++ b/docs/content/api/MySqlConnector/MySqlRowUpdatingEventArgs/MySqlRowUpdatingEventArgs.md
@@ -5,7 +5,7 @@ title: MySqlRowUpdatingEventArgs constructor
 # MySqlRowUpdatingEventArgs constructor
 
 ```csharp
-public MySqlRowUpdatingEventArgs(DataRow row, IDbCommand command, StatementType statementType, 
+public MySqlRowUpdatingEventArgs(DataRow row, IDbCommand? command, StatementType statementType, 
     DataTableMapping tableMapping)
 ```
 
diff --git a/docs/content/api/MySqlConnector/MySqlTransaction/CommitAsync.md b/docs/content/api/MySqlConnector/MySqlTransaction/CommitAsync.md
index 0c738180a..0eb3b9519 100644
--- a/docs/content/api/MySqlConnector/MySqlTransaction/CommitAsync.md
+++ b/docs/content/api/MySqlConnector/MySqlTransaction/CommitAsync.md
@@ -7,7 +7,7 @@ title: MySqlTransaction.CommitAsync method
 Asynchronously commits the database transaction.
 
 ```csharp
-public override Task CommitAsync(CancellationToken cancellationToken = default(CancellationToken))
+public override Task CommitAsync(CancellationToken cancellationToken = default)
 ```
 
 | parameter | description |
diff --git a/docs/content/api/MySqlConnector/MySqlTransaction/Connection.md b/docs/content/api/MySqlConnector/MySqlTransaction/Connection.md
index d57deae03..795159c21 100644
--- a/docs/content/api/MySqlConnector/MySqlTransaction/Connection.md
+++ b/docs/content/api/MySqlConnector/MySqlTransaction/Connection.md
@@ -7,7 +7,7 @@ title: MySqlTransaction.Connection property
 Gets the [`MySqlConnection`](../../MySqlConnectionType/) that this transaction is associated with.
 
 ```csharp
-public MySqlConnection Connection { get; }
+public MySqlConnection? Connection { get; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlTransaction/DbConnection.md b/docs/content/api/MySqlConnector/MySqlTransaction/DbConnection.md
index c6578f58f..8a19d6126 100644
--- a/docs/content/api/MySqlConnector/MySqlTransaction/DbConnection.md
+++ b/docs/content/api/MySqlConnector/MySqlTransaction/DbConnection.md
@@ -7,7 +7,7 @@ title: MySqlTransaction.DbConnection property
 Gets the [`MySqlConnection`](../../MySqlConnectionType/) that this transaction is associated with.
 
 ```csharp
-protected override DbConnection DbConnection { get; }
+protected override DbConnection? DbConnection { get; }
 ```
 
 ## See Also
diff --git a/docs/content/api/MySqlConnector/MySqlTransaction/ReleaseAsync.md b/docs/content/api/MySqlConnector/MySqlTransaction/ReleaseAsync.md
index 3a0b070a9..7ed4c792c 100644
--- a/docs/content/api/MySqlConnector/MySqlTransaction/ReleaseAsync.md
+++ b/docs/content/api/MySqlConnector/MySqlTransaction/ReleaseAsync.md
@@ -8,7 +8,7 @@ Asynchronously removes the named transaction savepoint with the specified *savep
 
 ```csharp
 public override Task ReleaseAsync(string savepointName, 
-    CancellationToken cancellationToken = default(CancellationToken))
+    CancellationToken cancellationToken = default)
 ```
 
 | parameter | description |
diff --git a/docs/content/api/MySqlConnector/MySqlTransaction/RollbackAsync.md b/docs/content/api/MySqlConnector/MySqlTransaction/RollbackAsync.md
index 869816ab5..49f2784e8 100644
--- a/docs/content/api/MySqlConnector/MySqlTransaction/RollbackAsync.md
+++ b/docs/content/api/MySqlConnector/MySqlTransaction/RollbackAsync.md
@@ -7,7 +7,7 @@ title: MySqlTransaction.RollbackAsync methods
 Asynchronously rolls back the database transaction.
 
 ```csharp
-public override Task RollbackAsync(CancellationToken cancellationToken = default(CancellationToken))
+public override Task RollbackAsync(CancellationToken cancellationToken = default)
 ```
 
 | parameter | description |
@@ -32,7 +32,7 @@ Asynchronously rolls back the current transaction to the savepoint with the spec
 
 ```csharp
 public override Task RollbackAsync(string savepointName, 
-    CancellationToken cancellationToken = default(CancellationToken))
+    CancellationToken cancellationToken = default)
 ```
 
 | parameter | description |
diff --git a/docs/content/api/MySqlConnector/MySqlTransaction/SaveAsync.md b/docs/content/api/MySqlConnector/MySqlTransaction/SaveAsync.md
index e097192b2..255e23577 100644
--- a/docs/content/api/MySqlConnector/MySqlTransaction/SaveAsync.md
+++ b/docs/content/api/MySqlConnector/MySqlTransaction/SaveAsync.md
@@ -7,8 +7,7 @@ title: MySqlTransaction.SaveAsync method
 Asynchronously sets a named transaction savepoint with the specified *savepointName*. If the current transaction already has a savepoint with the same name, the old savepoint is deleted and a new one is set.
 
 ```csharp
-public override Task SaveAsync(string savepointName, 
-    CancellationToken cancellationToken = default(CancellationToken))
+public override Task SaveAsync(string savepointName, CancellationToken cancellationToken = default)
 ```
 
 | parameter | description |
diff --git a/docs/content/api/MySqlConnectorAssembly.md b/docs/content/api/MySqlConnectorAssembly.md
index 035b4362d..f03a48bee 100644
--- a/docs/content/api/MySqlConnectorAssembly.md
+++ b/docs/content/api/MySqlConnectorAssembly.md
@@ -23,18 +23,23 @@ title: MySqlConnector
 | class [MySqlCommand](../MySqlConnector/MySqlCommandType/) | [`MySqlCommand`](../MySqlConnector/MySqlCommandType/) represents a SQL statement or stored procedure name to execute against a MySQL database. |
 | class [MySqlCommandBuilder](../MySqlConnector/MySqlCommandBuilderType/) |  |
 | class [MySqlConnection](../MySqlConnector/MySqlConnectionType/) | [`MySqlConnection`](../MySqlConnector/MySqlConnectionType/) represents a connection to a MySQL database. |
+| delegate [MySqlConnectionOpenedCallback](../MySqlConnector/MySqlConnectionOpenedCallbackType/) | A callback that is invoked when a new [`MySqlConnection`](../MySqlConnector/MySqlConnectionType/) is opened. |
+| [Flags] enum [MySqlConnectionOpenedConditions](../MySqlConnector/MySqlConnectionOpenedConditionsType/) | Bitflags giving the conditions under which a connection was opened. |
+| class [MySqlConnectionOpenedContext](../MySqlConnector/MySqlConnectionOpenedContextType/) | Contains information passed to [`MySqlConnectionOpenedCallback`](../MySqlConnector/MySqlConnectionOpenedCallbackType/) when a new [`MySqlConnection`](../MySqlConnector/MySqlConnectionType/) is opened. |
 | enum [MySqlConnectionProtocol](../MySqlConnector/MySqlConnectionProtocolType/) | Specifies the type of connection to make to the server. |
 | class [MySqlConnectionStringBuilder](../MySqlConnector/MySqlConnectionStringBuilderType/) | [`MySqlConnectionStringBuilder`](../MySqlConnector/MySqlConnectionStringBuilderType/) allows you to construct a MySQL connection string by setting properties on the builder then reading the ConnectionString property. |
 | class [MySqlConnectorFactory](../MySqlConnector/MySqlConnectorFactoryType/) | An implementation of DbProviderFactory that creates MySqlConnector objects. |
 | class [MySqlConversionException](../MySqlConnector/MySqlConversionExceptionType/) | [`MySqlConversionException`](../MySqlConnector/MySqlConversionExceptionType/) is thrown when a MySQL value can't be converted to another type. |
 | class [MySqlDataAdapter](../MySqlConnector/MySqlDataAdapterType/) |  |
 | class [MySqlDataReader](../MySqlConnector/MySqlDataReaderType/) |  |
-| class [MySqlDataSource](../MySqlConnector/MySqlDataSourceType/) | [`MySqlDataSource`](../MySqlConnector/MySqlDataSourceType/) implements a MySQL data source which can be used to obtain open connections, and against which commands can be executed directly. |
+| class [MySqlDataSource](../MySqlConnector/MySqlDataSourceType/) | [`MySqlDataSource`](../MySqlConnector/MySqlDataSourceType/) implements a MySQL data source which can be used to obtain open connections. |
+| class [MySqlDataSourceBuilder](../MySqlConnector/MySqlDataSourceBuilderType/) | [`MySqlDataSourceBuilder`](../MySqlConnector/MySqlDataSourceBuilderType/) provides an API for configuring and creating a [`MySqlDataSource`](../MySqlConnector/MySqlDataSourceType/), from which [`MySqlConnection`](../MySqlConnector/MySqlConnectionType/) objects can be obtained. |
 | struct [MySqlDateTime](../MySqlConnector/MySqlDateTimeType/) | Represents a MySQL date/time value. This type can be used to store `DATETIME` values such as `0000-00-00` that can be stored in MySQL (when [`AllowZeroDateTime`](../MySqlConnector/MySqlConnectionStringBuilder/AllowZeroDateTime/) is true) but can't be stored in a DateTime value. |
 | enum [MySqlDateTimeKind](../MySqlConnector/MySqlDateTimeKindType/) | The DateTimeKind used when reading DateTime from the database. |
 | class [MySqlDbColumn](../MySqlConnector/MySqlDbColumnType/) |  |
 | enum [MySqlDbType](../MySqlConnector/MySqlDbTypeType/) |  |
 | struct [MySqlDecimal](../MySqlConnector/MySqlDecimalType/) | [`MySqlDecimal`](../MySqlConnector/MySqlDecimalType/) represents a MySQL `DECIMAL` value that is too large to fit in a .NET Decimal. |
+| class [MySqlEndOfStreamException](../MySqlConnector/MySqlEndOfStreamExceptionType/) |  |
 | class [MySqlError](../MySqlConnector/MySqlErrorType/) | [`MySqlError`](../MySqlConnector/MySqlErrorType/) represents an error or warning that occurred during the execution of a SQL statement. |
 | enum [MySqlErrorCode](../MySqlConnector/MySqlErrorCodeType/) | MySQL Server error codes. Taken from [Server Error Codes and Messages](https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html). |
 | class [MySqlException](../MySqlConnector/MySqlExceptionType/) | [`MySqlException`](../MySqlConnector/MySqlExceptionType/) is thrown when MySQL Server returns an error code, or there is a communication error with the server. |
@@ -64,6 +69,7 @@ title: MySqlConnector
 | --- | --- |
 | static class [AuthenticationPlugins](../MySqlConnector.Authentication/AuthenticationPluginsType/) | A registry of known authentication plugins. |
 | interface [IAuthenticationPlugin](../MySqlConnector.Authentication/IAuthenticationPluginType/) | The primary interface implemented by an authentication plugin. |
+| interface [IAuthenticationPlugin2](../MySqlConnector.Authentication/IAuthenticationPlugin2Type/) | [`IAuthenticationPlugin2`](../MySqlConnector.Authentication/IAuthenticationPlugin2Type/) is an extension to [`IAuthenticationPlugin`](../MySqlConnector.Authentication/IAuthenticationPluginType/) that returns a hash of the client's password. |
 
 ## MySqlConnector.Logging namespace
 
diff --git a/docs/content/api/MySqlConnectorAuthenticationNamespace.md b/docs/content/api/MySqlConnectorAuthenticationNamespace.md
index fb40ce5dd..4f76be588 100644
--- a/docs/content/api/MySqlConnectorAuthenticationNamespace.md
+++ b/docs/content/api/MySqlConnectorAuthenticationNamespace.md
@@ -8,5 +8,6 @@ title: MySqlConnector.Authentication
 | --- | --- |
 | static class [AuthenticationPlugins](../MySqlConnector.Authentication/AuthenticationPluginsType/) | A registry of known authentication plugins. |
 | interface [IAuthenticationPlugin](../MySqlConnector.Authentication/IAuthenticationPluginType/) | The primary interface implemented by an authentication plugin. |
+| interface [IAuthenticationPlugin2](../MySqlConnector.Authentication/IAuthenticationPlugin2Type/) | [`IAuthenticationPlugin2`](../MySqlConnector.Authentication/IAuthenticationPlugin2Type/) is an extension to [`IAuthenticationPlugin`](../MySqlConnector.Authentication/IAuthenticationPluginType/) that returns a hash of the client's password. |
 
 <!-- DO NOT EDIT: generated by xmldocmd for MySqlConnector.dll -->
diff --git a/docs/content/api/MySqlConnectorNamespace.md b/docs/content/api/MySqlConnectorNamespace.md
index 83238b378..5c9eaa40b 100644
--- a/docs/content/api/MySqlConnectorNamespace.md
+++ b/docs/content/api/MySqlConnectorNamespace.md
@@ -21,18 +21,23 @@ title: MySqlConnector
 | class [MySqlCommand](../MySqlConnector/MySqlCommandType/) | [`MySqlCommand`](../MySqlConnector/MySqlCommandType/) represents a SQL statement or stored procedure name to execute against a MySQL database. |
 | class [MySqlCommandBuilder](../MySqlConnector/MySqlCommandBuilderType/) |  |
 | class [MySqlConnection](../MySqlConnector/MySqlConnectionType/) | [`MySqlConnection`](../MySqlConnector/MySqlConnectionType/) represents a connection to a MySQL database. |
+| delegate [MySqlConnectionOpenedCallback](../MySqlConnector/MySqlConnectionOpenedCallbackType/) | A callback that is invoked when a new [`MySqlConnection`](../MySqlConnector/MySqlConnectionType/) is opened. |
+| [Flags] enum [MySqlConnectionOpenedConditions](../MySqlConnector/MySqlConnectionOpenedConditionsType/) | Bitflags giving the conditions under which a connection was opened. |
+| class [MySqlConnectionOpenedContext](../MySqlConnector/MySqlConnectionOpenedContextType/) | Contains information passed to [`MySqlConnectionOpenedCallback`](../MySqlConnector/MySqlConnectionOpenedCallbackType/) when a new [`MySqlConnection`](../MySqlConnector/MySqlConnectionType/) is opened. |
 | enum [MySqlConnectionProtocol](../MySqlConnector/MySqlConnectionProtocolType/) | Specifies the type of connection to make to the server. |
 | class [MySqlConnectionStringBuilder](../MySqlConnector/MySqlConnectionStringBuilderType/) | [`MySqlConnectionStringBuilder`](../MySqlConnector/MySqlConnectionStringBuilderType/) allows you to construct a MySQL connection string by setting properties on the builder then reading the ConnectionString property. |
 | class [MySqlConnectorFactory](../MySqlConnector/MySqlConnectorFactoryType/) | An implementation of DbProviderFactory that creates MySqlConnector objects. |
 | class [MySqlConversionException](../MySqlConnector/MySqlConversionExceptionType/) | [`MySqlConversionException`](../MySqlConnector/MySqlConversionExceptionType/) is thrown when a MySQL value can't be converted to another type. |
 | class [MySqlDataAdapter](../MySqlConnector/MySqlDataAdapterType/) |  |
 | class [MySqlDataReader](../MySqlConnector/MySqlDataReaderType/) |  |
-| class [MySqlDataSource](../MySqlConnector/MySqlDataSourceType/) | [`MySqlDataSource`](../MySqlConnector/MySqlDataSourceType/) implements a MySQL data source which can be used to obtain open connections, and against which commands can be executed directly. |
+| class [MySqlDataSource](../MySqlConnector/MySqlDataSourceType/) | [`MySqlDataSource`](../MySqlConnector/MySqlDataSourceType/) implements a MySQL data source which can be used to obtain open connections. |
+| class [MySqlDataSourceBuilder](../MySqlConnector/MySqlDataSourceBuilderType/) | [`MySqlDataSourceBuilder`](../MySqlConnector/MySqlDataSourceBuilderType/) provides an API for configuring and creating a [`MySqlDataSource`](../MySqlConnector/MySqlDataSourceType/), from which [`MySqlConnection`](../MySqlConnector/MySqlConnectionType/) objects can be obtained. |
 | struct [MySqlDateTime](../MySqlConnector/MySqlDateTimeType/) | Represents a MySQL date/time value. This type can be used to store `DATETIME` values such as `0000-00-00` that can be stored in MySQL (when [`AllowZeroDateTime`](../MySqlConnector/MySqlConnectionStringBuilder/AllowZeroDateTime/) is true) but can't be stored in a DateTime value. |
 | enum [MySqlDateTimeKind](../MySqlConnector/MySqlDateTimeKindType/) | The DateTimeKind used when reading DateTime from the database. |
 | class [MySqlDbColumn](../MySqlConnector/MySqlDbColumnType/) |  |
 | enum [MySqlDbType](../MySqlConnector/MySqlDbTypeType/) |  |
 | struct [MySqlDecimal](../MySqlConnector/MySqlDecimalType/) | [`MySqlDecimal`](../MySqlConnector/MySqlDecimalType/) represents a MySQL `DECIMAL` value that is too large to fit in a .NET Decimal. |
+| class [MySqlEndOfStreamException](../MySqlConnector/MySqlEndOfStreamExceptionType/) |  |
 | class [MySqlError](../MySqlConnector/MySqlErrorType/) | [`MySqlError`](../MySqlConnector/MySqlErrorType/) represents an error or warning that occurred during the execution of a SQL statement. |
 | enum [MySqlErrorCode](../MySqlConnector/MySqlErrorCodeType/) | MySQL Server error codes. Taken from [Server Error Codes and Messages](https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html). |
 | class [MySqlException](../MySqlConnector/MySqlExceptionType/) | [`MySqlException`](../MySqlConnector/MySqlExceptionType/) is thrown when MySQL Server returns an error code, or there is a communication error with the server. |
diff --git a/docs/content/connection-options.md b/docs/content/connection-options.md
index 669d6d29f..3f822ed57 100644
--- a/docs/content/connection-options.md
+++ b/docs/content/connection-options.md
@@ -1,8 +1,9 @@
 ---
-lastmod: 2022-08-13
+lastmod: 2024-01-22
 date: 2016-10-16
 title: Connection Options
-customtitle: MySQL Connection String for C# .NET Core Programs
+customtitle: MySQL Connection String Options for .NET/C#
+description: A MySqlConnection is configured using a connection string. The connection string options are key/value pairs described on this page.
 weight: 30
 menu:
   main:
@@ -228,7 +229,7 @@ Connection pooling is enabled by default. These options are used to configure it
     <td>The minimum number of connections to leave in the pool if ConnectionIdleTimeout is reached.</td>
   </tr>
   <tr id="MaximumPoolSize">
-    <td>Maximum Pool Size, Max Pool Size, MaximumPoolsize, maxpoolsize</td>
+    <td>Maximum Pool Size, Max Pool Size, MaximumPoolSize, maxpoolsize</td>
     <td>100</td>
     <td>The maximum number of connections allowed in the pool.</td>
   </tr>
@@ -285,8 +286,13 @@ These are the other options that MySqlConnector supports. They are set to sensib
   <tr id="ApplicationName">
     <td>Application Name, ApplicationName</td>
     <td>null</td>
-    <td>Sets the <c>program_name</c> connection attribute passed to MySQL Server. This value may be displayed by diagnostic tools,
-    e.g., as the “Program” column in “Client Connections” in <a href="https://www.mysql.com/products/workbench/">MySQL Workbench</a>.</td>
+    <td><p>Sets the <c>program_name</c> connection attribute passed to MySQL Server. This value may be displayed by diagnostic tools,
+    e.g., as the “Program” column in “Client Connections” in <a href="https://www.mysql.com/products/workbench/">MySQL Workbench</a>.
+    It also sets the connection pool name reported by the <code>pool.name</code> tag associated with connection pool metrics.</p>
+    <p>This connection string option is deprecated and is provided for backwards compatibility. Newer applications should
+    use <a href="/api/MySqlConnector/MySqlDataSourceBuilder/UseName/"><code>MySqlDataSourceBuilder.UseName</code></a> instead.
+    </p></td>
+    </td>
   </tr>
   <tr id="AutoEnlist">
     <td>Auto Enlist, AutoEnlist</td>
@@ -366,7 +372,7 @@ These are the other options that MySqlConnector supports. They are set to sensib
   <tr id="InteractiveSession">
     <td>Interactive Session, Interactive, InteractiveSession</td>
     <td>false</td>
-    <td>If <code>true</code>, the session <code>wait_timeout</code> variable is initialized from the global <code>interactive_timeout<code> value instead of the global <code>wait_timeout</code> value.</td>
+    <td>If <code>true</code>, the session <code>wait_timeout</code> variable is initialized from the global <code>interactive_timeout</code> value instead of the global <code>wait_timeout</code> value.</td>
   </tr>
   <tr id="Keepalive">
     <td>Keep Alive, Keepalive</td>
@@ -508,6 +514,11 @@ from your connection string when migrating from Connector/NET to MySqlConnector.
     <td>false</td>
     <td>Use <a href="/overview/logging/">MySqlConnector logging</a> (which is more flexible) instead.</td>
   </tr>
+  <tr id="OldGetStringBehavior">
+    <td>OldGetStringBehavior</td>
+    <td>false</td>
+    <td>This option is temporary in Connector/NET and unsupported in MySqlConnector.</td>
+  </tr>
   <tr id="OldSyntax">
     <td>OldSyntax, Old Syntax, UseOldSyntax, Use Old Syntax</td>
     <td>false</td>
@@ -541,7 +552,7 @@ from your connection string when migrating from Connector/NET to MySqlConnector.
   <tr id="SshHostName">
     <td>SshHostName, SshPort, SshUserName, SshPassword, SshKeyFile, SshPassPhrase</td>
     <td></td>
-    <td>Connecting via SSH isn’t built into MySqlConnector, but can be set up easily by following <a href="../tutorials/connect-ssh/" title="Connecting to MySQL Server with SSH from C#">these instructions</a>.</td>
+    <td>Connecting via SSH isn’t built into MySqlConnector, but can be set up easily by following <a href="/tutorials/connect-ssh/" title="Connecting to MySQL Server with SSH from C#">these instructions</a>.</td>
   </tr>
   <tr id="SqlServerMode">
     <td>SqlServerMode, Sql Server Mode</td>
diff --git a/docs/content/diagnostics/logging.md b/docs/content/diagnostics/logging.md
new file mode 100644
index 000000000..f6f7d6f02
--- /dev/null
+++ b/docs/content/diagnostics/logging.md
@@ -0,0 +1,184 @@
+---
+lastmod: 2023-11-11
+date: 2018-01-20
+menu:
+  main:
+    parent: diagnostics
+title: Logging
+weight: 10
+---
+
+# Logging
+
+MySqlConnector 2.3.0 and later supports logging through the standard [Microsoft.Extensions.Logging](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging) interfaces.
+
+## Console Programs
+
+To set up logging in MySqlConnector, create your `ILoggerFactory` as usual, and then configure a `MySqlDataSource` with it.
+Any use of connections handed out by the data source will log via your provided logger factory.
+
+The following shows a minimal console application logging to the console via [Microsoft.Extensions.Logging.Console](https://www.nuget.org/packages/Microsoft.Extensions.Logging.Console):
+
+```csharp
+using Microsoft.Extensions.Logging;
+using MySqlConnector;
+
+// Create a Microsoft.Extensions.Logging LoggerFactory, configuring it with the providers,
+// log levels and other desired configuration.
+var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
+
+// Create a MySqlDataSourceBuilder, configuring it with our LoggerFactory.
+var dataSourceBuilder = new MySqlDataSourceBuilder("Host=localhost;User ID=root;Password=pass");
+dataSourceBuilder.UseLoggerFactory(loggerFactory);
+await using var dataSource = dataSourceBuilder.Build();
+
+// Any connections handed out by the data source will log via the LoggerFactory:
+await using var connection = await dataSource.OpenConnectionAsync();
+await using var command = new MySqlCommand("SELECT 1", connection);
+_ = await command.ExecuteScalarAsync();
+```
+
+## ASP.NET and Dependency Injection
+
+If you're using ASP.NET, you can use the additional [MySqlConnector.DependencyInjection package](https://www.nuget.org/packages/MySqlConnector.DependencyInjection), which provides seamless integration with dependency injection and logging:
+
+```csharp
+var builder = WebApplication.CreateBuilder(args);
+builder.Logging.AddConsole();
+builder.Services.AddMySqlDataSource("Host=localhost;User ID=root;Password=pass");
+```
+
+The `AddMySqlDataSource` method registers a data source with the DI container.
+This data source automatically uses the logger factory configured by ASP.NET by default.
+This allows your endpoints to get injected with MySQL connections which log to the application's logger factory.
+
+## Global Logging
+
+### Migrating from Global Logging
+
+<blockquote class="highlight">
+In MySqlConnector 2.4.0, the <tt>MySqlConnectorLogManager.Provider</tt> API has been deprecated.
+Follow these instructions to migrate to the new logging API.
+</blockquote>
+
+With the deprecated logging framework, you may have had code that looked like this:
+
+```csharp
+log4net.Config.XmlConfigurator.Configure();
+MySqlConnectorLogManager.Provider = new Log4netLoggerProvider();
+using var connection = new MySqlConnection(connectionString);
+```
+
+To migrate to the new logging API, you will need to:
+
+* Create a `LoggerFactory`
+* Connect that `LoggerFactory` to your desired logging framework
+* Create a `MySqlDataSource` configurated with that `LoggerFactory`
+* Create new `MySqlConnection` objects using `MySqlDataSource.CreateConnection()` (or `OpenConnection[Async]()`).
+
+This will look like the following, depending on your exact configuration:
+
+```csharp
+// create a LoggerFactory and configure it with the desired logging framework
+// use ONLY ONE of the "Add" methods below, depending on your logging framework
+var loggerFactory = LoggerFactory.Create(builder =>
+  // if you just want console logging
+  builder.AddConsole();
+
+  // connect to log4net via Microsoft.Extensions.Logging.Log4Net.AspNetCore
+  builder.AddLog4Net(new Log4NetProviderOptions
+  {
+    UseWebOrAppConfig = true, // set this if you're storing your settings in Web.config instead of log4net.config
+    ExternalConfigurationSetup = true, // set this instead if you're initializing log4net yourself
+    // see other options at https://github.com/huorswords/Microsoft.Extensions.Logging.Log4Net.AspNetCore
+  }));
+
+  // connect to NLog via NLog.Extensions.Logging
+  builder.AddNLog();
+
+  // connect to Serilog via Serilog.Extensions.Logging
+  builder.AddSerilog(dispose: true);
+);
+
+// now create a MySqlDataSource and configure it with the LoggerFactory
+using var dataSource = new MySqlDataSourceBuilder(yourConnectionString)
+  .UseLoggerFactory(loggerFactory)
+  .Build();
+
+// create all MySqlConnection objects via the MySqlDataSource, not directly
+// DON'T: using var connection = new MySqlConnection(yourConnectionString);
+using var connection = dataSource.CreateConnection();
+
+// you can also create open connections
+using var connection = await dataSource.OpenConnectionAsync();
+```
+
+### Deprecated Logging Framework
+
+<blockquote class="warning">
+The following information is for MySqlConnector versions prior to 2.3.0.
+The global logging interface that is described is deprecated and shouldn't be used in new code.
+</blockquote>
+
+MySqlConnector implements a custom logging framework with the `IMySqlConnectorLogger` and `IMySqlConnectorLoggerProvider` interfaces.
+To connect MySqlConnector to an existing logging framework, write an implementation of `IMySqlConnectorLoggerProvider` that adapts
+the existing logging framework, and install it by setting `MySqlConnector.Logging.MySqlConnectorLogManager.Provider = provider;`.
+
+The `MySqlConnectorLogManager.Provider` property may only be set once, and must be set before any other MySqlConnector library methods are called.
+
+Trace-level logging is useful for diagnosing problems with MySqlConnector itself; it is recommended that applications limit MySqlConnector
+logging to Info or higher.
+
+### Existing Logging Providers
+
+There are NuGet packages that adapt MySqlConnector logging for popular logging frameworks.
+
+#### log4net
+
+Install [MySqlConnector.Logging.log4net](https://www.nuget.org/packages/MySqlConnector.Logging.log4net/).
+
+Add the following line of code to your application startup routine:
+
+```csharp
+MySqlConnectorLogManager.Provider = new Log4netLoggerProvider();
+```
+
+To reduce the verbosity of MySqlConnector logging, add the following element to your log4net config:
+
+```xml
+<log4net>
+  ...
+  <logger name="MySqlConnector">
+    <level value="WARN" /> <!-- or "INFO" -->
+  </logger>
+```
+
+#### Microsoft.Extensions.Logging
+
+Install [MySqlConnector.Logging.Microsoft.Extensions.Logging](https://www.nuget.org/packages/MySqlConnector.Logging.Microsoft.Extensions.Logging/).
+
+Add the following line of code to your `Startup.Configure` method:
+
+```csharp
+MySqlConnectorLogManager.Provider = new MicrosoftExtensionsLoggingLoggerProvider(loggerFactory);
+```
+
+#### NLog
+
+Install [MySqlConnector.Logging.NLog](https://www.nuget.org/packages/MySqlConnector.Logging.NLog/).
+
+Add the following line of code to your application startup routine:
+
+```csharp
+MySqlConnectorLogManager.Provider = new NLogLoggerProvider();
+```
+
+#### Serilog
+
+Install [MySqlConnector.Logging.Serilog](https://www.nuget.org/packages/MySqlConnector.Logging.Serilog/).
+
+Add the following line of code to your application startup routine:
+
+```csharp
+MySqlConnectorLogManager.Provider = new SerilogLoggerProvider();
+```
diff --git a/docs/content/diagnostics/metrics.md b/docs/content/diagnostics/metrics.md
new file mode 100644
index 000000000..f62f7f569
--- /dev/null
+++ b/docs/content/diagnostics/metrics.md
@@ -0,0 +1,96 @@
+---
+lastmod: 2024-01-14
+date: 2023-11-11
+menu:
+  main:
+    parent: diagnostics
+title: Metrics
+weight: 30
+---
+
+# Metrics
+
+MySqlConnector exposes metrics that follow the [OpenTelemetry Semantic Conventions for Database Metrics](https://opentelemetry.io/docs/specs/semconv/database/database-metrics/) via types in the [System.Diagnostics.Metrics](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.metrics) namespace.
+
+<blockquote class="note">Metrics are only supported in MySqlConnector 2.3.0 or later.</blockquote>
+
+The name used for the meter is `MySqlConnector`.
+
+Connect a listener using the `MeterListener` class:
+
+```csharp
+new MeterListener
+{
+    InstrumentPublished = (instrument, listener) =>
+    {
+        if (instrument.Meter.Name == "MySqlConnector")
+            listener.EnableMeasurementEvents(instrument);
+    }
+};
+```
+
+## Supported Instruments
+
+Name | Type | Unit | Description
+--- | --- | --- | ---
+`db.client.connections.usage` | UpDownCounter | `{connection}` | The number of connections that are currently in the state described by the state tag.
+`db.client.connections.pending_requests` | UpDownCounter | `{request}` | The number of pending requests for an open connection, cumulative for the entire pool.
+`db.client.connections.timeouts` | Counter | `{timeout}` | The number of connection timeouts that have occurred trying to obtain a connection from the pool.
+`db.client.connections.create_time` | Histogram | `s` | The time it took to create a new connection.
+`db.client.connections.use_time` | Histogram | `s` | The time between borrowing a connection and returning it to the pool.
+`db.client.connections.wait_time` | Histogram | `s` | The time it took to obtain an open connection from the pool.
+`db.client.connections.idle.max` | UpDownCounter | `{connection}` | The maximum number of idle open connections allowed; this corresponds to `MaximumPoolSize` in the connection string.
+`db.client.connections.idle.min` | UpDownCounter | `{connection}` | The minimum number of idle open connections allowed; this corresponds to `MinimumPoolSize` in the connection string.
+`db.client.connections.max` | UpDownCounter | `{connection}` | The maximum number of open connections allowed; this corresponds to `MaximumPoolSize` in the connection string.
+
+## Supported Tags
+
+Name | Description
+--- | ---
+`pool.name` | The name of the connection pool (see below).
+`state` | `idle` or `used`; this tag is used with the `db.client.connections.usage` instrument.
+
+## Connection Pool Name
+
+A connection pool name can be specified by one of the following means:
+
+### UseName
+
+The `MySqlDataSourceBuilder.UseName` method can be used to specify a name for the connection pool:
+
+```csharp
+using var dataSource = new MySqlDataSourceBuilder("...connection string...")
+    .UseName("MyPoolName")
+    .Build();
+```
+
+This can also be used with dependency injection:
+
+```csharp
+builder.Services.AddMySqlDataSource(builder.Configuration.GetConnectionString("Default"),
+    x => x.UseName("MyPoolName"));
+```
+
+### Keyed Services
+
+Use the `AddKeyedMySqlDataSource` method to register a `MySqlDataSource` as a [keyed service](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8/runtime#keyed-di-services).
+If the service key is a string, it will automatically be used as the `MySqlDataSource` name.
+
+```csharp
+builder.Services.AddKeyedMySqlDataSource("MyPoolName",
+    builder.Configuration.GetConnectionString("MyConnectionString"));
+```
+
+### Application Name
+
+Finally, the connection pool name can be specified by setting the `Application Name` connection string option:
+
+```csharp
+using var connection = new MySqlConnection("server=dbserver;...;Application Name=MyPoolName");
+```
+
+If `UseName` is used, it will override the `Application Name` connection string option.
+
+### Default
+
+For metrics, if no pool name is specified, the connection string (without a password) will be used as the pool name.
diff --git a/docs/content/tutorials/tracing.md b/docs/content/diagnostics/tracing.md
similarity index 93%
rename from docs/content/tutorials/tracing.md
rename to docs/content/diagnostics/tracing.md
index ee7243424..07002a253 100644
--- a/docs/content/tutorials/tracing.md
+++ b/docs/content/diagnostics/tracing.md
@@ -1,10 +1,11 @@
 ---
+lastmod: 2023-11-11
 date: 2021-10-16
 menu:
   main:
-    parent: tutorials
+    parent: diagnostics
 title: Tracing
-weight: 12
+weight: 20
 ---
 
 # Tracing
diff --git a/docs/content/home.md b/docs/content/home.md
index 2f416c769..cb1899de0 100644
--- a/docs/content/home.md
+++ b/docs/content/home.md
@@ -1,5 +1,5 @@
 ---
-lastmod: 2019-06-20
+lastmod: 2023-11-19
 date: 2016-10-16
 title: Home
 weight: 10
@@ -8,23 +8,17 @@ menu:
     url: "/"
 ---
 
-# MySqlConnector: High Performance MySQL Library for .NET
-
-* **Asynchronous:** True asynchronous I/O for maximum throughput
-* **Fast:** Optimized for speed and low memory usage
-* **Reliable:** Fixes [dozens of open bugs](/tutorials/migrating-from-connector-net/#fixed-bugs) in Oracle’s Connector/NET; passes all [ADO.NET Specification Tests](https://mysql-net.github.io/AdoNetResults/)
-* **Leading Edge:** First MySQL library to support .NET Core; uses the latest .NET features
-* **Open:** MIT license; development [happens on GitHub](https://github.com/mysql-net/MySqlConnector) with publicly visible roadmaps, issues, and PRs
+# MySqlConnector: High Performance .NET MySQL Driver
 
 ## About
 
-MySqlConnector is an [ADO.NET](https://msdn.microsoft.com/en-us/library/e80y5yhx.aspx) data
-provider for [MySQL Server](https://www.mysql.com/), [MariaDB](https://mariadb.org/),
-[Percona Server](https://www.percona.com/software/mysql-database/percona-server), [Amazon Aurora](https://aws.amazon.com/rds/aurora/),
-[Azure Database for MySQL](https://azure.microsoft.com/en-us/services/mysql/),
-[Google Cloud SQL for MySQL](https://cloud.google.com/sql/docs/mysql/) and more. It provides implementations of
+MySqlConnector is a C# [ADO.NET](https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/) driver for [MySQL Server](https://www.mysql.com/), [MariaDB](https://mariadb.org/),
+[Amazon Aurora](https://aws.amazon.com/rds/aurora/),
+[Azure Database for MySQL](https://azure.microsoft.com/en-us/products/mysql/),
+[Google Cloud SQL for MySQL](https://cloud.google.com/sql/docs/mysql/),
+[Percona Server](https://www.percona.com/software/mysql-database/percona-server) and more. It provides implementations of
 `DbConnection`, `DbCommand`, `DbDataReader`, `DbTransaction` &mdash; the classes
-needed to query and update databases from managed code.
+needed to query and update databases from C# code.
 
 ## Getting Started
 
@@ -60,61 +54,35 @@ while (await reader.ReadAsync())
     Console.WriteLine(reader.GetString(0));
 ```
 
+## Server Compatibility
+
+MySqlConnector is compatible with the following servers.
+Version numbers in **bold** indicate versions that are regularly tested by the [integration tests](https://dev.azure.com/mysqlnet/MySqlConnector/_build?definitionId=2&_a=summary) run on every commit.
+
+Server  | Versions | Notes
+--- | --- | ---
+Amazon Aurora RDS | 2.x, 3.x | Use `Pipelining=False` [for Aurora 2.x](https://mysqlconnector.net/troubleshooting/aurora-freeze/)
+Azure Database for MySQL | 5.7, 8.0 | Single Server and Flexible Server
+Google Cloud SQL for MySQL | 5.6, 5.7, 8.0 |
+MariaDB | 10.x (**10.6**, **10.11**), 11.x (**11.4**, **11.8**) |
+MySQL | 5.5, 5.6, 5.7, 8.x (**8.0**, **8.4**), 9.x (**9.4**) | 5.5 is EOL and has some [compatibility issues](https://github.com/mysql-net/MySqlConnector/issues/1192); 5.6 and 5.7 are EOL
+Percona Server | 5.6, 5.7, 8.0 |
+PlanetScale | | See PlanetScale [MySQL compatibility notes](https://planetscale.com/docs/reference/mysql-compatibility)
+ProxySQL | 2.x | Some [compatibility issues](https://github.com/search?q=repo%3Amysql-net%2FMySqlConnector+proxysql&type=issues)
+SingleStoreDB | |
+TiDB | |
+
 ## Performance
 
 MySqlConnector outperforms Connector/NET (MySql.Data) on benchmarks:
 
-![Benchmark](https://files.logoscdn.com/v1/files/63673908/assets/13928411/content.png?signature=MVHBZxDfB0J-0Pueja8NtvuLD9A)
+<p><img src="https://files.logoscdn.com/v1/assets/15435916/optimized" alt="Benchmark results for MySql.Data vs MySqlConnector" width="736" height="454"></p>
 
-(Client: MySqlConnector 2.2.0, Ubuntu 20.04, .NET 7.0; Server: Azure Database for MySQL 8.0.28, TLS 1.2)
+(Client: MySqlConnector 2.3.1, MySql.Data 8.2.0, Ubuntu 23.04, .NET 8.0; Server: Azure Database for MySQL 8.0.34, TLS 1.2)
 
-## Why use MySqlConnector over Oracle’s Connector/NET?
+## Why use MySqlConnector over Oracle’s MySQL Connector/NET?
 
 MySqlConnector is a clean-room reimplementation of the [MySQL Protocol](https://dev.mysql.com/doc/internals/en/client-server-protocol.html)
-and is not based on [Oracle’s Connector/NET](https://github.com/mysql/mysql-connector-net).
-
-<table class="table table-striped table-hover" style="max-width: 650px">
-  <thead>
-    <th style="width:25%"></th>
-    <th style="width:25%">MySqlConnector</th>
-    <th style="width:25%">Oracle’s Connector/NET</th>
-    <th style="width:25%">MySqlConnector Advantage</th>
-  </thead>
-  <tr>
-    <td><strong>Async</strong></td>
-    <td><strong>Fully asynchronous</strong> I/O</td>
-    <td>Async calls map to synchronous I/O</td>
-    <td>Uses fewer thread pool threads; higher throughput</td>
-  </tr>
-  <tr>
-    <td><strong>Development</strong></td>
-    <td>
-      <strong>Open and Collaborative</strong> Development on
-      <a href="https://github.com/mysql-net/MySqlConnector">GitHub</a>
-    </td>
-    <td>
-      Closed Development Roadmap. Code is viewable on
-      <a href="https://github.com/mysql/mysql-connector-net">GitHub</a>,
-      some issues addressed in <a href="http://forums.mysql.com/list.php?38">forums</a>
-    </td>
-    <td>
-      <a href="https://github.com/mysql-net/MySqlConnector/issues?q=is%3Aissue+is%3Aopen+label%3A%22up+for+grabs%22">Get involved!</a> View
-      <a href="https://github.com/mysql-net/MySqlConnector/milestones">our roadmap</a>,
-      discuss <a href="https://github.com/mysql-net/MySqlConnector/issues">issues</a>,
-      contribute <a href="https://github.com/mysql-net/MySqlConnector/pulls">pull requests</a>
-    </td>
-  </tr>
-  <tr>
-    <td><strong>License</strong></td>
-    <td>
-      The <strong><a href="https://github.com/mysql-net/MySqlConnector/blob/master/LICENSE">MIT License</a></strong>
-    </td>
-    <td>
-      <a href="http://www.gnu.org/licenses/old-licenses/gpl-2.0.html">GPLv2</a>
-      with <a href="http://www.mysql.com/about/legal/licensing/foss-exception/">FOSS Exception</a>; or
-      <a href="https://www.mysql.com/about/legal/licensing/oem/">commercial license</a>
-    </td>
-    <td>More Permissive</td>
-  </tr>
-</table>
+and is not based on [Oracle’s MySQL Connector/NET](https://github.com/mysql/mysql-connector-net).
 
+See [MySqlConnector vs MySql.Data](/tutorials/migrating-from-connector-net/) for reasons to switch to MySqlConnector and details on migrating.
diff --git a/docs/content/overview/command-cancellation.md b/docs/content/overview/command-cancellation.md
index be71122e4..fb1ce454d 100644
--- a/docs/content/overview/command-cancellation.md
+++ b/docs/content/overview/command-cancellation.md
@@ -23,6 +23,17 @@ The mechanisms for cancelling a command are:
 
 **CommandTimeout**. Each `MySqlCommand` has a `CommandTimeout` property that specifies a timeout in seconds. (This is initialized from the [`DefaultCommandTimeout`](/connection-options/#DefaultCommandTimeout) setting in the connection string.) If this timeout is not `0`, then a soft cancellation will be attempted when the timeout expires. If successful, the `ExecuteX(Async)` method will throw a `MySqlException` with `MySqlErrorCode.CommandTimeoutExpired`; its inner exception will be a `MySqlException` with `MySqlErrorCode.QueryInterrupted`.
 
+> **NOTE**
+>
+> `CommandTimeout` does not specify a timeout for the entire result set to be read.
+> Instead, as per [the documentation for `SqlCommand.CommandTimeout`](https://learn.microsoft.com/en-us/dotnet/api/system.data.sqlclient.sqlcommand.commandtimeout), it specifies "the cumulative time-out for all network packets that are read during the invocation of a method".
+> That is, the timeout is reset at the beginning of each call to a public API method, e.g., `MySqlDataReader.Read` or `MySqlDataReader.NextResult`.
+>
+> For methods such as `MySqlCommand.ExecuteNonQuery`, which perform all their work within a single public API method, then `CommandTimeout` does apply to the entirety of that operation.
+> However, it still only controls the cumulative timeout for all network packets, not the total time taken to execute the command.
+>
+> To stop a MySQL operation after a specified amount of time, use an `ExecuteXAsync(CancellationToken)` overload and pass a `CancellationToken` that is cancelled after the desired timeout.
+
 **CancellationTimeout**. The connection string can specify a [`CancellationTimeout`](/connection-options/#CancellationTimeout) timeout value. After `CommandTimeout` elapses (and a soft cancellation is started), the `CancellationTimeout` timer starts. If this times out, a hard cancellation is performed and the underlying network connection is closed. When this occurs, the `ExecuteX(Async)` method will throw a `MySqlException` with `MySqlErrorCode.CommandTimeoutExpired`.
 
 ### Notes
diff --git a/docs/content/overview/dbproviderfactories.md b/docs/content/overview/dbproviderfactories.md
index a2efe3166..751913f4e 100644
--- a/docs/content/overview/dbproviderfactories.md
+++ b/docs/content/overview/dbproviderfactories.md
@@ -1,15 +1,16 @@
 ---
-lastmod: 2020-06-14
+lastmod: 2023-01-28
 date: 2019-03-06
 menu:
   main:
     parent: getting started
 title: DbProviderFactories
+customtitle: "Using DbProviderFactories"
+description: How to install MySqlConnector into DbProviderFactories by using app.config or DbProviderFactories.RegisterFactory.
 weight: 15
 ---
 
-Using DbProviderFactories
-==========
+# Using DbProviderFactories
 
 MySqlConnector can be registered with `DbProviderFactories` and obtained via `DbProviderFactories.GetFactory("MySqlConnector")`, or by
 using the methods [described here](https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/obtaining-a-dbproviderfactory).
diff --git a/docs/content/overview/installing.md b/docs/content/overview/installing.md
index b6f8e51d8..c0e7e6205 100644
--- a/docs/content/overview/installing.md
+++ b/docs/content/overview/installing.md
@@ -1,22 +1,22 @@
 ---
-lastmod: 2020-01-07
+lastmod: 2023-01-28
 date: 2016-10-16
+title: Installing
+description: How to install MySqlConnector from NuGet into your .NET application.
 menu:
   main:
     parent: getting started
-title: Installing
 weight: 10
 ---
 
-Installing
-==========
+# Installing
 
 The recommended way of installing MySqlConnector is through [NuGet](https://www.nuget.org/packages/MySqlConnector/).
 Note that if you are using the `MySql.Data` NuGet package, it must be uninstalled first.
 
 ### Automatically
 
-If using the new project system, run: `dotnet add package MySqlConnector`
+At the command line, run: `dotnet add package MySqlConnector`
 
 Or, in Visual Studio, use the _NuGet Package Manager_ to browse for and install `MySqlConnector`.
 
@@ -30,11 +30,11 @@ Or, in Visual Studio, use the _NuGet Package Manager_ to browse for and install
     <AssemblyTitle>My Application</AssemblyTitle>
     <Description>A great application</Description>
     <OutputType>Exe</OutputType>
-    <TargetFramework>netcoreapp3.1</TargetFramework>
+    <TargetFramework>net8.0</TargetFramework>
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="MySqlConnector" Version="0.61.0" />
+    <PackageReference Include="MySqlConnector" Version="2.3.7" />
   </ItemGroup>
 
 </Project>
diff --git a/docs/content/overview/logging.md b/docs/content/overview/logging.md
deleted file mode 100644
index 3c113ae58..000000000
--- a/docs/content/overview/logging.md
+++ /dev/null
@@ -1,75 +0,0 @@
----
-lastmod: 2018-12-14
-date: 2018-01-20
-menu:
-  main:
-    parent: getting started
-title: Logging
-weight: 50
----
-
-Logging
-=======
-
-MySqlConnector implements a custom logging framework with the `IMySqlConnectorLogger` and `IMySqlConnectorLoggerProvider` interfaces.
-To connect MySqlConnector to an existing logging framework, write an implementation of `IMySqlConnectorLoggerProvider` that adapts
-the existing logging framework, and install it by setting `MySqlConnector.Logging.MySqlConnectorLogManager.Provider = provider;`.
-
-The `MySqlConnectorLogManager.Provider` property may only be set once, and must be set before any other MySqlConnector library methods are called.
-
-Debug-level logging is useful for diagnosing problems with MySqlConnector itself; it is recommend that applications limit MySqlConnector
-logging to Info or higher.
-
-### Existing Logging Providers
-
-There are NuGet packages that adapt MySqlConnector logging for popular logging frameworks.
-
-#### log4net
-
-Install [MySqlConnector.Logging.log4net](https://www.nuget.org/packages/MySqlConnector.Logging.log4net/).
-
-Add the following line of code to your application startup routine:
-
-```csharp
-MySqlConnectorLogManager.Provider = new Log4netLoggerProvider();
-```
-
-To reduce the verbosity of MySqlConnector logging, add the following element to your log4net config:
-
-```xml
-<log4net>
-  ...
-  <logger name="MySqlConnector">
-    <level value="WARN" /> <!-- or "INFO" -->
-  </logger>
-```
-
-#### Microsoft.Extensions.Logging
-
-Install [MySqlConnector.Logging.Microsoft.Extensions.Logging](https://www.nuget.org/packages/MySqlConnector.Logging.Microsoft.Extensions.Logging/).
-
-Add the following line of code to your `Startup.Configure` method:
-
-```csharp
-MySqlConnectorLogManager.Provider = new MicrosoftExtensionsLoggingLoggerProvider(loggerFactory);
-```
-
-#### NLog
-
-Install [MySqlConnector.Logging.NLog](https://www.nuget.org/packages/MySqlConnector.Logging.NLog/).
-
-Add the following line of code to your application startup routine:
-
-```csharp
-MySqlConnectorLogManager.Provider = new NLogLoggerProvider();
-```
-
-#### Serilog
-
-Install [MySqlConnector.Logging.Serilog](https://www.nuget.org/packages/MySqlConnector.Logging.Serilog/).
-
-Add the following line of code to your application startup routine:
-
-```csharp
-MySqlConnectorLogManager.Provider = new SerilogLoggerProvider(loggerFactory);
-```
diff --git a/docs/content/overview/schema-collections.md b/docs/content/overview/schema-collections.md
index db75bc68b..36ba177ca 100644
--- a/docs/content/overview/schema-collections.md
+++ b/docs/content/overview/schema-collections.md
@@ -1,11 +1,12 @@
 ---
 date: 2021-04-24
-lastmod: 2022-08-12
+lastmod: 2024-10-05
+title: Schema Collections
+customtitle: "Supported Schema Collections"
+description: Information about the schemas supported by MySqlConnection.GetSchema.
 menu:
   main:
     parent: getting started
-title: Schema Collections
-customtitle: "Supported Schema Collections"
 weight: 80
 ---
 
@@ -40,7 +41,9 @@ weight: 80
 * `Tables`—[information about tables](../schema/tables/)
 * `TableConstraints`
 * `TablePrivileges`
-* `TableSpaces`
 * `Triggers`
 * `UserPrivileges`
 * `Views`
+* `Foreign Keys`—[information about foreign keys in the server's SQL syntax](../schema/foreign keys/)
+* `Indexes`—[information about indexes in the server's SQL syntax](../schema/indexes/)
+* `IndexColumns`—[information about indexes in the server's SQL syntax](../schema/indexcolumns/)
diff --git a/docs/content/overview/schema/foreign keys.md b/docs/content/overview/schema/foreign keys.md
new file mode 100644
index 000000000..7abcc899f
--- /dev/null
+++ b/docs/content/overview/schema/foreign keys.md	
@@ -0,0 +1,34 @@
+---
+date: 2022-07-10
+lastmod: 2023-12-16
+title: Foreign Keys Schema
+---
+
+# Foreign Keys Schema
+
+The `Foreign Keys` schema provides information about foreign keys in the server's SQL syntax.
+
+Column Name | Data Type | Description
+--- | --- | ---
+CONSTRAINT_CATALOG | string | 
+CONSTRAINT_SCHEMA | string | 
+CONSTRAINT_NAME | string | 
+TABLE_CATALOG | string | 
+TABLE_SCHEMA | string | 
+TABLE_NAME | string | 
+MATCH_OPTION | string | 
+UPDATE_RULE | string | 
+DELETE_RULE | string | 
+REFERENCED_TABLE_CATALOG | string | 
+REFERENCED_TABLE_SCHEMA | string | 
+REFERENCED_TABLE_NAME | string | 
+
+The following restrictions are supported:
+
+Restriction Name | Restriction Default | Restriction Number
+--- | --- | --:
+Catalog | TABLE_CATALOG | 1
+Schema | TABLE_SCHEMA | 2
+Table | TABLE_NAME | 3
+Constraint Name | CONSTRAINT_NAME | 4
+
diff --git a/docs/content/overview/schema/indexcolumns.md b/docs/content/overview/schema/indexcolumns.md
new file mode 100644
index 000000000..e36acc429
--- /dev/null
+++ b/docs/content/overview/schema/indexcolumns.md
@@ -0,0 +1,30 @@
+---
+date: 2022-07-10
+lastmod: 2023-12-19
+title: IndexColumns Schema
+---
+
+# IndexColumns Schema
+
+The `IndexColumns` schema provides information about indexes in the server's SQL syntax.
+
+Column Name | Data Type | Description
+--- | --- | ---
+INDEX_CATALOG | string | 
+INDEX_SCHEMA | string | 
+INDEX_NAME | string | 
+TABLE_NAME | string | 
+COLUMN_NAME | string | 
+ORDINAL_POSITION | int | 
+SORT_ORDER | string | 
+
+The following restrictions are supported:
+
+Restriction Name | Restriction Default | Restriction Number
+--- | --- | --:
+Catalog | TABLE_CATALOG | 1
+Schema | TABLE_SCHEMA | 2
+Table | TABLE_NAME | 3
+Name | INDEX_NAME | 4
+Column | COLUMN_NAME | 5
+
diff --git a/docs/content/overview/schema/indexes.md b/docs/content/overview/schema/indexes.md
new file mode 100644
index 000000000..251118081
--- /dev/null
+++ b/docs/content/overview/schema/indexes.md
@@ -0,0 +1,30 @@
+---
+date: 2022-07-10
+lastmod: 2023-12-16
+title: Indexes Schema
+---
+
+# Indexes Schema
+
+The `Indexes` schema provides information about indexes in the server's SQL syntax.
+
+Column Name | Data Type | Description
+--- | --- | ---
+INDEX_CATALOG | string | 
+INDEX_SCHEMA | string | 
+INDEX_NAME | string | 
+TABLE_NAME | string | 
+UNIQUE | bool | 
+PRIMARY | bool | 
+TYPE | string | 
+COMMENT | string | 
+
+The following restrictions are supported:
+
+Restriction Name | Restriction Default | Restriction Number
+--- | --- | --:
+Catalog | TABLE_CATALOG | 1
+Schema | TABLE_SCHEMA | 2
+Table | TABLE_NAME | 3
+Name | INDEX_NAME | 4
+
diff --git a/docs/content/overview/use-with-orms.md b/docs/content/overview/use-with-orms.md
index baf7bb988..840389b18 100644
--- a/docs/content/overview/use-with-orms.md
+++ b/docs/content/overview/use-with-orms.md
@@ -1,10 +1,11 @@
 ---
-lastmod: 2017-11-06
+lastmod: 2023-01-28
 date: 2016-10-16
+title: Use with ORMs
+description: A list of .NET object-relational mappers that work well with MySqlConnector.
 menu:
   main:
     parent: getting started
-title: Use with ORMs
 weight: 60
 ---
 
@@ -14,6 +15,8 @@ Use with ORMs
 This library is compatible with popular .NET ORMs including:
 
 * [Dapper](https://dapperlib.github.io/Dapper/) ([GitHub](https://github.com/DapperLib/Dapper), [NuGet](https://www.nuget.org/packages/Dapper))
+  * See [Tutorial: Connect to MySQL with Dapper](/tutorials/dapper/).
+* [Faithlife.Data](https://faithlife.github.io/FaithlifeData/) ([GitHub](https://github.com/Faithlife/FaithlifeData), [NuGet](https://www.nuget.org/packages/Faithlife.Data))
 * [FreeSql](https://freesql.net/) ([GitHub](https://github.com/dotnetcore/FreeSql), [NuGet](https://www.nuget.org/packages/FreeSql.Provider.MySqlConnector/))
 * [LINQ to DB](https://linq2db.github.io) ([GitHub](https://github.com/linq2db/linq2db), [NuGet](https://www.nuget.org/packages/linq2db.MySqlConnector))
 * [NHibernate](https://nhibernate.info/) ([GitHub](https://github.com/nhibernate/NHibernate.MySqlConnector), [NuGet](https://www.nuget.org/packages/NHibernate.Driver.MySqlConnector))
@@ -26,3 +29,4 @@ This library is compatible with popular .NET ORMs including:
 For Entity Framework support, use:
 
 * Pomelo.EntityFrameworkCore.MySql ([GitHub](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql), [NuGet](https://www.nuget.org/packages/Pomelo.EntityFrameworkCore.MySql))
+  * See [Tutorial: Connect to MySQL with Entity Framework Core](/tutorials/efcore/).
diff --git a/docs/content/overview/using-addwithvalue.md b/docs/content/overview/using-addwithvalue.md
index 54330de5f..1e476bde3 100644
--- a/docs/content/overview/using-addwithvalue.md
+++ b/docs/content/overview/using-addwithvalue.md
@@ -1,11 +1,12 @@
 ---
+lastmod: 2023-01-28
 date: 2020-01-06
-lastmod: 2021-07-04
+title: Using AddWithValue
 customtitle: "Start Using AddWithValue with MySQL"
+description: Why you should use Parameters.AddWithValue with MySqlConnector, and why it's OK to use with MySQL.
 menu:
   main:
     parent: getting started
-title: Using AddWithValue
 weight: 70
 ---
 
diff --git a/docs/content/overview/version-history.md b/docs/content/overview/version-history.md
index 10770dea9..be5a12b1f 100644
--- a/docs/content/overview/version-history.md
+++ b/docs/content/overview/version-history.md
@@ -1,22 +1,159 @@
 ---
-lastmod: 2023-07-21
+lastmod: 2024-04-22
 date: 2017-03-27
 menu:
   main:
     parent: getting started
 title: Version History
+description: Version history for the MySqlConnector NuGet package
 weight: 30
 ---
 
 # Version History
 
+### 2.4.0
+
+* Support .NET 9.0.
+* **Possibly breaking** Mark `MySqlConnectorLogManager.Provider` as `[Obsolete]`: [#1397](https://github.com/mysql-net/MySqlConnector/issues/1397).
+* **Possibly breaking** Remove end-of-life .NET 7 target framework.
+  * Users who wish to use `DbDataSource` need to target .NET 8.0 or later.
+* **Possibly breaking** Update dependencies:
+  * Microsoft.Extensions.Logging.Abstractions from 7.0.1 to 8.0.2.
+  * System.Diagnostics.DiagnosticSource from 7.0.2 to 8.0.1.
+* Support trimming and native AOT: [#1265](https://github.com/mysql-net/MySqlConnector/issues/1265).
+* Add `MySqlDataSourceBuilder.UseConnectionOpenedCallback`: [#1508](https://github.com/mysql-net/MySqlConnector/issues/1508).
+* Add `MySqlDbColumn.TableName`: [#1521](https://github.com/mysql-net/MySqlConnector/issues/1521).
+* Support [zero-configuration SSL](https://mariadb.org/mission-impossible-zero-configuration-ssl/) with MariaDB: [#1500](https://github.com/mysql-net/MySqlConnector/pull/1500).
+* Support `SET` data type in functions: [#1491](https://github.com/mysql-net/MySqlConnector/issues/1491).
+* Remove `TABLESPACES` schema: [#1477](https://github.com/mysql-net/MySqlConnector/issues/1477).
+* Rewrite server redirection logic based on latest MariaDB specification: [#1499](https://github.com/mysql-net/MySqlConnector/pull/1499).
+* Change variable name for better server compatibility: [#1502](https://github.com/mysql-net/MySqlConnector/issues/1502).
+* Performance: Avoid `SET NAMES` commands when not necessary: [#1497](https://github.com/mysql-net/MySqlConnector/pull/1497).
+* Fix "Can't replace active reader": [#1469](https://github.com/mysql-net/MySqlConnector/issues/1469).
+* Fix potential `NullReferenceException` in `MySqlConnection.Cancel`: [#1506](https://github.com/mysql-net/MySqlConnector/issues/1506).
+* Fix `InvalidOperationException` in `FinishQuerying`: [#1341](https://github.com/mysql-net/MySqlConnector/issues/1341).
+* Provide logger to new MySqlConnection: [#1510](https://github.com/mysql-net/MySqlConnector/issues/1510).
+* Use correct hostname when cancelling query: [#1514](https://github.com/mysql-net/MySqlConnector/issues/1514).
+* Thanks to [Alex Kerman](https://github.com/AlexKerman) and [Diego Dupin](https://github.com/rusher) for contributions to this release.
+
+### 2.3.7
+
+* Fix `NullReferenceException` in `MySqlDataReader.ActivateResultSet` and `MySqlDataReader.DisposeAsync`: [#1459](https://github.com/mysql-net/MySqlConnector/issues/1459).
+  * This bug likely also caused "Packet received out-of-order" errors.
+  * This was introduced in 2.3.0.
+* Fix rare `NullReferenceException` in `ServerSession` during cancellation: [#1472](https://github.com/mysql-net/MySqlConnector/issues/1472).
+* Fix `Foreign Keys` schema not being generated asynchronously.
+
+### 2.3.6
+
+* Fix `VerifyCA` incompatibility with AWS Aurora: [#1462](https://github.com/mysql-net/MySqlConnector/issues/1462).
+* Verify that the server's root certificate is present (in the list of provided CA certificates) when using the `SslCa` connection string option and `SslMode` is `VerifyCA` or `VerifyFull`.
+* Optimization: Use pipelining to begin a transaction: [#1286](https://github.com/mysql-net/MySqlConnector/issues/1286).
+  * Use `Pipelining = False;` in your connection string to disable this optimization if there are compatibility issues.
+* Optimization: cache `START TRANSACTION` payloads.
+* Add transaction logging: [#1411](https://github.com/mysql-net/MySqlConnector/issues/1411).
+* Fix incompatibility with MySQL Server 5.1: [#1445](https://github.com/mysql-net/MySqlConnector/issues/1445).
+  * This fixes a regression introduced in 2.3.0.
+
+#### MySqlConnector.DependencyInjection
+
+* Add overload of `AddMySqlDataSource` that takes an `Action<IServiceProvider, MySqlDataSourceBuilder>` action to configure the `MySqlDataSourceBuilder` instance: [#1316](https://github.com/mysql-net/MySqlConnector/issues/1316).
+
+### 2.3.5
+
+* DLL files within the NuGet packages are now digitally signed.
+* Thanks to [Devolutions](https://devolutions.net/) for sponsoring this change.
+
+### 2.3.4
+
+* Unify `Application Name` connection string option and `MySqlDataSourceBuilder.UseName`: [#1434](https://github.com/mysql-net/MySqlConnector/issues/1434).
+* Remove TLS version fallback code: [#1409](https://github.com/mysql-net/MySqlConnector/issues/1409).
+* Fix `ArgumentOutOfRangeException` in `TimerQueue.Add`: [#1424](https://github.com/mysql-net/MySqlConnector/issues/1424).
+* Fix `InvalidCastException` when using stored procedures with MySQL 5.0: [#1431](https://github.com/mysql-net/MySqlConnector/issues/1431).
+* Fix hang when disposing `MySqlDataSource` twice on .NET Framework.
+* Thanks to [Yong Yao](https://github.com/SolacomYongYao) for contributions to this release.
+
+### 2.3.3
+
+* Support `COLUMN_NAME` restriction for `IndexColumns` schema; this improves NHibernate compatibility.
+
+### 2.3.2
+
+* Add `Foreign Keys`, `Indexes`, `Index Columns` schemas: [#1419](https://github.com/mysql-net/MySqlConnector/pull/1419).
+* Fix exception when server doesn't send expected metadata: [#1398](https://github.com/mysql-net/MySqlConnector/issues/1398).
+* Clear activity tags when retrying TLS: [#1405](https://github.com/mysql-net/MySqlConnector/issues/1405).
+* Use underlying enum type when writing binary parameters: [#1421](https://github.com/mysql-net/MySqlConnector/pull/1421).
+* Avoid retrying TLS negotiation when a non-recoverable exception occurs.
+* Thanks to [Alisha Kim](https://github.com/daebo01), [Evgeni Alesinskyy](https://github.com/eaexo), and [Jan Trejbal](https://github.com/trejjam) for contributions to this release.
+
+### 2.3.1
+
+* Add `db.client.connections.timeouts` counter to metrics: [#1392](https://github.com/mysql-net/MySqlConnector/pull/1392)
+* Allow empty `schemaRestrictions` array to be passed to `GetSchema`: [#1394](https://github.com/mysql-net/MySqlConnector/pull/1394)
+* Use higher-resolution timestamps for duration metrics: [#1395](https://github.com/mysql-net/MySqlConnector/issues/1395).
+* **Possibly Breaking** Use seconds as the units for duration metrics; change type to `double`: [#1396](https://github.com/mysql-net/MySqlConnector/issues/1396).
+* Thanks to [Tuomas Hietanen](https://github.com/Thorium) for contributions to this release.
+
+#### MySqlConnector.DependencyInjection
+
+* Add `AddKeyedMySqlDataSource` to support keyed services: [#1391](https://github.com/mysql-net/MySqlConnector/pull/1391).
+* Update `Microsoft.Extensions.DependencyInjection.Abstractions` dependency to 8.0.0.
+
+### 2.3.0
+
+* Support .NET 8.0.
+  * Mark some serialization APIs as `[Obsolete]`.
+  * Implement `DbBatchCommand.CreateParameter`: [#1352](https://github.com/mysql-net/MySqlConnector/issues/1352).
+  * Optimizations: Use `IUtf8SpanFormattable`, `Ascii.FromUtf16`, `Guid(bigEndian: true)` constructor, argument-throwing helpers, etc.
+* Support .NET Framework 4.8 (`net48`) TFM: [#1355](https://github.com/mysql-net/MySqlConnector/discussions/1355)
+  * This allows `TlsVersion = TLSv1.3` to be used on .NET Framework 4.8.
+* Drop support for .NET 4.6.1 and .NET Core 3.1: [#636](https://github.com/mysql-net/MySqlConnector/issues/636), [#1273](https://github.com/mysql-net/MySqlConnector/issues/1273).
+  * .NET 4.6.1 support ended on 26 April 2022, and .NET Core 3.1 on 13 December 2022.
+  * The minimum supported versions are now .NET Framework 4.6.2 and .NET 6.0, although other frameworks should be supported via `netstandard2.0`.
+* `MySqlDataSource` is now available for all TFMs, not just .NET 7.0: [#1269](https://github.com/mysql-net/MySqlConnector/pull/1269).
+  * This provides a single place to configure a MySQL connection and makes it easier to register `MySqlConnection` with dependency injection.
+  * Add `MySqlDataSourceBuilder` class to configure `MySqlDataSource` instances.
+  * Add `MySqlDataSource.Name` and `MySqlDataSourceBuilder.UseName`.
+* Microsoft.Extensions.Logging is now used as the core logging abstraction: [#1110](https://github.com/mysql-net/MySqlConnector/issues/1110).
+  * `MySqlConnectorLogManager.Provider` can still be used to add a logging destination, but it is now deprecated.
+  * Use `MySqlDataSourceBuilder.UseLoggerFactory` to configure logging.
+* Add new MySqlConnector.DependencyInjection package: [#1271](https://github.com/mysql-net/MySqlConnector/issues/1271).
+  * `MySqlDataSource` and `MySqlConnection` can be registered with dependency injection by using `builder.Services.AddMySqlDataSource(connectionString)`.
+  * This also configures logging automatically.
+* Expose connection pool metrics: [#491](https://github.com/mysql-net/MySqlConnector/issues/491).
+* Remove `COM_MULTI` protocol support: [#946](https://github.com/mysql-net/MySqlConnector/issues/946).
+* Support `ZEROFILL` columns in `MySqlDecimal`: [#1354](https://github.com/mysql-net/MySqlConnector/issues/1354).
+* Support higher-precision `DateTime` values: [#1379](https://github.com/mysql-net/MySqlConnector/issues/1379).
+* Use `ValueTask` in `MySqlBulkCopy` API for all TFMs: [#1364](https://github.com/mysql-net/MySqlConnector/issues/1364).
+  * **Breaking** This changes the return type of `WriteToServerAsync` from `Task<MySqlBulkCopyResult>` to `ValueTask<MySqlBulkCopyResult>` on .NET Framework
+* Support skipping metadata for prepared statements with MariaDB 10.6 and later: [#1301](https://github.com/mysql-net/MySqlConnector/pull/1301).
+* Support multiple authentication methods when connecting: [#1303](https://github.com/mysql-net/MySqlConnector/pull/1303).
+* Support per-query variables for `CommandBehavior.SchemaOnly` and `SingleRow`: [#1312](https://github.com/mysql-net/MySqlConnector/pull/1312).
+* Recycle `MySqlDataReader` objects: [#1277](https://github.com/mysql-net/MySqlConnector/issues/1277).
+* Perform XA rollback when preparing a transaction fails: [#1348](https://github.com/mysql-net/MySqlConnector/issues/1348).
+* Implement faster parsing for result sets with multiple rows: [#1330](https://github.com/mysql-net/MySqlConnector/pull/1330).
+* Optimize parameter encoding for ASCII strings: [#1296](https://github.com/mysql-net/MySqlConnector/pull/1296).
+* Use `TcpClient.ConnectAsync` overload with `CancellationToken` on .NET 5.0 and later: [#1291](https://github.com/mysql-net/MySqlConnector/pull/1291).
+* Fix cancellation when using a redirected connection: [#1305](https://github.com/mysql-net/MySqlConnector/pull/1305).
+* Fix `MySqlConnection.CloneWith` for connections created from a `MySqlDataSource`: [#1306](https://github.com/mysql-net/MySqlConnector/issues/1306).
+* Work around ephemeral PEM bug on Windows: [#1278](https://github.com/mysql-net/MySqlConnector/issues/1278).
+* Reduce allocations on common code paths.
+* Fix bug when column name begins with `@` in `MySqlBulkCopy`: [#1365](https://github.com/mysql-net/MySqlConnector/issues/1365).
+* Ignore `MySqlDbType` when serializing enum values: [#1384](https://github.com/mysql-net/MySqlConnector/issues/1384).
+* Fix bug that didn't copy `MySqlDataSource` in `MySqlConnection.Clone`: [#1267](https://github.com/mysql-net/MySqlConnector/issues/1267).
+* Fix version parsing for MariaDB 11.0 and later: [#1311](https://github.com/mysql-net/MySqlConnector/pull/1311).
+* Fix potential error in reallocating an internal buffer when writing ASCII text.
+* Update handling of `ActivityStatus` to latest conventions: [#1334](https://github.com/mysql-net/MySqlConnector/pull/1334).
+* Reduce overhead of `CommandTimeout`: [#1338](https://github.com/mysql-net/MySqlConnector/pull/1338).
+* Reword end-of-stream message to be more generic: [#1383](https://github.com/mysql-net/MySqlConnector/issues/1383).
+* Thanks to [Guillaume Boucher](https://github.com/gboucher90), [JackBOBO](https://github.com/JackBOBO), [Piotr Kiełkowicz](https://github.com/Kielek), [Diego Dupin](https://github.com/rusher), and [Nickolay Batov](https://github.com/stilettk) for contributions to this release.
+
 ### 2.2.7
 
 * Respect ConnectTimeout when resetting connection: [#1321](https://github.com/mysql-net/MySqlConnector/issues/1321).
 * Prevent connection pool falling back to an unsupported TLS version: [#1349](https://github.com/mysql-net/MySqlConnector/issues/1349).
 
 ### 2.2.6
-
 * Ignore deadlock exception when rolling back an XA transaction: [#1317](https://github.com/mysql-net/MySqlConnector/issues/1317).
 * Work around ephemeral PEM bug on Windows: [#1278](https://github.com/mysql-net/MySqlConnector/issues/1278).
 * Support new MariaDB version numbers: [#1259](https://github.com/mysql-net/MySqlConnector/issues/1259).
@@ -611,7 +748,7 @@ weight: 30
 
 * **Breaking** `MySqlBulkLoader` (for local files) and `LOAD DATA LOCAL INFILE` are disabled by default.
   * Set `AllowLoadLocalInfile=true` in the connection string to enable loading local data.
-  * This is a security measure; see https://fl.vu/mysql-load-data for details.
+  * This is a security measure; see https://mysqlconnector.net/load-data for details.
 * Add `AllowLoadLocalInfile` connection string option: [#643](https://github.com/mysql-net/MySqlConnector/issues/643).
 * Add `SslCert` and `SslKey` connection string options to specify a client certificate using PEM files: [#641](https://github.com/mysql-net/MySqlConnector/issues/641).
 * Add `SslCa` alias for the `CACertificateFile` connection string option: [#640](https://github.com/mysql-net/MySqlConnector/issues/640).
diff --git a/docs/content/troubleshooting/aurora-freeze.md b/docs/content/troubleshooting/aurora-freeze.md
index 664f4f6e7..83cecbea5 100644
--- a/docs/content/troubleshooting/aurora-freeze.md
+++ b/docs/content/troubleshooting/aurora-freeze.md
@@ -1,6 +1,8 @@
 ---
+lastmod: 2023-01-28
 date: 2021-11-25
 title: Aurora Freeze
+description: How to work around a hang when calling MySqlConnection.Open() with Amazon Aurora by setting Pipelining=False in the connection string.
 customtitle: "MySqlConnection.Open Freezes with Amazon Aurora"
 weight: 5
 menu:
diff --git a/docs/content/troubleshooting/connection-reuse.md b/docs/content/troubleshooting/connection-reuse.md
index 91e5c2f14..352c87653 100644
--- a/docs/content/troubleshooting/connection-reuse.md
+++ b/docs/content/troubleshooting/connection-reuse.md
@@ -1,7 +1,8 @@
 ---
-lastmod: 2018-09-29
+lastmod: 2023-01-28
 date: 2018-09-29
 title: Connection Reuse
+description: Invalid usages of MySqlConnection in multithreaded scenarios and how to fix them.
 weight: 10
 menu:
   main:
diff --git a/docs/content/troubleshooting/datetime-storage.md b/docs/content/troubleshooting/datetime-storage.md
index 302599b7a..7b82d71ab 100644
--- a/docs/content/troubleshooting/datetime-storage.md
+++ b/docs/content/troubleshooting/datetime-storage.md
@@ -1,6 +1,8 @@
 ---
+lastmod: 2023-01-28
 date: 2020-02-08
 title: DateTime Storage
+description: How to store .NET DateTime and DateTimeOffset values in MySQL using DATETIME and TIME columns.
 weight: 24
 menu:
   main:
diff --git a/docs/content/troubleshooting/delimiter.md b/docs/content/troubleshooting/delimiter.md
index 348c04b10..239a2f6c4 100644
--- a/docs/content/troubleshooting/delimiter.md
+++ b/docs/content/troubleshooting/delimiter.md
@@ -1,7 +1,9 @@
 ---
+lastmod: 2023-01-28
 date: 2021-07-27
 title: Delimiter
 customtitle: "Fix: Using DELIMITER in SQL"
+description: DELIMITER $$ is unnecessary when using MySqlConnector and should be removed from SQL statements.
 weight: 28
 menu:
   main:
diff --git a/docs/content/troubleshooting/failed-add-reference-comerr.md b/docs/content/troubleshooting/failed-add-reference-comerr.md
new file mode 100644
index 000000000..08c63e5ac
--- /dev/null
+++ b/docs/content/troubleshooting/failed-add-reference-comerr.md
@@ -0,0 +1,62 @@
+---
+lastmod: 2023-03-10
+date: 2023-01-28
+title: Failed to Add comerr
+description: How to fix the error "Failed to add reference to 'comerr64'" when installing MySql.Data using NuGet.
+customtitle: "Fix: Failed to add reference to 'comerr64' when installing MySql.Data"
+weight: 50
+menu:
+  main:
+    parent: troubleshooting
+---
+
+# Fix: Failed to add reference to 'comerr64' when installing MySql.Data
+
+## Problem
+
+When installing MySql.Data 8.0.32, you may receive the following error:
+
+```
+Install-Package : Failed to add reference to 'comerr64'.
+  Please make sure that the file is accessible, and that it is a valid assembly or COM component.
+At line:1 char:1
++ Install-Package MySql.Data
+```
+
+Or, when running `dotnet build`, you may receive the following warnings:
+
+```
+C:\Program Files\dotnet\sdk\7.0.100\Microsoft.Common.CurrentVersion.targets(2352,5):
+warning MSB3246: Resolved file has a bad image, no metadata, or is otherwise inaccessible. System.BadImageFormatException: PE image does not have metadata. [C:\MySql.csproj]
+warning MSB3246:    at System.Reflection.Metadata.MetadataReader.GetAssemblyName(String assemblyFile) [C:\MySql.csproj]
+warning MSB3246:    at Microsoft.Build.Shared.AssemblyNameExtension.GetAssemblyNameEx(String path) [C:\MySql.csproj]
+warning MSB3246:    at Microsoft.Build.Tasks.SystemState.GetAssemblyName(String path) [C:\MySql.csproj]
+warning MSB3246:    at Microsoft.Build.Tasks.ReferenceTable.SetPrimaryAssemblyReferenceItem(ITaskItem referenceAssemblyName) [C:\MySql.csproj]
+```
+
+## Cause
+
+This is [a bug](https://bugs.mysql.com/bug.php?id=109716) in the MySql.Data NuGet package introduced in version 8.0.32.
+
+## Fix
+
+A fix from Oracle is now available in [MySql.Data version 8.0.32.1](https://www.nuget.org/packages/MySql.Data/8.0.32.1).
+This is mentioned in the fix for [bug 109670](https://bugs.mysql.com/bug.php?id=109670).
+Should that not resolve the problem, there are two other fixes:
+
+### Recommended Fix
+
+The recommended fix for this problem is to [switch to MySqlConnector](/overview/installing), which fixes this problem and [many other bugs](/tutorials/migrating-from-connector-net/#fixed-bugs) in MySql.Data.
+To do this, use **Tools** > **NuGet Package Manager** > **Manage NuGet Packages for Solution...** to uninstall MySql.Data and install MySqlConnector instead.
+
+For more information, see our guide on [migrating from MySql.Data to MySqlConnector](/tutorials/migrating-from-connector-net/).
+
+### Alternative Fix
+
+Users report that the problem only occurs when using `packages.config`.
+You may be able to fix the problem by switching to `<PackageReference>` elements.
+To do this, right-click `packages.config` in Solution Explorer and select **Migrate packages.config to PackageReference**.
+For more information see [Migrate from packages.config to PackageReference](https://learn.microsoft.com/en-us/nuget/consume-packages/migrate-packages-config-to-package-reference) at MSDN.
+
+After doing this, the installation of MySql.Data 8.0.32 should succeed.
+A full conversion to SDK-style projects should not be required.
diff --git a/docs/content/troubleshooting/load-data-local-infile.md b/docs/content/troubleshooting/load-data-local-infile.md
index 8d3b45d9a..d12e2d33e 100644
--- a/docs/content/troubleshooting/load-data-local-infile.md
+++ b/docs/content/troubleshooting/load-data-local-infile.md
@@ -1,8 +1,9 @@
 ---
-lastmod: 2019-08-21
+lastmod: 2023-01-28
 date: 2019-05-23
 title: Load Data Local Infile
 customtitle: "Fix: Using Load Data Local Infile"
+description: Add AllowLoadLocalInfile=true to the connection string to allow LOAD DATA LOCAL INFILE to succeed.
 weight: 30
 menu:
   main:
diff --git a/docs/content/troubleshooting/parameter-types.md b/docs/content/troubleshooting/parameter-types.md
index 628496c83..ae5123b0b 100644
--- a/docs/content/troubleshooting/parameter-types.md
+++ b/docs/content/troubleshooting/parameter-types.md
@@ -1,8 +1,9 @@
 ---
-lastmod: 2021-04-23
+lastmod: 2023-01-28
 date: 2021-01-15
 title: MySqlParameter Types
 customtitle: "Type of value supplied to MySqlParameter.Value isn’t supported"
+description: List of supported .NET types that can be stored in MySqlParameter.Value and what to do for unsupported types.
 weight: 35
 menu:
   main:
@@ -37,6 +38,8 @@ In some cases, this may be as simple as calling `.ToString()` or `.ToString(Cult
 
 * .NET primitives: `bool`, `byte`, `char`, `double`, `float`, `int`, `long`, `sbyte`, `short`, `uint`, `ulong`, `ushort`
 * Common types: `BigInteger`, `DateOnly`, `DateTime`, `DateTimeOffset`, `decimal`, `enum`, `Guid`, `string`, `TimeOnly`, `TimeSpan`
-* BLOB types: `ArraySegment<byte>`, `byte[]`, `Memory<byte>`, `ReadOnlyMemory<byte>`
+* BLOB types: `ArraySegment<byte>`, `byte[]`, `Memory<byte>`, `ReadOnlyMemory<byte>`. `MemoryStream`
+  * NOTE: `System.IO.Stream` and derived types (other than `MemoryStream`) are supported only when `MySqlCommand.Prepare` is called first. The data in the `Stream` will be streamed to the database server as binary data.
+* Vector types: `float[]`, `Memory<float>`, `ReadOnlyMemory<float>`
 * String types: `Memory<char>`, `ReadOnlyMemory<char>`, `StringBuilder`
 * Custom MySQL types: `MySqlDateTime`, `MySqlDecimal`, `MySqlGeometry`
diff --git a/docs/content/troubleshooting/retrieval-public-key.md b/docs/content/troubleshooting/retrieval-public-key.md
index 75a145418..3c4be0bf9 100644
--- a/docs/content/troubleshooting/retrieval-public-key.md
+++ b/docs/content/troubleshooting/retrieval-public-key.md
@@ -1,8 +1,9 @@
 ---
-lastmod: 2019-07-30
+lastmod: 2023-01-28
 date: 2019-07-07
 title: Retrieval of Public Key
 customtitle: "Fix: Retrieval of the RSA public key is not enabled for insecure connections"
+description: Fixing 'Retrieval of the RSA public key is not enabled for insecure connections' error by using MySqlConnector and AllowPublicKeyRetrieval=True in the connection string.
 weight: 40
 menu:
   main:
diff --git a/docs/content/troubleshooting/transaction-usage.md b/docs/content/troubleshooting/transaction-usage.md
index bdb061207..7d6c803bc 100644
--- a/docs/content/troubleshooting/transaction-usage.md
+++ b/docs/content/troubleshooting/transaction-usage.md
@@ -1,8 +1,9 @@
 ---
-lastmod: 2021-09-21
+lastmod: 2023-01-28
 date: 2018-09-29
 title: Transaction Usage
 customtitle: "Fix: The transaction associated with this command is not the connection’s active transaction"
+description: How to fix the error "The transaction associated with this command is not the connection’s active transaction" by setting the Transaction property on MySqlCommand.
 weight: 20
 menu:
   main:
@@ -15,7 +16,7 @@ menu:
 
 When using `MySqlTransaction` from a C# program, you may receive the following error:
 
-* **System.InvalidOperationException: The transaction associated with this command is not the connection's active transaction; see https://fl.vu/mysql-trans**
+* **System.InvalidOperationException: The transaction associated with this command is not the connection's active transaction**
 
 By default, MySqlConnector requires `MySqlCommand.Transaction` to be set to the connection's active transaction in order for the command to be executed successfully. This strictness is intended to catch programming bugs related to using the wrong transaction, a disposed transaction, or forgetting to set the transaction (and using the default value `null`).
 
@@ -23,7 +24,7 @@ However, this strictness can make migrating from Connector/NET more difficult, a
 
 ## Workaround: Use IgnoreCommandTransaction=true
 
-To easily migrate code from Connector/NET, use the `IgnoreCommandTransaction=true` connection string setting to emulate Connector/NET's behaviour and not validate the value of `MySqlCommand.Transaction`. By doing this, you will not need the code fixes prescribed below.
+To easily migrate code from Connector/NET, use the `IgnoreCommandTransaction=true` connection string setting to emulate Connector/NET's behavior and not validate the value of `MySqlCommand.Transaction`. By doing this, you will not need the code fixes prescribed below.
 
 ## Code Fix: Set MySqlCommand.Transaction
 
@@ -40,7 +41,8 @@ command.CommandText = "SELECT ...";
 // *** ADD THIS LINE ***
 command.Transaction = transaction;
 
-// otherwise, this will throw System.InvalidOperationException: The transaction associated with this command is not the connection's active transaction.
+// otherwise, this will throw System.InvalidOperationException: The transaction
+// associated with this command is not the connection's active transaction.
 command.ExecuteScalar();
 
 // ... remaining code
@@ -54,7 +56,8 @@ using var connection = new MySqlConnection(...);
 connection.Open();
 using var transaction = connection.BeginTransaction();
 
-// this will throw System.InvalidOperationException: The transaction associated with this command is not the connection's active transaction.
+// this will throw System.InvalidOperationException: The transaction
+// associated with this command is not the connection's active transaction.
 connection.Query("SELECT ...");
 
 // use this instead:
diff --git a/docs/content/tutorials/basic-api.md b/docs/content/tutorials/basic-api.md
index ebbda970e..2948fa999 100644
--- a/docs/content/tutorials/basic-api.md
+++ b/docs/content/tutorials/basic-api.md
@@ -1,4 +1,5 @@
 ---
+lastmod: 2023-01-21
 date: 2018-06-05
 menu:
   main:
@@ -16,25 +17,23 @@ Here’s a basic code snippet to get you started.
 ```csharp
 var connString = "Server=myserver;User ID=mylogin;Password=mypass;Database=mydatabase";
 
-using (var conn = new MySqlConnection(connString))
+await using var connection = new MySqlConnection(connString);
+await connection.OpenAsync();
+
+// Insert some data
+using (var cmd = new MySqlCommand())
 {
-    await conn.OpenAsync();
-
-    // Insert some data
-    using (var cmd = new MySqlCommand())
-    {
-        cmd.Connection = conn;
-        cmd.CommandText = "INSERT INTO data (some_field) VALUES (@p)";
-        cmd.Parameters.AddWithValue("p", "Hello world");
-        await cmd.ExecuteNonQueryAsync();
-    }
-
-    // Retrieve all rows
-    using (var cmd = new MySqlCommand("SELECT some_field FROM data", conn))
-    using (var reader = await cmd.ExecuteReaderAsync())
-        while (await reader.ReadAsync())
-            Console.WriteLine(reader.GetString(0));
+    cmd.Connection = connection;
+    cmd.CommandText = "INSERT INTO data (some_field) VALUES (@p)";
+    cmd.Parameters.AddWithValue("p", "Hello world");
+    await cmd.ExecuteNonQueryAsync();
 }
+
+// Retrieve all rows
+using var command = new MySqlCommand("SELECT some_field FROM data", connection);
+using var reader = await cmd.ExecuteReaderAsync();
+while (await reader.ReadAsync())
+    Console.WriteLine(reader.GetString(0));
 ```
 
 You can find more info about the ADO.NET API in the [MSDN documentation](https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/ado-net-overview) or in many tutorials on the Internet.
diff --git a/docs/content/tutorials/best-practices.md b/docs/content/tutorials/best-practices.md
index 50eb5b0a5..696b16df0 100644
--- a/docs/content/tutorials/best-practices.md
+++ b/docs/content/tutorials/best-practices.md
@@ -1,5 +1,5 @@
 ---
-lastmod: 2021-09-21
+lastmod: 2023-01-21
 date: 2016-10-16
 menu:
   main:
@@ -67,7 +67,7 @@ should be familiar with [Async/Await - Best Practices in Asynchronous Programmin
   </tr>
   <tr>
     <td>
-      <span class="text-danger">*</span><a href="api/mysql-connection">MySqlConnection</a>.BeginTransactionAsync
+      BeginTransactionAsync
     </td>
     <td>BeginTransaction</td>
   </tr>
@@ -102,21 +102,18 @@ should be familiar with [Async/Await - Best Practices in Asynchronous Programmin
       <a href="https://docs.microsoft.com/en-us/dotnet/core/api/system.data.common.dbtransaction">DbTransaction</a>
     </td>
     <td>
-      <span class="text-danger">*</span><a href="api/mysql-transaction">MySqlTransaction</a>.CommitAsync
+      CommitAsync
     </td>
     <td>Commit</td>
   </tr>
   <tr>
     <td>
-      <span class="text-danger">*</span><a href="api/mysql-transaction">MySqlTransaction</a>.RollbackAsync
+      RollbackAsync
     </td>
     <td>Rollback</td>
   </tr>
 </table>
 
-<span class="text-danger">*</span>Async Transaction methods are not part of ADO.NET, they are provided by
-MySqlConnector to allow database code to remain fully asynchronous.
-
 ### Exceptions: DbDataReader.GetFieldValueAsync and IsDBNullAsync
 
 Once `DbDataReader.ReadAsync` (or `DbDataReader.Read`) has returned `true`, the full contents of the current
@@ -139,33 +136,32 @@ Example assumes a [configured AppDb](/overview/configuration) object in the `MyS
 using System.Threading.Tasks;
 using System.Collections.Generic;
 
-namespace MySqlConnector.Examples
+namespace MySqlConnector.Examples;
+
+public class Program
 {
-    public class Program
+    public static async Task Main(string[] args)
     {
-        public static async Task Main(string[] args)
+        var tasks = new List<Task>();
+        for (var i=0; i<100; i++)
         {
-            var tasks = new List<Task>();
-            for (var i=0; i<100; i++)
-            {
-                tasks.Add(Controllers.SleepOne());
-            }
-            // these 100 queries should all complete in around
-            // 1 second if "Max Pool Size=100" (the default)
-            await Task.WhenAll(tasks);
+            tasks.Add(Controllers.SleepOne());
         }
+        // these 100 queries should all complete in around
+        // 1 second if "Max Pool Size=100" (the default)
+        await Task.WhenAll(tasks);
     }
+}
 
-    public class Controllers
+public class Controllers
+{
+    public static async Task SleepOne()
     {
-        public static async Task SleepOne()
-        {
-            using var db = new AppDb();
-            await db.Connection.OpenAsync();
-            using var cmd = db.Connection.CreateCommand();
-            cmd.CommandText = @"SELECT SLEEP(1)";
-            await cmd.ExecuteNonQueryAsync();
-        }
+        using var db = new AppDb();
+        await db.Connection.OpenAsync();
+        using var cmd = db.Connection.CreateCommand();
+        cmd.CommandText = @"SELECT SLEEP(1)";
+        await cmd.ExecuteNonQueryAsync();
     }
 }
 ```
diff --git a/docs/content/tutorials/connect-to-mysql.md b/docs/content/tutorials/connect-to-mysql.md
index ee0bec476..1a23fed24 100644
--- a/docs/content/tutorials/connect-to-mysql.md
+++ b/docs/content/tutorials/connect-to-mysql.md
@@ -1,4 +1,5 @@
 ---
+lastmod: 2023-11-10
 date: 2019-11-18
 menu:
   main:
@@ -17,7 +18,7 @@ This tutorial will teach you how to connect to MySQL from .NET Core using C#.
 First, install the [MySqlConnector NuGet package](https://www.nuget.org/packages/MySqlConnector/). From
 a command prompt, run:
 
-```
+```txt
 dotnet add package MySqlConnector
 ```
 
@@ -39,25 +40,32 @@ If you are using ASP.NET Core, your connection string will usually be stored in
 
 ```json
 {
-    ....
-    "ConnectionStrings": {
-        "Default": "Server=YOURSERVER;User ID=YOURUSERID;Password=YOURPASSWORD;Database=YOURDATABASE"
-    }
+  ....
+  "ConnectionStrings": {
+    "Default": "Server=YOURSERVER;User ID=YOURUSERID;Password=YOURPASSWORD;Database=YOURDATABASE"
+  }
 }
 ```
 
 ## 3. Configure Service (ASP.NET Core)
 
-If using ASP.NET Core, you will want to register a database connection in `Startup.cs`:
+If using ASP.NET Core, you will want to register a database connection in `Program.cs`:
+
+**Recommended** Install [MySqlConnector.DependencyInjection](https://www.nuget.org/packages/MySqlConnector.DependencyInjection/) via `dotnet add package MySqlConnector.DependencyInjection`. Then add the following to `Program.cs`:
 
 ```csharp
-public void ConfigureServices(IServiceCollection services)
-{
-    // ...
-    services.AddTransient<MySqlConnection>(_ => new MySqlConnection(Configuration["ConnectionStrings:Default"]));
-}
+builder.Services.AddMySqlDataSource(builder.Configuration.GetConnectionString("Default")!);
 ```
 
+Alternatively, register a transient `MySqlConnection` object explicitly:
+
+```csharp
+builder.services.AddTransient<MySqlConnection>(_ =>
+    new MySqlConnection(builder.Configuration.GetConnectionString["Default"]));
+```
+
+The advantage of using `AddMySqlDataSource` is that it will automatically integrate with logging, and also register `DbDataSource` and `DbConnection` with the service collection.
+
 ## 4. Open and Use the Connection
 
 In ASP.NET Core, the `MySqlConnection` object will be dependency-injected into your `Controller` class. For
diff --git a/docs/content/tutorials/dapper.md b/docs/content/tutorials/dapper.md
new file mode 100644
index 000000000..549b67a4a
--- /dev/null
+++ b/docs/content/tutorials/dapper.md
@@ -0,0 +1,98 @@
+---
+date: 2023-01-28
+title: Dapper
+customtitle: "Tutorial: Connect to MySQL with Dapper using C#"
+description: How to create a C# .NET application that connects to MySQL with MySqlConnector and Dapper.
+menu:
+  main:
+    parent: tutorials
+weight: 11
+---
+
+# Tutorial: Connect to MySQL with Dapper using C#
+
+## Introduction
+
+[Dapper](https://github.com/DapperLib/Dapper/blob/main/Readme.md) is a popular "micro-ORM" for connecting to databases from .NET.
+It can be used with MySqlConnector to connect to MySQL and retrieve data.
+Here's how to do it using C#.
+
+## 1. Create Your Project
+
+If you don't already have a .NET project, create one using Visual Studio, or by running `dotnet new console` or `dotnet new webapi` at the command line.
+
+## 2. Install NuGet Packages
+
+You will need to install the following NuGet packages:
+
+* [MySqlConnector](https://www.nuget.org/packages/MySqlConnector/): `dotnet add package MySqlConnector`
+* [Dapper](https://www.nuget.org/packages/Dapper/): `dotnet add package Dapper`
+
+## 3. Build Your Connection String
+
+Build your connection string by substituting the appropriate values in this template:
+
+```txt
+Server=YOURSERVER; User ID=YOURUSERID; Password=YOURPASSWORD; Database=YOURDATABASE
+```
+
+In an ASP.NET Core Web Application or Web API, store this connection string in `appsettings.json`:
+
+```json
+{
+  ....
+  "ConnectionStrings": {
+    "Default": "Server=YOURSERVER; User ID=YOURUSERID; Password=YOURPASSWORD; Database=YOURDATABASE"
+  }
+}
+```
+
+In a console application, you can store this in a constant string:
+
+```csharp
+const string connectionString = "Server=YOURSERVER; User ID=YOURUSERID; Password=YOURPASSWORD; Database=YOURDATABASE";
+```
+
+## 4. Create Your Connection
+
+In an ASP.NET Core Web Application or Web API, you can use dependency injection to create a connection in `Program.cs`:
+
+```csharp
+builder.Services.AddTransient(x =>
+  new MySqlConnection(builder.Configuration.GetConnectionString("Default")));
+```
+
+In a console application, you can create a connection in your `Main` method:
+
+```csharp
+using var connection = new MySqlConnection(connectionString);
+```
+
+## 5. Query Your Database
+
+You can now use Dapper to query your database by using the extension methods it adds on `IDbConnection`:
+
+For ASP.NET Core with minimal APIs in .NET 7.0:
+
+```csharp
+app.MapGet("/users/{userId}", (int userId, [FromServices] MySqlConnection connection) =>
+{
+    var users = connection.Query<string>("select user_name from users where user_id = @userId", new { userId });
+    if (users.FirstOrDefault() is string userName)
+        return Results.Ok(new { Name = userName });
+    else
+        return Results.NotFound();
+});
+```
+
+For a console application:
+
+```csharp
+var userId = 1;
+var users = connection.Query<string>("select user_name from users where user_id = @userId", new { userId });
+Console.WriteLine($"Name: {users.FirstOrDefault()}");
+```
+
+## 6. Use More Dapper Features
+
+To learn about more advanced Dapper features, such as list support and multi-mapping, see the [Dapper documentation](https://github.com/DapperLib/Dapper/blob/main/Readme.md#features).
diff --git a/docs/content/tutorials/efcore.md b/docs/content/tutorials/efcore.md
new file mode 100644
index 000000000..d37e42c1f
--- /dev/null
+++ b/docs/content/tutorials/efcore.md
@@ -0,0 +1,150 @@
+---
+date: 2023-01-29
+title: EF Core
+customtitle: "Tutorial: Connect to MySQL with Entity Framework Core in C#"
+description: How to create a C# .NET application that connects to MySQL using Entity Framework Core.
+menu:
+  main:
+    parent: tutorials
+weight: 12
+---
+
+# Tutorial: Connect to MySQL with Entity Framework Core
+
+## Introduction
+
+Entity Framework Core (EF Core) is Microsoft's open-source object/relational mapper that allows C# developers to work with a relational database using .NET objects.
+Pomelo.EntityFrameworkCore.MySql is an EF Core provider for MySQL.
+It uses MySqlConnector to provide the core database connectivity.
+
+## 1. Create Your Project
+
+In this tutorial, we'll use a console application to demonstrate the core concepts.
+Create a console application, then add the Pomelo.EntityFrameworkCore.MySql NuGet package:
+
+```txt
+dotnet new console -o EFCoreMySQL
+cd EFCoreMySQL
+dotnet add package Pomelo.EntityFrameworkCore.MySql
+ ```
+
+## 2. Build Your Connection String
+
+Build your connection string by substituting the appropriate values in this template:
+
+```txt
+Server=YOURSERVER; User ID=YOURUSERID; Password=YOURPASSWORD; Database=YOURDATABASE
+```
+
+In this tutorial application, we will store this in a constant string:
+
+```csharp
+const string connectionString = "Server=localhost; User ID=root; Password=pass; Database=blog";
+```
+
+## 3. Create a Database Context
+
+The `BlogDataContext` is used for accessing application data through Entity Framework.
+It derives from the Entity Framework `DbContext` class and has public properties for accessing data.
+
+The `OnConfiguring()` method is used to connect to MySQL by using `options.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));`.
+
+We define two model classes, `Author` and `Post`, which will be stored in our database.
+
+Add the following code to the end of `Program.cs`:
+
+```csharp
+public class BlogDataContext : DbContext
+{
+    static readonly string connectionString = "Server=localhost; User ID=root; Password=pass; Database=blog";
+
+    public DbSet<Author> Authors { get; set; }
+    public DbSet<Post> Posts { get; set; }
+
+    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+    {
+        optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));
+    }
+}
+
+public class Post
+{
+    public int PostId { get; set; }
+    public string Title { get; set; }
+    public string Content { get; set; }
+    public Author Author { get; set; }
+}
+
+public class Author
+{
+    public int AuthorId { get; set; }
+    public string Name { get; set; }
+    public string Email { get; set; }
+
+    public List<Post> Posts { get; set; }
+}
+```
+
+## 4. Create Your Database
+
+Use [EF migrations](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/) to create your database by running the following commands:
+
+```
+dotnet tool install --global dotnet-ef
+dotnet add package Microsoft.EntityFrameworkCore.Design
+dotnet ef migrations add InitialCreate
+dotnet ef database update
+```
+
+## 5. Create and Save Data
+
+Use LINQ queries on the properties of the `BlogDataContext` class to create and save data.
+Add the following code to the top of `Program.cs` (before the `BlogDataContext` class):
+
+```csharp
+// create new blog posts
+using (var context = new BlogDataContext())
+{
+    var john = new Author { Name = "John T. Author", Email = "john@example.com" };
+    context.Authors.Add(john);
+
+    var jane = new Author { Name = "Jane Q. Hacker", Email = "jane@example.com" };
+    context.Authors.Add(jane);
+
+    var post = new Post { Title = "Hello World", Content = "I wrote an app using EF Core!", Author = jane };
+    context.Posts.Add(post);
+    post = new Post { Title = "How to use EF Core", Content = "It's pretty easy", Author = john };
+    context.Posts.Add(post);
+
+    context.SaveChanges();
+}
+
+// query the blog posts, using a join between the two tables
+using (var context = new BlogDataContext())
+{
+    var posts = context.Posts
+        .Include(p => p.Author)
+        .ToList();
+
+    foreach (var post in posts)
+    {
+        Console.WriteLine($"{post.Title} by {post.Author.Name}");
+    }
+}
+```
+
+## 6. Run Your Application
+
+Execute `dotnet run`. The program should run and print the following output:
+
+```
+How to use EF Core by John T. Author
+Hello World by Jane Q. Hacker
+```
+
+## Further Reading
+
+For more information, see:
+
+* [Entity Framework Core](https://learn.microsoft.com/en-us/ef/core/) (MSDN)
+* [Pomelo.EntityFrameworkCore.MySql README](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/blob/master/README.md)
diff --git a/docs/content/tutorials/migrating-from-connector-net.md b/docs/content/tutorials/migrating-from-connector-net.md
index a9303bf0d..270ddc7ea 100644
--- a/docs/content/tutorials/migrating-from-connector-net.md
+++ b/docs/content/tutorials/migrating-from-connector-net.md
@@ -1,15 +1,27 @@
 ---
-lastmod: 2022-01-21
+lastmod: 2024-02-05
 date: 2016-10-16
 menu:
   main:
     parent: tutorials
-title: Migrating from Connector/NET
+title: Migrating from MySql.Data
+customtitle: MySqlConnector vs MySql.Data
 weight: 20
 ---
 
-Migrating from Connector/NET
-============================
+# MySqlConnector vs MySql.Data
+
+Compared to MySql.Data (i.e., Oracle's MySQL Connector/NET), MySqlConnector has the following benefits:
+
+* Higher performance (see [benchmarks](/#performance)) and lower memory usage
+* More reliable, particularly when using `Async` methods (see [bugs fixed](#fixed-bugs))
+* Leading edge: first MySQL driver to support .NET Core, `DbBatch`, `DateOnly`, `DbDataSource` and other new .NET features
+* Better compatibility with non-MySQL databases ([MariaDB](https://bugs.mysql.com/bug.php?id=109331), Amazon Aurora, etc.)
+* Better license (MIT, not [GPL 2.0](https://github.com/mysql/mysql-connector-net/blob/8.0/LICENSE) with [Universal FOSS Exception](https://oss.oracle.com/licenses/universal-foss-exception/) or [commercial license](https://www.mysql.com/about/legal/licensing/oem/))
+* Faster pace of development (regular [NuGet releases](https://www.nuget.org/packages/MySqlConnector#versions-body-tab), not once per quarter)
+* Development happens in the open on GitHub
+
+## Migrating from Connector/NET
 
 ### Namespace
 
@@ -26,10 +38,16 @@ In a .NET Framework application, make the following `app.config` change to regis
 <system.data>
   <DbProviderFactories>
     <!-- REMOVE THIS -->
-    <!-- add name="MySQL Data Provider" invariant="MySql.Data.MySqlClient" description=".Net Framework Data Provider for MySQL" type="MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=8.0.20.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d" / -->
+    <!-- add name="MySQL Data Provider"
+      invariant="MySql.Data.MySqlClient"
+      description=".Net Framework Data Provider for MySQL"
+      type="MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=9.4.0.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d" / -->
 
     <!-- ADD THIS -->
-    <add name="MySqlConnector" invariant="MySqlConnector" description="MySQL Connector for .NET" type="MySqlConnector.MySqlConnectorFactory, MySqlConnector, Culture=neutral, PublicKeyToken=d33d3e53aa5f8c92" />
+    <add name="MySqlConnector"
+      invariant="MySqlConnector"
+      description="MySQL Connector for .NET"
+      type="MySqlConnector.MySqlConnectorFactory, MySqlConnector, Culture=neutral, PublicKeyToken=d33d3e53aa5f8c92" />
   </DbProviderFactories>
 </system.data>
 ```
@@ -73,7 +91,7 @@ MySqlConnector has some different default connection string options:
     <td><code>LoadBalance</code></td>
     <td>Default is <code>RoundRobin</code></td>
     <td>(not configurable, effective default is <code>FailOver</code>)</td>
-    <td>Connector/NET currently has <a href="https://bugs.mysql.com/bug.php?id=81650" title="MySQL bug #81650">a bug</a> that prevents multiple host names being used.</td>
+    <td>Connector/NET versions prior to v8.0.19 have <a href="https://bugs.mysql.com/bug.php?id=81650" title="MySQL bug #81650">a bug</a> that prevents multiple host names being used.</td>
   </tr>
   <tr>
     <td><code>ServerRSAPublicKeyFile</code></td>
@@ -94,8 +112,8 @@ supported in MySqlConnector, see the [Connection Options](/connection-options).
 
 Connector/NET implements the standard ADO.NET async methods, and adds some new ones (e.g., `MySqlConnection.BeginTransactionAsync`,
 `MySqlDataAdapter.FillAsync`) that don't exist in ADO.NET. None of these methods have an asynchronous implementation,
-but all execute synchronously then return a completed `Task`. This is a [longstanding known bug](https://bugs.mysql.com/bug.php?id=70111)
-in Connector/NET.
+but all execute synchronously then return a completed `Task`. This [longstanding known bug](https://bugs.mysql.com/bug.php?id=70111) was fixed in
+in Connector/NET v8.0.33.
 
 Because the Connector/NET methods aren't actually asynchronous, porting client code to MySqlConnector (which is asynchronous)
 can expose bugs that only occur when an async method completes asynchronously and resumes the `await`-ing code
@@ -104,7 +122,7 @@ and follow the [best practices in async programming](https://msdn.microsoft.com/
 
 ### Implicit Conversions
 
-Connector/NET allows `MySqlDataReader.GetString()` to be called on many non-textual columns, and will implicitly
+Prior to v8.3.0, Connector/NET allows `MySqlDataReader.GetString()` to be called on many non-textual columns, and will implicitly
 convert the value to a `string` (using the current locale). This is a frequent source of locale-dependent bugs, so
 MySqlConnector follows typical ADO.NET practice (e.g., SqlClient, npgsql) and disallows this (by throwing an `InvalidCastException`).
 
@@ -126,7 +144,7 @@ Connector/NET allows a `MySqlConnection` object to be reused after it has been d
 object to be created. See [#331](https://github.com/mysql-net/MySqlConnector/issues/331) for more details.
 
 The return value of `MySqlConnection.BeginTransactionAsync` has changed from `Task<MySqlTransaction>` to
-`ValueTask<MySqlTransaction>` to match the [standard API in .NET Core 3.0](https://github.com/dotnet/corefx/issues/35012).
+`ValueTask<MySqlTransaction>` to match the [standard API in .NET Core 3.0](https://github.com/dotnet/runtime/issues/28596).
 (This method does always perform I/O, so `ValueTask` is not an optimization for MySqlConnector.)
 
 ### MySqlConnectionStringBuilder
@@ -147,7 +165,7 @@ If `MySqlCommand.CommandType` is `CommandType.StoredProcedure`, the stored proce
 
 Connector/NET provides `MySqlDataAdapter.FillAsync`, `FillSchemaAsync`, and `UpdateAsync` methods, but these methods
 have a synchronous implementation. MySqlConnector only adds “Async” methods when they can be implemented asynchronously.
-This functionality depends on [dotnet/corefx#20658](https://github.com/dotnet/corefx/issues/20658) being implemented first.
+This functionality depends on [dotnet/runtime#22109](https://github.com/dotnet/runtime/issues/22109) being implemented first.
 To migrate code, change it to call the synchronous methods instead.
 
 ### MySqlGeometry
@@ -182,120 +200,150 @@ or used only as positional parameters if they’re unnamed.
 For consistency with other ADO.NET providers, MySqlConnector will throw `InvalidOperationException` (instead of `MySqlException`)
 for various precondition checks that indicate misuse of the API (and not a problem related to MySQL Server).
 
-### Fixed Bugs
+## Fixed Bugs
 
-The following bugs in Connector/NET are fixed by switching to MySqlConnector. (~~Strikethrough~~ indicates bugs that have since been fixed in a newer version of Connector/NET, but were fixed first in MySqlConnector.)
+The following bugs in Connector/NET are fixed by switching to MySqlConnector.
 
-* [#14115](https://bugs.mysql.com/bug.php?id=14115): Compound statements are not supported by `MySqlCommand.Prepare`
+### Open Bugs
+
+* [#14115](https://bugs.mysql.com/bug.php?id=14115): Compound statements are not supported by `MySqlCommand.Prepare` (Will never be supported)
 * [#37283](https://bugs.mysql.com/bug.php?id=37283), [#70587](https://bugs.mysql.com/bug.php?id=70587): Distributed transactions are not supported
 * [#50773](https://bugs.mysql.com/bug.php?id=50773): Can’t use multiple connections within one TransactionScope
 * [#61477](https://bugs.mysql.com/bug.php?id=61477): `ColumnOrdinal` in schema table is 1-based
-* ~~[#66476](https://bugs.mysql.com/bug.php?id=66476), [#106368](https://bugs.mysql.com/bug.php?id=106368): Connection pool uses queue instead of stack~~
-* [#70111](https://bugs.mysql.com/bug.php?id=70111): `Async` methods execute synchronously
-* ~~[#70686](https://bugs.mysql.com/bug.php?id=70686): `TIME(3)` and `TIME(6)` fields serialize milliseconds incorrectly~~
 * [#72494](https://bugs.mysql.com/bug.php?id=72494), [#83330](https://bugs.mysql.com/bug.php?id=83330): EndOfStreamException inserting large blob with UseCompression=True
-* [#73610](https://bugs.mysql.com/bug.php?id=73610): Invalid password exception has wrong number
 * [#73788](https://bugs.mysql.com/bug.php?id=73788): Can’t use `DateTimeOffset`
-* ~~[#75604](https://bugs.mysql.com/bug.php?id=75604): Crash after 29.4 days of uptime~~
 * [#75917](https://bugs.mysql.com/bug.php?id=75917), [#76597](https://bugs.mysql.com/bug.php?id=76597), [#77691](https://bugs.mysql.com/bug.php?id=77691), [#78650](https://bugs.mysql.com/bug.php?id=78650), [#78919](https://bugs.mysql.com/bug.php?id=78919), [#80921](https://bugs.mysql.com/bug.php?id=80921), [#82136](https://bugs.mysql.com/bug.php?id=82136): “Reading from the stream has failed” when connecting to a server
 * [#77421](https://bugs.mysql.com/bug.php?id=77421): Connection is not reset when pulled from the connection pool
-* [#78426](https://bugs.mysql.com/bug.php?id=78426): Unknown database exception has wrong number
-* ~~[#78760](https://bugs.mysql.com/bug.php?id=78760): Error when using tabs and newlines in SQL statements~~
-* ~~[#78917](https://bugs.mysql.com/bug.php?id=78917), [#79196](https://bugs.mysql.com/bug.php?id=79196), [#82292](https://bugs.mysql.com/bug.php?id=82292), [#89040](https://bugs.mysql.com/bug.php?id=89040): `TINYINT(1)` values start being returned as `sbyte` after `NULL`~~
-* ~~[#80030](https://bugs.mysql.com/bug.php?id=80030): Slow to connect with pooling disabled~~
-* ~~[#81650](https://bugs.mysql.com/bug.php?id=81650), [#88962](https://bugs.mysql.com/bug.php?id=88962): `Server` connection string option may now contain multiple, comma separated hosts that will be tried in order until a connection succeeds~~
 * [#83229](https://bugs.mysql.com/bug.php?id=83329): “Unknown command” exception inserting large blob with UseCompression=True
-* ~~[#83649](https://bugs.mysql.com/bug.php?id=83649): Connection cannot be made using IPv6~~
-* ~~[#84220](https://bugs.mysql.com/bug.php?id=84220): Cannot call a stored procedure with `.` in its name~~
-* ~~[#84701](https://bugs.mysql.com/bug.php?id=84701): Can’t create a parameter using a 64-bit enum with a value greater than int.MaxValue~~
 * [#85185](https://bugs.mysql.com/bug.php?id=85185): `ConnectionReset=True` does not preserve connection charset
-* ~~[#86263](https://bugs.mysql.com/bug.php?id=86263): Transaction isolation level affects all transactions in session~~
-* ~~[#87307](https://bugs.mysql.com/bug.php?id=87307): NextResult hangs instead of timing out~~
-* ~~[#87316](https://bugs.mysql.com/bug.php?id=87316): MySqlCommand.CommandTimeout can be set to a negative value~~
-* ~~[#87868](https://bugs.mysql.com/bug.php?id=87868): `ColumnSize` in schema table is incorrect for `CHAR(36)` and `BLOB` columns~~
-* ~~[#87876](https://bugs.mysql.com/bug.php?id=87876): `IsLong` is schema table is incorrect for `LONGTEXT` and `LONGBLOB` columns~~
-* ~~[#88058](https://bugs.mysql.com/bug.php?id=88058): `decimal(n, 0)` has wrong `NumericPrecision`~~
 * [#88124](https://bugs.mysql.com/bug.php?id=88124): CommandTimeout isn’t reset when calling Read/NextResult
-* ~~[#88472](https://bugs.mysql.com/bug.php?id=88472): `TINYINT(1)` is not returned as `bool` if `MySqlCommand.Prepare` is called~~
 * [#88611](https://bugs.mysql.com/bug.php?id=88611): `MySqlCommand` can be executed even if it has “wrong” transaction
-* ~~[#88660](https://bugs.mysql.com/bug.php?id=88660): `MySqlClientFactory.Instance.CreateDataAdapter()` and `CreateCommandBuilder` return `null`~~
 * [#89085](https://bugs.mysql.com/bug.php?id=89085): `MySqlConnection.Database` not updated after `USE database;`
-* ~~[#89159](https://bugs.mysql.com/bug.php?id=89159), [#97242](https://bugs.mysql.com/bug.php?id=97242): `MySqlDataReader` cannot outlive `MySqlCommand`~~
 * [#89335](https://bugs.mysql.com/bug.php?id=89335): `MySqlCommandBuilder.DeriveParameters` fails for `JSON` type
-* ~~[#89639](https://bugs.mysql.com/bug.php?id=89639): `ReservedWords` schema contains incorrect data~~
-* ~~[#90086](https://bugs.mysql.com/bug.php?id=90086): `MySqlDataReader` is closed by an unrelated command disposal~~
 * [#91123](https://bugs.mysql.com/bug.php?id=91123): Database names are case-sensitive when calling a stored procedure
 * [#91199](https://bugs.mysql.com/bug.php?id=91199): Can't insert `MySqlDateTime` values
-* ~~[#91751](https://bugs.mysql.com/bug.php?id=91751): `YEAR` column retrieved incorrectly with prepared command~~
-* ~~[#91752](https://bugs.mysql.com/bug.php?id=91752): `00:00:00` is converted to `NULL` with prepared command~~
 * [#91753](https://bugs.mysql.com/bug.php?id=91753): Unnamed parameter not supported by `MySqlCommand.Prepare`
 * [#91754](https://bugs.mysql.com/bug.php?id=91754): Inserting 16MiB `BLOB` shifts it by four bytes when prepared
-* ~~[#91770](https://bugs.mysql.com/bug.php?id=91770): `TIME(n)` column loses microseconds with prepared command~~
 * [#92367](https://bugs.mysql.com/bug.php?id=92367): `MySqlDataReader.GetDateTime` and `GetValue` return inconsistent values
-* [#92465](https://bugs.mysql.com/bug.php?id=92465): “There is already an open DataReader” `MySqlException` thrown from `TransactionScope.Dispose`
-* [#92734](https://bugs.mysql.com/bug.php?id=92734): `MySqlParameter.Clone` doesn't copy all property values
-* ~~[#92789](https://bugs.mysql.com/bug.php?id=92789): Illegal connection attributes written for non-ASCII values~~
-* ~~[#92912](https://bugs.mysql.com/bug.php?id=92912): `MySqlDbType.LongText` values encoded incorrectly with prepared statements~~
-* ~~[#92982](https://bugs.mysql.com/bug.php?id=92982), [#93399](https://bugs.mysql.com/bug.php?id=93399)~~: `FormatException` thrown when connecting to MySQL Server 8.0.13~~
 * [#93047](https://bugs.mysql.com/bug.php?id=93047): `MySqlDataAdapter` throws timeout exception when an error occurs
-* ~~[#93202](https://bugs.mysql.com/bug.php?id=93202): Connector runs `SHOW VARIABLES` when connection is made~~
 * [#93220](https://bugs.mysql.com/bug.php?id=93220): Can’t call FUNCTION when parameter name contains parentheses
-* [#93370](https://bugs.mysql.com/bug.php?id=93370): `MySqlParameterCollection.Add` precondition check isn't consistent
-* ~~[#93374](https://bugs.mysql.com/bug.php?id=93374): `MySqlDataReader.GetStream` throws `IndexOutOfRangeException`~~
 * [#93825](https://bugs.mysql.com/bug.php?id=93825): `MySqlException` loses data when serialized
-* ~~[#94075](https://bugs.mysql.com/bug.php?id=94075): `MySqlCommand.Cancel` throws exception~~
 * [#94760](https://bugs.mysql.com/bug.php?id=94760): `MySqlConnection.OpenAsync(CancellationToken)` doesn’t respect cancellation token
 * [#95348](https://bugs.mysql.com/bug.php?id=95348): Inefficient query when executing stored procedures
 * [#95436](https://bugs.mysql.com/bug.php?id=95436): Client doesn't authenticate with PEM certificate
-* ~~[#95984](https://bugs.mysql.com/bug.php?id=95984): “Incorrect arguments to mysqld_stmt_execute” using prepared statement with `MySqlDbType.JSON`~~
-* ~~[#95986](https://bugs.mysql.com/bug.php?id=95986): “Incorrect integer value” using prepared statement with `MySqlDbType.Int24`~~
-* ~~[#96355](https://bugs.mysql.com/bug.php?id=96355), [#96614](https://bugs.mysql.com/bug.php?id=96614): `Could not load file or assembly 'Renci.SshNet'` when opening connection~~
-* ~~[#96498](https://bugs.mysql.com/bug.php?id=96498): `WHERE` clause using `MySqlGeometry` as parameter finds no rows~~
-* ~~[#96499](https://bugs.mysql.com/bug.php?id=96499): `MySqlException` when inserting a `MySqlGeometry` value~~
 * [#96500](https://bugs.mysql.com/bug.php?id=96500): `MySqlDataReader.GetFieldValue<MySqlGeometry>` throws `InvalidCastException`
 * [#96636](https://bugs.mysql.com/bug.php?id=96636): `MySqlConnection.Open()` slow under load when using SSL
 * [#96717](https://bugs.mysql.com/bug.php?id=96717): Not compatible with MySQL Server 5.0
-* [#97061](https://bugs.mysql.com/bug.php?id=97061): `MySqlCommand.LastInsertedId` returns 0 after executing multiple statements
 * [#97067](https://bugs.mysql.com/bug.php?id=97067): Aggregate functions on BIT(n) columns return wrong result
-* ~~[#97300](https://bugs.mysql.com/bug.php?id=97300): `GetSchemaTable()` returns table for stored procedure with output parameters~~
-* ~~[#97448](https://bugs.mysql.com/bug.php?id=97448): Connecting fails if more than one IP is found in DNS for a named host~~
-* [#97473](https://bugs.mysql.com/bug.php?id=97473): `MySqlConnection.Clone` discloses connection password
 * [#97738](https://bugs.mysql.com/bug.php?id=97738): Cannot use PEM files when account uses `require subject`
 * [#97872](https://bugs.mysql.com/bug.php?id=97872): `KeepAlive` in connection string throws exception on .NET Core
-* ~~[#98322](https://bugs.mysql.com/bug.php?id=98322): `new MySqlConnection(null)` throws `NullReferenceException`~~
-* [#99091](https://bugs.mysql.com/bug.php?id=99091): Unexpected return value getting integer for `TINYINT(1)` column
-* ~~[#99793](https://bugs.mysql.com/bug.php?id=99793): Prepared stored procedure command doesn't verify parameter types~~
-* ~~[#100159](https://bugs.mysql.com/bug.php?id=100159): SQL with DateTime parameter returns String value~~
-* ~~[#100208](https://bugs.mysql.com/bug.php?id=100208): `GetSchema("Procedures")` returns `ROUTINE_DEFINITION` of `"System.Byte[]"`~~
-* ~~[#100218](https://bugs.mysql.com/bug.php?id=100218): `TIME(n)` microsecond values deserialized incorrectly with prepared command~~
-* ~~[#100306](https://bugs.mysql.com/bug.php?id=100306): `Command.Prepare` sends wrong statement to server~~
-* ~~[#100522](https://bugs.mysql.com/bug.php?id=100522): `	MySqlCommand.Parameters.Insert(-1)` succeeds but should fail~~
-* ~~[#101252](https://bugs.mysql.com/bug.php?id=101252): Can't query `CHAR(36)` column containing `NULL`~~
-* ~~[#101253](https://bugs.mysql.com/bug.php?id=101253): Default value for `MySqlParameter.Value` changed from null to `0`~~
-* ~~[#101302](https://bugs.mysql.com/bug.php?id=101302): Stored Procedure `BOOL` parameter can only be mapped to `MySqlDbType.Byte`~~
-* ~~[#101485](https://bugs.mysql.com/bug.php?id=101485): Stored Procedure `JSON` parameter throws “Unhandled type encountered” `MySqlException`~~
-* [#101507](https://bugs.mysql.com/bug.php?id=101507): `MySqlCommand.Cancel` throws `NullReferenceException` for a closed connection
-* ~~[#101714](https://bugs.mysql.com/bug.php?id=101714): Extremely slow performance reading result sets~~
-* ~~[#102303](https://bugs.mysql.com/bug.php?id=102303): Preparing command with incorrect parameter type puts connection in bad state~~
-* ~~[#102593](https://bugs.mysql.com/bug.php?id=102593): Can't use `MemoryStream` as `MySqlParameter.Value`~~
-* ~~[#103390](https://bugs.mysql.com/bug.php?id=103390): Can't query `CHAR(36)` column if `MySqlCommand` is prepared~~
-* ~~[#103430](https://bugs.mysql.com/bug.php?id=103430): Can't connect using named pipe on Windows~~
-* ~~[#103801](https://bugs.mysql.com/bug.php?id=103801): `TimeSpan` parameters lose microseconds with prepared statement~~
 * [#103819](https://bugs.mysql.com/bug.php?id=103819): Can't use `StringBuilder` containing non-BMP characters as `MySqlParameter.Value`
-* ~~[#104910](https://bugs.mysql.com/bug.php?id=104910): `MySqlConnectionStringBuilder.TryGetValue` always returns `false`~~
-* ~~[#104913](https://bugs.mysql.com/bug.php?id=104913): Cannot execute stored procedure with backtick in name~~
-* ~~[#105209](https://bugs.mysql.com/bug.php?id=105209): Timespan value of zero can't be read with prepared command~~
-* ~~[#105728](https://bugs.mysql.com/bug.php?id=105728): Named command parameters override query attribute values~~
-* ~~[#105730](https://bugs.mysql.com/bug.php?id=105730): `MySqlCommand.Clone` doesn't clone attributes~~
-* ~~[#105768](https://bugs.mysql.com/bug.php?id=105768): `MySqlCommandBuilder` doesn't support tables with `BIGINT UNSIGNED` primary key~~
 * [#105965](https://bugs.mysql.com/bug.php?id=105965): `MySqlParameterCollection.Add(object)` has quadratic performance
-* ~~[#106242](https://bugs.mysql.com/bug.php?id=106242): `MySqlConnection.Open` throws `AggregateException` instead of `MySqlException`~~
-* ~~[#106243](https://bugs.mysql.com/bug.php?id=106243): `CancellationToken` doesn't cancel `MySqlConnection.OpenAsync`~~
-* ~~[#106244](https://bugs.mysql.com/bug.php?id=106244): `MySqlDataReader.GetFieldValue<Stream>` throws `InvalidCastException`~~
-* [#106247](https://bugs.mysql.com/bug.php?id=106247): Can't use ~~`MySqlDbType.Enum` or~~ `MySqlDbType.Set` with prepared command
-* [#108756](https://bugs.mysql.com/bug.php?id=108756): Can't insert negative number using prepared statement with MySqlDbType.Int24]
+* [#107259](https://bugs.mysql.com/bug.php?id=107259): Character Set 'utf8mb3' is not supported by .NET Framework
 * [#108970](https://bugs.mysql.com/bug.php?id=108970): `MySqlConnectionStringBuilder.ContainsKey` method gives wrong result
 * [#109141](https://bugs.mysql.com/bug.php?id=109141): Insert of data into a table results in `System.ArgumentException`
 * [#109331](https://bugs.mysql.com/bug.php?id=109331): MySQL Connector/NET is incompatible with MariaDB 10.10 and later
 * [#109390](https://bugs.mysql.com/bug.php?id=109390): Transaction lock held after connection timeout exception
 * [#109476](https://bugs.mysql.com/bug.php?id=109476): `TransactionScope.Dispose` throws "Connection must be valid and open to rollback"
+* [#110790](https://bugs.mysql.com/bug.php?id=110790): `ExecuteReaderAsync` hangs instead of cancelling query after `CommandTimeout`
+* [#110791](https://bugs.mysql.com/bug.php?id=110791): `OpenAsync(CancellationToken)` doesn't throw for cancelled token
+* [#111797](https://bugs.mysql.com/bug.php?id=111797): Certain sequence of special characters can break connection string validation
+* [#112088](https://bugs.mysql.com/bug.php?id=112088): `BIT(n)` parameter size incorrectly returns 0
+* [#114567](https://bugs.mysql.com/bug.php?id=114567): MySql.Data 8.0.33 is twice as slow as previous versions
+* [#115155](https://bugs.mysql.com/bug.php?id=115155): MySql.Data executes `SET NAMES` using an unsupported value
+* [#115168](https://bugs.mysql.com/bug.php?id=115168): `MySqlDataReader.GetX` performs culture-sensitive conversions
+* [#115222](https://bugs.mysql.com/bug.php?id=115222): `MySqlPool.ClearAsync` is not thread-safe
+* [#115937](https://bugs.mysql.com/bug.php?id=115937): Transaction isn't released after I/O timeout
+* [#116146](https://bugs.mysql.com/bug.php?id=116146): `MySqlConnection.OpenAsync` throws exception when `UseCompression = true`
+* [#116257](https://bugs.mysql.com/bug.php?id=116257): `NullReferenceException` thrown when using prepared statement under OpenTelemetry
+* [#116382](https://bugs.mysql.com/bug.php?id=116382): `ExecuteNonQueryAsync` hangs with DDL if `UseCompression=True`
+
+### Fixed First by MySqlConnector
+
+These bugs are fixed in the latest version of MySQL Connector/NET, but were fixed first in MySqlConnector.
+
+* [#66476](https://bugs.mysql.com/bug.php?id=66476), [#106368](https://bugs.mysql.com/bug.php?id=106368): Connection pool uses queue instead of stack (fixed in v8.0.30)
+* [#70111](https://bugs.mysql.com/bug.php?id=70111): `Async` methods execute synchronously (fixed in v8.0.33)
+* [#70686](https://bugs.mysql.com/bug.php?id=70686): `TIME(3)` and `TIME(6)` fields serialize milliseconds incorrectly
+* [#73610](https://bugs.mysql.com/bug.php?id=73610): Invalid password exception has wrong number (fixed in v8.0.32)
+* [#74392](https://bugs.mysql.com/bug.php?id=74392): Can't use `Stream` to bulk load data
+* [#75604](https://bugs.mysql.com/bug.php?id=75604): Crash after 29.4 days of uptime
+* [#78426](https://bugs.mysql.com/bug.php?id=78426): Unknown database exception has wrong number
+* [#78760](https://bugs.mysql.com/bug.php?id=78760): Error when using tabs and newlines in SQL statements
+* [#78917](https://bugs.mysql.com/bug.php?id=78917), [#79196](https://bugs.mysql.com/bug.php?id=79196), [#82292](https://bugs.mysql.com/bug.php?id=82292), [#89040](https://bugs.mysql.com/bug.php?id=89040): `TINYINT(1)` values start being returned as `sbyte` after `NULL`
+* [#80030](https://bugs.mysql.com/bug.php?id=80030): Slow to connect with pooling disabled
+* [#81650](https://bugs.mysql.com/bug.php?id=81650), [#88962](https://bugs.mysql.com/bug.php?id=88962): `Server` connection string option may now contain multiple, comma separated hosts that will be tried in order until a connection succeeds
+* [#83649](https://bugs.mysql.com/bug.php?id=83649): Connection cannot be made using IPv6
+* [#84220](https://bugs.mysql.com/bug.php?id=84220): Cannot call a stored procedure with `.` in its name
+* [#84701](https://bugs.mysql.com/bug.php?id=84701): Can’t create a parameter using a 64-bit enum with a value greater than int.MaxValue
+* [#86263](https://bugs.mysql.com/bug.php?id=86263): Transaction isolation level affects all transactions in session
+* [#87307](https://bugs.mysql.com/bug.php?id=87307): NextResult hangs instead of timing out
+* [#87316](https://bugs.mysql.com/bug.php?id=87316): MySqlCommand.CommandTimeout can be set to a negative value
+* [#87868](https://bugs.mysql.com/bug.php?id=87868): `ColumnSize` in schema table is incorrect for `CHAR(36)` and `BLOB` columns
+* [#87876](https://bugs.mysql.com/bug.php?id=87876): `IsLong` is schema table is incorrect for `LONGTEXT` and `LONGBLOB` columns
+* [#88058](https://bugs.mysql.com/bug.php?id=88058): `decimal(n, 0)` has wrong `NumericPrecision`
+* [#88472](https://bugs.mysql.com/bug.php?id=88472): `TINYINT(1)` is not returned as `bool` if `MySqlCommand.Prepare` is called
+* [#88660](https://bugs.mysql.com/bug.php?id=88660): `MySqlClientFactory.Instance.CreateDataAdapter()` and `CreateCommandBuilder` return `null`
+* [#89159](https://bugs.mysql.com/bug.php?id=89159), [#97242](https://bugs.mysql.com/bug.php?id=97242): `MySqlDataReader` cannot outlive `MySqlCommand`
+* [#89639](https://bugs.mysql.com/bug.php?id=89639): `ReservedWords` schema contains incorrect data
+* [#90086](https://bugs.mysql.com/bug.php?id=90086): `MySqlDataReader` is closed by an unrelated command disposal
+* [#91751](https://bugs.mysql.com/bug.php?id=91751): `YEAR` column retrieved incorrectly with prepared command
+* [#91752](https://bugs.mysql.com/bug.php?id=91752): `00:00:00` is converted to `NULL` with prepared command
+* [#91770](https://bugs.mysql.com/bug.php?id=91770): `TIME(n)` column loses microseconds with prepared command
+* [#92465](https://bugs.mysql.com/bug.php?id=92465): “There is already an open DataReader” `MySqlException` thrown from `TransactionScope.Dispose`
+* [#92734](https://bugs.mysql.com/bug.php?id=92734): `MySqlParameter.Clone` doesn't copy all property values
+* [#92789](https://bugs.mysql.com/bug.php?id=92789): Illegal connection attributes written for non-ASCII values
+* [#92912](https://bugs.mysql.com/bug.php?id=92912): `MySqlDbType.LongText` values encoded incorrectly with prepared statements
+* [#92982](https://bugs.mysql.com/bug.php?id=92982), [#93399](https://bugs.mysql.com/bug.php?id=93399): `FormatException` thrown when connecting to MySQL Server 8.0.13
+* [#93202](https://bugs.mysql.com/bug.php?id=93202): Connector runs `SHOW VARIABLES` when connection is made
+* [#93370](https://bugs.mysql.com/bug.php?id=93370): `MySqlParameterCollection.Add` precondition check isn't consistent
+* [#93374](https://bugs.mysql.com/bug.php?id=93374): `MySqlDataReader.GetStream` throws `IndexOutOfRangeException`
+* [#94075](https://bugs.mysql.com/bug.php?id=94075): `MySqlCommand.Cancel` throws exception
+* [#95984](https://bugs.mysql.com/bug.php?id=95984): “Incorrect arguments to mysqld_stmt_execute” using prepared statement with `MySqlDbType.JSON`
+* [#95986](https://bugs.mysql.com/bug.php?id=95986): “Incorrect integer value” using prepared statement with `MySqlDbType.Int24`
+* [#96355](https://bugs.mysql.com/bug.php?id=96355), [#96614](https://bugs.mysql.com/bug.php?id=96614): `Could not load file or assembly 'Renci.SshNet'` when opening connection
+* [#96498](https://bugs.mysql.com/bug.php?id=96498): `WHERE` clause using `MySqlGeometry` as parameter finds no rows
+* [#96499](https://bugs.mysql.com/bug.php?id=96499): `MySqlException` when inserting a `MySqlGeometry` value
+* [#97061](https://bugs.mysql.com/bug.php?id=97061): `MySqlCommand.LastInsertedId` returns 0 after executing multiple statements
+* [#97300](https://bugs.mysql.com/bug.php?id=97300): `GetSchemaTable()` returns table for stored procedure with output parameters
+* [#97448](https://bugs.mysql.com/bug.php?id=97448): Connecting fails if more than one IP is found in DNS for a named host
+* [#97473](https://bugs.mysql.com/bug.php?id=97473): `MySqlConnection.Clone` discloses connection password
+* [#98322](https://bugs.mysql.com/bug.php?id=98322): `new MySqlConnection(null)` throws `NullReferenceException`
+* [#99091](https://bugs.mysql.com/bug.php?id=99091): Unexpected return value getting integer for `TINYINT(1)` column
+* [#99793](https://bugs.mysql.com/bug.php?id=99793): Prepared stored procedure command doesn't verify parameter types
+* [#100159](https://bugs.mysql.com/bug.php?id=100159): SQL with DateTime parameter returns String value
+* [#100208](https://bugs.mysql.com/bug.php?id=100208): `GetSchema("Procedures")` returns `ROUTINE_DEFINITION` of `"System.Byte[]"`
+* [#100218](https://bugs.mysql.com/bug.php?id=100218): `TIME(n)` microsecond values deserialized incorrectly with prepared command
+* [#100306](https://bugs.mysql.com/bug.php?id=100306): `Command.Prepare` sends wrong statement to server
+* [#100522](https://bugs.mysql.com/bug.php?id=100522): `MySqlCommand.Parameters.Insert(-1)` succeeds but should fail
+* [#101252](https://bugs.mysql.com/bug.php?id=101252): Can't query `CHAR(36)` column containing `NULL`
+* [#101253](https://bugs.mysql.com/bug.php?id=101253): Default value for `MySqlParameter.Value` changed from null to `0`
+* [#101302](https://bugs.mysql.com/bug.php?id=101302): Stored Procedure `BOOL` parameter can only be mapped to `MySqlDbType.Byte`
+* [#101485](https://bugs.mysql.com/bug.php?id=101485): Stored Procedure `JSON` parameter throws “Unhandled type encountered” `MySqlException`
+* [#101507](https://bugs.mysql.com/bug.php?id=101507): `MySqlCommand.Cancel` throws `NullReferenceException` for a closed connection
+* [#101714](https://bugs.mysql.com/bug.php?id=101714): Extremely slow performance reading result sets
+* [#102303](https://bugs.mysql.com/bug.php?id=102303): Preparing command with incorrect parameter type puts connection in bad state
+* [#102593](https://bugs.mysql.com/bug.php?id=102593): Can't use `MemoryStream` as `MySqlParameter.Value`
+* [#103390](https://bugs.mysql.com/bug.php?id=103390): Can't query `CHAR(36)` column if `MySqlCommand` is prepared
+* [#103430](https://bugs.mysql.com/bug.php?id=103430): Can't connect using named pipe on Windows
+* [#103801](https://bugs.mysql.com/bug.php?id=103801): `TimeSpan` parameters lose microseconds with prepared statement
+* [#104910](https://bugs.mysql.com/bug.php?id=104910): `MySqlConnectionStringBuilder.TryGetValue` always returns `false`
+* [#104913](https://bugs.mysql.com/bug.php?id=104913): Cannot execute stored procedure with backtick in name
+* [#105209](https://bugs.mysql.com/bug.php?id=105209): Timespan value of zero can't be read with prepared command
+* [#105516](https://bugs.mysql.com/bug.php?id=105516): Connector/Net doesn't recognize utf8mb3
+* [#105728](https://bugs.mysql.com/bug.php?id=105728): Named command parameters override query attribute values
+* [#105730](https://bugs.mysql.com/bug.php?id=105730): `MySqlCommand.Clone` doesn't clone attributes
+* [#105768](https://bugs.mysql.com/bug.php?id=105768): `MySqlCommandBuilder` doesn't support tables with `BIGINT UNSIGNED` primary key
+* [#106242](https://bugs.mysql.com/bug.php?id=106242): `MySqlConnection.Open` throws `AggregateException` instead of `MySqlException`
+* [#106243](https://bugs.mysql.com/bug.php?id=106243): `CancellationToken` doesn't cancel `MySqlConnection.OpenAsync`
+* [#106244](https://bugs.mysql.com/bug.php?id=106244): `MySqlDataReader.GetFieldValue<Stream>` throws `InvalidCastException`
+* [#106247](https://bugs.mysql.com/bug.php?id=106247): Can't use `MySqlDbType.Enum` or `MySqlDbType.Set` with prepared command
+* [#108756](https://bugs.mysql.com/bug.php?id=108756): Can't insert negative number using prepared statement with `MySqlDbType.Int24`
+* [#109670](https://bugs.mysql.com/bug.php?id=109670), [#109752](https://bugs.mysql.com/bug.php?id=109752): Many MSB3246 errors when referencing MySql.Data 8.0.32
+* [#109682](https://bugs.mysql.com/bug.php?id=109682): `MySqlParameter.Clone` loses specific `MySqlDbType`.
+* [#109683](https://bugs.mysql.com/bug.php?id=109683): `MySqlCommand.LastInsertedId` is incorrect if multiple rows are inserted.
+* [#109716](https://bugs.mysql.com/bug.php?id=109716), [#109727](https://bugs.mysql.com/bug.php?id=109727): Failed to add reference to 'comerr64'
+* [#110717](https://bugs.mysql.com/bug.php?id=110717), [#111759](https://bugs.mysql.com/bug.php?id=111759): MySql.Data is not thread-safe
+* [#110789](https://bugs.mysql.com/bug.php?id=110789): `OpenAsync` throws unhandled exception from thread pool
+* [#114527](https://bugs.mysql.com/bug.php?id=114527): Connector/NET hangs on second connection attempt to non-running database
+* [#117020](https://bugs.mysql.com/bug.php?id=117020): `MinPoolSize` configuration is not honored
diff --git a/docs/content/tutorials/net-core-mvc.md b/docs/content/tutorials/net-core-mvc.md
index aba61d55a..fec267d11 100644
--- a/docs/content/tutorials/net-core-mvc.md
+++ b/docs/content/tutorials/net-core-mvc.md
@@ -1,5 +1,5 @@
 ---
-lastmod: 2020-06-14
+lastmod: 2023-11-10
 date: 2016-10-16
 menu:
   main:
@@ -33,7 +33,7 @@ CREATE TABLE IF NOT EXISTS `BlogPost` (
 ### Initialize ASP.NET Core Web API
 
 Create a folder named `BlogPostApi`, then run `dotnet new webapi` at the root to create the initial project.
-Run `dotnet add package MySqlConnector`. You should have a working project at this point, use `dotnet run`
+Run `dotnet add package MySqlConnector.DependencyInjection`. You should have a working project at this point, use `dotnet run`
 to verify the project builds and runs successfully.
 
 ### Update Configuration Files
@@ -48,334 +48,219 @@ to verify the project builds and runs successfully.
             "Microsoft.Hosting.Lifetime": "Information"
         }
     },
+    "AllowedHosts": "*",
     "ConnectionStrings": {
-        "DefaultConnection": "Server=127.0.0.1;User ID=root;Password=pass;Port=3306;Database=blog"
-    }
-}
-```
-
-`AppDb.cs` is a disposable [Application Database Object](/overview/configuration/), adapted to read the ConnectionString
-from the Configuration Object:
-
-```csharp
-using System;
-using MySqlConnector;
-
-namespace BlogPostApi
-{
-    public class AppDb : IDisposable
-    {
-        public MySqlConnection Connection { get; }
-
-        public AppDb(string connectionString)
-        {
-            Connection = new MySqlConnection(connectionString);
-        }
-
-        public void Dispose() => Connection.Dispose();
+        "Default": "Server=127.0.0.1;User ID=root;Password=pass;Port=3306;Database=blog"
     }
 }
 ```
 
 ### .NET Core Startup
 
-`Startup.cs` contains runtime configuration and framework services. Add this call to `ConfigureServices` to make an instance of `AppDb` available to controller methods.
+`Program.cs` contains runtime configuration and framework services.
+Add this call (before `var app = builder.Build();`) to register a MySQL data source:
 
 ```csharp
-services.AddTransient<AppDb>(_ => new AppDb(Configuration["ConnectionStrings:DefaultConnection"]));
+builder.Services.AddMySqlDataSource(builder.Configuration.GetConnectionString("Default")!);
 ```
 
 Now our app is configured and we can focus on writing the core functionality!
 
 ### Models
 
-`BlogPost.cs` represents a single Blog Post, and contains Insert, Update, and Delete methods.
+`BlogPost.cs` is a Plain Old C# Object that represents a single Blog Post.
 
 ```csharp
-using System.Data;
-using System.Threading.Tasks;
-using MySqlConnector;
+namespace BlogPostApi;
 
-namespace BlogPostApi
+public class BlogPost
 {
-    public class BlogPost
-    {
-        public int Id { get; set; }
-        public string Title { get; set; }
-        public string Content { get; set; }
-
-        internal AppDb Db { get; set; }
-
-        public BlogPost()
-        {
-        }
-
-        internal BlogPost(AppDb db)
-        {
-            Db = db;
-        }
-
-        public async Task InsertAsync()
-        {
-            using var cmd = Db.Connection.CreateCommand();
-            cmd.CommandText = @"INSERT INTO `BlogPost` (`Title`, `Content`) VALUES (@title, @content);";
-            BindParams(cmd);
-            await cmd.ExecuteNonQueryAsync();
-            Id = (int) cmd.LastInsertedId;
-        }
-
-        public async Task UpdateAsync()
-        {
-            using var cmd = Db.Connection.CreateCommand();
-            cmd.CommandText = @"UPDATE `BlogPost` SET `Title` = @title, `Content` = @content WHERE `Id` = @id;";
-            BindParams(cmd);
-            BindId(cmd);
-            await cmd.ExecuteNonQueryAsync();
-        }
-
-        public async Task DeleteAsync()
-        {
-            using var cmd = Db.Connection.CreateCommand();
-            cmd.CommandText = @"DELETE FROM `BlogPost` WHERE `Id` = @id;";
-            BindId(cmd);
-            await cmd.ExecuteNonQueryAsync();
-        }
-
-        private void BindId(MySqlCommand cmd)
-        {
-            cmd.Parameters.Add(new MySqlParameter
-            {
-                ParameterName = "@id",
-                DbType = DbType.Int32,
-                Value = Id,
-            });
-        }
-
-        private void BindParams(MySqlCommand cmd)
-        {
-            cmd.Parameters.Add(new MySqlParameter
-            {
-                ParameterName = "@title",
-                DbType = DbType.String,
-                Value = Title,
-            });
-            cmd.Parameters.Add(new MySqlParameter
-            {
-                ParameterName = "@content",
-                DbType = DbType.String,
-                Value = Content,
-            });
-        }
-    }
+    public int Id { get; set; }
+    public string? Title { get; set; }
+    public string? Content { get; set; }
 }
 ```
 
-`BlogPostQuery.cs` contains commands to query Blog Posts from the database:
+`BlogPostRepository.cs` contains commands to create, retrieve, update, and delete blog posts from the database:
 
 ```csharp
-using System.Collections.Generic;
-using System.Data;
 using System.Data.Common;
-using System.Threading.Tasks;
 using MySqlConnector;
 
-namespace BlogPostApi
+namespace BlogPostApi;
+
+public class BlogPostRepository(MySqlDataSource database)
 {
-    public class BlogPostQuery
+    public async Task<BlogPost?> FindOneAsync(int id)
     {
-        public AppDb Db { get; }
+        using var connection = await database.OpenConnectionAsync();
+        using var command = connection.CreateCommand();
+        command.CommandText = @"SELECT `Id`, `Title`, `Content` FROM `BlogPost` WHERE `Id` = @id";
+        command.Parameters.AddWithValue("@id", id);
+        var result = await ReadAllAsync(await command.ExecuteReaderAsync());
+        return result.FirstOrDefault();
+    }
 
-        public BlogPostQuery(AppDb db)
-        {
-            Db = db;
-        }
+    public async Task<IReadOnlyList<BlogPost>> LatestPostsAsync()
+    {
+        using var connection = await database.OpenConnectionAsync();
+        using var command = connection.CreateCommand();
+        command.CommandText = @"SELECT `Id`, `Title`, `Content` FROM `BlogPost` ORDER BY `Id` DESC LIMIT 10;";
+        return await ReadAllAsync(await command.ExecuteReaderAsync());
+    }
 
-        public async Task<BlogPost> FindOneAsync(int id)
-        {
-            using var cmd = Db.Connection.CreateCommand();
-            cmd.CommandText = @"SELECT `Id`, `Title`, `Content` FROM `BlogPost` WHERE `Id` = @id";
-            cmd.Parameters.Add(new MySqlParameter
-            {
-                ParameterName = "@id",
-                DbType = DbType.Int32,
-                Value = id,
-            });
-            var result = await ReadAllAsync(await cmd.ExecuteReaderAsync());
-            return result.Count > 0 ? result[0] : null;
-        }
+    public async Task DeleteAllAsync()
+    {
+        using var connection = await database.OpenConnectionAsync();
+        using var command = connection.CreateCommand();
+        command.CommandText = @"DELETE FROM `BlogPost`";
+        await command.ExecuteNonQueryAsync();
+    }
 
-        public async Task<List<BlogPost>> LatestPostsAsync()
-        {
-            using var cmd = Db.Connection.CreateCommand();
-            cmd.CommandText = @"SELECT `Id`, `Title`, `Content` FROM `BlogPost` ORDER BY `Id` DESC LIMIT 10;";
-            return await ReadAllAsync(await cmd.ExecuteReaderAsync());
-        }
+    public async Task InsertAsync(BlogPost blogPost)
+    {
+        using var connection = await database.OpenConnectionAsync();
+        using var command = connection.CreateCommand();
+        command.CommandText = @"INSERT INTO `BlogPost` (`Title`, `Content`) VALUES (@title, @content);";
+        BindParams(command, blogPost);
+        await command.ExecuteNonQueryAsync();
+        blogPost.Id = (int)command.LastInsertedId;
+    }
 
-        public async Task DeleteAllAsync()
-        {
-            using var txn = await Db.Connection.BeginTransactionAsync();
-            using var cmd = Db.Connection.CreateCommand();
-            cmd.CommandText = @"DELETE FROM `BlogPost`";
-            await cmd.ExecuteNonQueryAsync();
-            await txn.CommitAsync();
-        }
+    public async Task UpdateAsync(BlogPost blogPost)
+    {
+        using var connection = await database.OpenConnectionAsync();
+        using var command = connection.CreateCommand();
+        command.CommandText = @"UPDATE `BlogPost` SET `Title` = @title, `Content` = @content WHERE `Id` = @id;";
+        BindParams(command, blogPost);
+        BindId(command, blogPost);
+        await command.ExecuteNonQueryAsync();
+    }
+
+    public async Task DeleteAsync(BlogPost blogPost)
+    {
+        using var connection = await database.OpenConnectionAsync();
+        using var command = connection.CreateCommand();
+        command.CommandText = @"DELETE FROM `BlogPost` WHERE `Id` = @id;";
+        BindId(command, blogPost);
+        await command.ExecuteNonQueryAsync();
+    }
 
-        private async Task<List<BlogPost>> ReadAllAsync(DbDataReader reader)
+    private async Task<IReadOnlyList<BlogPost>> ReadAllAsync(DbDataReader reader)
+    {
+        var posts = new List<BlogPost>();
+        using (reader)
         {
-            var posts = new List<BlogPost>();
-            using (reader)
+            while (await reader.ReadAsync())
             {
-                while (await reader.ReadAsync())
+                var post = new BlogPost
                 {
-                    var post = new BlogPost(Db)
-                    {
-                        Id = reader.GetInt32(0),
-                        Title = reader.GetString(1),
-                        Content = reader.GetString(2),
-                    };
-                    posts.Add(post);
-                }
+                    Id = reader.GetInt32(0),
+                    Title = reader.GetString(1),
+                    Content = reader.GetString(2),
+                };
+                posts.Add(post);
             }
-            return posts;
         }
+        return posts;
+    }
+
+    private static void BindId(MySqlCommand cmd, BlogPost blogPost)
+    {
+        cmd.Parameters.AddWithValue("@id", blogPost.Id);
+    }
+
+    private static void BindParams(MySqlCommand cmd, BlogPost blogPost)
+    {
+        cmd.Parameters.AddWithValue("@title", blogPost.Title);
+        cmd.Parameters.AddWithValue("@content", blogPost.Content);
     }
 }
 ```
 
-### Controller
+### Program.cs
 
-`BlogController.cs` expose Async API Endpoints for CRUD operations on Blog Posts:
+`Program.cs` exposes Async API Endpoints for CRUD operations on Blog Posts (using an ASP.NET Minimal API).
+Add the following methods before `app.Run();`:
 
 ```csharp
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Mvc;
-
-namespace BlogPostApi.Controllers
+// GET api/blog
+app.MapGet("/api/blog", async ([FromServices] MySqlDataSource db) =>
 {
-    [Route("api/[controller]")]
-    public class BlogController : ControllerBase
-    {
-        public BlogController(AppDb db)
-        {
-            Db = db;
-        }
-
-        // GET api/blog
-        [HttpGet]
-        public async Task<IActionResult> GetLatest()
-        {
-            await Db.Connection.OpenAsync();
-            var query = new BlogPostQuery(Db);
-            var result = await query.LatestPostsAsync();
-            return new OkObjectResult(result);
-        }
+    var repository = new BlogPostRepository(db);
+    return await repository.LatestPostsAsync();
+});
 
-        // GET api/blog/5
-        [HttpGet("{id}")]
-        public async Task<IActionResult> GetOne(int id)
-        {
-            await Db.Connection.OpenAsync();
-            var query = new BlogPostQuery(Db);
-            var result = await query.FindOneAsync(id);
-            if (result is null)
-                return new NotFoundResult();
-            return new OkObjectResult(result);
-        }
-
-        // POST api/blog
-        [HttpPost]
-        public async Task<IActionResult> Post([FromBody]BlogPost body)
-        {
-            await Db.Connection.OpenAsync();
-            body.Db = Db;
-            await body.InsertAsync();
-            return new OkObjectResult(body);
-        }
-
-        // PUT api/blog/5
-        [HttpPut("{id}")]
-        public async Task<IActionResult> PutOne(int id, [FromBody]BlogPost body)
-        {
-            await Db.Connection.OpenAsync();
-            var query = new BlogPostQuery(Db);
-            var result = await query.FindOneAsync(id);
-            if (result is null)
-                return new NotFoundResult();
-            result.Title = body.Title;
-            result.Content = body.Content;
-            await result.UpdateAsync();
-            return new OkObjectResult(result);
-        }
-
-        // DELETE api/blog/5
-        [HttpDelete("{id}")]
-        public async Task<IActionResult> DeleteOne(int id)
-        {
-            await Db.Connection.OpenAsync();
-            var query = new BlogPostQuery(Db);
-            var result = await query.FindOneAsync(id);
-            if (result is null)
-                return new NotFoundResult();
-            await result.DeleteAsync();
-            return new OkResult();
-        }
+// GET api/blog/5
+app.MapGet("/api/blog/{id}", async ([FromServices] MySqlDataSource db, int id) =>
+{
+    var repository = new BlogPostRepository(db);
+    var result = await repository.FindOneAsync(id);
+    return result is null ? Results.NotFound() : Results.Ok(result);
+});
 
-        // DELETE api/blog
-        [HttpDelete]
-        public async Task<IActionResult> DeleteAll()
-        {
-            await Db.Connection.OpenAsync();
-            var query = new BlogPostQuery(Db);
-            await query.DeleteAllAsync();
-            return new OkResult();
-        }
+// POST api/blog
+app.MapPost("/api/blog", async ([FromServices] MySqlDataSource db, [FromBody] BlogPost body) =>
+{
+    var repository = new BlogPostRepository(db);
+    await repository.InsertAsync(body);
+    return body;
+});
 
-        public AppDb Db { get; }
-    }
-}
+// PUT api/blog/5
+app.MapPut("/api/blog/{id}", async (int id, [FromServices] MySqlDataSource db, [FromBody] BlogPost body) =>
+{
+    var repository = new BlogPostRepository(db);
+    var result = await repository.FindOneAsync(id);
+    if (result is null)
+        return Results.NotFound();
+    result.Title = body.Title;
+    result.Content = body.Content;
+    await repository.UpdateAsync(result);
+    return Results.Ok(result);
+});
+
+// DELETE api/blog/5
+app.MapDelete("/api/blog/{id}", async ([FromServices] MySqlDataSource db, int id) =>
+{
+    var repository = new BlogPostRepository(db);
+    var result = await repository.FindOneAsync(id);
+    if (result is null)
+        return Results.NotFound();
+    await repository.DeleteAsync(result);
+    return Results.NoContent();
+});
+
+// DELETE api/blog
+app.MapDelete("/api/blog", async ([FromServices] MySqlDataSource db) =>
+{
+    var repository = new BlogPostRepository(db);
+    await repository.DeleteAllAsync();
+    return Results.NoContent();
+});
 ```
 
 ### Run the App
 
 Congratulations, you should have a fully functional app at this point!  You should be able to run `dotnet run` to start your application.
 
-The following API Endpoints should work.  Note to set `Content-Type: application/json` headers on `POST` and `PUT` methods.
-
-```txt
-POST https://localhost:5001/api/blog
-{ "Title": "One", "Content": "First Blog Post!" }
-
-POST https://localhost:5001/api/blog
-{ "Title": "Two", "Content": "Second Blog Post!" }
-
-POST https://localhost:5001/api/blog
-{ "Title": "Three", "Content": "Third Blog Post!" }
-
-GET https://localhost:5001/api/blog
-// Output:
-[
-    { "id": 3, "title": "Three", "content": "Third Blog Post!" },
-    { "id": 2, "title": "Two", "content": "Second Blog Post!" },
-    { "id": 1, "title": "One", "content": "First Blog Post!"}
-]
-
-DELETE https://localhost:5001/api/blog/1
-// blog post 1 is gone
+The following API Endpoints should work:
 
-PUT https://localhost:5001/api/blog/2
-{ "Title": "Two", "Content": "Second Blog Post Revised" }
+```http
+@BlostPostApi_HostAddress = http://localhost:5001
 
-GET https://localhost:5001/api/blog
-// Output:
-[
-    { "id": 3, "title": "Three", "content": "Third Blog Post!" },
-    { "id": 2, "title": "Two", "content": "Second Blog Post Revised" },
-]
+GET {{BlostPostApi_HostAddress}}/api/blog
+Accept: application/json
+###
+POST {{BlostPostApi_HostAddress}}/api/blog
+Accept: application/json
+Content-Type: application/json
 
-DELETE https://localhost:5001/api/blog
-// all blog posts are gone
+{"title":"test", "content":"test content"}
+###
+PUT {{BlostPostApi_HostAddress}}/api/blog/1
+Accept: application/json
+Content-Type: application/json
 
-GET https://localhost:5001/api/blog
-// Output:
-[]
+{"title":"test put", "content":"test content updated"}
 ```
diff --git a/docs/layouts/partials/footer.html b/docs/layouts/partials/footer.html
index c07ccc051..9b151ee89 100644
--- a/docs/layouts/partials/footer.html
+++ b/docs/layouts/partials/footer.html
@@ -1,5 +1,3 @@
-        </div>
-        {{ partial "menu.html" . }}
       </div>
     </div>
   </body>
diff --git a/docs/layouts/partials/header.html b/docs/layouts/partials/header.html
index 520e2a03c..59b8aa401 100644
--- a/docs/layouts/partials/header.html
+++ b/docs/layouts/partials/header.html
@@ -3,6 +3,8 @@
   <head>
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    {{ $js := resources.Get "js/site.js" | resources.Minify | resources.Fingerprint }}
+    <script src="{{ $js.Permalink }}"></script>
     <!-- Global site tag (gtag.js) - Google Analytics -->
     <script async src="https://www.googletagmanager.com/gtag/js?id=UA-142489411-1"></script>
     <script>
@@ -12,9 +14,10 @@
 
       gtag('config', 'UA-142489411-1');
     </script>
-    <link rel="preconnect" href='https://fonts.gstatic.com' crossorigin>
-    <link rel="preload" href="https://fonts.googleapis.com/css?family=Open+Sans:400,600,700&display=block" as="style">
-    <meta name="description" content="{{ with .Description }}{{ . }}{{ else }}{{ with .Site.Params.description }}{{ . }}{{ end }}{{ end }}">
+    <link rel="preconnect" href="https://fonts.googleapis.com">
+    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+    {{ .Scratch.Add "description" "" }}{{ if .Description }}{{ .Scratch.Set "description" .Description }}{{ else }}{{ .Scratch.Set "description" .Site.Params.description }}{{ end }}
+    <meta name="description" content="{{ .Scratch.Get "description" }}">
     {{ with .Site.Params.author }}<meta name="author" content="{{ . }}">{{ end }}
     <!--icons generated by http://www.favicon-generator.org/-->
     <link rel="apple-touch-icon" sizes="57x57" href="/img/icons/apple-icon-57x57.png">
@@ -36,11 +39,10 @@
     <meta name="theme-color" content="#ffffff">
     <!--end icons-->
 
-    {{ .Hugo.Generator }}
-    {{ .Scratch.Add "title" "" }}{{ if isset .Site.Data.titles .Title }}{{ .Scratch.Set "title" (index .Site.Data.titles .Title).title }}{{ else if isset .Page.Params "customtitle" }}{{ .Scratch.Set "title" .Page.Params.customtitle }}{{ else }}{{ .Scratch.Set "title" .Title}}{{end}}
+    {{ .Scratch.Add "title" "" }}{{ if isset .Page.Params "customtitle" }}{{ .Scratch.Set "title" .Page.Params.customtitle }}{{ else }}{{ .Scratch.Set "title" .Title}}{{end}}
     <title>{{ .Scratch.Get "title" }} - MySqlConnector</title>
     <meta property="og:title" content="{{ .Scratch.Get "title" }} - MySqlConnector" />
-    <meta property="og:description" content="MySqlConnector is a high-performance, asynchronous ADO.NET data provider for MySQL Server, MariaDB, Percona Server, Amazon Aurora, Azure Database for MySQL, Google Cloud SQL for MySQL and more." />
+    <meta property="og:description" content="{{ .Scratch.Get "description" }}" />
     <meta property="og:type" content="website" />
     <meta property="og:image" content="https://mysqlconnector.net/img/icons/android-icon-192x192.png" />
     <meta property="og:image:type" content="image/png" />
@@ -51,105 +53,29 @@
     <meta property="og:url" content="{{ .Page.Permalink }}" />
     <meta property="fb:app_id" content="422535981632483" />
 
-    <link href="https://netdna.bootstrapcdn.com/twitter-bootstrap/2.2.1/css/bootstrap-combined.min.css" rel="stylesheet">
-    <link href="https://fonts.googleapis.com/css?family=Open+Sans:400,600,700&display=block" rel="stylesheet">
-	<style type="text/css">
-/* derived from https://github.com/fsprojects/SQLProvider/blob/gh-pages/content/style.css */
-pre {
-	  word-wrap: inherit;
-}
-
-.masthead ul, .masthead li {
-	margin-bottom:0px;
-}
-.masthead .nav li {
-	margin-top: 15px;
-	font-size:110%;
-}
-.masthead h3 {
-	margin-bottom:5px;
-	font-size:170%;
-}
-
-/* Make table headings and td.title bold */
-td.title, thead {
-	font-weight:bold;
-}
-
-table {
-  width: 100%;
-  max-width: 100%;
-  margin-bottom: 21px;
-}
-
-table > thead > tr > th,
-table > tbody > tr > th,
-table > tfoot > tr > th,
-table > thead > tr > td,
-table > tbody > tr > td,
-table > tfoot > tr > td {
-  padding: 8px;
-  line-height: 1.42857143;
-  vertical-align: top;
-  border-top: 1px solid #dddddd;
-}
-table > thead > tr > th {
-  vertical-align: bottom;
-  border-bottom: 2px solid #dddddd;
-}
-table > caption + thead > tr:first-child > th,
-table > colgroup + thead > tr:first-child > th,
-table > thead:first-child > tr:first-child > th,
-table > caption + thead > tr:first-child > td,
-table > colgroup + thead > tr:first-child > td,
-table > thead:first-child > tr:first-child > td {
-  border-top: 0;
-}
-table > tbody + tbody {
-  border-top: 2px solid #ecf0f1;
-}
-table table {
-  background-color: #ffffff;
-}
-table > tbody > tr:nth-of-type(odd) {
-  background-color: #f9f9f9;
-}
-table > tbody > tr:hover {
-  background-color: #f5f5f5;
-}
-
-/* Format the right-side menu */
-#menu {
-	margin-top:50px;
-	font-size:11pt;
-	padding-left:20px;
-}
-
-#menu li {
-	line-height:25px;
-}
-
-/* Change font sizes for headings etc. */
-#main h1 { font-size: 26pt; margin:10px 0px 15px 0px; font-weight:400; }
-#main h2 { font-size: 20pt; margin:20px 0px 0px 0px; font-weight:400; }
-#main h3 { font-size: 14pt; margin:15px 0px 0px 0px; font-weight:600; }
-#main p  { font-size: 11pt; margin:5px 0px 15px 0px; }
-#main ul { font-size: 11pt; margin-top:10px; }
-#main li { font-size: 11pt; margin: 5px 0px 5px 0px; }
-#main strong { font-weight:700; }
-	</style>
+    {{ $style := resources.Get "css/site.css" | resources.Minify | resources.Fingerprint }}
+    <link rel="stylesheet" href="{{ $style.Permalink }}">
+    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
     <link rel="canonical" href="{{ .Page.Permalink }}" />
   </head>
 
-  <body style="font-family: 'Open Sans', serif;padding-top: 0;padding-bottom: 40px;">
+  <body>
+    <svg xmlns="http://www.w3.org/2000/svg" class="d-none">
+      <symbol id="svg-link" viewBox="0 0 24 24"><title>Link</title><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg></symbol>
+      <symbol id="svg-menu" viewBox="0 0 24 24"><title>Menu</title><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-menu"><line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line></svg></symbol>
+      <symbol id="svg-arrow-right" viewBox="0 0 24 24"><title>Expand</title><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-right"><polyline points="9 18 15 12 9 6"></polyline></svg></symbol>
+      <symbol id="svg-external-link" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-external-link"><title id="svg-external-link-title">(external link)</title><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></symbol>
+      <symbol id="svg-doc" viewBox="0 0 24 24"><title>Document</title><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path><polyline points="13 2 13 9 20 9"></polyline></svg></symbol>
+      <symbol id="svg-search" viewBox="0 0 24 24"><title>Search</title><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-search"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg></symbol>
+      <symbol id="svg-copy" viewBox="0 0 16 16"><title>Copy</title><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard" viewBox="0 0 16 16"><path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1v-1z"></path><path d="M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3z"></path></svg></symbol>
+      <symbol id="svg-copied" viewBox="0 0 16 16"><title>Copied</title><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-clipboard-check-fill" viewBox="0 0 16 16"><path d="M6.5 0A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3Zm3 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5h3Z"></path><path d="M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1A2.5 2.5 0 0 1 9.5 5h-3A2.5 2.5 0 0 1 4 2.5v-1Zm6.854 7.354-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 0 1 .708-.708L7.5 10.793l2.646-2.647a.5.5 0 0 1 .708.708Z"></path></svg></symbol>
+    </svg>
+
+    <div class="side-bar">
+      <div class="site-header"> <a href="/" class="site-title lh-tight"> MySqlConnector </a> <a href="#" id="menu-button" class="site-button"> <svg viewBox="0 0 24 24" class="icon"><use xlink:href="#svg-menu"></use></svg> </a></div>
+    {{ partial "menu.html" . }}
+    </div>
+
+    <div class="main" id="top">
+        <div id="main-content-wrap" class="main-content-wrap">
 
-    <div class="container">
-        <div class="masthead" style="overflow: hidden">
-            <ul class="nav nav-pills pull-right">
-            <li><a href="https://github.com/mysql-net/MySqlConnector/">GitHub</a></li>
-            </ul>
-            <h3 class="muted"><a href="/" style="text-decoration: none; color: #999999">MySqlConnector</a></h3>
-        </div>
-        <hr style="margin:0 0 20px 0;"/>
-        <div class="row">
-            <div class="span9" id="main">
diff --git a/docs/layouts/partials/menu.html b/docs/layouts/partials/menu.html
index 7f27f8da1..965c608bc 100644
--- a/docs/layouts/partials/menu.html
+++ b/docs/layouts/partials/menu.html
@@ -1,17 +1,23 @@
-<div class="span3">
-    <ul class="nav nav-list" id="menu">
+<nav aria-label="Main" id="site-nav" class="site-nav">
+  {{ $currentPage := . }}
+  <ul class="nav-list">
 
-        {{ range $i, $el := .Site.Menus.main }}
-        <li class="nav-header" style="font-size:12pt;color:#606060;margin-top:20px;">
-            {{ if .HasChildren }}{{ .Name }}{{ else }}<a href="{{ .URL }}">{{ .Name }}</a>{{ end }}
-        </li>
+    {{ range $i, $el := .Site.Menus.main }}
+      <li class="nav-list-item{{ if $currentPage.HasMenuCurrent "main" . }} active{{ end }}">
+        {{ if .HasChildren }}<a href="#" class="nav-list-expander" aria-label="toggle links in UI Components category"> <svg viewBox="0 0 24 24"><use xlink:href="#svg-arrow-right"></use></svg> </a>{{ range first 1 .Children }}<a class="nav-list-link" href="{{ .URL }}">{{ end }}{{ .Name }}</a><ul class="nav-list">{{ else }}<a href="{{ .URL }}" class="nav-list-link{{ if $currentPage.IsMenuCurrent "main" . }} active{{ end }}">{{ .Name }}</a>{{ end }}
 
         {{ range .Children }}
-        <li>
-            <a href="{{.URL}}">{{ .Name }}</a>
-        </li>
+          <li class="nav-list-item{{ if $currentPage.HasMenuCurrent "main" . }} active{{ end }}">
+            <a href="{{.URL}}" class="nav-list-link{{ if $currentPage.IsMenuCurrent "main" . }} active{{ end }}">{{ .Name }}</a>
+          </li>
         {{ end }}
 
-        {{ end }}
-    </ul>
-</div>
+        {{ if .HasChildren }}</ul>{{ end }}
+      </li>
+    {{ end }}
+    <li class="nav-list-item external">
+      <a href="https://github.com/mysql-net/MySqlConnector" class="nav-list-link external">GitHub
+      <svg viewBox="0 0 24 24" aria-labelledby="svg-external-link-title"><use xlink:href="#svg-external-link"></use></svg></a>
+    </li>
+  </ul>
+</nav>
diff --git a/docs/static/js/scripts.js b/docs/static/js/scripts.js
deleted file mode 100644
index e69de29bb..000000000
diff --git a/global.json b/global.json
new file mode 100644
index 000000000..a02bac5a2
--- /dev/null
+++ b/global.json
@@ -0,0 +1,6 @@
+{
+  "sdk": {
+    "version": "9.0.200",
+    "rollForward": "latestFeature"
+  }
+}
diff --git a/netlify.toml b/netlify.toml
index f058c77da..835e01d1d 100644
--- a/netlify.toml
+++ b/netlify.toml
@@ -71,6 +71,12 @@ to = "/api/MySqlConnector/MySqlTransactionType/"
 status = 301
 force = true
 
+[[redirects]]
+from = "/diagnostics/"
+to = "/"
+status = 302
+force = true
+
 [[redirects]]
 from = "/groups/"
 to = "/"
@@ -100,3 +106,52 @@ from = "/tutorials/"
 to = "/"
 status = 302
 force = true
+
+[[redirects]]
+from = "/overview/logging/"
+to = "/diagnostics/logging/"
+status = 302
+force = true
+
+[[redirects]]
+from = "/tutorials/tracing/"
+to = "/diagnostics/tracing/"
+status = 302
+force = true
+
+[[redirects]]
+from = "/connection%E2%80%91options/"
+to = "/connection-options/"
+status = 301
+force = true
+
+[[redirects]]
+from = "/conn-reuse"
+to = "/troubleshooting/connection-reuse/"
+status = 301
+force = true
+
+[[redirects]]
+from = "/delimiter"
+to = "/troubleshooting/delimiter/"
+status = 301
+force = true
+
+[[redirects]]
+from = "/load-data"
+to = "/troubleshooting/load-data-local-infile/"
+status = 301
+force = true
+
+[[redirects]]
+from = "/param-type"
+to = "/troubleshooting/parameter-types/"
+status = 301
+force = true
+
+[[redirects]]
+from = "/trans"
+to = "/troubleshooting/transaction-usage/"
+status = 301
+force = true
+
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 56678b7b2..34b8405fd 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -1,10 +1,10 @@
 <Project>
+  <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
 
   <PropertyGroup>
     <SignAssembly>true</SignAssembly>
     <PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
     <AssemblyOriginatorKeyFile>..\..\MySqlConnector.snk</AssemblyOriginatorKeyFile>
-    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <PackageReleaseNotes>https://mysqlconnector.net/overview/version-history/</PackageReleaseNotes>
     <PackageProjectUrl>https://mysqlconnector.net/</PackageProjectUrl>
     <PackageLicenseExpression>MIT</PackageLicenseExpression>
@@ -17,40 +17,18 @@
     <PackageValidationBaselineVersion>2.0.0</PackageValidationBaselineVersion>
     <EnableStrictModeForCompatibleFrameworksInPackageValidation>true</EnableStrictModeForCompatibleFrameworksInPackageValidation>
     <EnableStrictModeForCompatibleTfms>true</EnableStrictModeForCompatibleTfms>
-    <LangVersion>preview</LangVersion>
     <AnalysisLevel>latest-all</AnalysisLevel>
     <RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
     <RunAnalyzersDuringLiveAnalysis>true</RunAnalyzersDuringLiveAnalysis>
+    <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
     <PublishRepositoryUrl>true</PublishRepositoryUrl>
     <EmbedUntrackedSources>true</EmbedUntrackedSources>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
-    <NoWarn>$(NoWarn);1591;CA1708;CA1835;CA2215;CA5397;NU5105;SYSLIB0039</NoWarn>
     <MinVerDefaultPreReleaseIdentifiers>beta.0</MinVerDefaultPreReleaseIdentifiers>
   </PropertyGroup>
 
-  <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
-    <RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild>
-  </PropertyGroup>
-
-  <PropertyGroup Condition=" '$(GITHUB_ACTIONS)' == 'true' OR '$(APPVEYOR)' == 'True' OR '$(TF_BUILD)' == 'True' ">
-    <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
-    <Deterministic>true</Deterministic>
-  </PropertyGroup>
-
-  <ItemGroup>
-    <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All"/>
-    <PackageReference Include="MinVer" Version="4.3.0">
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
-      <PrivateAssets>all</PrivateAssets>
-    </PackageReference>
-    <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.435">
-      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
-      <PrivateAssets>all</PrivateAssets>
-    </PackageReference>
-  </ItemGroup>
-
   <ItemGroup>
-    <None Include="..\..\docs\static\img\logo.png" Pack="true" PackagePath="\"/>
+    <None Include="..\..\docs\static\img\logo.png" Pack="true" PackagePath="\" />
   </ItemGroup>
 
 </Project>
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/CryptoBytes.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/CryptoBytes.cs
new file mode 100644
index 000000000..c637b6f98
--- /dev/null
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/CryptoBytes.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Runtime.CompilerServices;
+
+namespace Chaos.NaCl
+{
+    internal static class CryptoBytes
+    {
+        public static void Wipe(byte[] data)
+        {
+            if (data == null)
+                throw new ArgumentNullException("data");
+            InternalWipe(data, 0, data.Length);
+        }
+
+        // Secure wiping is hard
+        // * the GC can move around and copy memory
+        //   Perhaps this can be avoided by using unmanaged memory or by fixing the position of the array in memory
+        // * Swap files and error dumps can contain secret information
+        //   It seems possible to lock memory in RAM, no idea about error dumps
+        // * Compiler could optimize out the wiping if it knows that data won't be read back
+        //   I hope this is enough, suppressing inlining
+        //   but perhaps `RtlSecureZeroMemory` is needed
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        internal static void InternalWipe(byte[] data, int offset, int count)
+        {
+            Array.Clear(data, offset, count);
+        }
+
+        // shallow wipe of structs
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        internal static void InternalWipe<T>(ref T data)
+            where T : struct
+        {
+            data = default(T);
+        }
+    }
+}
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Ed25519.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Ed25519.cs
new file mode 100644
index 000000000..f3a0b7011
--- /dev/null
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Ed25519.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Security.Cryptography;
+using Chaos.NaCl.Internal.Ed25519Ref10;
+
+namespace Chaos.NaCl
+{
+    internal static class Ed25519
+    {
+        public static readonly int PublicKeySizeInBytes = 32;
+        public static readonly int SignatureSizeInBytes = 64;
+        public static readonly int ExpandedPrivateKeySizeInBytes = 32 * 2;
+        public static readonly int PrivateKeySeedSizeInBytes = 32;
+        public static readonly int SharedKeySizeInBytes = 32;
+
+        public static void Sign(ArraySegment<byte> signature, ArraySegment<byte> message, ArraySegment<byte> expandedPrivateKey)
+        {
+            if (signature.Array == null)
+                throw new ArgumentNullException("signature.Array");
+            if (signature.Count != SignatureSizeInBytes)
+                throw new ArgumentException("signature.Count");
+            if (expandedPrivateKey.Array == null)
+                throw new ArgumentNullException("expandedPrivateKey.Array");
+            if (expandedPrivateKey.Count != ExpandedPrivateKeySizeInBytes)
+                throw new ArgumentException("expandedPrivateKey.Count");
+            if (message.Array == null)
+                throw new ArgumentNullException("message.Array");
+            Ed25519Operations.crypto_sign2(signature.Array, signature.Offset, message.Array, message.Offset, message.Count, expandedPrivateKey.Array, expandedPrivateKey.Offset);
+        }
+
+        public static byte[] Sign(byte[] message, byte[] expandedPrivateKey)
+        {
+            var signature = new byte[SignatureSizeInBytes];
+            Sign(new ArraySegment<byte>(signature), new ArraySegment<byte>(message), new ArraySegment<byte>(expandedPrivateKey));
+            return signature;
+        }
+
+        public static void KeyPairFromSeed(out byte[] publicKey, out byte[] expandedPrivateKey, byte[] privateKeySeed)
+        {
+            if (privateKeySeed == null)
+                throw new ArgumentNullException("privateKeySeed");
+            if (privateKeySeed.Length != PrivateKeySeedSizeInBytes)
+                throw new ArgumentException("privateKeySeed");
+            var pk = new byte[PublicKeySizeInBytes];
+            var sk = new byte[ExpandedPrivateKeySizeInBytes];
+            Ed25519Operations.crypto_sign_keypair(pk, 0, sk, 0, privateKeySeed, 0);
+            publicKey = pk;
+            expandedPrivateKey = sk;
+        }
+    }
+}
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/FieldElement.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/FieldElement.cs
index 50c9290e1..71d014b0d 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/FieldElement.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/FieldElement.cs
@@ -1,5 +1,3 @@
-using System;
-
 namespace Chaos.NaCl.Internal.Ed25519Ref10;
 
 internal struct FieldElement
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/GroupElement.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/GroupElement.cs
index 0553b10f9..abeaca869 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/GroupElement.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/GroupElement.cs
@@ -1,54 +1,63 @@
-using System;
+﻿using System;
 
-namespace Chaos.NaCl.Internal.Ed25519Ref10;
-
-/*
-ge means group element.
-
-Here the group is the set of pairs (x,y) of field elements (see fe.h)
-satisfying -x^2 + y^2 = 1 + d x^2y^2
-where d = -121665/121666.
+namespace Chaos.NaCl.Internal.Ed25519Ref10
+{
+	/*
+	ge means group element.
 
-Representations:
-  ge_p2 (projective): (X:Y:Z) satisfying x=X/Z, y=Y/Z
-  ge_p3 (extended): (X:Y:Z:T) satisfying x=X/Z, y=Y/Z, XY=ZT
-  ge_p1p1 (completed): ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T
-  ge_precomp (Duif): (y+x,y-x,2dxy)
-*/
+	Here the group is the set of pairs (x,y) of field elements (see fe.h)
+	satisfying -x^2 + y^2 = 1 + d x^2y^2
+	where d = -121665/121666.
 
-internal struct GroupElementP2
-{
-	public FieldElement X;
-	public FieldElement Y;
-	public FieldElement Z;
-} ;
+	Representations:
+	  ge_p2 (projective): (X:Y:Z) satisfying x=X/Z, y=Y/Z
+	  ge_p3 (extended): (X:Y:Z:T) satisfying x=X/Z, y=Y/Z, XY=ZT
+	  ge_p1p1 (completed): ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T
+	  ge_precomp (Duif): (y+x,y-x,2dxy)
+	*/
 
-internal struct GroupElementP3
-{
-	public FieldElement X;
-	public FieldElement Y;
-	public FieldElement Z;
-	public FieldElement T;
-} ;
+	internal struct GroupElementP2
+	{
+		public FieldElement X;
+		public FieldElement Y;
+		public FieldElement Z;
+	} ;
 
-internal struct GroupElementP1P1
-{
-	public FieldElement X;
-	public FieldElement Y;
-	public FieldElement Z;
-	public FieldElement T;
-} ;
+	internal struct GroupElementP3
+	{
+		public FieldElement X;
+		public FieldElement Y;
+		public FieldElement Z;
+		public FieldElement T;
+	} ;
 
-internal struct GroupElementPreComp
-{
-	public FieldElement yplusx;
-	public FieldElement yminusx;
-	public FieldElement xy2d;
+	internal struct GroupElementP1P1
+	{
+		public FieldElement X;
+		public FieldElement Y;
+		public FieldElement Z;
+		public FieldElement T;
+	} ;
 
-	public GroupElementPreComp(FieldElement yplusx, FieldElement yminusx, FieldElement xy2d)
+	internal struct GroupElementPreComp
+	{
+		public FieldElement yplusx;
+		public FieldElement yminusx;
+		public FieldElement xy2d;
+
+		public GroupElementPreComp(FieldElement yplusx, FieldElement yminusx, FieldElement xy2d)
+		{
+			this.yplusx = yplusx;
+			this.yminusx = yminusx;
+			this.xy2d = xy2d;
+		}
+	} ;
+
+	internal struct GroupElementCached
 	{
-		this.yplusx = yplusx;
-		this.yminusx = yminusx;
-		this.xy2d = xy2d;
-	}
-} ;
+		public FieldElement YplusX;
+		public FieldElement YminusX;
+		public FieldElement Z;
+		public FieldElement T2d;
+	} ;
+}
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/base.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/base.cs
index 4d828baeb..a2d173b89 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/base.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/base.cs
@@ -1,5 +1,3 @@
-using System;
-
 namespace Chaos.NaCl.Internal.Ed25519Ref10;
 
 internal static partial class LookupTables
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/base2.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/base2.cs
new file mode 100644
index 000000000..03676e5cf
--- /dev/null
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/base2.cs
@@ -0,0 +1,50 @@
+﻿using System;
+
+namespace Chaos.NaCl.Internal.Ed25519Ref10
+{
+    internal static partial class LookupTables
+    {
+        internal static readonly GroupElementPreComp[] Base2 = new GroupElementPreComp[]{
+            new GroupElementPreComp(
+              new FieldElement( 25967493,-14356035,29566456,3660896,-12694345,4014787,27544626,-11754271,-6079156,2047605 ),
+            new FieldElement( -12545711,934262,-2722910,3049990,-727428,9406986,12720692,5043384,19500929,-15469378 ),
+            new FieldElement( -8738181,4489570,9688441,-14785194,10184609,-12363380,29287919,11864899,-24514362,-4438546 )
+             ),
+            new GroupElementPreComp(
+            new FieldElement( 15636291,-9688557,24204773,-7912398,616977,-16685262,27787600,-14772189,28944400,-1550024 ),
+            new FieldElement( 16568933,4717097,-11556148,-1102322,15682896,-11807043,16354577,-11775962,7689662,11199574 ),
+            new FieldElement( 30464156,-5976125,-11779434,-15670865,23220365,15915852,7512774,10017326,-17749093,-9920357 )
+             ),
+            new GroupElementPreComp(
+            new FieldElement( 10861363,11473154,27284546,1981175,-30064349,12577861,32867885,14515107,-15438304,10819380 ),
+            new FieldElement( 4708026,6336745,20377586,9066809,-11272109,6594696,-25653668,12483688,-12668491,5581306 ),
+            new FieldElement( 19563160,16186464,-29386857,4097519,10237984,-4348115,28542350,13850243,-23678021,-15815942 )
+             ),
+            new GroupElementPreComp(
+            new FieldElement( 5153746,9909285,1723747,-2777874,30523605,5516873,19480852,5230134,-23952439,-15175766 ),
+            new FieldElement( -30269007,-3463509,7665486,10083793,28475525,1649722,20654025,16520125,30598449,7715701 ),
+            new FieldElement( 28881845,14381568,9657904,3680757,-20181635,7843316,-31400660,1370708,29794553,-1409300 )
+             ),
+            new GroupElementPreComp(
+            new FieldElement( -22518993,-6692182,14201702,-8745502,-23510406,8844726,18474211,-1361450,-13062696,13821877 ),
+            new FieldElement( -6455177,-7839871,3374702,-4740862,-27098617,-10571707,31655028,-7212327,18853322,-14220951 ),
+            new FieldElement( 4566830,-12963868,-28974889,-12240689,-7602672,-2830569,-8514358,-10431137,2207753,-3209784 )
+             ),
+            new GroupElementPreComp(
+            new FieldElement( -25154831,-4185821,29681144,7868801,-6854661,-9423865,-12437364,-663000,-31111463,-16132436 ),
+            new FieldElement( 25576264,-2703214,7349804,-11814844,16472782,9300885,3844789,15725684,171356,6466918 ),
+            new FieldElement( 23103977,13316479,9739013,-16149481,817875,-15038942,8965339,-14088058,-30714912,16193877 )
+             ),
+            new GroupElementPreComp(
+            new FieldElement( -33521811,3180713,-2394130,14003687,-16903474,-16270840,17238398,4729455,-18074513,9256800 ),
+            new FieldElement( -25182317,-4174131,32336398,5036987,-21236817,11360617,22616405,9761698,-19827198,630305 ),
+            new FieldElement( -13720693,2639453,-24237460,-7406481,9494427,-5774029,-6554551,-15960994,-2449256,-14291300 )
+             ),
+            new GroupElementPreComp(
+            new FieldElement( -3151181,-5046075,9282714,6866145,-31907062,-863023,-18940575,15033784,25105118,-7894876 ),
+            new FieldElement( -24326370,15950226,-31801215,-14592823,-11662737,-5090925,1573892,-2625887,2198790,-15804619 ),
+            new FieldElement( -3099351,10324967,-2241613,7453183,-5446979,-2735503,-13812022,-16236442,-32461234,-12290683 )
+             )
+		};
+    }
+}
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/d.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/d.cs
index 55d09cd2f..e5a50872a 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/d.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/d.cs
@@ -1,5 +1,3 @@
-using System;
-
 namespace Chaos.NaCl.Internal.Ed25519Ref10;
 
 internal static partial class LookupTables
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/d2.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/d2.cs
index 049062e50..d340f03b3 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/d2.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/d2.cs
@@ -1,5 +1,3 @@
-using System;
-
 namespace Chaos.NaCl.Internal.Ed25519Ref10;
 
 internal static partial class LookupTables
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/fe_0.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/fe_0.cs
index da5393b16..b31e91ee4 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/fe_0.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/fe_0.cs
@@ -1,5 +1,3 @@
-using System;
-
 namespace Chaos.NaCl.Internal.Ed25519Ref10;
 
 internal static partial class FieldOperations
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/fe_1.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/fe_1.cs
index 4ff20137c..4a6c6701f 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/fe_1.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/fe_1.cs
@@ -1,5 +1,3 @@
-using System;
-
 namespace Chaos.NaCl.Internal.Ed25519Ref10;
 
 internal static partial class FieldOperations
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/fe_invert.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/fe_invert.cs
index ae9176602..cf20b1a15 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/fe_invert.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/fe_invert.cs
@@ -1,5 +1,3 @@
-using System;
-
 namespace Chaos.NaCl.Internal.Ed25519Ref10;
 
 internal static partial class FieldOperations
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/fe_isnegative.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/fe_isnegative.cs
index 9a2fd3944..cd47876d4 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/fe_isnegative.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/fe_isnegative.cs
@@ -1,5 +1,3 @@
-using System;
-
 namespace Chaos.NaCl.Internal.Ed25519Ref10;
 
 internal static partial class FieldOperations
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_madd.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_madd.cs
index 810669e74..e0b1c2118 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_madd.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_madd.cs
@@ -1,5 +1,3 @@
-using System;
-
 namespace Chaos.NaCl.Internal.Ed25519Ref10;
 
 internal static partial class GroupOperations
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p1p1_to_p2.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p1p1_to_p2.cs
index ff09ec999..e4be46778 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p1p1_to_p2.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p1p1_to_p2.cs
@@ -1,5 +1,3 @@
-using System;
-
 namespace Chaos.NaCl.Internal.Ed25519Ref10;
 
 internal static partial class GroupOperations
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p1p1_to_p3.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p1p1_to_p3.cs
index 0fa3380af..55b18d579 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p1p1_to_p3.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p1p1_to_p3.cs
@@ -1,5 +1,3 @@
-using System;
-
 namespace Chaos.NaCl.Internal.Ed25519Ref10;
 
 internal static partial class GroupOperations
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p2_dbl.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p2_dbl.cs
index 4570fc290..24cc1175f 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p2_dbl.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p2_dbl.cs
@@ -1,5 +1,3 @@
-using System;
-
 namespace Chaos.NaCl.Internal.Ed25519Ref10;
 
 internal static partial class GroupOperations
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p3_0.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p3_0.cs
index 420603c40..1706e9e8c 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p3_0.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p3_0.cs
@@ -1,5 +1,3 @@
-using System;
-
 namespace Chaos.NaCl.Internal.Ed25519Ref10;
 
 internal static partial class GroupOperations
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p3_dbl.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p3_dbl.cs
index 41d01b4c0..dd05ed907 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p3_dbl.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p3_dbl.cs
@@ -1,5 +1,3 @@
-using System;
-
 namespace Chaos.NaCl.Internal.Ed25519Ref10;
 
 internal static partial class GroupOperations
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p3_to_p2.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p3_to_p2.cs
index 14054a0fe..7015e681f 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p3_to_p2.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p3_to_p2.cs
@@ -1,5 +1,3 @@
-using System;
-
 namespace Chaos.NaCl.Internal.Ed25519Ref10;
 
 internal static partial class GroupOperations
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p3_tobytes.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p3_tobytes.cs
index ce810cbbd..bd93e9b9b 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p3_tobytes.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_p3_tobytes.cs
@@ -1,5 +1,3 @@
-using System;
-
 namespace Chaos.NaCl.Internal.Ed25519Ref10;
 
 internal static partial class GroupOperations
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_precomp_0.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_precomp_0.cs
index 2d89e38f2..61a716f40 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_precomp_0.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/ge_precomp_0.cs
@@ -1,5 +1,3 @@
-using System;
-
 namespace Chaos.NaCl.Internal.Ed25519Ref10;
 
 internal static partial class GroupOperations
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/keypair.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/keypair.cs
new file mode 100644
index 000000000..037efcfd1
--- /dev/null
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/keypair.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Security.Cryptography;
+
+namespace Chaos.NaCl.Internal.Ed25519Ref10
+{
+    internal static partial class Ed25519Operations
+    {
+        public static void crypto_sign_keypair(byte[] pk, int pkoffset, byte[] sk, int skoffset, byte[] seed, int seedoffset)
+        {
+            GroupElementP3 A;
+            int i;
+
+            Array.Copy(seed, seedoffset, sk, skoffset, 32);
+#if NET5_0_OR_GREATER
+            byte[] h = SHA512.HashData(sk.AsSpan(skoffset, 32));
+#else
+            using var hash = SHA512.Create();
+            byte[] h = hash.ComputeHash(sk, skoffset, 32);
+#endif
+            ScalarOperations.sc_clamp(h, 0);
+
+            GroupOperations.ge_scalarmult_base(out A, h, 0);
+            GroupOperations.ge_p3_tobytes(pk, pkoffset, ref A);
+
+            for (i = 0; i < 32; ++i) sk[skoffset + 32 + i] = pk[pkoffset + i];
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER
+            CryptographicOperations.ZeroMemory(h);
+#else
+            CryptoBytes.Wipe(h);
+#endif
+        }
+    }
+}
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sc_clamp.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sc_clamp.cs
index 5495024ab..e0f556023 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sc_clamp.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sc_clamp.cs
@@ -1,5 +1,3 @@
-using System;
-
 namespace Chaos.NaCl.Internal.Ed25519Ref10;
 
 internal static partial class ScalarOperations
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sign.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sign.cs
new file mode 100644
index 000000000..a56625557
--- /dev/null
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sign.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Security.Cryptography;
+
+namespace Chaos.NaCl.Internal.Ed25519Ref10
+{
+	internal static partial class Ed25519Operations
+	{
+		public static void crypto_sign2(
+			byte[] sig, int sigoffset,
+			byte[] m, int moffset, int mlen,
+			byte[] sk, int skoffset)
+		{
+			byte[] az;
+			byte[] r;
+			byte[] hram;
+			GroupElementP3 R;
+			using (var hasher = SHA512.Create())
+			{
+				az = hasher.ComputeHash(sk, skoffset, 32);
+				ScalarOperations.sc_clamp(az, 0);
+
+				hasher.Initialize();
+				hasher.TransformBlock(az, 32, 32, null, 0);
+				hasher.TransformFinalBlock(m, moffset, mlen);
+				r = hasher.Hash;
+
+				ScalarOperations.sc_reduce(r);
+				GroupOperations.ge_scalarmult_base(out R, r, 0);
+				GroupOperations.ge_p3_tobytes(sig, sigoffset, ref R);
+
+				hasher.Initialize();
+				hasher.TransformBlock(sig, sigoffset, 32, null, 0);
+				hasher.TransformBlock(sk, skoffset + 32, 32, null, 0);
+				hasher.TransformFinalBlock(m, moffset, mlen);
+				hram = hasher.Hash;
+
+				ScalarOperations.sc_reduce(hram);
+				var s = new byte[32];//todo: remove allocation
+				Array.Copy(sig, sigoffset + 32, s, 0, 32);
+				ScalarOperations.sc_muladd(s, hram, az, r);
+				Array.Copy(s, 0, sig, sigoffset + 32, 32);
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER
+				CryptographicOperations.ZeroMemory(s);
+#else
+				CryptoBytes.Wipe(s);
+#endif
+			}
+		}
+	}
+}
diff --git a/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sqrtm1.cs b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sqrtm1.cs
new file mode 100644
index 000000000..fb8b50122
--- /dev/null
+++ b/src/MySqlConnector.Authentication.Ed25519/Chaos.NaCl/Internal/Ed25519Ref10/sqrtm1.cs
@@ -0,0 +1,9 @@
+﻿using System;
+
+namespace Chaos.NaCl.Internal.Ed25519Ref10
+{
+	internal static partial class LookupTables
+	{
+		internal static FieldElement sqrtm1 = new FieldElement(-32595792, -7943725, 9377950, 3500415, 12389472, -272473, -25146209, -2005654, 326686, 11406482);
+	}
+}
\ No newline at end of file
diff --git a/src/MySqlConnector.Authentication.Ed25519/CompatibilitySuppressions.xml b/src/MySqlConnector.Authentication.Ed25519/CompatibilitySuppressions.xml
index a95918371..e66567aca 100644
--- a/src/MySqlConnector.Authentication.Ed25519/CompatibilitySuppressions.xml
+++ b/src/MySqlConnector.Authentication.Ed25519/CompatibilitySuppressions.xml
@@ -1,4 +1,5 @@
 ﻿<?xml version="1.0" encoding="utf-8"?>
+<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
 <Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <Suppression>
     <DiagnosticId>PKV006</DiagnosticId>
diff --git a/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs b/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs
index 117953366..b7f718b17 100644
--- a/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs
+++ b/src/MySqlConnector.Authentication.Ed25519/Ed25519AuthenticationPlugin.cs
@@ -1,6 +1,7 @@
 using System;
 using System.Security.Cryptography;
 using System.Text;
+using System.Threading;
 using Chaos.NaCl.Internal.Ed25519Ref10;
 
 namespace MySqlConnector.Authentication.Ed25519;
@@ -9,7 +10,9 @@ namespace MySqlConnector.Authentication.Ed25519;
 /// Provides an implementation of the <c>client_ed25519</c> authentication plugin for MariaDB.
 /// </summary>
 /// <remarks>See <a href="https://mariadb.com/kb/en/library/authentication-plugin-ed25519/">Authentication Plugin - ed25519</a>.</remarks>
-public sealed class Ed25519AuthenticationPlugin : IAuthenticationPlugin
+#pragma warning disable CS0618 // Type or member is obsolete
+public sealed class Ed25519AuthenticationPlugin : IAuthenticationPlugin3, IAuthenticationPlugin2
+#pragma warning restore CS0618 // Type or member is obsolete
 {
 	/// <summary>
 	/// Registers the Ed25519 authentication plugin with MySqlConnector. You must call this method once before
@@ -17,11 +20,8 @@ public sealed class Ed25519AuthenticationPlugin : IAuthenticationPlugin
 	/// </summary>
 	public static void Install()
 	{
-		if (!s_isInstalled)
-		{
+		if (Interlocked.CompareExchange(ref s_isInstalled, 1, 0) == 0)
 			AuthenticationPlugins.Register(new Ed25519AuthenticationPlugin());
-			s_isInstalled = true;
-		}
 	}
 
 	/// <summary>
@@ -33,6 +33,30 @@ public static void Install()
 	/// Creates the authentication response.
 	/// </summary>
 	public byte[] CreateResponse(string password, ReadOnlySpan<byte> authenticationData)
+	{
+		CreateResponseAndPasswordHash(password, authenticationData, out var authenticationResponse, out _);
+		return authenticationResponse;
+	}
+
+	/// <summary>
+	/// Creates the Ed25519 password hash.
+	/// </summary>
+	public byte[] CreatePasswordHash(string password, ReadOnlySpan<byte> authenticationData)
+	{
+		CreateResponseAndPasswordHash(password, authenticationData, out _, out var passwordHash);
+		return passwordHash;
+	}
+
+	/// <summary>
+	/// Creates the authentication response and hashes the client's password (e.g., for TLS certificate fingerprint verification).
+	/// </summary>
+	/// <param name="password">The client's password.</param>
+	/// <param name="authenticationData">The authentication data supplied by the server; this is the <code>auth method data</code>
+	/// from the <a href="https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest">Authentication
+	/// Method Switch Request Packet</a>.</param>
+	/// <param name="authenticationResponse">The authentication response.</param>
+	/// <param name="passwordHash">The authentication-method-specific hash of the client's password.</param>
+	public void CreateResponseAndPasswordHash(string password, ReadOnlySpan<byte> authenticationData, out byte[] authenticationResponse, out byte[] passwordHash)
 	{
 		// Java reference: https://github.com/MariaDB/mariadb-connector-j/blob/master/src/main/java/org/mariadb/jdbc/internal/com/send/authentication/Ed25519PasswordPlugin.java
 		// C reference:  https://github.com/MariaDB/server/blob/592fe954ef82be1bc08b29a8e54f7729eb1e1343/plugin/auth_ed25519/ref10/sign.c#L7
@@ -64,8 +88,12 @@ public byte[] CreateResponse(string password, ReadOnlySpan<byte> authenticationD
 			az[31] |= 64;
 		*/
 
+#if NET5_0_OR_GREATER
+		byte[] az = SHA512.HashData(passwordBytes);
+#else
 		using var sha512 = SHA512.Create();
 		byte[] az = sha512.ComputeHash(passwordBytes);
+#endif
 		ScalarOperations.sc_clamp(az, 0);
 
 		/*** Java
@@ -91,7 +119,11 @@ public byte[] CreateResponse(string password, ReadOnlySpan<byte> authenticationD
 		byte[] sm = new byte[64 + authenticationData.Length];
 		authenticationData.CopyTo(sm.AsSpan().Slice(64));
 		Buffer.BlockCopy(az, 32, sm, 32, 32);
+#if NET5_0_OR_GREATER
+		byte[] nonce = SHA512.HashData(sm.AsSpan(32, authenticationData.Length + 32));
+#else
 		byte[] nonce = sha512.ComputeHash(sm, 32, authenticationData.Length + 32);
+#endif
 
 		/*** Java
 			ScalarOps scalar = new ScalarOps();
@@ -111,6 +143,9 @@ public byte[] CreateResponse(string password, ReadOnlySpan<byte> authenticationD
 		GroupOperations.ge_scalarmult_base(out var A, az, 0);
 		GroupOperations.ge_p3_tobytes(sm, 32, ref A);
 
+		passwordHash = new byte[32];
+		Array.Copy(sm, 32, passwordHash, 0, 32);
+
 		/*** Java
 			nonce = scalar.reduce(nonce);
 			GroupElement elementRvalue = spec.getB().scalarMultiply(nonce);
@@ -146,7 +181,11 @@ public byte[] CreateResponse(string password, ReadOnlySpan<byte> authenticationD
 
 			return 0;
 		*/
+#if NET5_0_OR_GREATER
+		var hram = SHA512.HashData(sm);
+#else
 		var hram = sha512.ComputeHash(sm);
+#endif
 		ScalarOperations.sc_reduce(hram);
 		var temp = new byte[32];
 		ScalarOperations.sc_muladd(temp, hram, az, nonce);
@@ -154,12 +193,12 @@ public byte[] CreateResponse(string password, ReadOnlySpan<byte> authenticationD
 
 		var result = new byte[64];
 		Buffer.BlockCopy(sm, 0, result, 0, result.Length);
-		return result;
+		authenticationResponse = result;
 	}
 
 	private Ed25519AuthenticationPlugin()
 	{
 	}
 
-	private static bool s_isInstalled;
+	private static int s_isInstalled;
 }
diff --git a/src/MySqlConnector.Authentication.Ed25519/MySqlConnector.Authentication.Ed25519.csproj b/src/MySqlConnector.Authentication.Ed25519/MySqlConnector.Authentication.Ed25519.csproj
index ce61daed4..37349bfd2 100644
--- a/src/MySqlConnector.Authentication.Ed25519/MySqlConnector.Authentication.Ed25519.csproj
+++ b/src/MySqlConnector.Authentication.Ed25519/MySqlConnector.Authentication.Ed25519.csproj
@@ -1,22 +1,30 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFrameworks>net461;netstandard2.0</TargetFrameworks>
+    <TargetFrameworks>net462;net472;netstandard2.0;netstandard2.1;net6.0</TargetFrameworks>
     <Title>MySqlConnector Ed25519 Authentication Plugin</Title>
-    <Description>Implements the client_ed25519 authentication plugin for MariaDB.</Description>
-    <Copyright>Copyright 2019–2023 Bradley Grainger</Copyright>
+    <Description>Implements the client_ed25519 and parsec authentication plugins for MariaDB.</Description>
+    <Copyright>Copyright 2019–2024 Bradley Grainger</Copyright>
     <Authors>Bradley Grainger</Authors>
     <PackageReadmeFile>README.md</PackageReadmeFile>
-    <PackageTags>mariadb;mysqlconnector;authentication;ed25519</PackageTags>
-    <NoWarn>SA1001;SA1002;SA1005;SA1011;SA1012;SA1021;SA1025;SA1106;SA1107;SA1111;SA1119;SA1121;SA1300;SA1307;SA1312;SA1401;SA1413;SA1501;SA1505;SA1507;SA1508;SA1512;SA1518;SA1601</NoWarn>
+    <PackageTags>mariadb;mysqlconnector;authentication;ed25519;parsec</PackageTags>
+    <NoWarn>CA1305;CA1507;CA1802;CA2208;CS0649;IDE0049;SA1001;SA1002;SA1005;SA1011;SA1012;SA1021;SA1025;SA1028;SA1106;SA1107;SA1111;SA1119;SA1121;SA1124;SA1137;SA1214;SA1300;SA1307;SA1309;SA1312;SA1313;SA1401;SA1413;SA1501;SA1505;SA1507;SA1508;SA1509;SA1512;SA1515;SA1518;SA1520;SA1601</NoWarn>
   </PropertyGroup>
 
   <ItemGroup>
     <ProjectReference Include="..\MySqlConnector\MySqlConnector.csproj" />
   </ItemGroup>
 
+  <ItemGroup Condition=" '$(TargetFramework)' == 'net462' or '$(TargetFramework)' == 'netstandard2.0' ">
+    <PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" />
+  </ItemGroup>
+
+  <ItemGroup Condition=" '$(TargetFramework)' == 'net6.0' or '$(TargetFramework)' == 'netstandard2.1' ">
+    <Compile Remove="Chaos.NaCl\CryptoBytes.cs" />
+  </ItemGroup>
+
   <ItemGroup>
-    <None Include="docs\README.md" Pack="true" PackagePath="\"/>
+    <None Include="docs\README.md" Pack="true" PackagePath="\" />
   </ItemGroup>
 
 </Project>
diff --git a/src/MySqlConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs b/src/MySqlConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs
new file mode 100644
index 000000000..d10011216
--- /dev/null
+++ b/src/MySqlConnector.Authentication.Ed25519/ParsecAuthenticationPlugin.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
+
+namespace MySqlConnector.Authentication.Ed25519;
+
+/// <summary>
+/// Provides an implementation of the Parsec authentication plugin for MariaDB.
+/// </summary>
+public sealed class ParsecAuthenticationPlugin : IAuthenticationPlugin3
+{
+	/// <summary>
+	/// Registers the Parsec authentication plugin with MySqlConnector. You must call this method once before
+	/// opening a connection that uses Parsec authentication.
+	/// </summary>
+	public static void Install()
+	{
+		if (Interlocked.CompareExchange(ref s_isInstalled, 1, 0) == 0)
+			AuthenticationPlugins.Register(new ParsecAuthenticationPlugin());
+	}
+
+	/// <summary>
+	/// Gets the authentication plugin name.
+	/// </summary>
+	public string Name => "parsec";
+
+	/// <summary>
+	/// Creates the authentication response.
+	/// </summary>
+	public byte[] CreateResponse(string password, ReadOnlySpan<byte> authenticationData)
+	{
+		CreateResponseAndPasswordHash(password, authenticationData, out var response, out _);
+		return response;
+	}
+
+	/// <summary>
+	/// Creates the authentication response.
+	/// </summary>
+	public void CreateResponseAndPasswordHash(string password, ReadOnlySpan<byte> authenticationData, out byte[] authenticationResponse, out byte[] passwordHash)
+	{
+		// first 32 bytes are server scramble
+		var serverScramble = authenticationData.Slice(0, 32);
+
+		// generate client scramble
+#if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+		Span<byte> clientScramble = stackalloc byte[32];
+		RandomNumberGenerator.Fill(clientScramble);
+#else
+		var clientScramble = new byte[32];
+		using var randomNumberGenerator = RandomNumberGenerator.Create();
+		randomNumberGenerator.GetBytes(clientScramble);
+#endif
+
+		// parse extended salt from remaining authentication data and verify format
+		var extendedSalt = authenticationData.Slice(32);
+		if (extendedSalt[0] != (byte) 'P')
+			throw new ArgumentException("Invalid extended salt", nameof(authenticationData));
+		if (extendedSalt[1] is not (>= 0 and <= 3))
+			throw new ArgumentException("Invalid iteration count", nameof(authenticationData));
+
+		var iterationCount = 1024 << extendedSalt[1];
+		var salt = extendedSalt.Slice(2);
+
+		// derive private key using PBKDF2-SHA512
+		byte[] privateKeySeed;
+#if NET6_0_OR_GREATER
+		privateKeySeed = Rfc2898DeriveBytes.Pbkdf2(Encoding.UTF8.GetBytes(password), salt, iterationCount, HashAlgorithmName.SHA512, 32);
+#elif NET472_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+		using (var pbkdf2 = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(password), salt.ToArray(), iterationCount, HashAlgorithmName.SHA512))
+			privateKeySeed = pbkdf2.GetBytes(32);
+#else
+		privateKeySeed = Microsoft.AspNetCore.Cryptography.KeyDerivation.KeyDerivation.Pbkdf2(
+			password, salt.ToArray(), Microsoft.AspNetCore.Cryptography.KeyDerivation.KeyDerivationPrf.HMACSHA512,
+			iterationCount, numBytesRequested: 32);
+#endif
+		Chaos.NaCl.Ed25519.KeyPairFromSeed(out var publicKey, out var privateKey, privateKeySeed);
+
+		// generate Ed25519 keypair and sign concatenated scrambles
+		var message = new byte[serverScramble.Length + clientScramble.Length];
+		serverScramble.CopyTo(message);
+		clientScramble.CopyTo(message.AsSpan(serverScramble.Length));
+
+		var signature = Chaos.NaCl.Ed25519.Sign(message, privateKey);
+
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER
+		CryptographicOperations.ZeroMemory(privateKey);
+#endif
+
+		// return client scramble followed by signature
+		authenticationResponse = new byte[clientScramble.Length + signature.Length];
+		clientScramble.CopyTo(authenticationResponse.AsSpan());
+		signature.CopyTo(authenticationResponse.AsSpan(clientScramble.Length));
+
+		// "password hash" for parsec is the extended salt followed by the public key
+		passwordHash = [(byte) 'P', (byte) iterationCount, .. salt, .. publicKey];
+	}
+
+	private ParsecAuthenticationPlugin()
+	{
+	}
+
+	private static int s_isInstalled;
+}
diff --git a/src/MySqlConnector.Authentication.Ed25519/docs/README.md b/src/MySqlConnector.Authentication.Ed25519/docs/README.md
index 0dd5a9af7..b2d794296 100644
--- a/src/MySqlConnector.Authentication.Ed25519/docs/README.md
+++ b/src/MySqlConnector.Authentication.Ed25519/docs/README.md
@@ -1,7 +1,13 @@
 ## About
 
-This package implements the `client_ed25519` [authentication plugin for MariaDB](https://mariadb.com/kb/en/authentication-plugin-ed25519/).
+This package implements the following authentication plugins for MariaDB:
+
+* [`client_ed25519`](https://mariadb.com/kb/en/authentication-plugin-ed25519/).
+* [PARSEC](https://mariadb.com/kb/en/authentication-plugin-parsec/)
 
 ## How to Use
 
-Call `Ed25519AuthenticationPlugin.Install()` from your application startup code to enable it.
+Call either of the following methods from your application startup code to enable the corresponding authentication plugin:
+
+* `Ed25519AuthenticationPlugin.Install()`
+* `ParsecAuthenticationPlugin.Install()`
diff --git a/src/MySqlConnector.DependencyInjection/MySqlConnector.DependencyInjection.csproj b/src/MySqlConnector.DependencyInjection/MySqlConnector.DependencyInjection.csproj
new file mode 100644
index 000000000..69dbc32be
--- /dev/null
+++ b/src/MySqlConnector.DependencyInjection/MySqlConnector.DependencyInjection.csproj
@@ -0,0 +1,29 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+    <EnablePackageValidation>false</EnablePackageValidation>
+    <Title>MySqlConnector Dependency Injection Helpers</Title>
+    <Description>Provides extension methods for integrating MySqlConnector with .NET dependency injection.</Description>
+    <Copyright>Copyright 2024 Bradley Grainger</Copyright>
+    <Authors>Bradley Grainger</Authors>
+    <PackageReadmeFile>README.md</PackageReadmeFile>
+    <PackageTags>mysqlconnector;dependency injection;iservicecollection</PackageTags>
+    <IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">true</IsAotCompatible>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\MySqlConnector\MySqlConnector.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <None Include="docs\README.md" Pack="true" PackagePath="\" />
+  </ItemGroup>
+
+</Project>
diff --git a/src/MySqlConnector.DependencyInjection/MySqlConnectorServiceCollectionExtensions.cs b/src/MySqlConnector.DependencyInjection/MySqlConnectorServiceCollectionExtensions.cs
new file mode 100644
index 000000000..b7fb0b921
--- /dev/null
+++ b/src/MySqlConnector.DependencyInjection/MySqlConnectorServiceCollectionExtensions.cs
@@ -0,0 +1,192 @@
+using System.Data.Common;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Logging;
+
+namespace MySqlConnector;
+
+/// <summary>
+/// Extension method for setting up MySqlConnector services in an <see cref="IServiceCollection" />.
+/// </summary>
+public static class MySqlConnectorServiceCollectionExtensions
+{
+	/// <summary>
+	/// Registers a <see cref="MySqlDataSource" /> and a <see cref="MySqlConnection" /> in the <see cref="IServiceCollection" />.
+	/// </summary>
+	/// <param name="serviceCollection">The <see cref="IServiceCollection" /> to add services to.</param>
+	/// <param name="connectionString">A MySQL connection string.</param>
+	/// <param name="connectionLifetime">The lifetime with which to register the <see cref="MySqlConnection" /> in the container. Defaults to <see cref="ServiceLifetime.Transient" />.</param>
+	/// <param name="dataSourceLifetime">The lifetime with which to register the <see cref="MySqlDataSource" /> service in the container. Defaults to <see cref="ServiceLifetime.Singleton" />.</param>
+	/// <returns>The same service collection so that multiple calls can be chained.</returns>
+	public static IServiceCollection AddMySqlDataSource(
+		this IServiceCollection serviceCollection,
+		string connectionString,
+		ServiceLifetime connectionLifetime = ServiceLifetime.Transient,
+		ServiceLifetime dataSourceLifetime = ServiceLifetime.Singleton) =>
+		DoAddMySqlDataSource(serviceCollection, connectionString, dataSourceBuilderAction: null, connectionLifetime, dataSourceLifetime, builderActionState: null);
+
+	/// <summary>
+	/// Registers a <see cref="MySqlDataSource" /> and a <see cref="MySqlConnection" /> in the <see cref="IServiceCollection" />.
+	/// </summary>
+	/// <param name="serviceCollection">The <see cref="IServiceCollection" /> to add services to.</param>
+	/// <param name="connectionString">A MySQL connection string.</param>
+	/// <param name="dataSourceBuilderAction">An action to configure the <see cref="MySqlDataSourceBuilder" /> for further customizations of the <see cref="MySqlDataSource" />.</param>
+	/// <param name="connectionLifetime">The lifetime with which to register the <see cref="MySqlConnection" /> in the container. Defaults to <see cref="ServiceLifetime.Transient" />.</param>
+	/// <param name="dataSourceLifetime">The lifetime with which to register the <see cref="MySqlDataSource" /> service in the container. Defaults to <see cref="ServiceLifetime.Singleton" />.</param>
+	/// <returns>The same service collection so that multiple calls can be chained.</returns>
+	public static IServiceCollection AddMySqlDataSource(
+		this IServiceCollection serviceCollection,
+		string connectionString,
+		Action<MySqlDataSourceBuilder> dataSourceBuilderAction,
+		ServiceLifetime connectionLifetime = ServiceLifetime.Transient,
+		ServiceLifetime dataSourceLifetime = ServiceLifetime.Singleton) =>
+		DoAddMySqlDataSource(serviceCollection, connectionString, DataSourceBuilderThunk, connectionLifetime, dataSourceLifetime, builderActionState: dataSourceBuilderAction);
+
+	/// <summary>
+	/// Registers a <see cref="MySqlDataSource" /> and a <see cref="MySqlConnection" /> in the <see cref="IServiceCollection" />.
+	/// </summary>
+	/// <param name="serviceCollection">The <see cref="IServiceCollection" /> to add services to.</param>
+	/// <param name="connectionString">A MySQL connection string.</param>
+	/// <param name="dataSourceBuilderAction">An action to configure the <see cref="MySqlDataSourceBuilder" /> for further customizations of the <see cref="MySqlDataSource" />.</param>
+	/// <param name="connectionLifetime">The lifetime with which to register the <see cref="MySqlConnection" /> in the container. Defaults to <see cref="ServiceLifetime.Transient" />.</param>
+	/// <param name="dataSourceLifetime">The lifetime with which to register the <see cref="MySqlDataSource" /> service in the container. Defaults to <see cref="ServiceLifetime.Singleton" />.</param>
+	/// <returns>The same service collection so that multiple calls can be chained.</returns>
+	public static IServiceCollection AddMySqlDataSource(
+		this IServiceCollection serviceCollection,
+		string connectionString,
+		Action<IServiceProvider, MySqlDataSourceBuilder> dataSourceBuilderAction,
+		ServiceLifetime connectionLifetime = ServiceLifetime.Transient,
+		ServiceLifetime dataSourceLifetime = ServiceLifetime.Singleton) =>
+		DoAddMySqlDataSource(serviceCollection, connectionString, ServiceProviderDataSourceBuilderThunk, connectionLifetime, dataSourceLifetime, builderActionState: dataSourceBuilderAction);
+
+	/// <summary>
+	/// Registers a <see cref="MySqlDataSource" /> and a <see cref="MySqlConnection" /> in the <see cref="IServiceCollection" />.
+	/// </summary>
+	/// <param name="serviceCollection">The <see cref="IServiceCollection" /> to add services to.</param>
+	/// <param name="serviceKey">The <see cref="ServiceDescriptor.ServiceKey"/> of the service.</param>
+	/// <param name="connectionString">A MySQL connection string.</param>
+	/// <param name="connectionLifetime">The lifetime with which to register the <see cref="MySqlConnection" /> in the container. Defaults to <see cref="ServiceLifetime.Transient" />.</param>
+	/// <param name="dataSourceLifetime">The lifetime with which to register the <see cref="MySqlDataSource" /> service in the container. Defaults to <see cref="ServiceLifetime.Singleton" />.</param>
+	/// <returns>The same service collection so that multiple calls can be chained.</returns>
+	/// <remarks>If the <paramref name="serviceKey"/> is a <see langword="string"/>, it will automatically be used to initialize the data source name.</remarks>
+	public static IServiceCollection AddKeyedMySqlDataSource(
+		this IServiceCollection serviceCollection,
+		object? serviceKey,
+		string connectionString,
+		ServiceLifetime connectionLifetime = ServiceLifetime.Transient,
+		ServiceLifetime dataSourceLifetime = ServiceLifetime.Singleton) =>
+		DoAddMySqlDataSource(serviceCollection, serviceKey, connectionString, dataSourceBuilderAction: null, connectionLifetime, dataSourceLifetime, builderActionState: null);
+
+	/// <summary>
+	/// Registers a <see cref="MySqlDataSource" /> and a <see cref="MySqlConnection" /> in the <see cref="IServiceCollection" />.
+	/// </summary>
+	/// <param name="serviceCollection">The <see cref="IServiceCollection" /> to add services to.</param>
+	/// <param name="serviceKey">The <see cref="ServiceDescriptor.ServiceKey"/> of the service.</param>
+	/// <param name="connectionString">A MySQL connection string.</param>
+	/// <param name="dataSourceBuilderAction">An action to configure the <see cref="MySqlDataSourceBuilder" /> for further customizations of the <see cref="MySqlDataSource" />.</param>
+	/// <param name="connectionLifetime">The lifetime with which to register the <see cref="MySqlConnection" /> in the container. Defaults to <see cref="ServiceLifetime.Transient" />.</param>
+	/// <param name="dataSourceLifetime">The lifetime with which to register the <see cref="MySqlDataSource" /> service in the container. Defaults to <see cref="ServiceLifetime.Singleton" />.</param>
+	/// <returns>The same service collection so that multiple calls can be chained.</returns>
+	/// <remarks>If the <paramref name="serviceKey"/> is a <see langword="string"/>, it will automatically be used to initialize the data source name; this can
+	/// be overridden by the <paramref name="dataSourceBuilderAction"/> configuration action.</remarks>
+	public static IServiceCollection AddKeyedMySqlDataSource(
+		this IServiceCollection serviceCollection,
+		object? serviceKey,
+		string connectionString,
+		Action<MySqlDataSourceBuilder> dataSourceBuilderAction,
+		ServiceLifetime connectionLifetime = ServiceLifetime.Transient,
+		ServiceLifetime dataSourceLifetime = ServiceLifetime.Singleton) =>
+		DoAddMySqlDataSource(serviceCollection, serviceKey, connectionString, DataSourceBuilderThunk, connectionLifetime, dataSourceLifetime, builderActionState: dataSourceBuilderAction);
+
+	/// <summary>
+	/// Registers a <see cref="MySqlDataSource" /> and a <see cref="MySqlConnection" /> in the <see cref="IServiceCollection" />.
+	/// </summary>
+	/// <param name="serviceCollection">The <see cref="IServiceCollection" /> to add services to.</param>
+	/// <param name="serviceKey">The <see cref="ServiceDescriptor.ServiceKey"/> of the service.</param>
+	/// <param name="connectionString">A MySQL connection string.</param>
+	/// <param name="dataSourceBuilderAction">An action to configure the <see cref="MySqlDataSourceBuilder" /> for further customizations of the <see cref="MySqlDataSource" />.</param>
+	/// <param name="connectionLifetime">The lifetime with which to register the <see cref="MySqlConnection" /> in the container. Defaults to <see cref="ServiceLifetime.Transient" />.</param>
+	/// <param name="dataSourceLifetime">The lifetime with which to register the <see cref="MySqlDataSource" /> service in the container. Defaults to <see cref="ServiceLifetime.Singleton" />.</param>
+	/// <returns>The same service collection so that multiple calls can be chained.</returns>
+	/// <remarks>If the <paramref name="serviceKey"/> is a <see langword="string"/>, it will automatically be used to initialize the data source name; this can
+	/// be overridden by the <paramref name="dataSourceBuilderAction"/> configuration action.</remarks>
+	public static IServiceCollection AddKeyedMySqlDataSource(
+		this IServiceCollection serviceCollection,
+		object? serviceKey,
+		string connectionString,
+		Action<IServiceProvider, MySqlDataSourceBuilder> dataSourceBuilderAction,
+		ServiceLifetime connectionLifetime = ServiceLifetime.Transient,
+		ServiceLifetime dataSourceLifetime = ServiceLifetime.Singleton) =>
+		DoAddMySqlDataSource(serviceCollection, serviceKey, connectionString, ServiceProviderDataSourceBuilderThunk, connectionLifetime, dataSourceLifetime, builderActionState: dataSourceBuilderAction);
+
+	private static IServiceCollection DoAddMySqlDataSource(
+		this IServiceCollection serviceCollection,
+		string connectionString,
+		Action<IServiceProvider, MySqlDataSourceBuilder, object?>? dataSourceBuilderAction,
+		ServiceLifetime connectionLifetime,
+		ServiceLifetime dataSourceLifetime,
+		object? builderActionState)
+	{
+		serviceCollection.TryAdd(
+			new ServiceDescriptor(
+				typeof(MySqlDataSource),
+				serviceProvider =>
+				{
+					var dataSourceBuilder = new MySqlDataSourceBuilder(connectionString)
+						.UseLoggerFactory(serviceProvider.GetService<ILoggerFactory>());
+					dataSourceBuilderAction?.Invoke(serviceProvider, dataSourceBuilder, builderActionState);
+					return dataSourceBuilder.Build();
+				},
+				dataSourceLifetime));
+
+		serviceCollection.TryAdd(new ServiceDescriptor(typeof(MySqlConnection), static x => x.GetRequiredService<MySqlDataSource>().CreateConnection(), connectionLifetime));
+
+#if NET7_0_OR_GREATER
+		serviceCollection.TryAdd(new ServiceDescriptor(typeof(DbDataSource), static x => x.GetRequiredService<MySqlDataSource>(), dataSourceLifetime));
+#endif
+
+		serviceCollection.TryAdd(new ServiceDescriptor(typeof(DbConnection), static x => x.GetRequiredService<MySqlConnection>(), connectionLifetime));
+
+		return serviceCollection;
+	}
+
+	private static IServiceCollection DoAddMySqlDataSource(
+		this IServiceCollection serviceCollection,
+		object? serviceKey,
+		string connectionString,
+		Action<IServiceProvider, MySqlDataSourceBuilder, object?>? dataSourceBuilderAction,
+		ServiceLifetime connectionLifetime,
+		ServiceLifetime dataSourceLifetime,
+		object? builderActionState)
+	{
+		serviceCollection.TryAdd(
+			new ServiceDescriptor(
+				typeof(MySqlDataSource),
+				serviceKey,
+				(serviceProvider, serviceKey) =>
+				{
+					var dataSourceBuilder = new MySqlDataSourceBuilder(connectionString)
+						.UseLoggerFactory(serviceProvider.GetService<ILoggerFactory>())
+						.UseName(serviceKey as string);
+					dataSourceBuilderAction?.Invoke(serviceProvider, dataSourceBuilder, builderActionState);
+					return dataSourceBuilder.Build();
+				},
+				dataSourceLifetime));
+
+		serviceCollection.TryAdd(new ServiceDescriptor(typeof(MySqlConnection), serviceKey, static (sp, sk) => sp.GetRequiredKeyedService<MySqlDataSource>(sk).CreateConnection(), connectionLifetime));
+
+#if NET7_0_OR_GREATER
+		serviceCollection.TryAdd(new ServiceDescriptor(typeof(DbDataSource), serviceKey, static (sp, sk) => sp.GetRequiredKeyedService<MySqlDataSource>(sk), dataSourceLifetime));
+#endif
+
+		serviceCollection.TryAdd(new ServiceDescriptor(typeof(DbConnection), serviceKey, static (sp, sk) => sp.GetRequiredKeyedService<MySqlConnection>(sk), connectionLifetime));
+
+		return serviceCollection;
+	}
+
+	private static void DataSourceBuilderThunk(IServiceProvider serviceProvider, MySqlDataSourceBuilder dataSourceBuilder, object? state) =>
+		((Action<MySqlDataSourceBuilder>) state!)(dataSourceBuilder);
+
+	private static void ServiceProviderDataSourceBuilderThunk(IServiceProvider serviceProvider, MySqlDataSourceBuilder dataSourceBuilder, object? state) =>
+		((Action<IServiceProvider, MySqlDataSourceBuilder>) state!)(serviceProvider, dataSourceBuilder);
+}
diff --git a/src/MySqlConnector.DependencyInjection/docs/README.md b/src/MySqlConnector.DependencyInjection/docs/README.md
new file mode 100644
index 000000000..ea0e5ed2c
--- /dev/null
+++ b/src/MySqlConnector.DependencyInjection/docs/README.md
@@ -0,0 +1,80 @@
+## About
+
+MySqlConnector.DependencyInjection helps set up MySqlConnector in applications that use dependency injection, most notably in ASP.NET.
+It allows easy configuration of your MySQL connections and registers the appropriate services in your DI container.
+It also configures logging by integrating MySqlConnector with the `ILoggingFactory` registered with the service provider.
+
+## How to Use
+
+For example, if using the ASP.NET minimal web API, use the following to register MySqlConnector:
+
+```csharp
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddMySqlDataSource(builder.Configuration.GetConnectionString("Default"));
+```
+
+This registers a transient `MySqlConnection` which can get injected into your controllers:
+
+```csharp
+app.MapGet("/", async (MySqlConnection connection) =>
+{
+    await connection.OpenAsync();
+    await using var command = connection.CreateCommand();
+    command.CommandText = "SELECT name FROM users LIMIT 1";
+    return "Hello World: " + await command.ExecuteScalarAsync();
+});
+```
+
+You can use `MySqlDataSource` directly if you need more than one connection:
+
+```csharp
+app.MapGet("/", async (MySqlDataSource dataSource) =>
+{
+    await using var connection1 = await dataSource.OpenConnectionAsync();
+    await using var connection2 = await dataSource.OpenConnectionAsync();
+    // use the two connections...
+});
+```
+
+## Advanced Usage
+
+The `AddMySqlDataSource` method also accepts a lambda parameter allowing you to configure aspects of MySqlConnector beyond the connection string.
+
+```csharp
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddMySqlDataSource("Server=server;User ID=test;Password=test;Database=test",
+	x => x.UseRemoteCertificateValidationCallback((sender, certificate, chain, sslPolicyErrors) => { /* custom logic */ })
+);
+```
+
+## Keyed Services
+
+Use the `AddKeyedMySqlDataSource` method to register a `MySqlDataSource` as a [keyed service](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8/runtime#keyed-di-services).
+This is useful if you have multiple connection strings or need to connect to multiple databases.
+If the service key is a string, it will automatically be used as the `MySqlDataSource` name;
+to customize this, call the `AddKeyedMySqlDataSource(object?, string, Action<MySqlDataSourceBuilder>)` overload and call `MySqlDataSourceBuilder.UseName`.
+
+```csharp
+builder.Services.AddKeyedMySqlDataSource("users", builder.Configuration.GetConnectionString("Users"));
+builder.Services.AddKeyedMySqlDataSource("products", builder.Configuration.GetConnectionString("Products"));
+
+app.MapGet("/users/{userId}", async (int userId, [FromKeyedServices("users")] MySqlConnection connection) =>
+{
+    await connection.OpenAsync();
+    await using var command = connection.CreateCommand();
+    command.CommandText = "SELECT name FROM users WHERE user_id = @userId LIMIT 1";
+    command.Parameters.AddWithValue("@userId", userId);
+    return $"Hello, {await command.ExecuteScalarAsync()}";
+});
+
+app.MapGet("/products/{productId}", async (int productId, [FromKeyedServices("products")] MySqlConnection connection) =>
+{
+    await connection.OpenAsync();
+    await using var command = connection.CreateCommand();
+    command.CommandText = "SELECT name FROM products WHERE product_id = @productId LIMIT 1";
+    command.Parameters.AddWithValue("@productId", productId);
+    return await command.ExecuteScalarAsync();
+});
+```
diff --git a/src/MySqlConnector.Logging.Microsoft.Extensions.Logging/CompatibilitySuppressions.xml b/src/MySqlConnector.Logging.Microsoft.Extensions.Logging/CompatibilitySuppressions.xml
new file mode 100644
index 000000000..15ae18d7e
--- /dev/null
+++ b/src/MySqlConnector.Logging.Microsoft.Extensions.Logging/CompatibilitySuppressions.xml
@@ -0,0 +1,11 @@
+﻿<?xml version="1.0" encoding="utf-8"?>
+<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
+<Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+  <Suppression>
+    <DiagnosticId>CP0001</DiagnosticId>
+    <Target>T:Microsoft.Extensions.DependencyInjection.MySqlConnectorLoggingExtensions</Target>
+    <Left>lib/netstandard2.0/MySqlConnector.Logging.Microsoft.Extensions.Logging.dll</Left>
+    <Right>lib/netstandard2.0/MySqlConnector.Logging.Microsoft.Extensions.Logging.dll</Right>
+    <IsBaselineSuppression>true</IsBaselineSuppression>
+  </Suppression>
+</Suppressions>
\ No newline at end of file
diff --git a/src/MySqlConnector.Logging.Microsoft.Extensions.Logging/MicrosoftExtensionsLoggingLoggerProvider.cs b/src/MySqlConnector.Logging.Microsoft.Extensions.Logging/MicrosoftExtensionsLoggingLoggerProvider.cs
index 0bd8f9989..9f2f19713 100644
--- a/src/MySqlConnector.Logging.Microsoft.Extensions.Logging/MicrosoftExtensionsLoggingLoggerProvider.cs
+++ b/src/MySqlConnector.Logging.Microsoft.Extensions.Logging/MicrosoftExtensionsLoggingLoggerProvider.cs
@@ -61,8 +61,8 @@ public void Log(MySqlConnectorLogLevel level, string message, object?[]? args =
 			_ => throw new ArgumentOutOfRangeException(nameof(level), level, "Invalid value for 'level'."),
 		};
 
-		private static readonly Func<string, Exception, string> s_getMessage = static (s, e) => s;
-		private static readonly Func<(string Message, object?[] Args), Exception, string> s_messageFormatter = static (s, e) => string.Format(CultureInfo.InvariantCulture, s.Message, s.Args);
+		private static readonly Func<string, Exception?, string> s_getMessage = static (s, e) => s;
+		private static readonly Func<(string Message, object?[] Args), Exception?, string> s_messageFormatter = static (s, e) => string.Format(CultureInfo.InvariantCulture, s.Message, s.Args);
 
 		private readonly ILogger m_logger;
 	}
diff --git a/src/MySqlConnector.Logging.Microsoft.Extensions.Logging/MySqlConnector.Logging.Microsoft.Extensions.Logging.csproj b/src/MySqlConnector.Logging.Microsoft.Extensions.Logging/MySqlConnector.Logging.Microsoft.Extensions.Logging.csproj
index 2995826b4..ba6a73e1c 100644
--- a/src/MySqlConnector.Logging.Microsoft.Extensions.Logging/MySqlConnector.Logging.Microsoft.Extensions.Logging.csproj
+++ b/src/MySqlConnector.Logging.Microsoft.Extensions.Logging/MySqlConnector.Logging.Microsoft.Extensions.Logging.csproj
@@ -4,7 +4,7 @@
     <TargetFrameworks>netstandard2.0</TargetFrameworks>
     <Title>MySqlConnector Logging Adapter for Microsoft.Extensions.Logging</Title>
     <Description>Writes MySqlConnector logging output to Microsoft.Extensions.Logging with one line of code.</Description>
-    <Copyright>Copyright 2017–2023 Bradley Grainger</Copyright>
+    <Copyright>Copyright 2017–2024 Bradley Grainger</Copyright>
     <Authors>Bradley Grainger</Authors>
     <PackageReadmeFile>README.md</PackageReadmeFile>
     <PackageTags>mysql;mysqlconnector;async;ado.net;database;netcore;logging</PackageTags>
@@ -12,7 +12,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.0.0" />
+    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
   </ItemGroup>
 
   <ItemGroup>
@@ -20,7 +20,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <None Include="docs\README.md" Pack="true" PackagePath="\"/>
+    <None Include="docs\README.md" Pack="true" PackagePath="\" />
   </ItemGroup>
 
 </Project>
diff --git a/src/MySqlConnector.Logging.Microsoft.Extensions.Logging/MySqlConnectorLoggingExtensions.cs b/src/MySqlConnector.Logging.Microsoft.Extensions.Logging/MySqlConnectorLoggingExtensions.cs
index fac1d5c83..b5c59efe0 100644
--- a/src/MySqlConnector.Logging.Microsoft.Extensions.Logging/MySqlConnectorLoggingExtensions.cs
+++ b/src/MySqlConnector.Logging.Microsoft.Extensions.Logging/MySqlConnectorLoggingExtensions.cs
@@ -1,11 +1,11 @@
 using System;
 using Microsoft.Extensions.Logging;
-using MySqlConnector.Logging;
 
-namespace Microsoft.Extensions.DependencyInjection;
+namespace MySqlConnector.Logging;
 
 public static class MySqlConnectorLoggingExtensions
 {
+	[Obsolete("Use UseLoggerFactory or AddMySqlDataSource instead. See https://mysqlconnector.net/diagnostics/logging/.")]
 	public static IServiceProvider UseMySqlConnectorLogging(this IServiceProvider services)
 	{
 		var loggerFactory = (ILoggerFactory) services.GetService(typeof(ILoggerFactory));
diff --git a/src/MySqlConnector.Logging.NLog/CompatibilitySuppressions.xml b/src/MySqlConnector.Logging.NLog/CompatibilitySuppressions.xml
index a95918371..738dbc799 100644
--- a/src/MySqlConnector.Logging.NLog/CompatibilitySuppressions.xml
+++ b/src/MySqlConnector.Logging.NLog/CompatibilitySuppressions.xml
@@ -1,4 +1,5 @@
 ﻿<?xml version="1.0" encoding="utf-8"?>
+<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
 <Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <Suppression>
     <DiagnosticId>PKV006</DiagnosticId>
diff --git a/src/MySqlConnector.Logging.NLog/MySqlConnector.Logging.NLog.csproj b/src/MySqlConnector.Logging.NLog/MySqlConnector.Logging.NLog.csproj
index 15743672d..bf85d7097 100644
--- a/src/MySqlConnector.Logging.NLog/MySqlConnector.Logging.NLog.csproj
+++ b/src/MySqlConnector.Logging.NLog/MySqlConnector.Logging.NLog.csproj
@@ -1,10 +1,10 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFrameworks>net461;netstandard2.0</TargetFrameworks>
+    <TargetFrameworks>net462;netstandard2.0</TargetFrameworks>
     <Title>MySqlConnector Logging Adapter for NLog</Title>
     <Description>Writes lightly-structured MySqlConnector logging output to NLog.</Description>
-    <Copyright>Copyright 2018–2023 Bradley Grainger</Copyright>
+    <Copyright>Copyright 2018–2024 Bradley Grainger</Copyright>
     <Authors>Rolf Kristensen</Authors>
     <PackageReadmeFile>README.md</PackageReadmeFile>
     <PackageTags>mysql;mysqlconnector;async;ado.net;database;netcore;NLog;logging</PackageTags>
@@ -12,7 +12,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="NLog" Version="4.5.0" />
+    <PackageReference Include="NLog" />
   </ItemGroup>
 
   <ItemGroup>
@@ -20,7 +20,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <None Include="docs\README.md" Pack="true" PackagePath="\"/>
+    <None Include="docs\README.md" Pack="true" PackagePath="\" />
   </ItemGroup>
 
 </Project>
diff --git a/src/MySqlConnector.Logging.Serilog/CompatibilitySuppressions.xml b/src/MySqlConnector.Logging.Serilog/CompatibilitySuppressions.xml
index a95918371..738dbc799 100644
--- a/src/MySqlConnector.Logging.Serilog/CompatibilitySuppressions.xml
+++ b/src/MySqlConnector.Logging.Serilog/CompatibilitySuppressions.xml
@@ -1,4 +1,5 @@
 ﻿<?xml version="1.0" encoding="utf-8"?>
+<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
 <Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <Suppression>
     <DiagnosticId>PKV006</DiagnosticId>
diff --git a/src/MySqlConnector.Logging.Serilog/MySqlConnector.Logging.Serilog.csproj b/src/MySqlConnector.Logging.Serilog/MySqlConnector.Logging.Serilog.csproj
index a16297f82..8470a3926 100644
--- a/src/MySqlConnector.Logging.Serilog/MySqlConnector.Logging.Serilog.csproj
+++ b/src/MySqlConnector.Logging.Serilog/MySqlConnector.Logging.Serilog.csproj
@@ -1,10 +1,10 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFrameworks>net461;netstandard2.0</TargetFrameworks>
+    <TargetFrameworks>net462;netstandard2.0</TargetFrameworks>
     <Title>MySqlConnector Logging Adapter for Serilog</Title>
     <Description>Writes lightly-structured MySqlConnector logging output to Serilog.</Description>
-    <Copyright>Copyright 2017–2023 Bradley Grainger</Copyright>
+    <Copyright>Copyright 2017–2024 Bradley Grainger</Copyright>
     <Authors>Marc Lewandowski</Authors>
     <PackageReadmeFile>README.md</PackageReadmeFile>
     <PackageTags>mysql;mysqlconnector;async;ado.net;database;netcore;serilog;logging</PackageTags>
@@ -12,7 +12,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Serilog" Version="2.6.0" />
+    <PackageReference Include="Serilog" />
   </ItemGroup>
 
   <ItemGroup>
@@ -20,7 +20,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <None Include="docs\README.md" Pack="true" PackagePath="\"/>
+    <None Include="docs\README.md" Pack="true" PackagePath="\" />
   </ItemGroup>
 
 </Project>
diff --git a/src/MySqlConnector.Logging.log4net/CompatibilitySuppressions.xml b/src/MySqlConnector.Logging.log4net/CompatibilitySuppressions.xml
index a95918371..738dbc799 100644
--- a/src/MySqlConnector.Logging.log4net/CompatibilitySuppressions.xml
+++ b/src/MySqlConnector.Logging.log4net/CompatibilitySuppressions.xml
@@ -1,4 +1,5 @@
 ﻿<?xml version="1.0" encoding="utf-8"?>
+<!-- https://learn.microsoft.com/en-us/dotnet/fundamentals/package-validation/diagnostic-ids -->
 <Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <Suppression>
     <DiagnosticId>PKV006</DiagnosticId>
diff --git a/src/MySqlConnector.Logging.log4net/MySqlConnector.Logging.log4net.csproj b/src/MySqlConnector.Logging.log4net/MySqlConnector.Logging.log4net.csproj
index dcdf904a0..175af91d4 100644
--- a/src/MySqlConnector.Logging.log4net/MySqlConnector.Logging.log4net.csproj
+++ b/src/MySqlConnector.Logging.log4net/MySqlConnector.Logging.log4net.csproj
@@ -1,10 +1,10 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFrameworks>net461;netstandard2.0</TargetFrameworks>
+    <TargetFrameworks>net462;netstandard2.0</TargetFrameworks>
     <Title>MySqlConnector Logging Adapter for log4net</Title>
     <Description>Writes MySqlConnector logging output to log4net with one line of code.</Description>
-    <Copyright>Copyright 2017–2023 Bradley Grainger</Copyright>
+    <Copyright>Copyright 2017–2024 Bradley Grainger</Copyright>
     <Authors>Bradley Grainger</Authors>
     <PackageReadmeFile>README.md</PackageReadmeFile>
     <PackageTags>mysql;mysqlconnector;async;ado.net;database;netcore;log4net;logging</PackageTags>
@@ -12,7 +12,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="log4net" Version="2.0.10" />
+    <PackageReference Include="log4net" />
   </ItemGroup>
 
   <ItemGroup>
@@ -20,7 +20,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <None Include="docs\README.md" Pack="true" PackagePath="\"/>
+    <None Include="docs\README.md" Pack="true" PackagePath="\" />
   </ItemGroup>
 
 </Project>
diff --git a/src/MySqlConnector/Authentication/AuthenticationPlugins.cs b/src/MySqlConnector/Authentication/AuthenticationPlugins.cs
index 0b1c1246c..92fdf19c7 100644
--- a/src/MySqlConnector/Authentication/AuthenticationPlugins.cs
+++ b/src/MySqlConnector/Authentication/AuthenticationPlugins.cs
@@ -13,11 +13,18 @@ public static class AuthenticationPlugins
 	/// <param name="plugin">The authentication plugin.</param>
 	public static void Register(IAuthenticationPlugin plugin)
 	{
+#if NET6_0_OR_GREATER
+		ArgumentNullException.ThrowIfNull(plugin);
+#else
 		if (plugin is null)
 			throw new ArgumentNullException(nameof(plugin));
+#endif
+#if NET8_0_OR_GREATER
+		ArgumentException.ThrowIfNullOrEmpty(plugin.Name);
+#else
 		if (string.IsNullOrEmpty(plugin.Name))
 			throw new ArgumentException("Invalid plugin name.", nameof(plugin));
-
+#endif
 		lock (s_lock)
 			s_plugins.Add(plugin.Name, plugin);
 	}
@@ -28,6 +35,10 @@ internal static bool TryGetPlugin(string name, [NotNullWhen(true)] out IAuthenti
 			return s_plugins.TryGetValue(name, out plugin);
 	}
 
+#if NET9_0_OR_GREATER
+	private static readonly Lock s_lock = new();
+#else
 	private static readonly object s_lock = new();
-	private static readonly Dictionary<string, IAuthenticationPlugin> s_plugins = new();
+#endif
+	private static readonly Dictionary<string, IAuthenticationPlugin> s_plugins = [];
 }
diff --git a/src/MySqlConnector/Authentication/IAuthenticationPlugin.cs b/src/MySqlConnector/Authentication/IAuthenticationPlugin.cs
index 063bd4c8b..bc432c0d1 100644
--- a/src/MySqlConnector/Authentication/IAuthenticationPlugin.cs
+++ b/src/MySqlConnector/Authentication/IAuthenticationPlugin.cs
@@ -20,3 +20,38 @@ public interface IAuthenticationPlugin
 	/// <returns>The authentication response.</returns>
 	byte[] CreateResponse(string password, ReadOnlySpan<byte> authenticationData);
 }
+
+/// <summary>
+/// <see cref="IAuthenticationPlugin2"/> is an extension to <see cref="IAuthenticationPlugin"/> that returns a hash of the client's password.
+/// </summary>
+[Obsolete("Use IAuthenticationPlugin3 instead.")]
+public interface IAuthenticationPlugin2 : IAuthenticationPlugin
+{
+	/// <summary>
+	/// Hashes the client's password (e.g., for TLS certificate fingerprint verification).
+	/// </summary>
+	/// <param name="password">The client's password.</param>
+	/// <param name="authenticationData">The authentication data supplied by the server; this is the <code>auth method data</code>
+	/// from the <a href="https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest">Authentication
+	/// Method Switch Request Packet</a>.</param>
+	/// <returns>The authentication-method-specific hash of the client's password.</returns>
+	byte[] CreatePasswordHash(string password, ReadOnlySpan<byte> authenticationData);
+}
+
+/// <summary>
+/// <see cref="IAuthenticationPlugin3"/> is an extension to <see cref="IAuthenticationPlugin"/> that also returns a hash of the client's password.
+/// </summary>
+/// <remarks>If an authentication plugin supports this interface, the base <see cref="IAuthenticationPlugin.CreateResponse(string, ReadOnlySpan{byte})"/> method will not be called.</remarks>
+public interface IAuthenticationPlugin3 : IAuthenticationPlugin
+{
+	/// <summary>
+	/// Creates the authentication response and hashes the client's password (e.g., for TLS certificate fingerprint verification).
+	/// </summary>
+	/// <param name="password">The client's password.</param>
+	/// <param name="authenticationData">The authentication data supplied by the server; this is the <code>auth method data</code>
+	/// from the <a href="https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest">Authentication
+	/// Method Switch Request Packet</a>.</param>
+	/// <param name="authenticationResponse">The authentication response.</param>
+	/// <param name="passwordHash">The authentication-method-specific hash of the client's password.</param>
+	void CreateResponseAndPasswordHash(string password, ReadOnlySpan<byte> authenticationData, out byte[] authenticationResponse, out byte[] passwordHash);
+}
diff --git a/src/MySqlConnector/ColumnReaders/BinaryBooleanColumnReader.cs b/src/MySqlConnector/ColumnReaders/BinaryBooleanColumnReader.cs
new file mode 100644
index 000000000..9f45dedab
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/BinaryBooleanColumnReader.cs
@@ -0,0 +1,19 @@
+using System.Runtime.CompilerServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class BinaryBooleanColumnReader : ColumnReader
+{
+	public static BinaryBooleanColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	public override int? TryReadInt32(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data) ? 1 : 0;
+
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	private static bool DoReadValue(ReadOnlySpan<byte> data) =>
+		data[0] != 0;
+}
diff --git a/src/MySqlConnector/ColumnReaders/BinaryDateTimeColumnReader.cs b/src/MySqlConnector/ColumnReaders/BinaryDateTimeColumnReader.cs
new file mode 100644
index 000000000..defa68c19
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/BinaryDateTimeColumnReader.cs
@@ -0,0 +1,66 @@
+using System.Runtime.InteropServices;
+using System.Text;
+using MySqlConnector.Protocol.Payloads;
+using MySqlConnector.Utilities;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class BinaryDateTimeColumnReader : ColumnReader
+{
+	public BinaryDateTimeColumnReader(MySqlConnection connection)
+	{
+		m_allowZeroDateTime = connection.AllowZeroDateTime;
+		m_convertZeroDateTime = connection.ConvertZeroDateTime;
+		m_dateTimeKind = connection.DateTimeKind;
+	}
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition)
+	{
+		if (data.Length == 0)
+		{
+			if (m_convertZeroDateTime)
+				return DateTime.MinValue;
+			if (m_allowZeroDateTime)
+				return default(MySqlDateTime);
+			throw new InvalidCastException("Unable to convert MySQL date/time to System.DateTime.");
+		}
+
+		int year = data[0] + data[1] * 256;
+		int month = data[2];
+		int day = data[3];
+
+		int hour, minute, second;
+		if (data.Length <= 4)
+		{
+			hour = 0;
+			minute = 0;
+			second = 0;
+		}
+		else
+		{
+			hour = data[4];
+			minute = data[5];
+			second = data[6];
+		}
+
+		var microseconds = data.Length <= 7 ? 0 : MemoryMarshal.Read<int>(data[7..]);
+
+		try
+		{
+			return m_allowZeroDateTime ? new MySqlDateTime(year, month, day, hour, minute, second, microseconds) :
+#if NET7_0_OR_GREATER
+				new DateTime(year, month, day, hour, minute, second, microseconds / 1000, microseconds % 1000, m_dateTimeKind);
+#else
+				new DateTime(year, month, day, hour, minute, second, microseconds / 1000, m_dateTimeKind).AddTicks(microseconds % 1000 * 10);
+#endif
+		}
+		catch (Exception ex)
+		{
+			throw new FormatException($"Couldn't interpret value as a valid DateTime: {Encoding.UTF8.GetString(data)}", ex);
+		}
+	}
+
+	private readonly bool m_allowZeroDateTime;
+	private readonly bool m_convertZeroDateTime;
+	private readonly DateTimeKind m_dateTimeKind;
+}
diff --git a/src/MySqlConnector/ColumnReaders/BinaryDoubleColumnReader.cs b/src/MySqlConnector/ColumnReaders/BinaryDoubleColumnReader.cs
new file mode 100644
index 000000000..6b8c49456
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/BinaryDoubleColumnReader.cs
@@ -0,0 +1,12 @@
+using System.Runtime.InteropServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class BinaryDoubleColumnReader : ColumnReader
+{
+	public static BinaryDoubleColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		MemoryMarshal.Read<double>(data);
+}
diff --git a/src/MySqlConnector/ColumnReaders/BinaryFloatColumnReader.cs b/src/MySqlConnector/ColumnReaders/BinaryFloatColumnReader.cs
new file mode 100644
index 000000000..7ab4d5726
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/BinaryFloatColumnReader.cs
@@ -0,0 +1,12 @@
+using System.Runtime.InteropServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class BinaryFloatColumnReader : ColumnReader
+{
+	public static BinaryFloatColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		MemoryMarshal.Read<float>(data);
+}
diff --git a/src/MySqlConnector/ColumnReaders/BinarySignedInt16ColumnReader.cs b/src/MySqlConnector/ColumnReaders/BinarySignedInt16ColumnReader.cs
new file mode 100644
index 000000000..60e6ffa2b
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/BinarySignedInt16ColumnReader.cs
@@ -0,0 +1,20 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class BinarySignedInt16ColumnReader : ColumnReader
+{
+	public static BinarySignedInt16ColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	public override int? TryReadInt32(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	private static short DoReadValue(ReadOnlySpan<byte> data) =>
+		MemoryMarshal.Read<short>(data);
+}
diff --git a/src/MySqlConnector/ColumnReaders/BinarySignedInt32ColumnReader.cs b/src/MySqlConnector/ColumnReaders/BinarySignedInt32ColumnReader.cs
new file mode 100644
index 000000000..e35c499ca
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/BinarySignedInt32ColumnReader.cs
@@ -0,0 +1,20 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class BinarySignedInt32ColumnReader : ColumnReader
+{
+	public static BinarySignedInt32ColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	public override int? TryReadInt32(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	private static int DoReadValue(ReadOnlySpan<byte> data) =>
+		MemoryMarshal.Read<int>(data);
+}
diff --git a/src/MySqlConnector/ColumnReaders/BinarySignedInt64ColumnReader.cs b/src/MySqlConnector/ColumnReaders/BinarySignedInt64ColumnReader.cs
new file mode 100644
index 000000000..38b17e572
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/BinarySignedInt64ColumnReader.cs
@@ -0,0 +1,20 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class BinarySignedInt64ColumnReader : ColumnReader
+{
+	public static BinarySignedInt64ColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	public override int? TryReadInt32(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		checked((int) DoReadValue(data));
+
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	private static long DoReadValue(ReadOnlySpan<byte> data) =>
+		MemoryMarshal.Read<long>(data);
+}
diff --git a/src/MySqlConnector/ColumnReaders/BinarySignedInt8ColumnReader.cs b/src/MySqlConnector/ColumnReaders/BinarySignedInt8ColumnReader.cs
new file mode 100644
index 000000000..6737512e4
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/BinarySignedInt8ColumnReader.cs
@@ -0,0 +1,19 @@
+using System.Runtime.CompilerServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class BinarySignedInt8ColumnReader : ColumnReader
+{
+	public static BinarySignedInt8ColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	public override int? TryReadInt32(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	private static sbyte DoReadValue(ReadOnlySpan<byte> data) =>
+		(sbyte) data[0];
+}
diff --git a/src/MySqlConnector/ColumnReaders/BinaryTimeColumnReader.cs b/src/MySqlConnector/ColumnReaders/BinaryTimeColumnReader.cs
new file mode 100644
index 000000000..ff2b17fff
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/BinaryTimeColumnReader.cs
@@ -0,0 +1,39 @@
+using System.Runtime.InteropServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class BinaryTimeColumnReader : ColumnReader
+{
+	public static BinaryTimeColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition)
+	{
+		if (data.Length == 0)
+			return TimeSpan.Zero;
+
+		var isNegative = data[0];
+		var days = MemoryMarshal.Read<int>(data[1..]);
+		var hours = (int) data[5];
+		var minutes = (int) data[6];
+		var seconds = (int) data[7];
+		var microseconds = data.Length == 8 ? 0 : MemoryMarshal.Read<int>(data[8..]);
+
+		if (isNegative != 0)
+		{
+			days = -days;
+			hours = -hours;
+			minutes = -minutes;
+			seconds = -seconds;
+			microseconds = -microseconds;
+		}
+
+#if NET9_0_OR_GREATER
+		return TimeSpan.FromDays(days, hours, minutes, seconds, microseconds: microseconds);
+#elif NET7_0_OR_GREATER
+		return new TimeSpan(days, hours, minutes, seconds, microseconds / 1000, microseconds % 1000);
+#else
+		return new TimeSpan(days, hours, minutes, seconds) + TimeSpan.FromTicks(microseconds * 10);
+#endif
+	}
+}
diff --git a/src/MySqlConnector/ColumnReaders/BinaryUnsignedInt16ColumnReader.cs b/src/MySqlConnector/ColumnReaders/BinaryUnsignedInt16ColumnReader.cs
new file mode 100644
index 000000000..7b4a52099
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/BinaryUnsignedInt16ColumnReader.cs
@@ -0,0 +1,20 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class BinaryUnsignedInt16ColumnReader : ColumnReader
+{
+	public static BinaryUnsignedInt16ColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	public override int? TryReadInt32(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	private static ushort DoReadValue(ReadOnlySpan<byte> data) =>
+		MemoryMarshal.Read<ushort>(data);
+}
diff --git a/src/MySqlConnector/ColumnReaders/BinaryUnsignedInt32ColumnReader.cs b/src/MySqlConnector/ColumnReaders/BinaryUnsignedInt32ColumnReader.cs
new file mode 100644
index 000000000..b84065a0d
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/BinaryUnsignedInt32ColumnReader.cs
@@ -0,0 +1,20 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class BinaryUnsignedInt32ColumnReader : ColumnReader
+{
+	public static BinaryUnsignedInt32ColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	public override int? TryReadInt32(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		checked((int) DoReadValue(data));
+
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	private static uint DoReadValue(ReadOnlySpan<byte> data) =>
+		MemoryMarshal.Read<uint>(data);
+}
diff --git a/src/MySqlConnector/ColumnReaders/BinaryUnsignedInt64ColumnReader.cs b/src/MySqlConnector/ColumnReaders/BinaryUnsignedInt64ColumnReader.cs
new file mode 100644
index 000000000..a0c795f78
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/BinaryUnsignedInt64ColumnReader.cs
@@ -0,0 +1,20 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class BinaryUnsignedInt64ColumnReader : ColumnReader
+{
+	public static BinaryUnsignedInt64ColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	public override int? TryReadInt32(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		checked((int) DoReadValue(data));
+
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	private static ulong DoReadValue(ReadOnlySpan<byte> data) =>
+		MemoryMarshal.Read<ulong>(data);
+}
diff --git a/src/MySqlConnector/ColumnReaders/BinaryUnsignedInt8ColumnReader.cs b/src/MySqlConnector/ColumnReaders/BinaryUnsignedInt8ColumnReader.cs
new file mode 100644
index 000000000..4c7cbc88a
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/BinaryUnsignedInt8ColumnReader.cs
@@ -0,0 +1,19 @@
+using System.Runtime.CompilerServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class BinaryUnsignedInt8ColumnReader : ColumnReader
+{
+	public static BinaryUnsignedInt8ColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	public override int? TryReadInt32(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	private static byte DoReadValue(ReadOnlySpan<byte> data) =>
+		data[0];
+}
diff --git a/src/MySqlConnector/ColumnReaders/BinaryYearColumnReader.cs b/src/MySqlConnector/ColumnReaders/BinaryYearColumnReader.cs
new file mode 100644
index 000000000..d3f2506cd
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/BinaryYearColumnReader.cs
@@ -0,0 +1,20 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class BinaryYearColumnReader : ColumnReader
+{
+	public static BinaryYearColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	public override int? TryReadInt32(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	private static int DoReadValue(ReadOnlySpan<byte> data) =>
+		MemoryMarshal.Read<short>(data);
+}
diff --git a/src/MySqlConnector/ColumnReaders/BitColumnReader.cs b/src/MySqlConnector/ColumnReaders/BitColumnReader.cs
new file mode 100644
index 000000000..d4d87ea47
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/BitColumnReader.cs
@@ -0,0 +1,39 @@
+using System.Buffers.Text;
+using MySqlConnector.Protocol;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class BitColumnReader : ColumnReader
+{
+	public static BitColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data, columnDefinition);
+
+	public override int? TryReadInt32(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		checked((int) DoReadValue(data, columnDefinition));
+
+	private static ulong DoReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition)
+	{
+		if ((columnDefinition.ColumnFlags & ColumnFlags.Binary) == 0)
+		{
+			// when the Binary flag IS NOT set, the BIT column is transmitted as MSB byte array
+			ulong bitValue = 0;
+			for (int i = 0; i < data.Length; i++)
+				bitValue = bitValue * 256 + data[i];
+			return bitValue;
+		}
+		else if (columnDefinition.ColumnLength <= 5 && data.Length == 1 && data[0] < (byte) (1 << (int) columnDefinition.ColumnLength))
+		{
+			// a server bug may return the data as binary even when we expect text: https://github.com/mysql-net/MySqlConnector/issues/713
+			// in this case, the data can't possibly be an ASCII digit, so assume it's the binary serialisation of BIT(n) where n <= 5
+			return data[0];
+		}
+		else
+		{
+			// when the Binary flag IS set, the BIT column is transmitted as text
+			return !Utf8Parser.TryParse(data, out ulong value, out var bytesConsumed) || bytesConsumed != data.Length ? throw new FormatException() : value;
+		}
+	}
+}
diff --git a/src/MySqlConnector/ColumnReaders/BytesColumnReader.cs b/src/MySqlConnector/ColumnReaders/BytesColumnReader.cs
new file mode 100644
index 000000000..facd2d1cc
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/BytesColumnReader.cs
@@ -0,0 +1,11 @@
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class BytesColumnReader : ColumnReader
+{
+	public static BytesColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		data.ToArray();
+}
diff --git a/src/MySqlConnector/ColumnReaders/ColumnReader.cs b/src/MySqlConnector/ColumnReaders/ColumnReader.cs
new file mode 100644
index 000000000..24baba810
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/ColumnReader.cs
@@ -0,0 +1,128 @@
+using MySqlConnector.Protocol;
+using MySqlConnector.Protocol.Payloads;
+using MySqlConnector.Protocol.Serialization;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal abstract class ColumnReader
+{
+	public static ColumnReader Create(bool isBinary, ColumnDefinitionPayload columnDefinition, MySqlConnection connection)
+	{
+		var isUnsigned = (columnDefinition.ColumnFlags & ColumnFlags.Unsigned) != 0;
+		switch (columnDefinition.ColumnType)
+		{
+			case ColumnType.Tiny:
+				if (connection.TreatTinyAsBoolean && columnDefinition.ColumnLength == 1 && !isUnsigned)
+					return isBinary ? BinaryBooleanColumnReader.Instance : TextBooleanColumnReader.Instance;
+				return (isBinary, isUnsigned) switch
+				{
+					(false, false) => TextSignedInt8ColumnReader.Instance,
+					(false, true) => TextUnsignedInt8ColumnReader.Instance,
+					(true, false) => BinarySignedInt8ColumnReader.Instance,
+					(true, true) => BinaryUnsignedInt8ColumnReader.Instance,
+				};
+
+			case ColumnType.Int24:
+			case ColumnType.Long:
+				return (isBinary, isUnsigned) switch
+				{
+					(false, false) => TextSignedInt32ColumnReader.Instance,
+					(false, true) => TextUnsignedInt32ColumnReader.Instance,
+					(true, false) => BinarySignedInt32ColumnReader.Instance,
+					(true, true) => BinaryUnsignedInt32ColumnReader.Instance,
+				};
+
+			case ColumnType.Longlong:
+				return (isBinary, isUnsigned) switch
+				{
+					(false, false) => TextSignedInt64ColumnReader.Instance,
+					(false, true) => TextUnsignedInt64ColumnReader.Instance,
+					(true, false) => BinarySignedInt64ColumnReader.Instance,
+					(true, true) => BinaryUnsignedInt64ColumnReader.Instance,
+				};
+
+			case ColumnType.Bit:
+				return BitColumnReader.Instance;
+
+			case ColumnType.String:
+				if (connection.GuidFormat == MySqlGuidFormat.Char36 && columnDefinition.ColumnLength / ProtocolUtility.GetBytesPerCharacter(columnDefinition.CharacterSet) == 36)
+					return GuidChar36ColumnReader.Instance;
+				if (connection.GuidFormat == MySqlGuidFormat.Char32 && columnDefinition.ColumnLength / ProtocolUtility.GetBytesPerCharacter(columnDefinition.CharacterSet) == 32)
+					return GuidChar32ColumnReader.Instance;
+				goto case ColumnType.VarString;
+
+			case ColumnType.VarString:
+			case ColumnType.VarChar:
+			case ColumnType.TinyBlob:
+			case ColumnType.Blob:
+			case ColumnType.MediumBlob:
+			case ColumnType.LongBlob:
+			case ColumnType.Enum:
+			case ColumnType.Set:
+				if (columnDefinition.CharacterSet != CharacterSet.Binary)
+					return StringColumnReader.Instance;
+				if (columnDefinition.ColumnLength == 16)
+				{
+					return connection.GuidFormat switch
+					{
+						MySqlGuidFormat.Binary16 => GuidBinary16ColumnReader.Instance,
+						MySqlGuidFormat.TimeSwapBinary16 => GuidTimeSwapBinary16ColumnReader.Instance,
+						MySqlGuidFormat.LittleEndianBinary16 => GuidLittleEndianBinary16ColumnReader.Instance,
+						_ => BytesColumnReader.Instance,
+					};
+				}
+				return BytesColumnReader.Instance;
+
+			case ColumnType.Json:
+				return StringColumnReader.Instance;
+
+			case ColumnType.Short:
+				return (isBinary, isUnsigned) switch
+				{
+					(false, false) => TextSignedInt16ColumnReader.Instance,
+					(false, true) => TextUnsignedInt16ColumnReader.Instance,
+					(true, false) => BinarySignedInt16ColumnReader.Instance,
+					(true, true) => BinaryUnsignedInt16ColumnReader.Instance,
+				};
+
+			case ColumnType.Date:
+			case ColumnType.DateTime:
+			case ColumnType.NewDate:
+			case ColumnType.Timestamp:
+				return isBinary ? new BinaryDateTimeColumnReader(connection) : new TextDateTimeColumnReader(connection);
+
+			case ColumnType.Time:
+				return isBinary ? BinaryTimeColumnReader.Instance : TextTimeColumnReader.Instance;
+
+			case ColumnType.Year:
+				return isBinary ? BinaryYearColumnReader.Instance : TextSignedInt32ColumnReader.Instance;
+
+			case ColumnType.Float:
+				return isBinary ? BinaryFloatColumnReader.Instance : TextFloatColumnReader.Instance;
+
+			case ColumnType.Double:
+				return isBinary ? BinaryDoubleColumnReader.Instance : TextDoubleColumnReader.Instance;
+
+			case ColumnType.Decimal:
+			case ColumnType.NewDecimal:
+				return DecimalColumnReader.Instance;
+
+			case ColumnType.Geometry:
+				return BytesColumnReader.Instance;
+
+			case ColumnType.Null:
+				return NullColumnReader.Instance;
+
+			case ColumnType.Vector:
+				return VectorColumnReader.Instance;
+
+			default:
+				throw new NotImplementedException($"Reading {columnDefinition.ColumnType} not implemented");
+		}
+	}
+
+	public abstract object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition);
+
+	public virtual int? TryReadInt32(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		default;
+}
diff --git a/src/MySqlConnector/ColumnReaders/DecimalColumnReader.cs b/src/MySqlConnector/ColumnReaders/DecimalColumnReader.cs
new file mode 100644
index 000000000..44c12d4f0
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/DecimalColumnReader.cs
@@ -0,0 +1,20 @@
+using System.Buffers.Text;
+using System.Runtime.CompilerServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class DecimalColumnReader : ColumnReader
+{
+	public static DecimalColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	public override int? TryReadInt32(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		(int) DoReadValue(data);
+
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	private static decimal DoReadValue(ReadOnlySpan<byte> data) =>
+		!Utf8Parser.TryParse(data, out decimal decimalValue, out int bytesConsumed) || bytesConsumed != data.Length ? throw new FormatException() : decimalValue;
+}
diff --git a/src/MySqlConnector/ColumnReaders/GuidBinary16ColumnReader.cs b/src/MySqlConnector/ColumnReaders/GuidBinary16ColumnReader.cs
new file mode 100644
index 000000000..1c7f31c43
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/GuidBinary16ColumnReader.cs
@@ -0,0 +1,25 @@
+using System.Runtime.CompilerServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class GuidBinary16ColumnReader : ColumnReader
+{
+	public static GuidBinary16ColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		ReadGuid(data);
+
+#if NET5_0_OR_GREATER
+	[SkipLocalsInit]
+#endif
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	public static Guid ReadGuid(ReadOnlySpan<byte> data) =>
+#if NET8_0_OR_GREATER
+		new(data, bigEndian: true);
+#elif NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+		new(stackalloc byte[16] { data[3], data[2], data[1], data[0], data[5], data[4], data[7], data[6], data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15] });
+#else
+		new([data[3], data[2], data[1], data[0], data[5], data[4], data[7], data[6], data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15]]);
+#endif
+}
diff --git a/src/MySqlConnector/ColumnReaders/GuidChar32ColumnReader.cs b/src/MySqlConnector/ColumnReaders/GuidChar32ColumnReader.cs
new file mode 100644
index 000000000..94a4cd73d
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/GuidChar32ColumnReader.cs
@@ -0,0 +1,15 @@
+using System.Buffers.Text;
+using System.Text;
+using MySqlConnector.Protocol.Payloads;
+using MySqlConnector.Utilities;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class GuidChar32ColumnReader : ColumnReader
+{
+	public static GuidChar32ColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		Utf8Parser.TryParse(data, out Guid guid, out int guid32BytesConsumed, 'N') && guid32BytesConsumed == 32 ?
+			guid : throw new FormatException($"Could not parse CHAR(32) value as Guid: {Encoding.UTF8.GetString(data)}");
+}
diff --git a/src/MySqlConnector/ColumnReaders/GuidChar36ColumnReader.cs b/src/MySqlConnector/ColumnReaders/GuidChar36ColumnReader.cs
new file mode 100644
index 000000000..2dbfa4f62
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/GuidChar36ColumnReader.cs
@@ -0,0 +1,15 @@
+using System.Buffers.Text;
+using System.Text;
+using MySqlConnector.Protocol.Payloads;
+using MySqlConnector.Utilities;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class GuidChar36ColumnReader : ColumnReader
+{
+	public static GuidChar36ColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		Utf8Parser.TryParse(data, out Guid guid, out int guid36BytesConsumed, 'D') && guid36BytesConsumed == 36 ?
+			guid : throw new FormatException($"Could not parse CHAR(36) value as Guid: {Encoding.UTF8.GetString(data)}");
+}
diff --git a/src/MySqlConnector/ColumnReaders/GuidLittleEndianBinary16ColumnReader.cs b/src/MySqlConnector/ColumnReaders/GuidLittleEndianBinary16ColumnReader.cs
new file mode 100644
index 000000000..99e9fd96f
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/GuidLittleEndianBinary16ColumnReader.cs
@@ -0,0 +1,22 @@
+using System.Runtime.CompilerServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class GuidLittleEndianBinary16ColumnReader : ColumnReader
+{
+	public static GuidLittleEndianBinary16ColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		ReadGuid(data);
+
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	public static Guid ReadGuid(ReadOnlySpan<byte> data) =>
+#if NET8_0_OR_GREATER
+		new(data, bigEndian: false);
+#elif NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+		new(data);
+#else
+		new(data.ToArray());
+#endif
+}
diff --git a/src/MySqlConnector/ColumnReaders/GuidTimeSwapBinary16ColumnReader.cs b/src/MySqlConnector/ColumnReaders/GuidTimeSwapBinary16ColumnReader.cs
new file mode 100644
index 000000000..b6822167d
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/GuidTimeSwapBinary16ColumnReader.cs
@@ -0,0 +1,23 @@
+using System.Runtime.CompilerServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class GuidTimeSwapBinary16ColumnReader : ColumnReader
+{
+	public static GuidTimeSwapBinary16ColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		ReadGuid(data);
+
+#if NET5_0_OR_GREATER
+	[SkipLocalsInit]
+#endif
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	public static Guid ReadGuid(ReadOnlySpan<byte> data) =>
+#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+		new(stackalloc byte[16] { data[7], data[6], data[5], data[4], data[3], data[2], data[1], data[0], data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15] });
+#else
+		new([data[7], data[6], data[5], data[4], data[3], data[2], data[1], data[0], data[8], data[9], data[10], data[11], data[12], data[13], data[14], data[15]]);
+#endif
+}
diff --git a/src/MySqlConnector/ColumnReaders/NullColumnReader.cs b/src/MySqlConnector/ColumnReaders/NullColumnReader.cs
new file mode 100644
index 000000000..49ef0d2da
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/NullColumnReader.cs
@@ -0,0 +1,11 @@
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class NullColumnReader : ColumnReader
+{
+	public static NullColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DBNull.Value;
+}
diff --git a/src/MySqlConnector/ColumnReaders/StringColumnReader.cs b/src/MySqlConnector/ColumnReaders/StringColumnReader.cs
new file mode 100644
index 000000000..bd25bb34e
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/StringColumnReader.cs
@@ -0,0 +1,13 @@
+using System.Text;
+using MySqlConnector.Protocol.Payloads;
+using MySqlConnector.Utilities;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class StringColumnReader : ColumnReader
+{
+	public static StringColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		Encoding.UTF8.GetString(data);
+}
diff --git a/src/MySqlConnector/ColumnReaders/TextBooleanColumnReader.cs b/src/MySqlConnector/ColumnReaders/TextBooleanColumnReader.cs
new file mode 100644
index 000000000..d05f3c518
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/TextBooleanColumnReader.cs
@@ -0,0 +1,19 @@
+using System.Runtime.CompilerServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class TextBooleanColumnReader : ColumnReader
+{
+	public static TextBooleanColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	public override int? TryReadInt32(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data) ? 1 : 0;
+
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	private static bool DoReadValue(ReadOnlySpan<byte> data) =>
+		data[0] != (byte) '0';
+}
diff --git a/src/MySqlConnector/ColumnReaders/TextDateTimeColumnReader.cs b/src/MySqlConnector/ColumnReaders/TextDateTimeColumnReader.cs
new file mode 100644
index 000000000..b2863e9db
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/TextDateTimeColumnReader.cs
@@ -0,0 +1,99 @@
+using System.Buffers.Text;
+using System.Text;
+using MySqlConnector.Protocol.Payloads;
+using MySqlConnector.Utilities;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class TextDateTimeColumnReader : ColumnReader
+{
+	public TextDateTimeColumnReader(MySqlConnection connection)
+	{
+		m_allowZeroDateTime = connection.AllowZeroDateTime;
+		m_convertZeroDateTime = connection.ConvertZeroDateTime;
+		m_dateTimeKind = connection.DateTimeKind;
+	}
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		ParseDateTime(data, m_convertZeroDateTime, m_allowZeroDateTime, m_dateTimeKind);
+
+	public static object ParseDateTime(ReadOnlySpan<byte> data, bool convertZeroDateTime, bool allowZeroDateTime, DateTimeKind dateTimeKind)
+	{
+		Exception? exception = null;
+		if (!Utf8Parser.TryParse(data, out int year, out var bytesConsumed) || bytesConsumed != 4)
+			goto InvalidDateTime;
+		if (data.Length < 5 || data[4] != 45)
+			goto InvalidDateTime;
+		if (!Utf8Parser.TryParse(data[5..], out int month, out bytesConsumed) || bytesConsumed != 2)
+			goto InvalidDateTime;
+		if (data.Length < 8 || data[7] != 45)
+			goto InvalidDateTime;
+		if (!Utf8Parser.TryParse(data[8..], out int day, out bytesConsumed) || bytesConsumed != 2)
+			goto InvalidDateTime;
+
+		if (year == 0 && month == 0 && day == 0)
+		{
+			if (convertZeroDateTime)
+				return DateTime.MinValue;
+			if (allowZeroDateTime)
+				return default(MySqlDateTime);
+			throw new InvalidCastException("Unable to convert MySQL date/time to System.DateTime, set AllowZeroDateTime=True or ConvertZeroDateTime=True in the connection string. See https://mysqlconnector.net/connection-options/");
+		}
+
+		int hour, minute, second, ticks;
+		if (data.Length == 10)
+		{
+			hour = 0;
+			minute = 0;
+			second = 0;
+			ticks = 0;
+		}
+		else
+		{
+			if (data[10] != 32)
+				goto InvalidDateTime;
+			if (!Utf8Parser.TryParse(data[11..], out hour, out bytesConsumed) || bytesConsumed != 2)
+				goto InvalidDateTime;
+			if (data.Length < 14 || data[13] != 58)
+				goto InvalidDateTime;
+			if (!Utf8Parser.TryParse(data[14..], out minute, out bytesConsumed) || bytesConsumed != 2)
+				goto InvalidDateTime;
+			if (data.Length < 17 || data[16] != 58)
+				goto InvalidDateTime;
+			if (!Utf8Parser.TryParse(data[17..], out second, out bytesConsumed) || bytesConsumed != 2)
+				goto InvalidDateTime;
+
+			if (data.Length == 19)
+			{
+				ticks = 0;
+			}
+			else
+			{
+				if (data[19] != 46)
+					goto InvalidDateTime;
+
+				if (!Utf8Parser.TryParse(data[20..], out ticks, out bytesConsumed) || bytesConsumed != data.Length - 20 || bytesConsumed > 7)
+					goto InvalidDateTime;
+				for (; bytesConsumed < 7; bytesConsumed++)
+					ticks *= 10;
+			}
+		}
+
+		try
+		{
+			return allowZeroDateTime ? (ticks % 10 == 0 ? (object) new MySqlDateTime(year, month, day, hour, minute, second, ticks / 10) : throw new NotSupportedException("MySqlDateTime does not support sub-microsecond precision")) :
+				new DateTime(year, month, day, hour, minute, second, dateTimeKind).AddTicks(ticks);
+		}
+		catch (Exception ex)
+		{
+			exception = ex;
+		}
+
+InvalidDateTime:
+		throw new FormatException($"Couldn't interpret value as a valid DateTime: {Encoding.UTF8.GetString(data)}", exception);
+	}
+
+	private readonly bool m_allowZeroDateTime;
+	private readonly bool m_convertZeroDateTime;
+	private readonly DateTimeKind m_dateTimeKind;
+}
diff --git a/src/MySqlConnector/ColumnReaders/TextDoubleColumnReader.cs b/src/MySqlConnector/ColumnReaders/TextDoubleColumnReader.cs
new file mode 100644
index 000000000..270d1464d
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/TextDoubleColumnReader.cs
@@ -0,0 +1,24 @@
+using System.Buffers.Text;
+using System.Text;
+using MySqlConnector.Protocol.Payloads;
+using MySqlConnector.Utilities;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class TextDoubleColumnReader : ColumnReader
+{
+	public static TextDoubleColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition)
+	{
+		if (Utf8Parser.TryParse(data, out double doubleValue, out var doubleBytesConsumed) && doubleBytesConsumed == data.Length)
+			return doubleValue;
+		if (data.SequenceEqual("-inf"u8))
+			return double.NegativeInfinity;
+		if (data.SequenceEqual("inf"u8))
+			return double.PositiveInfinity;
+		if (data.SequenceEqual("nan"u8))
+			return double.NaN;
+		throw new FormatException($"Couldn't parse value as double: {Encoding.UTF8.GetString(data)}");
+	}
+}
diff --git a/src/MySqlConnector/ColumnReaders/TextFloatColumnReader.cs b/src/MySqlConnector/ColumnReaders/TextFloatColumnReader.cs
new file mode 100644
index 000000000..f32594390
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/TextFloatColumnReader.cs
@@ -0,0 +1,24 @@
+using System.Buffers.Text;
+using System.Text;
+using MySqlConnector.Protocol.Payloads;
+using MySqlConnector.Utilities;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class TextFloatColumnReader : ColumnReader
+{
+	public static TextFloatColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition)
+	{
+		if (Utf8Parser.TryParse(data, out float floatValue, out var floatBytesConsumed) && floatBytesConsumed == data.Length)
+			return floatValue;
+		if (data.SequenceEqual("-inf"u8))
+			return float.NegativeInfinity;
+		if (data.SequenceEqual("inf"u8))
+			return float.PositiveInfinity;
+		if (data.SequenceEqual("nan"u8))
+			return float.NaN;
+		throw new FormatException($"Couldn't parse value as float: {Encoding.UTF8.GetString(data)}");
+	}
+}
diff --git a/src/MySqlConnector/ColumnReaders/TextSignedInt16ColumnReader.cs b/src/MySqlConnector/ColumnReaders/TextSignedInt16ColumnReader.cs
new file mode 100644
index 000000000..595e5dc6b
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/TextSignedInt16ColumnReader.cs
@@ -0,0 +1,20 @@
+using System.Buffers.Text;
+using System.Runtime.CompilerServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class TextSignedInt16ColumnReader : ColumnReader
+{
+	public static TextSignedInt16ColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	public override int? TryReadInt32(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	private static short DoReadValue(ReadOnlySpan<byte> data) =>
+		!Utf8Parser.TryParse(data, out short value, out var bytesConsumed) || bytesConsumed != data.Length ? throw new FormatException() : value;
+}
diff --git a/src/MySqlConnector/ColumnReaders/TextSignedInt32ColumnReader.cs b/src/MySqlConnector/ColumnReaders/TextSignedInt32ColumnReader.cs
new file mode 100644
index 000000000..7b307c89f
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/TextSignedInt32ColumnReader.cs
@@ -0,0 +1,20 @@
+using System.Buffers.Text;
+using System.Runtime.CompilerServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class TextSignedInt32ColumnReader : ColumnReader
+{
+	public static TextSignedInt32ColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	public override int? TryReadInt32(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	private static int DoReadValue(ReadOnlySpan<byte> data) =>
+		!Utf8Parser.TryParse(data, out int value, out var bytesConsumed) || bytesConsumed != data.Length ? throw new FormatException() : value;
+}
diff --git a/src/MySqlConnector/ColumnReaders/TextSignedInt64ColumnReader.cs b/src/MySqlConnector/ColumnReaders/TextSignedInt64ColumnReader.cs
new file mode 100644
index 000000000..3115e08d7
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/TextSignedInt64ColumnReader.cs
@@ -0,0 +1,20 @@
+using System.Buffers.Text;
+using System.Runtime.CompilerServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class TextSignedInt64ColumnReader : ColumnReader
+{
+	public static TextSignedInt64ColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	public override int? TryReadInt32(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		checked((int) DoReadValue(data));
+
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	private static long DoReadValue(ReadOnlySpan<byte> data) =>
+		!Utf8Parser.TryParse(data, out long value, out var bytesConsumed) || bytesConsumed != data.Length ? throw new FormatException() : value;
+}
diff --git a/src/MySqlConnector/ColumnReaders/TextSignedInt8ColumnReader.cs b/src/MySqlConnector/ColumnReaders/TextSignedInt8ColumnReader.cs
new file mode 100644
index 000000000..9fa1c87b7
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/TextSignedInt8ColumnReader.cs
@@ -0,0 +1,20 @@
+using System.Buffers.Text;
+using System.Runtime.CompilerServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class TextSignedInt8ColumnReader : ColumnReader
+{
+	public static TextSignedInt8ColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	public override int? TryReadInt32(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	private static sbyte DoReadValue(ReadOnlySpan<byte> data) =>
+		!Utf8Parser.TryParse(data, out sbyte value, out var bytesConsumed) || bytesConsumed != data.Length ? throw new FormatException() : value;
+}
diff --git a/src/MySqlConnector/ColumnReaders/TextTimeColumnReader.cs b/src/MySqlConnector/ColumnReaders/TextTimeColumnReader.cs
new file mode 100644
index 000000000..b4d5d5779
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/TextTimeColumnReader.cs
@@ -0,0 +1,12 @@
+using MySqlConnector.Protocol.Payloads;
+using MySqlConnector.Utilities;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class TextTimeColumnReader : ColumnReader
+{
+	public static TextTimeColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		Utility.ParseTimeSpan(data);
+}
diff --git a/src/MySqlConnector/ColumnReaders/TextUnsignedInt16ColumnReader.cs b/src/MySqlConnector/ColumnReaders/TextUnsignedInt16ColumnReader.cs
new file mode 100644
index 000000000..c0b19800b
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/TextUnsignedInt16ColumnReader.cs
@@ -0,0 +1,20 @@
+using System.Buffers.Text;
+using System.Runtime.CompilerServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class TextUnsignedInt16ColumnReader : ColumnReader
+{
+	public static TextUnsignedInt16ColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	public override int? TryReadInt32(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	private static ushort DoReadValue(ReadOnlySpan<byte> data) =>
+		!Utf8Parser.TryParse(data, out ushort value, out var bytesConsumed) || bytesConsumed != data.Length ? throw new FormatException() : value;
+}
diff --git a/src/MySqlConnector/ColumnReaders/TextUnsignedInt32ColumnReader.cs b/src/MySqlConnector/ColumnReaders/TextUnsignedInt32ColumnReader.cs
new file mode 100644
index 000000000..44fdbbfa3
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/TextUnsignedInt32ColumnReader.cs
@@ -0,0 +1,18 @@
+using System.Buffers.Text;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class TextUnsignedInt32ColumnReader : ColumnReader
+{
+	public static TextUnsignedInt32ColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	public override int? TryReadInt32(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		checked((int) DoReadValue(data));
+
+	private static uint DoReadValue(ReadOnlySpan<byte> data) =>
+		!Utf8Parser.TryParse(data, out uint value, out var bytesConsumed) || bytesConsumed != data.Length ? throw new FormatException() : value;
+}
diff --git a/src/MySqlConnector/ColumnReaders/TextUnsignedInt64ColumnReader.cs b/src/MySqlConnector/ColumnReaders/TextUnsignedInt64ColumnReader.cs
new file mode 100644
index 000000000..60d31f581
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/TextUnsignedInt64ColumnReader.cs
@@ -0,0 +1,20 @@
+using System.Buffers.Text;
+using System.Runtime.CompilerServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class TextUnsignedInt64ColumnReader : ColumnReader
+{
+	public static TextUnsignedInt64ColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	public override int? TryReadInt32(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		checked((int) DoReadValue(data));
+
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	private static ulong DoReadValue(ReadOnlySpan<byte> data) =>
+		!Utf8Parser.TryParse(data, out ulong value, out var bytesConsumed) || bytesConsumed != data.Length ? throw new FormatException() : value;
+}
diff --git a/src/MySqlConnector/ColumnReaders/TextUnsignedInt8ColumnReader.cs b/src/MySqlConnector/ColumnReaders/TextUnsignedInt8ColumnReader.cs
new file mode 100644
index 000000000..02e64ac65
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/TextUnsignedInt8ColumnReader.cs
@@ -0,0 +1,20 @@
+using System.Buffers.Text;
+using System.Runtime.CompilerServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class TextUnsignedInt8ColumnReader : ColumnReader
+{
+	public static TextUnsignedInt8ColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	public override int? TryReadInt32(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
+		DoReadValue(data);
+
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	private static byte DoReadValue(ReadOnlySpan<byte> data) =>
+		!Utf8Parser.TryParse(data, out byte value, out var bytesConsumed) || bytesConsumed != data.Length ? throw new FormatException() : value;
+}
diff --git a/src/MySqlConnector/ColumnReaders/VectorColumnReader.cs b/src/MySqlConnector/ColumnReaders/VectorColumnReader.cs
new file mode 100644
index 000000000..23f574e72
--- /dev/null
+++ b/src/MySqlConnector/ColumnReaders/VectorColumnReader.cs
@@ -0,0 +1,37 @@
+using System.Buffers.Binary;
+using System.Runtime.InteropServices;
+using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.ColumnReaders;
+
+internal sealed class VectorColumnReader : ColumnReader
+{
+	public static VectorColumnReader Instance { get; } = new();
+
+	public override object ReadValue(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition)
+	{
+		if (BitConverter.IsLittleEndian)
+		{
+			return new ReadOnlyMemory<float>(MemoryMarshal.Cast<byte, float>(data).ToArray());
+		}
+		else
+		{
+			var floats = new float[data.Length / 4];
+
+#if !NET5_0_OR_GREATER
+			var bytes = data.ToArray();
+#endif
+			for (var i = 0; i < floats.Length; i++)
+			{
+#if NET5_0_OR_GREATER
+				floats[i] = BinaryPrimitives.ReadSingleLittleEndian(data.Slice(i * 4));
+#else
+				Array.Reverse(bytes, i * 4, 4);
+				floats[i] = BitConverter.ToSingle(bytes, i * 4);
+#endif
+			}
+
+			return new ReadOnlyMemory<float>(floats);
+		}
+	}
+}
diff --git a/src/MySqlConnector/CompatibilitySuppressions.xml b/src/MySqlConnector/CompatibilitySuppressions.xml
index e84eb5224..fc096128b 100644
--- a/src/MySqlConnector/CompatibilitySuppressions.xml
+++ b/src/MySqlConnector/CompatibilitySuppressions.xml
@@ -1,22 +1,183 @@
 ﻿<?xml version="1.0" encoding="utf-8"?>
+<!-- https://learn.microsoft.com/dotnet/fundamentals/package-validation/diagnostic-ids -->
 <Suppressions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
   <Suppression>
-    <DiagnosticId>CP0008</DiagnosticId>
-    <Target>T:MySqlConnector.MySqlBatchCommandCollection</Target>
-    <Left>lib/netcoreapp3.1/MySqlConnector.dll</Left>
-    <Right>lib/net6.0/MySqlConnector.dll</Right>
+    <DiagnosticId>CP0001</DiagnosticId>
+    <Target>T:System.Data.Common.DbColumn</Target>
+    <Left>lib/net461/MySqlConnector.dll</Left>
+    <Right>lib/netstandard2.0/MySqlConnector.dll</Right>
+    <IsBaselineSuppression>true</IsBaselineSuppression>
   </Suppression>
   <Suppression>
     <DiagnosticId>CP0001</DiagnosticId>
     <Target>T:System.Data.Common.DbColumn</Target>
+    <Left>lib/net462/MySqlConnector.dll</Left>
+    <Right>lib/net471/MySqlConnector.dll</Right>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0001</DiagnosticId>
+    <Target>T:System.Data.Common.DbDataSource</Target>
+    <Left>lib/net6.0/MySqlConnector.dll</Left>
+    <Right>lib/net8.0/MySqlConnector.dll</Right>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0002</DiagnosticId>
+    <Target>M:MySqlConnector.MySqlBulkCopy.WriteToServerAsync(System.Collections.Generic.IEnumerable{System.Data.DataRow},System.Int32,System.Threading.CancellationToken)</Target>
+    <Left>lib/net461/MySqlConnector.dll</Left>
+    <Right>lib/netstandard2.0/MySqlConnector.dll</Right>
+    <IsBaselineSuppression>true</IsBaselineSuppression>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0002</DiagnosticId>
+    <Target>M:MySqlConnector.MySqlBulkCopy.WriteToServerAsync(System.Data.DataTable,System.Threading.CancellationToken)</Target>
+    <Left>lib/net461/MySqlConnector.dll</Left>
+    <Right>lib/netstandard2.0/MySqlConnector.dll</Right>
+    <IsBaselineSuppression>true</IsBaselineSuppression>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0002</DiagnosticId>
+    <Target>M:MySqlConnector.MySqlBulkCopy.WriteToServerAsync(System.Data.IDataReader,System.Threading.CancellationToken)</Target>
+    <Left>lib/net461/MySqlConnector.dll</Left>
+    <Right>lib/netstandard2.0/MySqlConnector.dll</Right>
+    <IsBaselineSuppression>true</IsBaselineSuppression>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0002</DiagnosticId>
+    <Target>M:MySqlConnector.MySqlConnection.get_ProvideClientCertificatesCallback</Target>
     <Left>lib/net461/MySqlConnector.dll</Left>
+    <Right>lib/netstandard2.0/MySqlConnector.dll</Right>
+    <IsBaselineSuppression>true</IsBaselineSuppression>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0002</DiagnosticId>
+    <Target>M:MySqlConnector.MySqlConnection.ResetConnectionAsync(System.Threading.CancellationToken)</Target>
+    <Left>lib/net461/MySqlConnector.dll</Left>
+    <Right>lib/netstandard2.0/MySqlConnector.dll</Right>
+    <IsBaselineSuppression>true</IsBaselineSuppression>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0002</DiagnosticId>
+    <Target>M:MySqlConnector.MySqlBulkCopy.WriteToServerAsync(System.Collections.Generic.IEnumerable{System.Data.DataRow},System.Int32,System.Threading.CancellationToken)</Target>
+    <Left>lib/net471/MySqlConnector.dll</Left>
+    <Right>lib/net471/MySqlConnector.dll</Right>
+    <IsBaselineSuppression>true</IsBaselineSuppression>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0002</DiagnosticId>
+    <Target>M:MySqlConnector.MySqlBulkCopy.WriteToServerAsync(System.Data.DataTable,System.Threading.CancellationToken)</Target>
+    <Left>lib/net471/MySqlConnector.dll</Left>
+    <Right>lib/net471/MySqlConnector.dll</Right>
+    <IsBaselineSuppression>true</IsBaselineSuppression>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0002</DiagnosticId>
+    <Target>M:MySqlConnector.MySqlBulkCopy.WriteToServerAsync(System.Data.IDataReader,System.Threading.CancellationToken)</Target>
+    <Left>lib/net471/MySqlConnector.dll</Left>
+    <Right>lib/net471/MySqlConnector.dll</Right>
+    <IsBaselineSuppression>true</IsBaselineSuppression>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0002</DiagnosticId>
+    <Target>M:MySqlConnector.MySqlConnection.get_ProvideClientCertificatesCallback</Target>
+    <Left>lib/net471/MySqlConnector.dll</Left>
+    <Right>lib/net471/MySqlConnector.dll</Right>
+    <IsBaselineSuppression>true</IsBaselineSuppression>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0002</DiagnosticId>
+    <Target>M:MySqlConnector.MySqlConnection.ResetConnectionAsync(System.Threading.CancellationToken)</Target>
+    <Left>lib/net471/MySqlConnector.dll</Left>
     <Right>lib/net471/MySqlConnector.dll</Right>
+    <IsBaselineSuppression>true</IsBaselineSuppression>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0002</DiagnosticId>
+    <Target>M:MySqlConnector.MySqlConnection.get_ProvideClientCertificatesCallback</Target>
+    <Left>lib/netstandard2.0/MySqlConnector.dll</Left>
+    <Right>lib/netstandard2.0/MySqlConnector.dll</Right>
+    <IsBaselineSuppression>true</IsBaselineSuppression>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0002</DiagnosticId>
+    <Target>M:MySqlConnector.MySqlConnection.ResetConnectionAsync(System.Threading.CancellationToken)</Target>
+    <Left>lib/netstandard2.0/MySqlConnector.dll</Left>
+    <Right>lib/netstandard2.0/MySqlConnector.dll</Right>
+    <IsBaselineSuppression>true</IsBaselineSuppression>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0002</DiagnosticId>
+    <Target>M:MySqlConnector.MySqlCommand.DisposeAsync</Target>
+    <Left>lib/netstandard2.0/MySqlConnector.dll</Left>
+    <Right>lib/netstandard2.1/MySqlConnector.dll</Right>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0002</DiagnosticId>
+    <Target>M:MySqlConnector.MySqlConnection.DisposeAsync</Target>
+    <Left>lib/netstandard2.0/MySqlConnector.dll</Left>
+    <Right>lib/netstandard2.1/MySqlConnector.dll</Right>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0002</DiagnosticId>
+    <Target>M:MySqlConnector.MySqlDataReader.DisposeAsync</Target>
+    <Left>lib/netstandard2.0/MySqlConnector.dll</Left>
+    <Right>lib/netstandard2.1/MySqlConnector.dll</Right>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0002</DiagnosticId>
+    <Target>M:MySqlConnector.MySqlTransaction.DisposeAsync</Target>
+    <Left>lib/netstandard2.0/MySqlConnector.dll</Left>
+    <Right>lib/netstandard2.1/MySqlConnector.dll</Right>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0002</DiagnosticId>
+    <Target>M:MySqlConnector.MySqlBatchCommandCollection.GetEnumerator</Target>
+    <Left>lib/netstandard2.1/MySqlConnector.dll</Left>
+    <Right>lib/net6.0/MySqlConnector.dll</Right>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0002</DiagnosticId>
+    <Target>M:MySqlConnector.MySqlConnectorFactory.CreateBatch</Target>
+    <Left>lib/netstandard2.1/MySqlConnector.dll</Left>
+    <Right>lib/net6.0/MySqlConnector.dll</Right>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0002</DiagnosticId>
+    <Target>M:MySqlConnector.MySqlConnectorFactory.CreateBatchCommand</Target>
+    <Left>lib/netstandard2.1/MySqlConnector.dll</Left>
+    <Right>lib/net6.0/MySqlConnector.dll</Right>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0008</DiagnosticId>
+    <Target>T:MySqlConnector.MySqlConversionException</Target>
+    <Left>lib/net461/MySqlConnector.dll</Left>
+    <Right>lib/netstandard2.0/MySqlConnector.dll</Right>
+    <IsBaselineSuppression>true</IsBaselineSuppression>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0008</DiagnosticId>
+    <Target>T:MySqlConnector.MySqlException</Target>
+    <Left>lib/net461/MySqlConnector.dll</Left>
+    <Right>lib/netstandard2.0/MySqlConnector.dll</Right>
+    <IsBaselineSuppression>true</IsBaselineSuppression>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0008</DiagnosticId>
+    <Target>T:MySqlConnector.MySqlProtocolException</Target>
+    <Left>lib/net461/MySqlConnector.dll</Left>
+    <Right>lib/netstandard2.0/MySqlConnector.dll</Right>
+    <IsBaselineSuppression>true</IsBaselineSuppression>
   </Suppression>
   <Suppression>
     <DiagnosticId>CP0008</DiagnosticId>
     <Target>T:MySqlConnector.MySqlDataReader</Target>
     <Left>lib/netstandard2.0/MySqlConnector.dll</Left>
-    <Right>lib/net461/MySqlConnector.dll</Right>
+    <Right>lib/net462/MySqlConnector.dll</Right>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0008</DiagnosticId>
+    <Target>T:MySqlConnector.MySqlBatchCommandCollection</Target>
+    <Left>lib/netstandard2.1/MySqlConnector.dll</Left>
+    <Right>lib/net6.0/MySqlConnector.dll</Right>
   </Suppression>
   <Suppression>
     <DiagnosticId>PKV006</DiagnosticId>
diff --git a/src/MySqlConnector/Core/BatchedCommandPayloadCreator.cs b/src/MySqlConnector/Core/BatchedCommandPayloadCreator.cs
deleted file mode 100644
index 3b28ebe35..000000000
--- a/src/MySqlConnector/Core/BatchedCommandPayloadCreator.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using System.Buffers.Binary;
-using MySqlConnector.Protocol;
-using MySqlConnector.Protocol.Serialization;
-
-namespace MySqlConnector.Core;
-
-internal sealed class BatchedCommandPayloadCreator : ICommandPayloadCreator
-{
-	public static ICommandPayloadCreator Instance { get; } = new BatchedCommandPayloadCreator();
-
-	public bool WriteQueryCommand(ref CommandListPosition commandListPosition, IDictionary<string, CachedProcedure?> cachedProcedures, ByteBufferWriter writer, bool appendSemicolon)
-	{
-		writer.Write((byte) CommandKind.Multi);
-		bool? firstResult = default;
-		bool wroteCommand;
-		ReadOnlySpan<byte> padding = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0 };
-		do
-		{
-			// save room for command length
-			var position = writer.Position;
-			writer.Write(padding);
-
-			wroteCommand = SingleCommandPayloadCreator.Instance.WriteQueryCommand(ref commandListPosition, cachedProcedures, writer, appendSemicolon);
-			firstResult ??= wroteCommand;
-
-			// write command length
-			var commandLength = writer.Position - position - padding.Length;
-			var span = writer.ArraySegment.AsSpan(position);
-			span[0] = 0xFE;
-			BinaryPrimitives.WriteUInt64LittleEndian(span[1..], (ulong) commandLength);
-		} while (wroteCommand);
-
-		// remove the padding that was saved for the final command (which wasn't written)
-		writer.TrimEnd(padding.Length);
-		return firstResult.Value;
-	}
-}
diff --git a/src/MySqlConnector/Core/BinaryRow.cs b/src/MySqlConnector/Core/BinaryRow.cs
deleted file mode 100644
index cf984a11b..000000000
--- a/src/MySqlConnector/Core/BinaryRow.cs
+++ /dev/null
@@ -1,233 +0,0 @@
-using System.Buffers.Text;
-using System.Runtime.InteropServices;
-using System.Text;
-using MySqlConnector.Protocol;
-using MySqlConnector.Protocol.Payloads;
-using MySqlConnector.Protocol.Serialization;
-using MySqlConnector.Utilities;
-
-namespace MySqlConnector.Core;
-
-internal sealed class BinaryRow : Row
-{
-	public BinaryRow(ResultSet resultSet)
-		: base(resultSet)
-	{
-	}
-
-	protected override Row CloneCore() => new BinaryRow(ResultSet);
-
-	protected override void GetDataOffsets(ReadOnlySpan<byte> data, int[] dataOffsets, int[] dataLengths)
-	{
-		Array.Clear(dataOffsets, 0, dataOffsets.Length);
-		for (var column = 0; column < dataOffsets.Length; column++)
-		{
-			if ((data[(column + 2) / 8 + 1] & (1 << ((column + 2) % 8))) != 0)
-			{
-				// column is NULL
-				dataOffsets[column] = -1;
-			}
-		}
-
-		var reader = new ByteArrayReader(data);
-
-		// skip packet header (1 byte) and NULL bitmap (formula for length at https://dev.mysql.com/doc/internals/en/null-bitmap.html)
-		reader.Offset += 1 + (dataOffsets.Length + 7 + 2) / 8;
-		for (var column = 0; column < dataOffsets.Length; column++)
-		{
-			if (dataOffsets[column] == -1)
-			{
-				dataLengths[column] = 0;
-			}
-			else
-			{
-				var columnDefinition = ResultSet.ColumnDefinitions![column];
-				var length = columnDefinition.ColumnType switch
-				{
-					ColumnType.Longlong or ColumnType.Double => 8,
-					ColumnType.Long or ColumnType.Int24 or ColumnType.Float => 4,
-					ColumnType.Short or ColumnType.Year => 2,
-					ColumnType.Tiny => 1,
-					ColumnType.Date or ColumnType.DateTime or ColumnType.NewDate or ColumnType.Timestamp or ColumnType.Time => reader.ReadByte(),
-					ColumnType.DateTime2 or ColumnType.Timestamp2 => throw new NotSupportedException($"ColumnType {columnDefinition.ColumnType} is not supported"),
-					_ => checked((int) reader.ReadLengthEncodedInteger()),
-				};
-
-				dataLengths[column] = length;
-				dataOffsets[column] = reader.Offset;
-			}
-
-			reader.Offset += dataLengths[column];
-		}
-	}
-
-	protected override int GetInt32Core(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition)
-	{
-		var isUnsigned = (columnDefinition.ColumnFlags & ColumnFlags.Unsigned) != 0;
-		return columnDefinition.ColumnType switch
-		{
-			ColumnType.Tiny => isUnsigned ? (int) data[0] : (sbyte) data[0],
-			ColumnType.Decimal or ColumnType.NewDecimal => Utf8Parser.TryParse(data, out decimal decimalValue, out int bytesConsumed) && bytesConsumed == data.Length ? checked((int) decimalValue) : throw new FormatException(),
-			ColumnType.Int24 or ColumnType.Long => isUnsigned ? checked((int) MemoryMarshal.Read<uint>(data)) : MemoryMarshal.Read<int>(data),
-			ColumnType.Longlong => isUnsigned ? checked((int) MemoryMarshal.Read<ulong>(data)) : checked((int) MemoryMarshal.Read<long>(data)),
-			ColumnType.Short => isUnsigned ? (int) MemoryMarshal.Read<ushort>(data) : MemoryMarshal.Read<short>(data),
-			ColumnType.Year => MemoryMarshal.Read<short>(data),
-			_ => throw new FormatException(),
-		};
-	}
-
-	protected override object GetValueCore(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition)
-	{
-		var isUnsigned = (columnDefinition.ColumnFlags & ColumnFlags.Unsigned) != 0;
-		switch (columnDefinition.ColumnType)
-		{
-		case ColumnType.Tiny:
-			if (Connection.TreatTinyAsBoolean && columnDefinition.ColumnLength == 1 && !isUnsigned)
-				return data[0] != 0;
-			return isUnsigned ? (object) data[0] : (sbyte) data[0];
-
-		case ColumnType.Int24:
-		case ColumnType.Long:
-			return isUnsigned ? (object) MemoryMarshal.Read<uint>(data) : MemoryMarshal.Read<int>(data);
-
-		case ColumnType.Longlong:
-			return isUnsigned ? (object) MemoryMarshal.Read<ulong>(data) : MemoryMarshal.Read<long>(data);
-
-		case ColumnType.Bit:
-			return ReadBit(data, columnDefinition);
-
-		case ColumnType.String:
-			if (Connection.GuidFormat == MySqlGuidFormat.Char36 && columnDefinition.ColumnLength / ProtocolUtility.GetBytesPerCharacter(columnDefinition.CharacterSet) == 36)
-				return Utf8Parser.TryParse(data, out Guid guid, out int guid36BytesConsumed, 'D') && guid36BytesConsumed == 36 ? guid : throw new FormatException($"Could not parse CHAR(36) value as Guid: {Encoding.UTF8.GetString(data)}");
-			if (Connection.GuidFormat == MySqlGuidFormat.Char32 && columnDefinition.ColumnLength / ProtocolUtility.GetBytesPerCharacter(columnDefinition.CharacterSet) == 32)
-				return Utf8Parser.TryParse(data, out Guid guid, out int guid32BytesConsumed, 'N') && guid32BytesConsumed == 32 ? guid : throw new FormatException($"Could not parse CHAR(32) value as Guid: {Encoding.UTF8.GetString(data)}");
-			goto case ColumnType.VarString;
-
-		case ColumnType.VarString:
-		case ColumnType.VarChar:
-		case ColumnType.TinyBlob:
-		case ColumnType.Blob:
-		case ColumnType.MediumBlob:
-		case ColumnType.LongBlob:
-		case ColumnType.Enum:
-		case ColumnType.Set:
-			if (columnDefinition.CharacterSet == CharacterSet.Binary)
-			{
-				var guidFormat = Connection.GuidFormat;
-				if ((guidFormat is MySqlGuidFormat.Binary16 or MySqlGuidFormat.TimeSwapBinary16 or MySqlGuidFormat.LittleEndianBinary16) && columnDefinition.ColumnLength == 16)
-					return CreateGuidFromBytes(guidFormat, data);
-
-				return data.ToArray();
-			}
-			return Encoding.UTF8.GetString(data);
-
-		case ColumnType.Json:
-			return Encoding.UTF8.GetString(data);
-
-		case ColumnType.Short:
-			return isUnsigned ? (object) MemoryMarshal.Read<ushort>(data) : MemoryMarshal.Read<short>(data);
-
-		case ColumnType.Date:
-		case ColumnType.DateTime:
-		case ColumnType.NewDate:
-		case ColumnType.Timestamp:
-			return ReadDateTime(data);
-
-		case ColumnType.Time:
-			return ReadTimeSpan(data);
-
-		case ColumnType.Year:
-			return (int) MemoryMarshal.Read<short>(data);
-
-		case ColumnType.Float:
-			return MemoryMarshal.Read<float>(data);
-
-		case ColumnType.Double:
-			return MemoryMarshal.Read<double>(data);
-
-		case ColumnType.Decimal:
-		case ColumnType.NewDecimal:
-			return Utf8Parser.TryParse(data, out decimal decimalValue, out int bytesConsumed) && bytesConsumed == data.Length ? decimalValue : throw new FormatException();
-
-		case ColumnType.Geometry:
-			return data.ToArray();
-
-		default:
-			throw new NotImplementedException($"Reading {columnDefinition.ColumnType} not implemented");
-		}
-	}
-
-	private object ReadDateTime(ReadOnlySpan<byte> value)
-	{
-		if (value.Length == 0)
-		{
-			if (Connection.ConvertZeroDateTime)
-				return DateTime.MinValue;
-			if (Connection.AllowZeroDateTime)
-				return default(MySqlDateTime);
-			throw new InvalidCastException("Unable to convert MySQL date/time to System.DateTime.");
-		}
-
-		int year = value[0] + value[1] * 256;
-		int month = value[2];
-		int day = value[3];
-
-		int hour, minute, second;
-		if (value.Length <= 4)
-		{
-			hour = 0;
-			minute = 0;
-			second = 0;
-		}
-		else
-		{
-			hour = value[4];
-			minute = value[5];
-			second = value[6];
-		}
-
-		var microseconds = value.Length <= 7 ? 0 : MemoryMarshal.Read<int>(value[7..]);
-
-		try
-		{
-			return Connection.AllowZeroDateTime ? (object) new MySqlDateTime(year, month, day, hour, minute, second, microseconds) :
-#if NET7_0_OR_GREATER
-				new DateTime(year, month, day, hour, minute, second, microseconds / 1000, microseconds % 1000, Connection.DateTimeKind);
-#else
-				new DateTime(year, month, day, hour, minute, second, microseconds / 1000, Connection.DateTimeKind).AddTicks(microseconds % 1000 * 10);
-#endif
-		}
-		catch (Exception ex)
-		{
-			throw new FormatException($"Couldn't interpret value as a valid DateTime: {Encoding.UTF8.GetString(value)}", ex);
-		}
-	}
-
-	private static TimeSpan ReadTimeSpan(ReadOnlySpan<byte> value)
-	{
-		if (value.Length == 0)
-			return TimeSpan.Zero;
-
-		var isNegative = value[0];
-		var days = MemoryMarshal.Read<int>(value[1..]);
-		var hours = (int) value[5];
-		var minutes = (int) value[6];
-		var seconds = (int) value[7];
-		var microseconds = value.Length == 8 ? 0 : MemoryMarshal.Read<int>(value[8..]);
-
-		if (isNegative != 0)
-		{
-			days = -days;
-			hours = -hours;
-			minutes = -minutes;
-			seconds = -seconds;
-			microseconds = -microseconds;
-		}
-
-#if NET7_0_OR_GREATER
-		return new TimeSpan(days, hours, minutes, seconds, microseconds / 1000, microseconds % 1000);
-#else
-		return new TimeSpan(days, hours, minutes, seconds) + TimeSpan.FromTicks(microseconds * 10);
-#endif
-	}
-}
diff --git a/src/MySqlConnector/Core/CachedParameter.cs b/src/MySqlConnector/Core/CachedParameter.cs
index 3edaed0c5..74d1c2911 100644
--- a/src/MySqlConnector/Core/CachedParameter.cs
+++ b/src/MySqlConnector/Core/CachedParameter.cs
@@ -2,7 +2,7 @@ namespace MySqlConnector.Core;
 
 internal sealed class CachedParameter
 {
-	public CachedParameter(int ordinalPosition, string? mode, string name, string dataType, bool unsigned, int length)
+	public CachedParameter(int ordinalPosition, string? mode, string name, string dataType, bool unsigned, int length, MySqlGuidFormat guidFormat)
 	{
 		Position = ordinalPosition;
 		if (Position == 0)
@@ -14,7 +14,7 @@ public CachedParameter(int ordinalPosition, string? mode, string name, string da
 		else if (string.Equals(mode, "out", StringComparison.OrdinalIgnoreCase))
 			Direction = ParameterDirection.Output;
 		Name = name;
-		MySqlDbType = TypeMapper.Instance.GetMySqlDbType(dataType, unsigned, length);
+		MySqlDbType = TypeMapper.Instance.GetMySqlDbType(dataType, unsigned, length, guidFormat);
 		Length = length;
 	}
 
diff --git a/src/MySqlConnector/Core/CachedProcedure.cs b/src/MySqlConnector/Core/CachedProcedure.cs
index 655e2754d..d018c88a9 100644
--- a/src/MySqlConnector/Core/CachedProcedure.cs
+++ b/src/MySqlConnector/Core/CachedProcedure.cs
@@ -1,6 +1,8 @@
+using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using System.Text;
 using System.Text.RegularExpressions;
+using Microsoft.Extensions.Logging;
 using MySqlConnector.Logging;
 using MySqlConnector.Protocol.Serialization;
 
@@ -8,12 +10,11 @@ namespace MySqlConnector.Core;
 
 internal sealed class CachedProcedure
 {
-	public static async Task<CachedProcedure?> FillAsync(IOBehavior ioBehavior, MySqlConnection connection, string schema, string component, CancellationToken cancellationToken)
+	public static async Task<CachedProcedure?> FillAsync(IOBehavior ioBehavior, MySqlConnection connection, string schema, string component, ILogger logger, CancellationToken cancellationToken)
 	{
 		// try to use mysql.proc first, as it is much faster
-		if (!connection.Session.ServerVersion.IsMariaDb
-		    && connection.Session.ServerVersion.Version < ServerVersions.RemovesMySqlProcTable
-		    && !connection.Session.ProcAccessDenied)
+		if (!connection.Session.ProcAccessDenied &&
+			(connection.Session.ServerVersion.IsMariaDb || connection.Session.ServerVersion.Version < ServerVersions.RemovesMySqlProcTable))
 		{
 			try
 			{
@@ -28,25 +29,28 @@ internal sealed class CachedProcedure
 				if (!exists)
 					return null;
 
-				var parametersSqlBytes = (byte[]) reader.GetValue(0);
-				var returnsSqlBytes = (byte[]) reader.GetValue(1);
-
 				// ASSUME this is UTF-8 encoded; it's possible that the `character_set_client` column would need to be used?
+				var parametersSqlBytes = (byte[]) reader.GetValue(0);
 				var parametersSql = Encoding.UTF8.GetString(parametersSqlBytes);
-				var returnsSql = Encoding.UTF8.GetString(returnsSqlBytes);
 
-				var parsedParameters = ParseParameters(parametersSql);
+				var returnsSql = reader.GetValue(1) switch
+				{
+					string s => s,
+					object o => Encoding.UTF8.GetString((byte[]) o),
+				};
+
+				var parsedParameters = ParseParameters(parametersSql, connection.GuidFormat);
 				if (returnsSql.Length != 0)
 				{
 					var returnDataType = ParseDataType(returnsSql, out var unsigned, out var length);
-					parsedParameters.Insert(0, CreateCachedParameter(0, null, "", returnDataType, unsigned, length, returnsSql));
+					parsedParameters.Insert(0, CreateCachedParameter(0, null, "", returnDataType, unsigned, length, connection.GuidFormat, returnsSql));
 				}
 
 				return new CachedProcedure(schema, component, parsedParameters);
 			}
 			catch (MySqlException ex)
 			{
-				Log.Info("Session{0} failed to retrieve metadata for Schema={1} Component={2}; falling back to INFORMATION_SCHEMA. Error: {3}", connection.Session.Id, schema, component, ex.Message);
+				Log.FailedToRetrieveProcedureMetadata(logger, ex, connection.Session.Id, schema, component, ex.Message);
 				if (ex.ErrorCode == MySqlErrorCode.TableAccessDenied)
 					connection.Session.ProcAccessDenied = true;
 			}
@@ -54,7 +58,7 @@ internal sealed class CachedProcedure
 
 		if (connection.Session.ServerVersion.Version < ServerVersions.SupportsProcedureCache)
 		{
-			Log.Info("Session{0} ServerVersion={1} does not support cached procedures", connection.Session.Id, connection.Session.ServerVersion.OriginalString);
+			Log.ServerDoesNotSupportCachedProcedures(logger, connection.Session.Id, connection.Session.ServerVersion.OriginalString);
 			return null;
 		}
 
@@ -74,9 +78,9 @@ FROM information_schema.parameters
 			cmd.Parameters.AddWithValue("@component", component);
 
 			using var reader = await cmd.ExecuteReaderNoResetTimeoutAsync(CommandBehavior.Default, ioBehavior, cancellationToken).ConfigureAwait(false);
-			await reader.ReadAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+			_ = await reader.ReadAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
 			routineCount = reader.GetInt32(0);
-			await reader.NextResultAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+			_ = await reader.NextResultAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
 
 			while (await reader.ReadAsync(ioBehavior, cancellationToken).ConfigureAwait(false))
 			{
@@ -87,13 +91,13 @@ FROM information_schema.parameters
 					!reader.IsDBNull(2) ? reader.GetString(2) : "",
 					dataType,
 					unsigned,
-					length
+					length,
+					connection.GuidFormat
 				));
 			}
 		}
 
-		if (Log.IsTraceEnabled())
-			Log.Trace("Procedure for Schema={0} Component={1} has RoutineCount={2}, ParameterCount={3}", schema, component, routineCount, parameters.Count);
+		Log.ProcedureHasRoutineCount(logger, schema, component, routineCount, parameters.Count);
 		return routineCount == 0 ? null : new CachedProcedure(schema, component, parameters);
 	}
 
@@ -129,6 +133,10 @@ internal MySqlParameterCollection AlignParamsWithDb(MySqlParameterCollection? pa
 			if (!alignParam.HasSetDbType)
 				alignParam.MySqlDbType = cachedParam.MySqlDbType;
 
+			// for a GUID column, pass along the length so the out parameter can be cast to the right size
+			if (alignParam.MySqlDbType == MySqlDbType.Guid && cachedParam.Direction is ParameterDirection.Output or ParameterDirection.InputOutput)
+				alignParam.Size = cachedParam.Length;
+
 			// cached parameters are ordered by ordinal position
 			alignedParams.Add(alignParam);
 		}
@@ -136,7 +144,7 @@ internal MySqlParameterCollection AlignParamsWithDb(MySqlParameterCollection? pa
 		return alignedParams;
 	}
 
-	internal static List<CachedParameter> ParseParameters(string parametersSql)
+	internal static List<CachedParameter> ParseParameters(string parametersSql, MySqlGuidFormat guidFormat)
 	{
 		// strip comments
 		parametersSql = s_cStyleComments.Replace(parametersSql, "");
@@ -146,13 +154,13 @@ internal static List<CachedParameter> ParseParameters(string parametersSql)
 		parametersSql = s_multipleSpaces.Replace(parametersSql, " ");
 
 		if (string.IsNullOrWhiteSpace(parametersSql))
-			return new List<CachedParameter>();
+			return [];
 
 		// strip precision specifier containing comma
-		parametersSql = s_numericTypes.Replace(parametersSql, @"$1");
+		parametersSql = s_numericTypes.Replace(parametersSql, "$1");
 
 		// strip enum values containing commas (these would have been stripped by ParseDataType anyway)
-		parametersSql = s_enum.Replace(parametersSql, "ENUM");
+		parametersSql = s_enumOrSet.Replace(parametersSql, "$1");
 
 		var parameters = parametersSql.Split(',');
 		var cachedParameters = new List<CachedParameter>(parameters.Length);
@@ -164,24 +172,24 @@ internal static List<CachedParameter> ParseParameters(string parametersSql)
 			if (parameter.StartsWith("INOUT ", StringComparison.OrdinalIgnoreCase))
 			{
 				direction = "INOUT";
-				parameter = parameter.Substring(6);
+				parameter = parameter[6..];
 			}
 			else if (parameter.StartsWith("OUT ", StringComparison.OrdinalIgnoreCase))
 			{
 				direction = "OUT";
-				parameter = parameter.Substring(4);
+				parameter = parameter[4..];
 			}
 			else if (parameter.StartsWith("IN ", StringComparison.OrdinalIgnoreCase))
 			{
 				direction = "IN";
-				parameter = parameter.Substring(3);
+				parameter = parameter[3..];
 			}
 
 			var parts = s_parameterName.Match(parameter);
 			var name = parts.Groups[1].Success ? parts.Groups[1].Value.Replace("``", "`") : parts.Groups[2].Value;
 
 			var dataType = ParseDataType(parts.Groups[3].Value, out var unsigned, out var length);
-			cachedParameters.Add(CreateCachedParameter(i + 1, direction, name, dataType, unsigned, length, originalString));
+			cachedParameters.Add(CreateCachedParameter(i + 1, direction, name, dataType, unsigned, length, guidFormat, originalString));
 		}
 
 		return cachedParameters;
@@ -191,7 +199,7 @@ internal static string ParseDataType(string sql, out bool unsigned, out int leng
 	{
 		sql = s_characterSet.Replace(sql, "");
 		sql = s_collate.Replace(sql, "");
-		sql = s_enum.Replace(sql, "ENUM");
+		sql = s_enumOrSet.Replace(sql, "$1");
 
 		length = 0;
 		var match = s_length.Match(sql);
@@ -201,8 +209,8 @@ internal static string ParseDataType(string sql, out bool unsigned, out int leng
 			sql = s_length.Replace(sql, "");
 		}
 
-		var list = sql.Trim().Split(new[] { ' ' });
-		var type = string.Empty;
+		var list = sql.Trim().Split(' ');
+		string? type;
 
 		if (list.Length < 2 || !s_typeMapping.TryGetValue(list[0] + ' ' + list[1], out type))
 		{
@@ -219,11 +227,11 @@ internal static string ParseDataType(string sql, out bool unsigned, out int leng
 		return type ?? list[0];
 	}
 
-	private static CachedParameter CreateCachedParameter(int ordinal, string? direction, string name, string dataType, bool unsigned, int length, string originalSql)
+	private static CachedParameter CreateCachedParameter(int ordinal, string? direction, string name, string dataType, bool unsigned, int length, MySqlGuidFormat guidFormat, string originalSql)
 	{
 		try
 		{
-			return new CachedParameter(ordinal, direction, name, dataType, unsigned, length);
+			return new CachedParameter(ordinal, direction, name, dataType, unsigned, length, guidFormat);
 		}
 		catch (NullReferenceException ex)
 		{
@@ -233,7 +241,7 @@ private static CachedParameter CreateCachedParameter(int ordinal, string? direct
 
 	private string FullyQualified => $"`{m_schema}`.`{m_component}`";
 
-	private static readonly IMySqlConnectorLogger Log = MySqlConnectorLogManager.CreateLogger(nameof(CachedProcedure));
+	[SuppressMessage("Performance", "CA1859:Use concrete types when possible for improved performance", Justification = "Avoid mutable static field")]
 	private static readonly IReadOnlyDictionary<string, string> s_typeMapping = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
 	{
 		{ "BOOL", "TINYINT" },
@@ -256,7 +264,7 @@ private static CachedParameter CreateCachedParameter(int ordinal, string? direct
 	private static readonly Regex s_singleLineComments = new(@"(^|\s)--.*?$", RegexOptions.Multiline);
 	private static readonly Regex s_multipleSpaces = new(@"\s+");
 	private static readonly Regex s_numericTypes = new(@"(DECIMAL|DEC|FIXED|NUMERIC|FLOAT|DOUBLE PRECISION|DOUBLE|REAL)\s*\([0-9]+(,\s*[0-9]+)\)", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
-	private static readonly Regex s_enum = new(@"ENUM\s*\([^)]+\)", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
+	private static readonly Regex s_enumOrSet = new(@"(ENUM|SET)\s*\([^)]+\)", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
 	private static readonly Regex s_parameterName = new(@"^(?:`((?:[\u0001-\u005F\u0061-\uFFFF]+|``)+)`|([A-Za-z0-9$_\u0080-\uFFFF]+)) (.*)$");
 	private static readonly Regex s_characterSet = new(" (CHARSET|CHARACTER SET) [A-Za-z0-9_]+", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
 	private static readonly Regex s_collate = new(" (COLLATE) [A-Za-z0-9_]+", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
diff --git a/src/MySqlConnector/Core/ColumnTypeMetadata.cs b/src/MySqlConnector/Core/ColumnTypeMetadata.cs
index 48e4a4b1b..aeed89694 100644
--- a/src/MySqlConnector/Core/ColumnTypeMetadata.cs
+++ b/src/MySqlConnector/Core/ColumnTypeMetadata.cs
@@ -1,31 +1,32 @@
+using System.Runtime.CompilerServices;
+
 namespace MySqlConnector.Core;
 
-internal sealed class ColumnTypeMetadata
+internal sealed class ColumnTypeMetadata(string dataTypeName, DbTypeMapping dbTypeMapping, MySqlDbType mySqlDbType, bool isUnsigned = false, bool binary = false, int length = 0, string? simpleDataTypeName = null, string? createFormat = null, long columnSize = 0, MySqlGuidFormat guidFormat = MySqlGuidFormat.Default)
 {
-	public static string CreateLookupKey(string columnTypeName, bool isUnsigned, int length) => $"{columnTypeName}|{(isUnsigned ? "u" : "s")}|{length}";
+	public static string CreateLookupKey(string columnTypeName, bool isUnsigned, int length, MySqlGuidFormat guidFormat) =>
+		$"{columnTypeName}|{(isUnsigned ? "u" : "s")}|{length}|{GetGuidFormatLookupKey(guidFormat)}";
 
-	public ColumnTypeMetadata(string dataTypeName, DbTypeMapping dbTypeMapping, MySqlDbType mySqlDbType, bool isUnsigned = false, bool binary = false, int length = 0, string? simpleDataTypeName = null, string? createFormat = null, long columnSize = 0)
-	{
-		DataTypeName = dataTypeName;
-		SimpleDataTypeName = simpleDataTypeName ?? dataTypeName;
-		CreateFormat = createFormat ?? (dataTypeName + (isUnsigned ? " UNSIGNED" : ""));
-		DbTypeMapping = dbTypeMapping;
-		MySqlDbType = mySqlDbType;
-		ColumnSize = columnSize;
-		IsUnsigned = isUnsigned;
-		Binary = binary;
-		Length = length;
-	}
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	private static string GetGuidFormatLookupKey(MySqlGuidFormat guidFormat) =>
+		guidFormat switch
+		{
+			MySqlGuidFormat.Char36 => "c36",
+			MySqlGuidFormat.Char32 => "c32",
+			MySqlGuidFormat.Binary16 or MySqlGuidFormat.TimeSwapBinary16 or MySqlGuidFormat.LittleEndianBinary16 => "b16",
+			_ => "def",
+		};
 
-	public string DataTypeName { get; }
-	public string SimpleDataTypeName { get; }
-	public string CreateFormat { get; }
-	public DbTypeMapping DbTypeMapping { get; }
-	public MySqlDbType MySqlDbType { get; }
-	public bool Binary { get; }
-	public long ColumnSize { get; }
-	public bool IsUnsigned { get; }
-	public int Length { get; }
+	public string DataTypeName { get; } = dataTypeName;
+	public string SimpleDataTypeName { get; } = simpleDataTypeName ?? dataTypeName;
+	public string CreateFormat { get; } = createFormat ?? (dataTypeName + (isUnsigned ? " UNSIGNED" : ""));
+	public DbTypeMapping DbTypeMapping { get; } = dbTypeMapping;
+	public MySqlDbType MySqlDbType { get; } = mySqlDbType;
+	public bool Binary { get; } = binary;
+	public long ColumnSize { get; } = columnSize;
+	public bool IsUnsigned { get; } = isUnsigned;
+	public int Length { get; } = length;
+	public MySqlGuidFormat GuidFormat { get; } = guidFormat;
 
-	public string CreateLookupKey() => CreateLookupKey(DataTypeName, IsUnsigned, Length);
+	public string CreateLookupKey() => CreateLookupKey(DataTypeName, IsUnsigned, Length, GuidFormat);
 }
diff --git a/src/MySqlConnector/Core/CommandExecutor.cs b/src/MySqlConnector/Core/CommandExecutor.cs
index 53cc0185e..732fec43c 100644
--- a/src/MySqlConnector/Core/CommandExecutor.cs
+++ b/src/MySqlConnector/Core/CommandExecutor.cs
@@ -8,26 +8,25 @@ namespace MySqlConnector.Core;
 
 internal static class CommandExecutor
 {
-	public static async Task<MySqlDataReader> ExecuteReaderAsync(IReadOnlyList<IMySqlCommand> commands, ICommandPayloadCreator payloadCreator, CommandBehavior behavior, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken)
+	public static async ValueTask<MySqlDataReader> ExecuteReaderAsync(CommandListPosition commandListPosition, ICommandPayloadCreator payloadCreator, CommandBehavior behavior, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
 		try
 		{
 			cancellationToken.ThrowIfCancellationRequested();
-			var commandListPosition = new CommandListPosition(commands);
-			var command = commands[0];
+			var command = commandListPosition.CommandAt(0);
 
 			// pre-requisite: Connection is non-null must be checked before calling this method
 			var connection = command.Connection!;
 
-			if (Log.IsTraceEnabled())
-				Log.Trace("Session{0} ExecuteReader {1} CommandCount: {2}", connection.Session.Id, ioBehavior, commands.Count);
+			Log.CommandExecutorExecuteReader(command.Logger, connection.Session.Id, ioBehavior, commandListPosition.CommandCount);
 
 			Dictionary<string, CachedProcedure?>? cachedProcedures = null;
-			foreach (var command2 in commands)
+			for (var commandIndex = 0; commandIndex < commandListPosition.CommandCount; commandIndex++)
 			{
+				var command2 = commandListPosition.CommandAt(commandIndex);
 				if (command2.CommandType == CommandType.StoredProcedure)
 				{
-					cachedProcedures ??= new();
+					cachedProcedures ??= [];
 					var commandText = command2.CommandText!;
 					if (!cachedProcedures.ContainsKey(commandText))
 					{
@@ -40,6 +39,8 @@ public static async Task<MySqlDataReader> ExecuteReaderAsync(IReadOnlyList<IMySq
 				}
 			}
 
+			await payloadCreator.SendCommandPrologueAsync(connection, commandListPosition, ioBehavior, cancellationToken).ConfigureAwait(false);
+
 			var writer = new ByteBufferWriter();
 			//// cachedProcedures will be non-null if there is a stored procedure, which is also the only time it will be read
 			if (!payloadCreator.WriteQueryCommand(ref commandListPosition, cachedProcedures!, writer, false))
@@ -48,16 +49,18 @@ public static async Task<MySqlDataReader> ExecuteReaderAsync(IReadOnlyList<IMySq
 			cancellationToken.ThrowIfCancellationRequested();
 
 			using var payload = writer.ToPayloadData();
-			connection.Session.StartQuerying(command.CancellableCommand);
+			var session = connection.Session;
+			session.StartQuerying(command.CancellableCommand);
 			command.SetLastInsertedId(0);
 			try
 			{
-				await connection.Session.SendAsync(payload, ioBehavior, CancellationToken.None).ConfigureAwait(false);
-				return await MySqlDataReader.CreateAsync(commandListPosition, payloadCreator, cachedProcedures, command, behavior, activity, ioBehavior, cancellationToken).ConfigureAwait(false);
+				await session.SendAsync(payload, ioBehavior, CancellationToken.None).ConfigureAwait(false);
+				await session.DataReader.InitAsync(commandListPosition, payloadCreator, cachedProcedures, command, behavior, activity, ioBehavior, cancellationToken).ConfigureAwait(false);
+				return session.DataReader;
 			}
 			catch (MySqlException ex) when (ex.ErrorCode == MySqlErrorCode.QueryInterrupted && cancellationToken.IsCancellationRequested)
 			{
-				Log.Info("Session{0} query was interrupted", connection.Session.Id);
+				Log.QueryWasInterrupted(command.Logger, session.Id);
 				throw new OperationCanceledException(ex.Message, ex, cancellationToken);
 			}
 			catch (Exception ex) when (payload.Span.Length > 4_194_304 && (ex is SocketException or IOException or MySqlProtocolException))
@@ -75,6 +78,4 @@ public static async Task<MySqlDataReader> ExecuteReaderAsync(IReadOnlyList<IMySq
 			throw;
 		}
 	}
-
-	private static readonly IMySqlConnectorLogger Log = MySqlConnectorLogManager.CreateLogger(nameof(CommandExecutor));
 }
diff --git a/src/MySqlConnector/Core/CommandListPosition.cs b/src/MySqlConnector/Core/CommandListPosition.cs
index 7e0188b24..a54cb3766 100644
--- a/src/MySqlConnector/Core/CommandListPosition.cs
+++ b/src/MySqlConnector/Core/CommandListPosition.cs
@@ -5,17 +5,42 @@ namespace MySqlConnector.Core;
 /// </summary>
 internal struct CommandListPosition
 {
-	public CommandListPosition(IReadOnlyList<IMySqlCommand> commands)
+	public CommandListPosition(object commands)
 	{
-		Commands = commands;
+		m_commands = commands;
+		CommandCount = commands switch
+		{
+			MySqlCommand _ => 1,
+			IReadOnlyList<MySqlBatchCommand> list => list.Count,
+			_ => 0,
+		};
+		PreparedStatements = null;
 		CommandIndex = 0;
 		PreparedStatementIndex = 0;
 	}
 
+	public readonly IMySqlCommand CommandAt(int index) =>
+		m_commands switch
+		{
+			MySqlCommand command when index is 0 => command,
+			IReadOnlyList<MySqlBatchCommand> list => list[index],
+			_ => throw new ArgumentOutOfRangeException(nameof(index)),
+		};
+
+	/// <summary>
+	/// The commands in this list; either a singular <see cref="MySqlCommand"/> or a <see cref="IReadOnlyList{MySqlBatchCommand}"/>.
+	/// </summary>
+	private readonly object m_commands;
+
+	/// <summary>
+	/// The number of commands in the list.
+	/// </summary>
+	public readonly int CommandCount;
+
 	/// <summary>
-	/// The commands in the list.
+	/// Associated prepared statements of commands
 	/// </summary>
-	public IReadOnlyList<IMySqlCommand> Commands { get; }
+	public PreparedStatements? PreparedStatements;
 
 	/// <summary>
 	/// The index of the current command.
@@ -26,4 +51,9 @@ public CommandListPosition(IReadOnlyList<IMySqlCommand> commands)
 	/// If the current command is a prepared statement, the index of the current prepared statement for that command.
 	/// </summary>
 	public int PreparedStatementIndex;
+
+	/// <summary>
+	/// Retrieve the last used prepared statement
+	/// </summary>
+	public PreparedStatement? LastUsedPreparedStatement;
 }
diff --git a/src/MySqlConnector/Core/CompressionMethod.cs b/src/MySqlConnector/Core/CompressionMethod.cs
new file mode 100644
index 000000000..bd16884dd
--- /dev/null
+++ b/src/MySqlConnector/Core/CompressionMethod.cs
@@ -0,0 +1,8 @@
+namespace MySqlConnector.Core;
+
+internal enum CompressionMethod
+{
+	None,
+	Zlib,
+	Zstandard,
+}
diff --git a/src/MySqlConnector/Core/ConcatenatedCommandPayloadCreator.cs b/src/MySqlConnector/Core/ConcatenatedCommandPayloadCreator.cs
index 498f78da8..c87f59080 100644
--- a/src/MySqlConnector/Core/ConcatenatedCommandPayloadCreator.cs
+++ b/src/MySqlConnector/Core/ConcatenatedCommandPayloadCreator.cs
@@ -8,16 +8,19 @@ internal sealed class ConcatenatedCommandPayloadCreator : ICommandPayloadCreator
 {
 	public static ICommandPayloadCreator Instance { get; } = new ConcatenatedCommandPayloadCreator();
 
+	public ValueTask SendCommandPrologueAsync(MySqlConnection connection, CommandListPosition commandListPosition, IOBehavior ioBehavior, CancellationToken cancellationToken) =>
+		default;
+
 	public bool WriteQueryCommand(ref CommandListPosition commandListPosition, IDictionary<string, CachedProcedure?> cachedProcedures, ByteBufferWriter writer, bool appendSemicolon)
 	{
-		if (commandListPosition.CommandIndex == commandListPosition.Commands.Count)
+		if (commandListPosition.CommandIndex == commandListPosition.CommandCount)
 			return false;
 
 		writer.Write((byte) CommandKind.Query);
 
 		// ConcatenatedCommandPayloadCreator is only used by MySqlBatch, and MySqlBatchCommand doesn't expose attributes,
 		// so just write an empty attribute set if the server needs it.
-		if (commandListPosition.Commands[commandListPosition.CommandIndex].Connection!.Session.SupportsQueryAttributes)
+		if (commandListPosition.CommandAt(commandListPosition.CommandIndex).Connection!.Session.SupportsQueryAttributes)
 		{
 			// attribute count
 			writer.WriteLengthEncodedInteger(0);
@@ -29,17 +32,16 @@ public bool WriteQueryCommand(ref CommandListPosition commandListPosition, IDict
 		bool isComplete;
 		do
 		{
-			var command = commandListPosition.Commands[commandListPosition.CommandIndex];
-			if (Log.IsTraceEnabled())
-				Log.Trace("Session{0} Preparing command payload; CommandText: {1}", command.Connection!.Session.Id, command.CommandText);
+			var command = commandListPosition.CommandAt(commandListPosition.CommandIndex);
+			Log.PreparingCommandPayload(command.Logger, command.Connection!.Session.Id, command.CommandText!);
 
-			isComplete = SingleCommandPayloadCreator.WriteQueryPayload(command, cachedProcedures, writer, commandListPosition.CommandIndex < commandListPosition.Commands.Count - 1 || appendSemicolon);
+			isComplete = SingleCommandPayloadCreator.WriteQueryPayload(command, cachedProcedures, writer,
+				commandListPosition.CommandIndex < commandListPosition.CommandCount - 1 || appendSemicolon,
+				commandListPosition.CommandIndex == 0,
+				commandListPosition.CommandIndex == commandListPosition.CommandCount - 1);
 			commandListPosition.CommandIndex++;
-		}
-		while (commandListPosition.CommandIndex < commandListPosition.Commands.Count && isComplete);
+		} while (commandListPosition.CommandIndex < commandListPosition.CommandCount && isComplete);
 
 		return true;
 	}
-
-	private static readonly IMySqlConnectorLogger Log = MySqlConnectorLogManager.CreateLogger(nameof(ConcatenatedCommandPayloadCreator));
 }
diff --git a/src/MySqlConnector/Core/ConnectionPool.cs b/src/MySqlConnector/Core/ConnectionPool.cs
index 1a3985e0c..a11391cd5 100644
--- a/src/MySqlConnector/Core/ConnectionPool.cs
+++ b/src/MySqlConnector/Core/ConnectionPool.cs
@@ -1,23 +1,28 @@
 using System.Collections.Concurrent;
 using System.Diagnostics;
-using System.Globalization;
 using System.Net;
-using System.Security.Authentication;
+using Microsoft.Extensions.Logging;
 using MySqlConnector.Logging;
 using MySqlConnector.Protocol.Serialization;
 using MySqlConnector.Utilities;
 
 namespace MySqlConnector.Core;
 
-internal sealed class ConnectionPool : IDisposable
+internal sealed class ConnectionPool : IConnectionPoolMetadata, IDisposable
 {
 	public int Id { get; }
 
-	public ConnectionSettings ConnectionSettings { get; }
+	ConnectionPool? IConnectionPoolMetadata.ConnectionPool => this;
+
+	int IConnectionPoolMetadata.Generation => m_generation;
+
+	int IConnectionPoolMetadata.GetNewSessionId() => Interlocked.Increment(ref m_lastSessionId); 
 
-	public SslProtocols SslProtocols { get; set; }
+	public string? Name { get; }
+
+	public ConnectionSettings ConnectionSettings { get; }
 
-	public async ValueTask<ServerSession> GetSessionAsync(MySqlConnection connection, int startTickCount, int timeoutMilliseconds, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken)
+	public async ValueTask<ServerSession> GetSessionAsync(MySqlConnection connection, long startingTimestamp, int timeoutMilliseconds, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
 		cancellationToken.ThrowIfCancellationRequested();
 
@@ -26,7 +31,7 @@ public async ValueTask<ServerSession> GetSessionAsync(MySqlConnection connection
 		// on the lock in RecoverLeakedSessions in high-concurrency situations
 		if (IsEmpty && unchecked(((uint) Environment.TickCount) - m_lastRecoveryTime) >= 1000u)
 		{
-			Log.Debug("Pool{0} is empty; scanning for any leaked sessions", m_logArguments);
+			Log.ScanningForLeakedSessions(m_logger, Id);
 			await RecoverLeakedSessionsAsync(ioBehavior).ConfigureAwait(false);
 		}
 
@@ -34,7 +39,7 @@ public async ValueTask<ServerSession> GetSessionAsync(MySqlConnection connection
 			await CreateMinimumPooledSessions(connection, ioBehavior, cancellationToken).ConfigureAwait(false);
 
 		// wait for an open slot (until the cancellationToken is cancelled, which is typically due to timeout)
-		Log.Trace("Pool{0} waiting for an available session", m_logArguments);
+		Log.WaitingForAvailableSession(m_logger, Id);
 		if (ioBehavior == IOBehavior.Asynchronous)
 			await m_sessionSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
 		else
@@ -48,18 +53,20 @@ public async ValueTask<ServerSession> GetSessionAsync(MySqlConnection connection
 			{
 				if (m_sessions.Count > 0)
 				{
+					// NOTE: MetricsReporter updated outside lock below
 					session = m_sessions.First!.Value;
 					m_sessions.RemoveFirst();
 				}
 			}
 			if (session is not null)
 			{
-				Log.Trace("Pool{0} found an existing session; checking it for validity", m_logArguments);
+				MetricsReporter.RemoveIdle(this);
+				Log.FoundExistingSession(m_logger, Id);
 				bool reuseSession;
 
 				if (session.PoolGeneration != m_generation)
 				{
-					Log.Trace("Pool{0} discarding session due to wrong generation", m_logArguments);
+					Log.DiscardingSessionDueToWrongGeneration(m_logger, Id);
 					reuseSession = false;
 				}
 				else
@@ -67,7 +74,7 @@ public async ValueTask<ServerSession> GetSessionAsync(MySqlConnection connection
 					if (ConnectionSettings.ConnectionReset || session.DatabaseOverride is not null)
 					{
 						if (timeoutMilliseconds != 0)
-							session.SetTimeout(Math.Max(1, timeoutMilliseconds - (Environment.TickCount - startTickCount)));
+							session.SetTimeout(Math.Max(1, timeoutMilliseconds - Utility.GetElapsedMilliseconds(startingTimestamp)));
 						reuseSession = await session.TryResetConnectionAsync(ConnectionSettings, connection, ioBehavior, cancellationToken).ConfigureAwait(false);
 						session.SetTimeout(Constants.InfiniteTimeout);
 					}
@@ -80,7 +87,7 @@ public async ValueTask<ServerSession> GetSessionAsync(MySqlConnection connection
 				if (!reuseSession)
 				{
 					// session is either old or cannot communicate with the server
-					Log.Info("Pool{0} Session{1} is unusable; destroying it", m_logArguments[0], session.Id);
+					Log.SessionIsUnusable(m_logger, Id, session.Id);
 					AdjustHostConnectionCount(session, -1);
 					await session.DisposeAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
 				}
@@ -94,15 +101,20 @@ public async ValueTask<ServerSession> GetSessionAsync(MySqlConnection connection
 						m_leasedSessions.Add(session.Id, session);
 						leasedSessionsCountPooled = m_leasedSessions.Count;
 					}
+
+					MetricsReporter.AddUsed(this);
 					ActivitySourceHelper.CopyTags(session.ActivityTags, activity);
-					if (Log.IsTraceEnabled())
-						Log.Trace("Pool{0} returning pooled Session{1} to caller; LeasedSessionsCount={2}", m_logArguments[0], session.Id, leasedSessionsCountPooled);
+					Log.ReturningPooledSession(m_logger, Id, session.Id, leasedSessionsCountPooled);
+
+					session.LastLeasedTimestamp = Stopwatch.GetTimestamp();
+					MetricsReporter.RecordWaitTime(this, Utility.GetElapsedSeconds(startingTimestamp, session.LastLeasedTimestamp));
 					return session;
 				}
 			}
 
 			// create a new session
-			session = await ConnectSessionAsync(connection, "Pool{0} no pooled session available; created new Session{1}", startTickCount, activity, ioBehavior, cancellationToken).ConfigureAwait(false);
+			session = await ServerSession.ConnectAndRedirectAsync(m_connectionLogger, m_logger, this, ConnectionSettings, m_loadBalancer,
+				connection, s_createdNewSession, startingTimestamp, activity, ioBehavior, cancellationToken).ConfigureAwait(false);
 			AdjustHostConnectionCount(session, 1);
 			session.OwningConnection = new(connection);
 			int leasedSessionsCountNew;
@@ -111,8 +123,11 @@ public async ValueTask<ServerSession> GetSessionAsync(MySqlConnection connection
 				m_leasedSessions.Add(session.Id, session);
 				leasedSessionsCountNew = m_leasedSessions.Count;
 			}
-			if (Log.IsTraceEnabled())
-				Log.Trace("Pool{0} returning new Session{1} to caller; LeasedSessionsCount={2}", m_logArguments[0], session.Id, leasedSessionsCountNew);
+			MetricsReporter.AddUsed(this);
+			Log.ReturningNewSession(m_logger, Id, session.Id, leasedSessionsCountNew);
+
+			session.LastLeasedTimestamp = Stopwatch.GetTimestamp();
+			MetricsReporter.RecordCreateTime(this, Utility.GetElapsedSeconds(startingTimestamp, session.LastLeasedTimestamp));
 			return session;
 		}
 		catch (Exception ex)
@@ -121,17 +136,17 @@ public async ValueTask<ServerSession> GetSessionAsync(MySqlConnection connection
 			{
 				try
 				{
-					Log.Debug(ex, "Pool{0} disposing created Session{1} due to exception: {2}", m_logArguments[0], session.Id, ex.Message);
+					Log.DisposingCreatedSessionDueToException(m_logger, ex, Id, session.Id, ex.Message);
 					AdjustHostConnectionCount(session, -1);
 					await session.DisposeAsync(ioBehavior, CancellationToken.None).ConfigureAwait(false);
 				}
 				catch (Exception unexpectedException)
 				{
-					Log.Warn(unexpectedException, "Pool{0} unexpected error in GetSessionAsync: {1}", m_logArguments[0], unexpectedException.Message);
+					Log.UnexpectedErrorInGetSessionAsync(m_logger, unexpectedException, Id, unexpectedException.Message);
 				}
 			}
 
-			m_sessionSemaphore.Release();
+			_ = m_sessionSemaphore.Release();
 			throw;
 		}
 	}
@@ -150,7 +165,7 @@ private int GetSessionHealth(ServerSession session)
 		if (session.PoolGeneration != m_generation)
 			return 2;
 		if (ConnectionSettings.ConnectionLifeTime > 0
-			&& unchecked((uint) Environment.TickCount) - session.CreatedTicks >= ConnectionSettings.ConnectionLifeTime)
+			&& Utility.GetElapsedMilliseconds(session.CreatedTimestamp) >= ConnectionSettings.ConnectionLifeTime)
 			return 3;
 
 		return 0;
@@ -158,41 +173,43 @@ private int GetSessionHealth(ServerSession session)
 
 	public async ValueTask ReturnAsync(IOBehavior ioBehavior, ServerSession session)
 	{
-		if (Log.IsTraceEnabled())
-			Log.Trace("Pool{0} receiving Session{1} back", m_logArguments[0], session.Id);
+		Log.ReceivingSessionBack(m_logger, Id, session.Id);
 
 		try
 		{
 			lock (m_leasedSessions)
-				m_leasedSessions.Remove(session.Id);
+				_ = m_leasedSessions.Remove(session.Id);
+			MetricsReporter.RemoveUsed(this);
 			session.OwningConnection = null;
+			session.DataReader = new();
 			var sessionHealth = GetSessionHealth(session);
 			if (sessionHealth == 0)
 			{
 				lock (m_sessions)
-					m_sessions.AddFirst(session);
+					_ = m_sessions.AddFirst(session);
+				MetricsReporter.AddIdle(this);
 			}
 			else
 			{
 				if (sessionHealth == 1)
-					Log.Info("Pool{0} received invalid Session{1}; destroying it", m_logArguments[0], session.Id);
+					Log.ReceivedInvalidSession(m_logger, Id, session.Id);
 				else
-					Log.Debug("Pool{0} received expired Session{1}; destroying it", m_logArguments[0], session.Id);
+					Log.ReceivedExpiredSession(m_logger, Id, session.Id);
 				AdjustHostConnectionCount(session, -1);
 				await session.DisposeAsync(ioBehavior, CancellationToken.None).ConfigureAwait(false);
 			}
 		}
 		finally
 		{
-			m_sessionSemaphore.Release();
+			_ = m_sessionSemaphore.Release();
 		}
 	}
 
 	public async Task ClearAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
 		// increment the generation of the connection pool
-		Log.Info("Pool{0} clearing connection pool", m_logArguments);
-		Interlocked.Increment(ref m_generation);
+		Log.ClearingConnectionPool(m_logger, Id);
+		_ = Interlocked.Increment(ref m_generation);
 		m_procedureCache = null;
 		await RecoverLeakedSessionsAsync(ioBehavior).ConfigureAwait(false);
 		await CleanPoolAsync(ioBehavior, session => session.PoolGeneration != m_generation, false, cancellationToken).ConfigureAwait(false);
@@ -200,9 +217,9 @@ public async Task ClearAsync(IOBehavior ioBehavior, CancellationToken cancellati
 
 	public async Task ReapAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
-		Log.Trace("Pool{0} reaping connection pool", m_logArguments);
+		Log.ReapingConnectionPool(m_logger, Id);
 		await RecoverLeakedSessionsAsync(ioBehavior).ConfigureAwait(false);
-		await CleanPoolAsync(ioBehavior, session => (unchecked((uint) Environment.TickCount) - session.LastReturnedTicks) / 1000 >= ConnectionSettings.ConnectionIdleTimeout, true, cancellationToken).ConfigureAwait(false);
+		await CleanPoolAsync(ioBehavior, session => Utility.GetElapsedMilliseconds(session.LastReturnedTimestamp) / 1000 >= ConnectionSettings.ConnectionIdleTimeout, true, cancellationToken).ConfigureAwait(false);
 	}
 
 	/// <summary>
@@ -224,7 +241,9 @@ public async Task ReapAsync(IOBehavior ioBehavior, CancellationToken cancellatio
 
 	public void Dispose()
 	{
-		Log.Debug("Pool{0} disposing connection pool", m_logArguments);
+		Log.DisposingConnectionPool(m_logger, Id);
+		lock (s_allPools)
+			s_allPools.Remove(this);
 #if NET6_0_OR_GREATER
 		m_dnsCheckTimer?.Dispose();
 		m_dnsCheckTimer = null;
@@ -236,12 +255,14 @@ public void Dispose()
 			using var dnsCheckWaitHandle = new ManualResetEvent(false);
 			m_dnsCheckTimer.Dispose(dnsCheckWaitHandle);
 			dnsCheckWaitHandle.WaitOne();
+			m_dnsCheckTimer = null;
 		}
 		if (m_reaperTimer is not null)
 		{
 			using var reaperWaitHandle = new ManualResetEvent(false);
 			m_reaperTimer.Dispose(reaperWaitHandle);
 			reaperWaitHandle.WaitOne();
+			m_reaperTimer = null;
 		}
 #endif
 	}
@@ -269,9 +290,9 @@ private async Task RecoverLeakedSessionsAsync(IOBehavior ioBehavior)
 			}
 		}
 		if (recoveredSessions.Count == 0)
-			Log.Trace("Pool{0} recovered no sessions", m_logArguments);
+			Log.RecoveredNoSessions(m_logger, Id);
 		else
-			Log.Warn("Pool{0}: RecoveredSessionCount={1}", m_logArguments[0], recoveredSessions.Count);
+			Log.RecoveredSessionCount(m_logger, Id, recoveredSessions.Count);
 
 		foreach (var (session, connection) in recoveredSessions)
 		{
@@ -327,17 +348,19 @@ private async Task CleanPoolAsync(IOBehavior ioBehavior, Func<ServerSession, boo
 					{
 						if (m_sessions.Count > 0)
 						{
+							// NOTE: MetricsReporter updated outside lock below
 							session = m_sessions.Last!.Value;
 							m_sessions.RemoveLast();
 						}
 					}
 					if (session is null)
 						return;
+					MetricsReporter.RemoveIdle(this);
 
 					if (shouldCleanFn(session))
 					{
 						// session should be cleaned; dispose it and keep iterating
-						Log.Debug("Pool{0} found Session{1} to clean up", m_logArguments[0], session.Id);
+						Log.FoundSessionToCleanUp(m_logger, Id, session.Id);
 						await session.DisposeAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
 					}
 					else
@@ -345,6 +368,7 @@ private async Task CleanPoolAsync(IOBehavior ioBehavior, Func<ServerSession, boo
 						// session should not be cleaned; put it back in the queue and stop iterating
 						lock (m_sessions)
 							m_sessions.AddLast(session);
+						MetricsReporter.AddIdle(this);
 						return;
 					}
 				}
@@ -386,112 +410,42 @@ private async Task CreateMinimumPooledSessions(MySqlConnection connection, IOBeh
 
 			try
 			{
-				var session = await ConnectSessionAsync(connection, "Pool{0} created Session{1} to reach minimum pool size", Environment.TickCount, null, ioBehavior, cancellationToken).ConfigureAwait(false);
+				var session = await ServerSession.ConnectAndRedirectAsync(m_connectionLogger, m_logger, this, ConnectionSettings, m_loadBalancer,
+					connection, s_createdToReachMinimumPoolSize, Stopwatch.GetTimestamp(), null, ioBehavior, cancellationToken).ConfigureAwait(false);
 				AdjustHostConnectionCount(session, 1);
 				lock (m_sessions)
-					m_sessions.AddFirst(session);
+					_ = m_sessions.AddFirst(session);
+				MetricsReporter.AddIdle(this);
 			}
 			finally
 			{
 				// connection is in pool; semaphore shouldn't be held any more
-				m_sessionSemaphore.Release();
-			}
-		}
-	}
-
-	private async ValueTask<ServerSession> ConnectSessionAsync(MySqlConnection connection, string logMessage, int startTickCount, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken)
-	{
-		var session = new ServerSession(this, m_generation, Interlocked.Increment(ref m_lastSessionId));
-		if (Log.IsDebugEnabled())
-			Log.Debug(logMessage, m_logArguments[0], session.Id);
-		string? statusInfo;
-		try
-		{
-			statusInfo = await session.ConnectAsync(ConnectionSettings, connection, startTickCount, m_loadBalancer, activity, ioBehavior, cancellationToken).ConfigureAwait(false);
-		}
-		catch (Exception)
-		{
-			await session.DisposeAsync(ioBehavior, default).ConfigureAwait(false);
-			throw;
-		}
-
-		Exception? redirectionException = null;
-		if (statusInfo is not null && statusInfo.StartsWith("Location: mysql://", StringComparison.Ordinal))
-		{
-			// server redirection string has the format "Location: mysql://{host}:{port}/user={userId}[&ttl={ttl}]"
-			Log.Trace("Session{0} has server redirection header {1}", session.Id, statusInfo);
-
-			if (ConnectionSettings.ServerRedirectionMode == MySqlServerRedirectionMode.Disabled)
-			{
-				Log.Trace("Pool{0} server redirection is disabled; ignoring redirection", m_logArguments);
-			}
-			else if (Utility.TryParseRedirectionHeader(statusInfo, out var host, out var port, out var user))
-			{
-				if (host != ConnectionSettings.HostNames![0] || port != ConnectionSettings.Port || user != ConnectionSettings.UserID)
-				{
-					var redirectedSettings = ConnectionSettings.CloneWith(host, port, user);
-					Log.Debug("Pool{0} opening new connection to Host={1}; Port={2}; User={3}", m_logArguments[0], host, port, user);
-					var redirectedSession = new ServerSession(this, m_generation, Interlocked.Increment(ref m_lastSessionId));
-					try
-					{
-						await redirectedSession.ConnectAsync(redirectedSettings, connection, startTickCount, m_loadBalancer, activity, ioBehavior, cancellationToken).ConfigureAwait(false);
-					}
-					catch (Exception ex)
-					{
-						Log.Info(ex, "Pool{0} failed to connect redirected Session{1}", m_logArguments[0], redirectedSession.Id);
-						redirectionException = ex;
-					}
-
-					if (redirectionException is null)
-					{
-						Log.Trace("Pool{0} closing Session{1} to use redirected Session{2} instead", m_logArguments[0], session.Id, redirectedSession.Id);
-						await session.DisposeAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
-						return redirectedSession;
-					}
-					else
-					{
-						try
-						{
-							await redirectedSession.DisposeAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
-						}
-						catch (Exception)
-						{
-						}
-					}
-				}
-				else
-				{
-					Log.Trace("Session{0} is already connected to this server; ignoring redirection", session.Id);
-				}
+				_ = m_sessionSemaphore.Release();
 			}
 		}
-
-		if (ConnectionSettings.ServerRedirectionMode == MySqlServerRedirectionMode.Required)
-		{
-			Log.Error("Pool{0} requires server redirection but server doesn't support it", m_logArguments);
-			throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Server does not support redirection", redirectionException);
-		}
-		return session;
 	}
 
-	public static ConnectionPool? CreatePool(string connectionString)
+	public static ConnectionPool? CreatePool(string connectionString, MySqlConnectorLoggingConfiguration loggingConfiguration, string? name)
 	{
 		// parse connection string and check for 'Pooling' setting; return 'null' if pooling is disabled
 		var connectionStringBuilder = new MySqlConnectionStringBuilder(connectionString);
 		if (!connectionStringBuilder.Pooling)
-		{
 			return null;
-		}
+
+		if (name is not null)
+			connectionStringBuilder.ApplicationName = name;
 
 		// force a new pool to be created, ignoring the cache
 		var connectionSettings = new ConnectionSettings(connectionStringBuilder);
-		var pool = new ConnectionPool(connectionSettings);
+		var pool = new ConnectionPool(loggingConfiguration, connectionSettings);
 		pool.StartReaperTask();
 		pool.StartDnsCheckTimer();
 		return pool;
 	}
 
-	public static ConnectionPool? GetPool(string connectionString)
+	// Gets an existing (unnamed) ConnectionPool, creating it if it's missing. If 'createIfNotFound' is false, then 'loggingConfiguration'
+	// may be set to null; otherwise, it must be provided.
+	public static ConnectionPool? GetPool(string connectionString, MySqlConnectorLoggingConfiguration? loggingConfiguration, bool createIfNotFound = true)
 	{
 		// check single-entry MRU cache for this exact connection string; most applications have just one
 		// connection string and will get a cache hit here
@@ -526,9 +480,12 @@ private async ValueTask<ServerSession> ConnectSessionAsync(MySqlConnection conne
 			return pool;
 		}
 
+		if (!createIfNotFound)
+			return null;
+
 		// create a new pool and attempt to insert it; if someone else beats us to it, just use their value
 		var connectionSettings = new ConnectionSettings(connectionStringBuilder);
-		var newPool = new ConnectionPool(connectionSettings);
+		var newPool = new ConnectionPool(loggingConfiguration!, connectionSettings);
 		pool = s_pools.GetOrAdd(normalizedConnectionString, newPool);
 
 		if (pool == newPool)
@@ -541,9 +498,10 @@ private async ValueTask<ServerSession> ConnectSessionAsync(MySqlConnection conne
 			if (connectionString != normalizedConnectionString)
 				s_pools.GetOrAdd(connectionString, pool);
 		}
-		else if (pool != newPool && Log.IsDebugEnabled())
+		else if (pool != newPool)
 		{
-			Log.Debug("Pool{0} was created but will not be used (due to race)", newPool.m_logArguments);
+			Log.CreatedPoolWillNotBeUsed(newPool.m_logger, newPool.Id);
+			newPool.Dispose();
 		}
 
 		return pool;
@@ -551,34 +509,36 @@ private async ValueTask<ServerSession> ConnectSessionAsync(MySqlConnection conne
 
 	public static async Task ClearPoolsAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
-		foreach (var pool in GetAllPools())
+		foreach (var pool in GetCachedPools())
 			await pool.ClearAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
-	}
 
-	private static IReadOnlyList<ConnectionPool> GetAllPools()
-	{
-		var pools = new List<ConnectionPool>(s_pools.Count);
-		var uniquePools = new HashSet<ConnectionPool>();
-		foreach (var pool in s_pools.Values)
+		static List<ConnectionPool> GetCachedPools()
 		{
-			if (pool is not null && uniquePools.Add(pool))
-				pools.Add(pool);
+			var pools = new List<ConnectionPool>(s_pools.Count);
+			var uniquePools = new HashSet<ConnectionPool>();
+			foreach (var pool in s_pools.Values)
+			{
+				if (pool is not null && uniquePools.Add(pool))
+					pools.Add(pool);
+			}
+			return pools;
 		}
-		return pools;
 	}
 
-	private ConnectionPool(ConnectionSettings cs)
+	private ConnectionPool(MySqlConnectorLoggingConfiguration loggingConfiguration, ConnectionSettings cs)
 	{
+		m_logger = loggingConfiguration.PoolLogger;
+		m_connectionLogger = loggingConfiguration.ConnectionLogger;
 		ConnectionSettings = cs;
-		SslProtocols = cs.TlsVersions;
+		Name = cs.ApplicationName;
 		m_generation = 0;
 		m_cleanSemaphore = new(1);
 		m_sessionSemaphore = new(cs.MaximumPoolSize);
 		m_sessions = new();
-		m_leasedSessions = new();
+		m_leasedSessions = [];
 		if (cs.ConnectionProtocol == MySqlConnectionProtocol.Sockets && cs.LoadBalance == MySqlLoadBalance.LeastConnections)
 		{
-			m_hostSessions = new();
+			m_hostSessions = [];
 			foreach (var hostName in cs.HostNames!)
 				m_hostSessions[hostName] = 0;
 		}
@@ -587,12 +547,21 @@ private ConnectionPool(ConnectionSettings cs)
 			cs.HostNames!.Count == 1 || cs.LoadBalance == MySqlLoadBalance.FailOver ? FailOverLoadBalancer.Instance :
 			cs.LoadBalance == MySqlLoadBalance.Random ? RandomLoadBalancer.Instance :
 			cs.LoadBalance == MySqlLoadBalance.LeastConnections ? new LeastConnectionsLoadBalancer(m_hostSessions!) :
-			(ILoadBalancer) new RoundRobinLoadBalancer();
+			 new RoundRobinLoadBalancer();
+
+		// create tag lists for reporting pool metrics
+		var connectionString = cs.ConnectionStringBuilder.GetConnectionString(includePassword: false);
+		m_stateTagList =
+		[
+			new("state", "idle"),
+			new("pool.name", Name ?? connectionString),
+			new("state", "used"),
+		];
 
 		Id = Interlocked.Increment(ref s_poolId);
-		m_logArguments = new object[] { Id.ToString(CultureInfo.InvariantCulture) };
-		if (Log.IsInfoEnabled())
-			Log.Info("Pool{0} creating new connection pool for ConnectionString: {1}", m_logArguments[0], cs.ConnectionStringBuilder.GetConnectionString(includePassword: false));
+		lock (s_allPools)
+			s_allPools.Add(this);
+		Log.CreatingNewConnectionPool(m_logger, Id, connectionString);
 	}
 
 	private void StartReaperTask()
@@ -659,7 +628,7 @@ async Task RunTimer()
 		{
 			while (await m_dnsCheckTimer.WaitForNextTickAsync().ConfigureAwait(false))
 			{
-				Log.Trace("Pool{0} checking for DNS changes", m_logArguments);
+				Log.CheckingForDnsChanges(m_logger, Id);
 				var hostNamesChanged = false;
 				for (var hostNameIndex = 0; hostNameIndex < hostNames.Count; hostNameIndex++)
 				{
@@ -672,7 +641,7 @@ async Task RunTimer()
 						}
 						else if (hostAddresses[hostNameIndex].Except(ipAddresses).Any())
 						{
-							Log.Debug("Pool{0} detected DNS change for HostName '{1}': {2} to {3}", m_logArguments[0], hostNames[hostNameIndex], string.Join<IPAddress>(',', hostAddresses[hostNameIndex]), string.Join<IPAddress>(',', ipAddresses));
+							Log.DetectedDnsChange(m_logger, Id, hostNames[hostNameIndex], string.Join<IPAddress>(',', hostAddresses[hostNameIndex]), string.Join<IPAddress>(',', ipAddresses));
 							hostAddresses[hostNameIndex] = ipAddresses;
 							hostNamesChanged = true;
 						}
@@ -680,12 +649,12 @@ async Task RunTimer()
 					catch (Exception ex)
 					{
 						// do nothing; we'll try again later
-						Log.Debug("Pool{0} DNS check failed; ignoring HostName '{1}': {2}", m_logArguments[0], hostNames[hostNameIndex], ex.Message);
+						Log.DnsCheckFailed(m_logger, ex, Id, hostNames[hostNameIndex], ex.Message);
 					}
 				}
 				if (hostNamesChanged)
 				{
-					Log.Info("Pool{0} clearing pool due to DNS changes", m_logArguments);
+					Log.ClearingPoolDueToDnsChanges(m_logger, Id);
 					await ClearAsync(IOBehavior.Asynchronous, CancellationToken.None).ConfigureAwait(false);
 				}
 			}
@@ -694,7 +663,7 @@ async Task RunTimer()
 		var interval = Math.Min(int.MaxValue / 1000, ConnectionSettings.DnsCheckInterval) * 1000;
 		m_dnsCheckTimer = new Timer(t =>
 		{
-			Log.Trace("Pool{0} checking for DNS changes", m_logArguments);
+			Log.CheckingForDnsChanges(m_logger, Id);
 			var hostNamesChanged = false;
 			for (var hostNameIndex = 0; hostNameIndex < hostNames.Count; hostNameIndex++)
 			{
@@ -707,7 +676,7 @@ async Task RunTimer()
 					}
 					else if (hostAddresses[hostNameIndex].Except(ipAddresses).Any())
 					{
-						Log.Debug("Pool{0} detected DNS change for HostName '{1}': {2} to {3}", m_logArguments[0], hostNames[hostNameIndex], string.Join<IPAddress>(",", hostAddresses[hostNameIndex]), string.Join<IPAddress>(",", ipAddresses));
+						Log.DetectedDnsChange(m_logger, Id, hostNames[hostNameIndex], string.Join<IPAddress>(",", hostAddresses[hostNameIndex]), string.Join<IPAddress>(",", ipAddresses));
 						hostAddresses[hostNameIndex] = ipAddresses;
 						hostNamesChanged = true;
 					}
@@ -715,12 +684,12 @@ async Task RunTimer()
 				catch (Exception ex)
 				{
 					// do nothing; we'll try again later
-					Log.Debug("Pool{0} DNS check failed; ignoring HostName '{1}': {2}", m_logArguments[0], hostNames[hostNameIndex], ex.Message);
+					Log.DnsCheckFailed(m_logger, ex, Id, hostNames[hostNameIndex], ex.Message);
 				}
 			}
 			if (hostNamesChanged)
 			{
-				Log.Info("Pool{0} clearing pool due to DNS changes", m_logArguments);
+				Log.ClearingPoolDueToDnsChanges(m_logger, Id);
 				ClearAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
 			}
 			((Timer) t!).Change(interval, -1);
@@ -738,29 +707,32 @@ private void AdjustHostConnectionCount(ServerSession session, int delta)
 		}
 	}
 
-	private sealed class LeastConnectionsLoadBalancer : ILoadBalancer
-	{
-		public LeastConnectionsLoadBalancer(Dictionary<string, int> hostSessions) => m_hostSessions = hostSessions;
+	// Provides a slice of m_stateTagList that contains either the 'idle' or 'used' state tag along with the pool name.
+	public ReadOnlySpan<KeyValuePair<string, object?>> IdleStateTagList => m_stateTagList.AsSpan(0, 2);
+	public ReadOnlySpan<KeyValuePair<string, object?>> UsedStateTagList => m_stateTagList.AsSpan(1, 2);
 
-		public IReadOnlyList<string> LoadBalance(IReadOnlyList<string> hosts)
-		{
-			lock (m_hostSessions)
-				return m_hostSessions.OrderBy(static x => x.Value).Select(static x => x.Key).ToList();
-		}
+	// A slice of m_stateTagList that contains only the pool name tag.
+	public ReadOnlySpan<KeyValuePair<string, object?>> PoolNameTagList => m_stateTagList.AsSpan(1, 1);
 
-		private readonly Dictionary<string, int> m_hostSessions;
+	public static List<ConnectionPool> GetAllPools()
+	{
+		lock (s_allPools)
+			return new(s_allPools);
 	}
 
-	private sealed class ConnectionStringPool
+	private sealed class LeastConnectionsLoadBalancer(Dictionary<string, int> hostSessions) : ILoadBalancer
 	{
-		public ConnectionStringPool(string connectionString, ConnectionPool? pool)
+		public IReadOnlyList<string> LoadBalance(IReadOnlyList<string> hosts)
 		{
-			ConnectionString = connectionString;
-			Pool = pool;
+			lock (hostSessions)
+				return hostSessions.OrderBy(static x => x.Value).Select(static x => x.Key).ToList();
 		}
+	}
 
-		public string ConnectionString { get; }
-		public ConnectionPool? Pool { get; }
+	private sealed class ConnectionStringPool(string connectionString, ConnectionPool? pool)
+	{
+		public string ConnectionString { get; } = connectionString;
+		public ConnectionPool? Pool { get; } = pool;
 	}
 
 	static ConnectionPool()
@@ -772,19 +744,26 @@ static ConnectionPool()
 	private static void OnAppDomainShutDown(object? sender, EventArgs e) =>
 		ClearPoolsAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
 
-	private static readonly IMySqlConnectorLogger Log = MySqlConnectorLogManager.CreateLogger(nameof(ConnectionPool));
 	private static readonly ConcurrentDictionary<string, ConnectionPool?> s_pools = new();
-
+	private static readonly List<ConnectionPool> s_allPools = [];
+	private static readonly Action<ILogger, int, string, Exception?> s_createdNewSession = LoggerMessage.Define<int, string>(
+		LogLevel.Debug, new EventId(EventIds.PoolCreatedNewSession, nameof(EventIds.PoolCreatedNewSession)),
+		"Pool {PoolId} has no pooled session available; created new session {SessionId}");
+	private static readonly Action<ILogger, int, string, Exception?> s_createdToReachMinimumPoolSize = LoggerMessage.Define<int, string>(
+		LogLevel.Debug, new EventId(EventIds.CreatedSessionToReachMinimumPoolCount, nameof(EventIds.CreatedSessionToReachMinimumPoolCount)),
+		"Pool {PoolId} created session {SessionId} to reach minimum pool size");
 	private static int s_poolId;
 	private static ConnectionStringPool? s_mruCache;
 
+	private readonly ILogger m_logger;
+	private readonly ILogger m_connectionLogger;
+	private readonly KeyValuePair<string, object?>[] m_stateTagList;
 	private readonly SemaphoreSlim m_cleanSemaphore;
 	private readonly SemaphoreSlim m_sessionSemaphore;
 	private readonly LinkedList<ServerSession> m_sessions;
 	private readonly Dictionary<string, ServerSession> m_leasedSessions;
 	private readonly ILoadBalancer? m_loadBalancer;
 	private readonly Dictionary<string, int>? m_hostSessions;
-	private readonly object[] m_logArguments;
 	private int m_generation;
 	private uint m_lastRecoveryTime;
 	private int m_lastSessionId;
diff --git a/src/MySqlConnector/Core/ConnectionSettings.cs b/src/MySqlConnector/Core/ConnectionSettings.cs
index ba1c34c0d..f43162fbe 100644
--- a/src/MySqlConnector/Core/ConnectionSettings.cs
+++ b/src/MySqlConnector/Core/ConnectionSettings.cs
@@ -1,6 +1,8 @@
+#if NETCOREAPP3_0_OR_GREATER
 using System.Net.Security;
+#endif
+using System.Net;
 using System.Security.Authentication;
-using MySqlConnector.Logging;
 using MySqlConnector.Utilities;
 
 namespace MySqlConnector.Core;
@@ -12,7 +14,7 @@ public ConnectionSettings(MySqlConnectionStringBuilder csb)
 		ConnectionStringBuilder = csb;
 		ConnectionString = csb.ConnectionString;
 
-		if (csb.ConnectionProtocol == MySqlConnectionProtocol.UnixSocket || (!Utility.IsWindows() && (csb.Server.StartsWith("/", StringComparison.Ordinal) || csb.Server.StartsWith("./", StringComparison.Ordinal))))
+		if (csb.ConnectionProtocol == MySqlConnectionProtocol.UnixSocket || (!Utility.IsWindows() && (csb.Server.StartsWith('/') || csb.Server.StartsWith("./", StringComparison.Ordinal))))
 		{
 			if (csb.LoadBalance != MySqlLoadBalance.RoundRobin)
 				throw new NotSupportedException("LoadBalance not supported when ConnectionProtocol=UnixSocket");
@@ -27,7 +29,7 @@ public ConnectionSettings(MySqlConnectionStringBuilder csb)
 			if (csb.LoadBalance != MySqlLoadBalance.RoundRobin)
 				throw new NotSupportedException("LoadBalance not supported when ConnectionProtocol=NamedPipe");
 			ConnectionProtocol = MySqlConnectionProtocol.NamedPipe;
-			HostNames = (csb.Server == "." || string.Equals(csb.Server, "localhost", StringComparison.OrdinalIgnoreCase)) ? s_localhostPipeServer : new[] { csb.Server };
+			HostNames = (csb.Server == "." || string.Equals(csb.Server, "localhost", StringComparison.OrdinalIgnoreCase)) ? s_localhostPipeServer : [csb.Server];
 			PipeName = csb.PipeName;
 		}
 		else if (csb.ConnectionProtocol == MySqlConnectionProtocol.SharedMemory)
@@ -73,7 +75,7 @@ public ConnectionSettings(MySqlConnectionStringBuilder csb)
 					TlsVersions |= SslProtocols.Tls11;
 				else if (minorVersion == '2')
 					TlsVersions |= SslProtocols.Tls12;
-#if NETCOREAPP3_0_OR_GREATER
+#if NETCOREAPP3_0_OR_GREATER || NET48_OR_GREATER
 				else if (minorVersion == '3')
 					TlsVersions |= SslProtocols.Tls13;
 #endif
@@ -111,10 +113,6 @@ public ConnectionSettings(MySqlConnectionStringBuilder csb)
 		ConnectionLifeTime = Math.Min(csb.ConnectionLifeTime, uint.MaxValue / 1000) * 1000;
 		ConnectionReset = csb.ConnectionReset;
 		ConnectionIdleTimeout = (int) csb.ConnectionIdleTimeout;
-#pragma warning disable 618
-		if (!csb.DeferConnectionReset)
-			Log.Warn("DeferConnectionReset=false is not supported; using regular connection reset behavior.");
-#pragma warning restore 618
 		if (csb.MinimumPoolSize > csb.MaximumPoolSize)
 			throw new MySqlException("MaximumPoolSize must be greater than or equal to MinimumPoolSize");
 		MinimumPoolSize = ToSigned(csb.MinimumPoolSize);
@@ -126,7 +124,7 @@ public ConnectionSettings(MySqlConnectionStringBuilder csb)
 		AllowPublicKeyRetrieval = csb.AllowPublicKeyRetrieval;
 		AllowUserVariables = csb.AllowUserVariables;
 		AllowZeroDateTime = csb.AllowZeroDateTime;
-		ApplicationName = csb.ApplicationName;
+		ApplicationName = csb.ApplicationName is { Length: 0 } ? null : csb.ApplicationName;
 		AutoEnlist = csb.AutoEnlist;
 		CancellationTimeout = csb.CancellationTimeout;
 		ConnectionTimeout = ToSigned(csb.ConnectionTimeout);
@@ -153,25 +151,27 @@ public ConnectionSettings(MySqlConnectionStringBuilder csb)
 		static int ToSigned(uint value) => value >= int.MaxValue ? int.MaxValue : (int) value;
 	}
 
-	public ConnectionSettings CloneWith(string host, int port, string userId) => new ConnectionSettings(this, host, port, userId);
+	public ConnectionSettings CloneWith(string host, int port, string userId) => new(this, host, port, userId, null);
+
+	public ConnectionSettings CloneWith(IPAddress ipAddress) => new(this, HostNames![0], Port, UserID, ipAddress);
 
 	private static MySqlGuidFormat GetEffectiveGuidFormat(MySqlGuidFormat guidFormat, bool oldGuids)
 	{
 		switch (guidFormat)
 		{
-		case MySqlGuidFormat.Default:
-			return oldGuids ? MySqlGuidFormat.LittleEndianBinary16 : MySqlGuidFormat.Char36;
-		case MySqlGuidFormat.None:
-		case MySqlGuidFormat.Char36:
-		case MySqlGuidFormat.Char32:
-		case MySqlGuidFormat.Binary16:
-		case MySqlGuidFormat.TimeSwapBinary16:
-		case MySqlGuidFormat.LittleEndianBinary16:
-			if (oldGuids)
-				throw new MySqlException("OldGuids cannot be used with GuidFormat");
-			return guidFormat;
-		default:
-			throw new MySqlException("Unknown GuidFormat");
+			case MySqlGuidFormat.Default:
+				return oldGuids ? MySqlGuidFormat.LittleEndianBinary16 : MySqlGuidFormat.Char36;
+			case MySqlGuidFormat.None:
+			case MySqlGuidFormat.Char36:
+			case MySqlGuidFormat.Char32:
+			case MySqlGuidFormat.Binary16:
+			case MySqlGuidFormat.TimeSwapBinary16:
+			case MySqlGuidFormat.LittleEndianBinary16:
+				if (oldGuids)
+					throw new MySqlException("OldGuids cannot be used with GuidFormat");
+				return guidFormat;
+			default:
+				throw new MySqlException("Unknown GuidFormat");
 		}
 	}
 
@@ -185,6 +185,7 @@ private static MySqlGuidFormat GetEffectiveGuidFormat(MySqlGuidFormat guidFormat
 	public string ConnectionString { get; }
 	public MySqlConnectionProtocol ConnectionProtocol { get; }
 	public IReadOnlyList<string>? HostNames { get; }
+	public IPAddress? IPAddress { get; }
 	public MySqlLoadBalance LoadBalance { get; }
 	public int Port { get; }
 	public string PipeName { get; }
@@ -221,7 +222,7 @@ private static MySqlGuidFormat GetEffectiveGuidFormat(MySqlGuidFormat guidFormat
 	public bool AllowPublicKeyRetrieval { get; }
 	public bool AllowUserVariables { get; }
 	public bool AllowZeroDateTime { get; }
-	public string ApplicationName { get; }
+	public string? ApplicationName { get; }
 	public bool AutoEnlist { get; }
 	public int CancellationTimeout { get; }
 	public int ConnectionTimeout { get; }
@@ -271,13 +272,18 @@ public int ConnectionTimeoutMilliseconds
 		}
 	}
 
-	private ConnectionSettings(ConnectionSettings other, string host, int port, string userId)
+	private ConnectionSettings(ConnectionSettings other, string host, int port, string userId, IPAddress? ipAddress)
 	{
-		ConnectionStringBuilder = other.ConnectionStringBuilder;
-		ConnectionString = other.ConnectionString;
+		ConnectionStringBuilder = new MySqlConnectionStringBuilder(other.ConnectionString);
+		ConnectionStringBuilder.Port = (uint) port;
+		ConnectionStringBuilder.Server = host;
+		ConnectionStringBuilder.UserID = userId;
+
+		ConnectionString = ConnectionStringBuilder.ConnectionString;
 
 		ConnectionProtocol = MySqlConnectionProtocol.Sockets;
-		HostNames = new[] { host };
+		HostNames = [host];
+		IPAddress = ipAddress;
 		LoadBalance = other.LoadBalance;
 		Port = port;
 		PipeName = other.PipeName;
@@ -331,6 +337,5 @@ private ConnectionSettings(ConnectionSettings other, string host, int port, stri
 		UseXaTransactions = other.UseXaTransactions;
 	}
 
-	private static readonly IMySqlConnectorLogger Log = MySqlConnectorLogManager.CreateLogger(nameof(ConnectionSettings));
-	private static readonly string[] s_localhostPipeServer = { "." };
+	private static readonly string[] s_localhostPipeServer = ["."];
 }
diff --git a/src/MySqlConnector/Core/DbTypeMapping.cs b/src/MySqlConnector/Core/DbTypeMapping.cs
index 6fe3b096d..f72611ee7 100644
--- a/src/MySqlConnector/Core/DbTypeMapping.cs
+++ b/src/MySqlConnector/Core/DbTypeMapping.cs
@@ -2,24 +2,15 @@
 
 namespace MySqlConnector.Core;
 
-internal sealed class DbTypeMapping
+internal sealed class DbTypeMapping(Type clrType, DbType[] dbTypes, Func<object, object>? convert = null)
 {
-	public DbTypeMapping(Type clrType, DbType[] dbTypes, Func<object, object>? convert = null)
-	{
-		ClrType = clrType;
-		DbTypes = dbTypes;
-		m_convert = convert;
-	}
-
-	public Type ClrType { get; }
-	public DbType[] DbTypes { get; }
+	public Type ClrType { get; } = clrType;
+	public DbType[] DbTypes { get; } = dbTypes;
 
 	public object DoConversion(object obj)
 	{
 		if (obj.GetType() == ClrType)
 			return obj;
-		return m_convert is null ? Convert.ChangeType(obj, ClrType, CultureInfo.InvariantCulture)! : m_convert(obj);
+		return convert is null ? Convert.ChangeType(obj, ClrType, CultureInfo.InvariantCulture)! : convert(obj);
 	}
-
-	private readonly Func<object, object>? m_convert;
 }
diff --git a/src/MySqlConnector/Core/EnlistedTransactionBase.cs b/src/MySqlConnector/Core/EnlistedTransactionBase.cs
index 73a156846..c688d596b 100644
--- a/src/MySqlConnector/Core/EnlistedTransactionBase.cs
+++ b/src/MySqlConnector/Core/EnlistedTransactionBase.cs
@@ -2,15 +2,18 @@
 
 namespace MySqlConnector.Core;
 
-internal abstract class EnlistedTransactionBase : IEnlistmentNotification
+internal abstract class EnlistedTransactionBase(Transaction transaction, MySqlConnection connection) : IEnlistmentNotification
 {
 	// A MySqlConnection that holds the ServerSession that was enrolled in the transaction
-	public MySqlConnection Connection { get; set; }
+	public MySqlConnection Connection { get; set; } = connection;
 
 	// Whether the connection is idle, i.e., a client has closed it and is no longer using it
 	public bool IsIdle { get; set; }
 
-	public Transaction Transaction { get; private set; }
+	// Whether the distributed transaction was prepared successfully
+	public bool IsPrepared { get; private set; }
+
+	public Transaction Transaction { get; private set; } = transaction;
 
 	public void Start()
 	{
@@ -20,8 +23,16 @@ public void Start()
 
 	void IEnlistmentNotification.Prepare(PreparingEnlistment preparingEnlistment)
 	{
-		OnPrepare(preparingEnlistment);
-		preparingEnlistment.Prepared();
+		try
+		{
+			OnPrepare(preparingEnlistment);
+			IsPrepared = true;
+			preparingEnlistment.Prepared();
+		}
+		catch (Exception ex)
+		{
+			preparingEnlistment.ForceRollback(ex);
+		}
 	}
 
 	void IEnlistmentNotification.Commit(Enlistment enlistment)
@@ -40,12 +51,6 @@ void IEnlistmentNotification.Rollback(Enlistment enlistment)
 
 	public void InDoubt(Enlistment enlistment) => throw new NotImplementedException();
 
-	protected EnlistedTransactionBase(Transaction transaction, MySqlConnection connection)
-	{
-		Transaction = transaction;
-		Connection = connection;
-	}
-
 	protected abstract void OnStart();
 	protected abstract void OnPrepare(PreparingEnlistment enlistment);
 	protected abstract void OnCommit(Enlistment enlistment);
diff --git a/src/MySqlConnector/Core/ICancellableCommand.cs b/src/MySqlConnector/Core/ICancellableCommand.cs
index 2eb99a367..04a713fcc 100644
--- a/src/MySqlConnector/Core/ICancellableCommand.cs
+++ b/src/MySqlConnector/Core/ICancellableCommand.cs
@@ -9,9 +9,10 @@ internal interface ICancellableCommand
 {
 	int CommandId { get; }
 	int CommandTimeout { get; }
+	int? EffectiveCommandTimeout { get; set; }
 	int CancelAttemptCount { get; set; }
 	MySqlConnection? Connection { get; }
-	IDisposable? RegisterCancel(CancellationToken cancellationToken);
+	CancellationTokenRegistration RegisterCancel(CancellationToken cancellationToken);
 	void SetTimeout(int milliseconds);
 	bool IsTimedOut { get; }
 }
@@ -23,22 +24,6 @@ internal static class ICancellableCommandExtensions
 	/// </summary>
 	public static int GetNextId() => Interlocked.Increment(ref s_id);
 
-	/// <summary>
-	/// Returns the time (in seconds) until a command should be canceled, clamping it to the maximum time
-	/// allowed including CancellationTimeout.
-	/// </summary>
-	public static int GetCommandTimeUntilCanceled(this ICancellableCommand command)
-	{
-		var commandTimeout = command.CommandTimeout;
-		var session = command.Connection?.Session;
-		if (commandTimeout == 0 || session is null)
-			return 0;
-
-		// the total cancellation period (graphically) is [===CommandTimeout===][=CancellationTimeout=], which can't
-		// exceed int.MaxValue/1000 because it has to be multiplied by 1000 to be converted to milliseconds
-		return Math.Min(commandTimeout, Math.Max(1, (int.MaxValue / 1000) - session.CancellationTimeout));
-	}
-
 	/// <summary>
 	/// Causes the effective command timeout to be reset back to the value specified by <see cref="ICancellableCommand.CommandTimeout"/>
 	/// plus <see cref="MySqlConnectionStringBuilder.CancellationTimeout"/>. This allows for the command to time out, a cancellation to attempt
@@ -53,28 +38,53 @@ public static int GetCommandTimeUntilCanceled(this ICancellableCommand command)
 	/// method call.</remarks>
 	public static void ResetCommandTimeout(this ICancellableCommand command)
 	{
+		// read value cached on the command
+		var effectiveCommandTimeout = command.EffectiveCommandTimeout;
+
+		// early out if there is no timeout
+		if (effectiveCommandTimeout == Constants.InfiniteTimeout)
+			return;
+
 		var session = command.Connection?.Session;
-		if (session is not null)
+		if (session is null)
+			return;
+
+		// determine the effective command timeout if not already cached
+		if (effectiveCommandTimeout is null)
 		{
-			if (command.CommandTimeout == 0 || session.CancellationTimeout == 0)
+			var commandTimeout = command.CommandTimeout;
+			var cancellationTimeout = session.CancellationTimeout;
+
+			if (commandTimeout == 0 || cancellationTimeout == 0)
 			{
-				session.SetTimeout(Constants.InfiniteTimeout);
+				// if commandTimeout is zero, then cancellation doesn't occur
+				effectiveCommandTimeout = Constants.InfiniteTimeout;
 			}
 			else
 			{
-				var commandTimeUntilCanceled = command.GetCommandTimeUntilCanceled() * 1000;
-				if (session.CancellationTimeout > 0)
-				{
-					// try to cancel first, then close socket
-					command.SetTimeout(commandTimeUntilCanceled);
-					session.SetTimeout(commandTimeUntilCanceled + session.CancellationTimeout * 1000);
-				}
-				else
-				{
-					// close socket once the timeout is reached
-					session.SetTimeout(commandTimeUntilCanceled);
-				}
+				// the total cancellation period (graphically) is [===CommandTimeout===][=CancellationTimeout=], which can't
+				// exceed int.MaxValue/1000 because it has to be multiplied by 1000 to be converted to milliseconds
+				effectiveCommandTimeout = Math.Min(commandTimeout, Math.Max(1, (int.MaxValue / 1000) - Math.Max(0, session.CancellationTimeout))) * 1000;
 			}
+
+			command.EffectiveCommandTimeout = effectiveCommandTimeout;
+		}
+
+		if (effectiveCommandTimeout == Constants.InfiniteTimeout)
+		{
+			// for no timeout, we set an infinite timeout once (then early out above)
+			session.SetTimeout(Constants.InfiniteTimeout);
+		}
+		else if (session.CancellationTimeout > 0)
+		{
+			// try to cancel first, then close socket
+			command.SetTimeout(effectiveCommandTimeout.Value);
+			session.SetTimeout(effectiveCommandTimeout.Value + (session.CancellationTimeout * 1000));
+		}
+		else
+		{
+			// close socket once the timeout is reached
+			session.SetTimeout(effectiveCommandTimeout.Value);
 		}
 	}
 
diff --git a/src/MySqlConnector/Core/ICommandPayloadCreator.cs b/src/MySqlConnector/Core/ICommandPayloadCreator.cs
index c6e32d04a..14d6fb9c2 100644
--- a/src/MySqlConnector/Core/ICommandPayloadCreator.cs
+++ b/src/MySqlConnector/Core/ICommandPayloadCreator.cs
@@ -7,6 +7,16 @@ namespace MySqlConnector.Core;
 /// </summary>
 internal interface ICommandPayloadCreator
 {
+	/// <summary>
+	/// Sends any data that is required to be sent to the server before the current command in the command list.
+	/// </summary>
+	/// <param name="connection">The <see cref="MySqlConnection"/> to which the data will be written.</param>
+	/// <param name="commandListPosition">The <see cref="CommandListPosition"/> giving the current command and current prepared statement.</param>
+	/// <param name="ioBehavior">The IO behavior.</param>
+	/// <param name="cancellationToken">A cancellation token to cancel the asynchronous operation.</param>
+	/// <returns>A <see cref="ValueTask"/> representing the asynchronous operation or a completed <see cref="ValueTask"/> if no data needed to be sent.</returns>
+	ValueTask SendCommandPrologueAsync(MySqlConnection connection, CommandListPosition commandListPosition, IOBehavior ioBehavior, CancellationToken cancellationToken);
+
 	/// <summary>
 	/// Writes the payload for an "execute query" command to <paramref name="writer"/>.
 	/// </summary>
diff --git a/src/MySqlConnector/Core/IConnectionPoolMetadata.cs b/src/MySqlConnector/Core/IConnectionPoolMetadata.cs
new file mode 100644
index 000000000..64f8dd31b
--- /dev/null
+++ b/src/MySqlConnector/Core/IConnectionPoolMetadata.cs
@@ -0,0 +1,26 @@
+namespace MySqlConnector.Core;
+
+internal interface IConnectionPoolMetadata
+{
+	/// <summary>
+	/// Returns the <see cref="ConnectionPool"/> this <see cref="IConnectionPoolMetadata"/> is associated with,
+	/// or <c>null</c> if it represents a non-pooled connection.
+	/// </summary>
+	ConnectionPool? ConnectionPool { get; }
+
+	/// <summary>
+	/// Returns the ID of the connection pool, or 0 if this is a non-pooled connection.
+	/// </summary>
+	int Id { get; }
+
+	/// <summary>
+	/// Returns the generation of the connection pool, or 0 if this is a non-pooled connection.
+	/// </summary>
+	int Generation { get; }
+
+	/// <summary>
+	/// Returns a new session ID.
+	/// </summary>
+	/// <returns>A new session ID.</returns>
+	int GetNewSessionId();
+}
diff --git a/src/MySqlConnector/Core/ILoadBalancer.cs b/src/MySqlConnector/Core/ILoadBalancer.cs
index d6dd82ebb..48edbf47d 100644
--- a/src/MySqlConnector/Core/ILoadBalancer.cs
+++ b/src/MySqlConnector/Core/ILoadBalancer.cs
@@ -27,26 +27,38 @@ internal sealed class RandomLoadBalancer : ILoadBalancer
 	public IReadOnlyList<string> LoadBalance(IReadOnlyList<string> hosts)
 	{
 #pragma warning disable CA5394 // Do not use insecure randomness
-		// from https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm
+#if NET8_0_OR_GREATER
+		var shuffled = hosts.ToArray();
+		lock (m_lock)
+			m_random.Shuffle(shuffled);
+		return shuffled;
+#else
 		var shuffled = new List<string>(hosts);
+		// from https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm
 		for (var i = hosts.Count - 1; i >= 1; i--)
 		{
 			int j;
-			lock (m_random)
+			lock (m_lock)
 				j = m_random.Next(i + 1);
 			if (i != j)
-			{
-				var temp = shuffled[i];
-				shuffled[i] = shuffled[j];
-				shuffled[j] = temp;
-			}
+				(shuffled[j], shuffled[i]) = (shuffled[i], shuffled[j]);
 		}
 		return shuffled;
+#endif
 	}
 
-	private RandomLoadBalancer() => m_random = new();
+	private RandomLoadBalancer()
+	{
+		m_random = new();
+		m_lock = new();
+	}
 
 	private readonly Random m_random;
+#if NET9_0_OR_GREATER
+	private readonly Lock m_lock;
+#else
+	private readonly object m_lock;
+#endif
 }
 
 internal sealed class RoundRobinLoadBalancer : ILoadBalancer
@@ -67,6 +79,10 @@ public IReadOnlyList<string> LoadBalance(IReadOnlyList<string> hosts)
 		return shuffled;
 	}
 
+#if NET9_0_OR_GREATER
+	private readonly Lock m_lock;
+#else
 	private readonly object m_lock;
+#endif
 	private uint m_counter;
 }
diff --git a/src/MySqlConnector/Core/IMySqlCommand.cs b/src/MySqlConnector/Core/IMySqlCommand.cs
index 768c7dbaa..5d4663438 100644
--- a/src/MySqlConnector/Core/IMySqlCommand.cs
+++ b/src/MySqlConnector/Core/IMySqlCommand.cs
@@ -1,3 +1,5 @@
+using Microsoft.Extensions.Logging;
+
 namespace MySqlConnector.Core;
 
 /// <summary>
@@ -18,6 +20,7 @@ internal interface IMySqlCommand
 	MySqlParameterCollection? OutParameters { get; set; }
 	MySqlParameter? ReturnParameter { get; set; }
 	ICancellableCommand CancellableCommand { get; }
+	ILogger Logger { get; }
 }
 
 internal static class IMySqlCommandExtensions
diff --git a/src/MySqlConnector/Core/IServerCapabilities.cs b/src/MySqlConnector/Core/IServerCapabilities.cs
new file mode 100644
index 000000000..233e63cd0
--- /dev/null
+++ b/src/MySqlConnector/Core/IServerCapabilities.cs
@@ -0,0 +1,7 @@
+namespace MySqlConnector.Core;
+
+internal interface IServerCapabilities
+{
+	bool SupportsDeprecateEof { get; }
+	bool SupportsSessionTrack { get; }
+}
diff --git a/src/MySqlConnector/Core/IValuesEnumerator.cs b/src/MySqlConnector/Core/IValuesEnumerator.cs
index f53a481df..0d3cac1d3 100644
--- a/src/MySqlConnector/Core/IValuesEnumerator.cs
+++ b/src/MySqlConnector/Core/IValuesEnumerator.cs
@@ -12,49 +12,35 @@ internal interface IValuesEnumerator
 	void GetValues(object[] values);
 }
 
-internal sealed class DbDataReaderValuesEnumerator : IValuesEnumerator
+internal sealed class DbDataReaderValuesEnumerator(DbDataReader dataReader) : IValuesEnumerator
 {
-	public DbDataReaderValuesEnumerator(DbDataReader dataReader) => m_dataReader = dataReader;
+	public int FieldCount => dataReader.FieldCount;
 
-	public int FieldCount => m_dataReader.FieldCount;
+	public ValueTask<bool> MoveNextAsync() => new(dataReader.ReadAsync());
 
-	public ValueTask<bool> MoveNextAsync() => new ValueTask<bool>(m_dataReader.ReadAsync());
+	public bool MoveNext() => dataReader.Read();
 
-	public bool MoveNext() => m_dataReader.Read();
-
-	public void GetValues(object[] values) => m_dataReader.GetValues(values);
-
-	private readonly DbDataReader m_dataReader;
+	public void GetValues(object[] values) => dataReader.GetValues(values);
 }
 
-internal sealed class DataReaderValuesEnumerator : IValuesEnumerator
+internal sealed class DataReaderValuesEnumerator(IDataReader dataReader) : IValuesEnumerator
 {
 	public static IValuesEnumerator Create(IDataReader dataReader) => dataReader is DbDataReader dbDataReader ? (IValuesEnumerator) new DbDataReaderValuesEnumerator(dbDataReader) : new DataReaderValuesEnumerator(dataReader);
 
-	public DataReaderValuesEnumerator(IDataReader dataReader) => m_dataReader = dataReader;
-
-	public int FieldCount => m_dataReader.FieldCount;
+	public int FieldCount => dataReader.FieldCount;
 
 	public ValueTask<bool> MoveNextAsync() => new(MoveNext());
 
-	public bool MoveNext() => m_dataReader.Read();
-
-	public void GetValues(object[] values) => m_dataReader.GetValues(values);
+	public bool MoveNext() => dataReader.Read();
 
-	private readonly IDataReader m_dataReader;
+	public void GetValues(object[] values) => dataReader.GetValues(values);
 }
 
-internal sealed class DataRowsValuesEnumerator : IValuesEnumerator
+internal sealed class DataRowsValuesEnumerator(IEnumerable<DataRow> dataRows, int columnCount) : IValuesEnumerator
 {
 	public static IValuesEnumerator Create(DataTable dataTable) => new DataRowsValuesEnumerator(dataTable.Rows.Cast<DataRow>().Where(static x => x is not null).Select(static x => x!), dataTable.Columns.Count);
 
-	public DataRowsValuesEnumerator(IEnumerable<DataRow> dataRows, int columnCount)
-	{
-		m_dataRows = dataRows.GetEnumerator();
-		FieldCount = columnCount;
-	}
-
-	public int FieldCount { get; }
+	public int FieldCount { get; } = columnCount;
 
 	public ValueTask<bool> MoveNextAsync() => new(MoveNext());
 
@@ -73,5 +59,5 @@ public void GetValues(object[] values)
 			values[i] = row[i];
 	}
 
-	private readonly IEnumerator<DataRow> m_dataRows;
+	private readonly IEnumerator<DataRow> m_dataRows = dataRows.GetEnumerator();
 }
diff --git a/src/MySqlConnector/Core/MetricsReporter.cs b/src/MySqlConnector/Core/MetricsReporter.cs
new file mode 100644
index 000000000..22440918a
--- /dev/null
+++ b/src/MySqlConnector/Core/MetricsReporter.cs
@@ -0,0 +1,60 @@
+using System.Diagnostics.Metrics;
+using MySqlConnector.Utilities;
+
+namespace MySqlConnector.Core;
+
+internal static class MetricsReporter
+{
+	public static void AddIdle(ConnectionPool pool) => s_connectionsUsageCounter.Add(1, pool.IdleStateTagList);
+	public static void RemoveIdle(ConnectionPool pool) => s_connectionsUsageCounter.Add(-1, pool.IdleStateTagList);
+	public static void AddUsed(ConnectionPool pool) => s_connectionsUsageCounter.Add(1, pool.UsedStateTagList);
+	public static void RemoveUsed(ConnectionPool pool) => s_connectionsUsageCounter.Add(-1, pool.UsedStateTagList);
+	public static void AddTimeout(ConnectionPool? pool, ConnectionSettings connectionSettings) => s_connectionTimeouts.Add(1, new KeyValuePair<string, object?>("pool.name", pool?.Name ?? connectionSettings.ApplicationName ?? connectionSettings.ConnectionStringBuilder.GetConnectionString(includePassword: false)));
+	public static void RecordCreateTime(ConnectionPool pool, double seconds) => s_createTimeHistory.Record(seconds, pool.PoolNameTagList);
+	public static void RecordUseTime(ConnectionPool pool, double seconds) => s_useTimeHistory.Record(seconds, pool.PoolNameTagList);
+	public static void RecordWaitTime(ConnectionPool pool, double seconds) => s_waitTimeHistory.Record(seconds, pool.PoolNameTagList);
+
+	public static void AddPendingRequest(ConnectionPool? pool)
+	{
+		if (pool is not null)
+			s_pendingRequestsCounter.Add(1, pool.PoolNameTagList);
+	}
+
+	public static void RemovePendingRequest(ConnectionPool? pool)
+	{
+		if (pool is not null)
+			s_pendingRequestsCounter.Add(-1, pool.PoolNameTagList);
+	}
+
+	static MetricsReporter()
+	{
+		_ = ActivitySourceHelper.Meter.CreateObservableUpDownCounter<int>("db.client.connections.idle.max",
+			observeValues: GetMaximumConnections, unit: "{connection}",
+			description: "The maximum number of idle open connections allowed; this corresponds to MaximumPoolSize in the connection string.");
+		_ = ActivitySourceHelper.Meter.CreateObservableUpDownCounter<int>("db.client.connections.idle.min",
+			observeValues: GetMinimumConnections, unit: "{connection}",
+			description: "The minimum number of idle open connections allowed; this corresponds to MinimumPoolSize in the connection string.");
+		_ = ActivitySourceHelper.Meter.CreateObservableUpDownCounter<int>("db.client.connections.max",
+			observeValues: GetMaximumConnections, unit: "{connection}",
+			description: "The maximum number of open connections allowed; this corresponds to MaximumPoolSize in the connection string.");
+
+		static IEnumerable<Measurement<int>> GetMaximumConnections() =>
+			ConnectionPool.GetAllPools().Select(static x => new Measurement<int>(x.ConnectionSettings.MaximumPoolSize, x.PoolNameTagList));
+
+		static IEnumerable<Measurement<int>> GetMinimumConnections() =>
+			ConnectionPool.GetAllPools().Select(static x => new Measurement<int>(x.ConnectionSettings.MinimumPoolSize, x.PoolNameTagList));
+	}
+
+	private static readonly UpDownCounter<int> s_connectionsUsageCounter = ActivitySourceHelper.Meter.CreateUpDownCounter<int>("db.client.connections.usage",
+			unit: "{connection}", description: "The number of connections that are currently in the state described by the state tag.");
+	private static readonly UpDownCounter<int> s_pendingRequestsCounter = ActivitySourceHelper.Meter.CreateUpDownCounter<int>("db.client.connections.pending_requests",
+			unit: "{request}", description: "The number of pending requests for an open connection, cumulative for the entire pool.");
+	private static readonly Counter<int> s_connectionTimeouts = ActivitySourceHelper.Meter.CreateCounter<int>("db.client.connections.timeouts",
+			unit: "{timeout}", description: "The number of connection timeouts that have occurred trying to obtain a connection from the pool.");
+	private static readonly Histogram<double> s_createTimeHistory = ActivitySourceHelper.Meter.CreateHistogram<double>("db.client.connections.create_time",
+			unit: "s", description: "The time it took to create a new connection.");
+	private static readonly Histogram<double> s_useTimeHistory = ActivitySourceHelper.Meter.CreateHistogram<double>("db.client.connections.use_time",
+		unit: "s", description: "The time between borrowing a connection and returning it to the pool.");
+	private static readonly Histogram<double> s_waitTimeHistory = ActivitySourceHelper.Meter.CreateHistogram<double>("db.client.connections.wait_time",
+			unit: "s", description: "The time it took to obtain an open connection from the pool.");
+}
diff --git a/src/MySqlConnector/Core/NonPooledConnectionPoolMetadata.cs b/src/MySqlConnector/Core/NonPooledConnectionPoolMetadata.cs
new file mode 100644
index 000000000..20d296722
--- /dev/null
+++ b/src/MySqlConnector/Core/NonPooledConnectionPoolMetadata.cs
@@ -0,0 +1,13 @@
+namespace MySqlConnector.Core;
+
+internal sealed class NonPooledConnectionPoolMetadata : IConnectionPoolMetadata
+{
+	public static IConnectionPoolMetadata Instance { get; } = new NonPooledConnectionPoolMetadata();
+
+	public ConnectionPool? ConnectionPool => null;
+	public int Id => 0;
+	public int Generation => 0;
+	public int GetNewSessionId() => Interlocked.Increment(ref m_lastId);
+
+	private int m_lastId;
+}
diff --git a/src/MySqlConnector/Core/NormalizedSchema.cs b/src/MySqlConnector/Core/NormalizedSchema.cs
index d0d5bd997..d8667c30e 100644
--- a/src/MySqlConnector/Core/NormalizedSchema.cs
+++ b/src/MySqlConnector/Core/NormalizedSchema.cs
@@ -17,15 +17,13 @@ internal sealed partial class NormalizedSchema
 	private static readonly Regex s_nameRegex = new(ReName, RegexOptions.Compiled);
 #endif
 
-	public static NormalizedSchema MustNormalize(string name, string? defaultSchema = null)
-	{
-		var normalized = new NormalizedSchema(name, defaultSchema);
-		if (normalized.Component is null)
-			throw new ArgumentException("Could not determine function/procedure name", nameof(name));
-		if (normalized.Schema is null)
-			throw new ArgumentException("Could not determine schema", nameof(defaultSchema));
-		return normalized;
-	}
+	public static NormalizedSchema MustNormalize(string name, string? defaultSchema = null) =>
+		new NormalizedSchema(name, defaultSchema) switch
+		{
+			{ Component: null } => throw new ArgumentException("Could not determine function/procedure name", nameof(name)),
+			{ Schema: null } => throw new ArgumentException("Could not determine schema", nameof(defaultSchema)),
+			{ } normalized => normalized,
+		};
 
 	public NormalizedSchema(string name, string? defaultSchema = null)
 	{
diff --git a/src/MySqlConnector/Core/ParsedStatement.cs b/src/MySqlConnector/Core/ParsedStatement.cs
index d04ca7236..489a81a00 100644
--- a/src/MySqlConnector/Core/ParsedStatement.cs
+++ b/src/MySqlConnector/Core/ParsedStatement.cs
@@ -15,17 +15,17 @@ internal sealed class ParsedStatement
 	/// The names of the parameters (if known) of the parameters in the prepared statement. There
 	/// is one entry in this list for each parameter, which will be <c>null</c> if the name is unknown.
 	/// </summary>
-	public List<string?> ParameterNames { get; } = new();
+	public List<string?> ParameterNames { get; } = [];
 
 	/// <summary>
 	/// The normalized names of the parameters (if known) of the parameters in the prepared statement. There
 	/// is one entry in this list for each parameter, which will be <c>null</c> if the name is unknown.
 	/// </summary>
-	public List<string?> NormalizedParameterNames { get; } = new();
+	public List<string?> NormalizedParameterNames { get; } = [];
 
 	/// <summary>
 	/// The indexes of the parameters in the prepared statement. There is one entry in this list for
 	/// each parameter; it will be <c>-1</c> if the parameter is named.
 	/// </summary>
-	public List<int> ParameterIndexes { get; } = new();
+	public List<int> ParameterIndexes { get; } = [];
 }
diff --git a/src/MySqlConnector/Core/ParsedStatements.cs b/src/MySqlConnector/Core/ParsedStatements.cs
index 9c224ebc8..fa429edf0 100644
--- a/src/MySqlConnector/Core/ParsedStatements.cs
+++ b/src/MySqlConnector/Core/ParsedStatements.cs
@@ -6,23 +6,14 @@ namespace MySqlConnector.Core;
 /// <see cref="ParsedStatements"/> wraps a collection of <see cref="ParsedStatement"/> objects.
 /// It implements <see cref="IDisposable"/> to return the memory backing the statements to a shared pool.
 /// </summary>
-internal sealed class ParsedStatements : IDisposable
+internal sealed class ParsedStatements(List<ParsedStatement> statements, PayloadData payloadData) : IDisposable
 {
-	public IReadOnlyList<ParsedStatement> Statements => m_statements;
+	public IReadOnlyList<ParsedStatement> Statements => statements;
 
 	public void Dispose()
 	{
-		m_statements.Clear();
-		m_payloadData.Dispose();
-		m_payloadData = default;
+		statements.Clear();
+		payloadData.Dispose();
+		payloadData = default;
 	}
-
-	internal ParsedStatements(List<ParsedStatement> statements, PayloadData payloadData)
-	{
-		m_statements = statements;
-		m_payloadData = payloadData;
-	}
-
-	private readonly List<ParsedStatement> m_statements;
-	private PayloadData m_payloadData;
 }
diff --git a/src/MySqlConnector/Core/PreparedStatement.cs b/src/MySqlConnector/Core/PreparedStatement.cs
index cd9db2891..cbba24ea6 100644
--- a/src/MySqlConnector/Core/PreparedStatement.cs
+++ b/src/MySqlConnector/Core/PreparedStatement.cs
@@ -5,18 +5,10 @@ namespace MySqlConnector.Core;
 /// <summary>
 /// <see cref="PreparedStatement"/> is a statement that has been prepared on the MySQL Server.
 /// </summary>
-internal sealed class PreparedStatement
+internal sealed class PreparedStatement(int statementId, ParsedStatement statement, ColumnDefinitionPayload[]? columns, ColumnDefinitionPayload[]? parameters)
 {
-	public PreparedStatement(int statementId, ParsedStatement statement, ColumnDefinitionPayload[]? columns, ColumnDefinitionPayload[]? parameters)
-	{
-		StatementId = statementId;
-		Statement = statement;
-		Columns = columns;
-		Parameters = parameters;
-	}
-
-	public int StatementId { get; }
-	public ParsedStatement Statement { get; }
-	public ColumnDefinitionPayload[]? Columns { get; }
-	public ColumnDefinitionPayload[]? Parameters { get; }
+	public int StatementId { get; } = statementId;
+	public ParsedStatement Statement { get; } = statement;
+	public ColumnDefinitionPayload[]? Columns { get; set; } = columns;
+	public ColumnDefinitionPayload[]? Parameters { get; } = parameters;
 }
diff --git a/src/MySqlConnector/Core/PreparedStatements.cs b/src/MySqlConnector/Core/PreparedStatements.cs
index f4798a141..1bbd32e5a 100644
--- a/src/MySqlConnector/Core/PreparedStatements.cs
+++ b/src/MySqlConnector/Core/PreparedStatements.cs
@@ -1,20 +1,12 @@
 namespace MySqlConnector.Core;
 
-internal sealed class PreparedStatements : IDisposable
+internal sealed class PreparedStatements(IReadOnlyList<PreparedStatement> preparedStatements, ParsedStatements parsedStatements) : IDisposable
 {
-	public IReadOnlyList<PreparedStatement> Statements { get; }
-
-	public PreparedStatements(IReadOnlyList<PreparedStatement> preparedStatements, ParsedStatements parsedStatements)
-	{
-		Statements = preparedStatements;
-		m_parsedStatements = parsedStatements;
-	}
+	public IReadOnlyList<PreparedStatement> Statements { get; } = preparedStatements;
 
 	public void Dispose()
 	{
-		m_parsedStatements?.Dispose();
-		m_parsedStatements = null;
+		parsedStatements?.Dispose();
+		parsedStatements = null!;
 	}
-
-	private ParsedStatements? m_parsedStatements;
 }
diff --git a/src/MySqlConnector/Core/ResultSet.cs b/src/MySqlConnector/Core/ResultSet.cs
index 4404e1fd5..dd535c66b 100644
--- a/src/MySqlConnector/Core/ResultSet.cs
+++ b/src/MySqlConnector/Core/ResultSet.cs
@@ -9,19 +9,13 @@
 
 namespace MySqlConnector.Core;
 
-internal sealed class ResultSet
+internal sealed class ResultSet(MySqlDataReader dataReader)
 {
-	public ResultSet(MySqlDataReader dataReader)
-	{
-		DataReader = dataReader;
-	}
-
 	public void Reset()
 	{
 		// ResultSet can be re-used, so initialize everything
 		BufferState = ResultSetState.None;
-		ColumnDefinitions = null;
-		ColumnTypes = null;
+		m_columnDefinitions = default;
 		WarningCount = 0;
 		State = ResultSetState.None;
 		ContainsCommandParameters = false;
@@ -44,7 +38,7 @@ public async Task ReadResultSetHeaderAsync(IOBehavior ioBehavior)
 				var firstByte = payload.HeaderByte;
 				if (firstByte == OkPayload.Signature)
 				{
-					var ok = OkPayload.Create(payload.Span, Session.SupportsDeprecateEof, Session.SupportsSessionTrack);
+					var ok = OkPayload.Create(payload.Span, Session);
 
 					// if we've read a result set header then this is a SELECT statement, so we shouldn't overwrite RecordsAffected
 					// (which should be -1 for SELECT) unless the server reports a non-zero count
@@ -56,8 +50,7 @@ public async Task ReadResultSetHeaderAsync(IOBehavior ioBehavior)
 					WarningCount = ok.WarningCount;
 					if (ok.NewSchema is not null)
 						Connection.Session.DatabaseOverride = ok.NewSchema;
-					ColumnDefinitions = null;
-					ColumnTypes = null;
+					m_columnDefinitions = default;
 					State = (ok.ServerStatus & ServerStatus.MoreResultsExist) == 0
 						? ResultSetState.NoMoreData
 						: ResultSetState.HasMoreData;
@@ -69,41 +62,41 @@ public async Task ReadResultSetHeaderAsync(IOBehavior ioBehavior)
 					try
 					{
 						if (!Connection.AllowLoadLocalInfile)
-							throw new NotSupportedException("To use LOAD DATA LOCAL INFILE, set AllowLoadLocalInfile=true in the connection string. See https://fl.vu/mysql-load-data");
+							throw new NotSupportedException("To use LOAD DATA LOCAL INFILE, set AllowLoadLocalInfile=true in the connection string. See https://mysqlconnector.net/load-data");
 						var localInfile = LocalInfilePayload.Create(payload.Span);
 						var hasSourcePrefix = localInfile.FileName.StartsWith(MySqlBulkLoader.SourcePrefix, StringComparison.Ordinal);
 						if (!IsHostVerified(Connection) && !hasSourcePrefix)
-							throw new NotSupportedException("Use SourceStream or SslMode >= VerifyCA for LOAD DATA LOCAL INFILE. See https://fl.vu/mysql-load-data");
+							throw new NotSupportedException("Use SourceStream or SslMode >= VerifyCA for LOAD DATA LOCAL INFILE. See https://mysqlconnector.net/load-data");
 
 						var source = hasSourcePrefix ?
 							MySqlBulkLoader.GetAndRemoveSource(localInfile.FileName) :
 							File.OpenRead(localInfile.FileName);
 						switch (source)
 						{
-						case Stream stream:
-							var buffer = ArrayPool<byte>.Shared.Rent(1048576);
-							try
-							{
-								int byteCount;
-								while ((byteCount = await stream.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None).ConfigureAwait(false)) > 0)
+							case Stream stream:
+								var buffer = ArrayPool<byte>.Shared.Rent(1048576);
+								try
+								{
+									int byteCount;
+									while ((byteCount = await stream.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None).ConfigureAwait(false)) > 0)
+									{
+										payload = new(new ArraySegment<byte>(buffer, 0, byteCount));
+										await Session.SendReplyAsync(payload, ioBehavior, CancellationToken.None).ConfigureAwait(false);
+									}
+								}
+								finally
 								{
-									payload = new(new ArraySegment<byte>(buffer, 0, byteCount));
-									await Session.SendReplyAsync(payload, ioBehavior, CancellationToken.None).ConfigureAwait(false);
+									ArrayPool<byte>.Shared.Return(buffer);
+									stream.Dispose();
 								}
-							}
-							finally
-							{
-								ArrayPool<byte>.Shared.Return(buffer);
-								stream.Dispose();
-							}
-							break;
-
-						case MySqlBulkCopy bulkCopy:
-							await bulkCopy.SendDataReaderAsync(ioBehavior, CancellationToken.None).ConfigureAwait(false);
-							break;
-
-						default:
-							throw new InvalidOperationException($"Unsupported Source type: {source.GetType().Name}");
+								break;
+
+							case MySqlBulkCopy bulkCopy:
+								await bulkCopy.SendDataReaderAsync(ioBehavior, CancellationToken.None).ConfigureAwait(false);
+								break;
+
+							default:
+								throw new InvalidOperationException($"Unsupported Source type: {source.GetType().Name}");
 						}
 					}
 					catch (Exception ex)
@@ -116,49 +109,64 @@ public async Task ReadResultSetHeaderAsync(IOBehavior ioBehavior)
 				}
 				else
 				{
-					static int ReadColumnCount(ReadOnlySpan<byte> span)
+					var columnCountPacket = ColumnCountPayload.Create(payload.Span, Session.SupportsCachedPreparedMetadata);
+					var columnCount = columnCountPacket.ColumnCount;
+					if (!columnCountPacket.MetadataFollows)
 					{
-						var reader = new ByteArrayReader(span);
-						var columnCount_ = (int) reader.ReadLengthEncodedInteger();
-						if (reader.BytesRemaining != 0)
-							throw new MySqlException("Unexpected data at end of column_count packet; see https://github.com/mysql-net/MySqlConnector/issues/324");
-						return columnCount_;
+						// reuse previous metadata
+						m_columnDefinitions = DataReader.LastUsedPreparedStatement!.Columns!;
+						if (m_columnDefinitions.Length != columnCount)
+							throw new InvalidOperationException($"Expected result set to have {m_columnDefinitions.Length} columns, but it contains {columnCount} columns");
 					}
-					var columnCount = ReadColumnCount(payload.Span);
+					else
+					{
+						// parse columns
+						// reserve adequate space to hold a copy of all column definitions (but note that this can be resized below if we guess too small)
+						Utility.Resize(ref m_columnDefinitionPayloadBytes, columnCount * 96);
 
-					// reserve adequate space to hold a copy of all column definitions (but note that this can be resized below if we guess too small)
-					Utility.Resize(ref m_columnDefinitionPayloads, columnCount * 96);
+						// increase the cache size to be large enough to hold all the column definitions
+						if (m_columnDefinitionPayloadCache is null)
+							m_columnDefinitionPayloadCache = new ColumnDefinitionPayload[columnCount];
+						else if (m_columnDefinitionPayloadCache.Length < columnCount)
+							Array.Resize(ref m_columnDefinitionPayloadCache, Math.Max(columnCount, m_columnDefinitionPayloadCache.Length * 2));
+						m_columnDefinitions = m_columnDefinitionPayloadCache.AsMemory(0, columnCount);
 
-					ColumnDefinitions = new ColumnDefinitionPayload[columnCount];
-					ColumnTypes = new MySqlDbType[columnCount];
+						// if the server supports metadata caching but has re-sent it, something has changed since last prepare/execution and we need to update the columns
+						var preparedColumns = Session.SupportsCachedPreparedMetadata ? DataReader.LastUsedPreparedStatement?.Columns : null;
 
-					for (var column = 0; column < ColumnDefinitions.Length; column++)
-					{
-						payload = await Session.ReceiveReplyAsync(ioBehavior, CancellationToken.None).ConfigureAwait(false);
-						var payloadLength = payload.Span.Length;
+						for (var column = 0; column < columnCount; column++)
+						{
+							payload = await Session.ReceiveReplyAsync(ioBehavior, CancellationToken.None).ConfigureAwait(false);
+							var payloadLength = payload.Span.Length;
+
+							// 'Session.ReceiveReplyAsync' reuses a shared buffer; make a copy so that the column definitions can always be safely read at any future point
+							if (m_columnDefinitionPayloadUsedBytes + payloadLength > m_columnDefinitionPayloadBytes.Count)
+								Utility.Resize(ref m_columnDefinitionPayloadBytes, m_columnDefinitionPayloadUsedBytes + payloadLength);
+							payload.Span.CopyTo(m_columnDefinitionPayloadBytes.AsSpan(m_columnDefinitionPayloadUsedBytes));
+
+							// create/update the column definition in our cache
+							var payloadBytesSegment = new ResizableArraySegment<byte>(m_columnDefinitionPayloadBytes, m_columnDefinitionPayloadUsedBytes, payloadLength);
+							ColumnDefinitionPayload.Initialize(ref m_columnDefinitionPayloadCache[column], payloadBytesSegment);
 
-						// 'Session.ReceiveReplyAsync' reuses a shared buffer; make a copy so that the column definitions can always be safely read at any future point
-						if (m_columnDefinitionPayloadUsedBytes + payloadLength > m_columnDefinitionPayloads.Count)
-							Utility.Resize(ref m_columnDefinitionPayloads, m_columnDefinitionPayloadUsedBytes + payloadLength);
-						payload.Span.CopyTo(m_columnDefinitionPayloads.AsSpan(m_columnDefinitionPayloadUsedBytes));
+							// if there was a prepared statement, update its cached columns too
+							if (preparedColumns is not null)
+								ColumnDefinitionPayload.Initialize(ref preparedColumns[column], payloadBytesSegment);
 
-						var columnDefinition = ColumnDefinitionPayload.Create(new ResizableArraySegment<byte>(m_columnDefinitionPayloads, m_columnDefinitionPayloadUsedBytes, payloadLength));
-						ColumnDefinitions[column] = columnDefinition;
-						ColumnTypes[column] = TypeMapper.ConvertToMySqlDbType(columnDefinition, treatTinyAsBoolean: Connection.TreatTinyAsBoolean, guidFormat: Connection.GuidFormat);
-						m_columnDefinitionPayloadUsedBytes += payloadLength;
+							m_columnDefinitionPayloadUsedBytes += payloadLength;
+						}
 					}
 
 					if (!Session.SupportsDeprecateEof)
 					{
 						payload = await Session.ReceiveReplyAsync(ioBehavior, CancellationToken.None).ConfigureAwait(false);
-						EofPayload.Create(payload.Span);
+						_ = EofPayload.Create(payload.Span);
 					}
 
 					if (ColumnDefinitions.Length == (Command?.OutParameters?.Count + 1) && ColumnDefinitions[0].Name == SingleCommandPayloadCreator.OutParameterSentinelColumnName)
 						ContainsCommandParameters = true;
 					WarningCount = 0;
 					State = ResultSetState.ReadResultSetHeader;
-					if (DataReader.Activity is { IsAllDataRequested: true })
+					if (DataReader.Activity is { IsAllDataRequested: true } && (Command?.Connection!.MySqlDataSource?.TracingOptions.EnableResultSetHeaderEvent ?? MySqlConnectorTracingOptions.Default.EnableResultSetHeaderEvent))
 						DataReader.Activity.AddEvent(new ActivityEvent("read-result-set-header"));
 					break;
 				}
@@ -174,16 +182,13 @@ static int ReadColumnCount(ReadOnlySpan<byte> span)
 		}
 	}
 
-	private static bool IsHostVerified(MySqlConnection connection)
-	{
-		return connection.SslMode == MySqlSslMode.VerifyCA
-			|| connection.SslMode == MySqlSslMode.VerifyFull;
-	}
+	private static bool IsHostVerified(MySqlConnection connection) =>
+		connection.SslMode is MySqlSslMode.VerifyCA or MySqlSslMode.VerifyFull;
 
 	public async Task ReadEntireAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
 		while (State is ResultSetState.ReadingRows or ResultSetState.ReadResultSetHeader)
-			await ReadAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+			_ = await ReadAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
 	}
 
 	public bool Read()
@@ -224,62 +229,48 @@ public async Task<bool> ReadAsync(IOBehavior ioBehavior, CancellationToken cance
 		return row;
 	}
 
-	private ValueTask<Row?> ScanRowAsync(IOBehavior ioBehavior, Row? row, CancellationToken cancellationToken)
+	private async ValueTask<Row?> ScanRowAsync(IOBehavior ioBehavior, Row? row, CancellationToken cancellationToken)
 	{
 		// if we've already read past the end of this resultset, Read returns false
 		if (BufferState is ResultSetState.HasMoreData or ResultSetState.NoMoreData or ResultSetState.None)
-			return new ValueTask<Row?>(default(Row?));
+			return null;
 
-		using var registration = Command.CancellableCommand.RegisterCancel(cancellationToken);
-		var payloadValueTask = Session.ReceiveReplyAsync(ioBehavior, CancellationToken.None);
-		return payloadValueTask.IsCompletedSuccessfully
-			? new ValueTask<Row?>(ScanRowAsyncRemainder(this, payloadValueTask.Result, row))
-			: new ValueTask<Row?>(ScanRowAsyncAwaited(this, payloadValueTask.AsTask(), row, cancellationToken));
+		PayloadData payload;
+		try
+		{
+			payload = await Session.ReceiveReplyAsync(ioBehavior, CancellationToken.None).ConfigureAwait(false);
+		}
+		catch (MySqlException ex)
+		{
+			BufferState = State = ResultSetState.NoMoreData;
+			if (ex.ErrorCode == MySqlErrorCode.QueryInterrupted && cancellationToken.IsCancellationRequested)
+				throw new OperationCanceledException(ex.Message, ex, cancellationToken);
+			if (ex.ErrorCode == MySqlErrorCode.QueryInterrupted && Command.CancellableCommand.IsTimedOut)
+				throw MySqlException.CreateForTimeout(ex);
+			throw;
+		}
 
-		static async Task<Row?> ScanRowAsyncAwaited(ResultSet resultSet, Task<PayloadData> payloadTask, Row? row, CancellationToken token)
+		if (payload.HeaderByte == EofPayload.Signature)
 		{
-			PayloadData payloadData;
-			try
+			if (Session.SupportsDeprecateEof && OkPayload.IsOk(payload.Span, Session))
 			{
-				payloadData = await payloadTask.ConfigureAwait(false);
+				var ok = OkPayload.Create(payload.Span, Session);
+				BufferState = (ok.ServerStatus & ServerStatus.MoreResultsExist) == 0 ? ResultSetState.NoMoreData : ResultSetState.HasMoreData;
+				return null;
 			}
-			catch (MySqlException ex)
+			if (!Session.SupportsDeprecateEof && EofPayload.IsEof(payload))
 			{
-				resultSet.BufferState = resultSet.State = ResultSetState.NoMoreData;
-				if (ex.ErrorCode == MySqlErrorCode.QueryInterrupted && token.IsCancellationRequested)
-					throw new OperationCanceledException(ex.Message, ex, token);
-				if (ex.ErrorCode == MySqlErrorCode.QueryInterrupted && resultSet.Command.CancellableCommand.IsTimedOut)
-					throw MySqlException.CreateForTimeout(ex);
-				throw;
+				var eof = EofPayload.Create(payload.Span);
+				BufferState = (eof.ServerStatus & ServerStatus.MoreResultsExist) == 0 ? ResultSetState.NoMoreData : ResultSetState.HasMoreData;
+				return null;
 			}
-			return ScanRowAsyncRemainder(resultSet, payloadData, row);
 		}
 
-		static Row? ScanRowAsyncRemainder(ResultSet resultSet, PayloadData payload, Row? row)
-		{
-			if (payload.HeaderByte == EofPayload.Signature)
-			{
-				var span = payload.Span;
-				if (resultSet.Session.SupportsDeprecateEof && OkPayload.IsOk(span, resultSet.Session.SupportsDeprecateEof))
-				{
-					var ok = OkPayload.Create(span, resultSet.Session.SupportsDeprecateEof, resultSet.Session.SupportsSessionTrack);
-					resultSet.BufferState = (ok.ServerStatus & ServerStatus.MoreResultsExist) == 0 ? ResultSetState.NoMoreData : ResultSetState.HasMoreData;
-					return null;
-				}
-				if (!resultSet.Session.SupportsDeprecateEof && EofPayload.IsEof(payload))
-				{
-					var eof = EofPayload.Create(span);
-					resultSet.BufferState = (eof.ServerStatus & ServerStatus.MoreResultsExist) == 0 ? ResultSetState.NoMoreData : ResultSetState.HasMoreData;
-					return null;
-				}
-			}
-
-			row ??= resultSet.Command.TryGetPreparedStatements() is null ? new TextRow(resultSet) : new BinaryRow(resultSet);
-			row.SetData(payload.Memory);
-			resultSet.m_hasRows = true;
-			resultSet.BufferState = ResultSetState.ReadingRows;
-			return row;
-		}
+		row ??= new Row(Command.TryGetPreparedStatements() is not null, this);
+		row.SetData(payload.Memory);
+		m_hasRows = true;
+		BufferState = ResultSetState.ReadingRows;
+		return row;
 	}
 
 #pragma warning disable CA1822 // Mark members as static
@@ -290,7 +281,7 @@ public async Task<bool> ReadAsync(IOBehavior ioBehavior, CancellationToken cance
 
 	public string GetName(int ordinal)
 	{
-		if (ColumnDefinitions is null)
+		if (!HasResultSet)
 			throw new InvalidOperationException("There is no current result set.");
 		if (ordinal < 0 || ordinal >= ColumnDefinitions.Length)
 			throw new IndexOutOfRangeException($"value must be between 0 and {ColumnDefinitions.Length - 1}");
@@ -299,12 +290,12 @@ public string GetName(int ordinal)
 
 	public string GetDataTypeName(int ordinal)
 	{
-		if (ColumnDefinitions is null)
+		if (!HasResultSet)
 			throw new InvalidOperationException("There is no current result set.");
 		if (ordinal < 0 || ordinal >= ColumnDefinitions.Length)
 			throw new IndexOutOfRangeException($"value must be between 0 and {ColumnDefinitions.Length - 1}");
 
-		var mySqlDbType = ColumnTypes![ordinal];
+		var mySqlDbType = GetColumnType(ordinal);
 		if (mySqlDbType == MySqlDbType.String)
 			return string.Format(CultureInfo.InvariantCulture, "CHAR({0})", ColumnDefinitions[ordinal].ColumnLength / ProtocolUtility.GetBytesPerCharacter(ColumnDefinitions[ordinal].CharacterSet));
 		return TypeMapper.Instance.GetColumnTypeMetadata(mySqlDbType).SimpleDataTypeName;
@@ -312,18 +303,21 @@ public string GetDataTypeName(int ordinal)
 
 	public Type GetFieldType(int ordinal)
 	{
-		if (ColumnDefinitions is null)
+		if (!HasResultSet)
 			throw new InvalidOperationException("There is no current result set.");
 		if (ordinal < 0 || ordinal >= ColumnDefinitions.Length)
 			throw new IndexOutOfRangeException($"value must be between 0 and {ColumnDefinitions.Length - 1}");
 
-		var type = TypeMapper.Instance.GetColumnTypeMetadata(ColumnTypes![ordinal]).DbTypeMapping.ClrType;
+		var type = TypeMapper.Instance.GetColumnTypeMetadata(GetColumnType(ordinal)).DbTypeMapping.ClrType;
 		if (Connection.AllowZeroDateTime && type == typeof(DateTime))
 			type = typeof(MySqlDateTime);
 		return type;
 	}
 
-	public int FieldCount => ColumnDefinitions?.Length ?? 0;
+	public MySqlDbType GetColumnType(int ordinal) =>
+		TypeMapper.ConvertToMySqlDbType(ColumnDefinitions[ordinal], Connection.TreatTinyAsBoolean, Connection.GuidFormat);
+
+	public int FieldCount => ColumnDefinitions.Length;
 
 	public bool HasRows
 	{
@@ -337,9 +331,13 @@ public bool HasRows
 
 	public int GetOrdinal(string name)
 	{
+#if NET6_0_OR_GREATER
+		ArgumentNullException.ThrowIfNull(name);
+#else
 		if (name is null)
 			throw new ArgumentNullException(nameof(name));
-		if (ColumnDefinitions is null)
+#endif
+		if (!HasResultSet)
 			throw new InvalidOperationException("There is no current result set.");
 
 		for (var column = 0; column < ColumnDefinitions.Length; column++)
@@ -358,22 +356,24 @@ public Row GetCurrentRow()
 		return m_row ?? throw new InvalidOperationException("There is no current row.");
 	}
 
-	public MySqlDataReader DataReader { get; }
+	public MySqlDataReader DataReader { get; } = dataReader;
 	public ExceptionDispatchInfo? ReadResultSetHeaderException { get; private set; }
 	public IMySqlCommand Command => DataReader.Command!;
 	public MySqlConnection Connection => DataReader.Connection!;
 	public ServerSession Session => DataReader.Session!;
 
 	public ResultSetState BufferState { get; private set; }
-	public ColumnDefinitionPayload[]? ColumnDefinitions { get; private set; }
-	public MySqlDbType[]? ColumnTypes { get; private set; }
+	public ReadOnlySpan<ColumnDefinitionPayload> ColumnDefinitions => m_columnDefinitions.Span;
 	public int WarningCount { get; private set; }
 	public ResultSetState State { get; private set; }
+	public bool HasResultSet => !(State == ResultSetState.None || ColumnDefinitions.Length == 0);
 	public bool ContainsCommandParameters { get; private set; }
 
-	private ResizableArray<byte>? m_columnDefinitionPayloads;
+	private ResizableArray<byte>? m_columnDefinitionPayloadBytes;
 	private int m_columnDefinitionPayloadUsedBytes;
 	private Queue<Row>? m_readBuffer;
 	private Row? m_row;
 	private bool m_hasRows;
+	private ReadOnlyMemory<ColumnDefinitionPayload> m_columnDefinitions;
+	private ColumnDefinitionPayload[]? m_columnDefinitionPayloadCache;
 }
diff --git a/src/MySqlConnector/Core/Row.cs b/src/MySqlConnector/Core/Row.cs
index d4661f245..a9f424c9d 100644
--- a/src/MySqlConnector/Core/Row.cs
+++ b/src/MySqlConnector/Core/Row.cs
@@ -1,41 +1,97 @@
-using System.Buffers.Text;
-using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
 using System.Text;
+using MySqlConnector.ColumnReaders;
 using MySqlConnector.Protocol;
-using MySqlConnector.Protocol.Payloads;
+using MySqlConnector.Protocol.Serialization;
+#if !NETCOREAPP2_1_OR_GREATER && !NETSTANDARD2_1_OR_GREATER
 using MySqlConnector.Utilities;
+#endif
 
 namespace MySqlConnector.Core;
 
-internal abstract class Row
+internal sealed class Row
 {
-	public void SetData(ReadOnlyMemory<byte> data)
+	public Row(bool isBinary, ResultSet resultSet)
 	{
-		m_data = data;
-		GetDataOffsets(m_data.Span, m_dataOffsets, m_dataLengths!);
+		m_isBinary = isBinary;
+		ResultSet = resultSet;
+
+		var columnDefinitions = ResultSet.ColumnDefinitions!;
+		m_dataOffsetLengths = new OffsetLength[columnDefinitions.Length];
+		m_columnReaders = new ColumnReader[columnDefinitions.Length];
+		for (var column = 0; column < m_columnReaders.Length; column++)
+			m_columnReaders[column] = ColumnReader.Create(isBinary, columnDefinitions[column], resultSet.Connection);
 	}
 
-	public Row Clone()
+	public void SetData(ReadOnlyMemory<byte> data)
 	{
-		var clonedRow = CloneCore();
-		var clonedData = new byte[m_data.Length];
-		m_data.CopyTo(clonedData);
-		clonedRow.SetData(clonedData);
-		return clonedRow;
+		m_data = data;
+		if (m_isBinary)
+		{
+			Array.Clear(m_dataOffsetLengths, 0, m_dataOffsetLengths.Length);
+			for (var column = 0; column < m_dataOffsetLengths.Length; column++)
+			{
+				if ((data.Span[((column + 2) / 8) + 1] & (1 << ((column + 2) % 8))) != 0)
+				{
+					// column is NULL
+					m_dataOffsetLengths[column] = (-1, 0);
+				}
+			}
+
+			var reader = new ByteArrayReader(data.Span);
+
+			// skip packet header (1 byte) and NULL bitmap (formula for length at https://dev.mysql.com/doc/internals/en/null-bitmap.html)
+			reader.Offset += 1 + ((m_dataOffsetLengths.Length + 7 + 2) / 8);
+			for (var column = 0; column < m_dataOffsetLengths.Length; column++)
+			{
+				if (m_dataOffsetLengths[column].Offset != -1)
+				{
+					var columnDefinition = ResultSet.ColumnDefinitions![column];
+					var length = columnDefinition.ColumnType switch
+					{
+						ColumnType.Longlong or ColumnType.Double => 8,
+						ColumnType.Long or ColumnType.Int24 or ColumnType.Float => 4,
+						ColumnType.Short or ColumnType.Year => 2,
+						ColumnType.Tiny => 1,
+						ColumnType.Date or ColumnType.DateTime or ColumnType.NewDate or ColumnType.Timestamp or ColumnType.Time => reader.ReadByte(),
+						ColumnType.DateTime2 or ColumnType.Timestamp2 => throw new NotSupportedException($"ColumnType {columnDefinition.ColumnType} is not supported"),
+						_ => checked((int) reader.ReadLengthEncodedInteger()),
+					};
+
+					m_dataOffsetLengths[column] = (reader.Offset, length);
+					reader.Offset += length;
+				}
+			}
+		}
+		else
+		{
+			var reader = new ByteArrayReader(data.Span);
+			for (var column = 0; column < m_dataOffsetLengths.Length; column++)
+			{
+				var length = reader.ReadLengthEncodedIntegerOrNull();
+				m_dataOffsetLengths[column] = length == -1 ? (-1, 0) : (reader.Offset, length);
+				reader.Offset += m_dataOffsetLengths[column].Length;
+			}
+		}
 	}
 
 	public object GetValue(int ordinal)
 	{
+#if NET8_0_OR_GREATER
+		ArgumentOutOfRangeException.ThrowIfNegative(ordinal);
+		ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(ordinal, ResultSet.ColumnDefinitions!.Length);
+#else
 		if (ordinal < 0 || ordinal >= ResultSet.ColumnDefinitions!.Length)
 			throw new ArgumentOutOfRangeException(nameof(ordinal), $"value must be between 0 and {ResultSet.ColumnDefinitions!.Length - 1}");
+#endif
 
-		if (m_dataOffsets[ordinal] == -1)
+		if (m_dataOffsetLengths[ordinal].Offset == -1)
 			return DBNull.Value;
 
-		var data = m_data.Slice(m_dataOffsets[ordinal], m_dataLengths[ordinal]).Span;
+		var (offset, length) = m_dataOffsetLengths[ordinal];
+		var data = m_data.Slice(offset, length).Span;
 		var columnDefinition = ResultSet.ColumnDefinitions[ordinal];
-		return GetValueCore(data, columnDefinition);
+		return m_columnReaders[ordinal].ReadValue(data, columnDefinition);
 	}
 
 	public bool GetBoolean(int ordinal)
@@ -103,15 +159,15 @@ public long GetBytes(int ordinal, long dataOffset, byte[]? buffer, int bufferOff
 		{
 			// this isn't required by the DbDataReader.GetBytes API documentation, but is what mysql-connector-net does
 			// (as does SqlDataReader: http://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqldatareader.getbytes.aspx)
-			return m_dataLengths[ordinal];
+			return m_dataOffsetLengths[ordinal].Length;
 		}
 
 		CheckBufferArguments(dataOffset, buffer, bufferOffset, length);
 
 		var offset = (int) dataOffset;
-		var lengthToCopy = Math.Max(0, Math.Min(m_dataLengths[ordinal] - offset, length));
+		var lengthToCopy = Math.Max(0, Math.Min(m_dataOffsetLengths[ordinal].Length - offset, length));
 		if (lengthToCopy > 0)
-			m_data.Slice(m_dataOffsets[ordinal] + offset, lengthToCopy).Span.CopyTo(buffer.AsSpan(bufferOffset));
+			m_data.Slice(m_dataOffsetLengths[ordinal].Offset + offset, lengthToCopy).Span.CopyTo(buffer.AsSpan(bufferOffset));
 		return lengthToCopy;
 	}
 
@@ -146,7 +202,14 @@ public Guid GetGuid(int ordinal)
 			return guid;
 
 		if (value is byte[] { Length: 16 } bytes)
-			return CreateGuidFromBytes(Connection.GuidFormat, bytes);
+		{
+			return Connection.GuidFormat switch
+			{
+				MySqlGuidFormat.Binary16 => GuidBinary16ColumnReader.ReadGuid(bytes),
+				MySqlGuidFormat.TimeSwapBinary16 => GuidTimeSwapBinary16ColumnReader.ReadGuid(bytes),
+				_ => GuidLittleEndianBinary16ColumnReader.ReadGuid(bytes),
+			};
+		}
 
 		return (Guid) value;
 	}
@@ -172,41 +235,19 @@ public short GetInt16(int ordinal)
 
 	public int GetInt32(int ordinal)
 	{
-		if (ordinal < 0 || ordinal >= ResultSet.ColumnDefinitions!.Length || m_dataOffsets[ordinal] == -1)
-			return (int) GetValue(ordinal);
+		if (ordinal < 0 || ordinal >= ResultSet.ColumnDefinitions!.Length)
+			throw new ArgumentOutOfRangeException(nameof(ordinal), $"value must be between 0 and {ResultSet.ColumnDefinitions!.Length - 1}");
+		if (m_dataOffsetLengths[ordinal].Offset == -1)
+			throw new InvalidCastException("Can't convert NULL to Int32");
 
+		var (offset, length) = m_dataOffsetLengths[ordinal];
+		var data = m_data.Slice(offset, length).Span;
 		var columnDefinition = ResultSet.ColumnDefinitions[ordinal];
-		if (columnDefinition.ColumnType is ColumnType.Decimal or ColumnType.NewDecimal)
-		{
-			return (int) (decimal) GetValue(ordinal);
-		}
-		else if (columnDefinition.ColumnType is not ColumnType.Tiny and not ColumnType.Short
-			and not ColumnType.Int24 and not ColumnType.Long and not ColumnType.Longlong
-			and not ColumnType.Bit and not ColumnType.Year)
-		{
-			throw new InvalidCastException($"Can't convert {ResultSet.ColumnTypes![ordinal]} to Int32");
-		}
-
-		var data = m_data.Slice(m_dataOffsets[ordinal], m_dataLengths[ordinal]).Span;
-
-		if (columnDefinition.ColumnType == ColumnType.Bit)
-			return checked((int) ReadBit(data, columnDefinition));
-
-		var result = GetInt32Core(data, columnDefinition);
-		if (columnDefinition.ColumnType == ColumnType.Tiny &&
-			Connection.TreatTinyAsBoolean &&
-			columnDefinition.ColumnLength == 1 &&
-			(columnDefinition.ColumnFlags & ColumnFlags.Unsigned) == 0 &&
-			result != 0)
-		{
-			// coerce all non-zero TINYINT(1) results to 1, since it represents a BOOL value
-			result = 1;
-		}
-		return result;
+		if (m_columnReaders[ordinal].TryReadInt32(data, columnDefinition) is { } value)
+			return value;
+		throw new InvalidCastException($"Can't convert {ResultSet.GetColumnType(ordinal)} to Int32");
 	}
 
-	protected abstract int GetInt32Core(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition);
-
 	public long GetInt64(int ordinal)
 	{
 		var value = GetValue(ordinal);
@@ -291,7 +332,7 @@ public DateTime GetDateTime(int ordinal)
 		{
 			// slightly inefficient to roundtrip the bytes through a string, but this is assumed to be an infrequent code path; this could be optimised to reprocess the original bytes
 			if (dateString.Length is >= 10 and <= 26)
-				value = ParseDateTime(Encoding.UTF8.GetBytes(dateString));
+				value = TextDateTimeColumnReader.ParseDateTime(Encoding.UTF8.GetBytes(dateString), Connection.ConvertZeroDateTime, Connection.AllowZeroDateTime, Connection.DateTimeKind);
 			else
 				throw new FormatException($"Couldn't interpret value as a valid DateTime: {value}");
 		}
@@ -301,13 +342,14 @@ public DateTime GetDateTime(int ordinal)
 		return (DateTime) value;
 	}
 
-	public DateTimeOffset GetDateTimeOffset(int ordinal) => new DateTimeOffset(DateTime.SpecifyKind(GetDateTime(ordinal), DateTimeKind.Utc));
+	public DateTimeOffset GetDateTimeOffset(int ordinal) => new(DateTime.SpecifyKind(GetDateTime(ordinal), DateTimeKind.Utc));
 
 	public Stream GetStream(int ordinal)
 	{
 		CheckBinaryColumn(ordinal);
+		var (offset, length) = m_dataOffsetLengths[ordinal];
 		return MemoryMarshal.TryGetArray(m_data, out var arraySegment) ?
-			new MemoryStream(arraySegment.Array!, arraySegment.Offset + m_dataOffsets[ordinal], m_dataLengths[ordinal], writable: false) :
+			new MemoryStream(arraySegment.Array!, arraySegment.Offset + offset, length, writable: false) :
 			throw new InvalidOperationException("Can't get underlying array.");
 	}
 
@@ -346,7 +388,7 @@ public float GetFloat(int ordinal)
 		{
 			float floatValue => floatValue,
 			double doubleValue when doubleValue is >= float.MinValue and <= float.MaxValue => (float) doubleValue,
-			double _ => throw new InvalidCastException("The value cannot be safely cast to Single."),
+			double => throw new InvalidCastException("The value cannot be safely cast to Single."),
 			decimal decimalValue => (float) decimalValue,
 			_ => (float) value,
 		};
@@ -372,7 +414,8 @@ public MySqlDecimal GetMySqlDecimal(int ordinal)
 	{
 		if (IsDBNull(ordinal))
 			return (MySqlDecimal) GetValue(ordinal);
-		var data = m_data.Slice(m_dataOffsets[ordinal], m_dataLengths[ordinal]).Span;
+		var (offset, length) = m_dataOffsetLengths[ordinal];
+		var data = m_data.Slice(offset, length).Span;
 		var columnType = ResultSet.ColumnDefinitions![ordinal].ColumnType;
 		if (columnType is ColumnType.NewDecimal or ColumnType.Decimal)
 			return new MySqlDecimal(Encoding.UTF8.GetString(data));
@@ -381,160 +424,30 @@ public MySqlDecimal GetMySqlDecimal(int ordinal)
 
 	public int GetValues(object[] values)
 	{
-		int count = Math.Min((values ?? throw new ArgumentNullException(nameof(values))).Length, ResultSet.ColumnDefinitions!.Length);
+#if NET6_0_OR_GREATER
+		ArgumentNullException.ThrowIfNull(values);
+#else
+		if (values is null)
+			throw new ArgumentNullException(nameof(values));
+#endif
+		int count = Math.Min(values.Length, ResultSet.ColumnDefinitions!.Length);
 		for (int i = 0; i < count; i++)
 			values[i] = GetValue(i);
 		return count;
 	}
 
-	public bool IsDBNull(int ordinal) => m_dataOffsets[ordinal] == -1;
+	public bool IsDBNull(int ordinal) => m_dataOffsetLengths[ordinal].Offset == -1;
 
 	public object this[int ordinal] => GetValue(ordinal);
 
 	public object this[string name] => GetValue(ResultSet.GetOrdinal(name));
 
-	protected Row(ResultSet resultSet)
-	{
-		ResultSet = resultSet;
-		m_dataOffsets = new int[ResultSet.ColumnDefinitions!.Length];
-		m_dataLengths = new int[ResultSet.ColumnDefinitions.Length];
-	}
-
-	protected abstract Row CloneCore();
-
-	protected abstract object GetValueCore(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition);
-
-	protected abstract void GetDataOffsets(ReadOnlySpan<byte> data, int[] dataOffsets, int[] dataLengths);
-
-	protected ResultSet ResultSet { get; }
-	protected MySqlConnection Connection => ResultSet.Connection;
-
-	protected object ParseDateTime(ReadOnlySpan<byte> value)
-	{
-		Exception? exception = null;
-		if (!Utf8Parser.TryParse(value, out int year, out var bytesConsumed) || bytesConsumed != 4)
-			goto InvalidDateTime;
-		if (value.Length < 5 || value[4] != 45)
-			goto InvalidDateTime;
-		if (!Utf8Parser.TryParse(value[5..], out int month, out bytesConsumed) || bytesConsumed != 2)
-			goto InvalidDateTime;
-		if (value.Length < 8 || value[7] != 45)
-			goto InvalidDateTime;
-		if (!Utf8Parser.TryParse(value[8..], out int day, out bytesConsumed) || bytesConsumed != 2)
-			goto InvalidDateTime;
-
-		if (year == 0 && month == 0 && day == 0)
-		{
-			if (Connection.ConvertZeroDateTime)
-				return DateTime.MinValue;
-			if (Connection.AllowZeroDateTime)
-				return default(MySqlDateTime);
-			throw new InvalidCastException("Unable to convert MySQL date/time to System.DateTime, set AllowZeroDateTime=True or ConvertZeroDateTime=True in the connection string. See https://mysqlconnector.net/connection-options/");
-		}
-
-		int hour, minute, second, microseconds;
-		if (value.Length == 10)
-		{
-			hour = 0;
-			minute = 0;
-			second = 0;
-			microseconds = 0;
-		}
-		else
-		{
-			if (value[10] != 32)
-				goto InvalidDateTime;
-			if (!Utf8Parser.TryParse(value[11..], out hour, out bytesConsumed) || bytesConsumed != 2)
-				goto InvalidDateTime;
-			if (value.Length < 14 || value[13] != 58)
-				goto InvalidDateTime;
-			if (!Utf8Parser.TryParse(value[14..], out minute, out bytesConsumed) || bytesConsumed != 2)
-				goto InvalidDateTime;
-			if (value.Length < 17 || value[16] != 58)
-				goto InvalidDateTime;
-			if (!Utf8Parser.TryParse(value[17..], out second, out bytesConsumed) || bytesConsumed != 2)
-				goto InvalidDateTime;
-
-			if (value.Length == 19)
-			{
-				microseconds = 0;
-			}
-			else
-			{
-				if (value[19] != 46)
-					goto InvalidDateTime;
-
-				if (!Utf8Parser.TryParse(value[20..], out microseconds, out bytesConsumed) || bytesConsumed != value.Length - 20)
-					goto InvalidDateTime;
-				for (; bytesConsumed < 6; bytesConsumed++)
-					microseconds *= 10;
-			}
-		}
-
-		try
-		{
-			return Connection.AllowZeroDateTime ? (object) new MySqlDateTime(year, month, day, hour, minute, second, microseconds) :
-#if NET7_0_OR_GREATER
-				new DateTime(year, month, day, hour, minute, second, microseconds / 1000, microseconds % 1000, Connection.DateTimeKind);
-#else
-				new DateTime(year, month, day, hour, minute, second, microseconds / 1000, Connection.DateTimeKind).AddTicks(microseconds % 1000 * 10);
-#endif
-		}
-		catch (Exception ex)
-		{
-			exception = ex;
-		}
-
-InvalidDateTime:
-		throw new FormatException($"Couldn't interpret value as a valid DateTime: {Encoding.UTF8.GetString(value)}", exception);
-	}
-
-#if NET5_0_OR_GREATER
-	[SkipLocalsInit]
-#endif
-	protected static unsafe Guid CreateGuidFromBytes(MySqlGuidFormat guidFormat, ReadOnlySpan<byte> bytes) =>
-		guidFormat switch
-		{
-#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
-			MySqlGuidFormat.Binary16 => new Guid(stackalloc byte[16] { bytes[3], bytes[2], bytes[1], bytes[0], bytes[5], bytes[4], bytes[7], bytes[6], bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15] }),
-			MySqlGuidFormat.TimeSwapBinary16 => new Guid(stackalloc byte[16] { bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2], bytes[1], bytes[0], bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15] }),
-			_ => new Guid(bytes),
-#else
-			MySqlGuidFormat.Binary16 => new Guid(new[] { bytes[3], bytes[2], bytes[1], bytes[0], bytes[5], bytes[4], bytes[7], bytes[6], bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15] }),
-			MySqlGuidFormat.TimeSwapBinary16 => new Guid(new[] { bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2], bytes[1], bytes[0], bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15] }),
-			_ => new Guid(bytes.ToArray()),
-#endif
-		};
-
-	protected static ulong ReadBit(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition)
-	{
-		if ((columnDefinition.ColumnFlags & ColumnFlags.Binary) == 0)
-		{
-			// when the Binary flag IS NOT set, the BIT column is transmitted as MSB byte array
-			ulong bitValue = 0;
-			for (int i = 0; i < data.Length; i++)
-				bitValue = bitValue * 256 + data[i];
-			return bitValue;
-		}
-		else if (columnDefinition.ColumnLength <= 5 && data.Length == 1 && data[0] < (byte) (1 << (int) columnDefinition.ColumnLength))
-		{
-			// a server bug may return the data as binary even when we expect text: https://github.com/mysql-net/MySqlConnector/issues/713
-			// in this case, the data can't possibly be an ASCII digit, so assume it's the binary serialisation of BIT(n) where n <= 5
-			return (ulong) data[0];
-		}
-		else
-		{
-			// when the Binary flag IS set, the BIT column is transmitted as text
-			return ParseUInt64(data);
-		}
-	}
-
-	protected static ulong ParseUInt64(ReadOnlySpan<byte> data) =>
-		!Utf8Parser.TryParse(data, out ulong value, out var bytesConsumed) || bytesConsumed != data.Length ? throw new FormatException() : value;
+	private ResultSet ResultSet { get; }
+	private MySqlConnection Connection => ResultSet.Connection;
 
 	private void CheckBinaryColumn(int ordinal)
 	{
-		if (m_dataOffsets[ordinal] == -1)
+		if (m_dataOffsetLengths[ordinal].Offset == -1)
 			throw new InvalidCastException("Column is NULL.");
 
 		var column = ResultSet.ColumnDefinitions![ordinal];
@@ -542,7 +455,7 @@ private void CheckBinaryColumn(int ordinal)
 		if ((column.ColumnFlags & ColumnFlags.Binary) == 0 ||
 			(columnType != ColumnType.String && columnType != ColumnType.VarString && columnType != ColumnType.TinyBlob &&
 			columnType != ColumnType.Blob && columnType != ColumnType.MediumBlob && columnType != ColumnType.LongBlob &&
-			columnType != ColumnType.Geometry))
+			columnType != ColumnType.Geometry && columnType != ColumnType.Vector))
 		{
 			throw new InvalidCastException($"Can't convert {columnType} to bytes.");
 		}
@@ -550,6 +463,13 @@ private void CheckBinaryColumn(int ordinal)
 
 	private static void CheckBufferArguments<T>(long dataOffset, T[] buffer, int bufferOffset, int length)
 	{
+#if NET8_0_OR_GREATER
+		ArgumentOutOfRangeException.ThrowIfNegative(dataOffset);
+		ArgumentOutOfRangeException.ThrowIfGreaterThan(dataOffset, int.MaxValue);
+		ArgumentOutOfRangeException.ThrowIfNegative(length);
+		ArgumentOutOfRangeException.ThrowIfNegative(bufferOffset);
+		ArgumentOutOfRangeException.ThrowIfGreaterThan(bufferOffset, buffer.Length);
+#else
 		if (dataOffset < 0)
 			throw new ArgumentOutOfRangeException(nameof(dataOffset), dataOffset, nameof(dataOffset) + " must be non-negative");
 		if (dataOffset > int.MaxValue)
@@ -560,11 +480,27 @@ private static void CheckBufferArguments<T>(long dataOffset, T[] buffer, int buf
 			throw new ArgumentOutOfRangeException(nameof(bufferOffset), bufferOffset, nameof(bufferOffset) + " must be non-negative");
 		if (bufferOffset > buffer.Length)
 			throw new ArgumentOutOfRangeException(nameof(bufferOffset), bufferOffset, nameof(bufferOffset) + " must be within the buffer");
+#endif
 		if (checked(bufferOffset + length) > buffer.Length)
 			throw new ArgumentException(nameof(bufferOffset) + " + " + nameof(length) + " cannot exceed " + nameof(buffer) + "." + nameof(buffer.Length), nameof(length));
 	}
 
-	private readonly int[] m_dataOffsets;
-	private readonly int[] m_dataLengths;
+	private readonly struct OffsetLength(int offset, int length)
+	{
+		public static implicit operator OffsetLength((int Offset, int Length) x) => new(x.Offset, x.Length);
+
+		public void Deconstruct(out int offset, out int length)
+		{
+			offset = Offset;
+			length = Length;
+		}
+
+		public int Offset { get; } = offset;
+		public int Length { get; } = length;
+	}
+
+	private readonly bool m_isBinary;
+	private readonly OffsetLength[] m_dataOffsetLengths;
+	private readonly ColumnReader[] m_columnReaders;
 	private ReadOnlyMemory<byte> m_data;
 }
diff --git a/src/MySqlConnector/Core/SchemaProvider.cs b/src/MySqlConnector/Core/SchemaProvider.cs
index 2617d63e6..a5a8b74be 100644
--- a/src/MySqlConnector/Core/SchemaProvider.cs
+++ b/src/MySqlConnector/Core/SchemaProvider.cs
@@ -1,22 +1,19 @@
+#if NET6_0_OR_GREATER
 using System.Globalization;
+#endif
 using MySqlConnector.Protocol.Serialization;
 
 namespace MySqlConnector.Core;
 
-internal sealed partial class SchemaProvider
+internal sealed partial class SchemaProvider(MySqlConnection connection)
 {
-	public SchemaProvider(MySqlConnection connection)
-	{
-		m_connection = connection;
-	}
-
 	private void DoFillDataSourceInformation(DataTable dataTable)
 	{
 		var row = dataTable.NewRow();
 		row["CompositeIdentifierSeparatorPattern"] = @"\.";
 		row["DataSourceProductName"] = "MySQL";
-		row["DataSourceProductVersion"] = m_connection.ServerVersion;
-		row["DataSourceProductVersionNormalized"] = GetVersion(m_connection.Session.ServerVersion.Version);
+		row["DataSourceProductVersion"] = connection.ServerVersion;
+		row["DataSourceProductVersionNormalized"] = GetVersion(connection.Session.ServerVersion.Version);
 		row["GroupByBehavior"] = GroupByBehavior.Unrelated;
 		row["IdentifierPattern"] = @"(^\[\p{Lo}\p{Lu}\p{Ll}_@#][\p{Lo}\p{Lu}\p{Ll}\p{Nd}@$#_]*$)|(^\[[^\]\0]|\]\]+\]$)|(^\""[^\""\0]|\""\""+\""$)";
 		row["IdentifierCase"] = IdentifierCase.Insensitive;
@@ -103,12 +100,12 @@ private static void DoFillReservedWords(DataTable dataTable)
 	{
 		// Note:
 		// For MySQL 8.0, the INFORMATION_SCHEMA.KEYWORDS table could be used to load the list at runtime,
-		// unfortunately this bug https://bugs.mysql.com/bug.php?id=90160 makes it impratical to do it
+		// unfortunately this bug https://bugs.mysql.com/bug.php?id=90160 makes it impractical to do it
 		// (the bug is marked as fixed in MySQL 8.0.13, not published yet at the time of writing this note).
 		//
 		// Note:
 		// Once the previously mentioned bug will be fixed, for versions >= 8.0.13 reserved words could be
-		// loaded at runtime form INFORMATION_SCHEMA.KEYWORDS, and for other versions the hard coded list
+		// loaded at runtime from INFORMATION_SCHEMA.KEYWORDS, and for other versions the hard coded list
 		// could be used (notice the list could change with the release, adopting the 8.0.12 list is a
 		// suboptimal one-size-fits-it-all solution.
 		// To get the current MySQL version at runtime one could query SELECT VERSION(); which returns a
@@ -116,8 +113,8 @@ private static void DoFillReservedWords(DataTable dataTable)
 		// (but has a higher version number)
 
 		// select word from information_schema.keywords where reserved = 1; on MySQL Server 8.0.18
-		var reservedWords = new[]
-		{
+		string[] reservedWords =
+		[
 			"ACCESSIBLE",
 			"ADD",
 			"ALL",
@@ -380,54 +377,62 @@ private static void DoFillReservedWords(DataTable dataTable)
 			"XOR",
 			"YEAR_MONTH",
 			"ZEROFILL",
-		};
+		];
 
 		foreach (string word in reservedWords)
 			dataTable.Rows.Add(word);
 	}
 
 	private async Task FillDataTableAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, List<KeyValuePair<string, string>>? columns, CancellationToken cancellationToken)
+	{
+		await FillDataTableAsync(ioBehavior, dataTable, command =>
+		{
+#pragma warning disable CA2100
+			command.CommandText = "SELECT " + string.Join(", ", dataTable.Columns.Cast<DataColumn>().Select(static x => x!.ColumnName)) + " FROM INFORMATION_SCHEMA." + tableName;
+#pragma warning restore CA2100
+			if (columns is { Count: > 0 })
+			{
+				command.CommandText += " WHERE " + string.Join(" AND ", columns.Select(static x => $@"{x.Key} = @{x.Key}"));
+				foreach (var column in columns)
+					command.Parameters.AddWithValue("@" + column.Key, column.Value);
+			}
+		}, cancellationToken).ConfigureAwait(false);
+	}
+
+	private async Task FillDataTableAsync(IOBehavior ioBehavior, DataTable dataTable, Action<MySqlCommand> configureCommand, CancellationToken cancellationToken)
 	{
 		Action? close = null;
-		if (m_connection.State != ConnectionState.Open)
+		if (connection.State != ConnectionState.Open)
 		{
-			await m_connection.OpenAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
-			close = m_connection.Close;
+			await connection.OpenAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+			close = connection.Close;
 		}
 
 		// remove columns that the server doesn't support
 		if (dataTable.TableName == "Columns")
 		{
-			using (var command = new MySqlCommand("SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = 'information_schema' AND table_name = 'COLUMNS' AND column_name = 'GENERATION_EXPRESSION';", m_connection))
+			using (var command = new MySqlCommand("SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = 'information_schema' AND table_name = 'COLUMNS' AND column_name = 'GENERATION_EXPRESSION';", connection))
 			{
 				if (await command.ExecuteScalarAsync(ioBehavior, cancellationToken).ConfigureAwait(false) is null)
 					dataTable.Columns.Remove("GENERATION_EXPRESSION");
 			}
 
-			using (var command = new MySqlCommand("SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = 'information_schema' AND table_name = 'COLUMNS' AND column_name = 'SRS_ID';", m_connection))
+			using (var command = new MySqlCommand("SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = 'information_schema' AND table_name = 'COLUMNS' AND column_name = 'SRS_ID';", connection))
 			{
 				if (await command.ExecuteScalarAsync(ioBehavior, cancellationToken).ConfigureAwait(false) is null)
 					dataTable.Columns.Remove("SRS_ID");
 			}
 		}
 
-		using (var command = m_connection.CreateCommand())
+		using (var command = connection.CreateCommand())
 		{
-#pragma warning disable CA2100
-			command.CommandText = "SELECT " + string.Join(", ", dataTable.Columns.Cast<DataColumn>().Select(static x => x!.ColumnName)) + " FROM INFORMATION_SCHEMA." + tableName;
-#pragma warning restore CA2100
-			if (columns is { Count: > 0 })
-			{
-				command.CommandText += " WHERE " + string.Join(" AND ", columns.Select(x => $@"{x.Key} = @{x.Key}"));
-				foreach (var column in columns)
-					command.Parameters.AddWithValue("@" + column.Key, column.Value);
-			}
+			configureCommand(command);
 
 			using var reader = await command.ExecuteReaderAsync(default, ioBehavior, cancellationToken).ConfigureAwait(false);
 			while (await reader.ReadAsync(ioBehavior, cancellationToken).ConfigureAwait(false))
 			{
 				var rowValues = new object[dataTable.Columns.Count];
-				reader.GetValues(rowValues);
+				_ = reader.GetValues(rowValues);
 				dataTable.Rows.Add(rowValues);
 			}
 		}
@@ -435,5 +440,103 @@ private async Task FillDataTableAsync(IOBehavior ioBehavior, DataTable dataTable
 		close?.Invoke();
 	}
 
-	private readonly MySqlConnection m_connection;
+	private Task DoFillForeignKeysAsync(IOBehavior ioBehavior, DataTable dataTable, string?[]? restrictionValues, CancellationToken cancellationToken) =>
+		FillDataTableAsync(ioBehavior, dataTable, command =>
+		{
+			command.CommandText = """
+				SELECT rc.constraint_catalog, rc.constraint_schema, rc.constraint_name,
+					kcu.table_catalog, kcu.table_schema,
+					rc.table_name, rc.match_option, rc.update_rule, rc.delete_rule, 
+					NULL as referenced_table_catalog, kcu.referenced_table_schema, rc.referenced_table_name 
+				FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc
+					LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON 
+					(
+						(kcu.constraint_catalog = rc.constraint_catalog OR (kcu.constraint_catalog IS NULL AND rc.constraint_catalog IS NULL)) AND
+						(kcu.constraint_schema = rc.constraint_schema OR (kcu.constraint_schema IS NULL AND rc.constraint_schema IS NULL)) AND
+						(kcu.constraint_name = rc.constraint_name OR (kcu.constraint_name IS NULL AND rc.constraint_name IS NULL))
+					)
+				WHERE kcu.ORDINAL_POSITION = 1
+				""";
+
+			if (restrictionValues is [_, { Length: > 0 } schema, ..])
+			{
+				command.CommandText += " AND rc.constraint_schema LIKE @schema";
+				command.Parameters.AddWithValue("@schema", schema);
+			}
+			if (restrictionValues is [_, _, { Length: > 0 } table, ..])
+			{
+				command.CommandText += " AND rc.table_name LIKE @table";
+				command.Parameters.AddWithValue("@table", table);
+			}
+			if (restrictionValues is [_, _, _, { Length: > 0 } constraint, ..])
+			{
+				command.CommandText += " AND rc.constraint_name LIKE @constraint";
+				command.Parameters.AddWithValue("@constraint", constraint);
+			}
+		}, cancellationToken);
+
+	private Task DoFillIndexesAsync(IOBehavior ioBehavior, DataTable dataTable, string?[]? restrictionValues, CancellationToken cancellationToken) =>
+		FillDataTableAsync(ioBehavior, dataTable, command =>
+		{
+			command.CommandText = """
+				SELECT null AS INDEX_CATALOG, INDEX_SCHEMA,
+					INDEX_NAME, TABLE_NAME,
+					!NON_UNIQUE as `UNIQUE`, 
+					INDEX_NAME='PRIMARY' as `PRIMARY`,
+					INDEX_TYPE as TYPE, COMMENT 
+				FROM INFORMATION_SCHEMA.STATISTICS
+				WHERE SEQ_IN_INDEX=1
+				""";
+
+			if (restrictionValues is [_, { Length: > 0 } schema, ..])
+			{
+				command.CommandText += " AND INDEX_SCHEMA LIKE @schema";
+				command.Parameters.AddWithValue("@schema", schema);
+			}
+			if (restrictionValues is [_, _, { Length: > 0 } table, ..])
+			{
+				command.CommandText += " AND TABLE_NAME LIKE @table";
+				command.Parameters.AddWithValue("@table", table);
+			}
+			if (restrictionValues is [_, _, _, { Length: > 0 } index, ..])
+			{
+				command.CommandText += " AND INDEX_NAME LIKE @index";
+				command.Parameters.AddWithValue("@index", index);
+			}
+		}, cancellationToken);
+
+	private Task DoFillIndexColumnsAsync(IOBehavior ioBehavior, DataTable dataTable, string?[]? restrictionValues, CancellationToken cancellationToken) =>
+		FillDataTableAsync(ioBehavior, dataTable, command =>
+		{
+			command.CommandText = """
+				SELECT null AS INDEX_CATALOG, INDEX_SCHEMA,
+					INDEX_NAME, TABLE_NAME,
+					COLUMN_NAME,
+					SEQ_IN_INDEX as `ORDINAL_POSITION`,
+					COLLATION as SORT_ORDER
+				FROM INFORMATION_SCHEMA.STATISTICS
+				WHERE 1=1
+				""";
+
+			if (restrictionValues is [_, { Length: > 0 } schema, ..])
+			{
+				command.CommandText += " AND INDEX_SCHEMA LIKE @schema";
+				command.Parameters.AddWithValue("@schema", schema);
+			}
+			if (restrictionValues is [_, _, { Length: > 0 } table, ..])
+			{
+				command.CommandText += " AND TABLE_NAME LIKE @table";
+				command.Parameters.AddWithValue("@table", table);
+			}
+			if (restrictionValues is [_, _, _, { Length: > 0 } index, ..])
+			{
+				command.CommandText += " AND INDEX_NAME LIKE @index";
+				command.Parameters.AddWithValue("@index", index);
+			}
+			if (restrictionValues is [_, _, _, _, { Length: > 0 } column, ..])
+			{
+				command.CommandText += " AND COLUMN_NAME LIKE @column";
+				command.Parameters.AddWithValue("@column", column);
+			}
+		}, cancellationToken);
 }
diff --git a/src/MySqlConnector/Core/SchemaProvider.g.cs b/src/MySqlConnector/Core/SchemaProvider.g.cs
index 7a9c5843a..adacc5473 100644
--- a/src/MySqlConnector/Core/SchemaProvider.g.cs
+++ b/src/MySqlConnector/Core/SchemaProvider.g.cs
@@ -9,8 +9,12 @@ internal sealed partial class SchemaProvider
 {
 	public async ValueTask<DataTable> GetSchemaAsync(IOBehavior ioBehavior, string collectionName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
+#if NET6_0_OR_GREATER
+		ArgumentNullException.ThrowIfNull(collectionName);
+#else
 		if (collectionName is null)
 			throw new ArgumentNullException(nameof(collectionName));
+#endif
 
 		var dataTable = new DataTable();
 		if (string.Equals(collectionName, "MetaDataCollections", StringComparison.OrdinalIgnoreCase))
@@ -63,14 +67,18 @@ public async ValueTask<DataTable> GetSchemaAsync(IOBehavior ioBehavior, string c
 			await FillTableConstraintsAsync(ioBehavior, dataTable, "TableConstraints", restrictionValues, cancellationToken).ConfigureAwait(false);
 		else if (string.Equals(collectionName, "TablePrivileges", StringComparison.OrdinalIgnoreCase))
 			await FillTablePrivilegesAsync(ioBehavior, dataTable, "TablePrivileges", restrictionValues, cancellationToken).ConfigureAwait(false);
-		else if (string.Equals(collectionName, "TableSpaces", StringComparison.OrdinalIgnoreCase))
-			await FillTableSpacesAsync(ioBehavior, dataTable, "TableSpaces", restrictionValues, cancellationToken).ConfigureAwait(false);
 		else if (string.Equals(collectionName, "Triggers", StringComparison.OrdinalIgnoreCase))
 			await FillTriggersAsync(ioBehavior, dataTable, "Triggers", restrictionValues, cancellationToken).ConfigureAwait(false);
 		else if (string.Equals(collectionName, "UserPrivileges", StringComparison.OrdinalIgnoreCase))
 			await FillUserPrivilegesAsync(ioBehavior, dataTable, "UserPrivileges", restrictionValues, cancellationToken).ConfigureAwait(false);
 		else if (string.Equals(collectionName, "Views", StringComparison.OrdinalIgnoreCase))
 			await FillViewsAsync(ioBehavior, dataTable, "Views", restrictionValues, cancellationToken).ConfigureAwait(false);
+		else if (string.Equals(collectionName, "Foreign Keys", StringComparison.OrdinalIgnoreCase))
+			await FillForeignKeysAsync(ioBehavior, dataTable, "Foreign Keys", restrictionValues, cancellationToken).ConfigureAwait(false);
+		else if (string.Equals(collectionName, "Indexes", StringComparison.OrdinalIgnoreCase))
+			await FillIndexesAsync(ioBehavior, dataTable, "Indexes", restrictionValues, cancellationToken).ConfigureAwait(false);
+		else if (string.Equals(collectionName, "IndexColumns", StringComparison.OrdinalIgnoreCase))
+			await FillIndexColumnsAsync(ioBehavior, dataTable, "IndexColumns", restrictionValues, cancellationToken).ConfigureAwait(false);
 		else
 			throw new ArgumentException($"Invalid collection name: '{collectionName}'.", nameof(collectionName));
 
@@ -79,16 +87,16 @@ public async ValueTask<DataTable> GetSchemaAsync(IOBehavior ioBehavior, string c
 
 	private Task FillMetaDataCollectionsAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'MetaDataCollections'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("CollectionName", typeof(string)),
 			new("NumberOfRestrictions", typeof(int)),
 			new("NumberOfIdentifierParts", typeof(int)),
-		});
+		]);
 
 		dataTable.Rows.Add("MetaDataCollections", 0, 0);
 		dataTable.Rows.Add("CharacterSets", 0, 0);
@@ -115,61 +123,63 @@ private Task FillMetaDataCollectionsAsync(IOBehavior ioBehavior, DataTable dataT
 		dataTable.Rows.Add("Tables", 4, 3);
 		dataTable.Rows.Add("TableConstraints", 0, 3);
 		dataTable.Rows.Add("TablePrivileges", 0, 0);
-		dataTable.Rows.Add("TableSpaces", 0, 0);
 		dataTable.Rows.Add("Triggers", 0, 3);
 		dataTable.Rows.Add("UserPrivileges", 0, 0);
 		dataTable.Rows.Add("Views", 0, 3);
+		dataTable.Rows.Add("Foreign Keys", 4, 0);
+		dataTable.Rows.Add("Indexes", 4, 0);
+		dataTable.Rows.Add("IndexColumns", 5, 0);
 
 		return Task.CompletedTask;
 	}
 
 	private async Task FillCharacterSetsAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'CharacterSets'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("CHARACTER_SET_NAME", typeof(string)),
 			new("DEFAULT_COLLATE_NAME", typeof(string)),
 			new("DESCRIPTION", typeof(string)),
 			new("MAXLEN", typeof(int)),
-		});
+		]);
 
 		await FillDataTableAsync(ioBehavior, dataTable, "CHARACTER_SETS", null, cancellationToken).ConfigureAwait(false);
 	}
 
 	private async Task FillCollationsAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'Collations'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("COLLATION_NAME", typeof(string)),
 			new("CHARACTER_SET_NAME", typeof(string)),
 			new("ID", typeof(int)),
 			new("IS_DEFAULT", typeof(string)),
 			new("IS_COMPILED", typeof(string)),
 			new("SORTLEN", typeof(int)),
-		});
+		]);
 
 		await FillDataTableAsync(ioBehavior, dataTable, "COLLATIONS", null, cancellationToken).ConfigureAwait(false);
 	}
 
 	private async Task FillCollationCharacterSetApplicabilityAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'CollationCharacterSetApplicability'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("COLLATION_NAME", typeof(string)),
 			new("CHARACTER_SET_NAME", typeof(string)),
-		});
+		]);
 
 		await FillDataTableAsync(ioBehavior, dataTable, "COLLATION_CHARACTER_SET_APPLICABILITY", null, cancellationToken).ConfigureAwait(false);
 	}
@@ -180,8 +190,8 @@ private async Task FillColumnsAsync(IOBehavior ioBehavior, DataTable dataTable,
 			throw new ArgumentException("More than 4 restrictionValues are not supported for schema 'Columns'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("TABLE_CATALOG", typeof(string)),
 			new("TABLE_SCHEMA", typeof(string)),
 			new("TABLE_NAME", typeof(string)),
@@ -203,7 +213,7 @@ private async Task FillColumnsAsync(IOBehavior ioBehavior, DataTable dataTable,
 			new("COLUMN_COMMENT", typeof(string)),
 			new("GENERATION_EXPRESSION", typeof(string)),
 			new("SRS_ID", typeof(string)),
-		});
+		]);
 
 		var columns = new List<KeyValuePair<string, string>>();
 		if (restrictionValues is not null)
@@ -223,30 +233,30 @@ private async Task FillColumnsAsync(IOBehavior ioBehavior, DataTable dataTable,
 
 	private async Task FillDatabasesAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'Databases'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("CATALOG_NAME", typeof(string)),
 			new("SCHEMA_NAME", typeof(string)),
 			new("DEFAULT_CHARACTER_SET_NAME", typeof(string)),
 			new("DEFAULT_COLLATION_NAME", typeof(string)),
 			new("SQL_PATH", typeof(string)),
-		});
+		]);
 
 		await FillDataTableAsync(ioBehavior, dataTable, "SCHEMATA", null, cancellationToken).ConfigureAwait(false);
 	}
 
 	private Task FillDataSourceInformationAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'DataSourceInformation'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("CompositeIdentifierSeparatorPattern", typeof(string)),
 			new("DataSourceProductName", typeof(string)),
 			new("DataSourceProductVersion", typeof(string)),
@@ -264,7 +274,7 @@ private Task FillDataSourceInformationAsync(IOBehavior ioBehavior, DataTable dat
 			new("StatementSeparatorPattern", typeof(string)),
 			new("StringLiteralPattern", typeof(string)),
 			new("SupportedJoinOperators", typeof(SupportedJoinOperators)),
-		});
+		]);
 
 		DoFillDataSourceInformation(dataTable);
 
@@ -273,12 +283,12 @@ private Task FillDataSourceInformationAsync(IOBehavior ioBehavior, DataTable dat
 
 	private Task FillDataTypesAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'DataTypes'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("TypeName", typeof(string)),
 			new("ProviderDbType", typeof(int)),
 			new("ColumnSize", typeof(long)),
@@ -302,7 +312,7 @@ private Task FillDataTypesAsync(IOBehavior ioBehavior, DataTable dataTable, stri
 			new("LiteralPrefix", typeof(string)),
 			new("LiteralSuffix", typeof(string)),
 			new("NativeDataType", typeof(string)),
-		});
+		]);
 
 		DoFillDataTypes(dataTable);
 
@@ -311,31 +321,31 @@ private Task FillDataTypesAsync(IOBehavior ioBehavior, DataTable dataTable, stri
 
 	private async Task FillEnginesAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'Engines'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("ENGINE", typeof(string)),
 			new("SUPPORT", typeof(string)),
 			new("COMMENT", typeof(string)),
 			new("TRANSACTIONS", typeof(string)),
 			new("XA", typeof(string)),
 			new("SAVEPOINTS", typeof(string)),
-		});
+		]);
 
 		await FillDataTableAsync(ioBehavior, dataTable, "ENGINES", null, cancellationToken).ConfigureAwait(false);
 	}
 
 	private async Task FillKeyColumnUsageAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'KeyColumnUsage'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("CONSTRAINT_CATALOG", typeof(string)),
 			new("CONSTRAINT_SCHEMA", typeof(string)),
 			new("CONSTRAINT_NAME", typeof(string)),
@@ -348,34 +358,34 @@ private async Task FillKeyColumnUsageAsync(IOBehavior ioBehavior, DataTable data
 			new("REFERENCED_TABLE_SCHEMA", typeof(string)),
 			new("REFERENCED_TABLE_NAME", typeof(string)),
 			new("REFERENCED_COLUMN_NAME", typeof(string)),
-		});
+		]);
 
 		await FillDataTableAsync(ioBehavior, dataTable, "KEY_COLUMN_USAGE", null, cancellationToken).ConfigureAwait(false);
 	}
 
 	private async Task FillKeyWordsAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'KeyWords'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("WORD", typeof(string)),
 			new("RESERVED", typeof(int)),
-		});
+		]);
 
 		await FillDataTableAsync(ioBehavior, dataTable, "KEYWORDS", null, cancellationToken).ConfigureAwait(false);
 	}
 
 	private async Task FillParametersAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'Parameters'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("SPECIFIC_CATALOG", typeof(string)),
 			new("SPECIFIC_SCHEMA", typeof(string)),
 			new("SPECIFIC_NAME", typeof(string)),
@@ -392,19 +402,19 @@ private async Task FillParametersAsync(IOBehavior ioBehavior, DataTable dataTabl
 			new("COLLATION_NAME", typeof(string)),
 			new("DTD_IDENTIFIER", typeof(string)),
 			new("ROUTINE_TYPE", typeof(string)),
-		});
+		]);
 
 		await FillDataTableAsync(ioBehavior, dataTable, "PARAMETERS", null, cancellationToken).ConfigureAwait(false);
 	}
 
 	private async Task FillPartitionsAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'Partitions'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("TABLE_CATALOG", typeof(string)),
 			new("TABLE_SCHEMA", typeof(string)),
 			new("TABLE_NAME", typeof(string)),
@@ -430,19 +440,19 @@ private async Task FillPartitionsAsync(IOBehavior ioBehavior, DataTable dataTabl
 			new("PARTITION_COMMENT", typeof(string)),
 			new("NODEGROUP", typeof(string)),
 			new("TABLESPACE_NAME", typeof(string)),
-		});
+		]);
 
 		await FillDataTableAsync(ioBehavior, dataTable, "PARTITIONS", null, cancellationToken).ConfigureAwait(false);
 	}
 
 	private async Task FillPluginsAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'Plugins'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("PLUGIN_NAME", typeof(string)),
 			new("PLUGIN_VERSION", typeof(string)),
 			new("PLUGIN_STATUS", typeof(string)),
@@ -454,19 +464,19 @@ private async Task FillPluginsAsync(IOBehavior ioBehavior, DataTable dataTable,
 			new("PLUGIN_DESCRIPTION", typeof(string)),
 			new("PLUGIN_LICENSE", typeof(string)),
 			new("LOAD_OPTION", typeof(string)),
-		});
+		]);
 
 		await FillDataTableAsync(ioBehavior, dataTable, "PLUGINS", null, cancellationToken).ConfigureAwait(false);
 	}
 
 	private async Task FillProceduresAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'Procedures'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("SPECIFIC_NAME", typeof(string)),
 			new("ROUTINE_CATALOG", typeof(string)),
 			new("ROUTINE_SCHEMA", typeof(string)),
@@ -487,19 +497,19 @@ private async Task FillProceduresAsync(IOBehavior ioBehavior, DataTable dataTabl
 			new("SQL_MODE", typeof(string)),
 			new("ROUTINE_COMMENT", typeof(string)),
 			new("DEFINER", typeof(string)),
-		});
+		]);
 
 		await FillDataTableAsync(ioBehavior, dataTable, "ROUTINES", null, cancellationToken).ConfigureAwait(false);
 	}
 
 	private async Task FillProcessListAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'ProcessList'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("ID", typeof(long)),
 			new("USER", typeof(string)),
 			new("HOST", typeof(string)),
@@ -508,19 +518,19 @@ private async Task FillProcessListAsync(IOBehavior ioBehavior, DataTable dataTab
 			new("TIME", typeof(int)),
 			new("STATE", typeof(string)),
 			new("INFO", typeof(string)),
-		});
+		]);
 
 		await FillDataTableAsync(ioBehavior, dataTable, "PROCESSLIST", null, cancellationToken).ConfigureAwait(false);
 	}
 
 	private async Task FillProfilingAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'Profiling'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("QUERY_ID", typeof(int)),
 			new("SEQ", typeof(int)),
 			new("STATE", typeof(string)),
@@ -539,19 +549,19 @@ private async Task FillProfilingAsync(IOBehavior ioBehavior, DataTable dataTable
 			new("SOURCE_FUNCTION", typeof(string)),
 			new("SOURCE_FILE", typeof(string)),
 			new("SOURCE_LINE", typeof(int)),
-		});
+		]);
 
 		await FillDataTableAsync(ioBehavior, dataTable, "PROFILING", null, cancellationToken).ConfigureAwait(false);
 	}
 
 	private async Task FillReferentialConstraintsAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'ReferentialConstraints'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("CONSTRAINT_CATALOG", typeof(string)),
 			new("CONSTRAINT_SCHEMA", typeof(string)),
 			new("CONSTRAINT_NAME", typeof(string)),
@@ -563,21 +573,21 @@ private async Task FillReferentialConstraintsAsync(IOBehavior ioBehavior, DataTa
 			new("DELETE_RULE", typeof(string)),
 			new("TABLE_NAME", typeof(string)),
 			new("REFERENCED_TABLE_NAME", typeof(string)),
-		});
+		]);
 
 		await FillDataTableAsync(ioBehavior, dataTable, "REFERENTIAL_CONSTRAINTS", null, cancellationToken).ConfigureAwait(false);
 	}
 
 	private Task FillReservedWordsAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'ReservedWords'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("ReservedWord", typeof(string)),
-		});
+		]);
 
 		DoFillReservedWords(dataTable);
 
@@ -586,35 +596,35 @@ private Task FillReservedWordsAsync(IOBehavior ioBehavior, DataTable dataTable,
 
 	private async Task FillResourceGroupsAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'ResourceGroups'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("RESOURCE_GROUP_NAME", typeof(string)),
 			new("RESOURCE_GROUP_TYPE", typeof(string)),
 			new("RESOURCE_GROUP_ENABLED", typeof(int)),
 			new("VCPU_IDS", typeof(string)),
 			new("THREAD_PRIORITY", typeof(int)),
-		});
+		]);
 
 		await FillDataTableAsync(ioBehavior, dataTable, "RESOURCE_GROUPS", null, cancellationToken).ConfigureAwait(false);
 	}
 
 	private Task FillRestrictionsAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'Restrictions'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("CollectionName", typeof(string)),
 			new("RestrictionName", typeof(string)),
 			new("RestrictionDefault", typeof(string)),
 			new("RestrictionNumber", typeof(int)),
-		});
+		]);
 
 		dataTable.Rows.Add("Columns", "Catalog", "TABLE_CATALOG", 1);
 		dataTable.Rows.Add("Columns", "Schema", "TABLE_SCHEMA", 2);
@@ -624,24 +634,37 @@ private Task FillRestrictionsAsync(IOBehavior ioBehavior, DataTable dataTable, s
 		dataTable.Rows.Add("Tables", "Schema", "TABLE_SCHEMA", 2);
 		dataTable.Rows.Add("Tables", "Table", "TABLE_NAME", 3);
 		dataTable.Rows.Add("Tables", "TableType", "TABLE_TYPE", 4);
+		dataTable.Rows.Add("Foreign Keys", "Catalog", "TABLE_CATALOG", 1);
+		dataTable.Rows.Add("Foreign Keys", "Schema", "TABLE_SCHEMA", 2);
+		dataTable.Rows.Add("Foreign Keys", "Table", "TABLE_NAME", 3);
+		dataTable.Rows.Add("Foreign Keys", "Constraint Name", "CONSTRAINT_NAME", 4);
+		dataTable.Rows.Add("Indexes", "Catalog", "TABLE_CATALOG", 1);
+		dataTable.Rows.Add("Indexes", "Schema", "TABLE_SCHEMA", 2);
+		dataTable.Rows.Add("Indexes", "Table", "TABLE_NAME", 3);
+		dataTable.Rows.Add("Indexes", "Name", "INDEX_NAME", 4);
+		dataTable.Rows.Add("IndexColumns", "Catalog", "TABLE_CATALOG", 1);
+		dataTable.Rows.Add("IndexColumns", "Schema", "TABLE_SCHEMA", 2);
+		dataTable.Rows.Add("IndexColumns", "Table", "TABLE_NAME", 3);
+		dataTable.Rows.Add("IndexColumns", "Name", "INDEX_NAME", 4);
+		dataTable.Rows.Add("IndexColumns", "Column", "COLUMN_NAME", 5);
 
 		return Task.CompletedTask;
 	}
 
 	private async Task FillSchemaPrivilegesAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'SchemaPrivileges'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("GRANTEE", typeof(string)),
 			new("TABLE_CATALOG", typeof(string)),
 			new("TABLE_SCHEMA", typeof(string)),
 			new("PRIVILEGE_TYPE", typeof(string)),
 			new("IS_GRANTABLE", typeof(string)),
-		});
+		]);
 
 		await FillDataTableAsync(ioBehavior, dataTable, "SCHEMA_PRIVILEGES", null, cancellationToken).ConfigureAwait(false);
 	}
@@ -652,8 +675,8 @@ private async Task FillTablesAsync(IOBehavior ioBehavior, DataTable dataTable, s
 			throw new ArgumentException("More than 4 restrictionValues are not supported for schema 'Tables'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("TABLE_CATALOG", typeof(string)),
 			new("TABLE_SCHEMA", typeof(string)),
 			new("TABLE_NAME", typeof(string)),
@@ -675,7 +698,7 @@ private async Task FillTablesAsync(IOBehavior ioBehavior, DataTable dataTable, s
 			new("CHECKSUM", typeof(string)),
 			new("CREATE_OPTIONS", typeof(string)),
 			new("TABLE_COMMENT", typeof(string)),
-		});
+		]);
 
 		var columns = new List<KeyValuePair<string, string>>();
 		if (restrictionValues is not null)
@@ -695,72 +718,50 @@ private async Task FillTablesAsync(IOBehavior ioBehavior, DataTable dataTable, s
 
 	private async Task FillTableConstraintsAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'TableConstraints'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("CONSTRAINT_CATALOG", typeof(string)),
 			new("CONSTRAINT_SCHEMA", typeof(string)),
 			new("CONSTRAINT_NAME", typeof(string)),
 			new("TABLE_SCHEMA", typeof(string)),
 			new("TABLE_NAME", typeof(string)),
 			new("CONSTRAINT_TYPE", typeof(string)),
-		});
+		]);
 
 		await FillDataTableAsync(ioBehavior, dataTable, "TABLE_CONSTRAINTS", null, cancellationToken).ConfigureAwait(false);
 	}
 
 	private async Task FillTablePrivilegesAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'TablePrivileges'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("GRANTEE", typeof(string)),
 			new("TABLE_CATALOG", typeof(string)),
 			new("TABLE_SCHEMA", typeof(string)),
 			new("TABLE_NAME", typeof(string)),
 			new("PRIVILEGE_TYPE", typeof(string)),
 			new("IS_GRANTABLE", typeof(string)),
-		});
+		]);
 
 		await FillDataTableAsync(ioBehavior, dataTable, "TABLE_PRIVILEGES", null, cancellationToken).ConfigureAwait(false);
 	}
 
-	private async Task FillTableSpacesAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
-	{
-		if (restrictionValues is not null)
-			throw new ArgumentException("restrictionValues is not supported for schema 'TableSpaces'.", nameof(restrictionValues));
-
-		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
-			new("TABLESPACE_NAME", typeof(string)),
-			new("ENGINE", typeof(string)),
-			new("TABLESPACE_TYPE", typeof(string)),
-			new("LOGFILE_GROUP_NAME", typeof(string)),
-			new("EXTENT_SIZE", typeof(long)),
-			new("AUTOEXTEND_SIZE", typeof(long)),
-			new("MAXIMUM_SIZE", typeof(long)),
-			new("NODEGROUP_ID", typeof(long)),
-			new("TABLESPACE_COMMENT", typeof(string)),
-		});
-
-		await FillDataTableAsync(ioBehavior, dataTable, "TABLESPACES", null, cancellationToken).ConfigureAwait(false);
-	}
-
 	private async Task FillTriggersAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'Triggers'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("TRIGGER_CATALOG", typeof(string)),
 			new("TRIGGER_SCHEMA", typeof(string)),
 			new("TRIGGER_NAME", typeof(string)),
@@ -783,36 +784,36 @@ private async Task FillTriggersAsync(IOBehavior ioBehavior, DataTable dataTable,
 			new("CHARACTER_SET_CLIENT", typeof(string)),
 			new("COLLATION_CONNECTION", typeof(string)),
 			new("DATABASE_COLLATION", typeof(string)),
-		});
+		]);
 
 		await FillDataTableAsync(ioBehavior, dataTable, "TRIGGERS", null, cancellationToken).ConfigureAwait(false);
 	}
 
 	private async Task FillUserPrivilegesAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'UserPrivileges'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("GRANTEE", typeof(string)),
 			new("TABLE_CATALOG", typeof(string)),
 			new("PRIVILEGE_TYPE", typeof(string)),
 			new("IS_GRANTABLE", typeof(string)),
-		});
+		]);
 
 		await FillDataTableAsync(ioBehavior, dataTable, "USER_PRIVILEGES", null, cancellationToken).ConfigureAwait(false);
 	}
 
 	private async Task FillViewsAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 	{
-		if (restrictionValues is not null)
+		if (restrictionValues is not null && restrictionValues.Length > 0)
 			throw new ArgumentException("restrictionValues is not supported for schema 'Views'.", nameof(restrictionValues));
 
 		dataTable.TableName = tableName;
-		dataTable.Columns.AddRange(new DataColumn[]
-		{
+		dataTable.Columns.AddRange(
+		[
 			new("TABLE_CATALOG", typeof(string)),
 			new("TABLE_SCHEMA", typeof(string)),
 			new("TABLE_NAME", typeof(string)),
@@ -823,9 +824,75 @@ private async Task FillViewsAsync(IOBehavior ioBehavior, DataTable dataTable, st
 			new("SECURITY_TYPE", typeof(string)),
 			new("CHARACTER_SET_CLIENT", typeof(string)),
 			new("COLLATION_CONNECTION", typeof(string)),
-		});
+		]);
 
 		await FillDataTableAsync(ioBehavior, dataTable, "VIEWS", null, cancellationToken).ConfigureAwait(false);
 	}
 
+	private async Task FillForeignKeysAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
+	{
+		if (restrictionValues is { Length: > 4 })
+			throw new ArgumentException("More than 4 restrictionValues are not supported for schema 'Foreign Keys'.", nameof(restrictionValues));
+
+		dataTable.TableName = tableName;
+		dataTable.Columns.AddRange(
+		[
+			new("CONSTRAINT_CATALOG", typeof(string)),
+			new("CONSTRAINT_SCHEMA", typeof(string)),
+			new("CONSTRAINT_NAME", typeof(string)),
+			new("TABLE_CATALOG", typeof(string)),
+			new("TABLE_SCHEMA", typeof(string)),
+			new("TABLE_NAME", typeof(string)),
+			new("MATCH_OPTION", typeof(string)),
+			new("UPDATE_RULE", typeof(string)),
+			new("DELETE_RULE", typeof(string)),
+			new("REFERENCED_TABLE_CATALOG", typeof(string)),
+			new("REFERENCED_TABLE_SCHEMA", typeof(string)),
+			new("REFERENCED_TABLE_NAME", typeof(string)),
+		]);
+
+		await DoFillForeignKeysAsync(ioBehavior, dataTable, restrictionValues, cancellationToken).ConfigureAwait(false);
+	}
+
+	private async Task FillIndexesAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
+	{
+		if (restrictionValues is { Length: > 4 })
+			throw new ArgumentException("More than 4 restrictionValues are not supported for schema 'Indexes'.", nameof(restrictionValues));
+
+		dataTable.TableName = tableName;
+		dataTable.Columns.AddRange(
+		[
+			new("INDEX_CATALOG", typeof(string)),
+			new("INDEX_SCHEMA", typeof(string)),
+			new("INDEX_NAME", typeof(string)),
+			new("TABLE_NAME", typeof(string)),
+			new("UNIQUE", typeof(bool)),
+			new("PRIMARY", typeof(bool)),
+			new("TYPE", typeof(string)),
+			new("COMMENT", typeof(string)),
+		]);
+
+		await DoFillIndexesAsync(ioBehavior, dataTable, restrictionValues, cancellationToken).ConfigureAwait(false);
+	}
+
+	private async Task FillIndexColumnsAsync(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
+	{
+		if (restrictionValues is { Length: > 5 })
+			throw new ArgumentException("More than 5 restrictionValues are not supported for schema 'IndexColumns'.", nameof(restrictionValues));
+
+		dataTable.TableName = tableName;
+		dataTable.Columns.AddRange(
+		[
+			new("INDEX_CATALOG", typeof(string)),
+			new("INDEX_SCHEMA", typeof(string)),
+			new("INDEX_NAME", typeof(string)),
+			new("TABLE_NAME", typeof(string)),
+			new("COLUMN_NAME", typeof(string)),
+			new("ORDINAL_POSITION", typeof(int)),
+			new("SORT_ORDER", typeof(string)),
+		]);
+
+		await DoFillIndexColumnsAsync(ioBehavior, dataTable, restrictionValues, cancellationToken).ConfigureAwait(false);
+	}
+
 }
diff --git a/src/MySqlConnector/Core/ServerSession.cs b/src/MySqlConnector/Core/ServerSession.cs
index c41b25b32..eb7826021 100644
--- a/src/MySqlConnector/Core/ServerSession.cs
+++ b/src/MySqlConnector/Core/ServerSession.cs
@@ -11,6 +11,7 @@
 using System.Security.Cryptography;
 using System.Security.Cryptography.X509Certificates;
 using System.Text;
+using Microsoft.Extensions.Logging;
 using MySqlConnector.Authentication;
 using MySqlConnector.Logging;
 using MySqlConnector.Protocol;
@@ -22,58 +23,60 @@ namespace MySqlConnector.Core;
 
 #pragma warning disable CA1001 // Types that own disposable fields should be disposable
 
-internal sealed class ServerSession
+internal sealed partial class ServerSession : IServerCapabilities
 {
-	public ServerSession()
-		: this(null, 0, Interlocked.Increment(ref s_lastId))
-	{
-	}
-
-	public ServerSession(ConnectionPool? pool, int poolGeneration, int id)
+	public ServerSession(ILogger logger, IConnectionPoolMetadata pool)
 	{
+		m_logger = logger;
 		m_lock = new();
 		m_payloadCache = new();
-		Id = (pool?.Id ?? 0) + "." + id;
+		Id = pool.Id + "." + pool.GetNewSessionId();
 		ServerVersion = ServerVersion.Empty;
-		CreatedTicks = unchecked((uint) Environment.TickCount);
-		Pool = pool;
-		PoolGeneration = poolGeneration;
+		CreatedTimestamp = Stopwatch.GetTimestamp();
+		Pool = pool.ConnectionPool;
+		PoolGeneration = pool.Generation;
 		HostName = "";
-		m_logArguments = new object?[] { Id.ToString(CultureInfo.InvariantCulture), null };
-		m_activityTags = new ActivityTagsCollection();
-		Log.Trace("Session{0} created new session", m_logArguments);
+		m_activityTags = [];
+		DataReader = new();
+		Log.CreatedNewSession(m_logger, Id);
 	}
 
 	public string Id { get; }
 	public ServerVersion ServerVersion { get; set; }
+	public bool SupportsPerQueryVariables => ServerVersion.IsMariaDb && ServerVersion.Version >= ServerVersions.MariaDbSupportsPerQueryVariables;
 	public int ActiveCommandId { get; private set; }
 	public int CancellationTimeout { get; private set; }
 	public int ConnectionId { get; set; }
 	public byte[]? AuthPluginData { get; set; }
-	public uint CreatedTicks { get; }
+	public long CreatedTimestamp { get; }
 	public ConnectionPool? Pool { get; }
 	public int PoolGeneration { get; }
-	public uint LastReturnedTicks { get; private set; }
+	public long LastLeasedTimestamp { get; set; }
+	public long LastReturnedTimestamp { get; private set; }
 	public string? DatabaseOverride { get; set; }
 	public string HostName { get; private set; }
-	public IPAddress? IPAddress => (m_tcpClient?.Client.RemoteEndPoint as IPEndPoint)?.Address;
+	public IPEndPoint? IPEndPoint => m_tcpClient?.Client.RemoteEndPoint as IPEndPoint;
+	public string? UserID { get; private set; }
 	public WeakReference<MySqlConnection>? OwningConnection { get; set; }
-	public bool SupportsComMulti => m_supportsComMulti;
-	public bool SupportsDeprecateEof => m_supportsDeprecateEof;
+	public bool SupportsDeprecateEof { get; private set; }
+	public bool SupportsCachedPreparedMetadata { get; private set; }
 	public bool SupportsQueryAttributes { get; private set; }
-	public bool SupportsSessionTrack => m_supportsSessionTrack;
+	public bool SupportsSessionTrack { get; private set; }
 	public bool ProcAccessDenied { get; set; }
 	public ICollection<KeyValuePair<string, object?>> ActivityTags => m_activityTags;
+	public MySqlDataReader DataReader { get; set; }
+	public MySqlConnectionOpenedConditions Conditions { get; private set; }
 
 	public ValueTask ReturnToPoolAsync(IOBehavior ioBehavior, MySqlConnection? owningConnection)
 	{
-		if (Log.IsTraceEnabled())
-		{
-			m_logArguments[1] = Pool?.Id;
-			Log.Trace("Session{0} returning to Pool{1}", m_logArguments);
-		}
-		LastReturnedTicks = unchecked((uint) Environment.TickCount);
-		return Pool is null ? default : Pool.ReturnAsync(ioBehavior, this);
+		Log.ReturningToPool(m_logger, Id, Pool?.Id ?? 0);
+		Conditions = MySqlConnectionOpenedConditions.None;
+		LastReturnedTimestamp = Stopwatch.GetTimestamp();
+		if (Pool is null)
+			return default;
+		MetricsReporter.RecordUseTime(Pool, Utility.GetElapsedSeconds(LastLeasedTimestamp, LastReturnedTimestamp));
+		LastLeasedTimestamp = 0;
+		return Pool.ReturnAsync(ioBehavior, this);
 	}
 
 	public bool IsConnected
@@ -99,18 +102,18 @@ public bool TryStartCancel(ICancellableCommand command)
 			m_state = State.CancelingQuery;
 		}
 
-		Log.Debug("Session{0} will cancel CommandId: {1} (CancelledAttempts={2}) CommandText: {3}", m_logArguments[0], command.CommandId, command.CancelAttemptCount, (command as MySqlCommand)?.CommandText);
+		Log.WillCancelCommand(m_logger, Id, command.CommandId, command.CancelAttemptCount, (command as MySqlCommand)?.CommandText);
 		return true;
 	}
 
 	public void DoCancel(ICancellableCommand commandToCancel, MySqlCommand killCommand)
 	{
-		Log.Info("Session{0} canceling CommandId {1} from Session{2}; CommandText: {3}", m_logArguments[0], commandToCancel.CommandId, killCommand.Connection!.Session.Id, (commandToCancel as MySqlCommand)?.CommandText);
+		Log.CancelingCommandFromSession(m_logger, Id, commandToCancel.CommandId, killCommand.Connection!.Session.Id, (commandToCancel as MySqlCommand)?.CommandText);
 		lock (m_lock)
 		{
 			if (ActiveCommandId != commandToCancel.CommandId)
 			{
-				Log.Debug("Session{0} ActiveCommandId {1} is not the CommandId {2} being canceled; ignoring cancellation.", m_logArguments[0], ActiveCommandId, commandToCancel.CommandId);
+				Log.IgnoringCancellationForInactiveCommand(m_logger, Id, ActiveCommandId, commandToCancel.CommandId);
 				return;
 			}
 
@@ -120,8 +123,8 @@ public void DoCancel(ICancellableCommand commandToCancel, MySqlCommand killComma
 			// command would be killed (because "KILL QUERY" specifies the connection whose command should be killed, not
 			// a unique identifier of the command itself). As a mitigation, we set the CommandTimeout to a low value to avoid
 			// blocking the other thread for an extended duration.
-			Log.Debug("Session{0} canceling CommandId {1} with CommandText {2}", killCommand.Connection!.Session.Id, commandToCancel.CommandId, killCommand.CommandText);
-			killCommand.ExecuteNonQuery();
+			Log.CancelingCommand(m_logger, killCommand.Connection!.Session.Id, commandToCancel.CommandId, killCommand.CommandText);
+			_ = killCommand.ExecuteNonQuery();
 		}
 	}
 
@@ -154,7 +157,7 @@ public async Task PrepareAsync(IMySqlCommand command, IOBehavior ioBehavior, Can
 
 			var parameterCount = cachedProcedure.Parameters.Count;
 #if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
-			commandToPrepare = string.Create(commandText.Length + 7 + parameterCount * 2 + (parameterCount == 0 ? 1 : 0), (commandText, parameterCount), static (buffer, state) =>
+			commandToPrepare = string.Create(commandText.Length + 7 + (parameterCount * 2) + (parameterCount == 0 ? 1 : 0), (commandText, parameterCount), static (buffer, state) =>
 			{
 				buffer[0] = 'C';
 				buffer[1] = 'A';
@@ -181,7 +184,7 @@ public async Task PrepareAsync(IMySqlCommand command, IOBehavior ioBehavior, Can
 				buffer[1] = ';';
 			});
 #else
-			var callStatement = new StringBuilder("CALL ", commandText.Length + 8 + parameterCount * 2);
+			var callStatement = new StringBuilder("CALL ", commandText.Length + 8 + (parameterCount * 2));
 			callStatement.Append(commandText);
 			callStatement.Append('(');
 			for (int i = 0; i < parameterCount; i++)
@@ -189,7 +192,7 @@ public async Task PrepareAsync(IMySqlCommand command, IOBehavior ioBehavior, Can
 			if (parameterCount == 0)
 				callStatement.Append(')');
 			else
-				callStatement[callStatement.Length - 1] = ')';
+				callStatement[^1] = ')';
 			callStatement.Append(';');
 			commandToPrepare = callStatement.ToString();
 #endif
@@ -232,7 +235,7 @@ public async Task PrepareAsync(IMySqlCommand command, IOBehavior ioBehavior, Can
 					var payloadLength = payload.Span.Length;
 					Utility.Resize(ref columnsAndParameters, columnsAndParametersSize + payloadLength);
 					payload.Span.CopyTo(columnsAndParameters.AsSpan(columnsAndParametersSize));
-					parameters[i] = ColumnDefinitionPayload.Create(new(columnsAndParameters, columnsAndParametersSize, payloadLength));
+					ColumnDefinitionPayload.Initialize(ref parameters[i], new(columnsAndParameters, columnsAndParametersSize, payloadLength));
 					columnsAndParametersSize += payloadLength;
 				}
 				if (!SupportsDeprecateEof)
@@ -252,7 +255,7 @@ public async Task PrepareAsync(IMySqlCommand command, IOBehavior ioBehavior, Can
 					var payloadLength = payload.Span.Length;
 					Utility.Resize(ref columnsAndParameters, columnsAndParametersSize + payloadLength);
 					payload.Span.CopyTo(columnsAndParameters.AsSpan(columnsAndParametersSize));
-					columns[i] = ColumnDefinitionPayload.Create(new(columnsAndParameters, columnsAndParametersSize, payloadLength));
+					ColumnDefinitionPayload.Initialize(ref columns[i], new(columnsAndParameters, columnsAndParametersSize, payloadLength));
 					columnsAndParametersSize += payloadLength;
 				}
 				if (!SupportsDeprecateEof)
@@ -265,7 +268,7 @@ public async Task PrepareAsync(IMySqlCommand command, IOBehavior ioBehavior, Can
 			preparedStatements.Add(new(response.StatementId, statement, columns, parameters));
 		}
 
-		m_preparedStatements ??= new();
+		m_preparedStatements ??= [];
 		m_preparedStatements.Add(commandText, new(preparedStatements, parsedStatements));
 	}
 
@@ -278,9 +281,8 @@ public void StartQuerying(ICancellableCommand command)
 		{
 			if (m_state is State.Querying or State.CancelingQuery)
 			{
-				m_logArguments[1] = m_state;
-				Log.Error("Session{0} can't execute new command when in SessionState: {1}", m_logArguments[0], m_state);
-				throw new InvalidOperationException("This MySqlConnection is already in use. See https://fl.vu/mysql-conn-reuse");
+				CannotExecuteNewCommandInState(m_logger, Id, m_state);
+				throw new InvalidOperationException("This MySqlConnection is already in use. See https://mysqlconnector.net/conn-reuse");
 			}
 
 			VerifyState(State.Connected);
@@ -293,16 +295,24 @@ public void StartQuerying(ICancellableCommand command)
 
 	public void FinishQuerying()
 	{
-		m_logArguments[1] = m_state;
-		Log.Trace("Session{0} entering FinishQuerying; SessionState={1}", m_logArguments);
-		bool clearConnection = false;
+		EnteringFinishQuerying(m_logger, Id, m_state);
+		var clearConnection = false;
 		lock (m_lock)
 		{
+			// entering this method, we expect to be in Querying, CancelingQuery, or Failed state
 			if (m_state == State.CancelingQuery)
 			{
 				m_state = State.ClearingPendingCancellation;
 				clearConnection = true;
 			}
+			else
+			{
+				if (m_state is State.Querying)
+					m_state = State.Connected;
+				else
+					VerifyState(State.Failed);
+				ActiveCommandId = 0;
+			}
 		}
 
 		if (clearConnection)
@@ -310,22 +320,20 @@ public void FinishQuerying()
 			// KILL QUERY will kill a subsequent query if the command it was intended to cancel has already completed.
 			// In order to handle this case, we issue a dummy query that will consume the pending cancellation.
 			// See https://bugs.mysql.com/bug.php?id=45679
-			Log.Debug("Session{0} sending 'SLEEP(0)' command to clear pending cancellation", m_logArguments);
+			Log.SendingSleepToClearPendingCancellation(m_logger, Id);
 			var payload = SupportsQueryAttributes ? s_sleepWithAttributesPayload : s_sleepNoAttributesPayload;
 #pragma warning disable CA2012 // Safe because method completes synchronously
 			SendAsync(payload, IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
 			payload = ReceiveReplyAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
 #pragma warning restore CA2012
-			OkPayload.Create(payload.Span, SupportsDeprecateEof, SupportsSessionTrack);
-		}
+			OkPayload.Verify(payload.Span, this);
 
-		lock (m_lock)
-		{
-			if (m_state is State.Querying or State.ClearingPendingCancellation)
+			lock (m_lock)
+			{
+				VerifyState(State.ClearingPendingCancellation);
 				m_state = State.Connected;
-			else
-				VerifyState(State.Failed);
-			ActiveCommandId = 0;
+				ActiveCommandId = 0;
+			}
 		}
 	}
 
@@ -361,7 +369,7 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
 			{
 				try
 				{
-					Log.Trace("Session{0} sending QUIT command", m_logArguments);
+					Log.SendingQuitCommand(m_logger, Id);
 					m_payloadHandler.StartNewConversation();
 					await m_payloadHandler.WritePayloadAsync(QuitPayload.Instance.Memory, ioBehavior).ConfigureAwait(false);
 				}
@@ -387,10 +395,8 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
 			m_state = State.Closed;
 	}
 
-	public async Task<string?> ConnectAsync(ConnectionSettings cs, MySqlConnection connection, int startTickCount, ILoadBalancer? loadBalancer, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken)
+	private async Task<string?> ConnectAsync(ConnectionSettings cs, MySqlConnection connection, long startingTimestamp, ILoadBalancer? loadBalancer, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
-		string? statusInfo = null;
-
 		try
 		{
 			lock (m_lock)
@@ -409,205 +415,362 @@ public async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancella
 					m_activityTags.Add(ActivitySourceHelper.DatabaseNameTagName, cs.Database);
 				if (activity is { IsAllDataRequested: true })
 				{
-					activity.SetTag(ActivitySourceHelper.DatabaseSystemTagName, ActivitySourceHelper.DatabaseSystemValue);
-					activity.SetTag(ActivitySourceHelper.DatabaseConnectionStringTagName, connectionString);
-					activity.SetTag(ActivitySourceHelper.DatabaseUserTagName, cs.UserID);
+					activity.SetTag(ActivitySourceHelper.DatabaseSystemTagName, ActivitySourceHelper.DatabaseSystemValue)
+						.SetTag(ActivitySourceHelper.DatabaseConnectionStringTagName, connectionString)
+						.SetTag(ActivitySourceHelper.DatabaseUserTagName, cs.UserID);
 					if (cs.Database.Length != 0)
 						activity.SetTag(ActivitySourceHelper.DatabaseNameTagName, cs.Database);
 				}
 			}
 
-			// TLS negotiation should automatically fall back to the best version supported by client and server. However,
-			// Windows Schannel clients will fail to connect to a yaSSL-based MySQL Server if TLS 1.2 is requested and
-			// have to use only TLS 1.1: https://github.com/mysql-net/MySqlConnector/pull/101
-			// In order to use the best protocol possible (i.e., not always default to TLS 1.1), we try the OS-default protocol
-			// (which is SslProtocols.None; see https://docs.microsoft.com/en-us/dotnet/framework/network-programming/tls),
-			// then fall back to SslProtocols.Tls11 if that fails and it's possible that the cause is a yaSSL server.
-			bool shouldRetrySsl;
-			var shouldUpdatePoolSslProtocols = false;
-			var sslProtocols = Pool?.SslProtocols ?? cs.TlsVersions;
-			PayloadData payload;
-			InitialHandshakePayload initialHandshake;
-			do
-			{
-				var isTls11or10Supported = (sslProtocols & (SslProtocols.Tls | SslProtocols.Tls11)) != SslProtocols.None;
-				var isTls12Supported = (sslProtocols & SslProtocols.Tls12) == SslProtocols.Tls12;
-				shouldRetrySsl = (sslProtocols == SslProtocols.None || (isTls12Supported && isTls11or10Supported)) && Utility.IsWindows();
-
-				var connected = false;
-				if (cs.ConnectionProtocol == MySqlConnectionProtocol.Sockets)
-					connected = await OpenTcpSocketAsync(cs, loadBalancer ?? throw new ArgumentNullException(nameof(loadBalancer)), activity, ioBehavior, cancellationToken).ConfigureAwait(false);
-				else if (cs.ConnectionProtocol == MySqlConnectionProtocol.UnixSocket)
-					connected = await OpenUnixSocketAsync(cs, activity, ioBehavior, cancellationToken).ConfigureAwait(false);
-				else if (cs.ConnectionProtocol == MySqlConnectionProtocol.NamedPipe)
-					connected = await OpenNamedPipeAsync(cs, startTickCount, activity, ioBehavior, cancellationToken).ConfigureAwait(false);
-				if (!connected)
-				{
-					lock (m_lock)
-						m_state = State.Failed;
-					Log.Error("Session{0} connecting failed", m_logArguments);
-					throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Unable to connect to any of the specified MySQL hosts.");
-				}
+			Conditions = MySqlConnectionOpenedConditions.New;
+			var connected = cs.ConnectionProtocol switch
+			{
+				MySqlConnectionProtocol.Sockets => await OpenTcpSocketAsync(cs, loadBalancer ?? throw new ArgumentNullException(nameof(loadBalancer)), activity, ioBehavior, cancellationToken).ConfigureAwait(false),
+				MySqlConnectionProtocol.UnixSocket => await OpenUnixSocketAsync(cs, activity, ioBehavior, cancellationToken).ConfigureAwait(false),
+				MySqlConnectionProtocol.NamedPipe => await OpenNamedPipeAsync(cs, startingTimestamp, activity, ioBehavior, cancellationToken).ConfigureAwait(false),
+				_ => false,
+			};
+			if (!connected)
+			{
+				lock (m_lock)
+					m_state = State.Failed;
+				Log.ConnectingFailed(m_logger, Id);
+				throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Unable to connect to any of the specified MySQL hosts.");
+			}
 
-				var byteHandler = m_socket is null ? new StreamByteHandler(m_stream!) : (IByteHandler) new SocketByteHandler(m_socket);
-				if (cs.ConnectionTimeout != 0)
-					byteHandler.RemainingTimeout = Math.Max(1, cs.ConnectionTimeoutMilliseconds - unchecked(Environment.TickCount - startTickCount));
-				m_payloadHandler = new StandardPayloadHandler(byteHandler);
+			var byteHandler = m_socket is null ? new StreamByteHandler(m_stream!) : (IByteHandler) new SocketByteHandler(m_socket);
+			if (cs.ConnectionTimeout != 0)
+				byteHandler.RemainingTimeout = Math.Max(1, cs.ConnectionTimeoutMilliseconds - Utility.GetElapsedMilliseconds(startingTimestamp));
+			m_payloadHandler = new StandardPayloadHandler(byteHandler);
 
-				payload = await ReceiveAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
-				initialHandshake = InitialHandshakePayload.Create(payload.Span);
+			var payload = await ReceiveAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+			var initialHandshake = InitialHandshakePayload.Create(payload.Span);
 
-				// if PluginAuth is supported, then use the specified auth plugin; else, fall back to protocol capabilities to determine the auth type to use
-				string authPluginName;
-				if ((initialHandshake.ProtocolCapabilities & ProtocolCapabilities.PluginAuth) != 0)
-					authPluginName = initialHandshake.AuthPluginName!;
-				else
-					authPluginName = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.SecureConnection) == 0 ? "mysql_old_password" : "mysql_native_password";
-				m_logArguments[1] = authPluginName;
-				Log.Trace("Session{0} server sent AuthPluginName={1}", m_logArguments);
-				if (authPluginName != "mysql_native_password" && authPluginName != "sha256_password" && authPluginName != "caching_sha2_password")
-				{
-					Log.Error("Session{0} unsupported authentication method AuthPluginName={1}", m_logArguments);
-					throw new NotSupportedException($"Authentication method '{initialHandshake.AuthPluginName}' is not supported.");
-				}
+			// if PluginAuth is supported, then use the specified auth plugin; else, fall back to protocol capabilities to determine the auth type to use
+			var currentAuthenticationMethod = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.PluginAuth) != 0 ? initialHandshake.AuthPluginName! :
+				(initialHandshake.ProtocolCapabilities & ProtocolCapabilities.SecureConnection) == 0 ? "mysql_old_password" :
+				"mysql_native_password";
+			Log.ServerSentAuthPluginName(m_logger, Id, currentAuthenticationMethod);
+			if (currentAuthenticationMethod is not "mysql_native_password" and not "sha256_password" and not "caching_sha2_password")
+			{
+				Log.UnsupportedAuthenticationMethod(m_logger, Id, currentAuthenticationMethod);
+				throw new NotSupportedException($"Authentication method '{initialHandshake.AuthPluginName}' is not supported.");
+			}
 
-				ServerVersion = new(initialHandshake.ServerVersion);
-				ConnectionId = initialHandshake.ConnectionId;
-				AuthPluginData = initialHandshake.AuthPluginData;
-				m_useCompression = cs.UseCompression && (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.Compress) != 0;
-				CancellationTimeout = cs.CancellationTimeout;
+			ServerVersion = new(initialHandshake.ServerVersion);
+			ConnectionId = initialHandshake.ConnectionId;
+			AuthPluginData = initialHandshake.AuthPluginData;
+			m_compressionMethod = !cs.UseCompression ? CompressionMethod.None :
+				((initialHandshake.ProtocolCapabilities & ProtocolCapabilities.ZstandardCompressionAlgorithm) != 0 && connection.ZstandardPlugin is not null) ? CompressionMethod.Zstandard :
+				((initialHandshake.ProtocolCapabilities & ProtocolCapabilities.Compress) != 0) ? CompressionMethod.Zlib : CompressionMethod.None;
+			CancellationTimeout = cs.CancellationTimeout;
+			UserID = cs.UserID;
 
-				// set activity tags
-				{
-					var connectionId = ConnectionId.ToString(CultureInfo.InvariantCulture);
-					m_activityTags[ActivitySourceHelper.DatabaseConnectionIdTagName] = connectionId;
-					if (activity is { IsAllDataRequested: true })
-						activity.SetTag(ActivitySourceHelper.DatabaseConnectionIdTagName, connectionId);
-				}
+			// set activity tags
+			{
+				var connectionId = ConnectionId.ToString(CultureInfo.InvariantCulture);
+				m_activityTags[ActivitySourceHelper.DatabaseConnectionIdTagName] = connectionId;
+				if (activity is { IsAllDataRequested: true })
+					activity.SetTag(ActivitySourceHelper.DatabaseConnectionIdTagName, connectionId);
+			}
 
-				m_supportsComMulti = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.MariaDbComMulti) != 0;
-				m_supportsConnectionAttributes = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.ConnectionAttributes) != 0;
-				m_supportsDeprecateEof = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.DeprecateEof) != 0;
-				SupportsQueryAttributes = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.QueryAttributes) != 0;
-				m_supportsSessionTrack = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.SessionTrack) != 0;
-				var serverSupportsSsl = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.Ssl) != 0;
-				m_characterSet = ServerVersion.Version >= ServerVersions.SupportsUtf8Mb4 ? CharacterSet.Utf8Mb4GeneralCaseInsensitive : CharacterSet.Utf8Mb3GeneralCaseInsensitive;
-				m_setNamesPayload = ServerVersion.Version >= ServerVersions.SupportsUtf8Mb4 ?
-					(SupportsQueryAttributes ? s_setNamesUtf8mb4WithAttributesPayload : s_setNamesUtf8mb4NoAttributesPayload) :
-					(SupportsQueryAttributes ? s_setNamesUtf8WithAttributesPayload : s_setNamesUtf8NoAttributesPayload);
-
-				// disable pipelining for RDS MySQL 5.7 (assuming Aurora); otherwise take it from the connection string or default to true
-				if (!cs.Pipelining.HasValue && ServerVersion.Version.Major == 5 && ServerVersion.Version.Minor == 7 && HostName.EndsWith(".rds.amazonaws.com", StringComparison.OrdinalIgnoreCase))
-				{
-					m_logArguments[1] = HostName;
-					Log.Debug("Session{0} auto-detected Aurora 5.7 at '{1}'; disabling pipelining", m_logArguments);
-					m_supportsPipelining = false;
-				}
-				else
-				{
-					// pipelining is not currently compatible with compression
-					m_supportsPipelining = !cs.UseCompression && cs.Pipelining is not false;
+			m_supportsConnectionAttributes = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.ConnectionAttributes) != 0;
+			SupportsDeprecateEof = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.DeprecateEof) != 0;
+			SupportsCachedPreparedMetadata = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.MariaDbCacheMetadata) != 0;
+			SupportsQueryAttributes = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.QueryAttributes) != 0;
+			SupportsSessionTrack = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.SessionTrack) != 0;
+			var serverSupportsSsl = (initialHandshake.ProtocolCapabilities & ProtocolCapabilities.Ssl) != 0;
+			m_characterSet = ServerVersion.Version >= ServerVersions.SupportsUtf8Mb4 ? CharacterSet.Utf8Mb4GeneralCaseInsensitive : CharacterSet.Utf8Mb3GeneralCaseInsensitive;
+			m_setNamesPayload = ServerVersion.Version >= ServerVersions.SupportsUtf8Mb4 ?
+				(SupportsQueryAttributes ? s_setNamesUtf8mb4WithAttributesPayload : s_setNamesUtf8mb4NoAttributesPayload) :
+				(SupportsQueryAttributes ? s_setNamesUtf8WithAttributesPayload : s_setNamesUtf8NoAttributesPayload);
+
+			// disable pipelining for RDS MySQL 5.7 (assuming Aurora); otherwise take it from the connection string or default to true
+			if (!cs.Pipelining.HasValue && ServerVersion.Version.Major == 5 && ServerVersion.Version.Minor == 7 && HostName.EndsWith(".rds.amazonaws.com", StringComparison.OrdinalIgnoreCase))
+			{
+				Log.AutoDetectedAurora57(m_logger, Id, HostName);
+				m_supportsPipelining = false;
+			}
+			else
+			{
+				// pipelining is not currently compatible with compression
+				m_supportsPipelining = m_compressionMethod == CompressionMethod.None && cs.Pipelining is not false;
 
-					// for pipelining, concatenate reset connection and SET NAMES query into one buffer
-					if (m_supportsPipelining)
-					{
-						m_pipelinedResetConnectionBytes = new byte[m_setNamesPayload.Span.Length + 9];
+				// for pipelining, concatenate reset connection and SET NAMES query into one buffer
+				if (m_supportsPipelining)
+				{
+					m_pipelinedResetConnectionBytes = new byte[m_setNamesPayload.Span.Length + 9];
 
-						// first packet: reset connection
-						m_pipelinedResetConnectionBytes[0] = 1;
-						m_pipelinedResetConnectionBytes[4] = (byte) CommandKind.ResetConnection;
+					// first packet: reset connection
+					m_pipelinedResetConnectionBytes[0] = 1;
+					m_pipelinedResetConnectionBytes[4] = (byte) CommandKind.ResetConnection;
 
-						// second packet: SET NAMES query
-						m_pipelinedResetConnectionBytes[5] = (byte) m_setNamesPayload.Span.Length;
-						m_setNamesPayload.Span.CopyTo(m_pipelinedResetConnectionBytes.AsSpan()[9..]);
-					}
+					// second packet: SET NAMES query
+					m_pipelinedResetConnectionBytes[5] = (byte) m_setNamesPayload.Span.Length;
+					m_setNamesPayload.Span.CopyTo(m_pipelinedResetConnectionBytes.AsSpan()[9..]);
 				}
+			}
 
-				Log.Debug("Session{0} made connection; ServerVersion={1}; ConnectionId={2}; Compression={3}; Attributes={4}; DeprecateEof={5}; Ssl={6}; SessionTrack={7}; Pipelining={8}; QueryAttributes={9}",
-					m_logArguments[0], ServerVersion.OriginalString, ConnectionId,
-					m_useCompression, m_supportsConnectionAttributes, m_supportsDeprecateEof, serverSupportsSsl, m_supportsSessionTrack, m_supportsPipelining, SupportsQueryAttributes);
-
-				if (cs.SslMode != MySqlSslMode.None && (cs.SslMode != MySqlSslMode.Preferred || serverSupportsSsl))
-				{
-					if (!serverSupportsSsl)
-					{
-						Log.Error("Session{0} requires SSL but server doesn't support it", m_logArguments);
-						throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Server does not support SSL");
-					}
+			Log.SessionMadeConnection(m_logger, Id, ServerVersion.OriginalString, ConnectionId, m_compressionMethod != CompressionMethod.None, m_supportsConnectionAttributes, SupportsDeprecateEof, SupportsCachedPreparedMetadata, serverSupportsSsl, SupportsSessionTrack, m_supportsPipelining, SupportsQueryAttributes);
 
-					try
-					{
-						await InitSslAsync(initialHandshake.ProtocolCapabilities, cs, connection, sslProtocols, ioBehavior, cancellationToken).ConfigureAwait(false);
-						shouldRetrySsl = false;
-						if (shouldUpdatePoolSslProtocols && Pool is not null)
-							Pool.SslProtocols = sslProtocols;
-					}
-					catch (ArgumentException ex) when (ex.ParamName == "sslProtocolType" && sslProtocols == SslProtocols.None)
-					{
-						Log.Debug(ex, "Session{0} doesn't support SslProtocols.None; falling back to explicitly specifying SslProtocols", m_logArguments);
-						sslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12;
-					}
-					catch (Exception ex) when (shouldRetrySsl && ((ex is MySqlException && ex.InnerException is AuthenticationException or IOException) || ex is AuthenticationException or IOException))
-					{
-						// negotiating TLS 1.2 with a yaSSL-based server throws an exception on Windows, see comment at top of method
-						Log.Warn(ex, "Session{0} failed negotiating TLS; falling back to TLS 1.1", m_logArguments);
-						sslProtocols = sslProtocols == SslProtocols.None ? SslProtocols.Tls | SslProtocols.Tls11 : (SslProtocols.Tls | SslProtocols.Tls11) & sslProtocols;
-						shouldUpdatePoolSslProtocols = true;
-					}
-				}
-				else
+			if (cs.SslMode != MySqlSslMode.None && (cs.SslMode != MySqlSslMode.Preferred || serverSupportsSsl))
+			{
+				if (!serverSupportsSsl)
 				{
-					shouldRetrySsl = false;
+					Log.ServerDoesNotSupportSsl(m_logger, Id);
+					throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Server does not support SSL");
 				}
-			} while (shouldRetrySsl);
+
+				await InitSslAsync(initialHandshake.ProtocolCapabilities, cs, connection, cs.TlsVersions, ioBehavior, cancellationToken).ConfigureAwait(false);
+			}
 
 			if (m_supportsConnectionAttributes && cs.ConnectionAttributes is null)
 				cs.ConnectionAttributes = CreateConnectionAttributes(cs.ApplicationName);
 
 			var password = GetPassword(cs, connection);
-			using (var handshakeResponsePayload = HandshakeResponse41Payload.Create(initialHandshake, cs, password, m_useCompression, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null))
+
+			// send a caching_sha2_password response if the server advertised support in the initial handshake
+			var useCachingSha2 = initialHandshake.AuthPluginName == "caching_sha2_password";
+			byte[] authenticationResponse;
+			if (useCachingSha2)
+				authenticationResponse = AuthenticationUtility.CreateScrambleResponse(Utility.TrimZeroByte(initialHandshake.AuthPluginData.AsSpan()), password);
+			else
+				AuthenticationUtility.CreateResponseAndPasswordHash(password, initialHandshake.AuthPluginData, out authenticationResponse, out m_passwordHash);
+
+			using (var handshakeResponsePayload = HandshakeResponse41Payload.Create(initialHandshake, cs, authenticationResponse, m_compressionMethod, connection.ZstandardPlugin?.CompressionLevel, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null))
 				await SendReplyAsync(handshakeResponsePayload, ioBehavior, cancellationToken).ConfigureAwait(false);
 			payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
 
 			// if server doesn't support the authentication fast path, it will send a new challenge
-			if (payload.HeaderByte == AuthenticationMethodSwitchRequestPayload.Signature)
+			while (payload.HeaderByte == AuthenticationMethodSwitchRequestPayload.Signature)
 			{
 				payload = await SwitchAuthenticationAsync(cs, password, payload, ioBehavior, cancellationToken).ConfigureAwait(false);
 			}
 
-			var ok = OkPayload.Create(payload.Span, SupportsDeprecateEof, SupportsSessionTrack);
-			statusInfo = ok.StatusInfo;
+			// check if caching_sha2_password response was sent
+			if (useCachingSha2 && payload.HeaderByte == CachingSha2ServerResponsePayload.Signature)
+			{
+				// process a successful response, or fall back to sending the password if the server doesn't have it
+				var cachingSha2ServerResponsePayload = CachingSha2ServerResponsePayload.Create(payload.Span);
+				if (cachingSha2ServerResponsePayload.Succeeded)
+				{
+					payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+				}
+				else if (!m_isSecureConnection && password.Length != 0)
+				{
+					var publicKey = await GetRsaPublicKeyAsync(currentAuthenticationMethod, cs, ioBehavior, cancellationToken).ConfigureAwait(false);
+					payload = await SendEncryptedPasswordAsync(AuthPluginData, publicKey, password, ioBehavior, cancellationToken).ConfigureAwait(false);
+				}
+				else
+				{
+					payload = await SendClearPasswordAsync(password, ioBehavior, cancellationToken).ConfigureAwait(false);
+				}
+			}
+
+			var ok = OkPayload.Create(payload.Span, this);
 
-			if (m_useCompression)
+			if (m_sslPolicyErrors != SslPolicyErrors.None)
+			{
+				// SSL would normally have thrown error, but this was suppressed in ValidateRemoteCertificate; now we need to verify the server certificate
+				// pass only if :
+				// * connection method is MitM-proof (e.g. unix socket)
+				// * auth plugin is MitM-proof and check SHA2(user's hashed password, scramble, certificate fingerprint)
+				// see https://mariadb.org/mission-impossible-zero-configuration-ssl/
+				var ignoreCertificateError = false;
+
+				if (cs.ConnectionProtocol == MySqlConnectionProtocol.UnixSocket)
+				{
+					Log.CertificateErrorUnixSocket(m_logger, Id, m_sslPolicyErrors);
+					ignoreCertificateError = true;
+				}
+				else if (string.IsNullOrEmpty(password))
+				{
+					// there is no shared secret that can be used to validate the certificate
+					Log.CertificateErrorNoPassword(m_logger, Id, m_sslPolicyErrors);
+				}
+				else if (ValidateFingerprint(ok.StatusInfo, initialHandshake.AuthPluginData.AsSpan(0, 20)))
+				{
+					Log.CertificateErrorValidThumbprint(m_logger, Id, m_sslPolicyErrors);
+					ignoreCertificateError = true;
+				}
+
+				if (!ignoreCertificateError)
+				{
+					ShutdownSocket();
+					HostName = "";
+					lock (m_lock)
+						m_state = State.Failed;
+
+					// throw a MySqlException with an AuthenticationException InnerException to mimic what would have happened if ValidateRemoteCertificate returned false
+					var innerException = new AuthenticationException($"The remote certificate was rejected due to the following error: {m_sslPolicyErrors}");
+					Log.CouldNotInitializeTlsConnection(m_logger, innerException, Id);
+					throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "SSL Authentication Error", innerException);
+				}
+			}
+
+			var redirectionUrl = ok.RedirectionUrl;
+
+			if (m_compressionMethod == CompressionMethod.Zlib)
 				m_payloadHandler = new CompressedPayloadHandler(m_payloadHandler.ByteHandler);
+			else if (m_compressionMethod == CompressionMethod.Zstandard)
+				m_payloadHandler = connection.ZstandardPlugin!.CreatePayloadHandler(m_payloadHandler.ByteHandler);
 
-			// set 'collation_connection' to the server default
-			await SendAsync(m_setNamesPayload, ioBehavior, cancellationToken).ConfigureAwait(false);
-			payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
-			OkPayload.Create(payload.Span, SupportsDeprecateEof, SupportsSessionTrack);
+			// send 'SET NAMES' to set the character set and collation unless the server reports that it's already using the desired character set (e.g., MariaDB >= 11.5)
+			if (ok.NewCharacterSet != (ServerVersion.Version >= ServerVersions.SupportsUtf8Mb4 ? CharacterSet.Utf8Mb4Binary : CharacterSet.Utf8Mb3Binary))
+			{
+				// set 'collation_connection' to the server default
+				await SendAsync(m_setNamesPayload, ioBehavior, cancellationToken).ConfigureAwait(false);
+				payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+				OkPayload.Verify(payload.Span, this);
+			}
 
 			if (ShouldGetRealServerDetails(cs))
+			{
 				await GetRealServerDetailsAsync(ioBehavior, CancellationToken.None).ConfigureAwait(false);
+			}
+			else if (ok.NewConnectionId is int newConnectionId && newConnectionId != ConnectionId)
+			{
+				Log.ChangingConnectionId(m_logger, Id, ConnectionId, newConnectionId, ServerVersion.OriginalString, ServerVersion.OriginalString);
+				ConnectionId = newConnectionId;
+			}
 
 			m_payloadHandler.ByteHandler.RemainingTimeout = Constants.InfiniteTimeout;
+			return redirectionUrl;
 		}
 		catch (ArgumentException ex)
 		{
-			Log.Error(ex, "Session{0} couldn't connect to server", m_logArguments);
+			Log.CouldNotConnectToServer(m_logger, ex, Id);
 			throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Couldn't connect to server", ex);
 		}
 		catch (IOException ex)
 		{
-			Log.Error(ex, "Session{0} couldn't connect to server", m_logArguments);
+			Log.CouldNotConnectToServer(m_logger, ex, Id);
 			throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Couldn't connect to server", ex);
 		}
+	}
+
+	/// <summary>
+	/// Validate SSL validation hash (from OK packet).
+	/// </summary>
+	/// <param name="validationHash">The validation hash received from the server.</param>
+	/// <param name="challenge">The auth plugin data from the initial handshake.</param>
+	/// <returns><c>true</c> if the validation hash matches the locally-computed value; otherwise, <c>false</c>.</returns>
+	private bool ValidateFingerprint(byte[]? validationHash, ReadOnlySpan<byte> challenge)
+	{
+		// expect 0x01 followed by 64 hex characters giving a SHA2 hash
+		if (validationHash?.Length != 65 || validationHash[0] != 1)
+			return false;
 
-		return statusInfo;
+		// the authentication plugin must have provided a password hash (via IAuthenticationPlugin3) that we saved for future use
+		if (m_passwordHash is null)
+			return false;
+
+		// hash password hash || scramble || certificate thumbprint
+		Span<byte> hashBytes = stackalloc byte[32];
+		Span<byte> combined = [.. m_passwordHash, .. challenge, .. m_remoteCertificateSha2Thumbprint!];
+#if NET5_0_OR_GREATER
+		SHA256.TryHashData(combined, hashBytes, out _);
+#else
+		using var sha256 = SHA256.Create();
+		sha256.TryComputeHash(combined, hashBytes, out _);
+#endif
+
+		Span<byte> serverHash = combined[0..32];
+		return TryConvertFromHexString(validationHash.AsSpan(1), serverHash) && serverHash.SequenceEqual(hashBytes);
+
+		static bool TryConvertFromHexString(ReadOnlySpan<byte> hexChars, Span<byte> data)
+		{
+			ReadOnlySpan<byte> hexDigits = "0123456789ABCDEFabcdef"u8;
+			for (var i = 0; i < hexChars.Length; i += 2)
+			{
+				var high = hexDigits.IndexOf(hexChars[i]);
+				var low = hexDigits.IndexOf(hexChars[i + 1]);
+				if (high == -1 || low == -1)
+					return false;
+				if (high > 15)
+					high -= 6;
+				if (low > 15)
+					low -= 6;
+				data[i / 2] = (byte) ((high << 4) | low);
+			}
+			return true;
+		}
+	}
+
+	public static async ValueTask<ServerSession> ConnectAndRedirectAsync(ILogger connectionLogger, ILogger poolLogger, IConnectionPoolMetadata pool, ConnectionSettings cs, ILoadBalancer? loadBalancer, MySqlConnection connection, Action<ILogger, int, string, Exception?>? logMessage, long startingTimestamp, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken)
+	{
+		var session = new ServerSession(connectionLogger, pool);
+		if (logMessage is not null && poolLogger.IsEnabled(LogLevel.Debug))
+			logMessage(poolLogger, pool.Id, session.Id, null);
+
+		string? redirectionUrl;
+		try
+		{
+			redirectionUrl = await session.ConnectAsync(cs, connection, startingTimestamp, loadBalancer, activity, ioBehavior, cancellationToken).ConfigureAwait(false);
+		}
+		catch (Exception)
+		{
+			await session.DisposeAsync(ioBehavior, default).ConfigureAwait(false);
+			throw;
+		}
+
+		Exception? redirectionException = null;
+		if (redirectionUrl is not null)
+		{
+			Log.HasServerRedirectionHeader(connectionLogger, session.Id, redirectionUrl);
+			if (cs.ServerRedirectionMode == MySqlServerRedirectionMode.Disabled)
+			{
+				Log.ServerRedirectionIsDisabled(connectionLogger, session.Id);
+				return session;
+			}
+
+			if (Utility.TryParseRedirectionHeader(redirectionUrl, cs.UserID, out var host, out var port, out var user))
+			{
+				if (host != cs.HostNames![0] || port != cs.Port || user != cs.UserID)
+				{
+					var redirectedSettings = cs.CloneWith(host, port, user);
+					Log.OpeningNewConnection(connectionLogger, session.Id, host, port, user);
+					var redirectedSession = new ServerSession(connectionLogger, pool);
+					try
+					{
+						await redirectedSession.ConnectAsync(redirectedSettings, connection, startingTimestamp, loadBalancer, activity, ioBehavior, cancellationToken).ConfigureAwait(false);
+						Log.ClosingSessionToUseRedirectedSession(connectionLogger, session.Id, redirectedSession.Id);
+						await session.DisposeAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+						return redirectedSession;
+					}
+					catch (Exception ex)
+					{
+						redirectionException = ex;
+						Log.FailedToConnectRedirectedSession(connectionLogger, ex, session.Id, redirectedSession.Id);
+						try
+						{
+							await redirectedSession.DisposeAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+						}
+						catch (Exception)
+						{
+						}
+					}
+				}
+				else
+				{
+					Log.SessionAlreadyConnectedToServer(connectionLogger, session.Id);
+				}
+			}
+		}
+
+		if (cs.ServerRedirectionMode == MySqlServerRedirectionMode.Required)
+		{
+			Log.RequiresServerRedirection(connectionLogger, session.Id);
+			throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Server does not support redirection", redirectionException);
+		}
+		return session;
 	}
 
 	public async Task<bool> TryResetConnectionAsync(ConnectionSettings cs, MySqlConnection connection, IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
 		VerifyState(State.Connected);
+		Conditions |= MySqlConnectionOpenedConditions.Reset;
 
 		try
 		{
@@ -615,87 +778,82 @@ public async Task<bool> TryResetConnectionAsync(ConnectionSettings cs, MySqlConn
 			ClearPreparedStatements();
 
 			PayloadData payload;
-			if (DatabaseOverride is null
-			    && ((!ServerVersion.IsMariaDb && ServerVersion.Version.CompareTo(ServerVersions.SupportsResetConnection) >= 0)
-			    || (ServerVersion.IsMariaDb && ServerVersion.Version.CompareTo(ServerVersions.MariaDbSupportsResetConnection) >= 0)))
+			if (DatabaseOverride is null &&
+				((!ServerVersion.IsMariaDb && ServerVersion.Version.CompareTo(ServerVersions.SupportsResetConnection) >= 0) ||
+				(ServerVersion.IsMariaDb && ServerVersion.Version.CompareTo(ServerVersions.MariaDbSupportsResetConnection) >= 0)))
 			{
-				m_logArguments[1] = ServerVersion.OriginalString;
-
 				if (m_supportsPipelining)
 				{
-					Log.Trace("Session{0} ServerVersion={1} supports reset connection and pipelining; sending pipelined reset connection request", m_logArguments);
+					Log.SendingPipelinedResetConnectionRequest(m_logger, Id, ServerVersion.OriginalString);
 
 					// send both packets at once
-					await m_payloadHandler!.ByteHandler.WriteBytesAsync(m_pipelinedResetConnectionBytes!, ioBehavior).ConfigureAwait(false);
+					await SendRawAsync(m_pipelinedResetConnectionBytes, ioBehavior, cancellationToken).ConfigureAwait(false);
 
 					// read two OK replies
-					m_payloadHandler.SetNextSequenceNumber(1);
-					payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
-					OkPayload.Create(payload.Span, SupportsDeprecateEof, SupportsSessionTrack);
+					payload = await ReceiveReplyAsync(1, ioBehavior, cancellationToken).ConfigureAwait(false);
+					OkPayload.Verify(payload.Span, this);
 
-					m_payloadHandler.SetNextSequenceNumber(1);
-					payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
-					OkPayload.Create(payload.Span, SupportsDeprecateEof, SupportsSessionTrack);
+					payload = await ReceiveReplyAsync(1, ioBehavior, cancellationToken).ConfigureAwait(false);
+					OkPayload.Verify(payload.Span, this);
 
 					return true;
 				}
 
-				Log.Trace("Session{0} ServerVersion={1} supports reset connection; sending reset connection request", m_logArguments);
+				Log.SendingResetConnectionRequest(m_logger, Id, ServerVersion.OriginalString);
 				await SendAsync(ResetConnectionPayload.Instance, ioBehavior, cancellationToken).ConfigureAwait(false);
 				payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
-				OkPayload.Create(payload.Span, SupportsDeprecateEof, SupportsSessionTrack);
+				OkPayload.Verify(payload.Span, this);
 			}
 			else
 			{
 				// optimistically hash the password with the challenge from the initial handshake (supported by MariaDB; doesn't appear to be supported by MySQL)
 				if (DatabaseOverride is null)
 				{
-					m_logArguments[1] = ServerVersion.OriginalString;
-					Log.Trace("Session{0} ServerVersion={1} doesn't support reset connection; sending change user request", m_logArguments);
+					Log.SendingChangeUserRequest(m_logger, Id, ServerVersion.OriginalString);
 				}
 				else
 				{
-					m_logArguments[1] = DatabaseOverride;
-					Log.Debug("Session{0} sending change user request due to changed Database={1}", m_logArguments);
+					Log.SendingChangeUserRequestDueToChangedDatabase(m_logger, Id, DatabaseOverride);
 					DatabaseOverride = null;
 				}
 				var password = GetPassword(cs, connection);
-				var hashedPassword = AuthenticationUtility.CreateAuthenticationResponse(AuthPluginData!, password);
-				using (var changeUserPayload = ChangeUserPayload.Create(cs.UserID, hashedPassword, cs.Database, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null))
+				AuthenticationUtility.CreateResponseAndPasswordHash(password, AuthPluginData, out var nativeResponse, out m_passwordHash);
+				using (var changeUserPayload = ChangeUserPayload.Create(cs.UserID, nativeResponse, cs.Database, m_characterSet, m_supportsConnectionAttributes ? cs.ConnectionAttributes : null))
 					await SendAsync(changeUserPayload, ioBehavior, cancellationToken).ConfigureAwait(false);
 				payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
 				if (payload.HeaderByte == AuthenticationMethodSwitchRequestPayload.Signature)
 				{
-					Log.Trace("Session{0} optimistic reauthentication failed; logging in again", m_logArguments);
+					Log.OptimisticReauthenticationFailed(m_logger, Id);
 					payload = await SwitchAuthenticationAsync(cs, password, payload, ioBehavior, cancellationToken).ConfigureAwait(false);
 				}
-				OkPayload.Create(payload.Span, SupportsDeprecateEof, SupportsSessionTrack);
+				OkPayload.Verify(payload.Span, this);
 			}
 
 			// set 'collation_connection' to the server default
 			await SendAsync(m_setNamesPayload, ioBehavior, cancellationToken).ConfigureAwait(false);
 			payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
-			OkPayload.Create(payload.Span, SupportsDeprecateEof, SupportsSessionTrack);
+			OkPayload.Verify(payload.Span, this);
 
 			return true;
 		}
 		catch (IOException ex)
 		{
-			Log.Trace(ex, "Session{0} ignoring IOException in TryResetConnectionAsync", m_logArguments);
+			Log.IgnoringFailureInTryResetConnectionAsync(m_logger, ex, Id, "IOException");
 		}
 		catch (MySqlException ex) when (ex.ErrorCode == MySqlErrorCode.ClientInteractionTimeout)
 		{
-			Log.Trace(ex, "Session{0} ignoring ClientInteractionTimeout MySqlException in TryResetConnectionAsync", m_logArguments);
+			Log.IgnoringFailureInTryResetConnectionAsync(m_logger, ex, Id, "ClientInteractionTimeout MySqlException");
 		}
 		catch (ObjectDisposedException ex)
 		{
-			Log.Trace(ex, "Session{0} ignoring ObjectDisposedException in TryResetConnectionAsync", m_logArguments);
+			Log.IgnoringFailureInTryResetConnectionAsync(m_logger, ex, Id, "ObjectDisposedException");
 		}
 		catch (SocketException ex)
 		{
-			Log.Trace(ex, "Session{0} ignoring SocketException in TryResetConnectionAsync", m_logArguments);
+			Log.IgnoringFailureInTryResetConnectionAsync(m_logger, ex, Id, "SocketException");
 		}
 
+		Conditions &= ~MySqlConnectionOpenedConditions.Reset;
 		return false;
 	}
 
@@ -703,83 +861,98 @@ private async Task<PayloadData> SwitchAuthenticationAsync(ConnectionSettings cs,
 	{
 		// if the server didn't support the hashed password; rehash with the new challenge
 		var switchRequest = AuthenticationMethodSwitchRequestPayload.Create(payload.Span);
-		m_logArguments[1] = switchRequest.Name;
-		Log.Trace("Session{0} switching to AuthenticationMethod '{1}'", m_logArguments);
+		Log.SwitchingToAuthenticationMethod(m_logger, Id, switchRequest.Name);
 		switch (switchRequest.Name)
 		{
-		case "mysql_native_password":
-			AuthPluginData = switchRequest.Data;
-			var hashedPassword = AuthenticationUtility.CreateAuthenticationResponse(AuthPluginData, password);
-			payload = new(hashedPassword);
-			await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
-			return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+			case "mysql_native_password":
+				AuthPluginData = switchRequest.Data;
+				AuthenticationUtility.CreateResponseAndPasswordHash(password, AuthPluginData, out var nativeResponse, out m_passwordHash);
+				payload = new(nativeResponse);
+				await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
+				return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
 
-		case "mysql_clear_password":
-			if (!m_isSecureConnection)
-			{
-				Log.Error("Session{0} needs a secure connection to use AuthenticationMethod '{1}'", m_logArguments);
-				throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, $"Authentication method '{switchRequest.Name}' requires a secure connection.");
-			}
+			case "mysql_clear_password":
+				if (!m_isSecureConnection && !m_isLoopbackConnection)
+				{
+					Log.NeedsSecureConnection(m_logger, Id, switchRequest.Name);
+					throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, $"Authentication method '{switchRequest.Name}' requires a secure connection.");
+				}
 
-			// send the password as a NULL-terminated UTF-8 string
-			var passwordBytes = Encoding.UTF8.GetBytes(password);
-			Array.Resize(ref passwordBytes, passwordBytes.Length + 1);
-			payload = new(passwordBytes);
-			await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
-			return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+				// send the password as a NULL-terminated UTF-8 string
+				var passwordBytes = AuthenticationUtility.GetNullTerminatedPasswordBytes(password);
+				payload = new(passwordBytes);
+				await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
+				return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
 
-		case "caching_sha2_password":
-			var scrambleBytes = AuthenticationUtility.CreateScrambleResponse(Utility.TrimZeroByte(switchRequest.Data.AsSpan()), password);
-			payload = new(scrambleBytes);
-			await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
-			payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+			case "caching_sha2_password":
+				// see https://dev.mysql.com/doc/dev/mysql-server/latest/page_caching_sha2_authentication_exchanges.html
+				var scrambleBytes = AuthenticationUtility.CreateScrambleResponse(Utility.TrimZeroByte(switchRequest.Data.AsSpan()), password);
+				payload = new(scrambleBytes);
+				await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
+				payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
 
-			// OK payload can be sent immediately (e.g., if password is empty( (short-circuiting the )
-			if (OkPayload.IsOk(payload.Span, SupportsDeprecateEof))
-				return payload;
+				// OK payload can be sent immediately (e.g., if password is empty) bypassing even the fast authentication path
+				if (OkPayload.IsOk(payload.Span, this))
+					return payload;
 
-			var cachingSha2ServerResponsePayload = CachingSha2ServerResponsePayload.Create(payload.Span);
-			if (cachingSha2ServerResponsePayload.Succeeded)
-				return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+				var cachingSha2ServerResponsePayload = CachingSha2ServerResponsePayload.Create(payload.Span);
+				if (cachingSha2ServerResponsePayload.Succeeded)
+					return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
 
-			goto case "sha256_password";
+				goto case "sha256_password";
 
-		case "sha256_password":
-			if (!m_isSecureConnection && password.Length != 0)
-			{
-				var publicKey = await GetRsaPublicKeyAsync(switchRequest.Name, cs, ioBehavior, cancellationToken).ConfigureAwait(false);
-				return await SendEncryptedPasswordAsync(switchRequest, publicKey, password, ioBehavior, cancellationToken).ConfigureAwait(false);
-			}
-			else
-			{
-				return await SendClearPasswordAsync(password, ioBehavior, cancellationToken).ConfigureAwait(false);
-			}
+			case "sha256_password":
+				if (!m_isSecureConnection && password.Length != 0)
+				{
+					var publicKey = await GetRsaPublicKeyAsync(switchRequest.Name, cs, ioBehavior, cancellationToken).ConfigureAwait(false);
+					return await SendEncryptedPasswordAsync(switchRequest.Data, publicKey, password, ioBehavior, cancellationToken).ConfigureAwait(false);
+				}
+				else
+				{
+					return await SendClearPasswordAsync(password, ioBehavior, cancellationToken).ConfigureAwait(false);
+				}
 
-		case "auth_gssapi_client":
-			return await AuthGSSAPI.AuthenticateAsync(cs, switchRequest.Data, this, ioBehavior, cancellationToken).ConfigureAwait(false);
+			case "auth_gssapi_client":
+				return await AuthGSSAPI.AuthenticateAsync(cs, switchRequest.Data, this, ioBehavior, cancellationToken).ConfigureAwait(false);
 
-		case "mysql_old_password":
-			Log.Error("Session{0} is requesting AuthenticationMethod '{1}' which is not supported", m_logArguments);
-			throw new NotSupportedException("'MySQL Server is requesting the insecure pre-4.1 auth mechanism (mysql_old_password). The user password must be upgraded; see https://dev.mysql.com/doc/refman/5.7/en/account-upgrades.html.");
+			case "mysql_old_password":
+				Log.AuthenticationMethodNotSupported(m_logger, Id, switchRequest.Name);
+				throw new NotSupportedException("'MySQL Server is requesting the insecure pre-4.1 auth mechanism (mysql_old_password). The user password must be upgraded; see https://dev.mysql.com/doc/refman/5.7/en/account-upgrades.html.");
 
-		case "client_ed25519":
-			if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var ed25519Plugin))
-				throw new NotSupportedException("You must install the MySqlConnector.Authentication.Ed25519 package and call Ed25519AuthenticationPlugin.Install to use client_ed25519 authentication.");
-			payload = new(ed25519Plugin.CreateResponse(password, switchRequest.Data));
-			await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
-			return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+			case "client_ed25519":
+				if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var ed25519Plugin) || ed25519Plugin is not IAuthenticationPlugin3 ed25519Plugin3)
+					throw new NotSupportedException("You must install the MySqlConnector.Authentication.Ed25519 package and call Ed25519AuthenticationPlugin.Install to use client_ed25519 authentication.");
+				ed25519Plugin3.CreateResponseAndPasswordHash(password, switchRequest.Data, out var ed25519Response, out m_passwordHash);
+				payload = new(ed25519Response);
+				await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
+				return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+
+			case "parsec":
+				if (!AuthenticationPlugins.TryGetPlugin(switchRequest.Name, out var parsecPlugin) || parsecPlugin is not IAuthenticationPlugin3 parsecPlugin3)
+					throw new NotSupportedException("You must install the MySqlConnector.Authentication.Ed25519 package and call ParsecAuthenticationPlugin.Install to use parsec authentication.");
+				payload = new([]);
+				await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
+				payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+
+				Span<byte> combinedData = stackalloc byte[switchRequest.Data.Length + payload.Span.Length];
+				switchRequest.Data.CopyTo(combinedData);
+				payload.Span.CopyTo(combinedData.Slice(switchRequest.Data.Length));
 
-		default:
-			Log.Error("Session{0} is requesting AuthenticationMethod '{1}' which is not supported", m_logArguments);
-			throw new NotSupportedException($"Authentication method '{switchRequest.Name}' is not supported.");
+				parsecPlugin3.CreateResponseAndPasswordHash(password, combinedData, out var parsecResponse, out m_passwordHash);
+				payload = new(parsecResponse);
+				await SendReplyAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
+				return await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+
+			default:
+				Log.AuthenticationMethodNotSupported(m_logger, Id, switchRequest.Name);
+				throw new NotSupportedException($"Authentication method '{switchRequest.Name}' is not supported.");
 		}
 	}
 
 	private async Task<PayloadData> SendClearPasswordAsync(string password, IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
 		// add NUL terminator to password
-		var passwordBytes = Encoding.UTF8.GetBytes(password);
-		Array.Resize(ref passwordBytes, passwordBytes.Length + 1);
+		var passwordBytes = AuthenticationUtility.GetNullTerminatedPasswordBytes(password);
 
 		// send plaintext password
 		var payload = new PayloadData(passwordBytes);
@@ -788,7 +961,7 @@ private async Task<PayloadData> SendClearPasswordAsync(string password, IOBehavi
 	}
 
 	private async Task<PayloadData> SendEncryptedPasswordAsync(
-		AuthenticationMethodSwitchRequestPayload switchRequest,
+		byte[] switchRequestData,
 		string rsaPublicKey,
 		string password,
 		IOBehavior ioBehavior,
@@ -802,7 +975,7 @@ private async Task<PayloadData> SendEncryptedPasswordAsync(
 		}
 		catch (Exception ex)
 		{
-			Log.Error(ex, "Session{0} couldn't load server's RSA public key", m_logArguments);
+			Log.CouldNotLoadServerRsaPublicKey(m_logger, ex, Id);
 			throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Couldn't load server's RSA public key; try using a secure connection instead.", ex);
 		}
 #else
@@ -814,7 +987,7 @@ private async Task<PayloadData> SendEncryptedPasswordAsync(
 		}
 		catch (Exception ex)
 		{
-			Log.Error(ex, "Session{0} couldn't load server's RSA public key", m_logArguments);
+			Log.CouldNotLoadServerRsaPublicKey(m_logger, ex, Id);
 			throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Couldn't load server's RSA public key; try using a secure connection instead.", ex);
 		}
 
@@ -822,11 +995,10 @@ private async Task<PayloadData> SendEncryptedPasswordAsync(
 #endif
 
 		// add NUL terminator to password
-		var passwordBytes = Encoding.UTF8.GetBytes(password);
-		Array.Resize(ref passwordBytes, passwordBytes.Length + 1);
+		var passwordBytes = AuthenticationUtility.GetNullTerminatedPasswordBytes(password);
 
 		// XOR the password bytes with the challenge
-		AuthPluginData = Utility.TrimZeroByte(switchRequest.Data);
+		AuthPluginData = Utility.TrimZeroByte(switchRequestData);
 		for (var i = 0; i < passwordBytes.Length; i++)
 			passwordBytes[i] ^= AuthPluginData[i % AuthPluginData.Length];
 
@@ -848,24 +1020,22 @@ private async Task<string> GetRsaPublicKeyAsync(string switchRequestName, Connec
 			}
 			catch (IOException ex)
 			{
-				m_logArguments[1] = cs.ServerRsaPublicKeyFile;
-				Log.Error(ex, "Session{0} couldn't load server's RSA public key from PublicKeyFile '{1}'", m_logArguments);
+				Log.CouldNotLoadServerRsaPublicKeyFromFile(m_logger, ex, Id, cs.ServerRsaPublicKeyFile);
 				throw new MySqlException($"Couldn't load server's RSA public key from '{cs.ServerRsaPublicKeyFile}'", ex);
 			}
 		}
 
-		if (cs.AllowPublicKeyRetrieval)
+		if (cs.AllowPublicKeyRetrieval || m_isLoopbackConnection)
 		{
 			// request the RSA public key
 			var payloadContent = switchRequestName == "caching_sha2_password" ? (byte) 0x02 : (byte) 0x01;
-			await SendReplyAsync(new PayloadData(new[] { payloadContent }), ioBehavior, cancellationToken).ConfigureAwait(false);
+			await SendReplyAsync(new PayloadData([payloadContent]), ioBehavior, cancellationToken).ConfigureAwait(false);
 			var payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
 			var publicKeyPayload = AuthenticationMoreDataPayload.Create(payload.Span);
 			return Encoding.ASCII.GetString(publicKeyPayload.Data);
 		}
 
-		m_logArguments[1] = switchRequestName;
-		Log.Error("Session{0} couldn't use AuthenticationMethod '{1}' because RSA key wasn't specified or couldn't be retrieved", m_logArguments);
+		Log.CouldNotUseAuthenticationMethodForRsa(m_logger, Id, switchRequestName);
 		throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, $"Authentication method '{switchRequestName}' failed. Either use a secure connection, specify the server's RSA public key with ServerRSAPublicKeyFile, or set AllowPublicKeyRetrieval=True.");
 	}
 
@@ -876,27 +1046,24 @@ public async ValueTask<bool> TryPingAsync(bool logInfo, IOBehavior ioBehavior, C
 		// send ping payload to verify client and server socket are still connected
 		try
 		{
-			Log.Trace("Session{0} pinging server", m_logArguments);
+			Log.PingingServer(m_logger, Id);
 			await SendAsync(PingPayload.Instance, ioBehavior, cancellationToken).ConfigureAwait(false);
 			var payload = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
-			OkPayload.Create(payload.Span, SupportsDeprecateEof, SupportsSessionTrack);
-			if (logInfo)
-				Log.Info("Session{0} successfully pinged server", m_logArguments);
-			else
-				Log.Trace("Session{0} successfully pinged server", m_logArguments);
+			OkPayload.Verify(payload.Span, this);
+			Log.SuccessfullyPingedServer(m_logger, logInfo ? LogLevel.Information : LogLevel.Trace, Id);
 			return true;
 		}
 		catch (IOException ex)
 		{
-			Log.Trace(ex, "Session{0} ping failed due to IOException", m_logArguments);
+			Log.PingFailed(m_logger, ex, Id, "IOException");
 		}
 		catch (MySqlException ex) when (ex.ErrorCode == MySqlErrorCode.ClientInteractionTimeout)
 		{
-			Log.Trace(ex, "Session{0} ping failed due to ClientInteractionTimeout MySqlException", m_logArguments);
+			Log.PingFailed(m_logger, ex, Id, "ClientInteractionTimeout MySqlException");
 		}
 		catch (SocketException ex)
 		{
-			Log.Trace(ex, "Session{0} ping failed due to SocketException", m_logArguments);
+			Log.PingFailed(m_logger, ex, Id, "SocketException");
 		}
 
 		VerifyState(State.Failed);
@@ -918,81 +1085,70 @@ public ValueTask<PayloadData> ReceiveAsync(IOBehavior ioBehavior, CancellationTo
 	}
 
 	// Continues a conversation with the server by receiving a response to a packet sent with 'Send' or 'SendReply'.
-	public ValueTask<PayloadData> ReceiveReplyAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
+	public async ValueTask<PayloadData> ReceiveReplyAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
-		ValueTask<ArraySegment<byte>> task;
-		try
+		if (CreateExceptionForInvalidState() is { } exception)
 		{
-			VerifyConnected();
-			task = m_payloadHandler!.ReadPayloadAsync(m_payloadCache, ProtocolErrorBehavior.Throw, ioBehavior);
+			Log.FailedInReceiveReplyAsync(m_logger, exception, Id);
+			throw exception;
 		}
-		catch (Exception ex)
-		{
-			Log.Debug(ex, "Session{0} failed in ReceiveReplyAsync", m_logArguments);
-			if ((ex as MySqlException)?.ErrorCode == MySqlErrorCode.CommandTimeoutExpired)
-				HandleTimeout();
-			task = ValueTaskExtensions.FromException<ArraySegment<byte>>(ex);
-		}
-
-		if (task.IsCompletedSuccessfully)
-		{
-			var payload = new PayloadData(task.Result);
-			if (payload.HeaderByte != ErrorPayload.Signature)
-				return new ValueTask<PayloadData>(payload);
-
-			var exception = CreateExceptionForErrorPayload(payload.Span);
-			return ValueTaskExtensions.FromException<PayloadData>(exception);
-		}
-
-		return ReceiveReplyAsyncAwaited(task);
-	}
 
-	private async ValueTask<PayloadData> ReceiveReplyAsyncAwaited(ValueTask<ArraySegment<byte>> task)
-	{
 		ArraySegment<byte> bytes;
 		try
 		{
-			bytes = await task.ConfigureAwait(false);
+			bytes = await m_payloadHandler!.ReadPayloadAsync(m_payloadCache, ProtocolErrorBehavior.Throw, ioBehavior).ConfigureAwait(false);
 		}
 		catch (Exception ex)
 		{
+			if (ex is MySqlEndOfStreamException endOfStream)
+				Log.ExpectedToReadMoreBytes(m_logger, Id, endOfStream.ExpectedByteCount, endOfStream.ReadByteCount);
 			SetFailed(ex);
-			if (ex is MySqlException { ErrorCode: MySqlErrorCode.CommandTimeoutExpired })
-				HandleTimeout();
 			throw;
 		}
+
 		var payload = new PayloadData(bytes);
-		if (payload.HeaderByte == ErrorPayload.Signature)
-			throw CreateExceptionForErrorPayload(payload.Span);
-		return payload;
+		return payload.HeaderByte == ErrorPayload.Signature ? throw CreateExceptionForErrorPayload(payload.Span) : payload;
+	}
+
+	public ValueTask<PayloadData> ReceiveReplyAsync(int expectedSequenceNumber, IOBehavior ioBehavior, CancellationToken cancellationToken)
+	{
+		m_payloadHandler!.SetNextSequenceNumber(expectedSequenceNumber);
+		return ReceiveReplyAsync(ioBehavior, cancellationToken);
 	}
 
 	// Continues a conversation with the server by sending a reply to a packet received with 'Receive' or 'ReceiveReply'.
-	public ValueTask SendReplyAsync(PayloadData payload, IOBehavior ioBehavior, CancellationToken cancellationToken)
+	public async ValueTask SendReplyAsync(PayloadData payload, IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
-		ValueTask task;
+		if (CreateExceptionForInvalidState() is { } exception)
+		{
+			Log.FailedInSendReplyAsync(m_logger, exception, Id);
+			throw exception;
+		}
+
 		try
 		{
-			VerifyConnected();
-			task = m_payloadHandler!.WritePayloadAsync(payload.Memory, ioBehavior);
+			await m_payloadHandler!.WritePayloadAsync(payload.Memory, ioBehavior).ConfigureAwait(false);
 		}
 		catch (Exception ex)
 		{
-			Log.Debug(ex, "Session{0} failed in SendReplyAsync", m_logArguments);
-			task = ValueTaskExtensions.FromException(ex);
+			SetFailed(ex);
+			throw;
 		}
-
-		if (task.IsCompletedSuccessfully)
-			return task;
-
-		return SendReplyAsyncAwaited(task);
 	}
 
-	private async ValueTask SendReplyAsyncAwaited(ValueTask task)
+	// Sends raw bytes over the wire without formatting them into a payload. The caller is expected to set the packet header(s) correctly.
+	public async ValueTask SendRawAsync(ReadOnlyMemory<byte> data, IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
+		if (CreateExceptionForInvalidState() is { } exception)
+		{
+			Log.FailedInSendReplyAsync(m_logger, exception, Id);
+			throw exception;
+		}
+
 		try
 		{
-			await task.ConfigureAwait(false);
+			// send both packets at once
+			await m_payloadHandler!.ByteHandler.WriteBytesAsync(data, ioBehavior).ConfigureAwait(false);
 		}
 		catch (Exception ex)
 		{
@@ -1009,24 +1165,20 @@ public static void ThrowIfStatementContainsDelimiter(MySqlException exception, I
 			var parser = new DelimiterSqlParser(command);
 			parser.Parse(command.CommandText);
 			if (parser.HasDelimiter)
-				throw new MySqlException(MySqlErrorCode.DelimiterNotSupported, "'DELIMITER' should not be used with MySqlConnector. See https://fl.vu/mysql-delimiter", exception);
+				throw new MySqlException(MySqlErrorCode.DelimiterNotSupported, "'DELIMITER' should not be used with MySqlConnector. See https://mysqlconnector.net/delimiter", exception);
 		}
 	}
 
-	internal void HandleTimeout()
-	{
-		if (OwningConnection is not null && OwningConnection.TryGetTarget(out var connection))
-			connection.SetState(ConnectionState.Closed);
-	}
-
-	private void VerifyConnected()
+	private InvalidOperationException? CreateExceptionForInvalidState()
 	{
 		lock (m_lock)
 		{
-			if (m_state == State.Closed)
-				throw new ObjectDisposedException(nameof(ServerSession));
-			if (m_state != State.Connected && m_state != State.Querying && m_state != State.CancelingQuery && m_state != State.ClearingPendingCancellation && m_state != State.Closing)
-				throw new InvalidOperationException("ServerSession is not connected.");
+			return m_state switch
+			{
+				State.Closed => new ObjectDisposedException(nameof(ServerSession)),
+				State.Connected or State.Querying or State.CancelingQuery or State.ClearingPendingCancellation or State.Closing => null,
+				_ => new InvalidOperationException("ServerSession is not connected."),
+			};
 		}
 	}
 
@@ -1053,18 +1205,25 @@ private async Task<bool> OpenTcpSocketAsync(ConnectionSettings cs, ILoadBalancer
 			IPAddress[] ipAddresses;
 			try
 			{
-				ipAddresses = ioBehavior == IOBehavior.Asynchronous
+				if (cs.IPAddress is { } ipAddress)
+				{
+					ipAddresses = [ipAddress];
+				}
+				else
+				{
+					ipAddresses = ioBehavior == IOBehavior.Asynchronous
 #if NET6_0_OR_GREATER
-					? await Dns.GetHostAddressesAsync(hostName, cancellationToken).ConfigureAwait(false)
+						? await Dns.GetHostAddressesAsync(hostName, cancellationToken).ConfigureAwait(false)
 #else
-					? await Dns.GetHostAddressesAsync(hostName).ConfigureAwait(false)
+						? await Dns.GetHostAddressesAsync(hostName).ConfigureAwait(false)
 #endif
-					: Dns.GetHostAddresses(hostName);
+						: Dns.GetHostAddresses(hostName);
+				}
 			}
 			catch (SocketException ex)
 			{
 				// name couldn't be resolved
-				Log.Warn("Session{0} failed to resolve HostName '{1}' ({2} of {3}): {4}", m_logArguments[0], hostName, hostNameIndex + 1, hostNames.Count, ex.Message);
+				Log.FailedToResolveHostName(m_logger, ex, Id, hostName, hostNameIndex + 1, hostNames.Count, ex.Message);
 				continue;
 			}
 
@@ -1073,8 +1232,7 @@ private async Task<bool> OpenTcpSocketAsync(ConnectionSettings cs, ILoadBalancer
 			{
 				var ipAddress = ipAddresses[ipAddressIndex];
 				var ipAddressString = ipAddress.ToString();
-				if (Log.IsTraceEnabled())
-					Log.Trace("Session{0} connecting to IpAddress {1} ({2} of {3}) for HostName '{4}' ({5} of {6})", m_logArguments[0], ipAddressString, ipAddressIndex + 1, ipAddresses.Length, hostName, hostNameIndex + 1, hostNames.Count);
+				Log.ConnectingToIpAddress(m_logger, Id, ipAddressString, ipAddressIndex + 1, ipAddresses.Length, hostName, hostNameIndex + 1, hostNames.Count);
 
 				// set activity tags for the current IP address/hostname
 				{
@@ -1099,13 +1257,17 @@ private async Task<bool> OpenTcpSocketAsync(ConnectionSettings cs, ILoadBalancer
 				{
 					tcpClient = new(ipAddress.AddressFamily);
 
-					using (cancellationToken.Register(() => tcpClient.Client?.Dispose()))
+					using (cancellationToken.Register(() => tcpClient?.Client?.Dispose()))
 					{
 						try
 						{
 							if (ioBehavior == IOBehavior.Asynchronous)
 							{
+#if NET5_0_OR_GREATER
+								await tcpClient.ConnectAsync(ipAddress, cs.Port, cancellationToken).ConfigureAwait(false);
+#else
 								await tcpClient.ConnectAsync(ipAddress, cs.Port).ConfigureAwait(false);
+#endif
 							}
 							else
 							{
@@ -1129,7 +1291,7 @@ private async Task<bool> OpenTcpSocketAsync(ConnectionSettings cs, ILoadBalancer
 						catch (Exception ex) when (cancellationToken.IsCancellationRequested && ex is ObjectDisposedException or SocketException)
 						{
 							SafeDispose(ref tcpClient);
-							Log.Info("Session{0} connect timeout expired connecting to IpAddress {1} for HostName '{2}'", m_logArguments[0], ipAddress, hostName);
+							Log.ConnectTimeoutExpired(m_logger, ex, Id, ipAddressString, hostName);
 							throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Connect Timeout expired.");
 						}
 					}
@@ -1144,20 +1306,20 @@ private async Task<bool> OpenTcpSocketAsync(ConnectionSettings cs, ILoadBalancer
 						lock (m_lock)
 							m_state = State.Failed;
 						if (hostNames.Count == 1 && ipAddresses.Length == 1)
-							Log.Info("Session{0} failed to connect to IpAddress {1} for HostName '{2}': {3}", m_logArguments[0], ipAddress, hostName, ex.Message);
+							Log.FailedToConnectToSingleIpAddress(m_logger, ex, Id, ipAddressString, hostName, ex.Message);
 						else
-							Log.Info("Session{0} failed to connect to IpAddress {1} ({2} of {3}) for HostName '{4}' ({5} of {6}): {7}", m_logArguments[0], ipAddress, ipAddressIndex + 1, ipAddresses.Length, hostName, hostNameIndex + 1, hostNames.Count, ex.Message);
+							Log.FailedToConnectToIpAddress(m_logger, ex, LogLevel.Information, Id, ipAddressString, ipAddressIndex + 1, ipAddresses.Length, hostName, hostNameIndex + 1, hostNames.Count, ex.Message);
 						throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Unable to connect to any of the specified MySQL hosts.");
 					}
 
-					Log.Trace("Session{0} failed to connect to IpAddress {1} ({2} of {3}) for HostName '{4}' ({5} of {6}): {7}", m_logArguments[0], ipAddress, ipAddressIndex + 1, ipAddresses.Length, hostName, hostNameIndex + 1, hostNames.Count, ex.Message);
+					Log.FailedToConnectToIpAddress(m_logger, ex, LogLevel.Trace, Id, ipAddressString, ipAddressIndex + 1, ipAddresses.Length, hostName, hostNameIndex + 1, hostNames.Count, ex.Message);
 					continue;
 				}
 
 				if (!tcpClient.Connected && cancellationToken.IsCancellationRequested)
 				{
 					SafeDispose(ref tcpClient);
-					Log.Info("Session{0} connect timeout expired connecting to IpAddress {1} for HostName '{2}'", m_logArguments[0], ipAddress, hostName);
+					Log.ConnectTimeoutExpired(m_logger, null, Id, ipAddressString, hostName);
 					throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Connect Timeout expired.");
 				}
 
@@ -1169,19 +1331,20 @@ private async Task<bool> OpenTcpSocketAsync(ConnectionSettings cs, ILoadBalancer
 					m_socket.NoDelay = true;
 					m_stream = m_tcpClient.GetStream();
 					m_socket.SetKeepAlive(cs.Keepalive);
+					m_isLoopbackConnection = IPAddress.IsLoopback(ipAddress);
 				}
 				catch (ObjectDisposedException) when (cancellationToken.IsCancellationRequested)
 				{
 					Utility.Dispose(ref m_stream);
 					SafeDispose(ref m_tcpClient);
 					SafeDispose(ref m_socket);
-					Log.Info("Session{0} connect timeout expired connecting to IpAddress {1} for HostName '{2}'", m_logArguments[0], ipAddress, hostName);
+					Log.ConnectTimeoutExpired(m_logger, null, Id, ipAddressString, hostName);
 					throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Connect Timeout expired.");
 				}
 
 				lock (m_lock)
 					m_state = State.Connected;
-				Log.Trace("Session{0} connected to IpAddress {1} for HostName '{2}' with local Port {3}", m_logArguments[0], ipAddress, hostName, (m_socket.LocalEndPoint as IPEndPoint)?.Port);
+				Log.ConnectedToIpAddress(m_logger, Id, ipAddressString, hostName, (m_socket.LocalEndPoint as IPEndPoint)?.Port);
 				return true;
 			}
 		}
@@ -1190,8 +1353,7 @@ private async Task<bool> OpenTcpSocketAsync(ConnectionSettings cs, ILoadBalancer
 
 	private async Task<bool> OpenUnixSocketAsync(ConnectionSettings cs, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
-		m_logArguments[1] = cs.UnixSocket;
-		Log.Trace("Session{0} connecting to UNIX Socket '{1}'", m_logArguments);
+		Log.ConnectingToUnixSocket(m_logger, Id, cs.UnixSocket!);
 
 		// set activity tags
 		{
@@ -1199,8 +1361,8 @@ private async Task<bool> OpenUnixSocketAsync(ConnectionSettings cs, Activity? ac
 			m_activityTags.Add(ActivitySourceHelper.NetPeerNameTagName, cs.UnixSocket);
 			if (activity is { IsAllDataRequested: true })
 			{
-				activity.SetTag(ActivitySourceHelper.NetTransportTagName, ActivitySourceHelper.NetTransportUnixValue);
-				activity.SetTag(ActivitySourceHelper.NetPeerNameTagName, cs.UnixSocket);
+				activity.SetTag(ActivitySourceHelper.NetTransportTagName, ActivitySourceHelper.NetTransportUnixValue)
+					.SetTag(ActivitySourceHelper.NetPeerNameTagName, cs.UnixSocket);
 			}
 		}
 
@@ -1208,7 +1370,7 @@ private async Task<bool> OpenUnixSocketAsync(ConnectionSettings cs, Activity? ac
 		var unixEp = new UnixDomainSocketEndPoint(cs.UnixSocket!);
 		try
 		{
-			using (cancellationToken.Register(() => socket.Dispose()))
+			using (cancellationToken.Register(socket.Dispose))
 			{
 				try
 				{
@@ -1223,7 +1385,7 @@ private async Task<bool> OpenUnixSocketAsync(ConnectionSettings cs, Activity? ac
 				}
 				catch (ObjectDisposedException) when (cancellationToken.IsCancellationRequested)
 				{
-					Log.Info("Session{0} connect timeout expired connecting to UNIX Socket '{1}'", m_logArguments);
+					Log.ConnectTimeoutExpiredForUnixSocket(m_logger, Id, cs.UnixSocket!);
 					throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Connect Timeout expired.");
 				}
 			}
@@ -1246,15 +1408,14 @@ private async Task<bool> OpenUnixSocketAsync(ConnectionSettings cs, Activity? ac
 		return false;
 	}
 
-	private async Task<bool> OpenNamedPipeAsync(ConnectionSettings cs, int startTickCount, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken)
+	private async Task<bool> OpenNamedPipeAsync(ConnectionSettings cs, long startingTimestamp, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
-		if (Log.IsTraceEnabled())
-			Log.Trace("Session{0} connecting to NamedPipe '{1}' on Server '{2}'", m_logArguments[0], cs.PipeName, cs.HostNames![0]);
+		Log.ConnectingToNamedPipe(m_logger, Id, cs.PipeName, cs.HostNames![0]);
 
 		// set activity tags
 		{
 			// see https://docs.microsoft.com/en-us/windows/win32/ipc/pipe-names for pipe name format
-			var pipeName = @"\\" + cs.HostNames![0] + @"\pipe\" + cs.PipeName;
+			var pipeName = $@"\\{cs.HostNames![0]}\pipe\{cs.PipeName}";
 			m_activityTags.Add(ActivitySourceHelper.NetTransportTagName, ActivitySourceHelper.NetTransportNamedPipeValue);
 			m_activityTags.Add(ActivitySourceHelper.NetPeerNameTagName, pipeName);
 			if (activity is { IsAllDataRequested: true })
@@ -1265,10 +1426,10 @@ private async Task<bool> OpenNamedPipeAsync(ConnectionSettings cs, int startTick
 		}
 
 		var namedPipeStream = new NamedPipeClientStream(cs.HostNames![0], cs.PipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
-		var timeout = Math.Max(1, cs.ConnectionTimeoutMilliseconds - unchecked(Environment.TickCount - startTickCount));
+		var timeout = Math.Max(1, cs.ConnectionTimeoutMilliseconds - Utility.GetElapsedMilliseconds(startingTimestamp));
 		try
 		{
-			using (cancellationToken.Register(() => namedPipeStream.Dispose()))
+			using (cancellationToken.Register(namedPipeStream.Dispose))
 			{
 				try
 				{
@@ -1279,8 +1440,7 @@ private async Task<bool> OpenNamedPipeAsync(ConnectionSettings cs, int startTick
 				}
 				catch (Exception ex) when ((ex is ObjectDisposedException && cancellationToken.IsCancellationRequested) || ex is TimeoutException)
 				{
-					m_logArguments[1] = cs.PipeName;
-					Log.Info("Session{0} connect timeout expired connecting to named pipe '{1}'", m_logArguments);
+					Log.ConnectTimeoutExpiredForNamedPipe(m_logger, ex, Id, cs.PipeName, cs.HostNames![0]);
 					throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Connect Timeout expired.");
 				}
 			}
@@ -1304,7 +1464,7 @@ private async Task<bool> OpenNamedPipeAsync(ConnectionSettings cs, int startTick
 
 	private async Task InitSslAsync(ProtocolCapabilities serverCapabilities, ConnectionSettings cs, MySqlConnection connection, SslProtocols sslProtocols, IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
-		Log.Trace("Session{0} initializing TLS connection", m_logArguments);
+		Log.InitializingTlsConnection(m_logger, Id);
 		X509CertificateCollection? clientCertificates = null;
 
 		if (cs.CertificateStoreLocation != MySqlCertificateStoreLocation.None)
@@ -1312,14 +1472,14 @@ private async Task InitSslAsync(ProtocolCapabilities serverCapabilities, Connect
 			try
 			{
 				var storeLocation = (cs.CertificateStoreLocation == MySqlCertificateStoreLocation.CurrentUser) ? StoreLocation.CurrentUser : StoreLocation.LocalMachine;
-				var store = new X509Store(StoreName.My, storeLocation);
+				using var store = new X509Store(StoreName.My, storeLocation);
 				store.Open(OpenFlags.ReadOnly | OpenFlags.OpenExistingOnly);
 
 				if (cs.CertificateThumbprint.Length == 0)
 				{
 					if (store.Certificates.Count == 0)
 					{
-						Log.Error("Session{0} no certificates were found in the certificate store", m_logArguments);
+						Log.NoCertificatesFound(m_logger, Id);
 						throw new MySqlException("No certificates were found in the certificate store");
 					}
 
@@ -1331,8 +1491,7 @@ private async Task InitSslAsync(ProtocolCapabilities serverCapabilities, Connect
 					var foundCertificates = store.Certificates.Find(X509FindType.FindByThumbprint, cs.CertificateThumbprint, requireValid);
 					if (foundCertificates.Count == 0)
 					{
-						m_logArguments[1] = cs.CertificateThumbprint;
-						Log.Error("Session{0} certificate with Thumbprint={1} not found in store", m_logArguments);
+						Log.CertificateNotFoundInStore(m_logger, Id, cs.CertificateThumbprint);
 						throw new MySqlException($"Certificate with Thumbprint {cs.CertificateThumbprint} not found");
 					}
 
@@ -1341,8 +1500,7 @@ private async Task InitSslAsync(ProtocolCapabilities serverCapabilities, Connect
 			}
 			catch (CryptographicException ex)
 			{
-				m_logArguments[1] = cs.CertificateStoreLocation;
-				Log.Error(ex, "Session{0} couldn't load certificate from CertificateStoreLocation={1}", m_logArguments);
+				Log.CouldNotLoadCertificate(m_logger, ex, Id, cs.CertificateStoreLocation);
 				throw new MySqlException("Certificate couldn't be loaded from the CertificateStoreLocation", ex);
 			}
 		}
@@ -1355,23 +1513,25 @@ private async Task InitSslAsync(ProtocolCapabilities serverCapabilities, Connect
 		{
 			try
 			{
+#if NET9_0_OR_GREATER
+				var certificate = X509CertificateLoader.LoadPkcs12FromFile(cs.CertificateFile, cs.CertificatePassword, X509KeyStorageFlags.MachineKeySet);
+#else
 				var certificate = new X509Certificate2(cs.CertificateFile, cs.CertificatePassword, X509KeyStorageFlags.MachineKeySet);
+#endif
 				if (!certificate.HasPrivateKey)
 				{
 					certificate.Dispose();
 
-					m_logArguments[1] = cs.CertificateFile;
-					Log.Error("Session{0} no private key included with CertificateFile '{1}'", m_logArguments);
+					Log.NoPrivateKeyIncludedWithCertificateFile(m_logger, Id, cs.CertificateFile);
 					throw new MySqlException("CertificateFile does not contain a private key. " +
 						"CertificateFile should be in PKCS #12 (.pfx) format and contain both a Certificate and Private Key");
 				}
 				m_clientCertificate = certificate;
-				clientCertificates = new() { certificate };
+				clientCertificates = [certificate];
 			}
 			catch (CryptographicException ex)
 			{
-				m_logArguments[1] = cs.CertificateFile;
-				Log.Error(ex, "Session{0} couldn't load certificate from CertificateFile '{1}'", m_logArguments);
+				Log.CouldNotLoadCertificateFromFile(m_logger, ex, Id, cs.CertificateFile);
 				if (!File.Exists(cs.CertificateFile))
 					throw new MySqlException("Cannot find Certificate File", ex);
 				throw new MySqlException("Either the Certificate Password is incorrect or the Certificate File is invalid", ex);
@@ -1380,15 +1540,14 @@ private async Task InitSslAsync(ProtocolCapabilities serverCapabilities, Connect
 
 		if (clientCertificates is null && connection.ProvideClientCertificatesCallback is { } clientCertificatesProvider)
 		{
-			clientCertificates = new();
+			clientCertificates = [];
 			try
 			{
 				await clientCertificatesProvider(clientCertificates).ConfigureAwait(false);
 			}
 			catch (Exception ex)
 			{
-				m_logArguments[1] = ex.Message;
-				Log.Error(ex, "Session{0} failed to obtain client certificates via ProvideClientCertificatesCallback: {1}", m_logArguments);
+				Log.FailedToObtainClientCertificates(m_logger, ex, Id, ex.Message);
 				throw new MySqlException("Failed to obtain client certificates via ProvideClientCertificatesCallback", ex);
 			}
 		}
@@ -1408,8 +1567,7 @@ private async Task InitSslAsync(ProtocolCapabilities serverCapabilities, Connect
 			try
 			{
 				// read the CA Certificate File
-				m_logArguments[1] = cs.CACertificateFile;
-				Log.Trace("Session{0} loading CA certificate(s) from CertificateFile '{1}'", m_logArguments);
+				Log.LoadingCaCertificatesFromFile(m_logger, Id, cs.CACertificateFile);
 				byte[] certificateBytes;
 				try
 				{
@@ -1417,7 +1575,7 @@ private async Task InitSslAsync(ProtocolCapabilities serverCapabilities, Connect
 				}
 				catch (Exception ex)
 				{
-					Log.Error(ex, "Session{0} couldn't load CA certificate from CertificateFile '{1}'", m_logArguments);
+					Log.CouldNotLoadCaCertificateFromFile(m_logger, ex, LogLevel.Error, Id, cs.CACertificateFile);
 					if (!File.Exists(cs.CACertificateFile))
 						throw new MySqlException("Cannot find CA Certificate File: " + cs.CACertificateFile, ex);
 					throw new MySqlException("Could not load CA Certificate File: " + cs.CACertificateFile, ex);
@@ -1426,13 +1584,14 @@ private async Task InitSslAsync(ProtocolCapabilities serverCapabilities, Connect
 				// find the index of each individual certificate in the file (assuming there may be multiple certificates concatenated together)
 				for (var index = 0; index != -1;)
 				{
-					var nextIndex = Utility.FindNextIndex(certificateBytes, index + 1, BeginCertificateBytes);
+					var nextIndex = Utility.FindNextIndex(certificateBytes, index + 1, "-----BEGIN CERTIFICATE-----"u8);
 					try
 					{
 						// load the certificate at this index; note that 'new X509Certificate' stops at the end of the first certificate it loads
-						m_logArguments[1] = index;
-						Log.Trace("Session{0} loading certificate at Index {1} in the CA certificate file.", m_logArguments);
-#if NET5_0_OR_GREATER
+						Log.LoadingCaCertificate(m_logger, Id, index);
+#if NET9_0_OR_GREATER
+						var caCertificate = X509CertificateLoader.LoadCertificate(certificateBytes.AsSpan(index, (nextIndex == -1 ? certificateBytes.Length : nextIndex) - index));
+#elif NET5_0_OR_GREATER
 						var caCertificate = new X509Certificate2(certificateBytes.AsSpan(index, (nextIndex == -1 ? certificateBytes.Length : nextIndex) - index), default(ReadOnlySpan<char>), X509KeyStorageFlags.MachineKeySet);
 #else
 						var caCertificate = new X509Certificate2(Utility.ArraySlice(certificateBytes, index, (nextIndex == -1 ? certificateBytes.Length : nextIndex) - index), default(string), X509KeyStorageFlags.MachineKeySet);
@@ -1441,15 +1600,13 @@ private async Task InitSslAsync(ProtocolCapabilities serverCapabilities, Connect
 					}
 					catch (CryptographicException ex)
 					{
-						m_logArguments[1] = cs.CACertificateFile;
-						Log.Warn(ex, "Session{0} couldn't load CA certificate from CertificateFile '{1}'", m_logArguments);
+						Log.CouldNotLoadCaCertificateFromFile(m_logger, ex, LogLevel.Warning, Id, cs.CACertificateFile);
 					}
 					index = nextIndex;
 				}
 
 				// success
-				if (Log.IsTraceEnabled())
-					Log.Trace("Session{0} loaded certificates from CertificateFile '{1}'; CertificateCount: {2}", m_logArguments[0], cs.CACertificateFile, certificateChain.ChainPolicy.ExtraStore.Count);
+				Log.LoadedCaCertificatesFromFile(m_logger, Id, certificateChain.ChainPolicy.ExtraStore.Count, cs.CACertificateFile);
 				caCertificateChain = certificateChain;
 				certificateChain = null;
 			}
@@ -1463,23 +1620,54 @@ private async Task InitSslAsync(ProtocolCapabilities serverCapabilities, Connect
 
 		bool ValidateRemoteCertificate(object rcbSender, X509Certificate? rcbCertificate, X509Chain? rcbChain, SslPolicyErrors rcbPolicyErrors)
 		{
+			// if no CA verification is required, then we trust any remote certificate
 			if (cs.SslMode is MySqlSslMode.Preferred or MySqlSslMode.Required)
 				return true;
 
+			// if there are errors, then try to build a path to a root certificate from the certificates presented by the remote host
 			if ((rcbPolicyErrors & SslPolicyErrors.RemoteCertificateChainErrors) != 0 &&
-				rcbCertificate is not null &&
+				rcbChain is not null &&
 				caCertificateChain is not null &&
-				caCertificateChain.Build((X509Certificate2) rcbCertificate) &&
+				caCertificateChain.Build(rcbChain.ChainElements[^1].Certificate) &&
 				caCertificateChain.ChainStatus.Length > 0)
 			{
-				var chainStatus = caCertificateChain.ChainStatus[0].Status & ~X509ChainStatusFlags.UntrustedRoot;
-				if (chainStatus == X509ChainStatusFlags.NoError)
-					rcbPolicyErrors &= ~SslPolicyErrors.RemoteCertificateChainErrors;
+				// if the only error is an Untrusted Root Certificate, then check all provided SSL CA certificates to see if one is the root
+				if (caCertificateChain.ChainStatus[0].Status == X509ChainStatusFlags.UntrustedRoot)
+				{
+					var rootCertificate = caCertificateChain.ChainElements[^1].Certificate;
+					foreach (var sslCaCertificate in caCertificateChain.ChainPolicy.ExtraStore)
+					{
+#if NET7_0_OR_GREATER
+						if (rootCertificate.RawDataMemory.Span.SequenceEqual(sslCaCertificate.RawDataMemory.Span))
+#else
+						if (rootCertificate.RawData.AsSpan().SequenceEqual(sslCaCertificate.RawData))
+#endif
+						{
+							rcbPolicyErrors &= ~SslPolicyErrors.RemoteCertificateChainErrors;
+							break;
+						}
+					}
+				}
 			}
 
 			if (cs.SslMode == MySqlSslMode.VerifyCA)
 				rcbPolicyErrors &= ~SslPolicyErrors.RemoteCertificateNameMismatch;
 
+			if (rcbCertificate is X509Certificate2 cert2)
+			{
+				// saving sha256 thumbprint and SSL errors until thumbprint validation
+#if NET7_0_OR_GREATER
+				m_remoteCertificateSha2Thumbprint = SHA256.HashData(cert2.RawDataMemory.Span);
+#elif NET5_0_OR_GREATER
+				m_remoteCertificateSha2Thumbprint = SHA256.HashData(cert2.RawData);
+#else
+				using var sha256 = SHA256.Create();
+				m_remoteCertificateSha2Thumbprint = sha256.ComputeHash(cert2.RawData);
+#endif
+				m_sslPolicyErrors = rcbPolicyErrors;
+				return true;
+			}
+
 			return rcbPolicyErrors == SslPolicyErrors.None;
 		}
 
@@ -1489,16 +1677,15 @@ caCertificateChain is not null &&
 		{
 			if (caCertificateChain is not null)
 			{
-				Log.Warn("Session{0} not using client-provided RemoteCertificateValidationCallback because SslCA is specified", m_logArguments);
+				Log.NotUsingRemoteCertificateValidationCallbackDueToSslCa(m_logger, Id);
 			}
 			else if (cs.SslMode is not MySqlSslMode.Preferred and not MySqlSslMode.Required)
 			{
-				m_logArguments[1] = cs.SslMode;
-				Log.Warn("Session{0} not using client-provided RemoteCertificateValidationCallback because SslMode is {1}", m_logArguments);
+				Log.NotUsingRemoteCertificateValidationCallbackDueToSslMode(m_logger, Id, cs.SslMode);
 			}
 			else
 			{
-				Log.Debug("Session{0} using client-provided RemoteCertificateValidationCallback", m_logArguments);
+				Log.UsingRemoteCertificateValidationCallback(m_logger, Id);
 				validateRemoteCertificate = connection.RemoteCertificateValidationCallback;
 			}
 		}
@@ -1508,7 +1695,7 @@ caCertificateChain is not null &&
 
 		var checkCertificateRevocation = cs.SslMode == MySqlSslMode.VerifyFull;
 
-		using (var initSsl = HandshakeResponse41Payload.CreateWithSsl(serverCapabilities, cs, m_useCompression, m_characterSet))
+		using (var initSsl = HandshakeResponse41Payload.CreateWithSsl(serverCapabilities, cs, m_compressionMethod, m_characterSet))
 			await SendReplyAsync(initSsl, ioBehavior, cancellationToken).ConfigureAwait(false);
 
 		var clientAuthenticationOptions = new SslClientAuthenticationOptions
@@ -1520,8 +1707,10 @@ caCertificateChain is not null &&
 		};
 
 #if NETCOREAPP3_0_OR_GREATER
+#pragma warning disable CA1416 // Validate platform compatibility
 		if (cs.TlsCipherSuites is { Count: > 0 })
 			clientAuthenticationOptions.CipherSuitesPolicy = new CipherSuitesPolicy(cs.TlsCipherSuites);
+#pragma warning restore CA1416 // Validate platform compatibility
 #endif
 
 		try
@@ -1552,18 +1741,27 @@ await sslStream.AuthenticateAsClientAsync(clientAuthenticationOptions.TargetHost
 			m_payloadHandler!.ByteHandler = sslByteHandler;
 			m_isSecureConnection = true;
 			m_sslStream = sslStream;
-			if (Log.IsDebugEnabled())
+			if (m_sslPolicyErrors != SslPolicyErrors.None)
 			{
 #if NETCOREAPP3_0_OR_GREATER
-				Log.Debug("Session{0} connected TLS with SslProtocol={1}, NegotiatedCipherSuite={2}", m_logArguments[0], sslStream.SslProtocol, sslStream.NegotiatedCipherSuite);
+				Log.ConnectedTlsBasicPreliminary(m_logger, Id, m_sslPolicyErrors, sslStream.SslProtocol, sslStream.NegotiatedCipherSuite);
 #else
-				Log.Debug("Session{0} connected TLS with SslProtocol={1}, CipherAlgorithm={2}, HashAlgorithm={3}, KeyExchangeAlgorithm={4}, KeyExchangeStrength={5}", m_logArguments[0], sslStream.SslProtocol, sslStream.CipherAlgorithm, sslStream.HashAlgorithm, sslStream.KeyExchangeAlgorithm, sslStream.KeyExchangeStrength);
+				Log.ConnectedTlsDetailedPreliminary(m_logger, Id, m_sslPolicyErrors, sslStream.SslProtocol, sslStream.CipherAlgorithm, sslStream.HashAlgorithm, sslStream.KeyExchangeAlgorithm, sslStream.KeyExchangeStrength);
+#endif
+			}
+			else
+			{
+#if NETCOREAPP3_0_OR_GREATER
+				Log.ConnectedTlsBasic(m_logger, Id, sslStream.SslProtocol, sslStream.NegotiatedCipherSuite);
+#else
+				Log.ConnectedTlsDetailed(m_logger, Id, sslStream.SslProtocol, sslStream.CipherAlgorithm, sslStream.HashAlgorithm, sslStream.KeyExchangeAlgorithm, sslStream.KeyExchangeStrength);
 #endif
 			}
 		}
 		catch (Exception ex)
 		{
-			Log.Error(ex, "Session{0} couldn't initialize TLS connection", m_logArguments);
+			if (ex is not OperationCanceledException)
+				Log.CouldNotInitializeTlsConnection(m_logger, ex, Id);
 			sslStream.Dispose();
 			ShutdownSocket();
 			HostName = "";
@@ -1594,13 +1792,16 @@ X509CertificateCollection LoadCertificate(string sslKeyFile, string sslCertifica
 				// Schannel has a bug where ephemeral keys can't be loaded: https://github.com/dotnet/runtime/issues/23749#issuecomment-485947319
 				// The workaround is to export the key (which may make it "Perphemeral"): https://github.com/dotnet/runtime/issues/23749#issuecomment-739895373
 				var oldCertificate = m_clientCertificate;
+#if NET9_0_OR_GREATER
+				m_clientCertificate = X509CertificateLoader.LoadPkcs12(m_clientCertificate.Export(X509ContentType.Pkcs12, default(string?)), null);
+#else
 				m_clientCertificate = new X509Certificate2(m_clientCertificate.Export(X509ContentType.Pkcs12));
+#endif
 				oldCertificate.Dispose();
 			}
-			return new() { m_clientCertificate };
+			return [m_clientCertificate];
 #else
-			m_logArguments[1] = sslKeyFile;
-			Log.Trace("Session{0} loading client key from KeyFile '{1}'", m_logArguments);
+			Log.LoadingClientKeyFromKeyFile(m_logger, Id, sslKeyFile);
 			string keyPem;
 			try
 			{
@@ -1608,8 +1809,8 @@ X509CertificateCollection LoadCertificate(string sslKeyFile, string sslCertifica
 			}
 			catch (Exception ex)
 			{
-				Log.Error(ex, "Session{0} couldn't load client key from KeyFile '{1}'", m_logArguments);
-				throw new MySqlException("Could not load client key file: " + sslKeyFile, ex);
+				Log.CouldNotLoadClientKeyFromKeyFile(m_logger, ex, Id, sslKeyFile);
+				throw new MySqlException($"Could not load the client key from '{sslKeyFile}'", ex);
 			}
 
 			RSAParameters rsaParameters;
@@ -1619,8 +1820,8 @@ X509CertificateCollection LoadCertificate(string sslKeyFile, string sslCertifica
 			}
 			catch (FormatException ex)
 			{
-				Log.Error(ex, "Session{0} couldn't load client key from KeyFile '{1}'", m_logArguments);
-				throw new MySqlException("Could not load the client key from " + sslKeyFile, ex);
+				Log.CouldNotLoadClientKeyFromKeyFile(m_logger, ex, Id, sslKeyFile);
+				throw new MySqlException($"Could not load the client key from '{sslKeyFile}'", ex);
 			}
 
 			try
@@ -1644,7 +1845,7 @@ X509CertificateCollection LoadCertificate(string sslKeyFile, string sslCertifica
 				}
 				rsa.ImportParameters(rsaParameters);
 
-#if NET461 || NET471
+#if NET462 || NET471
 				var certificate = new X509Certificate2(sslCertificateFile, "", X509KeyStorageFlags.MachineKeySet)
 				{
 					PrivateKey = rsa,
@@ -1656,17 +1857,17 @@ X509CertificateCollection LoadCertificate(string sslKeyFile, string sslCertifica
 #endif
 
 				m_clientCertificate = certificate;
-				return new() { certificate };
+				return [certificate];
 			}
 			catch (CryptographicException ex)
 			{
-				Log.Error(ex, "Session{0} couldn't load client key from KeyFile '{1}'", m_logArguments);
+				Log.CouldNotLoadClientKeyFromKeyFile(m_logger, ex, Id, sslCertificateFile);
 				if (!File.Exists(sslCertificateFile))
 					throw new MySqlException("Cannot find client certificate file: " + sslCertificateFile, ex);
 				throw new MySqlException("Could not load the client key from " + sslCertificateFile, ex);
 			}
 #endif
-		}
+			}
 	}
 
 #if !NETCOREAPP2_1_OR_GREATER && !NETSTANDARD2_1_OR_GREATER
@@ -1690,72 +1891,76 @@ private bool ShouldGetRealServerDetails(ConnectionSettings cs)
 			return true;
 
 		// detect Azure Database for MySQL DNS suffixes, if a "user@host" user ID is being used
-		if (cs.ConnectionProtocol == MySqlConnectionProtocol.Sockets && cs.UserID.IndexOf('@') != -1)
+		if (cs.ConnectionProtocol == MySqlConnectionProtocol.Sockets && cs.UserID.Contains('@'))
 		{
 			return HostName.EndsWith(".mysql.database.azure.com", StringComparison.OrdinalIgnoreCase) ||
 				HostName.EndsWith(".database.windows.net", StringComparison.OrdinalIgnoreCase) ||
 				HostName.EndsWith(".mysql.database.chinacloudapi.cn", StringComparison.OrdinalIgnoreCase);
 		}
 
+		// detect AWS RDS Proxy, if hostname like <name>.proxy-<random-chars>.<region>.rds.amazonaws.com
+		if (HostName.EndsWith(".rds.amazonaws.com", StringComparison.OrdinalIgnoreCase) &&
+			HostName.Contains(".proxy-", StringComparison.OrdinalIgnoreCase))
+		{
+			return true;
+		}
+
 		return false;
 	}
 
 	private async Task GetRealServerDetailsAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
-		Log.Debug("Session{0} detected proxy; getting CONNECTION_ID(), VERSION() from server", m_logArguments);
+		Log.DetectedProxy(m_logger, Id);
 		try
 		{
 			var payload = SupportsQueryAttributes ? s_selectConnectionIdVersionWithAttributesPayload : s_selectConnectionIdVersionNoAttributesPayload;
 			await SendAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
 
 			// column count: 2
-			await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+			_ = await ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
 
 			// CONNECTION_ID() and VERSION() columns
-			await ReceiveReplyAsync(ioBehavior, CancellationToken.None).ConfigureAwait(false);
-			await ReceiveReplyAsync(ioBehavior, CancellationToken.None).ConfigureAwait(false);
+			_ = await ReceiveReplyAsync(ioBehavior, CancellationToken.None).ConfigureAwait(false);
+			_ = await ReceiveReplyAsync(ioBehavior, CancellationToken.None).ConfigureAwait(false);
 
 			if (!SupportsDeprecateEof)
 			{
 				payload = await ReceiveReplyAsync(ioBehavior, CancellationToken.None).ConfigureAwait(false);
-				EofPayload.Create(payload.Span);
+				_ = EofPayload.Create(payload.Span);
 			}
 
 			// first (and only) row
 			payload = await ReceiveReplyAsync(ioBehavior, CancellationToken.None).ConfigureAwait(false);
-			static void ReadRow(ReadOnlySpan<byte> span, out int? connectionId, out ServerVersion? serverVersion)
-			{
-				var reader = new ByteArrayReader(span);
-				var length = reader.ReadLengthEncodedIntegerOrNull();
-				connectionId = (length != -1 && Utf8Parser.TryParse(reader.ReadByteString(length), out int id, out _)) ? id : default(int?);
-				length = reader.ReadLengthEncodedIntegerOrNull();
-				serverVersion = length != -1 ? new ServerVersion(reader.ReadByteString(length)) : default;
-			}
-			ReadRow(payload.Span, out var connectionId, out var serverVersion);
+
+			var reader = new ByteArrayReader(payload.Span);
+			var length = reader.ReadLengthEncodedIntegerOrNull();
+			var connectionId = (length != -1 && Utf8Parser.TryParse(reader.ReadByteString(length), out int id, out _)) ? id : default(int?);
+			length = reader.ReadLengthEncodedIntegerOrNull();
+			var serverVersion = length != -1 ? new ServerVersion(reader.ReadByteString(length)) : default;
 
 			// OK/EOF payload
 			payload = await ReceiveReplyAsync(ioBehavior, CancellationToken.None).ConfigureAwait(false);
-			if (OkPayload.IsOk(payload.Span, SupportsDeprecateEof))
-				OkPayload.Create(payload.Span, SupportsDeprecateEof, SupportsSessionTrack);
+			if (OkPayload.IsOk(payload.Span, this))
+				OkPayload.Verify(payload.Span, this);
 			else
 				EofPayload.Create(payload.Span);
 
-			if (connectionId.HasValue && serverVersion is not null)
+			if (connectionId is int newConnectionId && serverVersion is not null)
 			{
-				Log.Debug("Session{0} changing ConnectionIdOld {1} to ConnectionId {2} and ServerVersionOld {3} to ServerVersion {4}", m_logArguments[0], ConnectionId, connectionId.Value, ServerVersion.OriginalString, serverVersion.OriginalString);
-				ConnectionId = connectionId.Value;
+				Log.ChangingConnectionId(m_logger, Id, ConnectionId, newConnectionId, ServerVersion.OriginalString, serverVersion.OriginalString);
+				ConnectionId = newConnectionId;
 				ServerVersion = serverVersion;
 			}
 		}
 		catch (MySqlException ex)
 		{
-			Log.Info(ex, "Session{0} failed to get CONNECTION_ID(), VERSION()", m_logArguments);
+			Log.FailedToGetConnectionId(m_logger, ex, Id);
 		}
 	}
 
 	private void ShutdownSocket()
 	{
-		Log.Debug("Session{0} closing stream/socket", m_logArguments);
+		Log.ClosingStreamSocket(m_logger, Id);
 		Utility.Dispose(ref m_payloadHandler);
 		Utility.Dispose(ref m_stream);
 		SafeDispose(ref m_tcpClient);
@@ -1791,7 +1996,7 @@ private static void SafeDispose<T>(ref T? disposable)
 
 	internal void SetFailed(Exception exception)
 	{
-		Log.Debug(exception, "Session{0} setting state to Failed", m_logArguments);
+		Log.SettingStateToFailed(m_logger, exception, Id);
 		lock (m_lock)
 			m_state = State.Failed;
 		if (OwningConnection is not null && OwningConnection.TryGetTarget(out var connection))
@@ -1802,7 +2007,7 @@ private void VerifyState(State state)
 	{
 		if (m_state != state)
 		{
-			Log.Error("Session{0} should have SessionStateExpected {1} but was SessionState {2}", m_logArguments[0], state, m_state);
+			ExpectedSessionState1(m_logger, Id, state, m_state);
 			throw new InvalidOperationException($"Expected state to be {state} but was {m_state}.");
 		}
 	}
@@ -1811,8 +2016,7 @@ private void VerifyState(State state1, State state2, State state3, State state4,
 	{
 		if (m_state != state1 && m_state != state2 && m_state != state3 && m_state != state4 && m_state != state5 && m_state != state6)
 		{
-			Log.Error("Session{0} should have SessionStateExpected {1} or SessionStateExpected2 {2} or SessionStateExpected3 {3} or SessionStateExpected4 {4} or SessionStateExpected5 {5} or SessionStateExpected6 {6} but was SessionState {7}",
-				m_logArguments[0], state1, state2, state3, state4, state5, state6, m_state);
+			ExpectedSessionState6(m_logger, Id, state1, state2, state3, state4, state5, state6, m_state);
 			throw new InvalidOperationException($"Expected state to be ({state1}|{state2}|{state3}|{state4}|{state5}|{state6}) but was {m_state}.");
 		}
 	}
@@ -1827,9 +2031,9 @@ private void VerifyState(State state1, State state2, State state3, State state4,
 
 	internal SslProtocols SslProtocol => m_sslStream?.SslProtocol ?? SslProtocols.None;
 
-	private byte[] CreateConnectionAttributes(string programName)
+	private byte[] CreateConnectionAttributes(string? programName)
 	{
-		Log.Trace("Session{0} creating connection attributes", m_logArguments);
+		Log.CreatingConnectionAttributes(m_logger, Id);
 		var attributesWriter = new ByteBufferWriter();
 		attributesWriter.WriteLengthEncodedString("_client_name");
 		attributesWriter.WriteLengthEncodedString("MySqlConnector");
@@ -1838,7 +2042,7 @@ private byte[] CreateConnectionAttributes(string programName)
 		var version = typeof(ServerSession).GetTypeInfo().Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion;
 		var plusIndex = version.IndexOf('+');
 		if (plusIndex != -1)
-			version = version.Substring(0, plusIndex);
+			version = version[..plusIndex];
 		attributesWriter.WriteLengthEncodedString(version);
 
 		try
@@ -1865,7 +2069,7 @@ private byte[] CreateConnectionAttributes(string programName)
 #endif
 		attributesWriter.WriteLengthEncodedString("_pid");
 		attributesWriter.WriteLengthEncodedString(processId.ToString(CultureInfo.InvariantCulture));
-		if (programName.Length != 0)
+		if (!string.IsNullOrEmpty(programName))
 		{
 			attributesWriter.WriteLengthEncodedString("program_name");
 			attributesWriter.WriteLengthEncodedString(programName!);
@@ -1879,11 +2083,10 @@ private byte[] CreateConnectionAttributes(string programName)
 		return payload.Memory.ToArray();
 	}
 
-	private Exception CreateExceptionForErrorPayload(ReadOnlySpan<byte> span)
+	private MySqlException CreateExceptionForErrorPayload(ReadOnlySpan<byte> span)
 	{
 		var errorPayload = ErrorPayload.Create(span);
-		if (Log.IsDebugEnabled())
-			Log.Debug("Session{0} got error payload: Code={1}, State={2}, Message={3}", m_logArguments[0], errorPayload.ErrorCode, errorPayload.State, errorPayload.Message);
+		Log.ErrorPayload(m_logger, Id, errorPayload.ErrorCode, errorPayload.State, errorPayload.Message);
 		var exception = errorPayload.ToException();
 		if (exception.ErrorCode is MySqlErrorCode.ClientInteractionTimeout)
 			SetFailed(exception);
@@ -1909,13 +2112,12 @@ private string GetPassword(ConnectionSettings cs, MySqlConnection connection)
 		{
 			try
 			{
-				Log.Trace("Session{0} obtaining password via ProvidePasswordCallback", m_logArguments);
+				Log.ObtainingPasswordViaProvidePasswordCallback(m_logger, Id);
 				return passwordProvider(new(HostName, cs.Port, cs.UserID, cs.Database));
 			}
 			catch (Exception ex)
 			{
-				m_logArguments[1] = ex.Message;
-				Log.Error(ex, "Session{0} failed to obtain password via ProvidePasswordCallback: {1}", m_logArguments);
+				Log.FailedToObtainPassword(m_logger, ex, Id, ex.Message);
 				throw new MySqlException(MySqlErrorCode.ProvidePasswordCallbackFailed, "Failed to obtain password via ProvidePasswordCallback", ex);
 			}
 		}
@@ -1953,39 +2155,47 @@ private enum State
 		Failed,
 	}
 
-	private sealed class DelimiterSqlParser : SqlParser
+	private sealed class DelimiterSqlParser(IMySqlCommand command)
+		: SqlParser(new StatementPreparer(command.CommandText!, null, command.CreateStatementPreparerOptions()))
 	{
-		public DelimiterSqlParser(IMySqlCommand command)
-			: base(new StatementPreparer(command.CommandText!, null, command.CreateStatementPreparerOptions()))
-		{
-			m_sql = command.CommandText!;
-		}
-
 		public bool HasDelimiter { get; private set; }
 
 		protected override void OnStatementBegin(int index)
 		{
-			if (index + 10 < m_sql.Length && m_sql.AsSpan(index, 10).Equals("delimiter ".AsSpan(), StringComparison.OrdinalIgnoreCase))
+			if (index + 10 < Sql.Length && Sql.AsSpan(index, 10).Equals("delimiter ".AsSpan(), StringComparison.OrdinalIgnoreCase))
 				HasDelimiter = true;
 		}
 
-		private readonly string m_sql;
+		private string Sql { get; } = command.CommandText!;
 	}
 
-	private static ReadOnlySpan<byte> BeginCertificateBytes => "-----BEGIN CERTIFICATE-----"u8;
-	private static readonly IMySqlConnectorLogger Log = MySqlConnectorLogManager.CreateLogger(nameof(ServerSession));
+	[LoggerMessage(EventIds.CannotExecuteNewCommandInState, LogLevel.Error, "Session {SessionId} can't execute new command when in state {SessionState}")]
+	private static partial void CannotExecuteNewCommandInState(ILogger logger, string sessionId, State sessionState);
+
+	[LoggerMessage(EventIds.EnteringFinishQuerying, LogLevel.Trace, "Session {SessionId} entering FinishQuerying; state is {SessionState}")]
+	private static partial void EnteringFinishQuerying(ILogger logger, string sessionId, State sessionState);
+
+	[LoggerMessage(EventIds.ExpectedSessionState1, LogLevel.Error, "Session {SessionId} should have state {ExpectedState1} but was {SessionState}")]
+	private static partial void ExpectedSessionState1(ILogger logger, string sessionId, State expectedState1, State sessionState);
+
+	[LoggerMessage(EventIds.ExpectedSessionState6, LogLevel.Error, "Session {SessionId} should have state {ExpectedState1} or {ExpectedState2} or {ExpectedState3} or {ExpectedState4} or {ExpectedState5} or {ExpectedState6} but was {SessionState}")]
+	private static partial void ExpectedSessionState6(ILogger logger, string sessionId, State expectedState1, State expectedState2, State expectedState3, State expectedState4, State expectedState5, State expectedState6, State sessionState);
+
 	private static readonly PayloadData s_setNamesUtf8NoAttributesPayload = QueryPayload.Create(false, "SET NAMES utf8;"u8);
 	private static readonly PayloadData s_setNamesUtf8mb4NoAttributesPayload = QueryPayload.Create(false, "SET NAMES utf8mb4;"u8);
 	private static readonly PayloadData s_setNamesUtf8WithAttributesPayload = QueryPayload.Create(true, "SET NAMES utf8;"u8);
 	private static readonly PayloadData s_setNamesUtf8mb4WithAttributesPayload = QueryPayload.Create(true, "SET NAMES utf8mb4;"u8);
-	private static readonly PayloadData s_sleepNoAttributesPayload = QueryPayload.Create(false, "SELECT SLEEP(0) INTO @\uE001MySqlConnector\uE001Sleep;"u8);
-	private static readonly PayloadData s_sleepWithAttributesPayload = QueryPayload.Create(true, "SELECT SLEEP(0) INTO @\uE001MySqlConnector\uE001Sleep;"u8);
+	private static readonly PayloadData s_sleepNoAttributesPayload = QueryPayload.Create(false, "SELECT SLEEP(0) INTO @__MySqlConnector__Sleep;"u8);
+	private static readonly PayloadData s_sleepWithAttributesPayload = QueryPayload.Create(true, "SELECT SLEEP(0) INTO @__MySqlConnector__Sleep;"u8);
 	private static readonly PayloadData s_selectConnectionIdVersionNoAttributesPayload = QueryPayload.Create(false, "SELECT CONNECTION_ID(), VERSION();"u8);
 	private static readonly PayloadData s_selectConnectionIdVersionWithAttributesPayload = QueryPayload.Create(true, "SELECT CONNECTION_ID(), VERSION();"u8);
-	private static int s_lastId;
 
+	private readonly ILogger m_logger;
+#if NET9_0_OR_GREATER
+	private readonly Lock m_lock;
+#else
 	private readonly object m_lock;
-	private readonly object?[] m_logArguments;
+#endif
 	private readonly ArraySegmentHolder<byte> m_payloadCache;
 	private readonly ActivityTagsCollection m_activityTags;
 	private State m_state;
@@ -1995,15 +2205,16 @@ protected override void OnStatementBegin(int index)
 	private SslStream? m_sslStream;
 	private X509Certificate2? m_clientCertificate;
 	private IPayloadHandler? m_payloadHandler;
-	private bool m_useCompression;
+	private CompressionMethod m_compressionMethod;
 	private bool m_isSecureConnection;
-	private bool m_supportsComMulti;
+	private bool m_isLoopbackConnection;
 	private bool m_supportsConnectionAttributes;
-	private bool m_supportsDeprecateEof;
-	private bool m_supportsSessionTrack;
 	private bool m_supportsPipelining;
 	private CharacterSet m_characterSet;
 	private PayloadData m_setNamesPayload;
 	private byte[]? m_pipelinedResetConnectionBytes;
 	private Dictionary<string, PreparedStatements>? m_preparedStatements;
+	private byte[]? m_passwordHash;
+	private byte[]? m_remoteCertificateSha2Thumbprint;
+	private SslPolicyErrors m_sslPolicyErrors;
 }
diff --git a/src/MySqlConnector/Core/ServerVersion.cs b/src/MySqlConnector/Core/ServerVersion.cs
index 2feac84fc..f6822faf1 100644
--- a/src/MySqlConnector/Core/ServerVersion.cs
+++ b/src/MySqlConnector/Core/ServerVersion.cs
@@ -25,13 +25,13 @@ public ServerVersion(ReadOnlySpan<byte> versionString)
 		if (Utf8Parser.TryParse(versionString, out int major, out var bytesConsumed))
 		{
 			versionString = versionString[bytesConsumed..];
-			if (versionString is [ 0x2E, .. ])
+			if (versionString is [0x2E, ..])
 			{
 				versionString = versionString[1..];
 				if (Utf8Parser.TryParse(versionString, out minor, out bytesConsumed))
 				{
 					versionString = versionString[bytesConsumed..];
-					if (versionString is [ 0x2E, .. ])
+					if (versionString is [0x2E, ..])
 					{
 						versionString = versionString[1..];
 						if (Utf8Parser.TryParse(versionString, out build, out bytesConsumed))
diff --git a/src/MySqlConnector/Core/ServerVersions.cs b/src/MySqlConnector/Core/ServerVersions.cs
index 6e732e00a..fc8e498f5 100644
--- a/src/MySqlConnector/Core/ServerVersions.cs
+++ b/src/MySqlConnector/Core/ServerVersions.cs
@@ -16,4 +16,7 @@ internal static class ServerVersions
 
 	// https://ocelot.ca/blog/blog/2017/08/22/no-more-mysql-proc-in-mysql-8-0/
 	public static readonly Version RemovesMySqlProcTable = new(8, 0, 0);
+
+	// https://mariadb.com/kb/en/set-statement/
+	public static readonly Version MariaDbSupportsPerQueryVariables = new(10, 1, 2);
 }
diff --git a/src/MySqlConnector/Core/SingleCommandPayloadCreator.cs b/src/MySqlConnector/Core/SingleCommandPayloadCreator.cs
index ef9e0c426..122b4ccc8 100644
--- a/src/MySqlConnector/Core/SingleCommandPayloadCreator.cs
+++ b/src/MySqlConnector/Core/SingleCommandPayloadCreator.cs
@@ -1,7 +1,9 @@
+using System.Buffers;
+using System.Buffers.Binary;
+using System.Runtime.InteropServices;
 using MySqlConnector.Logging;
 using MySqlConnector.Protocol;
 using MySqlConnector.Protocol.Serialization;
-using MySqlConnector.Utilities;
 
 namespace MySqlConnector.Core;
 
@@ -13,17 +15,91 @@ internal sealed class SingleCommandPayloadCreator : ICommandPayloadCreator
 	// with this as the first column name, the result set will be treated as 'out' parameters for the previous command.
 	public static string OutParameterSentinelColumnName => "\uE001\b\x0B";
 
+	public async ValueTask SendCommandPrologueAsync(MySqlConnection connection, CommandListPosition commandListPosition, IOBehavior ioBehavior, CancellationToken cancellationToken)
+	{
+		// get the current command and check for prepared statements
+		var command = commandListPosition.CommandAt(commandListPosition.CommandIndex);
+		var preparedStatements = commandListPosition.PreparedStatements ?? command.TryGetPreparedStatements();
+		if (preparedStatements is not null)
+		{
+			// get the current prepared statement; WriteQueryCommand will advance this
+			var preparedStatement = preparedStatements.Statements[commandListPosition.PreparedStatementIndex];
+			if (preparedStatement.Parameters is { } parameters)
+			{
+				byte[]? buffer = null;
+				try
+				{
+					// check each parameter
+					for (var i = 0; i < parameters.Length; i++)
+					{
+						// look up this parameter in the command's parameter collection and check if it is a Stream
+						// NOTE: full parameter checks will be performed (and throw any necessary exceptions) in WritePreparedStatement
+						var parameterName = preparedStatement.Statement.NormalizedParameterNames![i];
+						var parameterIndex = parameterName is not null ? (command.RawParameters?.UnsafeIndexOf(parameterName) ?? -1) : preparedStatement.Statement.ParameterIndexes[i];
+						if (command.RawParameters is { } rawParameters && parameterIndex >= 0 && parameterIndex < rawParameters.Count && rawParameters[parameterIndex] is { Value: Stream stream and not MemoryStream })
+						{
+							// seven-byte COM_STMT_SEND_LONG_DATA header: https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_com_stmt_send_long_data.html
+							const int packetHeaderLength = 7;
+
+							// send almost-full packets, but don't send exactly ProtocolUtility.MaxPacketSize bytes in one payload (as that's ambiguous to whether another packet follows).
+							const int maxDataSize = 16_000_000;
+							int totalBytesRead;
+							while (true)
+							{
+								buffer ??= ArrayPool<byte>.Shared.Rent(packetHeaderLength + maxDataSize);
+								buffer[0] = (byte) CommandKind.StatementSendLongData;
+								BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(1), preparedStatement.StatementId);
+								BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(5), (ushort) i);
+
+								// keep reading from the stream until we've filled the buffer to send
+#if NET7_0_OR_GREATER
+								if (ioBehavior == IOBehavior.Synchronous)
+									totalBytesRead = stream.ReadAtLeast(buffer.AsSpan(packetHeaderLength, maxDataSize), maxDataSize, throwOnEndOfStream: false);
+								else
+									totalBytesRead = await stream.ReadAtLeastAsync(buffer.AsMemory(packetHeaderLength, maxDataSize), maxDataSize, throwOnEndOfStream: false, cancellationToken).ConfigureAwait(false);
+#else
+								totalBytesRead = 0;
+								int bytesRead;
+								do
+								{
+									var sizeToRead = maxDataSize - totalBytesRead;
+									if (ioBehavior == IOBehavior.Synchronous)
+										bytesRead = stream.Read(buffer, packetHeaderLength + totalBytesRead, sizeToRead);
+									else
+										bytesRead = await stream.ReadAsync(buffer, packetHeaderLength + totalBytesRead, sizeToRead, cancellationToken).ConfigureAwait(false);
+									totalBytesRead += bytesRead;
+								} while (bytesRead > 0 && totalBytesRead < maxDataSize);
+#endif
+
+								if (totalBytesRead == 0)
+									break;
+
+								// send StatementSendLongData; MySQL Server will keep appending the sent data to the parameter value
+								using var payload = new PayloadData(buffer.AsMemory(0, packetHeaderLength + totalBytesRead), isPooled: false);
+								await connection.Session.SendAsync(payload, ioBehavior, cancellationToken).ConfigureAwait(false);
+							}
+						}
+					}
+				}
+				finally
+				{
+					if (buffer is not null)
+						ArrayPool<byte>.Shared.Return(buffer);
+				}
+			}
+		}
+	}
+
 	public bool WriteQueryCommand(ref CommandListPosition commandListPosition, IDictionary<string, CachedProcedure?> cachedProcedures, ByteBufferWriter writer, bool appendSemicolon)
 	{
-		if (commandListPosition.CommandIndex == commandListPosition.Commands.Count)
+		if (commandListPosition.CommandIndex == commandListPosition.CommandCount)
 			return false;
 
-		var command = commandListPosition.Commands[commandListPosition.CommandIndex];
-		var preparedStatements = command.TryGetPreparedStatements();
-		if (preparedStatements is null)
+		var command = commandListPosition.CommandAt(commandListPosition.CommandIndex);
+		commandListPosition.PreparedStatements = command.TryGetPreparedStatements();
+		if (commandListPosition.PreparedStatements is null)
 		{
-			if (Log.IsTraceEnabled())
-				Log.Trace("Session{0} Preparing command payload; CommandText: {1}", command.Connection!.Session.Id, command.CommandText);
+			Log.PreparingCommandPayload(command.Logger, command.Connection!.Session.Id, command.CommandText!);
 
 			writer.Write((byte) CommandKind.Query);
 			var supportsQueryAttributes = command.Connection!.Session.SupportsQueryAttributes;
@@ -37,27 +113,30 @@ public bool WriteQueryCommand(ref CommandListPosition commandListPosition, IDict
 				writer.Write((byte) 1);
 
 				if (attributes?.Count > 0)
-					WriteBinaryParameters(writer, attributes.Select(x => x.ToParameter()).ToArray(), command, true, 0);
+					WriteBinaryParameters(writer, attributes.Select(static x => x.ToParameter()).ToArray(), command, true, 0);
 			}
 			else if (command.RawAttributes?.Count > 0)
 			{
-				Log.Warn("Session{0} has query attributes but server doesn't support them; CommandText: {1}", command.Connection!.Session.Id, command.CommandText);
+				Log.QueryAttributesNotSupported(command.Logger, command.Connection!.Session.Id, command.CommandText!);
 			}
 
-			WriteQueryPayload(command, cachedProcedures, writer, appendSemicolon);
-
+			WriteQueryPayload(command, cachedProcedures, writer, appendSemicolon, isFirstCommand: true, isLastCommand: true);
+			commandListPosition.LastUsedPreparedStatement = null;
 			commandListPosition.CommandIndex++;
 		}
 		else
 		{
 			writer.Write((byte) CommandKind.StatementExecute);
-			WritePreparedStatement(command, preparedStatements.Statements[commandListPosition.PreparedStatementIndex], writer);
+			commandListPosition.LastUsedPreparedStatement =
+				commandListPosition.PreparedStatements.Statements[commandListPosition.PreparedStatementIndex];
+			WritePreparedStatement(command, commandListPosition.LastUsedPreparedStatement, writer);
 
 			// advance to next prepared statement or next command
-			if (++commandListPosition.PreparedStatementIndex == preparedStatements.Statements.Count)
+			if (++commandListPosition.PreparedStatementIndex == commandListPosition.PreparedStatements.Statements.Count)
 			{
 				commandListPosition.CommandIndex++;
 				commandListPosition.PreparedStatementIndex = 0;
+				commandListPosition.PreparedStatements = null;
 			}
 		}
 		return true;
@@ -70,16 +149,17 @@ public bool WriteQueryCommand(ref CommandListPosition commandListPosition, IDict
 	/// <param name="cachedProcedures">The cached procedures.</param>
 	/// <param name="writer">The output writer.</param>
 	/// <param name="appendSemicolon">Whether a statement-separating semicolon should be appended if it's missing.</param>
+	/// <param name="isFirstCommand">Whether this command is the first one.</param>
+	/// <param name="isLastCommand">Whether this command is the last one.</param>
 	/// <returns><c>true</c> if a complete command was written; otherwise, <c>false</c>.</returns>
-	public static bool WriteQueryPayload(IMySqlCommand command, IDictionary<string, CachedProcedure?> cachedProcedures, ByteBufferWriter writer, bool appendSemicolon) =>
-		(command.CommandType == CommandType.StoredProcedure) ? WriteStoredProcedure(command, cachedProcedures, writer) : WriteCommand(command, writer, appendSemicolon);
+	public static bool WriteQueryPayload(IMySqlCommand command, IDictionary<string, CachedProcedure?> cachedProcedures, ByteBufferWriter writer, bool appendSemicolon, bool isFirstCommand, bool isLastCommand) =>
+		(command.CommandType == CommandType.StoredProcedure) ? WriteStoredProcedure(command, cachedProcedures, writer) : WriteCommand(command, writer, appendSemicolon, isFirstCommand, isLastCommand);
 
 	private static void WritePreparedStatement(IMySqlCommand command, PreparedStatement preparedStatement, ByteBufferWriter writer)
 	{
 		var parameterCollection = command.RawParameters;
 
-		if (Log.IsTraceEnabled())
-			Log.Trace("Session{0} Preparing command payload; CommandId: {1}; CommandText: {2}", command.Connection!.Session.Id, preparedStatement.StatementId, command.CommandText);
+		Log.PreparingCommandPayloadWithId(command.Logger, command.Connection!.Session.Id, preparedStatement.StatementId, command.CommandText!);
 
 		var attributes = command.RawAttributes;
 		var supportsQueryAttributes = command.Connection!.Session.SupportsQueryAttributes;
@@ -103,7 +183,7 @@ private static void WritePreparedStatement(IMySqlCommand command, PreparedStatem
 				writer.WriteLengthEncodedInteger((uint) commandParameterCount);
 			if (attributeCount > 0)
 			{
-				Log.Warn("Session{0} has attributes for CommandId {1} but the server does not support them", command.Connection!.Session.Id, preparedStatement.StatementId);
+				Log.QueryAttributesNotSupportedWithId(command.Logger, command.Connection!.Session.Id, preparedStatement.StatementId);
 				attributeCount = 0;
 			}
 		}
@@ -164,6 +244,10 @@ private static void WriteBinaryParameters(ByteBufferWriter writer, MySqlParamete
 				mySqlDbType = TypeMapper.Instance.GetMySqlDbTypeForDbType(dbType);
 			}
 
+			// HACK: MariaDB doesn't have a dedicated Vector type so mark it as binary data
+			if (mySqlDbType == MySqlDbType.Vector && command.Connection!.Session.ServerVersion.IsMariaDb)
+				mySqlDbType = MySqlDbType.LongBlob;
+
 			writer.Write(TypeMapper.ConvertToColumnTypeAndFlags(mySqlDbType, command.Connection!.GuidFormat));
 
 			if (supportsQueryAttributes)
@@ -200,25 +284,41 @@ private static bool WriteStoredProcedure(IMySqlCommand command, IDictionary<stri
 			var outName = "@outParam" + i;
 			switch (param.Direction)
 			{
-			case ParameterDirection.Input:
-			case ParameterDirection.InputOutput:
-				var inParam = param.WithParameterName(inName);
-				inParameters.Add(inParam);
-				if (param.Direction == ParameterDirection.InputOutput)
-				{
-					inOutSetParameters += $"SET {outName}={inName}; ";
-					goto case ParameterDirection.Output;
-				}
-				argParameterNames.Add(inName);
-				break;
-			case ParameterDirection.Output:
-				outParameters.Add(param);
-				outParameterNames.Add(outName);
-				argParameterNames.Add(outName);
-				break;
-			case ParameterDirection.ReturnValue:
-				returnParameter = param;
-				break;
+				case ParameterDirection.Input:
+				case ParameterDirection.InputOutput:
+					var inParam = param.WithParameterName(inName);
+					inParameters.Add(inParam);
+					if (param.Direction == ParameterDirection.InputOutput)
+					{
+						inOutSetParameters += $"SET {outName}={inName}; ";
+						goto case ParameterDirection.Output;
+					}
+					argParameterNames.Add(inName);
+					break;
+				case ParameterDirection.Output:
+					outParameters.Add(param);
+					argParameterNames.Add(outName);
+
+					// special handling for GUIDs to ensure that the result set has a type and length that will be autodetected as a GUID
+					switch (param.MySqlDbType, param.Size)
+					{
+						case (MySqlDbType.Guid, 16):
+							outParameterNames.Add($"CAST({outName} AS BINARY(16))");
+							break;
+						case (MySqlDbType.Guid, 32):
+							outParameterNames.Add($"CAST({outName} AS CHAR(32))");
+							break;
+						case (MySqlDbType.Guid, 36):
+							outParameterNames.Add($"CAST({outName} AS CHAR(36))");
+							break;
+						default:
+							outParameterNames.Add(outName);
+							break;
+					}
+					break;
+				case ParameterDirection.ReturnValue:
+					returnParameter = param;
+					break;
 			}
 		}
 
@@ -243,29 +343,30 @@ private static bool WriteStoredProcedure(IMySqlCommand command, IDictionary<stri
 		return preparer.ParseAndBindParameters(writer);
 	}
 
-	private static bool WriteCommand(IMySqlCommand command, ByteBufferWriter writer, bool appendSemicolon)
+	private static bool WriteCommand(IMySqlCommand command, ByteBufferWriter writer, bool appendSemicolon, bool isFirstCommand, bool isLastCommand)
 	{
 		var isSchemaOnly = (command.CommandBehavior & CommandBehavior.SchemaOnly) != 0;
 		var isSingleRow = (command.CommandBehavior & CommandBehavior.SingleRow) != 0;
-		if (isSchemaOnly)
+		if ((isSchemaOnly || isSingleRow) && isFirstCommand)
 		{
-			ReadOnlySpan<byte> setSqlSelectLimit0 = "SET sql_select_limit=0;\n"u8;
-			writer.Write(setSqlSelectLimit0);
-		}
-		else if (isSingleRow)
-		{
-			ReadOnlySpan<byte> setSqlSelectLimit1 = "SET sql_select_limit=1;\n"u8;
-			writer.Write(setSqlSelectLimit1);
+			writer.Write((command.Connection!.SupportsPerQueryVariables, isSingleRow) switch
+			{
+				// server doesn't support per-query variables; use multi-statements
+				(false, false) => "SET sql_select_limit=0;\n"u8,
+				(false, true) => "SET sql_select_limit=1;\n"u8,
+
+				// server supports per-query variables; use SET STATEMENT
+				(true, false) => "SET STATEMENT sql_select_limit=0 FOR "u8,
+				(true, true) => "SET STATEMENT sql_select_limit=1 FOR "u8,
+			});
 		}
+
 		var preparer = new StatementPreparer(command.CommandText!, command.RawParameters, command.CreateStatementPreparerOptions() | ((appendSemicolon || isSchemaOnly || isSingleRow) ? StatementPreparerOptions.AppendSemicolon : StatementPreparerOptions.None));
 		var isComplete = preparer.ParseAndBindParameters(writer);
-		if (isComplete && (isSchemaOnly || isSingleRow))
-		{
-			ReadOnlySpan<byte> clearSqlSelectLimit = "\nSET sql_select_limit=default;"u8;
-			writer.Write(clearSqlSelectLimit);
-		}
+
+		if ((isSchemaOnly || isSingleRow) && isLastCommand && isComplete && !command.Connection!.SupportsPerQueryVariables)
+			writer.Write("\nSET sql_select_limit=default;"u8);
+
 		return isComplete;
 	}
-
-	private static readonly IMySqlConnectorLogger Log = MySqlConnectorLogManager.CreateLogger(nameof(SingleCommandPayloadCreator));
 }
diff --git a/src/MySqlConnector/Core/SqlParser.cs b/src/MySqlConnector/Core/SqlParser.cs
index a5c312e0e..de08d26ff 100644
--- a/src/MySqlConnector/Core/SqlParser.cs
+++ b/src/MySqlConnector/Core/SqlParser.cs
@@ -2,15 +2,19 @@
 
 namespace MySqlConnector.Core;
 
-internal abstract class SqlParser
+internal abstract class SqlParser(StatementPreparer preparer)
 {
-	protected StatementPreparer Preparer { get; }
-
-	protected SqlParser(StatementPreparer preparer) => Preparer = preparer;
+	protected StatementPreparer Preparer { get; } = preparer;
 
 	public void Parse(string sql)
 	{
-		OnBeforeParse(sql ?? throw new ArgumentNullException(nameof(sql)));
+#if NET6_0_OR_GREATER
+		ArgumentNullException.ThrowIfNull(sql);
+#else
+		if (sql is null)
+			throw new ArgumentNullException(nameof(sql));
+#endif
+		OnBeforeParse(sql);
 
 		int parameterStartIndex = -1;
 		var noBackslashEscapes = (Preparer.Options & StatementPreparerOptions.NoBackslashEscapes) == StatementPreparerOptions.NoBackslashEscapes;
@@ -208,7 +212,7 @@ public void Parse(string sql)
 			}
 			else
 			{
-				if (state != State.Beginning && state != State.Statement)
+				if (state is not State.Beginning and not State.Statement)
 					throw new InvalidOperationException($"Unexpected state: {state}");
 
 				if (ch == '-' && index < sql.Length - 2 && sql[index + 1] == '-' && sql[index + 2] == ' ')
diff --git a/src/MySqlConnector/Core/StandardEnlistedTransaction.cs b/src/MySqlConnector/Core/StandardEnlistedTransaction.cs
index 45eff3b7c..32222aca5 100644
--- a/src/MySqlConnector/Core/StandardEnlistedTransaction.cs
+++ b/src/MySqlConnector/Core/StandardEnlistedTransaction.cs
@@ -3,13 +3,9 @@
 
 namespace MySqlConnector.Core;
 
-internal sealed class StandardEnlistedTransaction : EnlistedTransactionBase
+internal sealed class StandardEnlistedTransaction(Transaction transaction, MySqlConnection connection)
+	: EnlistedTransactionBase(transaction, connection)
 {
-	public StandardEnlistedTransaction(Transaction transaction, MySqlConnection connection)
-		: base(transaction, connection)
-	{
-	}
-
 	protected override void OnStart()
 	{
 		var isolationLevel = Transaction.IsolationLevel switch
diff --git a/src/MySqlConnector/Core/StatementPreparer.cs b/src/MySqlConnector/Core/StatementPreparer.cs
index 12e8d5689..1ae527984 100644
--- a/src/MySqlConnector/Core/StatementPreparer.cs
+++ b/src/MySqlConnector/Core/StatementPreparer.cs
@@ -4,39 +4,32 @@
 
 namespace MySqlConnector.Core;
 
-internal sealed class StatementPreparer
+internal sealed class StatementPreparer(string commandText, MySqlParameterCollection? parameters, StatementPreparerOptions options)
 {
-	public StatementPreparerOptions Options { get; }
-
-	public StatementPreparer(string commandText, MySqlParameterCollection? parameters, StatementPreparerOptions options)
-	{
-		m_commandText = commandText;
-		m_parameters = parameters;
-		Options = options;
-	}
+	public StatementPreparerOptions Options { get; } = options;
 
 	public ParsedStatements SplitStatements()
 	{
 		var statements = new List<ParsedStatement>();
 		var statementStartEndIndexes = new List<int>();
-		var writer = new ByteBufferWriter(m_commandText.Length + 1);
+		var writer = new ByteBufferWriter(CommandText.Length + 1);
 		var parser = new PreparedCommandSqlParser(this, statements, statementStartEndIndexes, writer);
-		parser.Parse(m_commandText);
+		parser.Parse(CommandText);
 		for (var i = 0; i < statements.Count; i++)
 #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
-			statements[i].StatementBytes = writer.ArraySegment[statementStartEndIndexes[i * 2]..statementStartEndIndexes[i * 2 + 1]];
+			statements[i].StatementBytes = writer.ArraySegment[statementStartEndIndexes[i * 2]..statementStartEndIndexes[(i * 2) + 1]];
 #else
-			statements[i].StatementBytes = writer.ArraySegment.Slice(statementStartEndIndexes[i * 2], statementStartEndIndexes[i * 2 + 1] - statementStartEndIndexes[i * 2]);
+			statements[i].StatementBytes = writer.ArraySegment.Slice(statementStartEndIndexes[i * 2], statementStartEndIndexes[(i * 2) + 1] - statementStartEndIndexes[i * 2]);
 #endif
 		return new ParsedStatements(statements, writer.ToPayloadData());
 	}
 
 	public bool ParseAndBindParameters(ByteBufferWriter writer)
 	{
-		if (!string.IsNullOrWhiteSpace(m_commandText))
+		if (!string.IsNullOrWhiteSpace(CommandText))
 		{
 			var parser = new ParameterSqlParser(this, writer);
-			parser.Parse(m_commandText);
+			parser.Parse(CommandText);
 			return parser.IsComplete;
 		}
 		return true;
@@ -44,7 +37,7 @@ public bool ParseAndBindParameters(ByteBufferWriter writer)
 
 	private int GetParameterIndex(string name)
 	{
-		var index = m_parameters?.NormalizedIndexOf(name) ?? -1;
+		var index = parameters?.NormalizedIndexOf(name) ?? -1;
 		if (index == -1 && (Options & StatementPreparerOptions.AllowUserVariables) == 0)
 			throw new MySqlException($"Parameter '{name}' must be defined. To use this as a variable, set 'Allow User Variables=true' in the connection string.");
 		return index;
@@ -52,27 +45,22 @@ private int GetParameterIndex(string name)
 
 	private MySqlParameter GetInputParameter(int index)
 	{
-		if (index >= (m_parameters?.Count ?? 0))
-			throw new MySqlException($"Parameter index {index} is invalid when only {m_parameters?.Count ?? 0} parameter{(m_parameters?.Count == 1 ? " is" : "s are")} defined.");
-		var parameter = m_parameters![index];
+		if (index >= (parameters?.Count ?? 0))
+			throw new MySqlException($"Parameter index {index} is invalid when only {parameters?.Count ?? 0} parameter{(parameters?.Count == 1 ? " is" : "s are")} defined.");
+		var parameter = parameters![index];
 		if (parameter.Direction != ParameterDirection.Input && (Options & StatementPreparerOptions.AllowOutputParameters) == 0)
 			throw new MySqlException($"Only ParameterDirection.Input is supported when CommandType is Text (parameter name: {parameter.ParameterName})");
 		return parameter;
 	}
 
-	private sealed class ParameterSqlParser : SqlParser
+	private sealed class ParameterSqlParser(StatementPreparer preparer, ByteBufferWriter writer)
+		: SqlParser(preparer)
 	{
-		public ParameterSqlParser(StatementPreparer preparer, ByteBufferWriter writer)
-			: base(preparer)
-		{
-			m_writer = writer;
-		}
-
 		public bool IsComplete { get; private set; }
 
 		protected override void OnNamedParameter(int index, int length)
 		{
-			var parameterIndex = Preparer.GetParameterIndex(Preparer.m_commandText.Substring(index, length));
+			var parameterIndex = Preparer.GetParameterIndex(Preparer.CommandText.Substring(index, length));
 			if (parameterIndex != -1)
 				DoAppendParameter(parameterIndex, index, length);
 		}
@@ -85,48 +73,42 @@ protected override void OnPositionalParameter(int index)
 
 		private void DoAppendParameter(int parameterIndex, int textIndex, int textLength)
 		{
-			m_writer.Write(Preparer.m_commandText, m_lastIndex, textIndex - m_lastIndex);
+			Writer.Write(Preparer.CommandText, m_lastIndex, textIndex - m_lastIndex);
 			var parameter = Preparer.GetInputParameter(parameterIndex);
-			parameter.AppendSqlString(m_writer, Preparer.Options);
+			parameter.AppendSqlString(Writer, Preparer.Options);
 			m_lastIndex = textIndex + textLength;
 		}
 
 		protected override void OnParsed(FinalParseStates states)
 		{
-			m_writer.Write(Preparer.m_commandText, m_lastIndex, Preparer.m_commandText.Length - m_lastIndex);
+			Writer.Write(Preparer.CommandText, m_lastIndex, Preparer.CommandText.Length - m_lastIndex);
 			if ((states & FinalParseStates.NeedsNewline) == FinalParseStates.NeedsNewline)
-				m_writer.Write((byte) '\n');
+				Writer.Write((byte) '\n');
 			if ((states & FinalParseStates.NeedsSemicolon) == FinalParseStates.NeedsSemicolon && (Preparer.Options & StatementPreparerOptions.AppendSemicolon) == StatementPreparerOptions.AppendSemicolon)
-				m_writer.Write((byte) ';');
+				Writer.Write((byte) ';');
 			IsComplete = (states & FinalParseStates.Complete) == FinalParseStates.Complete;
 		}
 
-		private readonly ByteBufferWriter m_writer;
+		private ByteBufferWriter Writer { get; } = writer;
+
 		private int m_currentParameterIndex;
 		private int m_lastIndex;
 	}
 
-	private sealed class PreparedCommandSqlParser : SqlParser
+	private sealed class PreparedCommandSqlParser(StatementPreparer preparer, List<ParsedStatement> statements, List<int> statementStartEndIndexes, ByteBufferWriter writer)
+		: SqlParser(preparer)
 	{
-		public PreparedCommandSqlParser(StatementPreparer preparer, List<ParsedStatement> statements, List<int> statementStartEndIndexes, ByteBufferWriter writer)
-			: base(preparer)
-		{
-			m_statements = statements;
-			m_statementStartEndIndexes = statementStartEndIndexes;
-			m_writer = writer;
-		}
-
 		protected override void OnStatementBegin(int index)
 		{
-			m_statements.Add(new ParsedStatement());
-			m_statementStartEndIndexes.Add(m_writer.Position);
-			m_writer.Write((byte) CommandKind.StatementPrepare);
+			Statements.Add(new ParsedStatement());
+			StatementStartEndIndexes.Add(Writer.Position);
+			Writer.Write((byte) CommandKind.StatementPrepare);
 			m_lastIndex = index;
 		}
 
 		protected override void OnNamedParameter(int index, int length)
 		{
-			var parameterName = Preparer.m_commandText.Substring(index, length);
+			var parameterName = Preparer.CommandText.Substring(index, length);
 			DoAppendParameter(parameterName, -1, index, length);
 		}
 
@@ -139,32 +121,32 @@ protected override void OnPositionalParameter(int index)
 		private void DoAppendParameter(string? parameterName, int parameterIndex, int textIndex, int textLength)
 		{
 			// write all SQL up to the parameter
-			m_writer.Write(Preparer.m_commandText, m_lastIndex, textIndex - m_lastIndex);
+			Writer.Write(Preparer.CommandText, m_lastIndex, textIndex - m_lastIndex);
 			m_lastIndex = textIndex + textLength;
 
 			// replace the parameter with a ? placeholder
-			m_writer.Write((byte) '?');
+			Writer.Write((byte) '?');
 
 			// store the parameter index
-			m_statements[m_statements.Count - 1].ParameterNames.Add(parameterName);
-			m_statements[m_statements.Count - 1].NormalizedParameterNames.Add(parameterName == null ? null : MySqlParameter.NormalizeParameterName(parameterName));
-			m_statements[m_statements.Count - 1].ParameterIndexes.Add(parameterIndex);
+			Statements[^1].ParameterNames.Add(parameterName);
+			Statements[^1].NormalizedParameterNames.Add(parameterName == null ? null : MySqlParameter.NormalizeParameterName(parameterName));
+			Statements[^1].ParameterIndexes.Add(parameterIndex);
 		}
 
 		protected override void OnStatementEnd(int index)
 		{
-			m_writer.Write(Preparer.m_commandText, m_lastIndex, index - m_lastIndex);
+			Writer.Write(Preparer.CommandText, m_lastIndex, index - m_lastIndex);
 			m_lastIndex = index;
-			m_statementStartEndIndexes.Add(m_writer.Position);
+			StatementStartEndIndexes.Add(Writer.Position);
 		}
 
-		private readonly List<ParsedStatement> m_statements;
-		private readonly List<int> m_statementStartEndIndexes;
-		private readonly ByteBufferWriter m_writer;
+		private List<ParsedStatement> Statements { get; } = statements;
+		private List<int> StatementStartEndIndexes { get; } = statementStartEndIndexes;
+		private ByteBufferWriter Writer { get; } = writer;
+
 		private int m_currentParameterIndex;
 		private int m_lastIndex;
 	}
 
-	private readonly string m_commandText;
-	private readonly MySqlParameterCollection? m_parameters;
+	private string CommandText { get; } = commandText;
 }
diff --git a/src/MySqlConnector/Core/TextRow.cs b/src/MySqlConnector/Core/TextRow.cs
deleted file mode 100644
index c8c172e02..000000000
--- a/src/MySqlConnector/Core/TextRow.cs
+++ /dev/null
@@ -1,148 +0,0 @@
-using System.Buffers.Text;
-using System.Text;
-using MySqlConnector.Protocol;
-using MySqlConnector.Protocol.Payloads;
-using MySqlConnector.Protocol.Serialization;
-using MySqlConnector.Utilities;
-
-namespace MySqlConnector.Core;
-
-internal sealed class TextRow : Row
-{
-	public TextRow(ResultSet resultSet)
-		: base(resultSet)
-	{
-	}
-
-	protected override Row CloneCore() => new TextRow(ResultSet);
-
-	protected override void GetDataOffsets(ReadOnlySpan<byte> data, int[] dataOffsets, int[] dataLengths)
-	{
-		var reader = new ByteArrayReader(data);
-		for (var column = 0; column < dataOffsets.Length; column++)
-		{
-			var length = reader.ReadLengthEncodedIntegerOrNull();
-			dataLengths[column] = length == -1 ? 0 : length;
-			dataOffsets[column] = length == -1 ? -1 : reader.Offset;
-			reader.Offset += dataLengths[column];
-		}
-	}
-
-	protected override int GetInt32Core(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition) =>
-		!Utf8Parser.TryParse(data, out int value, out var bytesConsumed) || bytesConsumed != data.Length ? throw new OverflowException() : value;
-
-	protected override object GetValueCore(ReadOnlySpan<byte> data, ColumnDefinitionPayload columnDefinition)
-	{
-		var isUnsigned = (columnDefinition.ColumnFlags & ColumnFlags.Unsigned) != 0;
-		switch (columnDefinition.ColumnType)
-		{
-		case ColumnType.Tiny:
-			var value = ParseInt32(data);
-			if (Connection.TreatTinyAsBoolean && columnDefinition.ColumnLength == 1 && !isUnsigned)
-				return value != 0;
-			return isUnsigned ? (object) (byte) value : (sbyte) value;
-
-		case ColumnType.Int24:
-		case ColumnType.Long:
-			return isUnsigned ? (object) ParseUInt32(data) : ParseInt32(data);
-
-		case ColumnType.Longlong:
-			return isUnsigned ? (object) ParseUInt64(data) : ParseInt64(data);
-
-		case ColumnType.Bit:
-			return ReadBit(data, columnDefinition);
-
-		case ColumnType.String:
-			if (Connection.GuidFormat == MySqlGuidFormat.Char36 && columnDefinition.ColumnLength / ProtocolUtility.GetBytesPerCharacter(columnDefinition.CharacterSet) == 36)
-				return Utf8Parser.TryParse(data, out Guid guid, out int guid36BytesConsumed, 'D') && guid36BytesConsumed == 36 ? guid : throw new FormatException($"Could not parse CHAR(36) value as Guid: {Encoding.UTF8.GetString(data)}");
-			if (Connection.GuidFormat == MySqlGuidFormat.Char32 && columnDefinition.ColumnLength / ProtocolUtility.GetBytesPerCharacter(columnDefinition.CharacterSet) == 32)
-				return Utf8Parser.TryParse(data, out Guid guid, out int guid32BytesConsumed, 'N') && guid32BytesConsumed == 32 ? guid : throw new FormatException($"Could not parse CHAR(32) value as Guid: {Encoding.UTF8.GetString(data)}");
-			goto case ColumnType.VarString;
-
-		case ColumnType.VarString:
-		case ColumnType.VarChar:
-		case ColumnType.TinyBlob:
-		case ColumnType.Blob:
-		case ColumnType.MediumBlob:
-		case ColumnType.LongBlob:
-		case ColumnType.Enum:
-		case ColumnType.Set:
-			if (columnDefinition.CharacterSet == CharacterSet.Binary)
-			{
-				var guidFormat = Connection.GuidFormat;
-				if ((guidFormat is MySqlGuidFormat.Binary16 or MySqlGuidFormat.TimeSwapBinary16 or MySqlGuidFormat.LittleEndianBinary16) && columnDefinition.ColumnLength == 16)
-					return CreateGuidFromBytes(guidFormat, data);
-
-				return data.ToArray();
-			}
-			return Encoding.UTF8.GetString(data);
-
-		case ColumnType.Json:
-			return Encoding.UTF8.GetString(data);
-
-		case ColumnType.Short:
-			return isUnsigned ? (object) ParseUInt16(data) : ParseInt16(data);
-
-		case ColumnType.Date:
-		case ColumnType.DateTime:
-		case ColumnType.NewDate:
-		case ColumnType.Timestamp:
-			return ParseDateTime(data);
-
-		case ColumnType.Time:
-			return Utility.ParseTimeSpan(data);
-
-		case ColumnType.Year:
-			return ParseInt32(data);
-
-		case ColumnType.Float:
-			if (Utf8Parser.TryParse(data, out float floatValue, out var floatBytesConsumed) && floatBytesConsumed == data.Length)
-				return floatValue;
-			ReadOnlySpan<byte> floatInfinity = "-inf"u8;
-			if (data.SequenceEqual(floatInfinity))
-				return float.NegativeInfinity;
-			if (data.SequenceEqual(floatInfinity.Slice(1)))
-				return float.PositiveInfinity;
-			if (data.SequenceEqual("nan"u8))
-				return float.NaN;
-			throw new FormatException($"Couldn't parse value as float: {Encoding.UTF8.GetString(data)}");
-
-		case ColumnType.Double:
-			if (Utf8Parser.TryParse(data, out double doubleValue, out var doubleBytesConsumed) && doubleBytesConsumed == data.Length)
-				return doubleValue;
-			ReadOnlySpan<byte> doubleInfinity = "-inf"u8;
-			if (data.SequenceEqual(doubleInfinity))
-				return double.NegativeInfinity;
-			if (data.SequenceEqual(doubleInfinity.Slice(1)))
-				return double.PositiveInfinity;
-			if (data.SequenceEqual("nan"u8))
-				return double.NaN;
-			throw new FormatException($"Couldn't parse value as double: {Encoding.UTF8.GetString(data)}");
-
-		case ColumnType.Decimal:
-		case ColumnType.NewDecimal:
-			return Utf8Parser.TryParse(data, out decimal decimalValue, out int bytesConsumed) && bytesConsumed == data.Length ? decimalValue : throw new FormatException();
-
-		case ColumnType.Geometry:
-			return data.ToArray();
-
-		default:
-			throw new NotImplementedException($"Reading {columnDefinition.ColumnType} not implemented");
-		}
-	}
-
-	private static short ParseInt16(ReadOnlySpan<byte> data) =>
-		!Utf8Parser.TryParse(data, out short value, out var bytesConsumed) || bytesConsumed != data.Length ? throw new FormatException() : value;
-
-	private static ushort ParseUInt16(ReadOnlySpan<byte> data) =>
-		!Utf8Parser.TryParse(data, out ushort value, out var bytesConsumed) || bytesConsumed != data.Length ? throw new FormatException() : value;
-
-	private static int ParseInt32(ReadOnlySpan<byte> data) =>
-		!Utf8Parser.TryParse(data, out int value, out var bytesConsumed) || bytesConsumed != data.Length ? throw new FormatException() : value;
-
-	private static uint ParseUInt32(ReadOnlySpan<byte> data) =>
-		!Utf8Parser.TryParse(data, out uint value, out var bytesConsumed) || bytesConsumed != data.Length ? throw new FormatException() : value;
-
-	private static long ParseInt64(ReadOnlySpan<byte> data) =>
-		!Utf8Parser.TryParse(data, out long value, out var bytesConsumed) || bytesConsumed != data.Length ? throw new FormatException() : value;
-}
diff --git a/src/MySqlConnector/Core/TypeMapper.cs b/src/MySqlConnector/Core/TypeMapper.cs
index 9df3dd64c..9d361d930 100644
--- a/src/MySqlConnector/Core/TypeMapper.cs
+++ b/src/MySqlConnector/Core/TypeMapper.cs
@@ -13,25 +13,25 @@ internal sealed class TypeMapper
 
 	private TypeMapper()
 	{
-		m_columnTypeMetadata = new();
-		m_dbTypeMappingsByClrType = new();
-		m_dbTypeMappingsByDbType = new();
+		m_columnTypeMetadata = [];
+		m_dbTypeMappingsByClrType = [];
+		m_dbTypeMappingsByDbType = [];
 		m_columnTypeMetadataLookup = new(StringComparer.OrdinalIgnoreCase);
-		m_mySqlDbTypeToColumnTypeMetadata = new();
+		m_mySqlDbTypeToColumnTypeMetadata = [];
 
 		// boolean
-		var typeBoolean = AddDbTypeMapping(new(typeof(bool), new[] { DbType.Boolean }, convert: static o => Convert.ToBoolean(o, CultureInfo.InvariantCulture)));
+		var typeBoolean = AddDbTypeMapping(new(typeof(bool), [DbType.Boolean], convert: static o => Convert.ToBoolean(o, CultureInfo.InvariantCulture)));
 		AddColumnTypeMetadata(new("TINYINT", typeBoolean, MySqlDbType.Bool, isUnsigned: false, length: 1, columnSize: 1, simpleDataTypeName: "BOOL", createFormat: "BOOL"));
 
 		// integers
-		var typeSbyte = AddDbTypeMapping(new(typeof(sbyte), new[] { DbType.SByte }, convert: static o => Convert.ToSByte(o, CultureInfo.InvariantCulture)));
-		var typeByte = AddDbTypeMapping(new(typeof(byte), new[] { DbType.Byte }, convert: static o => Convert.ToByte(o, CultureInfo.InvariantCulture)));
-		var typeShort = AddDbTypeMapping(new(typeof(short), new[] { DbType.Int16 }, convert: static o => Convert.ToInt16(o, CultureInfo.InvariantCulture)));
-		var typeUshort = AddDbTypeMapping(new(typeof(ushort), new[] { DbType.UInt16 }, convert: static o => Convert.ToUInt16(o, CultureInfo.InvariantCulture)));
-		var typeInt = AddDbTypeMapping(new(typeof(int), new[] { DbType.Int32 }, convert: static o => Convert.ToInt32(o, CultureInfo.InvariantCulture)));
-		var typeUint = AddDbTypeMapping(new(typeof(uint), new[] { DbType.UInt32 }, convert: static o => Convert.ToUInt32(o, CultureInfo.InvariantCulture)));
-		var typeLong = AddDbTypeMapping(new(typeof(long), new[] { DbType.Int64 }, convert: static o => Convert.ToInt64(o, CultureInfo.InvariantCulture)));
-		var typeUlong = AddDbTypeMapping(new(typeof(ulong), new[] { DbType.UInt64 }, convert: static o => Convert.ToUInt64(o, CultureInfo.InvariantCulture)));
+		var typeSbyte = AddDbTypeMapping(new(typeof(sbyte), [DbType.SByte], convert: static o => Convert.ToSByte(o, CultureInfo.InvariantCulture)));
+		var typeByte = AddDbTypeMapping(new(typeof(byte), [DbType.Byte], convert: static o => Convert.ToByte(o, CultureInfo.InvariantCulture)));
+		var typeShort = AddDbTypeMapping(new(typeof(short), [DbType.Int16], convert: static o => Convert.ToInt16(o, CultureInfo.InvariantCulture)));
+		var typeUshort = AddDbTypeMapping(new(typeof(ushort), [DbType.UInt16], convert: static o => Convert.ToUInt16(o, CultureInfo.InvariantCulture)));
+		var typeInt = AddDbTypeMapping(new(typeof(int), [DbType.Int32], convert: static o => Convert.ToInt32(o, CultureInfo.InvariantCulture)));
+		var typeUint = AddDbTypeMapping(new(typeof(uint), [DbType.UInt32], convert: static o => Convert.ToUInt32(o, CultureInfo.InvariantCulture)));
+		var typeLong = AddDbTypeMapping(new(typeof(long), [DbType.Int64], convert: static o => Convert.ToInt64(o, CultureInfo.InvariantCulture)));
+		var typeUlong = AddDbTypeMapping(new(typeof(ulong), [DbType.UInt64], convert: static o => Convert.ToUInt64(o, CultureInfo.InvariantCulture)));
 		AddColumnTypeMetadata(new("TINYINT", typeSbyte, MySqlDbType.Byte, isUnsigned: false));
 		AddColumnTypeMetadata(new("TINYINT", typeByte, MySqlDbType.UByte, isUnsigned: true, length: 1));
 		AddColumnTypeMetadata(new("TINYINT", typeByte, MySqlDbType.UByte, isUnsigned: true));
@@ -46,18 +46,22 @@ private TypeMapper()
 		AddColumnTypeMetadata(new("BIT", typeUlong, MySqlDbType.Bit));
 
 		// decimals
-		var typeDecimal = AddDbTypeMapping(new(typeof(decimal), new[] { DbType.Decimal, DbType.Currency, DbType.VarNumeric }, convert: static o => Convert.ToDecimal(o, CultureInfo.InvariantCulture)));
-		var typeDouble = AddDbTypeMapping(new(typeof(double), new[] { DbType.Double }, convert: static o => Convert.ToDouble(o, CultureInfo.InvariantCulture)));
-		var typeFloat = AddDbTypeMapping(new(typeof(float), new[] { DbType.Single }, convert: static o => Convert.ToSingle(o, CultureInfo.InvariantCulture)));
+		var typeDecimal = AddDbTypeMapping(new(typeof(decimal), [DbType.Decimal, DbType.Currency, DbType.VarNumeric], convert: static o => Convert.ToDecimal(o, CultureInfo.InvariantCulture)));
+		var typeDouble = AddDbTypeMapping(new(typeof(double), [DbType.Double], convert: static o => Convert.ToDouble(o, CultureInfo.InvariantCulture)));
+		var typeFloat = AddDbTypeMapping(new(typeof(float), [DbType.Single], convert: static o => Convert.ToSingle(o, CultureInfo.InvariantCulture)));
 		AddColumnTypeMetadata(new("DECIMAL", typeDecimal, MySqlDbType.NewDecimal, createFormat: "DECIMAL({0},{1});precision,scale"));
 		AddColumnTypeMetadata(new("DECIMAL", typeDecimal, MySqlDbType.NewDecimal, isUnsigned: true, createFormat: "DECIMAL({0},{1}) UNSIGNED;precision,scale"));
 		AddColumnTypeMetadata(new("DECIMAL", typeDecimal, MySqlDbType.Decimal));
 		AddColumnTypeMetadata(new("DOUBLE", typeDouble, MySqlDbType.Double));
 		AddColumnTypeMetadata(new("FLOAT", typeFloat, MySqlDbType.Float));
 
+		 // vector
+		var typeFloatReadOnlyMemory = AddDbTypeMapping(new(typeof(ReadOnlyMemory<float>), [DbType.Object]));
+		AddColumnTypeMetadata(new("VECTOR", typeFloatReadOnlyMemory, MySqlDbType.Vector, binary: true, simpleDataTypeName: "VECTOR", createFormat: "VECTOR({0})"));
+
 		// string
-		var typeFixedString = AddDbTypeMapping(new(typeof(string), new[] { DbType.StringFixedLength, DbType.AnsiStringFixedLength }, convert: Convert.ToString!));
-		var typeString = AddDbTypeMapping(new(typeof(string), new[] { DbType.String, DbType.AnsiString, DbType.Xml }, convert: Convert.ToString!));
+		var typeFixedString = AddDbTypeMapping(new(typeof(string), [DbType.StringFixedLength, DbType.AnsiStringFixedLength], convert: Convert.ToString!));
+		var typeString = AddDbTypeMapping(new(typeof(string), [DbType.String, DbType.AnsiString, DbType.Xml], convert: Convert.ToString!));
 		AddColumnTypeMetadata(new("VARCHAR", typeString, MySqlDbType.VarChar, createFormat: "VARCHAR({0});size"));
 		AddColumnTypeMetadata(new("VARCHAR", typeString, MySqlDbType.VarString));
 		AddColumnTypeMetadata(new("CHAR", typeFixedString, MySqlDbType.String, createFormat: "CHAR({0});size"));
@@ -70,7 +74,7 @@ private TypeMapper()
 		AddColumnTypeMetadata(new("JSON", typeString, MySqlDbType.JSON));
 
 		// binary
-		var typeBinary = AddDbTypeMapping(new(typeof(byte[]), new[] { DbType.Binary }));
+		var typeBinary = AddDbTypeMapping(new(typeof(byte[]), [DbType.Binary]));
 		AddColumnTypeMetadata(new("BLOB", typeBinary, MySqlDbType.Blob, binary: true, columnSize: ushort.MaxValue, simpleDataTypeName: "BLOB"));
 		AddColumnTypeMetadata(new("BINARY", typeBinary, MySqlDbType.Binary, binary: true, simpleDataTypeName: "BLOB", createFormat: "BINARY({0});length"));
 		AddColumnTypeMetadata(new("VARBINARY", typeBinary, MySqlDbType.VarBinary, binary: true, simpleDataTypeName: "BLOB", createFormat: "VARBINARY({0});length"));
@@ -91,15 +95,15 @@ private TypeMapper()
 
 		// date/time
 #if NET6_0_OR_GREATER
-		AddDbTypeMapping(new(typeof(DateOnly), new[] { DbType.Date }));
+		AddDbTypeMapping(new(typeof(DateOnly), [DbType.Date]));
 #endif
-		var typeDate = AddDbTypeMapping(new(typeof(DateTime), new[] { DbType.Date }));
-		var typeDateTime = AddDbTypeMapping(new(typeof(DateTime), new[] { DbType.DateTime, DbType.DateTime2, DbType.DateTimeOffset }));
-		AddDbTypeMapping(new(typeof(DateTimeOffset), new[] { DbType.DateTimeOffset }));
+		var typeDate = AddDbTypeMapping(new(typeof(DateTime), [DbType.Date]));
+		var typeDateTime = AddDbTypeMapping(new(typeof(DateTime), [DbType.DateTime, DbType.DateTime2, DbType.DateTimeOffset]));
+		AddDbTypeMapping(new(typeof(DateTimeOffset), [DbType.DateTimeOffset]));
 #if NET6_0_OR_GREATER
-		AddDbTypeMapping(new(typeof(TimeOnly), new[] { DbType.Time }));
+		AddDbTypeMapping(new(typeof(TimeOnly), [DbType.Time]));
 #endif
-		var typeTime = AddDbTypeMapping(new(typeof(TimeSpan), new[] { DbType.Time }, convert: static o => o is string s ? Utility.ParseTimeSpan(Encoding.UTF8.GetBytes(s)) : Convert.ChangeType(o, typeof(TimeSpan), CultureInfo.InvariantCulture)));
+		var typeTime = AddDbTypeMapping(new(typeof(TimeSpan), [DbType.Time], convert: static o => o is string s ? Utility.ParseTimeSpan(Encoding.UTF8.GetBytes(s)) : Convert.ChangeType(o, typeof(TimeSpan), CultureInfo.InvariantCulture)));
 		AddColumnTypeMetadata(new("DATETIME", typeDateTime, MySqlDbType.DateTime));
 		AddColumnTypeMetadata(new("DATE", typeDate, MySqlDbType.Date));
 		AddColumnTypeMetadata(new("DATE", typeDate, MySqlDbType.Newdate));
@@ -113,11 +117,14 @@ private TypeMapper()
 #else
 		Func<object, object> convertGuid = static o => Guid.Parse(Convert.ToString(o, CultureInfo.InvariantCulture)!);
 #endif
-		var typeGuid = AddDbTypeMapping(new(typeof(Guid), new[] { DbType.Guid }, convert: convertGuid));
+		var typeGuid = AddDbTypeMapping(new(typeof(Guid), [DbType.Guid], convert: convertGuid));
 		AddColumnTypeMetadata(new("CHAR", typeGuid, MySqlDbType.Guid, length: 36, simpleDataTypeName: "CHAR(36)", createFormat: "CHAR(36)"));
+		AddColumnTypeMetadata(new("CHAR", typeGuid, MySqlDbType.Guid, length: 32, guidFormat: MySqlGuidFormat.Char32));
+		AddColumnTypeMetadata(new("CHAR", typeGuid, MySqlDbType.Guid, length: 36, guidFormat: MySqlGuidFormat.Char36));
+		AddColumnTypeMetadata(new("BINARY", typeGuid, MySqlDbType.Guid, binary: true, length: 16, guidFormat: MySqlGuidFormat.Binary16));
 
 		// null
-		var typeNull = AddDbTypeMapping(new(typeof(object), new[] { DbType.Object }));
+		var typeNull = AddDbTypeMapping(new(typeof(object), [DbType.Object]));
 		AddColumnTypeMetadata(new("NULL", typeNull, MySqlDbType.Null));
 	}
 
@@ -154,37 +161,47 @@ private void AddColumnTypeMetadata(ColumnTypeMetadata columnTypeMetadata)
 	{
 		m_columnTypeMetadata.Add(columnTypeMetadata);
 		var lookupKey = columnTypeMetadata.CreateLookupKey();
+#if !NETCOREAPP2_0_OR_GREATER && !NETSTANDARD2_1_OR_GREATER
 		if (!m_columnTypeMetadataLookup.ContainsKey(lookupKey))
 			m_columnTypeMetadataLookup.Add(lookupKey, columnTypeMetadata);
 		if (!m_mySqlDbTypeToColumnTypeMetadata.ContainsKey(columnTypeMetadata.MySqlDbType))
 			m_mySqlDbTypeToColumnTypeMetadata.Add(columnTypeMetadata.MySqlDbType, columnTypeMetadata);
+#else
+		m_columnTypeMetadataLookup.TryAdd(lookupKey, columnTypeMetadata);
+		m_mySqlDbTypeToColumnTypeMetadata.TryAdd(columnTypeMetadata.MySqlDbType, columnTypeMetadata);
+#endif
 	}
 
 	internal DbTypeMapping? GetDbTypeMapping(Type clrType)
 	{
 		if (clrType.IsEnum)
 			clrType = Enum.GetUnderlyingType(clrType);
-		m_dbTypeMappingsByClrType.TryGetValue(clrType, out var dbTypeMapping);
+		_ = m_dbTypeMappingsByClrType.TryGetValue(clrType, out var dbTypeMapping);
 		return dbTypeMapping;
 	}
 
 	internal DbTypeMapping? GetDbTypeMapping(DbType dbType)
 	{
-		m_dbTypeMappingsByDbType.TryGetValue(dbType, out var dbTypeMapping);
+		_ = m_dbTypeMappingsByDbType.TryGetValue(dbType, out var dbTypeMapping);
 		return dbTypeMapping;
 	}
 
 	public DbTypeMapping? GetDbTypeMapping(string columnTypeName, bool unsigned = false, int length = 0)
 	{
-		return GetColumnTypeMetadata(columnTypeName, unsigned, length)?.DbTypeMapping;
+		return GetColumnTypeMetadata(columnTypeName, unsigned, length, MySqlGuidFormat.Default)?.DbTypeMapping;
 	}
 
-	public MySqlDbType GetMySqlDbType(string typeName, bool unsigned, int length) => GetColumnTypeMetadata(typeName, unsigned, length)!.MySqlDbType;
+	public MySqlDbType GetMySqlDbType(string typeName, bool unsigned, int length, MySqlGuidFormat guidFormat) =>
+		GetColumnTypeMetadata(typeName, unsigned, length, guidFormat)!.MySqlDbType;
 
-	private ColumnTypeMetadata? GetColumnTypeMetadata(string columnTypeName, bool unsigned, int length)
+	private ColumnTypeMetadata? GetColumnTypeMetadata(string columnTypeName, bool unsigned, int length, MySqlGuidFormat guidFormat)
 	{
-		if (!m_columnTypeMetadataLookup.TryGetValue(ColumnTypeMetadata.CreateLookupKey(columnTypeName, unsigned, length), out var columnTypeMetadata) && length != 0)
-			m_columnTypeMetadataLookup.TryGetValue(ColumnTypeMetadata.CreateLookupKey(columnTypeName, unsigned, 0), out columnTypeMetadata);
+		if (m_columnTypeMetadataLookup.TryGetValue(ColumnTypeMetadata.CreateLookupKey(columnTypeName, unsigned, length, guidFormat), out var columnTypeMetadata))
+			return columnTypeMetadata;
+		if (guidFormat != MySqlGuidFormat.Default && m_columnTypeMetadataLookup.TryGetValue(ColumnTypeMetadata.CreateLookupKey(columnTypeName, unsigned, length, MySqlGuidFormat.Default), out columnTypeMetadata))
+			return columnTypeMetadata;
+		if (length != 0)
+			m_columnTypeMetadataLookup.TryGetValue(ColumnTypeMetadata.CreateLookupKey(columnTypeName, unsigned, 0, MySqlGuidFormat.Default), out columnTypeMetadata);
 		return columnTypeMetadata;
 	}
 
@@ -193,113 +210,116 @@ public static MySqlDbType ConvertToMySqlDbType(ColumnDefinitionPayload columnDef
 		var isUnsigned = (columnDefinition.ColumnFlags & ColumnFlags.Unsigned) != 0;
 		switch (columnDefinition.ColumnType)
 		{
-		case ColumnType.Tiny:
-			return treatTinyAsBoolean && columnDefinition.ColumnLength == 1 && !isUnsigned ? MySqlDbType.Bool :
-				isUnsigned ? MySqlDbType.UByte : MySqlDbType.Byte;
+			case ColumnType.Tiny:
+				return treatTinyAsBoolean && columnDefinition.ColumnLength == 1 && !isUnsigned ? MySqlDbType.Bool :
+					isUnsigned ? MySqlDbType.UByte : MySqlDbType.Byte;
 
-		case ColumnType.Int24:
-			return isUnsigned ? MySqlDbType.UInt24 : MySqlDbType.Int24;
+			case ColumnType.Int24:
+				return isUnsigned ? MySqlDbType.UInt24 : MySqlDbType.Int24;
 
-		case ColumnType.Long:
-			return isUnsigned ? MySqlDbType.UInt32 : MySqlDbType.Int32;
+			case ColumnType.Long:
+				return isUnsigned ? MySqlDbType.UInt32 : MySqlDbType.Int32;
 
-		case ColumnType.Longlong:
-			return isUnsigned ? MySqlDbType.UInt64 : MySqlDbType.Int64;
+			case ColumnType.Longlong:
+				return isUnsigned ? MySqlDbType.UInt64 : MySqlDbType.Int64;
 
-		case ColumnType.Bit:
-			return MySqlDbType.Bit;
+			case ColumnType.Bit:
+				return MySqlDbType.Bit;
 
-		case ColumnType.String:
-			if (guidFormat == MySqlGuidFormat.Char36 && columnDefinition.ColumnLength / ProtocolUtility.GetBytesPerCharacter(columnDefinition.CharacterSet) == 36)
-				return MySqlDbType.Guid;
-			if (guidFormat == MySqlGuidFormat.Char32 && columnDefinition.ColumnLength / ProtocolUtility.GetBytesPerCharacter(columnDefinition.CharacterSet) == 32)
-				return MySqlDbType.Guid;
-			if ((columnDefinition.ColumnFlags & ColumnFlags.Enum) != 0)
-				return MySqlDbType.Enum;
-			if ((columnDefinition.ColumnFlags & ColumnFlags.Set) != 0)
-				return MySqlDbType.Set;
-			goto case ColumnType.VarString;
-
-		case ColumnType.VarChar:
-		case ColumnType.VarString:
-		case ColumnType.TinyBlob:
-		case ColumnType.Blob:
-		case ColumnType.MediumBlob:
-		case ColumnType.LongBlob:
-			var type = columnDefinition.ColumnType;
-			if (columnDefinition.CharacterSet == CharacterSet.Binary)
-			{
-				if ((guidFormat is MySqlGuidFormat.Binary16 or MySqlGuidFormat.TimeSwapBinary16 or MySqlGuidFormat.LittleEndianBinary16) && columnDefinition.ColumnLength == 16)
+			case ColumnType.String:
+				if (guidFormat == MySqlGuidFormat.Char36 && columnDefinition.ColumnLength / ProtocolUtility.GetBytesPerCharacter(columnDefinition.CharacterSet) == 36)
 					return MySqlDbType.Guid;
-
+				if (guidFormat == MySqlGuidFormat.Char32 && columnDefinition.ColumnLength / ProtocolUtility.GetBytesPerCharacter(columnDefinition.CharacterSet) == 32)
+					return MySqlDbType.Guid;
+				if ((columnDefinition.ColumnFlags & ColumnFlags.Enum) != 0)
+					return MySqlDbType.Enum;
+				if ((columnDefinition.ColumnFlags & ColumnFlags.Set) != 0)
+					return MySqlDbType.Set;
+				goto case ColumnType.VarString;
+
+			case ColumnType.VarChar:
+			case ColumnType.VarString:
+			case ColumnType.TinyBlob:
+			case ColumnType.Blob:
+			case ColumnType.MediumBlob:
+			case ColumnType.LongBlob:
+				var type = columnDefinition.ColumnType;
+				if (columnDefinition.CharacterSet == CharacterSet.Binary)
+				{
+					if ((guidFormat is MySqlGuidFormat.Binary16 or MySqlGuidFormat.TimeSwapBinary16 or MySqlGuidFormat.LittleEndianBinary16) && columnDefinition.ColumnLength == 16)
+						return MySqlDbType.Guid;
+
+					return type switch
+					{
+						ColumnType.String => MySqlDbType.Binary,
+						ColumnType.VarString => MySqlDbType.VarBinary,
+						ColumnType.TinyBlob => MySqlDbType.TinyBlob,
+						ColumnType.Blob => MySqlDbType.Blob,
+						ColumnType.MediumBlob => MySqlDbType.MediumBlob,
+						_ => MySqlDbType.LongBlob,
+					};
+				}
 				return type switch
 				{
-					ColumnType.String => MySqlDbType.Binary,
-					ColumnType.VarString => MySqlDbType.VarBinary,
-					ColumnType.TinyBlob => MySqlDbType.TinyBlob,
-					ColumnType.Blob => MySqlDbType.Blob,
-					ColumnType.MediumBlob => MySqlDbType.MediumBlob,
-					_ => MySqlDbType.LongBlob,
+					ColumnType.String => MySqlDbType.String,
+					ColumnType.VarString => MySqlDbType.VarChar,
+					ColumnType.TinyBlob => MySqlDbType.TinyText,
+					ColumnType.Blob => MySqlDbType.Text,
+					ColumnType.MediumBlob => MySqlDbType.MediumText,
+					_ => MySqlDbType.LongText,
 				};
-			}
-			return type switch
-			{
-				ColumnType.String => MySqlDbType.String,
-				ColumnType.VarString => MySqlDbType.VarChar,
-				ColumnType.TinyBlob => MySqlDbType.TinyText,
-				ColumnType.Blob => MySqlDbType.Text,
-				ColumnType.MediumBlob => MySqlDbType.MediumText,
-				_ => MySqlDbType.LongText,
-			};
 
-		case ColumnType.Json:
-			return MySqlDbType.JSON;
+			case ColumnType.Json:
+				return MySqlDbType.JSON;
 
-		case ColumnType.Short:
-			return isUnsigned ? MySqlDbType.UInt16 : MySqlDbType.Int16;
+			case ColumnType.Short:
+				return isUnsigned ? MySqlDbType.UInt16 : MySqlDbType.Int16;
 
-		case ColumnType.Date:
-		case ColumnType.NewDate:
-			return MySqlDbType.Date;
+			case ColumnType.Date:
+			case ColumnType.NewDate:
+				return MySqlDbType.Date;
 
-		case ColumnType.DateTime:
-			return MySqlDbType.DateTime;
+			case ColumnType.DateTime:
+				return MySqlDbType.DateTime;
 
-		case ColumnType.Timestamp:
-			return MySqlDbType.Timestamp;
+			case ColumnType.Timestamp:
+				return MySqlDbType.Timestamp;
 
-		case ColumnType.Time:
-			return MySqlDbType.Time;
+			case ColumnType.Time:
+				return MySqlDbType.Time;
 
-		case ColumnType.Year:
-			return MySqlDbType.Year;
+			case ColumnType.Year:
+				return MySqlDbType.Year;
 
-		case ColumnType.Float:
-			return MySqlDbType.Float;
+			case ColumnType.Float:
+				return MySqlDbType.Float;
 
-		case ColumnType.Double:
-			return MySqlDbType.Double;
+			case ColumnType.Double:
+				return MySqlDbType.Double;
 
-		case ColumnType.Decimal:
-			return MySqlDbType.Decimal;
+			case ColumnType.Decimal:
+				return MySqlDbType.Decimal;
 
-		case ColumnType.NewDecimal:
-			return MySqlDbType.NewDecimal;
+			case ColumnType.NewDecimal:
+				return MySqlDbType.NewDecimal;
 
-		case ColumnType.Geometry:
-			return MySqlDbType.Geometry;
+			case ColumnType.Geometry:
+				return MySqlDbType.Geometry;
 
-		case ColumnType.Null:
-			return MySqlDbType.Null;
+			case ColumnType.Null:
+				return MySqlDbType.Null;
 
-		case ColumnType.Enum:
-			return MySqlDbType.Enum;
+			case ColumnType.Enum:
+				return MySqlDbType.Enum;
+
+			case ColumnType.Set:
+				return MySqlDbType.Set;
 
-		case ColumnType.Set:
-			return MySqlDbType.Set;
+			case ColumnType.Vector:
+				return MySqlDbType.Vector;
 
-		default:
-			throw new NotImplementedException($"ConvertToMySqlDbType for {columnDefinition.ColumnType} is not implemented");
+			default:
+				throw new NotImplementedException($"ConvertToMySqlDbType for {columnDefinition.ColumnType} is not implemented");
 		}
 	}
 
@@ -334,6 +354,7 @@ public static ushort ConvertToColumnTypeAndFlags(MySqlDbType dbType, MySqlGuidFo
 			MySqlDbType.NewDecimal => ColumnType.NewDecimal,
 			MySqlDbType.Geometry => ColumnType.Geometry,
 			MySqlDbType.Null => ColumnType.Null,
+			MySqlDbType.Vector => ColumnType.Vector,
 			_ => throw new NotImplementedException($"ConvertToColumnTypeAndFlags for {dbType} is not implemented"),
 		};
 		return (ushort) ((byte) columnType | (isUnsigned ? 0x8000 : 0));
diff --git a/src/MySqlConnector/Core/XaEnlistedTransaction.cs b/src/MySqlConnector/Core/XaEnlistedTransaction.cs
index 3937d420d..9f1ffe73c 100644
--- a/src/MySqlConnector/Core/XaEnlistedTransaction.cs
+++ b/src/MySqlConnector/Core/XaEnlistedTransaction.cs
@@ -3,13 +3,8 @@
 
 namespace MySqlConnector.Core;
 
-internal sealed class XaEnlistedTransaction : EnlistedTransactionBase
+internal sealed class XaEnlistedTransaction(Transaction transaction, MySqlConnection connection) : EnlistedTransactionBase(transaction, connection)
 {
-	public XaEnlistedTransaction(Transaction transaction, MySqlConnection connection)
-		: base(transaction, connection)
-	{
-	}
-
 	protected override void OnStart()
 	{
 		// generate an "xid" with "gtrid" (Global TRansaction ID) from the .NET Transaction and "bqual" (Branch QUALifier)
@@ -37,7 +32,9 @@ protected override void OnRollback(Enlistment enlistment)
 	{
 		try
 		{
-			ExecuteXaCommand("END");
+			if (!IsPrepared)
+				ExecuteXaCommand("END");
+
 			ExecuteXaCommand("ROLLBACK");
 		}
 		catch (MySqlException ex) when (ex.ErrorCode is MySqlErrorCode.XARBDeadlock)
diff --git a/src/MySqlConnector/Logging/ConsoleLoggerProvider.cs b/src/MySqlConnector/Logging/ConsoleLoggerProvider.cs
index f264ffb54..1c210e730 100644
--- a/src/MySqlConnector/Logging/ConsoleLoggerProvider.cs
+++ b/src/MySqlConnector/Logging/ConsoleLoggerProvider.cs
@@ -7,7 +7,7 @@ public class ConsoleLoggerProvider : IMySqlConnectorLoggerProvider
 {
 	public ConsoleLoggerProvider(MySqlConnectorLogLevel minimumLevel = MySqlConnectorLogLevel.Info, bool isColored = true)
 	{
-		if (minimumLevel < MySqlConnectorLogLevel.Trace || minimumLevel > MySqlConnectorLogLevel.Fatal)
+		if (minimumLevel is < MySqlConnectorLogLevel.Trace or > MySqlConnectorLogLevel.Fatal)
 			throw new ArgumentOutOfRangeException(nameof(minimumLevel), "minimumLevel must be between Trace and Fatal");
 
 		m_minimumLevel = minimumLevel;
@@ -16,15 +16,9 @@ public ConsoleLoggerProvider(MySqlConnectorLogLevel minimumLevel = MySqlConnecto
 
 	public IMySqlConnectorLogger CreateLogger(string name) => new ConsoleLogger(this, name);
 
-	private sealed class ConsoleLogger : IMySqlConnectorLogger
+	private sealed class ConsoleLogger(ConsoleLoggerProvider provider, string name) : IMySqlConnectorLogger
 	{
-		public ConsoleLogger(ConsoleLoggerProvider provider, string name)
-		{
-			m_provider = provider;
-			m_name = name;
-		}
-
-		public bool IsEnabled(MySqlConnectorLogLevel level) => level >= m_provider.m_minimumLevel && level <= MySqlConnectorLogLevel.Fatal;
+		public bool IsEnabled(MySqlConnectorLogLevel level) => level >= Provider.m_minimumLevel && level <= MySqlConnectorLogLevel.Fatal;
 
 		public void Log(MySqlConnectorLogLevel level, string message, object?[]? args = null, Exception? exception = null)
 		{
@@ -34,7 +28,7 @@ public void Log(MySqlConnectorLogLevel level, string message, object?[]? args =
 			var sb = new StringBuilder();
 			sb.Append(s_levels[(int) level]);
 			sb.Append('\t');
-			sb.Append(m_name);
+			sb.Append(Name);
 			sb.Append('\t');
 
 			if (args is null || args.Length == 0)
@@ -46,9 +40,9 @@ public void Log(MySqlConnectorLogLevel level, string message, object?[]? args =
 			if (exception is not null)
 				sb.AppendLine(exception.ToString());
 
-			if (m_provider.m_isColored)
+			if (Provider.m_isColored)
 			{
-				lock (m_provider)
+				lock (Provider)
 				{
 					var oldColor = Console.ForegroundColor;
 					Console.ForegroundColor = s_colors[(int) level];
@@ -63,7 +57,7 @@ public void Log(MySqlConnectorLogLevel level, string message, object?[]? args =
 		}
 
 		private static readonly string[] s_levels =
-		{
+		[
 			"",
 			"[TRACE]",
 			"[DEBUG]",
@@ -71,10 +65,10 @@ public void Log(MySqlConnectorLogLevel level, string message, object?[]? args =
 			"[WARN]",
 			"[ERROR]",
 			"[FATAL]",
-		};
+		];
 
 		private static readonly ConsoleColor[] s_colors =
-		{
+		[
 			ConsoleColor.Black,
 			ConsoleColor.DarkGray,
 			ConsoleColor.Gray,
@@ -82,10 +76,10 @@ public void Log(MySqlConnectorLogLevel level, string message, object?[]? args =
 			ConsoleColor.Yellow,
 			ConsoleColor.Red,
 			ConsoleColor.Red,
-		};
+		];
 
-		private readonly ConsoleLoggerProvider m_provider;
-		private readonly string m_name;
+		private ConsoleLoggerProvider Provider { get; } = provider;
+		private string Name { get; } = name;
 	}
 
 	private readonly MySqlConnectorLogLevel m_minimumLevel;
diff --git a/src/MySqlConnector/Logging/EventIds.cs b/src/MySqlConnector/Logging/EventIds.cs
new file mode 100644
index 000000000..de87e8590
--- /dev/null
+++ b/src/MySqlConnector/Logging/EventIds.cs
@@ -0,0 +1,185 @@
+namespace MySqlConnector.Logging;
+
+internal static class EventIds
+{
+	// DataSource events, 1000-1099
+	public const int DataSourceCreatedWithPoolWithName = 1000;
+	public const int DataSourceCreatedWithoutPoolWithName = 1001;
+	public const int DataSourceCreatedWithPoolWithoutName = 1002;
+	public const int DataSourceCreatedWithoutPoolWithoutName = 1003;
+
+	// DataSource errors, 1100-1199
+	public const int PeriodicPasswordProviderFailed = 1100;
+
+	// Core Session events, 2000-2099
+	public const int CreatedNewSession = 2000;
+	public const int CreatedNonPooledSession = 2001;
+	public const int ResettingConnection = 2002;
+	public const int ReturningToPool = 2003;
+	public const int SendingQuitCommand = 2004;
+	public const int ClosingStreamSocket = 2005;
+	public const int ErrorPayload = 2006;
+	public const int FailedInSendReplyAsync = 2007;
+	public const int FailedInReceiveReplyAsync = 2008;
+	public const int SettingStateToFailed = 2009;
+	public const int ExpectedToReadMoreBytes = 2010;
+	public const int ExpectedSessionState1 = 2011;
+	public const int ExpectedSessionState6 = 2016;
+
+	// Session connecting events, 2100-2199
+	public const int ConnectingFailed = 2100;
+	public const int ServerSentAuthPluginName = 2101;
+	public const int UnsupportedAuthenticationMethod = 2102;
+	public const int AutoDetectedAurora57 = 2103;
+	public const int SessionMadeConnection = 2104;
+	public const int ServerDoesNotSupportSsl = 2105;
+	public const int CouldNotConnectToServer = 2108;
+	public const int SendingPipelinedResetConnectionRequest = 2109;
+	public const int SendingResetConnectionRequest = 2110;
+	public const int SendingChangeUserRequest = 2111;
+	public const int SendingChangeUserRequestDueToChangedDatabase = 2112;
+	public const int OptimisticReauthenticationFailed = 2113;
+	public const int IgnoringFailureInTryResetConnectionAsync = 2114;
+	public const int SwitchingToAuthenticationMethod = 2115;
+	public const int NeedsSecureConnection = 2116;
+	public const int AuthenticationMethodNotSupported = 2117;
+	public const int CouldNotLoadServerRsaPublicKey = 2118;
+	public const int CouldNotLoadServerRsaPublicKeyFromFile = 2119;
+	public const int CouldNotUseAuthenticationMethodForRsa = 2120;
+	public const int FailedToResolveHostName = 2121;
+	public const int ConnectingToIpAddress = 2122;
+	public const int ConnectTimeoutExpired = 2123;
+	public const int FailedToConnectToSingleIpAddress = 2124;
+	public const int FailedToConnectToIpAddress = 2125;
+	public const int ConnectedToIpAddress = 2126;
+	public const int ConnectingToUnixSocket = 2127;
+	public const int ConnectTimeoutExpiredForUnixSocket = 2128;
+	public const int ConnectingToNamedPipe = 2129;
+	public const int ConnectTimeoutExpiredForNamedPipe = 2130;
+	public const int InitializingTlsConnection = 2131;
+	public const int NoCertificatesFound = 2132;
+	public const int CertificateNotFoundInStore = 2133;
+	public const int CouldNotLoadCertificate = 2134;
+	public const int NoPrivateKeyIncludedWithCertificateFile = 2135;
+	public const int CouldNotLoadCertificateFromFile = 2136;
+	public const int FailedToObtainClientCertificates = 2137;
+	public const int LoadingCaCertificatesFromFile = 2138;
+	public const int CouldNotLoadCaCertificateFromFile = 2139;
+	public const int LoadingCaCertificate = 2140;
+	public const int LoadedCaCertificatesFromFile = 2141;
+	public const int NotUsingRemoteCertificateValidationCallbackDueToSslCa = 2142;
+	public const int NotUsingRemoteCertificateValidationCallbackDueToSslMode = 2143;
+	public const int UsingRemoteCertificateValidationCallback = 2144;
+	public const int ConnectedTlsBasic = 2145;
+	public const int ConnectedTlsDetailed = 2146;
+	public const int CouldNotInitializeTlsConnection = 2147;
+	public const int LoadingClientKeyFromKeyFile = 2148;
+	public const int CouldNotLoadClientKeyFromKeyFile = 2149;
+	public const int DetectedProxy = 2150;
+	public const int ChangingConnectionId = 2151;
+	public const int FailedToGetConnectionId = 2152;
+	public const int CreatingConnectionAttributes = 2153;
+	public const int ObtainingPasswordViaProvidePasswordCallback = 2154;
+	public const int FailedToObtainPassword = 2155;
+	public const int ConnectedTlsBasicPreliminary = 2156;
+	public const int ConnectedTlsDetailedPreliminary = 2157;
+	public const int CertificateErrorUnixSocket = 2158;
+	public const int CertificateErrorNoPassword = 2159;
+	public const int CertificateErrorValidThumbprint = 2160;
+
+	// Command execution events, 2200-2299
+	public const int CannotExecuteNewCommandInState = 2200;
+	public const int EnteringFinishQuerying = 2201;
+	public const int CommandExecutorExecuteReader = 2202;
+	public const int QueryWasInterrupted = 2203;
+	public const int PreparingCommandPayload = 2204;
+	public const int PreparingCommandPayloadWithId = 2205;
+	public const int QueryAttributesNotSupported = 2206;
+	public const int QueryAttributesNotSupportedWithId = 2207;
+	public const int IgnoringExceptionInDisposeAsync = 2208;
+
+	// Command cancellation events, 2300-2399
+	public const int IgnoringCancellationForCommand = 2300;
+	public const int CommandHasBeenCanceled = 2301;
+	public const int IgnoringCancellationForClosedConnection = 2302;
+	public const int CancelingCommandFailed = 2303;
+	public const int WillCancelCommand = 2304;
+	public const int CancelingCommandFromSession = 2305;
+	public const int IgnoringCancellationForInactiveCommand = 2306;
+	public const int CancelingCommand = 2307;
+	public const int SendingSleepToClearPendingCancellation = 2308;
+
+	// Cached procedure events, 2400-2499
+	public const int GettingCachedProcedure = 2400;
+	public const int PoolDoesNotHaveSharedProcedureCache = 2401;
+	public const int CouldNotNormalizeDatabaseAndName = 2402;
+	public const int FailedToCacheProcedure = 2403;
+	public const int CachingProcedure = 2404;
+	public const int ProcedureCacheCount = 2405;
+	public const int DidNotFindCachedProcedure = 2406;
+	public const int ReturningCachedProcedure = 2407;
+	public const int FailedToRetrieveProcedureMetadata = 2408;
+	public const int ServerDoesNotSupportCachedProcedures = 2409;
+	public const int ProcedureHasRoutineCount = 2410;
+
+	// Ping events, 2500-2599
+	public const int PingingServer = 2500;
+	public const int SuccessfullyPingedServer = 2501;
+	public const int PingFailed = 2502;
+
+	// Bulk copy events, 2600-2699
+	public const int StartingBulkCopy = 2503;
+	public const int AddingDefaultColumnMapping = 2504;
+	public const int IgnoringColumn = 2505;
+	public const int FinishedBulkCopy = 2506;
+	public const int BulkCopyFailed = 2507;
+	public const int ColumnMappingAlreadyHasExpression = 2508;
+	public const int SettingExpressionToMapColumn = 2509;
+
+	// Transaction events, 2700-2799
+	public const int StartingTransaction = 2700;
+	public const int StartedTransaction = 2701;
+	public const int CommittingTransaction = 2702;
+	public const int CommittedTransaction = 2703;
+	public const int RollingBackTransaction = 2704;
+	public const int RolledBackTransaction = 2705;
+
+	// Connection pool events, 3000-3099
+	public const int WaitingForAvailableSession = 3000;
+	public const int CreatingNewConnectionPool = 3001;
+	public const int ScanningForLeakedSessions = 3002;
+	public const int FoundExistingSession = 3003;
+	public const int DiscardingSessionDueToWrongGeneration = 3004;
+	public const int SessionIsUnusable = 3005;
+	public const int ReturningPooledSession = 3006;
+	public const int ReturningNewSession = 3007;
+	public const int DisposingCreatedSessionDueToException = 3008;
+	public const int UnexpectedErrorInGetSessionAsync = 3009;
+	public const int ReceivingSessionBack = 3010;
+	public const int ReceivedInvalidSession = 3011;
+	public const int ReceivedExpiredSession = 3012;
+	public const int ClearingConnectionPool = 3013;
+	public const int DisposingConnectionPool = 3014;
+	public const int RecoveredNoSessions = 3015;
+	public const int RecoveredSessionCount = 3016;
+	public const int FoundSessionToCleanUp = 3017;
+	public const int PoolCreatedNewSession = 3018;
+	public const int CreatedSessionToReachMinimumPoolCount = 3019;
+	public const int CreatedPoolWillNotBeUsed = 3020;
+
+	// Connection pool background events, 3100-3199
+	public const int ReapingConnectionPool = 3100;
+	public const int CheckingForDnsChanges = 3101;
+	public const int DetectedDnsChange = 3102;
+	public const int DnsCheckFailed = 3103;
+	public const int ClearingPoolDueToDnsChanges = 3104;
+
+	// Server redirection events, 3200-3299
+	public const int HasServerRedirectionHeader = 3200;
+	public const int ServerRedirectionIsDisabled = 3201;
+	public const int OpeningNewConnection = 3202;
+	public const int FailedToConnectRedirectedSession = 3203;
+	public const int ClosingSessionToUseRedirectedSession = 3204;
+	public const int SessionAlreadyConnectedToServer = 3205;
+	public const int RequiresServerRedirection = 3206;
+}
diff --git a/src/MySqlConnector/Logging/Log.cs b/src/MySqlConnector/Logging/Log.cs
new file mode 100644
index 000000000..e9b4f88bc
--- /dev/null
+++ b/src/MySqlConnector/Logging/Log.cs
@@ -0,0 +1,460 @@
+using System.Net.Security;
+using System.Security.Authentication;
+using Microsoft.Extensions.Logging;
+using MySqlConnector.Protocol.Serialization;
+
+namespace MySqlConnector.Logging;
+
+internal static partial class Log
+{
+	[LoggerMessage(EventIds.DataSourceCreatedWithPoolWithName, LogLevel.Information, "Data source {DataSourceId} created with pool {PoolId} and name {DataSourceName}")]
+	public static partial void DataSourceCreatedWithPoolWithName(ILogger logger, int dataSourceId, int poolId, string dataSourceName);
+
+	[LoggerMessage(EventIds.DataSourceCreatedWithoutPoolWithName, LogLevel.Information, "Data source {DataSourceId} created with name {DataSourceName} and no pool")]
+	public static partial void DataSourceCreatedWithoutPoolWithName(ILogger logger, int dataSourceId, string dataSourceName);
+
+	[LoggerMessage(EventIds.DataSourceCreatedWithPoolWithoutName, LogLevel.Information, "Data source {DataSourceId} created with pool {PoolId} and no name")]
+	public static partial void DataSourceCreatedWithPoolWithoutName(ILogger logger, int dataSourceId, int poolId);
+
+	[LoggerMessage(EventIds.DataSourceCreatedWithoutPoolWithoutName, LogLevel.Information, "Data source {DataSourceId} created with no pool and no name")]
+	public static partial void DataSourceCreatedWithoutPoolWithoutName(ILogger logger, int dataSourceId);
+
+	[LoggerMessage(EventIds.PeriodicPasswordProviderFailed, LogLevel.Error, "Periodic password provider for data source {DataSourceId} failed: {ExceptionMessage}")]
+	public static partial void PeriodicPasswordProviderFailed(ILogger logger, Exception exception, int dataSourceId, string exceptionMessage);
+
+	[LoggerMessage(EventIds.CreatedNonPooledSession, LogLevel.Debug, "Created new non-pooled session {SessionId}")]
+	public static partial void CreatedNonPooledSession(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.ResettingConnection, LogLevel.Debug, "Session {SessionId} resetting connection")]
+	public static partial void ResettingConnection(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.ReturningToPool, LogLevel.Trace, "Session {SessionId} returning to pool {PoolId}")]
+	public static partial void ReturningToPool(ILogger logger, string sessionId, int poolId);
+
+	[LoggerMessage(EventIds.SendingQuitCommand, LogLevel.Trace, "Session {SessionId} sending QUIT command")]
+	public static partial void SendingQuitCommand(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.ConnectingFailed, LogLevel.Error, "Session {SessionId} connecting failed")]
+	public static partial void ConnectingFailed(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.ServerSentAuthPluginName, LogLevel.Trace, "Session {SessionId} server sent auth plugin name {AuthPluginName}")]
+	public static partial void ServerSentAuthPluginName(ILogger logger, string sessionId, string authPluginName);
+
+	[LoggerMessage(EventIds.UnsupportedAuthenticationMethod, LogLevel.Error, "Session {SessionId} unsupported authentication method {AuthPluginName}")]
+	public static partial void UnsupportedAuthenticationMethod(ILogger logger, string sessionId, string authPluginName);
+
+	[LoggerMessage(EventIds.AutoDetectedAurora57, LogLevel.Debug, "Session {SessionId} auto-detected Aurora 5.7 at '{HostName}'; disabling pipelining")]
+	public static partial void AutoDetectedAurora57(ILogger logger, string sessionId, string hostName);
+
+	[LoggerMessage(EventIds.SessionMadeConnection, LogLevel.Debug, "Session {SessionId} made connection; server version {ServerVersion}; connection ID {ConnectionId}; supports: compression {SupportsCompression}, attributes {SupportsAttributes}, deprecate EOF {SupportsDeprecateEof}, cached metadata {SupportsCachedMetadata}, SSL {SupportsSsl}, session track {SupportsSessionTrack}, pipelining {SupportsPipelining}, query attributes {SupportsQueryAttributes}")]
+	public static partial void SessionMadeConnection(ILogger logger, string sessionId, string serverVersion, int connectionId, bool supportsCompression, bool supportsAttributes, bool supportsDeprecateEof, bool supportsCachedMetadata, bool supportsSsl, bool supportsSessionTrack, bool supportsPipelining, bool supportsQueryAttributes);
+
+	[LoggerMessage(EventIds.ServerDoesNotSupportSsl, LogLevel.Error, "Session {SessionId} requires SSL but server doesn't support it")]
+	public static partial void ServerDoesNotSupportSsl(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.CouldNotConnectToServer, LogLevel.Error, "Session {SessionId} couldn't connect to server")]
+	public static partial void CouldNotConnectToServer(ILogger logger, Exception exception, string sessionId);
+
+	[LoggerMessage(EventIds.SendingPipelinedResetConnectionRequest, LogLevel.Trace, "Session {SessionId} server version {ServerVersion} supports reset connection and pipelining; sending pipelined reset connection request")]
+	public static partial void SendingPipelinedResetConnectionRequest(ILogger logger, string sessionId, string serverVersion);
+
+	[LoggerMessage(EventIds.SendingResetConnectionRequest, LogLevel.Trace, "Session {SessionId} server version {ServerVersion} supports reset connection; sending reset connection request")]
+	public static partial void SendingResetConnectionRequest(ILogger logger, string sessionId, string serverVersion);
+
+	[LoggerMessage(EventIds.SendingChangeUserRequest, LogLevel.Trace, "Session {SessionId} server version {ServerVersion} doesn't support reset connection; sending change user request")]
+	public static partial void SendingChangeUserRequest(ILogger logger, string sessionId, string serverVersion);
+
+	[LoggerMessage(EventIds.SendingChangeUserRequestDueToChangedDatabase, LogLevel.Debug, "Session {SessionId} sending change user request due to changed database {Database}")]
+	public static partial void SendingChangeUserRequestDueToChangedDatabase(ILogger logger, string sessionId, string database);
+
+	[LoggerMessage(EventIds.OptimisticReauthenticationFailed, LogLevel.Trace, "Session {SessionId} optimistic reauthentication failed; logging in again")]
+	public static partial void OptimisticReauthenticationFailed(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.IgnoringFailureInTryResetConnectionAsync, LogLevel.Trace, "Session {SessionId} ignoring {Failure} in TryResetConnectionAsync")]
+	public static partial void IgnoringFailureInTryResetConnectionAsync(ILogger logger, Exception exception, string sessionId, string failure);
+
+	[LoggerMessage(EventIds.SwitchingToAuthenticationMethod, LogLevel.Trace, "Session {SessionId} switching to authentication method {AuthenticationMethod}")]
+	public static partial void SwitchingToAuthenticationMethod(ILogger logger, string sessionId, string authenticationMethod);
+
+	[LoggerMessage(EventIds.NeedsSecureConnection, LogLevel.Error, "Session {SessionId} needs a secure connection to use authentication method {AuthenticationMethod}")]
+	public static partial void NeedsSecureConnection(ILogger logger, string sessionId, string authenticationMethod);
+
+	[LoggerMessage(EventIds.AuthenticationMethodNotSupported, LogLevel.Error, "Session {SessionId} is requesting authentication method {AuthenticationMethod} which is not supported")]
+	public static partial void AuthenticationMethodNotSupported(ILogger logger, string sessionId, string authenticationMethod);
+
+	[LoggerMessage(EventIds.CouldNotLoadServerRsaPublicKey, LogLevel.Error, "Session {SessionId} couldn't load server's RSA public key")]
+	public static partial void CouldNotLoadServerRsaPublicKey(ILogger logger, Exception exception, string sessionId);
+
+	[LoggerMessage(EventIds.CouldNotLoadServerRsaPublicKeyFromFile, LogLevel.Error, "Session {SessionId} couldn't load server's RSA public key from '{PublicKeyFilePath}'")]
+	public static partial void CouldNotLoadServerRsaPublicKeyFromFile(ILogger logger, Exception exception, string sessionId, string publicKeyFilePath);
+
+	[LoggerMessage(EventIds.CouldNotUseAuthenticationMethodForRsa, LogLevel.Error, "Session {SessionId} couldn't use authentication method {AuthenticationMethod} because RSA key wasn't specified or couldn't be retrieved")]
+	public static partial void CouldNotUseAuthenticationMethodForRsa(ILogger logger, string sessionId, string authenticationMethod);
+
+	[LoggerMessage(EventIds.FailedToResolveHostName, LogLevel.Warning, "Session {SessionId} failed to resolve host name {HostName} ({HostNameIndex} of {HostNameCount}): {ExceptionMessage}")]
+	public static partial void FailedToResolveHostName(ILogger logger, Exception exception, string sessionId, string hostName, int hostNameIndex, int hostNameCount, string exceptionMessage);
+
+	[LoggerMessage(EventIds.ConnectingToIpAddress, LogLevel.Trace, "Session {SessionId} connecting to IP address {IpAddress} ({IpAddressIndex} of {IpAddressCount}) for host name {HostName} ({HostNameIndex} of {HostNameCount})")]
+	public static partial void ConnectingToIpAddress(ILogger logger, string sessionId, string ipAddress, int ipAddressIndex, int ipAddressCount, string hostName, int hostNameIndex, int hostNameCount);
+
+	[LoggerMessage(EventIds.ConnectTimeoutExpired, LogLevel.Information, "Session {SessionId} connect timeout expired connecting to IP address {IpAddress} for host name {HostName}")]
+	public static partial void ConnectTimeoutExpired(ILogger logger, Exception? exception, string sessionId, string ipAddress, string hostName);
+
+	[LoggerMessage(EventIds.FailedToConnectToSingleIpAddress, LogLevel.Information, "Session {SessionId} failed to connect to IP address {IpAddress} for host name {HostName}: {ExceptionMessage}")]
+	public static partial void FailedToConnectToSingleIpAddress(ILogger logger, Exception exception, string sessionId, string ipAddress, string hostName, string exceptionMessage);
+
+	[LoggerMessage(EventId = EventIds.FailedToConnectToIpAddress, Message = "Session {SessionId} failed to connect to IP address {IpAddress} ({IpAddressIndex} of {IpAddressCount}) for host name {HostName} ({HostNameIndex} of {HostNameCount}): {ExceptionMessage}")]
+	public static partial void FailedToConnectToIpAddress(ILogger logger, Exception exception, LogLevel logLevel, string sessionId, string ipAddress, int ipAddressIndex, int ipAddressCount, string hostName, int hostNameIndex, int hostNameCount, string exceptionMessage);
+
+	[LoggerMessage(EventIds.ConnectedToIpAddress, LogLevel.Trace, "Session {SessionId} connected to IP address {IpAddress} for host name {HostName} with local port {LocalPort}")]
+	public static partial void ConnectedToIpAddress(ILogger logger, string sessionId, string ipAddress, string hostName, int? localPort);
+
+	[LoggerMessage(EventIds.ConnectingToUnixSocket, LogLevel.Trace, "Session {SessionId} connecting to UNIX socket {SocketPath}")]
+	public static partial void ConnectingToUnixSocket(ILogger logger, string sessionId, string socketPath);
+
+	[LoggerMessage(EventIds.ConnectTimeoutExpiredForUnixSocket, LogLevel.Information, "Session {SessionId} connect timeout expired connecting to UNIX socket {SocketPath}")]
+	public static partial void ConnectTimeoutExpiredForUnixSocket(ILogger logger, string sessionId, string socketPath);
+
+	[LoggerMessage(EventIds.ConnectingToNamedPipe, LogLevel.Trace, "Session {SessionId} connecting to named pipe {PipeName} on server {HostName}")]
+	public static partial void ConnectingToNamedPipe(ILogger logger, string sessionId, string pipeName, string hostName);
+
+	[LoggerMessage(EventIds.ConnectTimeoutExpiredForNamedPipe, LogLevel.Information, "Session {SessionId} connect timeout expired connecting to named pipe {PipeName} on server {HostName}")]
+	public static partial void ConnectTimeoutExpiredForNamedPipe(ILogger logger, Exception exception, string sessionId, string pipeName, string hostName);
+
+	[LoggerMessage(EventIds.InitializingTlsConnection, LogLevel.Trace, "Session {SessionId} initializing TLS connection")]
+	public static partial void InitializingTlsConnection(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.NoCertificatesFound, LogLevel.Error, "Session {SessionId} found no certificates in the certificate store")]
+	public static partial void NoCertificatesFound(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.CertificateNotFoundInStore, LogLevel.Error, "Session {SessionId} certificate with thumbprint {Thumbprint} not found in store")]
+	public static partial void CertificateNotFoundInStore(ILogger logger, string sessionId, string thumbprint);
+
+	[LoggerMessage(EventIds.CouldNotLoadCertificate, LogLevel.Error, "Session {SessionId} couldn't load certificate from {CertificateStoreLocation}")]
+	public static partial void CouldNotLoadCertificate(ILogger logger, Exception exception, string sessionId, MySqlCertificateStoreLocation certificateStoreLocation);
+
+	[LoggerMessage(EventIds.NoPrivateKeyIncludedWithCertificateFile, LogLevel.Error, "Session {SessionId} no private key included with certificate file '{CertificateFile}'")]
+	public static partial void NoPrivateKeyIncludedWithCertificateFile(ILogger logger, string sessionId, string certificateFile);
+
+	[LoggerMessage(EventIds.CouldNotLoadCertificateFromFile, LogLevel.Error, "Session {SessionId} couldn't load certificate from '{CertificateFile}'")]
+	public static partial void CouldNotLoadCertificateFromFile(ILogger logger, Exception exception, string sessionId, string certificateFile);
+
+	[LoggerMessage(EventIds.FailedToObtainClientCertificates, LogLevel.Error, "Session {SessionId} failed to obtain client certificates via ProvideClientCertificatesCallback: {ExceptionMessage}")]
+	public static partial void FailedToObtainClientCertificates(ILogger logger, Exception exception, string sessionId, string exceptionMessage);
+
+	[LoggerMessage(EventIds.LoadingCaCertificatesFromFile, LogLevel.Trace, "Session {SessionId} loading CA certificate(s) from '{CACertificateFile}'")]
+	public static partial void LoadingCaCertificatesFromFile(ILogger logger, string sessionId, string caCertificateFile);
+
+	[LoggerMessage(EventId = EventIds.CouldNotLoadCaCertificateFromFile, Message = "Session {SessionId} couldn't load CA certificate from '{CACertificateFile}'")]
+	public static partial void CouldNotLoadCaCertificateFromFile(ILogger logger, Exception exception, LogLevel logLevel, string sessionId, string caCertificateFile);
+
+	[LoggerMessage(EventIds.LoadingCaCertificate, LogLevel.Trace, "Session {SessionId} loading certificate at index {Index} in the CA certificate file.")]
+	public static partial void LoadingCaCertificate(ILogger logger, string sessionId, int index);
+
+	[LoggerMessage(EventIds.LoadedCaCertificatesFromFile, LogLevel.Trace, "Session {SessionId} loaded {CertificateCount} certificate(s) from '{CACertificateFile}'")]
+	public static partial void LoadedCaCertificatesFromFile(ILogger logger, string sessionId, int certificateCount, string caCertificateFile);
+
+	[LoggerMessage(EventIds.NotUsingRemoteCertificateValidationCallbackDueToSslCa, LogLevel.Warning, "Session {SessionId} not using client-provided RemoteCertificateValidationCallback because SslCA is specified")]
+	public static partial void NotUsingRemoteCertificateValidationCallbackDueToSslCa(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.NotUsingRemoteCertificateValidationCallbackDueToSslMode, LogLevel.Warning, "Session {SessionId} not using client-provided RemoteCertificateValidationCallback because SslMode is {SslMode}")]
+	public static partial void NotUsingRemoteCertificateValidationCallbackDueToSslMode(ILogger logger, string sessionId, MySqlSslMode sslMode);
+
+	[LoggerMessage(EventIds.UsingRemoteCertificateValidationCallback, LogLevel.Debug, "Session {SessionId} using client-provided RemoteCertificateValidationCallback")]
+	public static partial void UsingRemoteCertificateValidationCallback(ILogger logger, string sessionId);
+
+#if NETCOREAPP3_0_OR_GREATER
+	[LoggerMessage(EventIds.ConnectedTlsBasic, LogLevel.Debug, "Session {SessionId} connected TLS using {SslProtocol} and {NegotiatedCipherSuite}")]
+	public static partial void ConnectedTlsBasic(ILogger logger, string sessionId, SslProtocols sslProtocol, TlsCipherSuite negotiatedCipherSuite);
+#endif
+
+	[LoggerMessage(EventIds.ConnectedTlsDetailed, LogLevel.Debug, "Session {SessionId} connected TLS using {SslProtocol}, {CipherAlgorithm}, {HashAlgorithm}, {KeyExchangeAlgorithm}, {KeyExchangeStrength}")]
+	public static partial void ConnectedTlsDetailed(ILogger logger, string sessionId, SslProtocols sslProtocol, CipherAlgorithmType cipherAlgorithm, HashAlgorithmType hashAlgorithm, ExchangeAlgorithmType keyExchangeAlgorithm, int keyExchangeStrength);
+
+	[LoggerMessage(EventIds.CouldNotInitializeTlsConnection, LogLevel.Error, "Session {SessionId} couldn't initialize TLS connection")]
+	public static partial void CouldNotInitializeTlsConnection(ILogger logger, Exception exception, string sessionId);
+
+	[LoggerMessage(EventIds.LoadingClientKeyFromKeyFile, LogLevel.Trace, "Session {SessionId} loading client key from '{ClientKeyFilePath}'")]
+	public static partial void LoadingClientKeyFromKeyFile(ILogger logger, string sessionId, string clientKeyFilePath);
+
+	[LoggerMessage(EventIds.CouldNotLoadClientKeyFromKeyFile, LogLevel.Error, "Session {SessionId} couldn't load client key from '{ClientKeyFilePath}'")]
+	public static partial void CouldNotLoadClientKeyFromKeyFile(ILogger logger, Exception exception, string sessionId, string clientKeyFilePath);
+
+	[LoggerMessage(EventIds.DetectedProxy, LogLevel.Debug, "Session {SessionId} detected proxy; getting CONNECTION_ID(), VERSION() from server")]
+	public static partial void DetectedProxy(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.ChangingConnectionId, LogLevel.Debug, "Session {SessionId} changing connection id from {OldConnectionId} to {ConnectionId} and server version from {OldServerVersion} to {ServerVersion}")]
+	public static partial void ChangingConnectionId(ILogger logger, string sessionId, int oldConnectionId, int connectionId, string oldServerVersion, string serverVersion);
+
+	[LoggerMessage(EventIds.FailedToGetConnectionId, LogLevel.Information, "Session {SessionId} failed to get CONNECTION_ID(), VERSION()")]
+	public static partial void FailedToGetConnectionId(ILogger logger, Exception exception, string sessionId);
+
+	[LoggerMessage(EventIds.ClosingStreamSocket, LogLevel.Debug, "Session {SessionId} closing stream/socket")]
+	public static partial void ClosingStreamSocket(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.CreatingConnectionAttributes, LogLevel.Trace, "Session {SessionId} creating connection attributes")]
+	public static partial void CreatingConnectionAttributes(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.ObtainingPasswordViaProvidePasswordCallback, LogLevel.Trace, "Session {SessionId} obtaining password via ProvidePasswordCallback")]
+	public static partial void ObtainingPasswordViaProvidePasswordCallback(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.FailedToObtainPassword, LogLevel.Error, "Session {SessionId} failed to obtain password via ProvidePasswordCallback: {ExceptionMessage}")]
+	public static partial void FailedToObtainPassword(ILogger logger, Exception exception, string sessionId, string exceptionMessage);
+
+#if NETCOREAPP3_0_OR_GREATER
+	[LoggerMessage(EventIds.ConnectedTlsBasicPreliminary, LogLevel.Debug, "Session {SessionId} provisionally connected TLS with error {SslPolicyErrors} using {SslProtocol} and {NegotiatedCipherSuite}")]
+	public static partial void ConnectedTlsBasicPreliminary(ILogger logger, string sessionId, SslPolicyErrors sslPolicyErrors, SslProtocols sslProtocol, TlsCipherSuite negotiatedCipherSuite);
+#endif
+
+	[LoggerMessage(EventIds.ConnectedTlsDetailedPreliminary, LogLevel.Debug, "Session {SessionId} provisionally connected TLS with error {SslPolicyErrors} using {SslProtocol}, {CipherAlgorithm}, {HashAlgorithm}, {KeyExchangeAlgorithm}, {KeyExchangeStrength}")]
+	public static partial void ConnectedTlsDetailedPreliminary(ILogger logger, string sessionId, SslPolicyErrors sslPolicyErrors, SslProtocols sslProtocol, CipherAlgorithmType cipherAlgorithm, HashAlgorithmType hashAlgorithm, ExchangeAlgorithmType keyExchangeAlgorithm, int keyExchangeStrength);
+
+	[LoggerMessage(EventIds.CertificateErrorUnixSocket, LogLevel.Trace, "Session {SessionId} ignoring remote certificate error {SslPolicyErrors} due to Unix socket connection")]
+	public static partial void CertificateErrorUnixSocket(ILogger logger, string sessionId, SslPolicyErrors sslPolicyErrors);
+
+	[LoggerMessage(EventIds.CertificateErrorNoPassword, LogLevel.Trace, "Session {SessionId} acknowledging remote certificate error {SslPolicyErrors} due to passwordless connection")]
+	public static partial void CertificateErrorNoPassword(ILogger logger, string sessionId, SslPolicyErrors sslPolicyErrors);
+
+	[LoggerMessage(EventIds.CertificateErrorValidThumbprint, LogLevel.Trace, "Session {SessionId} ignoring remote certificate error {SslPolicyErrors} due to valid signature in OK packet")]
+	public static partial void CertificateErrorValidThumbprint(ILogger logger, string sessionId, SslPolicyErrors sslPolicyErrors);
+
+	[LoggerMessage(EventIds.IgnoringCancellationForCommand, LogLevel.Trace, "Ignoring cancellation for closed connection or invalid command {CommandId}")]
+	public static partial void IgnoringCancellationForCommand(ILogger logger, int commandId);
+
+	[LoggerMessage(EventIds.CommandHasBeenCanceled, LogLevel.Debug, "Command {CommandId} for session {SessionId} has been canceled via {CancellationSource}")]
+	public static partial void CommandHasBeenCanceled(ILogger logger, int commandId, string sessionId, string cancellationSource);
+
+	[LoggerMessage(EventIds.IgnoringCancellationForClosedConnection, LogLevel.Information, "Session {SessionId} ignoring cancellation for closed connection")]
+	public static partial void IgnoringCancellationForClosedConnection(ILogger logger, Exception exception, string sessionId);
+
+	[LoggerMessage(EventIds.CancelingCommandFailed, LogLevel.Information, "Session {SessionId} cancelling command {CommandId} failed")]
+	public static partial void CancelingCommandFailed(ILogger logger, Exception exception, string sessionId, int commandId);
+
+	[LoggerMessage(EventIds.WillCancelCommand, LogLevel.Debug, "Session {SessionId} will cancel command {CommandId} ({CancelAttemptCount} attempts); CommandText: {CommandText}")]
+	public static partial void WillCancelCommand(ILogger logger, string sessionId, int commandId, int cancelAttemptCount, string? commandText);
+
+	[LoggerMessage(EventIds.CancelingCommandFromSession, LogLevel.Information, "Session {SessionId} canceling command {CommandId} from session {CancelingSessionId}; CommandText: {CommandText}")]
+	public static partial void CancelingCommandFromSession(ILogger logger, string sessionId, int commandId, string cancelingSessionId, string? commandText);
+
+	[LoggerMessage(EventIds.IgnoringCancellationForInactiveCommand, LogLevel.Debug, "Session {SessionId} active command {ActiveCommandId} is not the command {CommandId} being canceled; ignoring cancellation.")]
+	public static partial void IgnoringCancellationForInactiveCommand(ILogger logger, string sessionId, int activeCommandId, int commandId);
+
+	[LoggerMessage(EventIds.CancelingCommand, LogLevel.Debug, "Session {SessionId} canceling command {CommandId} with text {CommandText}")]
+	public static partial void CancelingCommand(ILogger logger, string sessionId, int commandId, string commandText);
+
+	[LoggerMessage(EventIds.SendingSleepToClearPendingCancellation, LogLevel.Debug, "Session {SessionId} sending 'SLEEP(0)' command to clear pending cancellation")]
+	public static partial void SendingSleepToClearPendingCancellation(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.GettingCachedProcedure, LogLevel.Trace, "Session {SessionId} getting cached procedure named {ProcedureName}")]
+	public static partial void GettingCachedProcedure(ILogger logger, string sessionId, string procedureName);
+
+	[LoggerMessage(EventIds.PoolDoesNotHaveSharedProcedureCache, LogLevel.Information, "Session {SessionId} pool {PoolId} doesn't have a shared procedure cache; procedure will only be cached on this connection")]
+	public static partial void PoolDoesNotHaveSharedProcedureCache(ILogger logger, string sessionId, int? poolId);
+
+	[LoggerMessage(EventIds.CouldNotNormalizeDatabaseAndName, LogLevel.Information, "Session {SessionId} couldn't normalize the name '{ProcedureName}' in database {Database}; not caching procedure")]
+	public static partial void CouldNotNormalizeDatabaseAndName(ILogger logger, string sessionId, string procedureName, string database);
+
+	[LoggerMessage(EventIds.FailedToCacheProcedure, LogLevel.Information, "Session {SessionId} failed to cache procedure {Schema}.{Component}")]
+	public static partial void FailedToCacheProcedure(ILogger logger, string sessionId, string schema, string component);
+
+	[LoggerMessage(EventIds.CachingProcedure, LogLevel.Trace, "Session {SessionId} caching procedure {Schema}.{Component}")]
+	public static partial void CachingProcedure(ILogger logger, string sessionId, string schema, string component);
+
+	[LoggerMessage(EventIds.ProcedureCacheCount, LogLevel.Trace, "Session {SessionId} procedure cache count is {ProcedureCacheCount}")]
+	public static partial void ProcedureCacheCount(ILogger logger, string sessionId, int procedureCacheCount);
+
+	[LoggerMessage(EventIds.DidNotFindCachedProcedure, LogLevel.Information, "Session {SessionId} did not find cached procedure {Schema}.{Component}")]
+	public static partial void DidNotFindCachedProcedure(ILogger logger, string sessionId, string schema, string component);
+
+	[LoggerMessage(EventIds.ReturningCachedProcedure, LogLevel.Trace, "Session {SessionId} returning cached procedure {Schema}.{Component}")]
+	public static partial void ReturningCachedProcedure(ILogger logger, string sessionId, string schema, string component);
+
+	[LoggerMessage(EventIds.FailedToRetrieveProcedureMetadata, LogLevel.Information, "Session {SessionId} failed to retrieve metadata for {Schema}.{Component}; falling back to INFORMATION_SCHEMA: {ExceptionMessage}")]
+	public static partial void FailedToRetrieveProcedureMetadata(ILogger logger, Exception exception, string sessionId, string schema, string component, string exceptionMessage);
+
+	[LoggerMessage(EventIds.ServerDoesNotSupportCachedProcedures, LogLevel.Information, "Session {SessionId} server version {ServerVersion} does not support cached procedures")]
+	public static partial void ServerDoesNotSupportCachedProcedures(ILogger logger, string sessionId, string serverVersion);
+
+	[LoggerMessage(EventIds.ProcedureHasRoutineCount, LogLevel.Trace, "Procedure for {Schema}.{Component} has {RoutineCount} routines and {ParameterCount} parameters")]
+	public static partial void ProcedureHasRoutineCount(ILogger logger, string schema, string component, int routineCount, int parameterCount);
+
+	[LoggerMessage(EventIds.CreatedNewSession, LogLevel.Trace, "Created new session {SessionId}")]
+	public static partial void CreatedNewSession(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.PingingServer, LogLevel.Trace, "Session {SessionId} pinging server")]
+	public static partial void PingingServer(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventId = EventIds.SuccessfullyPingedServer, Message = "Session {SessionId} successfully pinged server")]
+	public static partial void SuccessfullyPingedServer(ILogger logger, LogLevel logLevel, string sessionId);
+
+	[LoggerMessage(EventIds.PingFailed, LogLevel.Trace, "Session {SessionId} ping failed due to {Failure}")]
+	public static partial void PingFailed(ILogger logger, Exception exception, string sessionId, string failure);
+
+	[LoggerMessage(EventIds.SettingStateToFailed, LogLevel.Debug, "Session {SessionId} setting state to Failed")]
+	public static partial void SettingStateToFailed(ILogger logger, Exception exception, string sessionId);
+
+	[LoggerMessage(EventIds.ExpectedToReadMoreBytes, LogLevel.Error, "Session {SessionId} expected to read {ExpectedByteCount} bytes but only read {ReadByteCount}")]
+	public static partial void ExpectedToReadMoreBytes(ILogger logger, string sessionId, int expectedByteCount, int readByteCount);
+
+	[LoggerMessage(EventIds.ErrorPayload, LogLevel.Debug, "Session {SessionId} got error payload: {ErrorCode}, {State}, {Message}")]
+	public static partial void ErrorPayload(ILogger logger, string sessionId, int errorCode, string state, string message);
+
+	[LoggerMessage(EventIds.CommandExecutorExecuteReader, LogLevel.Trace, "Session {SessionId} ExecuteReader {IOBehavior} for {CommandCount} command(s)")]
+	public static partial void CommandExecutorExecuteReader(ILogger logger, string sessionId, IOBehavior ioBehavior, int commandCount);
+
+	[LoggerMessage(EventIds.QueryWasInterrupted, LogLevel.Information, "Session {SessionId} query was interrupted")]
+	public static partial void QueryWasInterrupted(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.PreparingCommandPayload, LogLevel.Trace, "Session {SessionId} preparing command payload for: {CommandText}")]
+	public static partial void PreparingCommandPayload(ILogger logger, string sessionId, string commandText);
+
+	[LoggerMessage(EventIds.PreparingCommandPayloadWithId, LogLevel.Trace, "Session {SessionId} preparing statement payload with ID {StatementId} for: {CommandText}")]
+	public static partial void PreparingCommandPayloadWithId(ILogger logger, string sessionId, int statementId, string commandText);
+
+	[LoggerMessage(EventIds.QueryAttributesNotSupported, LogLevel.Warning, "Session {SessionId} has query attributes but server doesn't support them; CommandText: {CommandText}")]
+	public static partial void QueryAttributesNotSupported(ILogger logger, string sessionId, string commandText);
+
+	[LoggerMessage(EventIds.QueryAttributesNotSupportedWithId, LogLevel.Warning, "Session {SessionId} has attributes for statement {StatementId} but the server does not support them")]
+	public static partial void QueryAttributesNotSupportedWithId(ILogger logger, string sessionId, int statementId);
+
+	[LoggerMessage(EventIds.IgnoringExceptionInDisposeAsync, LogLevel.Warning, "Session {SessionId} ignoring exception in MySqlDataReader.DisposeAsync. Message: {ExceptionMessage}. CommandText: {CommandText}")]
+	public static partial void IgnoringExceptionInDisposeAsync(ILogger logger, Exception exception, string sessionId, string exceptionMessage, string commandText);
+
+	[LoggerMessage(EventIds.StartingBulkCopy, LogLevel.Debug, "Starting bulk copy to {TableName}")]
+	public static partial void StartingBulkCopy(ILogger logger, string tableName);
+
+	[LoggerMessage(EventIds.AddingDefaultColumnMapping, LogLevel.Debug, "Adding default column mapping from {SourceOrdinal} to {DestinationColumn}")]
+	public static partial void AddingDefaultColumnMapping(ILogger logger, int sourceOrdinal, string destinationColumn);
+
+	[LoggerMessage(EventIds.IgnoringColumn, LogLevel.Debug, "Ignoring column with source ordinal {SourceOrdinal}")]
+	public static partial void IgnoringColumn(ILogger logger, int sourceOrdinal);
+
+	[LoggerMessage(EventIds.FinishedBulkCopy, LogLevel.Debug, "Finished bulk copy to {TableName}")]
+	public static partial void FinishedBulkCopy(ILogger logger, string tableName);
+
+	[LoggerMessage(EventIds.BulkCopyFailed, LogLevel.Error, "Bulk copy to {TableName} failed: {RowsCopied} row(s) copied; {RowsInserted} row(s) inserted")]
+	public static partial void BulkCopyFailed(ILogger logger, string tableName, int rowsCopied, int rowsInserted);
+
+	[LoggerMessage(EventIds.ColumnMappingAlreadyHasExpression, LogLevel.Information, "Column mapping for {SourceOrdinal} to {DestinationColumn} already has expression {Expression}")]
+	public static partial void ColumnMappingAlreadyHasExpression(ILogger logger, int sourceOrdinal, string destinationColumn, string expression);
+
+	[LoggerMessage(EventIds.SettingExpressionToMapColumn, LogLevel.Trace, "Setting expression to map column {SourceOrdinal} to {DestinationColumn}: {Expression}")]
+	public static partial void SettingExpressionToMapColumn(ILogger logger, int sourceOrdinal, string destinationColumn, string expression);
+
+	[LoggerMessage(EventIds.StartingTransaction, LogLevel.Debug, "Session {SessionId} starting transaction")]
+	public static partial void StartingTransaction(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.StartedTransaction, LogLevel.Trace, "Session {SessionId} started transaction")]
+	public static partial void StartedTransaction(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.CommittingTransaction, LogLevel.Trace, "Session {SessionId} committing transaction")]
+	public static partial void CommittingTransaction(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.CommittedTransaction, LogLevel.Debug, "Session {SessionId} committed transaction")]
+	public static partial void CommittedTransaction(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.RollingBackTransaction, LogLevel.Trace, "Session {SessionId} rolling back transaction")]
+	public static partial void RollingBackTransaction(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.RolledBackTransaction, LogLevel.Debug, "Session {SessionId} rolled back transaction")]
+	public static partial void RolledBackTransaction(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.WaitingForAvailableSession, LogLevel.Trace, "Pool {PoolId} waiting for an available session")]
+	public static partial void WaitingForAvailableSession(ILogger logger, int poolId);
+
+	[LoggerMessage(EventIds.FailedInReceiveReplyAsync, LogLevel.Debug, "Session {SessionId} failed in ReceiveReplyAsync")]
+	public static partial void FailedInReceiveReplyAsync(ILogger logger, Exception exception, string sessionId);
+
+	[LoggerMessage(EventIds.FailedInSendReplyAsync, LogLevel.Debug, "Session {SessionId} failed in SendReplyAsync")]
+	public static partial void FailedInSendReplyAsync(ILogger logger, Exception exception, string sessionId);
+
+	[LoggerMessage(EventIds.CreatingNewConnectionPool, LogLevel.Information, "Creating new connection pool {PoolId} for {ConnectionString}")]
+	public static partial void CreatingNewConnectionPool(ILogger logger, int poolId, string connectionString);
+
+	[LoggerMessage(EventIds.ScanningForLeakedSessions, LogLevel.Debug, "Pool {PoolId} is empty; scanning for any leaked sessions")]
+	public static partial void ScanningForLeakedSessions(ILogger logger, int poolId);
+
+	[LoggerMessage(EventIds.FoundExistingSession, LogLevel.Trace, "Pool {PoolId} found an existing session; checking it for validity")]
+	public static partial void FoundExistingSession(ILogger logger, int poolId);
+
+	[LoggerMessage(EventIds.DiscardingSessionDueToWrongGeneration, LogLevel.Trace, "Pool {PoolId} discarding session due to wrong generation")]
+	public static partial void DiscardingSessionDueToWrongGeneration(ILogger logger, int poolId);
+
+	[LoggerMessage(EventIds.SessionIsUnusable, LogLevel.Information, "Pool {PoolId} session {SessionId} is unusable; destroying it")]
+	public static partial void SessionIsUnusable(ILogger logger, int poolId, string sessionId);
+
+	[LoggerMessage(EventIds.ReturningPooledSession, LogLevel.Trace, "Pool {PoolId} returning pooled session {SessionId} to caller; {LeasedSessionCount} leased session(s)")]
+	public static partial void ReturningPooledSession(ILogger logger, int poolId, string sessionId, int leasedSessionCount);
+
+	[LoggerMessage(EventIds.ReturningNewSession, LogLevel.Trace, "Pool {PoolId} returning new session {SessionId} to caller; {LeasedSessionCount} leased session(s)")]
+	public static partial void ReturningNewSession(ILogger logger, int poolId, string sessionId, int leasedSessionCount);
+
+	[LoggerMessage(EventIds.DisposingCreatedSessionDueToException, LogLevel.Debug, "Pool {PoolId} disposing created session {SessionId} due to exception: {ExceptionMessage}")]
+	public static partial void DisposingCreatedSessionDueToException(ILogger logger, Exception exception, int poolId, string sessionId, string exceptionMessage);
+
+	[LoggerMessage(EventIds.UnexpectedErrorInGetSessionAsync, LogLevel.Warning, "Pool {PoolId} unexpected error in GetSessionAsync: {ExceptionMessage}")]
+	public static partial void UnexpectedErrorInGetSessionAsync(ILogger logger, Exception exception, int poolId, string exceptionMessage);
+
+	[LoggerMessage(EventIds.ReceivingSessionBack, LogLevel.Trace, "Pool {PoolId} receiving session {SessionId} back")]
+	public static partial void ReceivingSessionBack(ILogger logger, int poolId, string sessionId);
+
+	[LoggerMessage(EventIds.ReceivedInvalidSession, LogLevel.Information, "Pool {PoolId} received invalid session {SessionId}; destroying it")]
+	public static partial void ReceivedInvalidSession(ILogger logger, int poolId, string sessionId);
+
+	[LoggerMessage(EventIds.ReceivedExpiredSession, LogLevel.Debug, "Pool {PoolId} received expired session {SessionId}; destroying it")]
+	public static partial void ReceivedExpiredSession(ILogger logger, int poolId, string sessionId);
+
+	[LoggerMessage(EventIds.ClearingConnectionPool, LogLevel.Information, "Pool {PoolId} clearing connection pool")]
+	public static partial void ClearingConnectionPool(ILogger logger, int poolId);
+
+	[LoggerMessage(EventIds.DisposingConnectionPool, LogLevel.Debug, "Pool {PoolId} disposing connection pool")]
+	public static partial void DisposingConnectionPool(ILogger logger, int poolId);
+
+	[LoggerMessage(EventIds.RecoveredNoSessions, LogLevel.Trace, "Pool {PoolId} recovered no sessions")]
+	public static partial void RecoveredNoSessions(ILogger logger, int poolId);
+
+	[LoggerMessage(EventIds.RecoveredSessionCount, LogLevel.Warning, "Pool {PoolId} recovered {SessionCount} sessions")]
+	public static partial void RecoveredSessionCount(ILogger logger, int poolId, int sessionCount);
+
+	[LoggerMessage(EventIds.FoundSessionToCleanUp, LogLevel.Debug, "Pool {PoolId} found session {SessionId} to clean up")]
+	public static partial void FoundSessionToCleanUp(ILogger logger, int poolId, string sessionId);
+
+	[LoggerMessage(EventIds.HasServerRedirectionHeader, LogLevel.Trace, "Session {SessionId} has server redirection header {Header}")]
+	public static partial void HasServerRedirectionHeader(ILogger logger, string sessionId, string header);
+
+	[LoggerMessage(EventIds.ServerRedirectionIsDisabled, LogLevel.Trace, "Session {SessionId} server redirection is disabled; ignoring redirection")]
+	public static partial void ServerRedirectionIsDisabled(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.OpeningNewConnection, LogLevel.Debug, "Session {SessionId} opening new connection to {Host}:{Port} as {User}")]
+	public static partial void OpeningNewConnection(ILogger logger, string sessionId, string host, int port, string user);
+
+	[LoggerMessage(EventIds.FailedToConnectRedirectedSession, LogLevel.Information, "Session {SessionId} failed to connect redirected session {RedirectedSessionId}")]
+	public static partial void FailedToConnectRedirectedSession(ILogger logger, Exception ex, string sessionId, string redirectedSessionId);
+
+	[LoggerMessage(EventIds.ClosingSessionToUseRedirectedSession, LogLevel.Trace, "Closing session {SessionId} to use redirected session {RedirectedSessionId} instead")]
+	public static partial void ClosingSessionToUseRedirectedSession(ILogger logger, string sessionId, string redirectedSessionId);
+
+	[LoggerMessage(EventIds.SessionAlreadyConnectedToServer, LogLevel.Trace, "Session {SessionId} is already connected to this server; ignoring redirection")]
+	public static partial void SessionAlreadyConnectedToServer(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.RequiresServerRedirection, LogLevel.Error, "Session {SessionId} requires server redirection but server doesn't support it")]
+	public static partial void RequiresServerRedirection(ILogger logger, string sessionId);
+
+	[LoggerMessage(EventIds.CreatedPoolWillNotBeUsed, LogLevel.Debug, "Pool {PoolId} was created but will not be used (due to race)")]
+	public static partial void CreatedPoolWillNotBeUsed(ILogger logger, int poolId);
+
+	[LoggerMessage(EventIds.ReapingConnectionPool, LogLevel.Trace, "Pool {PoolId} reaping connection pool")]
+	public static partial void ReapingConnectionPool(ILogger logger, int poolId);
+
+	[LoggerMessage(EventIds.CheckingForDnsChanges, LogLevel.Trace, "Pool {PoolId} checking for DNS changes")]
+	public static partial void CheckingForDnsChanges(ILogger logger, int poolId);
+
+	[LoggerMessage(EventIds.DetectedDnsChange, LogLevel.Debug, "Pool {PoolId} detected DNS change for '{HostName}': {OldAddresses} to {NewAddresses}")]
+	public static partial void DetectedDnsChange(ILogger logger, int poolId, string hostName, string oldAddresses, string newAddresses);
+
+	[LoggerMessage(EventIds.DnsCheckFailed, LogLevel.Debug, "Pool {PoolId} DNS check failed; ignoring '{HostName}': {ExceptionMessage}")]
+	public static partial void DnsCheckFailed(ILogger logger, Exception exception, int poolId, string hostName, string exceptionMessage);
+
+	[LoggerMessage(EventIds.ClearingPoolDueToDnsChanges, LogLevel.Information, "Pool {PoolId} clearing pool due to DNS changes")]
+	public static partial void ClearingPoolDueToDnsChanges(ILogger logger, int poolId);
+}
diff --git a/src/MySqlConnector/Logging/LoggerExtensions.cs b/src/MySqlConnector/Logging/LoggerExtensions.cs
deleted file mode 100644
index 79373429d..000000000
--- a/src/MySqlConnector/Logging/LoggerExtensions.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-namespace MySqlConnector.Logging;
-
-internal static class LoggerExtensions
-{
-	public static bool IsTraceEnabled(this IMySqlConnectorLogger log) => log.IsEnabled(MySqlConnectorLogLevel.Trace);
-	public static bool IsDebugEnabled(this IMySqlConnectorLogger log) => log.IsEnabled(MySqlConnectorLogLevel.Debug);
-	public static bool IsInfoEnabled(this IMySqlConnectorLogger log) => log.IsEnabled(MySqlConnectorLogLevel.Info);
-	public static bool IsWarnEnabled(this IMySqlConnectorLogger log) => log.IsEnabled(MySqlConnectorLogLevel.Warn);
-	public static bool IsErrorEnabled(this IMySqlConnectorLogger log) => log.IsEnabled(MySqlConnectorLogLevel.Error);
-	public static bool IsFatalEnabled(this IMySqlConnectorLogger log) => log.IsEnabled(MySqlConnectorLogLevel.Fatal);
-
-	public static void Trace(this IMySqlConnectorLogger log, string message, params object?[] args) => log.Log(MySqlConnectorLogLevel.Trace, message, args, null);
-	public static void Debug(this IMySqlConnectorLogger log, string message, params object?[] args) => log.Log(MySqlConnectorLogLevel.Debug, message, args, null);
-	public static void Info(this IMySqlConnectorLogger log, string message, params object?[] args) => log.Log(MySqlConnectorLogLevel.Info, message, args, null);
-	public static void Warn(this IMySqlConnectorLogger log, string message, params object?[] args) => log.Log(MySqlConnectorLogLevel.Warn, message, args, null);
-	public static void Error(this IMySqlConnectorLogger log, string message, params object?[] args) => log.Log(MySqlConnectorLogLevel.Error, message, args, null);
-	public static void Fatal(this IMySqlConnectorLogger log, string message, params object?[] args) => log.Log(MySqlConnectorLogLevel.Fatal, message, args, null);
-
-	public static void Trace(this IMySqlConnectorLogger log, Exception exception, string message, params object?[] args) => log.Log(MySqlConnectorLogLevel.Trace, message, args, exception);
-	public static void Debug(this IMySqlConnectorLogger log, Exception exception, string message, params object?[] args) => log.Log(MySqlConnectorLogLevel.Debug, message, args, exception);
-	public static void Info(this IMySqlConnectorLogger log, Exception exception, string message, params object?[] args) => log.Log(MySqlConnectorLogLevel.Info, message, args, exception);
-	public static void Warn(this IMySqlConnectorLogger log, Exception exception, string message, params object?[] args) => log.Log(MySqlConnectorLogLevel.Warn, message, args, exception);
-	public static void Error(this IMySqlConnectorLogger log, Exception exception, string message, params object?[] args) => log.Log(MySqlConnectorLogLevel.Error, message, args, exception);
-	public static void Fatal(this IMySqlConnectorLogger log, Exception exception, string message, params object?[] args) => log.Log(MySqlConnectorLogLevel.Fatal, message, args, exception);
-}
diff --git a/src/MySqlConnector/Logging/MySqlConnectorLogManager.cs b/src/MySqlConnector/Logging/MySqlConnectorLogManager.cs
index 2e09bf963..6f02b4f5e 100644
--- a/src/MySqlConnector/Logging/MySqlConnectorLogManager.cs
+++ b/src/MySqlConnector/Logging/MySqlConnectorLogManager.cs
@@ -1,3 +1,5 @@
+using Microsoft.Extensions.Logging;
+
 namespace MySqlConnector.Logging;
 
 /// <summary>
@@ -10,24 +12,53 @@ public static class MySqlConnectorLogManager
 	/// be set once, and must be set before any other library methods are used.
 	/// </summary>
 #pragma warning disable CA1044 // Properties should not be write only
+	[Obsolete("Use UseLoggerFactory or AddMySqlDataSource instead. See https://mysqlconnector.net/diagnostics/logging/.")]
 	public static IMySqlConnectorLoggerProvider Provider
 	{
-		internal get
+		set
 		{
-			s_providerRetrieved = true;
-			return s_provider;
+			MySqlConnectorLoggingConfiguration.GlobalConfiguration = new(new MySqlConnectorLoggerFactory(value));
 		}
-		set
+	}
+
+	// A helper class that adapts ILoggerFactory to the old-style IMySqlConnectorLoggerProvider interface.
+	private sealed class MySqlConnectorLoggerFactory(IMySqlConnectorLoggerProvider loggerProvider) : ILoggerFactory
+	{
+		public void AddProvider(ILoggerProvider provider) => throw new NotSupportedException();
+
+		public ILogger CreateLogger(string categoryName)
 		{
-			if (s_providerRetrieved)
-				throw new InvalidOperationException("The logging provider must be set before any MySqlConnector methods are called.");
+			// assume all logger names start with "MySqlConnector." but the old API didn't expect that prefix
+			return new MySqlConnectorLogger(loggerProvider.CreateLogger(categoryName[15..]));
+		}
 
-			s_provider = value;
+		public void Dispose()
+		{
 		}
 	}
 
-	internal static IMySqlConnectorLogger CreateLogger(string name) => Provider.CreateLogger(name);
+	// A helper class that adapts ILogger to the old-style IMySqlConnectorLogger interface.
+	private sealed class MySqlConnectorLogger(IMySqlConnectorLogger logger) : ILogger
+	{
+		public IDisposable BeginScope<TState>(TState state)
+			where TState : notnull
+			=> throw new NotSupportedException();
+
+		public bool IsEnabled(LogLevel logLevel) => logger.IsEnabled(ConvertLogLevel(logLevel));
+
+		public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) =>
+			logger.Log(ConvertLogLevel(logLevel), formatter(state, exception), exception: exception);
 
-	private static IMySqlConnectorLoggerProvider s_provider = new NoOpLoggerProvider();
-	private static bool s_providerRetrieved;
+		private static MySqlConnectorLogLevel ConvertLogLevel(LogLevel logLevel) =>
+			logLevel switch
+			{
+				LogLevel.Trace => MySqlConnectorLogLevel.Trace,
+				LogLevel.Debug => MySqlConnectorLogLevel.Debug,
+				LogLevel.Information => MySqlConnectorLogLevel.Info,
+				LogLevel.Warning => MySqlConnectorLogLevel.Warn,
+				LogLevel.Error => MySqlConnectorLogLevel.Error,
+				LogLevel.Critical => MySqlConnectorLogLevel.Fatal,
+				_ => MySqlConnectorLogLevel.Info,
+			};
+	}
 }
diff --git a/src/MySqlConnector/Logging/MySqlConnectorLoggingConfiguration.cs b/src/MySqlConnector/Logging/MySqlConnectorLoggingConfiguration.cs
new file mode 100644
index 000000000..211980ec6
--- /dev/null
+++ b/src/MySqlConnector/Logging/MySqlConnectorLoggingConfiguration.cs
@@ -0,0 +1,17 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace MySqlConnector.Logging;
+
+internal sealed class MySqlConnectorLoggingConfiguration(ILoggerFactory loggerFactory)
+{
+	public ILogger DataSourceLogger { get; } = loggerFactory.CreateLogger("MySqlConnector.MySqlDataSource");
+	public ILogger ConnectionLogger { get; } = loggerFactory.CreateLogger("MySqlConnector.MySqlConnection");
+	public ILogger CommandLogger { get; } = loggerFactory.CreateLogger("MySqlConnector.MySqlCommand");
+	public ILogger PoolLogger { get; } = loggerFactory.CreateLogger("MySqlConnector.ConnectionPool");
+	public ILogger BulkCopyLogger { get; } = loggerFactory.CreateLogger("MySqlConnector.MySqlBulkCopy");
+	public ILogger TransactionLogger { get; } = loggerFactory.CreateLogger("MySqlConnector.Transaction");
+
+	public static MySqlConnectorLoggingConfiguration NullConfiguration { get; } = new MySqlConnectorLoggingConfiguration(NullLoggerFactory.Instance);
+	public static MySqlConnectorLoggingConfiguration GlobalConfiguration { get; set; } = NullConfiguration;
+}
diff --git a/src/MySqlConnector/MySqlAttribute.cs b/src/MySqlConnector/MySqlAttribute.cs
index 613cd9cf9..5ff72b879 100644
--- a/src/MySqlConnector/MySqlAttribute.cs
+++ b/src/MySqlConnector/MySqlAttribute.cs
@@ -5,40 +5,31 @@ namespace MySqlConnector
 	/// </summary>
 	/// <remarks>See <a href="https://dev.mysql.com/doc/refman/8.0/en/query-attributes.html">Query Attributes</a> for information on using query attributes.</remarks>
 #pragma warning disable CA1711 // Identifiers should not have incorrect suffix
-	public sealed class MySqlAttribute : ICloneable
+	public sealed class MySqlAttribute(string attributeName, object? value) : ICloneable
 #pragma warning restore CA1711 // Identifiers should not have incorrect suffix
 	{
 		/// <summary>
 		/// Initializes a new <see cref="MySqlAttribute"/>.
 		/// </summary>
 		public MySqlAttribute()
+			: this("", null)
 		{
-			AttributeName = "";
-		}
-
-		/// <summary>
-		/// Initializes a new <see cref="MySqlAttribute"/> with the specified attribute name and value.
-		/// </summary>
-		public MySqlAttribute(string attributeName, object? value)
-		{
-			AttributeName = attributeName ?? "";
-			Value = value;
 		}
 
 		/// <summary>
 		/// Gets or sets the attribute name.
 		/// </summary>
-		public string AttributeName { get; set; }
+		public string AttributeName { get; set; } = attributeName ?? "";
 
 		/// <summary>
 		/// Gets or sets the attribute value.
 		/// </summary>
-		public object? Value { get; set; }
+		public object? Value { get; set; } = value;
 
 		/// <summary>
 		/// Returns a new <see cref="MySqlAttribute"/> with the same property values as this instance.
 		/// </summary>
-		public MySqlAttribute Clone() => new MySqlAttribute(AttributeName, Value);
+		public MySqlAttribute Clone() => new(AttributeName, Value);
 
 		object ICloneable.Clone() => Clone();
 
diff --git a/src/MySqlConnector/MySqlAttributeCollection.cs b/src/MySqlConnector/MySqlAttributeCollection.cs
index ed44a8907..cd7c7c273 100644
--- a/src/MySqlConnector/MySqlAttributeCollection.cs
+++ b/src/MySqlConnector/MySqlAttributeCollection.cs
@@ -19,8 +19,18 @@ public sealed class MySqlAttributeCollection : IEnumerable<MySqlAttribute>
 		/// <remarks>The attribute name must not be empty, and must not already exist in the collection.</remarks>
 		public void Add(MySqlAttribute attribute)
 		{
-			if (string.IsNullOrEmpty((attribute ?? throw new ArgumentNullException(nameof(attribute))).AttributeName))
+#if NET6_0_OR_GREATER
+			ArgumentNullException.ThrowIfNull(attribute);
+#else
+			if (attribute is null)
+				throw new ArgumentNullException(nameof(attribute));
+#endif
+#if NET8_0_OR_GREATER
+			ArgumentException.ThrowIfNullOrEmpty(attribute.AttributeName);
+#else
+			if (string.IsNullOrEmpty(attribute.AttributeName))
 				throw new ArgumentException("Attribute name must not be empty", nameof(attribute));
+#endif
 			foreach (var existingAttribute in m_attributes)
 			{
 				if (existingAttribute.AttributeName == attribute.AttributeName)
@@ -36,8 +46,12 @@ public void Add(MySqlAttribute attribute)
 		/// <param name="value">The attribute value.</param>
 		public void SetAttribute(string attributeName, object? value)
 		{
+#if NET8_0_OR_GREATER
+			ArgumentException.ThrowIfNullOrEmpty(attributeName);
+#else
 			if (string.IsNullOrEmpty(attributeName))
 				throw new ArgumentException("Attribute name must not be empty", nameof(attributeName));
+#endif
 			for (var i = 0; i < m_attributes.Count; i++)
 			{
 				if (m_attributes[i].AttributeName == attributeName)
@@ -78,7 +92,7 @@ public void SetAttribute(string attributeName, object? value)
 		/// </summary>
 		IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
 
-		internal MySqlAttributeCollection() => m_attributes = new();
+		internal MySqlAttributeCollection() => m_attributes = [];
 
 		private readonly List<MySqlAttribute> m_attributes;
 	}
diff --git a/src/MySqlConnector/MySqlBatch.cs b/src/MySqlConnector/MySqlBatch.cs
index a762e1bad..27145cd03 100644
--- a/src/MySqlConnector/MySqlBatch.cs
+++ b/src/MySqlConnector/MySqlBatch.cs
@@ -75,7 +75,7 @@ public MySqlBatch(MySqlConnection? connection = null, MySqlTransaction? transact
 	{
 		Connection = connection;
 		Transaction = transaction;
-		BatchCommands = new();
+		BatchCommands = [];
 		m_commandId = ICancellableCommandExtensions.GetNextId();
 	}
 
@@ -130,11 +130,14 @@ public async Task<MySqlDataReader> ExecuteReaderAsync(CancellationToken cancella
 #if NET6_0_OR_GREATER
 	protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior)
 #else
+	[SuppressMessage("Performance", "CA1859:Use concrete types when possible for improved performance", Justification = "Matches .NET 6.0 override")]
 	private DbDataReader ExecuteDbDataReader(CommandBehavior behavior)
 #endif
 	{
-		((ICancellableCommand) this).ResetCommandTimeout();
-		return ExecuteReaderAsync(behavior, IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
+		this.ResetCommandTimeout();
+#pragma warning disable CA2012 // OK to read .Result because the ValueTask is completed
+		return ExecuteReaderAsync(behavior, IOBehavior.Synchronous, CancellationToken.None).Result;
+#pragma warning restore CA2012
 	}
 
 #if NET6_0_OR_GREATER
@@ -143,24 +146,23 @@ protected override async Task<DbDataReader> ExecuteDbDataReaderAsync(CommandBeha
 	private async Task<DbDataReader> ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken)
 #endif
 	{
-		((ICancellableCommand) this).ResetCommandTimeout();
+		this.ResetCommandTimeout();
 		using var registration = ((ICancellableCommand) this).RegisterCancel(cancellationToken);
 		return await ExecuteReaderAsync(behavior, AsyncIOBehavior, cancellationToken).ConfigureAwait(false);
 	}
 
-	private Task<MySqlDataReader> ExecuteReaderAsync(CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken)
+	private ValueTask<MySqlDataReader> ExecuteReaderAsync(CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
 		if (!IsValid(out var exception))
-		 	return Task.FromException<MySqlDataReader>(exception);
+			return ValueTaskExtensions.FromException<MySqlDataReader>(exception);
 
 		CurrentCommandBehavior = behavior;
 		foreach (MySqlBatchCommand batchCommand in BatchCommands)
 			batchCommand.Batch = this;
 
-		var payloadCreator = Connection!.Session.SupportsComMulti ? BatchedCommandPayloadCreator.Instance :
-			IsPrepared ? SingleCommandPayloadCreator.Instance :
+		var payloadCreator = IsPrepared ? SingleCommandPayloadCreator.Instance :
 			ConcatenatedCommandPayloadCreator.Instance;
-		return CommandExecutor.ExecuteReaderAsync(BatchCommands!.Commands, payloadCreator, behavior, default, ioBehavior, cancellationToken);
+		return CommandExecutor.ExecuteReaderAsync(new(BatchCommands!.Commands), payloadCreator, behavior, default, ioBehavior, cancellationToken);
 	}
 
 #if NET6_0_OR_GREATER
@@ -191,11 +193,19 @@ public Task<int> ExecuteNonQueryAsync(CancellationToken cancellationToken = defa
 #endif
 		ExecuteScalarAsync(AsyncIOBehavior, cancellationToken);
 
+	public
 #if NET6_0_OR_GREATER
-	public override int Timeout { get; set; }
-#else
-	public int Timeout { get; set; }
+		override
 #endif
+		int Timeout
+	{
+		get => m_timeout;
+		set
+		{
+			m_timeout = value;
+			((ICancellableCommand) this).EffectiveCommandTimeout = null;
+		}
+	}
 
 #if NET6_0_OR_GREATER
 	public override void Prepare()
@@ -247,12 +257,13 @@ public void Dispose()
 
 	int ICancellableCommand.CommandId => m_commandId;
 	int ICancellableCommand.CommandTimeout => Timeout;
+	int? ICancellableCommand.EffectiveCommandTimeout { get; set; }
 	int ICancellableCommand.CancelAttemptCount { get; set; }
 
-	IDisposable? ICancellableCommand.RegisterCancel(CancellationToken cancellationToken)
+	CancellationTokenRegistration ICancellableCommand.RegisterCancel(CancellationToken cancellationToken)
 	{
 		if (!cancellationToken.CanBeCanceled)
-			return null;
+			return default;
 
 		m_cancelAction ??= Cancel;
 		return cancellationToken.Register(m_cancelAction);
@@ -282,7 +293,7 @@ private void CancelCommandForTimeout()
 
 	private async Task<int> ExecuteNonQueryAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
-		((ICancellableCommand) this).ResetCommandTimeout();
+		this.ResetCommandTimeout();
 		using var registration = ((ICancellableCommand) this).RegisterCancel(cancellationToken);
 		using var reader = await ExecuteReaderAsync(CommandBehavior.Default, ioBehavior, cancellationToken).ConfigureAwait(false);
 		do
@@ -296,7 +307,7 @@ private async Task<int> ExecuteNonQueryAsync(IOBehavior ioBehavior, Cancellation
 
 	private async Task<object?> ExecuteScalarAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
-		((ICancellableCommand) this).ResetCommandTimeout();
+		this.ResetCommandTimeout();
 		using var registration = ((ICancellableCommand) this).RegisterCancel(cancellationToken);
 		var hasSetResult = false;
 		object? result = null;
@@ -316,15 +327,14 @@ private async Task<int> ExecuteNonQueryAsync(IOBehavior ioBehavior, Cancellation
 
 	private bool IsValid([NotNullWhen(false)] out Exception? exception)
 	{
-		exception = null;
 		if (m_isDisposed)
 			exception = new ObjectDisposedException(GetType().Name);
 		else if (Connection is null)
 			exception = new InvalidOperationException("Connection property must be non-null.");
-		else if (Connection.State != ConnectionState.Open && Connection.State != ConnectionState.Connecting)
+		else if (Connection.State is not ConnectionState.Open and not ConnectionState.Connecting)
 			exception = new InvalidOperationException($"Connection must be Open; current state is {Connection.State}");
 		else if (!Connection.IgnoreCommandTransaction && Transaction != Connection.CurrentTransaction)
-			exception = new InvalidOperationException("The transaction associated with this batch is not the connection's active transaction; see https://fl.vu/mysql-trans");
+			exception = new InvalidOperationException("The transaction associated with this batch is not the connection's active transaction; see https://mysqlconnector.net/trans");
 		else if (BatchCommands.Count == 0)
 			exception = new InvalidOperationException("BatchCommands must contain a command");
 		else
@@ -351,7 +361,7 @@ private bool NeedsPrepare(out Exception? exception)
 		return exception is null && !Connection!.IgnorePrepare;
 	}
 
-	private Exception? GetExceptionForInvalidCommands()
+	private InvalidOperationException? GetExceptionForInvalidCommands()
 	{
 		foreach (var command in BatchCommands)
 		{
@@ -402,6 +412,7 @@ private bool IsPrepared
 
 	private readonly int m_commandId;
 	private bool m_isDisposed;
+	private int m_timeout;
 	private Action? m_cancelAction;
 	private Action? m_cancelForCommandTimeoutAction;
 	private uint m_cancelTimerId;
diff --git a/src/MySqlConnector/MySqlBatchCommand.cs b/src/MySqlConnector/MySqlBatchCommand.cs
index b0aebc84c..3a08107ee 100644
--- a/src/MySqlConnector/MySqlBatchCommand.cs
+++ b/src/MySqlConnector/MySqlBatchCommand.cs
@@ -1,3 +1,4 @@
+using Microsoft.Extensions.Logging;
 using MySqlConnector.Core;
 
 namespace MySqlConnector;
@@ -45,7 +46,21 @@ public MySqlBatchCommand(string? commandText)
 #else
 	public MySqlParameterCollection Parameters =>
 #endif
-		m_parameterCollection ??= new();
+		m_parameterCollection ??= [];
+
+#pragma warning disable CA1822 // Mark members as static
+	public
+#if NET8_0_OR_GREATER
+		override
+#endif
+		DbParameter CreateParameter() => new MySqlParameter();
+
+	public
+#if NET8_0_OR_GREATER
+		override
+#endif
+		bool CanCreateParameter => true;
+#pragma warning restore CA1822 // Mark members as static
 
 #if NET6_0_OR_GREATER
 	protected override DbParameterCollection DbParameterCollection => Parameters;
@@ -72,6 +87,7 @@ public MySqlBatchCommand(string? commandText)
 	MySqlParameter? IMySqlCommand.ReturnParameter { get; set; }
 
 	ICancellableCommand IMySqlCommand.CancellableCommand => Batch!;
+	ILogger IMySqlCommand.Logger => Batch!.Connection!.LoggingConfiguration.CommandLogger;
 
 	internal MySqlBatch? Batch { get; set; }
 
diff --git a/src/MySqlConnector/MySqlBatchCommandCollection.cs b/src/MySqlConnector/MySqlBatchCommandCollection.cs
index 018e59678..45382f90b 100644
--- a/src/MySqlConnector/MySqlBatchCommandCollection.cs
+++ b/src/MySqlConnector/MySqlBatchCommandCollection.cs
@@ -9,7 +9,7 @@ public sealed class MySqlBatchCommandCollection
 	: IList<MySqlBatchCommand>, ICollection<MySqlBatchCommand>, IEnumerable<MySqlBatchCommand>, IEnumerable
 #endif
 {
-	internal MySqlBatchCommandCollection() => m_commands = new();
+	internal MySqlBatchCommandCollection() => m_commands = [];
 
 #if NET6_0_OR_GREATER
 	public new MySqlBatchCommand this[int index] { get => (MySqlBatchCommand) base[index]; set => base[index] = value; }
@@ -34,18 +34,18 @@ public override IEnumerator<DbBatchCommand> GetEnumerator()
 	public MySqlBatchCommand this[int index] { get => m_commands[index]; set => m_commands[index] = value; }
 	public int Count => m_commands.Count;
 	public bool IsReadOnly => false;
-	public void Add(MySqlBatchCommand item) => m_commands.Add((MySqlBatchCommand) item);
+	public void Add(MySqlBatchCommand item) => m_commands.Add(item);
 	public void Clear() => m_commands.Clear();
-	public bool Contains(MySqlBatchCommand item) => m_commands.Contains((MySqlBatchCommand) item);
+	public bool Contains(MySqlBatchCommand item) => m_commands.Contains(item);
 	public void CopyTo(MySqlBatchCommand[] array, int arrayIndex) => throw new NotImplementedException();
 	public IEnumerator<MySqlBatchCommand> GetEnumerator()
 	{
 		foreach (var command in m_commands)
 			yield return command;
 	}
-	public int IndexOf(MySqlBatchCommand item) => m_commands.IndexOf((MySqlBatchCommand) item);
-	public void Insert(int index, MySqlBatchCommand item) => m_commands.Insert(index, (MySqlBatchCommand) item);
-	public bool Remove(MySqlBatchCommand item) => m_commands.Remove((MySqlBatchCommand) item);
+	public int IndexOf(MySqlBatchCommand item) => m_commands.IndexOf(item);
+	public void Insert(int index, MySqlBatchCommand item) => m_commands.Insert(index, item);
+	public bool Remove(MySqlBatchCommand item) => m_commands.Remove(item);
 	public void RemoveAt(int index) => m_commands.RemoveAt(index);
 	IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
 #endif
diff --git a/src/MySqlConnector/MySqlBulkCopy.cs b/src/MySqlConnector/MySqlBulkCopy.cs
index 779dd2233..b029cf2aa 100644
--- a/src/MySqlConnector/MySqlBulkCopy.cs
+++ b/src/MySqlConnector/MySqlBulkCopy.cs
@@ -3,6 +3,7 @@
 using System.Globalization;
 using System.Numerics;
 using System.Text;
+using Microsoft.Extensions.Logging;
 using MySqlConnector.Core;
 using MySqlConnector.Logging;
 using MySqlConnector.Protocol;
@@ -51,9 +52,16 @@ public sealed class MySqlBulkCopy
 	/// <param name="transaction">(Optional) The <see cref="MySqlTransaction"/> to use.</param>
 	public MySqlBulkCopy(MySqlConnection connection, MySqlTransaction? transaction = null)
 	{
-		m_connection = connection ?? throw new ArgumentNullException(nameof(connection));
+#if NET6_0_OR_GREATER
+		ArgumentNullException.ThrowIfNull(connection);
+#else
+		if (connection is null)
+			throw new ArgumentNullException(nameof(connection));
+#endif
+		m_connection = connection;
 		m_transaction = transaction;
-		ColumnMappings = new();
+		m_logger = m_connection.LoggingConfiguration.BulkCopyLogger;
+		ColumnMappings = [];
 	}
 
 	/// <summary>
@@ -109,13 +117,18 @@ public MySqlBulkCopy(MySqlConnection connection, MySqlTransaction? transaction =
 	/// <returns>A <see cref="MySqlBulkCopyResult"/> with the result of the bulk copy operation.</returns>
 	public MySqlBulkCopyResult WriteToServer(DataTable dataTable)
 	{
-		m_valuesEnumerator = DataRowsValuesEnumerator.Create(dataTable ?? throw new ArgumentNullException(nameof(dataTable)));
+#if NET6_0_OR_GREATER
+		ArgumentNullException.ThrowIfNull(dataTable);
+#else
+		if (dataTable is null)
+			throw new ArgumentNullException(nameof(dataTable));
+#endif
+		m_valuesEnumerator = DataRowsValuesEnumerator.Create(dataTable);
 #pragma warning disable CA2012 // Safe because method completes synchronously
 		return WriteToServerAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
 #pragma warning restore CA2012
 	}
 
-#if NETCOREAPP || NETSTANDARD2_0_OR_GREATER
 	/// <summary>
 	/// Asynchronously copies all rows in the supplied <see cref="DataTable"/> to the destination table specified by the
 	/// <see cref="DestinationTableName"/> property of the <see cref="MySqlBulkCopy"/> object.
@@ -125,23 +138,15 @@ public MySqlBulkCopyResult WriteToServer(DataTable dataTable)
 	/// <returns>A <see cref="MySqlBulkCopyResult"/> with the result of the bulk copy operation.</returns>
 	public async ValueTask<MySqlBulkCopyResult> WriteToServerAsync(DataTable dataTable, CancellationToken cancellationToken = default)
 	{
-		m_valuesEnumerator = DataRowsValuesEnumerator.Create(dataTable ?? throw new ArgumentNullException(nameof(dataTable)));
-		return await WriteToServerAsync(IOBehavior.Asynchronous, cancellationToken).ConfigureAwait(false);
-	}
+#if NET6_0_OR_GREATER
+		ArgumentNullException.ThrowIfNull(dataTable);
 #else
-	/// <summary>
-	/// Asynchronously copies all rows in the supplied <see cref="DataTable"/> to the destination table specified by the
-	/// <see cref="DestinationTableName"/> property of the <see cref="MySqlBulkCopy"/> object.
-	/// </summary>
-	/// <param name="dataTable">The <see cref="DataTable"/> to copy.</param>
-	/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
-	/// <returns>A <see cref="MySqlBulkCopyResult"/> with the result of the bulk copy operation.</returns>
-	public async Task<MySqlBulkCopyResult> WriteToServerAsync(DataTable dataTable, CancellationToken cancellationToken = default)
-	{
-		m_valuesEnumerator = DataRowsValuesEnumerator.Create(dataTable ?? throw new ArgumentNullException(nameof(dataTable)));
+		if (dataTable is null)
+			throw new ArgumentNullException(nameof(dataTable));
+#endif
+		m_valuesEnumerator = DataRowsValuesEnumerator.Create(dataTable);
 		return await WriteToServerAsync(IOBehavior.Asynchronous, cancellationToken).ConfigureAwait(false);
 	}
-#endif
 
 	/// <summary>
 	/// Copies all rows in the supplied sequence of <see cref="DataRow"/> objects to the destination table specified by the
@@ -153,13 +158,18 @@ public async Task<MySqlBulkCopyResult> WriteToServerAsync(DataTable dataTable, C
 	/// <returns>A <see cref="MySqlBulkCopyResult"/> with the result of the bulk copy operation.</returns>
 	public MySqlBulkCopyResult WriteToServer(IEnumerable<DataRow> dataRows, int columnCount)
 	{
-		m_valuesEnumerator = new DataRowsValuesEnumerator(dataRows ?? throw new ArgumentNullException(nameof(dataRows)), columnCount);
+#if NET6_0_OR_GREATER
+		ArgumentNullException.ThrowIfNull(dataRows);
+#else
+		if (dataRows is null)
+			throw new ArgumentNullException(nameof(dataRows));
+#endif
+		m_valuesEnumerator = new DataRowsValuesEnumerator(dataRows, columnCount);
 #pragma warning disable CA2012 // Safe because method completes synchronously
 		return WriteToServerAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
 #pragma warning restore CA2012
 	}
 
-#if NETCOREAPP || NETSTANDARD2_0_OR_GREATER
 	/// <summary>
 	/// Asynchronously copies all rows in the supplied sequence of <see cref="DataRow"/> objects to the destination table specified by the
 	/// <see cref="DestinationTableName"/> property of the <see cref="MySqlBulkCopy"/> object. The number of columns
@@ -171,25 +181,15 @@ public MySqlBulkCopyResult WriteToServer(IEnumerable<DataRow> dataRows, int colu
 	/// <returns>A <see cref="MySqlBulkCopyResult"/> with the result of the bulk copy operation.</returns>
 	public async ValueTask<MySqlBulkCopyResult> WriteToServerAsync(IEnumerable<DataRow> dataRows, int columnCount, CancellationToken cancellationToken = default)
 	{
-		m_valuesEnumerator = new DataRowsValuesEnumerator(dataRows ?? throw new ArgumentNullException(nameof(dataRows)), columnCount);
-		return await WriteToServerAsync(IOBehavior.Asynchronous, cancellationToken).ConfigureAwait(false);
-	}
+#if NET6_0_OR_GREATER
+		ArgumentNullException.ThrowIfNull(dataRows);
 #else
-	/// <summary>
-	/// Asynchronously copies all rows in the supplied sequence of <see cref="DataRow"/> objects to the destination table specified by the
-	/// <see cref="DestinationTableName"/> property of the <see cref="MySqlBulkCopy"/> object. The number of columns
-	/// to be read from the <see cref="DataRow"/> objects must be specified in advance.
-	/// </summary>
-	/// <param name="dataRows">The collection of <see cref="DataRow"/> objects.</param>
-	/// <param name="columnCount">The number of columns to copy (in each row).</param>
-	/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
-	/// <returns>A <see cref="MySqlBulkCopyResult"/> with the result of the bulk copy operation.</returns>
-	public async Task<MySqlBulkCopyResult> WriteToServerAsync(IEnumerable<DataRow> dataRows, int columnCount, CancellationToken cancellationToken = default)
-	{
-		m_valuesEnumerator = new DataRowsValuesEnumerator(dataRows ?? throw new ArgumentNullException(nameof(dataRows)), columnCount);
+		if (dataRows is null)
+			throw new ArgumentNullException(nameof(dataRows));
+#endif
+		m_valuesEnumerator = new DataRowsValuesEnumerator(dataRows, columnCount);
 		return await WriteToServerAsync(IOBehavior.Asynchronous, cancellationToken).ConfigureAwait(false);
 	}
-#endif
 
 	/// <summary>
 	/// Copies all rows in the supplied <see cref="IDataReader"/> to the destination table specified by the
@@ -199,13 +199,18 @@ public async Task<MySqlBulkCopyResult> WriteToServerAsync(IEnumerable<DataRow> d
 	/// <returns>A <see cref="MySqlBulkCopyResult"/> with the result of the bulk copy operation.</returns>
 	public MySqlBulkCopyResult WriteToServer(IDataReader dataReader)
 	{
-		m_valuesEnumerator = DataReaderValuesEnumerator.Create(dataReader ?? throw new ArgumentNullException(nameof(dataReader)));
+#if NET6_0_OR_GREATER
+		ArgumentNullException.ThrowIfNull(dataReader);
+#else
+		if (dataReader is null)
+			throw new ArgumentNullException(nameof(dataReader));
+#endif
+		m_valuesEnumerator = DataReaderValuesEnumerator.Create(dataReader);
 #pragma warning disable CA2012 // Safe because method completes synchronously
 		return WriteToServerAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
 #pragma warning restore CA2012
 	}
 
-#if NETCOREAPP || NETSTANDARD2_0_OR_GREATER
 	/// <summary>
 	/// Asynchronously copies all rows in the supplied <see cref="IDataReader"/> to the destination table specified by the
 	/// <see cref="DestinationTableName"/> property of the <see cref="MySqlBulkCopy"/> object.
@@ -215,30 +220,22 @@ public MySqlBulkCopyResult WriteToServer(IDataReader dataReader)
 	/// <returns>A <see cref="MySqlBulkCopyResult"/> with the result of the bulk copy operation.</returns>
 	public async ValueTask<MySqlBulkCopyResult> WriteToServerAsync(IDataReader dataReader, CancellationToken cancellationToken = default)
 	{
-		m_valuesEnumerator = DataReaderValuesEnumerator.Create(dataReader ?? throw new ArgumentNullException(nameof(dataReader)));
-		return await WriteToServerAsync(IOBehavior.Asynchronous, cancellationToken).ConfigureAwait(false);
-	}
+#if NET6_0_OR_GREATER
+		ArgumentNullException.ThrowIfNull(dataReader);
 #else
-	/// <summary>
-	/// Asynchronously copies all rows in the supplied <see cref="IDataReader"/> to the destination table specified by the
-	/// <see cref="DestinationTableName"/> property of the <see cref="MySqlBulkCopy"/> object.
-	/// </summary>
-	/// <param name="dataReader">The <see cref="IDataReader"/> to copy from.</param>
-	/// <param name="cancellationToken">A token to cancel the asynchronous operation.</param>
-	/// <returns>A <see cref="MySqlBulkCopyResult"/> with the result of the bulk copy operation.</returns>
-	public async Task<MySqlBulkCopyResult> WriteToServerAsync(IDataReader dataReader, CancellationToken cancellationToken = default)
-	{
-		m_valuesEnumerator = DataReaderValuesEnumerator.Create(dataReader ?? throw new ArgumentNullException(nameof(dataReader)));
+		if (dataReader is null)
+			throw new ArgumentNullException(nameof(dataReader));
+#endif
+		m_valuesEnumerator = DataReaderValuesEnumerator.Create(dataReader);
 		return await WriteToServerAsync(IOBehavior.Asynchronous, cancellationToken).ConfigureAwait(false);
 	}
-#endif
 
 	private async ValueTask<MySqlBulkCopyResult> WriteToServerAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
 		var tableName = DestinationTableName ?? throw new InvalidOperationException("DestinationTableName must be set before calling WriteToServer");
 		m_wasAborted = false;
 
-		Log.Debug("Starting bulk copy to {0}", tableName);
+		Log.StartingBulkCopy(m_logger, tableName);
 		var bulkLoader = new MySqlBulkLoader(m_connection)
 		{
 			CharacterSet = "utf8mb4",
@@ -274,7 +271,7 @@ private async ValueTask<MySqlBulkCopyResult> WriteToServerAsync(IOBehavior ioBeh
 				var destinationColumn = reader.GetName(i);
 				if (schema[i].DataTypeName == "BIT")
 				{
-					AddColumnMapping(columnMappings, addDefaultMappings, i, destinationColumn, $"@`\uE002\bcol{i}`", $"%COL% = CAST(%VAR% AS UNSIGNED)");
+					AddColumnMapping(m_logger, columnMappings, addDefaultMappings, i, destinationColumn, $"@`\uE002\bcol{i}`", $"%COL% = CAST(%VAR% AS UNSIGNED)");
 				}
 				else if (schema[i].DataTypeName == "YEAR")
 				{
@@ -286,11 +283,11 @@ private async ValueTask<MySqlBulkCopyResult> WriteToServerAsync(IOBehavior ioBeh
 					var type = schema[i].DataType;
 					if (type == typeof(byte[]) || (type == typeof(Guid) && (m_connection.GuidFormat is MySqlGuidFormat.Binary16 or MySqlGuidFormat.LittleEndianBinary16 or MySqlGuidFormat.TimeSwapBinary16)))
 					{
-						AddColumnMapping(columnMappings, addDefaultMappings, i, destinationColumn, $"@`\uE002\bcol{i}`", $"%COL% = UNHEX(%VAR%)");
+						AddColumnMapping(m_logger, columnMappings, addDefaultMappings, i, destinationColumn, $"@`\uE002\bcol{i}`", $"%COL% = UNHEX(%VAR%)");
 					}
 					else if (addDefaultMappings)
 					{
-						Log.Debug("Adding default column mapping from SourceOrdinal {0} to DestinationColumn {1}", i, destinationColumn);
+						Log.AddingDefaultColumnMapping(m_logger, i, destinationColumn);
 						columnMappings.Add(new(i, destinationColumn));
 					}
 				}
@@ -303,14 +300,14 @@ private async ValueTask<MySqlBulkCopyResult> WriteToServerAsync(IOBehavior ioBeh
 			var columnMapping = columnMappings.FirstOrDefault(x => x.SourceOrdinal == i);
 			if (columnMapping is null)
 			{
-				Log.Debug("Ignoring column with SourceOrdinal {0}", i);
+				Log.IgnoringColumn(m_logger, i);
 				bulkLoader.Columns.Add("@`\uE002\bignore`");
 			}
 			else
 			{
 				if (columnMapping.DestinationColumn.Length == 0)
 					throw new InvalidOperationException($"MySqlBulkCopyColumnMapping.DestinationName is not set for SourceOrdinal {columnMapping.SourceOrdinal}");
-				if (columnMapping.DestinationColumn[0] == '@')
+				if (columnMapping.DestinationColumn[0] == '@' && columnMapping.Expression is not null)
 					bulkLoader.Columns.Add(columnMapping.DestinationColumn);
 				else
 					bulkLoader.Columns.Add(QuoteIdentifier(columnMapping.DestinationColumn));
@@ -342,11 +339,11 @@ private async ValueTask<MySqlBulkCopyResult> WriteToServerAsync(IOBehavior ioBeh
 		if (closeConnection)
 			m_connection.Close();
 
-		Log.Debug("Finished bulk copy to {0}", tableName);
+		Log.FinishedBulkCopy(m_logger, tableName);
 
 		if (!m_wasAborted && rowsInserted != m_rowsCopied && ConflictOption is MySqlBulkLoaderConflictOption.None)
 		{
-			Log.Error("Bulk copy to DestinationTableName={0} failed; RowsCopied={1}; RowsInserted={2}", tableName, m_rowsCopied, rowsInserted);
+			Log.BulkCopyFailed(m_logger, tableName, m_rowsCopied, rowsInserted);
 			throw new MySqlException(MySqlErrorCode.BulkCopyFailed, $"{m_rowsCopied} row{(m_rowsCopied == 1 ? " was" : "s were")} copied to {tableName} but only {rowsInserted} {(rowsInserted == 1 ? "was" : "were")} inserted.");
 		}
 
@@ -354,7 +351,7 @@ private async ValueTask<MySqlBulkCopyResult> WriteToServerAsync(IOBehavior ioBeh
 
 		static string QuoteIdentifier(string identifier) => "`" + identifier.Replace("`", "``") + "`";
 
-		static void AddColumnMapping(List<MySqlBulkCopyColumnMapping> columnMappings, bool addDefaultMappings, int destinationOrdinal, string destinationColumn, string variableName, string expression)
+		static void AddColumnMapping(ILogger logger, List<MySqlBulkCopyColumnMapping> columnMappings, bool addDefaultMappings, int destinationOrdinal, string destinationColumn, string variableName, string expression)
 		{
 			expression = expression.Replace("%COL%", "`" + destinationColumn + "`").Replace("%VAR%", variableName);
 			var columnMapping = columnMappings.FirstOrDefault(x => destinationColumn.Equals(x.DestinationColumn, StringComparison.OrdinalIgnoreCase));
@@ -362,18 +359,18 @@ static void AddColumnMapping(List<MySqlBulkCopyColumnMapping> columnMappings, bo
 			{
 				if (columnMapping.Expression is not null)
 				{
-					Log.Info("Column mapping for SourceOrdinal {0}, DestinationColumn {1} already has Expression {2}", columnMapping.SourceOrdinal, destinationColumn, columnMapping.Expression);
+					Log.ColumnMappingAlreadyHasExpression(logger, columnMapping.SourceOrdinal, destinationColumn, columnMapping.Expression);
 				}
 				else
 				{
-					Log.Trace("Setting expression to map SourceOrdinal {0} to DestinationColumn {1}", columnMapping.SourceOrdinal, destinationColumn);
+					Log.SettingExpressionToMapColumn(logger, columnMapping.SourceOrdinal, destinationColumn, expression);
 					columnMappings.Remove(columnMapping);
 					columnMappings.Add(new(columnMapping.SourceOrdinal, variableName, expression));
 				}
 			}
 			else if (addDefaultMappings)
 			{
-				Log.Debug("Adding default column mapping from SourceOrdinal {0} to DestinationColumn {1}", destinationOrdinal, destinationColumn);
+				Log.AddingDefaultColumnMapping(logger, destinationOrdinal, destinationColumn);
 				columnMappings.Add(new(destinationOrdinal, variableName, expression));
 			}
 		}
@@ -703,12 +700,12 @@ static bool WriteBytes(ReadOnlySpan<byte> value, ref int inputIndex, Span<byte>
 		}
 	}
 
-	private static readonly char[] s_specialCharacters = new char[] { '\t', '\\', '\n' };
-	private static readonly IMySqlConnectorLogger Log = MySqlConnectorLogManager.CreateLogger(nameof(MySqlBulkCopy));
-	private static readonly Encoding s_utf8Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
+	private static readonly char[] s_specialCharacters = ['\t', '\\', '\n'];
+	private static readonly UTF8Encoding s_utf8Encoding = new(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
 
 	private readonly MySqlConnection m_connection;
 	private readonly MySqlTransaction? m_transaction;
+	private readonly ILogger m_logger;
 	private int m_rowsCopied;
 	private IValuesEnumerator? m_valuesEnumerator;
 	private bool m_wasAborted;
diff --git a/src/MySqlConnector/MySqlBulkCopyColumnMapping.cs b/src/MySqlConnector/MySqlBulkCopyColumnMapping.cs
index 9de514437..0a3328707 100644
--- a/src/MySqlConnector/MySqlBulkCopyColumnMapping.cs
+++ b/src/MySqlConnector/MySqlBulkCopyColumnMapping.cs
@@ -28,38 +28,28 @@ namespace MySqlConnector;
 /// },
 /// </code>
 /// </summary>
-public sealed class MySqlBulkCopyColumnMapping
+/// <param name="sourceOrdinal">The zero-based ordinal position of the source column.</param>
+/// <param name="destinationColumn">The name of the destination column.</param>
+/// <param name="expression">The optional expression to be used to set the destination column.</param>
+public sealed class MySqlBulkCopyColumnMapping(int sourceOrdinal, string destinationColumn, string? expression = null)
 {
 	/// <summary>
 	/// Initializes <see cref="MySqlBulkCopyColumnMapping"/> with the default values.
 	/// </summary>
 	public MySqlBulkCopyColumnMapping()
+		: this(0, "", null)
 	{
-		DestinationColumn = "";
-	}
-
-	/// <summary>
-	/// Initializes <see cref="MySqlBulkCopyColumnMapping"/> to the specified values.
-	/// </summary>
-	/// <param name="sourceOrdinal">The zero-based ordinal position of the source column.</param>
-	/// <param name="destinationColumn">The name of the destination column.</param>
-	/// <param name="expression">The optional expression to be used to set the destination column.</param>
-	public MySqlBulkCopyColumnMapping(int sourceOrdinal, string destinationColumn, string? expression = null)
-	{
-		SourceOrdinal = sourceOrdinal;
-		DestinationColumn = destinationColumn ?? throw new ArgumentNullException(nameof(destinationColumn));
-		Expression = expression;
 	}
 
 	/// <summary>
 	/// The zero-based ordinal position of the source column to map from.
 	/// </summary>
-	public int SourceOrdinal { get; set; }
+	public int SourceOrdinal { get; set; } = sourceOrdinal;
 
 	/// <summary>
 	/// The name of the destination column to copy to. To use an expression, this should be the name of a unique user-defined variable.
 	/// </summary>
-	public string DestinationColumn { get; set; }
+	public string DestinationColumn { get; set; } = destinationColumn;
 
 	/// <summary>
 	/// An optional expression for setting a destination column. To use an expression, the <see cref="DestinationColumn"/> should
@@ -67,5 +57,5 @@ public MySqlBulkCopyColumnMapping(int sourceOrdinal, string destinationColumn, s
 	/// </summary>
 	/// <remarks>To populate a binary column, you must set <see cref="DestinationColumn"/> to a variable name, and <see cref="Expression"/> to an
 	/// expression that uses <code>UNHEX</code> to set the column value, e.g., <code>`destColumn` = UNHEX(@variableName)</code>.</remarks>
-	public string? Expression { get; set; }
+	public string? Expression { get; set; } = expression;
 }
diff --git a/src/MySqlConnector/MySqlBulkLoader.cs b/src/MySqlConnector/MySqlBulkLoader.cs
index b69d06fb6..8b2fc8306 100644
--- a/src/MySqlConnector/MySqlBulkLoader.cs
+++ b/src/MySqlConnector/MySqlBulkLoader.cs
@@ -2,7 +2,6 @@
 using System.Globalization;
 using System.Text;
 using MySqlConnector.Protocol.Serialization;
-using MySqlConnector.Utilities;
 
 namespace MySqlConnector;
 
@@ -135,8 +134,8 @@ public MySqlBulkLoader(MySqlConnection connection)
 	{
 		Connection = connection;
 		Local = true;
-		Columns = new();
-		Expressions = new();
+		Columns = [];
+		Expressions = [];
 	}
 
 	/// <summary>
@@ -177,8 +176,7 @@ internal async ValueTask<int> LoadAsync(IOBehavior ioBehavior, CancellationToken
 			{
 				// replace the file name with a sentinel so that we know (when processing the result set) that it's not spoofed by the server
 				var newFileName = GenerateSourceFileName();
-				lock (s_lock)
-					s_sources.Add(newFileName, CreateFileStream(FileName!));
+				AddSource(newFileName, CreateFileStream(FileName!));
 				FileName = newFileName;
 			}
 		}
@@ -188,8 +186,7 @@ internal async ValueTask<int> LoadAsync(IOBehavior ioBehavior, CancellationToken
 				throw new InvalidOperationException("Local must be true to use SourceStream, SourceDataTable, or SourceDataReader.");
 
 			FileName = GenerateSourceFileName();
-			lock (s_lock)
-				s_sources.Add(FileName, Source!);
+			AddSource(FileName, Source!);
 		}
 
 		var closeConnection = false;
@@ -203,7 +200,7 @@ internal async ValueTask<int> LoadAsync(IOBehavior ioBehavior, CancellationToken
 		try
 		{
 			if (Local && !Connection.AllowLoadLocalInfile)
-				throw new NotSupportedException("To use MySqlBulkLoader.Local=true, set AllowLoadLocalInfile=true in the connection string. See https://fl.vu/mysql-load-data");
+				throw new NotSupportedException("To use MySqlBulkLoader.Local=true, set AllowLoadLocalInfile=true in the connection string. See https://mysqlconnector.net/load-data");
 
 			using var cmd = new MySqlCommand(CreateSql(), Connection, Connection.CurrentTransaction)
 			{
@@ -222,6 +219,12 @@ internal async ValueTask<int> LoadAsync(IOBehavior ioBehavior, CancellationToken
 			if (closeConnection)
 				Connection.Close();
 		}
+
+		static void AddSource(string name, object source)
+		{
+			lock (s_lock)
+				s_sources.Add(name, source);
+		}
 	}
 
 	internal const string SourcePrefix = ":SOURCE:";
@@ -282,7 +285,7 @@ private string CreateSql()
 		return sb.ToString();
 	}
 
-	private static Stream CreateFileStream(string fileName)
+	private static FileStream CreateFileStream(string fileName)
 	{
 		try
 		{
@@ -299,7 +302,7 @@ internal static object GetAndRemoveSource(string sourceKey)
 		lock (s_lock)
 		{
 			var source = s_sources[sourceKey];
-			s_sources.Remove(sourceKey);
+			_ = s_sources.Remove(sourceKey);
 			return source;
 		}
 	}
@@ -309,10 +312,7 @@ internal static bool TryGetAndRemoveSource(string sourceKey, [NotNullWhen(true)]
 		lock (s_lock)
 		{
 			if (s_sources.TryGetValue(sourceKey, out source))
-			{
-				s_sources.Remove(sourceKey);
-				return true;
-			}
+				return s_sources.Remove(sourceKey);
 		}
 
 		return false;
@@ -320,6 +320,10 @@ internal static bool TryGetAndRemoveSource(string sourceKey, [NotNullWhen(true)]
 
 	private static string GenerateSourceFileName() => SourcePrefix + Guid.NewGuid().ToString("N");
 
+#if NET9_0_OR_GREATER
+	private static readonly Lock s_lock = new();
+#else
 	private static readonly object s_lock = new();
-	private static readonly Dictionary<string, object> s_sources = new();
+#endif
+	private static readonly Dictionary<string, object> s_sources = [];
 }
diff --git a/src/MySqlConnector/MySqlCommand.cs b/src/MySqlConnector/MySqlCommand.cs
index 77745ea4f..0e72806c3 100644
--- a/src/MySqlConnector/MySqlCommand.cs
+++ b/src/MySqlConnector/MySqlCommand.cs
@@ -1,7 +1,6 @@
-using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.Logging;
 using MySqlConnector.Core;
-using MySqlConnector.Logging;
 using MySqlConnector.Protocol.Serialization;
 using MySqlConnector.Utilities;
 
@@ -71,6 +70,7 @@ private MySqlCommand(MySqlCommand other)
 	{
 		GC.SuppressFinalize(this);
 		m_commandTimeout = other.m_commandTimeout;
+		((ICancellableCommand) this).EffectiveCommandTimeout = null;
 		m_commandType = other.m_commandType;
 		DesignTimeVisible = other.DesignTimeVisible;
 		UpdatedRowSource = other.UpdatedRowSource;
@@ -81,14 +81,14 @@ private MySqlCommand(MySqlCommand other)
 	/// <summary>
 	/// The collection of <see cref="MySqlParameter"/> objects for this command.
 	/// </summary>
-	public new MySqlParameterCollection Parameters => m_parameterCollection ??= new();
+	public new MySqlParameterCollection Parameters => m_parameterCollection ??= [];
 
 	MySqlParameterCollection? IMySqlCommand.RawParameters => m_parameterCollection;
 
 	/// <summary>
 	/// The collection of <see cref="MySqlAttribute"/> objects for this command.
 	/// </summary>
-	public MySqlAttributeCollection Attributes => m_attributeCollection ??= new();
+	public MySqlAttributeCollection Attributes => m_attributeCollection ??= [];
 
 	MySqlAttributeCollection? IMySqlCommand.RawAttributes => m_attributeCollection;
 
@@ -97,6 +97,7 @@ private MySqlCommand(MySqlCommand other)
 	/// <inheritdoc/>
 	public override void Cancel() => Connection?.Cancel(this, m_commandId, true);
 
+#pragma warning disable CA2012 // OK to read .Result because the ValueTask is completed
 	/// <summary>
 	/// Executes this command on the associated <see cref="MySqlConnection"/>.
 	/// </summary>
@@ -104,14 +105,14 @@ private MySqlCommand(MySqlCommand other)
 	/// <remarks>For UPDATE, INSERT, and DELETE statements, the return value is the number of rows affected by the command.
 	/// For stored procedures, the return value is the number of rows affected by the last statement in the stored procedure,
 	/// or zero if the last statement is a SELECT. For all other types of statements, the return value is -1.</remarks>
-	public override int ExecuteNonQuery() => ExecuteNonQueryAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
+	public override int ExecuteNonQuery() => ExecuteNonQueryAsync(IOBehavior.Synchronous, CancellationToken.None).Result;
 
-	/// <inheritdoc/>
-	public override object? ExecuteScalar() => ExecuteScalarAsync(IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
+	public override object? ExecuteScalar() => ExecuteScalarAsync(IOBehavior.Synchronous, CancellationToken.None).Result;
 
-	public new MySqlDataReader ExecuteReader() => ExecuteReaderAsync(default, IOBehavior.Synchronous, default).GetAwaiter().GetResult();
+	public new MySqlDataReader ExecuteReader() => ExecuteReaderAsync(default, IOBehavior.Synchronous, default).Result;
 
 	public new MySqlDataReader ExecuteReader(CommandBehavior commandBehavior) => ExecuteReaderAsync(commandBehavior, IOBehavior.Synchronous, default).GetAwaiter().GetResult();
+#pragma warning restore CA2012 // OK to read .Result because the ValueTask is completed
 
 	/// <inheritdoc/>
 	public override void Prepare()
@@ -180,7 +181,7 @@ private bool NeedsPrepare(out Exception? exception)
 		if (exception is not null || Connection!.IgnorePrepare)
 			return false;
 
-		if (CommandType != CommandType.StoredProcedure && CommandType != CommandType.Text)
+		if (CommandType is not CommandType.StoredProcedure and not CommandType.Text)
 		{
 			exception = new NotSupportedException("Only CommandType.Text and CommandType.StoredProcedure are currently supported by MySqlCommand.Prepare.");
 			return false;
@@ -227,7 +228,17 @@ public override string CommandText
 	public override int CommandTimeout
 	{
 		get => Math.Min(m_commandTimeout ?? Connection?.DefaultCommandTimeout ?? 0, int.MaxValue / 1000);
-		set => m_commandTimeout = value >= 0 ? value : throw new ArgumentOutOfRangeException(nameof(value), "CommandTimeout must be greater than or equal to zero.");
+		set
+		{
+#if NET8_0_OR_GREATER
+			ArgumentOutOfRangeException.ThrowIfNegative(value);
+#else
+			if (value < 0)
+				throw new ArgumentOutOfRangeException(nameof(value), "CommandTimeout must be greater than or equal to zero.");
+#endif
+			m_commandTimeout = value;
+			((ICancellableCommand) this).EffectiveCommandTimeout = null;
+		}
 	}
 
 	/// <inheritdoc/>
@@ -236,7 +247,7 @@ public override CommandType CommandType
 		get => m_commandType;
 		set
 		{
-			if (value != CommandType.Text && value != CommandType.StoredProcedure)
+			if (value is not CommandType.Text and not CommandType.StoredProcedure)
 				throw new ArgumentException("CommandType must be Text or StoredProcedure.", nameof(value));
 			m_commandType = value;
 		}
@@ -274,8 +285,10 @@ protected override DbTransaction? DbTransaction
 
 	protected override DbParameter CreateDbParameter() => new MySqlParameter();
 
+#pragma warning disable CA2012 // OK to read .Result because the ValueTask is completed
 	protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) =>
-		ExecuteReaderAsync(behavior, IOBehavior.Synchronous, CancellationToken.None).GetAwaiter().GetResult();
+		ExecuteReaderAsync(behavior, IOBehavior.Synchronous, CancellationToken.None).Result;
+#pragma warning restore CA2012
 
 	/// <summary>
 	/// Executes this command asynchronously on the associated <see cref="MySqlConnection"/>.
@@ -286,9 +299,9 @@ protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) =>
 	/// For stored procedures, the return value is the number of rows affected by the last statement in the stored procedure,
 	/// or zero if the last statement is a SELECT. For all other types of statements, the return value is -1.</remarks>
 	public override Task<int> ExecuteNonQueryAsync(CancellationToken cancellationToken) =>
-		ExecuteNonQueryAsync(AsyncIOBehavior, cancellationToken);
+		ExecuteNonQueryAsync(AsyncIOBehavior, cancellationToken).AsTask();
 
-	internal async Task<int> ExecuteNonQueryAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
+	internal async ValueTask<int> ExecuteNonQueryAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
 		Volatile.Write(ref m_commandTimedOut, false);
 		this.ResetCommandTimeout();
@@ -304,9 +317,9 @@ internal async Task<int> ExecuteNonQueryAsync(IOBehavior ioBehavior, Cancellatio
 	}
 
 	public override Task<object?> ExecuteScalarAsync(CancellationToken cancellationToken) =>
-		ExecuteScalarAsync(AsyncIOBehavior, cancellationToken);
+		ExecuteScalarAsync(AsyncIOBehavior, cancellationToken).AsTask();
 
-	internal async Task<object?> ExecuteScalarAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
+	internal async ValueTask<object?> ExecuteScalarAsync(IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
 		Volatile.Write(ref m_commandTimedOut, false);
 		this.ResetCommandTimeout();
@@ -328,15 +341,15 @@ internal async Task<int> ExecuteNonQueryAsync(IOBehavior ioBehavior, Cancellatio
 	}
 
 	public new Task<MySqlDataReader> ExecuteReaderAsync(CancellationToken cancellationToken = default) =>
-		ExecuteReaderAsync(default, AsyncIOBehavior, cancellationToken);
+		ExecuteReaderAsync(default, AsyncIOBehavior, cancellationToken).AsTask();
 
 	public new Task<MySqlDataReader> ExecuteReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken = default) =>
-		ExecuteReaderAsync(behavior, AsyncIOBehavior, cancellationToken);
+		ExecuteReaderAsync(behavior, AsyncIOBehavior, cancellationToken).AsTask();
 
 	protected override async Task<DbDataReader> ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) =>
 		await ExecuteReaderAsync(behavior, AsyncIOBehavior, cancellationToken).ConfigureAwait(false);
 
-	internal async Task<MySqlDataReader> ExecuteReaderAsync(CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken)
+	internal async ValueTask<MySqlDataReader> ExecuteReaderAsync(CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
 		Volatile.Write(ref m_commandTimedOut, false);
 		this.ResetCommandTimeout();
@@ -344,15 +357,15 @@ internal async Task<MySqlDataReader> ExecuteReaderAsync(CommandBehavior behavior
 		return await ExecuteReaderNoResetTimeoutAsync(behavior, ioBehavior, cancellationToken).ConfigureAwait(false);
 	}
 
-	internal Task<MySqlDataReader> ExecuteReaderNoResetTimeoutAsync(CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken)
+	internal ValueTask<MySqlDataReader> ExecuteReaderNoResetTimeoutAsync(CommandBehavior behavior, IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
 		if (!IsValid(out var exception))
-			return Task.FromException<MySqlDataReader>(exception);
+			return ValueTaskExtensions.FromException<MySqlDataReader>(exception);
 
 		var activity = NoActivity ? null : Connection!.Session.StartActivity(ActivitySourceHelper.ExecuteActivityName,
 			ActivitySourceHelper.DatabaseStatementTagName, CommandText);
 		m_commandBehavior = behavior;
-		return CommandExecutor.ExecuteReaderAsync(new IMySqlCommand[] { this }, SingleCommandPayloadCreator.Instance, behavior, activity, ioBehavior, cancellationToken);
+		return CommandExecutor.ExecuteReaderAsync(new(this), SingleCommandPayloadCreator.Instance, behavior, activity, ioBehavior, cancellationToken);
 	}
 
 	public MySqlCommand Clone() => new(this);
@@ -386,10 +399,10 @@ public Task DisposeAsync()
 	/// <returns>An object that must be disposed to revoke the cancellation registration.</returns>
 	/// <remarks>This method is more efficient than calling <code>token.Register(Command.Cancel)</code> because it avoids
 	/// unnecessary allocations.</remarks>
-	IDisposable? ICancellableCommand.RegisterCancel(CancellationToken cancellationToken)
+	CancellationTokenRegistration ICancellableCommand.RegisterCancel(CancellationToken cancellationToken)
 	{
 		if (!cancellationToken.CanBeCanceled)
-			return null;
+			return default;
 
 		m_cancelAction ??= Cancel;
 		return cancellationToken.Register(m_cancelAction);
@@ -411,6 +424,8 @@ void ICancellableCommand.SetTimeout(int milliseconds)
 
 	int ICancellableCommand.CommandId => m_commandId;
 
+	int? ICancellableCommand.EffectiveCommandTimeout { get; set; }
+
 	int ICancellableCommand.CancelAttemptCount { get; set; }
 
 	ICancellableCommand IMySqlCommand.CancellableCommand => this;
@@ -430,10 +445,10 @@ private bool IsValid([NotNullWhen(false)] out Exception? exception)
 			exception = new ObjectDisposedException(GetType().Name);
 		else if (Connection is null)
 			exception = new InvalidOperationException("Connection property must be non-null.");
-		else if (Connection.State != ConnectionState.Open && Connection.State != ConnectionState.Connecting)
+		else if (Connection.State is not ConnectionState.Open and not ConnectionState.Connecting)
 			exception = new InvalidOperationException($"Connection must be Open; current state is {Connection.State}");
 		else if (!Connection.IgnoreCommandTransaction && Transaction != Connection.CurrentTransaction)
-			exception = new InvalidOperationException("The transaction associated with this command is not the connection's active transaction; see https://fl.vu/mysql-trans");
+			exception = new InvalidOperationException("The transaction associated with this command is not the connection's active transaction; see https://mysqlconnector.net/trans");
 		else if (string.IsNullOrWhiteSpace(CommandText))
 			exception = new InvalidOperationException("CommandText must be specified");
 		return exception is null;
@@ -445,6 +460,7 @@ private bool IsValid([NotNullWhen(false)] out Exception? exception)
 	CommandBehavior IMySqlCommand.CommandBehavior => m_commandBehavior;
 	MySqlParameterCollection? IMySqlCommand.OutParameters { get; set; }
 	MySqlParameter? IMySqlCommand.ReturnParameter { get; set; }
+	ILogger IMySqlCommand.Logger => Connection!.LoggingConfiguration.CommandLogger;
 
 	private readonly int m_commandId;
 	private bool m_isDisposed;
diff --git a/src/MySqlConnector/MySqlCommandBuilder.cs b/src/MySqlConnector/MySqlCommandBuilder.cs
index 5a9d361e8..7b63dd5ef 100644
--- a/src/MySqlConnector/MySqlCommandBuilder.cs
+++ b/src/MySqlConnector/MySqlCommandBuilder.cs
@@ -12,8 +12,12 @@ public sealed class MySqlCommandBuilder : DbCommandBuilder
 
 	private static async Task DeriveParametersAsync(IOBehavior ioBehavior, MySqlCommand command, CancellationToken cancellationToken)
 	{
+#if NET6_0_OR_GREATER
+		ArgumentNullException.ThrowIfNull(command);
+#else
 		if (command is null)
 			throw new ArgumentNullException(nameof(command));
+#endif
 		if (command.CommandType != CommandType.StoredProcedure)
 			throw new ArgumentException($"MySqlCommand.CommandType must be StoredProcedure not {command.CommandType}", nameof(command));
 		if (string.IsNullOrWhiteSpace(command.CommandText))
@@ -77,7 +81,7 @@ protected override void ApplyParameterInfo(DbParameter parameter, DataRow row, S
 
 	protected override void SetRowUpdatingHandler(DbDataAdapter adapter)
 	{
-		if (!(adapter is MySqlDataAdapter mySqlDataAdapter))
+		if (adapter is not MySqlDataAdapter mySqlDataAdapter)
 			throw new ArgumentException("adapter needs to be a MySqlDataAdapter", nameof(adapter));
 
 		if (adapter == DataAdapter)
@@ -90,7 +94,7 @@ protected override void SetRowUpdatingHandler(DbDataAdapter adapter)
 
 	public override string UnquoteIdentifier(string quotedIdentifier)
 	{
-		if (quotedIdentifier is [ '`', .., '`' ])
+		if (quotedIdentifier is ['`', .., '`'])
 			quotedIdentifier = quotedIdentifier[1..^1];
 		return quotedIdentifier.Replace("``", "`");
 	}
diff --git a/src/MySqlConnector/MySqlConnection.cs b/src/MySqlConnector/MySqlConnection.cs
index 7f2660256..62f70bdac 100644
--- a/src/MySqlConnector/MySqlConnection.cs
+++ b/src/MySqlConnector/MySqlConnection.cs
@@ -1,13 +1,18 @@
-using System.Collections.Generic;
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
+#if NET6_0_OR_GREATER
 using System.Globalization;
+#endif
+using System.Net;
 using System.Net.Security;
 using System.Net.Sockets;
 using System.Security.Authentication;
 using System.Security.Cryptography.X509Certificates;
+using Microsoft.Extensions.Logging;
 using MySqlConnector.Core;
 using MySqlConnector.Logging;
+using MySqlConnector.Plugins;
+using MySqlConnector.Protocol;
 using MySqlConnector.Protocol.Payloads;
 using MySqlConnector.Protocol.Serialization;
 using MySqlConnector.Utilities;
@@ -29,18 +34,24 @@ public MySqlConnection()
 	}
 
 	public MySqlConnection(string? connectionString)
+		: this(connectionString ?? "", MySqlConnectorLoggingConfiguration.GlobalConfiguration)
 	{
-		GC.SuppressFinalize(this);
-		m_connectionString = connectionString ?? "";
 	}
 
-#if NET7_0_OR_GREATER
 	internal MySqlConnection(MySqlDataSource dataSource)
-		: this(dataSource.ConnectionString)
+		: this(dataSource.ConnectionString, dataSource.LoggingConfiguration)
 	{
 		m_dataSource = dataSource;
 	}
-#endif
+
+	private MySqlConnection(string connectionString, MySqlConnectorLoggingConfiguration loggingConfiguration)
+	{
+		GC.SuppressFinalize(this);
+		m_connectionString = connectionString;
+		LoggingConfiguration = loggingConfiguration;
+		m_logger = loggingConfiguration.ConnectionLogger;
+		m_transactionLogger = loggingConfiguration.TransactionLogger;
+	}
 
 #pragma warning disable CA2012 // Safe because method completes synchronously
 	/// <summary>
@@ -93,16 +104,6 @@ internal MySqlConnection(MySqlDataSource dataSource)
 	/// <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>
@@ -128,6 +129,7 @@ protected override async ValueTask<DbTransaction> BeginDbTransactionAsync(Isolat
 	/// <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);
+#endif
 
 	/// <summary>
 	/// Begins a database transaction asynchronously.
@@ -138,8 +140,8 @@ protected override async ValueTask<DbTransaction> BeginDbTransactionAsync(Isolat
 	/// <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
 
+	// set session transaction isolation level read uncommitted;start transaction with consistent snapshot,read write;
 	private async ValueTask<MySqlTransaction> BeginTransactionAsync(IsolationLevel isolationLevel, bool? isReadOnly, IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
 		if (State != ConnectionState.Open)
@@ -149,39 +151,171 @@ private async ValueTask<MySqlTransaction> BeginTransactionAsync(IsolationLevel i
 		if (m_enlistedTransaction is not null)
 			throw new InvalidOperationException("Cannot begin a transaction when already enlisted in a transaction.");
 
-		var isolationLevelValue = isolationLevel switch
+		Log.StartingTransaction(m_transactionLogger, m_session!.Id);
+
+		// get the bytes for both payloads concatenated together (suitable for pipelining)
+		var startTransactionPayload = GetStartTransactionPayload(isolationLevel, isReadOnly, m_session.SupportsQueryAttributes);
+
+		if (GetInitializedConnectionSettings() is { UseCompression: false, Pipelining: not false })
+		{
+			// send the two packets together
+			await m_session.SendRawAsync(startTransactionPayload, ioBehavior, cancellationToken).ConfigureAwait(false);
+
+			// read the two OK replies
+			var payload = await m_session.ReceiveReplyAsync(1, ioBehavior, cancellationToken).ConfigureAwait(false);
+			OkPayload.Verify(payload.Span, m_session);
+
+			payload = await m_session.ReceiveReplyAsync(1, ioBehavior, cancellationToken).ConfigureAwait(false);
+			OkPayload.Verify(payload.Span, m_session);
+		}
+		else
+		{
+			// send the two packets separately
+			await m_session.SendAsync(new Protocol.PayloadData(startTransactionPayload.Slice(4, startTransactionPayload.Span[0])), ioBehavior, cancellationToken).ConfigureAwait(false);
+
+			var payload = await m_session.ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+			OkPayload.Verify(payload.Span, m_session);
+
+			await m_session.SendAsync(new Protocol.PayloadData(startTransactionPayload.Slice(8 + startTransactionPayload.Span[0], startTransactionPayload.Span[startTransactionPayload.Span[0] + 4])), ioBehavior, cancellationToken).ConfigureAwait(false);
+
+			payload = await m_session.ReceiveReplyAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+			OkPayload.Verify(payload.Span, m_session);
+		}
+
+		var transaction = new MySqlTransaction(this, isolationLevel, m_transactionLogger);
+		CurrentTransaction = transaction;
+		return transaction;
+	}
+
+	/// <summary>
+	/// Returns a <see cref="ReadOnlyMemory{Byte}"/> containing two payloads to set the isolation level and start a transaction.
+	/// </summary>
+	internal static ReadOnlyMemory<byte> GetStartTransactionPayload(IsolationLevel isolationLevel, bool? isReadOnly, bool supportsQueryAttributes)
+	{
+		var isolationLevelIndex = isolationLevel switch
 		{
-			IsolationLevel.ReadUncommitted => "read uncommitted",
-			IsolationLevel.ReadCommitted => "read committed",
-			IsolationLevel.RepeatableRead => "repeatable read",
-			IsolationLevel.Serializable => "serializable",
-			IsolationLevel.Snapshot => "repeatable read",
+			IsolationLevel.ReadUncommitted => 0,
+			IsolationLevel.ReadCommitted => 1,
+			IsolationLevel.Serializable => 2,
+			IsolationLevel.RepeatableRead => 3,
+			IsolationLevel.Snapshot => 3,
 
 			// "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",
+			IsolationLevel.Unspecified => 3,
 
 			_ => throw new NotSupportedException($"IsolationLevel.{isolationLevel} is not supported."),
 		};
 
-		using (var cmd = new MySqlCommand($"set session transaction isolation level {isolationLevelValue};", this) { NoActivity = true })
+		var consistentSnapshotIndex = isolationLevel == IsolationLevel.Snapshot ? 1 : 0;
+
+		var readOnlyIndex = isReadOnly switch
 		{
-			await cmd.ExecuteNonQueryAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+			null => 0,
+			false => 1,
+			true => 2,
+		};
+
+		var index = ((((supportsQueryAttributes ? 1 : 0) * 5) + isolationLevelIndex + consistentSnapshotIndex) * 3) + readOnlyIndex;
+
+		if (s_startTransactionPayloads[index].IsEmpty)
+		{
+			// create payload that's (four-byte header)(query)(set isolation level X)(four-byte header)(query)(start transaction)
+			var payload = new byte[125];
+			var startIndex = 4;
+			var length = 0;
+
+			payload[startIndex] = (byte) CommandKind.Query;
+			startIndex++;
+			length++;
+
+			if (supportsQueryAttributes)
+			{
+				payload[startIndex + 1] = 1;
+				startIndex += 2;
+				length += 2;
+			}
+
+			// copy in 'set isolation level' prefix
+			ReadOnlySpan<byte> setIsolationLevelText = "set session transaction isolation level "u8;
+			setIsolationLevelText.CopyTo(payload.AsSpan(startIndex));
+			length += setIsolationLevelText.Length;
+			startIndex += setIsolationLevelText.Length;
+
+			// copy in isolation level
+			ReadOnlySpan<byte> isolationLevelText = isolationLevelIndex switch
+			{
+				0 => "read uncommitted"u8,
+				1 => "read committed"u8,
+				2 => "serializable"u8,
+				_ => "repeatable read"u8,
+			};
+			isolationLevelText.CopyTo(payload.AsSpan(startIndex));
+			startIndex += isolationLevelText.Length;
+			length += isolationLevelText.Length;
+
+			payload[startIndex] = (byte) ';';
+			startIndex++;
+			length++;
+
+			// set the length of the first payload
+			payload[0] = (byte) length;
+
+			startIndex += 4;
 
-			var consistentSnapshotText = isolationLevel == IsolationLevel.Snapshot ? " with consistent snapshot" : "";
-			var readOnlyText = isReadOnly switch
+			payload[startIndex] = (byte) CommandKind.Query;
+			startIndex++;
+			length = 1;
+
+			if (supportsQueryAttributes)
 			{
-				true => " read only",
-				false => " read write",
-				null => "",
+				payload[startIndex + 1] = 1;
+				startIndex += 2;
+				length += 2;
+			}
+
+			// copy in 'start transaction' prefix
+			ReadOnlySpan<byte> startTransactionText = "start transaction"u8;
+			startTransactionText.CopyTo(payload.AsSpan(startIndex));
+			length += startTransactionText.Length;
+			startIndex += startTransactionText.Length;
+
+			// copy in 'with consistent snapshot'
+			if (consistentSnapshotIndex == 1)
+			{
+				ReadOnlySpan<byte> consistentSnapshotText = " with consistent snapshot"u8;
+				consistentSnapshotText.CopyTo(payload.AsSpan(startIndex));
+				length += consistentSnapshotText.Length;
+				startIndex += consistentSnapshotText.Length;
+			}
+
+			if (consistentSnapshotIndex > 0 && readOnlyIndex > 0)
+			{
+				payload[startIndex] = (byte) ',';
+				startIndex++;
+				length++;
+			}
+
+			ReadOnlySpan<byte> readOnlyText = readOnlyIndex switch
+			{
+				1 => " read write"u8,
+				2 => " read only"u8,
+				_ => ""u8,
 			};
-			var separatorText = (consistentSnapshotText.Length == 0 || readOnlyText.Length == 0) ? "" : ",";
-			cmd.CommandText = $"start transaction{consistentSnapshotText}{separatorText}{readOnlyText};";
-			await cmd.ExecuteNonQueryAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+			readOnlyText.CopyTo(payload.AsSpan(startIndex));
+			startIndex += readOnlyText.Length;
+			length += readOnlyText.Length;
+
+			payload[startIndex] = (byte) ';';
+			startIndex++;
+			length++;
+
+			// store length at the beginning of the second payload
+			payload[payload[0] + 4] = (byte) length;
+
+			s_startTransactionPayloads[index] = new ReadOnlyMemory<byte>(payload, 0, payload[0] + payload[payload[0] + 4] + 8);
 		}
 
-		var transaction = new MySqlTransaction(this, isolationLevel);
-		CurrentTransaction = transaction;
-		return transaction;
+		return s_startTransactionPayloads[index];
 	}
 
 	public override void EnlistTransaction(System.Transactions.Transaction? transaction)
@@ -211,14 +345,14 @@ public override void EnlistTransaction(System.Transactions.Transaction? transact
 			else
 			{
 				m_enlistedTransaction = GetInitializedConnectionSettings().UseXaTransactions ?
-					(EnlistedTransactionBase)new XaEnlistedTransaction(transaction, this) :
+					 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();
+						s_transactionConnections[transaction] = enlistedTransactions = [];
 					enlistedTransactions.Add(m_enlistedTransaction);
 				}
 			}
@@ -299,8 +433,12 @@ internal void UnenlistTransaction()
 	private void TakeSessionFrom(MySqlConnection other)
 	{
 #if DEBUG
+#if NET6_0_OR_GREATER
+		ArgumentNullException.ThrowIfNull(other);
+#else
 		if (other is null)
 			throw new ArgumentNullException(nameof(other));
+#endif
 		if (m_session is not null)
 			throw new InvalidOperationException("This connection must not have a session");
 		if (other.m_session is null)
@@ -351,11 +489,13 @@ private async Task ChangeDatabaseAsync(IOBehavior ioBehavior, string databaseNam
 		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);
+		OkPayload.Verify(payload.Span, m_session);
+
+		// for non session tracking servers
 		m_session.DatabaseOverride = databaseName;
 	}
 
-	public new MySqlCommand CreateCommand() => (MySqlCommand)base.CreateCommand();
+	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();
@@ -385,6 +525,8 @@ private async ValueTask<bool> PingAsync(IOBehavior ioBehavior, CancellationToken
 
 	internal async Task OpenAsync(IOBehavior? ioBehavior, CancellationToken cancellationToken)
 	{
+		var openStartingTimestamp = Stopwatch.GetTimestamp();
+
 		VerifyNotDisposed();
 		cancellationToken.ThrowIfCancellationRequested();
 		if (State != ConnectionState.Closed)
@@ -393,15 +535,10 @@ internal async Task OpenAsync(IOBehavior? ioBehavior, CancellationToken cancella
 		using var activity = ActivitySourceHelper.StartActivity(ActivitySourceHelper.OpenActivityName);
 		try
 		{
-			var openStartTickCount = Environment.TickCount;
-
 			SetState(ConnectionState.Connecting);
 
-			var pool =
-#if NET7_0_OR_GREATER
-				m_dataSource?.Pool ??
-#endif
-				ConnectionPool.GetPool(m_connectionString);
+			var pool = m_dataSource?.Pool ??
+				ConnectionPool.GetPool(m_connectionString, LoggingConfiguration, createIfNotFound: true);
 			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
@@ -414,19 +551,28 @@ internal async Task OpenAsync(IOBehavior? ioBehavior, CancellationToken cancella
 					ActivitySourceHelper.CopyTags(m_session!.ActivityTags, activity);
 					m_hasBeenOpened = true;
 					SetState(ConnectionState.Open);
+
+					if (ConnectionOpenedCallback is { } autoEnlistConnectionOpenedCallback)
+					{
+						cancellationToken.ThrowIfCancellationRequested();
+						await autoEnlistConnectionOpenedCallback(new(this, MySqlConnectionOpenedConditions.None), cancellationToken).ConfigureAwait(false);
+					}
+
 					return;
 				}
 			}
 
 			try
 			{
-				m_session = await CreateSessionAsync(pool, openStartTickCount, activity, ioBehavior, cancellationToken).ConfigureAwait(false);
+				m_session = await CreateSessionAsync(pool, openStartingTimestamp, activity, ioBehavior, cancellationToken).ConfigureAwait(false);
 				m_hasBeenOpened = true;
 				SetState(ConnectionState.Open);
 			}
-			catch (OperationCanceledException)
+			catch (OperationCanceledException ex)
 			{
 				SetState(ConnectionState.Closed);
+				if (!cancellationToken.Equals(ex.CancellationToken))
+					cancellationToken.ThrowIfCancellationRequested();
 				throw;
 			}
 			catch (MySqlException)
@@ -444,7 +590,11 @@ internal async Task OpenAsync(IOBehavior? ioBehavior, CancellationToken cancella
 			if (m_connectionSettings.AutoEnlist && System.Transactions.Transaction.Current is not null)
 				EnlistTransaction(System.Transactions.Transaction.Current);
 
-			activity?.SetSuccess();
+			if (ConnectionOpenedCallback is { } connectionOpenedCallback)
+			{
+				cancellationToken.ThrowIfCancellationRequested();
+				await connectionOpenedCallback(new(this, m_session.Conditions), cancellationToken).ConfigureAwait(false);
+			}
 		}
 		catch (Exception ex) when (activity is { IsAllDataRequested: true })
 		{
@@ -467,10 +617,10 @@ internal async Task OpenAsync(IOBehavior? ioBehavior, CancellationToken cancella
 	public async ValueTask ResetConnectionAsync(CancellationToken cancellationToken = default)
 	{
 		var session = Session;
-		Log.Debug("Session{0} resetting connection", session.Id);
+		Log.ResettingConnection(m_logger, 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);
+		OkPayload.Verify(payload.Span, session);
 	}
 
 	[AllowNull]
@@ -510,11 +660,7 @@ public override string ConnectionString
 	/// 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.
@@ -567,10 +713,14 @@ public override string ConnectionString
 
 	private static async Task ClearPoolAsync(MySqlConnection connection, IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
+#if NET6_0_OR_GREATER
+		ArgumentNullException.ThrowIfNull(connection);
+#else
 		if (connection is null)
 			throw new ArgumentNullException(nameof(connection));
+#endif
 
-		var pool = ConnectionPool.GetPool(connection.m_connectionString);
+		var pool = ConnectionPool.GetPool(connection.m_connectionString, null, createIfNotFound: false);
 		if (pool is not null)
 			await pool.ClearAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
 	}
@@ -589,14 +739,14 @@ private static async Task ClearPoolAsync(MySqlConnection connection, IOBehavior
 	/// <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="collectionName">The name of the schema to return. See <a href="https://mysqlconnector.net/overview/schema-collections/">Supported Schema Collections</a> for the list of supported schema names.</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="collectionName">The name of the schema to return. See <a href="https://mysqlconnector.net/overview/schema-collections/">Supported Schema Collections</a> for the list of supported schema names.</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();
@@ -703,12 +853,7 @@ public async Task DisposeAsync()
 		}
 	}
 
-	public MySqlConnection Clone() => new(m_connectionString, m_hasBeenOpened)
-	{
-		ProvideClientCertificatesCallback = ProvideClientCertificatesCallback,
-		ProvidePasswordCallback = ProvidePasswordCallback,
-		RemoteCertificateValidationCallback = RemoteCertificateValidationCallback,
-	};
+	public MySqlConnection Clone() => new(this, m_dataSource, m_connectionString, m_hasBeenOpened);
 
 	object ICloneable.Clone() => Clone();
 
@@ -724,16 +869,13 @@ public async Task DisposeAsync()
 	public MySqlConnection CloneWith(string connectionString)
 	{
 		var newBuilder = new MySqlConnectionStringBuilder(connectionString ?? throw new ArgumentNullException(nameof(connectionString)));
-		var currentBuilder = new MySqlConnectionStringBuilder(m_connectionString);
+		var currentBuilder = GetConnectionSettings().ConnectionStringBuilder;
 		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,
-		};
+		var newConnectionString = newBuilder.ConnectionString;
+		var dataSource = newConnectionString == currentBuilder.ConnectionString ? m_dataSource : null;
+		return new MySqlConnection(this, dataSource, newConnectionString, m_hasBeenOpened && shouldCopyPassword && !currentBuilder.PersistSecurityInfo);
 	}
 
 	internal ServerSession Session
@@ -751,14 +893,14 @@ internal ServerSession Session
 
 	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)
+		// NOTE: read and cache m_session in a local variable because it can be set to null by CloseAsync on another thread
+		if (m_session is not { } session || State != ConnectionState.Open || !session.TryStartCancel(command))
 		{
-			Log.Trace("Ignoring cancellation for closed connection or invalid CommandId {0}", commandId);
+			Log.IgnoringCancellationForCommand(m_logger, commandId);
 			return;
 		}
 
-		Log.Debug("CommandId {0} for Session{1} has been canceled via {2}.", commandId, sessionId, isCancel ? "Cancel()" : "command timeout");
-
+		Log.CommandHasBeenCanceled(m_logger, commandId, session.Id, isCancel ? "Cancel()" : "command timeout");
 		try
 		{
 			// open a dedicated connection to the server to kill the active query
@@ -767,12 +909,32 @@ internal void Cancel(ICancellableCommand command, int commandId, bool isCancel)
 				AutoEnlist = false,
 				Pooling = false,
 			};
-			if (m_session?.IPAddress is { } ipAddress)
-				csb.Server = ipAddress.ToString();
+
+			// connect directly to the session's IP address to ensure we're cancelling the query on the right server (in a load-balanced scenario)
+			IPAddress? ipAddress = null;
+			if (session.IPEndPoint is { Address: { } sessionIpAddress, Port: { } port })
+			{
+				// set the hostname to the existing session's hostname (for SSL validation)
+				csb.Server = session.HostName;
+				csb.Port = (uint) port;
+				ipAddress = sessionIpAddress;
+			}
+			csb.UserID = session.UserID;
 			var cancellationTimeout = GetConnectionSettings().CancellationTimeout;
 			csb.ConnectionTimeout = cancellationTimeout < 1 ? 3u : (uint) cancellationTimeout;
 
+			// forcibly set the IP address the new connection should use
+			var connectionSettings = new ConnectionSettings(csb);
+			if (ipAddress is not null)
+				connectionSettings = connectionSettings.CloneWith(ipAddress);
+
 			using var connection = CloneWith(csb.ConnectionString);
+			connection.m_connectionSettings = connectionSettings;
+
+			// clear the callback because this is not intended to be a user-visible MySqlConnection that will execute setup logic; it's a
+			// non-pooled connection that will execute "KILL QUERY" then immediately be closed
+			connection.ConnectionOpenedCallback = null;
+
 			connection.Open();
 #if NET6_0_OR_GREATER
 			var killQuerySql = string.Create(CultureInfo.InvariantCulture, $"KILL QUERY {command.Connection!.ServerThread}");
@@ -781,41 +943,40 @@ internal void Cancel(ICancellableCommand command, int commandId, bool isCancel)
 #endif
 			using var killCommand = new MySqlCommand(killQuerySql, connection);
 			killCommand.CommandTimeout = cancellationTimeout < 1 ? 3 : cancellationTimeout;
-			m_session?.DoCancel(command, killCommand);
+			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);
+			Log.IgnoringCancellationForClosedConnection(m_logger, ex, session.Id);
+			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);
+			Log.CancelingCommandFailed(m_logger, ex, session.Id, command.CommandId);
+			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);
+		Log.GettingCachedProcedure(m_logger, 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();
+			Log.PoolDoesNotHaveSharedProcedureCache(m_logger, m_session.Id, m_session.Pool?.Id);
+			cachedProcedures = m_cachedProcedures = [];
 		}
 
 		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);
+			Log.CouldNotNormalizeDatabaseAndName(m_logger, m_session.Id, name, Database);
 			return null;
 		}
 
@@ -825,35 +986,31 @@ internal void Cancel(ICancellableCommand command, int commandId, bool isCancel)
 			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);
-			}
+			cachedProcedure = await CachedProcedure.FillAsync(ioBehavior, this, normalized.Schema!, normalized.Component!, m_logger, cancellationToken).ConfigureAwait(false);
+			if (cachedProcedure is null)
+				Log.FailedToCacheProcedure(m_logger, m_session.Id, normalized.Schema!, normalized.Component!);
+			else
+				Log.CachingProcedure(m_logger, 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);
+			Log.ProcedureCacheCount(m_logger, 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);
-		}
+		if (cachedProcedure is null)
+			Log.DidNotFindCachedProcedure(m_logger, m_session.Id, normalized.Schema!, normalized.Component!);
+		else
+			Log.ReturningCachedProcedure(m_logger, m_session.Id, normalized.Schema!, normalized.Component!);
 		return cachedProcedure;
 	}
 
 	internal MySqlTransaction? CurrentTransaction { get; set; }
+	internal MySqlConnectorLoggingConfiguration LoggingConfiguration { get; }
+	internal ZstandardPlugin? ZstandardPlugin { get; set; }
+	internal MySqlConnectionOpenedCallback? ConnectionOpenedCallback { get; set; }
 	internal bool AllowLoadLocalInfile => GetInitializedConnectionSettings().AllowLoadLocalInfile;
 	internal bool AllowUserVariables => GetInitializedConnectionSettings().AllowUserVariables;
 	internal bool AllowZeroDateTime => GetInitializedConnectionSettings().AllowZeroDateTime;
@@ -874,12 +1031,17 @@ internal void Cancel(ICancellableCommand command, int commandId, bool isCancel)
 
 	internal int? ActiveCommandId => m_session?.ActiveCommandId;
 
+	internal bool SupportsPerQueryVariables => m_session?.SupportsPerQueryVariables ?? false;
 	internal bool HasActiveReader => m_activeReader is not null;
 
 	internal void SetActiveReader(MySqlDataReader dataReader)
 	{
+#if NET6_0_OR_GREATER
+		ArgumentNullException.ThrowIfNull(dataReader);
+#else
 		if (dataReader is null)
 			throw new ArgumentNullException(nameof(dataReader));
+#endif
 		if (m_activeReader is not null)
 			throw new InvalidOperationException("Can't replace active reader.");
 		m_activeReader = dataReader;
@@ -905,8 +1067,9 @@ internal void FinishQuerying(bool hasWarnings)
 		}
 	}
 
-	private async ValueTask<ServerSession> CreateSessionAsync(ConnectionPool? pool, int startTickCount, Activity? activity, IOBehavior? ioBehavior, CancellationToken cancellationToken)
+	private async ValueTask<ServerSession> CreateSessionAsync(ConnectionPool? pool, long startingTimestamp, Activity? activity, IOBehavior? ioBehavior, CancellationToken cancellationToken)
 	{
+		MetricsReporter.AddPendingRequest(pool);
 		var connectionSettings = GetInitializedConnectionSettings();
 		var actualIOBehavior = ioBehavior ?? (connectionSettings.ForceSynchronous ? IOBehavior.Synchronous : IOBehavior.Asynchronous);
 
@@ -917,7 +1080,7 @@ private async ValueTask<ServerSession> CreateSessionAsync(ConnectionPool? pool,
 			// 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))));
+				timeoutSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(Math.Max(1, connectionSettings.ConnectionTimeoutMilliseconds - Utility.GetElapsedMilliseconds(startingTimestamp))));
 			if (cancellationToken.CanBeCanceled && timeoutSource is not null)
 				linkedSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutSource.Token);
 			var connectToken = linkedSource?.Token ?? timeoutSource?.Token ?? cancellationToken;
@@ -926,40 +1089,38 @@ private async ValueTask<ServerSession> CreateSessionAsync(ConnectionPool? pool,
 			if (pool is not null)
 			{
 				// this returns an open session
-				return await pool.GetSessionAsync(this, startTickCount, connectionSettings.ConnectionTimeoutMilliseconds, activity, actualIOBehavior, connectToken).ConfigureAwait(false);
+				return await pool.GetSessionAsync(this, startingTimestamp, connectionSettings.ConnectionTimeoutMilliseconds, activity, 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();
+				var session = await ServerSession.ConnectAndRedirectAsync(m_logger, m_logger, NonPooledConnectionPoolMetadata.Instance, connectionSettings, loadBalancer, this, null, startingTimestamp, null, actualIOBehavior, connectToken).ConfigureAwait(false);
 				session.OwningConnection = new WeakReference<MySqlConnection>(this);
-				Log.Debug("Created new non-pooled Session{0}", session.Id);
-				try
-				{
-					await session.ConnectAsync(connectionSettings, this, startTickCount, loadBalancer, activity, actualIOBehavior, connectToken).ConfigureAwait(false);
-					return session;
-				}
-				catch (Exception)
-				{
-					await session.DisposeAsync(actualIOBehavior, default).ConfigureAwait(false);
-					throw;
-				}
+				Log.CreatedNonPooledSession(m_logger, session.Id);
+				return session;
 			}
 		}
 		catch (OperationCanceledException) when (timeoutSource?.IsCancellationRequested is true)
 		{
+			MetricsReporter.AddTimeout(pool, connectionSettings);
 			var messageSuffix = (pool?.IsEmpty is true) ? " All pooled connections are in use." : "";
 			throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Connect Timeout expired." + messageSuffix);
 		}
 		catch (MySqlException ex) when ((timeoutSource?.IsCancellationRequested is true) || (ex.ErrorCode == MySqlErrorCode.CommandTimeoutExpired))
 		{
+			MetricsReporter.AddTimeout(pool, connectionSettings);
 			throw new MySqlException(MySqlErrorCode.UnableToConnectToHost, "Connect Timeout expired.", ex);
 		}
+		catch (MySqlException ex) when (ex.ErrorCode == MySqlErrorCode.UnableToConnectToHost && ex.Message == "Connect Timeout expired.")
+		{
+			MetricsReporter.AddTimeout(pool, connectionSettings);
+			throw;
+		}
 		finally
 		{
+			MetricsReporter.RemovePendingRequest(pool);
 			linkedSource?.Dispose();
 			timeoutSource?.Dispose();
 		}
@@ -975,6 +1136,10 @@ private async ValueTask<ServerSession> CreateSessionAsync(ConnectionPool? pool,
 
 	internal SslProtocols SslProtocol => m_session!.SslProtocol;
 
+	internal IPEndPoint? SessionEndPoint => m_session!.IPEndPoint;
+
+	internal MySqlDataSource? MySqlDataSource => m_dataSource;
+
 	internal void SetState(ConnectionState newState)
 	{
 		if (m_connectionState != newState)
@@ -990,16 +1155,25 @@ internal void SetState(ConnectionState newState)
 		}
 	}
 
-	private MySqlConnection(string connectionString, bool hasBeenOpened)
-		: this(connectionString)
+	private MySqlConnection(MySqlConnection other, MySqlDataSource? dataSource, string connectionString, bool hasBeenOpened)
+		: this(connectionString, other.LoggingConfiguration)
 	{
+		m_dataSource = dataSource;
 		m_hasBeenOpened = hasBeenOpened;
+		ProvideClientCertificatesCallback = other.ProvideClientCertificatesCallback;
+		ProvidePasswordCallback = other.ProvidePasswordCallback;
+		RemoteCertificateValidationCallback = other.RemoteCertificateValidationCallback;
+		ConnectionOpenedCallback = other.ConnectionOpenedCallback;
 	}
 
 	private void VerifyNotDisposed()
 	{
+#if NET7_0_OR_GREATER
+		ObjectDisposedException.ThrowIf(m_isDisposed, this);
+#else
 		if (m_isDisposed)
 			throw new ObjectDisposedException(GetType().Name);
+#endif
 	}
 
 	private async Task CloseAsync(bool changeState, IOBehavior ioBehavior)
@@ -1039,12 +1213,10 @@ private async Task DoCloseAsync(bool changeState, IOBehavior ioBehavior)
 			// This connection is being closed, so create a new MySqlConnection that will own the ServerSession
 			// (which remains open). This ensures the ServerSession always has a valid OwningConnection (even
 			// if 'this' is GCed.
-			var connection = new MySqlConnection
+			var connection = new MySqlConnection(this, m_dataSource, m_connectionString, hasBeenOpened: true)
 			{
-				m_connectionString = m_connectionString,
 				m_connectionSettings = m_connectionSettings,
 				m_connectionState = m_connectionState,
-				m_hasBeenOpened = true,
 			};
 			connection.TakeSessionFrom(this);
 
@@ -1078,6 +1250,9 @@ private async Task DoCloseAsync(bool changeState, IOBehavior ioBehavior)
 		{
 			if (m_session is not null)
 			{
+				// under exceptional circumstances, we may enter this code without having called FinishQuerying, which clears this field
+				m_activeReader = null;
+
 				if (GetInitializedConnectionSettings().Pooling)
 				{
 					await m_session.ReturnToPoolAsync(ioBehavior, this).ConfigureAwait(false);
@@ -1112,16 +1287,20 @@ private ConnectionSettings GetConnectionSettings() =>
 	// This method may be called when it's known that the connection settings have been initialized.
 	private ConnectionSettings GetInitializedConnectionSettings() => m_connectionSettings!;
 
-	private static readonly IMySqlConnectorLogger Log = MySqlConnectorLogManager.CreateLogger(nameof(MySqlConnection));
 	private static readonly StateChangeEventArgs s_stateChangeClosedConnecting = new(ConnectionState.Closed, ConnectionState.Connecting);
 	private static readonly StateChangeEventArgs s_stateChangeConnectingOpen = new(ConnectionState.Connecting, ConnectionState.Open);
 	private static readonly StateChangeEventArgs s_stateChangeOpenClosed = new(ConnectionState.Open, ConnectionState.Closed);
+#if NET9_0_OR_GREATER
+	private static readonly Lock s_lock = new();
+#else
 	private static readonly object s_lock = new();
-	private static readonly Dictionary<System.Transactions.Transaction, List<EnlistedTransactionBase>> s_transactionConnections = new();
+#endif
+	private static readonly Dictionary<System.Transactions.Transaction, List<EnlistedTransactionBase>> s_transactionConnections = [];
+	private static readonly ReadOnlyMemory<byte>[] s_startTransactionPayloads = new ReadOnlyMemory<byte>[5 * 3 * 2];
 
-#if NET7_0_OR_GREATER
 	private readonly MySqlDataSource? m_dataSource;
-#endif
+	private readonly ILogger m_logger;
+	private readonly ILogger m_transactionLogger;
 	private string m_connectionString;
 	private ConnectionSettings? m_connectionSettings;
 	private ServerSession? m_session;
diff --git a/src/MySqlConnector/MySqlConnectionOpenedCallback.cs b/src/MySqlConnector/MySqlConnectionOpenedCallback.cs
new file mode 100644
index 000000000..c1b9c3941
--- /dev/null
+++ b/src/MySqlConnector/MySqlConnectionOpenedCallback.cs
@@ -0,0 +1,9 @@
+namespace MySqlConnector;
+
+/// <summary>
+/// A callback that is invoked when a new <see cref="MySqlConnection"/> is opened.
+/// </summary>
+/// <param name="context">A <see cref="MySqlConnectionOpenedContext"/> giving information about the connection being opened.</param>
+/// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used to cancel the asynchronous operation.</param>
+/// <returns>A <see cref="ValueTask"/> representing the result of the possibly-asynchronous operation.</returns>
+public delegate ValueTask MySqlConnectionOpenedCallback(MySqlConnectionOpenedContext context, CancellationToken cancellationToken);
diff --git a/src/MySqlConnector/MySqlConnectionOpenedConditions.cs b/src/MySqlConnector/MySqlConnectionOpenedConditions.cs
new file mode 100644
index 000000000..49c4c670a
--- /dev/null
+++ b/src/MySqlConnector/MySqlConnectionOpenedConditions.cs
@@ -0,0 +1,23 @@
+﻿namespace MySqlConnector;
+
+/// <summary>
+/// Bitflags giving the conditions under which a connection was opened.
+/// </summary>
+[Flags]
+public enum MySqlConnectionOpenedConditions
+{
+	/// <summary>
+	/// No specific conditions apply. This value may be used when an existing pooled connection is reused without being reset.
+	/// </summary>
+	None = 0,
+
+	/// <summary>
+	/// A new physical connection to a MySQL Server was opened. This value is mutually exclusive with <see cref="Reset"/>.
+	/// </summary>
+	New = 1,
+
+	/// <summary>
+	/// An existing pooled connection to a MySQL Server was reset. This value is mutually exclusive with <see cref="New"/>.
+	/// </summary>
+	Reset = 2,
+}
diff --git a/src/MySqlConnector/MySqlConnectionOpenedContext.cs b/src/MySqlConnector/MySqlConnectionOpenedContext.cs
new file mode 100644
index 000000000..46fe9adf3
--- /dev/null
+++ b/src/MySqlConnector/MySqlConnectionOpenedContext.cs
@@ -0,0 +1,23 @@
+namespace MySqlConnector;
+
+/// <summary>
+/// Contains information passed to <see cref="MySqlConnectionOpenedCallback"/> when a new <see cref="MySqlConnection"/> is opened.
+/// </summary>
+public sealed class MySqlConnectionOpenedContext
+{
+	/// <summary>
+	/// The <see cref="MySqlConnection"/> that was opened.
+	/// </summary>
+	public MySqlConnection Connection { get; }
+
+	/// <summary>
+	/// Bitflags giving the conditions under which a connection was opened.
+	/// </summary>
+	public MySqlConnectionOpenedConditions Conditions { get; }
+
+	internal MySqlConnectionOpenedContext(MySqlConnection connection, MySqlConnectionOpenedConditions conditions)
+	{
+		Connection = connection;
+		Conditions = conditions;
+	}
+}
diff --git a/src/MySqlConnector/MySqlConnectionStringBuilder.cs b/src/MySqlConnector/MySqlConnectionStringBuilder.cs
index 241187008..762bcc639 100644
--- a/src/MySqlConnector/MySqlConnectionStringBuilder.cs
+++ b/src/MySqlConnector/MySqlConnectionStringBuilder.cs
@@ -1,10 +1,11 @@
 using System.Collections;
+#if NET8_0_OR_GREATER
+using System.Collections.Frozen;
+#endif
 using System.ComponentModel;
 using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
-using System.Linq;
 using System.Text.RegularExpressions;
-using MySqlConnector.Utilities;
 
 namespace MySqlConnector;
 
@@ -707,8 +708,8 @@ public bool Pipelining
 	}
 
 	/// <summary>
-		/// Whether to use server redirection.
-		/// </summary>
+	/// Whether to use server redirection.
+	/// </summary>
 	[Category("Connection")]
 	[DefaultValue(MySqlServerRedirectionMode.Disabled)]
 	[Description("Whether to use server redirection.")]
@@ -804,28 +805,22 @@ public bool UseXaTransactions
 	/// <summary>
 	/// Returns an <see cref="ICollection"/> that contains the keys in the <see cref="MySqlConnectionStringBuilder"/>.
 	/// </summary>
-	public override ICollection Keys => base.Keys.Cast<string>().OrderBy(x => MySqlConnectionStringOption.OptionNames.IndexOf(x)).ToList();
+	public override ICollection Keys => base.Keys.Cast<string>().OrderBy(static x => MySqlConnectionStringOption.OptionNames.IndexOf(x)).ToList();
 
 	/// <summary>
 	/// Whether this <see cref="MySqlConnectionStringBuilder"/> contains a set option with the specified name.
 	/// </summary>
 	/// <param name="keyword">The option name.</param>
 	/// <returns><c>true</c> if an option with that name is set; otherwise, <c>false</c>.</returns>
-	public override bool ContainsKey(string keyword)
-	{
-		var option = MySqlConnectionStringOption.TryGetOptionForKey(keyword);
-		return option is object && base.ContainsKey(option.Key);
-	}
+	public override bool ContainsKey(string keyword) =>
+		MySqlConnectionStringOption.TryGetOptionForKey(keyword) is { } option && base.ContainsKey(option.Key);
 
 	/// <summary>
 	/// Removes the option with the specified name.
 	/// </summary>
 	/// <param name="keyword">The option name.</param>
-	public override bool Remove(string keyword)
-	{
-		var option = MySqlConnectionStringOption.TryGetOptionForKey(keyword);
-		return option is object && base.Remove(option.Key);
-	}
+	public override bool Remove(string keyword) =>
+		MySqlConnectionStringOption.TryGetOptionForKey(keyword) is { } option && base.Remove(option.Key);
 
 	/// <summary>
 	/// Retrieves an option value by name.
@@ -876,6 +871,9 @@ internal string GetConnectionString(bool includePassword)
 	/// Fills in <paramref name="propertyDescriptors"/> with information about the available properties on this object.
 	/// </summary>
 	/// <param name="propertyDescriptors">The collection of <see cref="PropertyDescriptor"/> objects to populate.</param>
+#if NET6_0_OR_GREATER
+	[RequiresUnreferencedCode("PropertyDescriptor's PropertyType cannot be statically discovered.")]
+#endif
 	protected override void GetProperties(Hashtable propertyDescriptors)
 	{
 		base.GetProperties(propertyDescriptors);
@@ -895,7 +893,7 @@ protected override void GetProperties(Hashtable propertyDescriptors)
 
 internal abstract partial class MySqlConnectionStringOption
 {
-	public static List<string> OptionNames { get; } = new();
+	public static List<string> OptionNames { get; } = [];
 
 	// Connection Options
 	public static readonly MySqlConnectionStringReferenceOption<string> Server;
@@ -978,10 +976,10 @@ protected MySqlConnectionStringOption(IReadOnlyList<string> keys)
 		m_keys = keys;
 	}
 
-	private static void AddOption(MySqlConnectionStringOption option)
+	private static void AddOption(Dictionary<string, MySqlConnectionStringOption> options, MySqlConnectionStringOption option)
 	{
-		foreach (string key in option.m_keys)
-			s_options.Add(key, option);
+		foreach (var key in option.m_keys)
+			options.Add(key, option);
 		OptionNames.Add(option.m_keys[0]);
 	}
 
@@ -989,76 +987,77 @@ private static void AddOption(MySqlConnectionStringOption option)
 #pragma warning disable CA1810 // Initialize reference type static fields inline
 	static MySqlConnectionStringOption()
 	{
-		s_options = new(StringComparer.OrdinalIgnoreCase);
+		var options = new Dictionary<string, MySqlConnectionStringOption>(StringComparer.OrdinalIgnoreCase);
 
 		// Base Options
-		AddOption(Server = new(
-			keys: new[] { "Server", "Host", "Data Source", "DataSource", "Address", "Addr", "Network Address" },
+#pragma warning disable SA1118 // Parameter should not span multiple lines
+		AddOption(options, Server = new(
+			keys: ["Server", "Host", "Data Source", "DataSource", "Address", "Addr", "Network Address"],
 			defaultValue: ""));
 
-		AddOption(Port = new(
-			keys: new[] { "Port" },
+		AddOption(options, Port = new(
+			keys: ["Port"],
 			defaultValue: 3306u));
 
-		AddOption(UserID = new(
-			keys: new[] { "User ID", "UserID", "Username", "Uid", "User name", "User" },
+		AddOption(options, UserID = new(
+			keys: ["User ID", "UserID", "Username", "Uid", "User name", "User"],
 			defaultValue: ""));
 
-		AddOption(Password = new(
-			keys: new[] { "Password", "pwd" },
+		AddOption(options, Password = new(
+			keys: ["Password", "pwd"],
 			defaultValue: ""));
 
-		AddOption(Database = new(
-			keys: new[] { "Database", "Initial Catalog" },
+		AddOption(options, Database = new(
+			keys: ["Database", "Initial Catalog"],
 			defaultValue: ""));
 
-		AddOption(LoadBalance = new(
-			keys: new[] { "Load Balance", "LoadBalance" },
+		AddOption(options, LoadBalance = new(
+			keys: ["Load Balance", "LoadBalance"],
 			defaultValue: MySqlLoadBalance.RoundRobin));
 
-		AddOption(ConnectionProtocol = new(
-			keys: new[] { "Connection Protocol", "ConnectionProtocol", "Protocol" },
+		AddOption(options, ConnectionProtocol = new(
+			keys: ["Connection Protocol", "ConnectionProtocol", "Protocol"],
 			defaultValue: MySqlConnectionProtocol.Socket));
 
-		AddOption(PipeName = new(
-			keys: new[] { "Pipe Name", "PipeName", "Pipe" },
+		AddOption(options, PipeName = new(
+			keys: ["Pipe Name", "PipeName", "Pipe"],
 			defaultValue: "MYSQL"));
 
 		// SSL/TLS Options
-		AddOption(SslMode = new(
-			keys: new[] { "SSL Mode", "SslMode" },
+		AddOption(options, SslMode = new(
+			keys: ["SSL Mode", "SslMode"],
 			defaultValue: MySqlSslMode.Preferred));
 
-		AddOption(CertificateFile = new(
-			keys: new[] { "Certificate File", "CertificateFile" },
+		AddOption(options, CertificateFile = new(
+			keys: ["Certificate File", "CertificateFile"],
 			defaultValue: ""));
 
-		AddOption(CertificatePassword = new(
-			keys: new[] { "Certificate Password", "CertificatePassword" },
+		AddOption(options, CertificatePassword = new(
+			keys: ["Certificate Password", "CertificatePassword"],
 			defaultValue: ""));
 
-		AddOption(CertificateStoreLocation = new(
-			keys: new[] { "Certificate Store Location", "CertificateStoreLocation" },
+		AddOption(options, CertificateStoreLocation = new(
+			keys: ["Certificate Store Location", "CertificateStoreLocation"],
 			defaultValue: MySqlCertificateStoreLocation.None));
 
-		AddOption(CertificateThumbprint = new(
-			keys: new[] { "Certificate Thumbprint", "CertificateThumbprint", "Certificate Thumb Print" },
+		AddOption(options, CertificateThumbprint = new(
+			keys: ["Certificate Thumbprint", "CertificateThumbprint", "Certificate Thumb Print"],
 			defaultValue: ""));
 
-		AddOption(SslCert = new(
-			keys: new[] { "SSL Cert", "SslCert", "Ssl-Cert" },
+		AddOption(options, SslCert = new(
+			keys: ["SSL Cert", "SslCert", "Ssl-Cert"],
 			defaultValue: ""));
 
-		AddOption(SslKey = new(
-			keys: new[] { "SSL Key", "SslKey", "Ssl-Key" },
+		AddOption(options, SslKey = new(
+			keys: ["SSL Key", "SslKey", "Ssl-Key"],
 			defaultValue: ""));
 
-		AddOption(SslCa = new(
-			keys: new[] { "SSL CA", "CACertificateFile", "CA Certificate File", "SslCa", "Ssl-Ca" },
+		AddOption(options, SslCa = new(
+			keys: ["SSL CA", "CACertificateFile", "CA Certificate File", "SslCa", "Ssl-Ca"],
 			defaultValue: ""));
 
-		AddOption(TlsVersion = new(
-			keys: new[] { "TLS Version", "TlsVersion", "Tls-Version" },
+		AddOption(options, TlsVersion = new(
+			keys: ["TLS Version", "TlsVersion", "Tls-Version"],
 			defaultValue: "",
 			coerce: value =>
 			{
@@ -1100,169 +1099,176 @@ static MySqlConnectionStringOption()
 				return coercedValue;
 			}));
 
-		AddOption(TlsCipherSuites = new(
-			keys: new[] { "TLS Cipher Suites", "TlsCipherSuites" },
+		AddOption(options, TlsCipherSuites = new(
+			keys: ["TLS Cipher Suites", "TlsCipherSuites"],
 			defaultValue: ""));
 
 		// Connection Pooling Options
-		AddOption(Pooling = new(
-			keys: new[] { "Pooling" },
+		AddOption(options, Pooling = new(
+			keys: ["Pooling"],
 			defaultValue: true));
 
-		AddOption(ConnectionLifeTime = new(
-			keys: new[] { "Connection Lifetime", "ConnectionLifeTime" },
+		AddOption(options, ConnectionLifeTime = new(
+			keys: ["Connection Lifetime", "ConnectionLifeTime"],
 			defaultValue: 0u));
 
-		AddOption(ConnectionReset = new(
-			keys: new[] { "Connection Reset", "ConnectionReset" },
+		AddOption(options, ConnectionReset = new(
+			keys: ["Connection Reset", "ConnectionReset"],
 			defaultValue: true));
 
-		AddOption(DeferConnectionReset = new(
-			keys: new[] { "Defer Connection Reset", "DeferConnectionReset" },
+		AddOption(options, DeferConnectionReset = new(
+			keys: ["Defer Connection Reset", "DeferConnectionReset"],
 			defaultValue: true));
 
-		AddOption(ConnectionIdlePingTime = new(
-			keys: new[] { "Connection Idle Ping Time", "ConnectionIdlePingTime" },
+		AddOption(options, ConnectionIdlePingTime = new(
+			keys: ["Connection Idle Ping Time", "ConnectionIdlePingTime"],
 			defaultValue: 0u));
 
-		AddOption(ConnectionIdleTimeout = new(
-			keys: new[] { "Connection Idle Timeout", "ConnectionIdleTimeout" },
+		AddOption(options, ConnectionIdleTimeout = new(
+			keys: ["Connection Idle Timeout", "ConnectionIdleTimeout"],
 			defaultValue: 180u));
 
-		AddOption(MinimumPoolSize = new(
-			keys: new[] { "Minimum Pool Size", "Min Pool Size", "MinimumPoolSize", "minpoolsize" },
+		AddOption(options, MinimumPoolSize = new(
+			keys: ["Minimum Pool Size", "Min Pool Size", "MinimumPoolSize", "minpoolsize"],
 			defaultValue: 0u));
 
-		AddOption(MaximumPoolSize = new(
-			keys: new[] { "Maximum Pool Size", "Max Pool Size", "MaximumPoolSize", "maxpoolsize" },
+		AddOption(options, MaximumPoolSize = new(
+			keys: ["Maximum Pool Size", "Max Pool Size", "MaximumPoolSize", "maxpoolsize"],
 			defaultValue: 100u));
 
-		AddOption(DnsCheckInterval = new(
-			keys: new[] { "DNS Check Interval", "DnsCheckInterval" },
+		AddOption(options, DnsCheckInterval = new(
+			keys: ["DNS Check Interval", "DnsCheckInterval"],
 			defaultValue: 0u));
 
 		// Other Options
-		AddOption(AllowLoadLocalInfile = new(
-			keys: new[] { "Allow Load Local Infile", "AllowLoadLocalInfile" },
+		AddOption(options, AllowLoadLocalInfile = new(
+			keys: ["Allow Load Local Infile", "AllowLoadLocalInfile"],
 			defaultValue: false));
 
-		AddOption(AllowPublicKeyRetrieval = new(
-			keys: new[] { "Allow Public Key Retrieval", "AllowPublicKeyRetrieval" },
+		AddOption(options, AllowPublicKeyRetrieval = new(
+			keys: ["Allow Public Key Retrieval", "AllowPublicKeyRetrieval"],
 			defaultValue: false));
 
-		AddOption(AllowUserVariables = new(
-			keys: new[] { "Allow User Variables", "AllowUserVariables" },
+		AddOption(options, AllowUserVariables = new(
+			keys: ["Allow User Variables", "AllowUserVariables"],
 			defaultValue: false));
 
-		AddOption(AllowZeroDateTime = new(
-			keys: new[] { "Allow Zero DateTime", "AllowZeroDateTime" },
+		AddOption(options, AllowZeroDateTime = new(
+			keys: ["Allow Zero DateTime", "AllowZeroDateTime"],
 			defaultValue: false));
 
-		AddOption(ApplicationName = new(
-			keys: new[] { "Application Name", "ApplicationName" },
+		AddOption(options, ApplicationName = new(
+			keys: ["Application Name", "ApplicationName"],
 			defaultValue: ""));
 
-		AddOption(AutoEnlist = new(
-			keys: new[] { "Auto Enlist", "AutoEnlist" },
+		AddOption(options, AutoEnlist = new(
+			keys: ["Auto Enlist", "AutoEnlist"],
 			defaultValue: true));
 
-		AddOption(CancellationTimeout = new(
-			keys: new[] { "Cancellation Timeout", "CancellationTimeout" },
+		AddOption(options, CancellationTimeout = new(
+			keys: ["Cancellation Timeout", "CancellationTimeout"],
 			defaultValue: 2,
-			coerce: x =>
+			coerce: static x =>
 			{
 				if (x < -1)
 					throw new ArgumentOutOfRangeException(nameof(CancellationTimeout), "CancellationTimeout must be greater than or equal to -1");
 				return x;
 			}));
 
-		AddOption(CharacterSet = new(
-			keys: new[] { "Character Set", "CharSet", "CharacterSet" },
+		AddOption(options, CharacterSet = new(
+			keys: ["Character Set", "CharSet", "CharacterSet"],
 			defaultValue: ""));
 
-		AddOption(ConnectionTimeout = new(
-			keys: new[] { "Connection Timeout", "ConnectionTimeout", "Connect Timeout" },
+		AddOption(options, ConnectionTimeout = new(
+			keys: ["Connection Timeout", "ConnectionTimeout", "Connect Timeout"],
 			defaultValue: 15u));
 
-		AddOption(ConvertZeroDateTime = new(
-			keys: new[] { "Convert Zero DateTime", "ConvertZeroDateTime" },
+		AddOption(options, ConvertZeroDateTime = new(
+			keys: ["Convert Zero DateTime", "ConvertZeroDateTime"],
 			defaultValue: false));
 
-		AddOption(DateTimeKind = new(
-			keys: new[] { "DateTime Kind", "DateTimeKind" },
+		AddOption(options, DateTimeKind = new(
+			keys: ["DateTime Kind", "DateTimeKind"],
 			defaultValue: MySqlDateTimeKind.Unspecified));
 
-		AddOption(DefaultCommandTimeout = new(
-			keys: new[] { "Default Command Timeout", "DefaultCommandTimeout", "Command Timeout" },
+		AddOption(options, DefaultCommandTimeout = new(
+			keys: ["Default Command Timeout", "DefaultCommandTimeout", "Command Timeout"],
 			defaultValue: 30u));
 
-		AddOption(ForceSynchronous = new(
-			keys: new[] { "Force Synchronous", "ForceSynchronous" },
+		AddOption(options, ForceSynchronous = new(
+			keys: ["Force Synchronous", "ForceSynchronous"],
 			defaultValue: false));
 
-		AddOption(GuidFormat = new(
-			keys: new[] { "GUID Format", "GuidFormat" },
+		AddOption(options, GuidFormat = new(
+			keys: ["GUID Format", "GuidFormat"],
 			defaultValue: MySqlGuidFormat.Default));
 
-		AddOption(IgnoreCommandTransaction = new(
-			keys: new[] { "Ignore Command Transaction", "IgnoreCommandTransaction" },
+		AddOption(options, IgnoreCommandTransaction = new(
+			keys: ["Ignore Command Transaction", "IgnoreCommandTransaction"],
 			defaultValue: false));
 
-		AddOption(IgnorePrepare = new(
-			keys: new[] { "Ignore Prepare", "IgnorePrepare" },
+		AddOption(options, IgnorePrepare = new(
+			keys: ["Ignore Prepare", "IgnorePrepare"],
 			defaultValue: false));
 
-		AddOption(InteractiveSession = new(
-			keys: new[] { "Interactive Session", "InteractiveSession", "Interactive" },
+		AddOption(options, InteractiveSession = new(
+			keys: ["Interactive Session", "InteractiveSession", "Interactive"],
 			defaultValue: false));
 
-		AddOption(Keepalive = new(
-			keys: new[] { "Keep Alive", "Keepalive" },
+		AddOption(options, Keepalive = new(
+			keys: ["Keep Alive", "Keepalive"],
 			defaultValue: 0u));
 
-		AddOption(NoBackslashEscapes = new(
-			keys: new[] { "No Backslash Escapes", "NoBackslashEscapes" },
+		AddOption(options, NoBackslashEscapes = new(
+			keys: ["No Backslash Escapes", "NoBackslashEscapes"],
 			defaultValue: false));
 
-		AddOption(OldGuids = new(
-			keys: new[] { "Old Guids", "OldGuids" },
+		AddOption(options, OldGuids = new(
+			keys: ["Old Guids", "OldGuids"],
 			defaultValue: false));
 
-		AddOption(PersistSecurityInfo = new(
-			keys: new[] { "Persist Security Info", "PersistSecurityInfo" },
+		AddOption(options, PersistSecurityInfo = new(
+			keys: ["Persist Security Info", "PersistSecurityInfo"],
 			defaultValue: false));
 
-		AddOption(Pipelining = new(
-			keys: new[] { "Pipelining" },
+		AddOption(options, Pipelining = new(
+			keys: ["Pipelining"],
 			defaultValue: true));
 
-		AddOption(ServerRedirectionMode = new(
-			keys: new[] { "Server Redirection Mode", "ServerRedirectionMode" },
+		AddOption(options, ServerRedirectionMode = new(
+			keys: ["Server Redirection Mode", "ServerRedirectionMode"],
 			defaultValue: MySqlServerRedirectionMode.Disabled));
 
-		AddOption(ServerRsaPublicKeyFile = new(
-			keys: new[] { "Server RSA Public Key File", "ServerRsaPublicKeyFile" },
+		AddOption(options, ServerRsaPublicKeyFile = new(
+			keys: ["Server RSA Public Key File", "ServerRsaPublicKeyFile"],
 			defaultValue: ""));
 
-		AddOption(ServerSPN = new(
-			keys: new[] { "Server SPN", "ServerSPN" },
+		AddOption(options, ServerSPN = new(
+			keys: ["Server SPN", "ServerSPN"],
 			defaultValue: ""));
 
-		AddOption(TreatTinyAsBoolean = new(
-			keys: new[] { "Treat Tiny As Boolean", "TreatTinyAsBoolean" },
+		AddOption(options, TreatTinyAsBoolean = new(
+			keys: ["Treat Tiny As Boolean", "TreatTinyAsBoolean"],
 			defaultValue: true));
 
-		AddOption(UseAffectedRows = new(
-			keys: new[] { "Use Affected Rows", "UseAffectedRows" },
+		AddOption(options, UseAffectedRows = new(
+			keys: ["Use Affected Rows", "UseAffectedRows"],
 			defaultValue: false));
 
-		AddOption(UseCompression = new(
-			keys: new[] { "Use Compression", "Compress", "UseCompression" },
+		AddOption(options, UseCompression = new(
+			keys: ["Use Compression", "Compress", "UseCompression"],
 			defaultValue: false));
 
-		AddOption(UseXaTransactions = new(
-			keys: new[] { "Use XA Transactions", "UseXaTransactions" },
+		AddOption(options, UseXaTransactions = new(
+			keys: ["Use XA Transactions", "UseXaTransactions"],
 			defaultValue: true));
+#pragma warning restore SA1118 // Parameter should not span multiple lines
+
+#if NET8_0_OR_GREATER
+		s_options = options.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
+#else
+		s_options = options;
+#endif
 	}
 
 	private const string c_tlsVersionsRegexPattern = @"\s*TLS( ?v?(1|1\.?0|1\.?1|1\.?2|1\.?3))?$";
@@ -1273,7 +1279,11 @@ static MySqlConnectionStringOption()
 	private static Regex TlsVersionsRegex() => s_tlsVersionsRegex;
 	private static readonly Regex s_tlsVersionsRegex = new(c_tlsVersionsRegexPattern, RegexOptions.IgnoreCase);
 #endif
+#if NET8_0_OR_GREATER
+	private static readonly FrozenDictionary<string, MySqlConnectionStringOption> s_options;
+#else
 	private static readonly Dictionary<string, MySqlConnectionStringOption> s_options;
+#endif
 
 	private readonly IReadOnlyList<string> m_keys;
 }
diff --git a/src/MySqlConnector/MySqlConnector.csproj b/src/MySqlConnector/MySqlConnector.csproj
index 3148d1cd3..023a6e28e 100644
--- a/src/MySqlConnector/MySqlConnector.csproj
+++ b/src/MySqlConnector/MySqlConnector.csproj
@@ -1,9 +1,9 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFrameworks>net461;net471;netstandard2.0;netstandard2.1;netcoreapp3.1;net6.0;net7.0</TargetFrameworks>
-    <Description>A truly async MySQL ADO.NET provider, supporting MySQL Server, MariaDB, Percona Server, Amazon Aurora, Azure Database for MySQL and more.</Description>
-    <Copyright>Copyright 2016–2023 Bradley Grainger</Copyright>
+    <TargetFrameworks>net462;net471;net48;netstandard2.0;netstandard2.1;net6.0;net8.0;net9.0</TargetFrameworks>
+    <Description>A truly async MySQL ADO.NET provider, supporting MySQL Server, MariaDB, Amazon Aurora, Azure Database for MySQL, Google Cloud SQL, and more.</Description>
+    <Copyright>Copyright 2016–2024 Bradley Grainger</Copyright>
     <Authors>Bradley Grainger</Authors>
     <AssemblyName>MySqlConnector</AssemblyName>
     <PackageId>MySqlConnector</PackageId>
@@ -13,25 +13,31 @@
     <Nullable>enable</Nullable>
     <CheckEolTargetFramework>false</CheckEolTargetFramework>
     <ImplicitUsings>enable</ImplicitUsings>
+    <IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net7.0'))">true</IsAotCompatible>
   </PropertyGroup>
 
-  <ItemGroup Condition=" '$(TargetFramework)' == 'net461' OR '$(TargetFramework)' == 'net471' ">
+  <ItemGroup>
+    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
+  </ItemGroup>
+
+  <ItemGroup Condition=" '$(TargetFramework)' == 'net462' OR '$(TargetFramework)' == 'net471'  OR '$(TargetFramework)' == 'net48' ">
     <Reference Include="System.Transactions" />
     <Using Remove="System.Net.Http" />
   </ItemGroup>
 
-  <ItemGroup Condition=" '$(TargetFramework)' == 'net461' OR '$(TargetFramework)' == 'net471' OR '$(TargetFramework)' == 'netstandard2.0' ">
-    <PackageReference Include="IndexRange" Version="1.0.2" PrivateAssets="All" />
-    <PackageReference Include="System.Buffers" Version="4.5.1" />
-    <PackageReference Include="System.Memory" Version="4.5.5" />
-    <PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
+  <ItemGroup Condition=" '$(TargetFramework)' == 'net462' OR '$(TargetFramework)' == 'net471' OR '$(TargetFramework)' == 'net48' OR '$(TargetFramework)' == 'netstandard2.0' ">
+    <PackageReference Include="IndexRange" PrivateAssets="All" />
+    <PackageReference Include="System.Threading.Tasks.Extensions" />
   </ItemGroup>
 
-  <ItemGroup Condition=" '$(TargetFramework)' == 'net461' OR '$(TargetFramework)' == 'net471' OR '$(TargetFramework)' == 'netstandard2.0' OR '$(TargetFramework)' == 'netstandard2.1' OR '$(TargetFramework)' == 'netcoreapp3.1' ">
-    <PackageReference Include="System.Diagnostics.DiagnosticSource" Version="6.0.0" />
+  <ItemGroup Condition=" '$(TargetFrameworkIdentifier)' != '.NETCoreApp' OR '$(TargetFramework)' == 'net6.0' ">
+    <PackageReference Include="System.Diagnostics.DiagnosticSource" />
   </ItemGroup>
 
   <ItemGroup>
+    <InternalsVisibleTo Include="MySqlConnector.DependencyInjection.Tests" Key="00240000048000001402000006020000002400005253413100100000010001000521c81bf0f0ec7b261bb89bb583611d3767205d542c16c9353e317455acf612d3ec3dd03b77e7e6fda1aa8f15c58576d90dae0fb9f4fd4bd48709ae199b8c771963fa67d70b35f7ed2fbb6c60423935adfae0606716ea6ce31a1fcd56fdb206fc0c3b1205ec6ba56fb20c14c42105a601ddd0bfaea7207d535b29a39ffe82f00880f4f64f86e6bcf26eb5242a133bad9d7a32e3126036b68b13b413ce4097dfc18d9a5b1e494f1aed54dc84d7089fd0d931a49e679fdc7c8f07a5121df38ec27c2c9993a8f8f136b2937849aed32aef7324a5b7e482dc2eb693c7988f6074e82e75a41dd001587be4d79108588b25d40ed9aeb30ff921edaf509c94f71428e48219ba940f5f10c061421dc0c006e09feadec30df20b2d13d02c3ce4ceb32b6fbefd254288d45f3bb2c425b197e19699d7efdfc7aba5dd45b727bc98abd866d2f6e69e33a64e4b5a5ab1e4d749266c7bf285550da9fb036f10eff76b697de9c5ed8de4a3cdbca1174543540bed6c3a95641cfdacbac834896639f8a75ed1fb9cfd9983d83d0b43b76bd3894bd2b3da0dd23d1e0362985217f087acce1a7f56546c214890acae8fc60e27890ff31c38578f85e220342061a1a5c867362a14aafdffa003dc13af064f5f860d1757883ea5237feed3a6228c86200062bd88f5592d5c399ef270a562d458ae8eac5eaa382b5bcc3f64298cc34b4598f0b33d7943b8" />
+    <InternalsVisibleTo Include="MySqlConnector.Encryption" Key="0024000004800000940000000602000000240000525341310004000001000100e57cf9527549be513974d12b6730ff1db99b10a1498bb424ef0a14f0441f7043c5159f51629255107b314c710bf29463e1cfa4585732f560c3ef25207c7dfa57004d28927078a28b0ab7fa534403c18f2d5aaeda506a3d276c26b3bcc62f7fc8efe9aaf3e68b14b839ad10f10f42db834d170c96b1da9fc463231afe73bbaabe" />
+    <InternalsVisibleTo Include="MySqlConnector.Zstandard" Key="0024000004800000940000000602000000240000525341310004000001000100e57cf9527549be513974d12b6730ff1db99b10a1498bb424ef0a14f0441f7043c5159f51629255107b314c710bf29463e1cfa4585732f560c3ef25207c7dfa57004d28927078a28b0ab7fa534403c18f2d5aaeda506a3d276c26b3bcc62f7fc8efe9aaf3e68b14b839ad10f10f42db834d170c96b1da9fc463231afe73bbaabe" />
     <InternalsVisibleTo Include="MySqlConnector.Tests" Key="00240000048000001402000006020000002400005253413100100000010001000521c81bf0f0ec7b261bb89bb583611d3767205d542c16c9353e317455acf612d3ec3dd03b77e7e6fda1aa8f15c58576d90dae0fb9f4fd4bd48709ae199b8c771963fa67d70b35f7ed2fbb6c60423935adfae0606716ea6ce31a1fcd56fdb206fc0c3b1205ec6ba56fb20c14c42105a601ddd0bfaea7207d535b29a39ffe82f00880f4f64f86e6bcf26eb5242a133bad9d7a32e3126036b68b13b413ce4097dfc18d9a5b1e494f1aed54dc84d7089fd0d931a49e679fdc7c8f07a5121df38ec27c2c9993a8f8f136b2937849aed32aef7324a5b7e482dc2eb693c7988f6074e82e75a41dd001587be4d79108588b25d40ed9aeb30ff921edaf509c94f71428e48219ba940f5f10c061421dc0c006e09feadec30df20b2d13d02c3ce4ceb32b6fbefd254288d45f3bb2c425b197e19699d7efdfc7aba5dd45b727bc98abd866d2f6e69e33a64e4b5a5ab1e4d749266c7bf285550da9fb036f10eff76b697de9c5ed8de4a3cdbca1174543540bed6c3a95641cfdacbac834896639f8a75ed1fb9cfd9983d83d0b43b76bd3894bd2b3da0dd23d1e0362985217f087acce1a7f56546c214890acae8fc60e27890ff31c38578f85e220342061a1a5c867362a14aafdffa003dc13af064f5f860d1757883ea5237feed3a6228c86200062bd88f5592d5c399ef270a562d458ae8eac5eaa382b5bcc3f64298cc34b4598f0b33d7943b8" />
     <InternalsVisibleTo Include="IntegrationTests" Key="00240000048000001402000006020000002400005253413100100000010001000521c81bf0f0ec7b261bb89bb583611d3767205d542c16c9353e317455acf612d3ec3dd03b77e7e6fda1aa8f15c58576d90dae0fb9f4fd4bd48709ae199b8c771963fa67d70b35f7ed2fbb6c60423935adfae0606716ea6ce31a1fcd56fdb206fc0c3b1205ec6ba56fb20c14c42105a601ddd0bfaea7207d535b29a39ffe82f00880f4f64f86e6bcf26eb5242a133bad9d7a32e3126036b68b13b413ce4097dfc18d9a5b1e494f1aed54dc84d7089fd0d931a49e679fdc7c8f07a5121df38ec27c2c9993a8f8f136b2937849aed32aef7324a5b7e482dc2eb693c7988f6074e82e75a41dd001587be4d79108588b25d40ed9aeb30ff921edaf509c94f71428e48219ba940f5f10c061421dc0c006e09feadec30df20b2d13d02c3ce4ceb32b6fbefd254288d45f3bb2c425b197e19699d7efdfc7aba5dd45b727bc98abd866d2f6e69e33a64e4b5a5ab1e4d749266c7bf285550da9fb036f10eff76b697de9c5ed8de4a3cdbca1174543540bed6c3a95641cfdacbac834896639f8a75ed1fb9cfd9983d83d0b43b76bd3894bd2b3da0dd23d1e0362985217f087acce1a7f56546c214890acae8fc60e27890ff31c38578f85e220342061a1a5c867362a14aafdffa003dc13af064f5f860d1757883ea5237feed3a6228c86200062bd88f5592d5c399ef270a562d458ae8eac5eaa382b5bcc3f64298cc34b4598f0b33d7943b8" />
     <Using Include="System.Data" />
@@ -39,7 +45,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <None Include="docs\README.md" Pack="true" PackagePath="\"/>
+    <None Include="docs\README.md" Pack="true" PackagePath="\" />
   </ItemGroup>
 
 </Project>
diff --git a/src/MySqlConnector/MySqlConnectorFactory.cs b/src/MySqlConnector/MySqlConnectorFactory.cs
index 6d91b47df..8e5c2889c 100644
--- a/src/MySqlConnector/MySqlConnectorFactory.cs
+++ b/src/MySqlConnector/MySqlConnectorFactory.cs
@@ -65,7 +65,7 @@ public sealed class MySqlConnectorFactory : DbProviderFactory
 #if NET6_0_OR_GREATER
 	public override DbBatch CreateBatch() => new MySqlBatch();
 #else
-	public MySqlBatch CreateBatch() => new MySqlBatch();
+	public MySqlBatch CreateBatch() => new();
 #endif
 
 	/// <summary>
@@ -74,7 +74,7 @@ public sealed class MySqlConnectorFactory : DbProviderFactory
 #if NET6_0_OR_GREATER
 	public override DbBatchCommand CreateBatchCommand() => new MySqlBatchCommand();
 #else
-	public MySqlBatchCommand CreateBatchCommand() => new MySqlBatchCommand();
+	public MySqlBatchCommand CreateBatchCommand() => new();
 #endif
 
 	/// <summary>
@@ -85,15 +85,17 @@ public sealed class MySqlConnectorFactory : DbProviderFactory
 #else
 	public bool CanCreateBatch => true;
 #endif
-#pragma warning restore CA1822 // Mark members as static
 
-#if NET7_0_OR_GREATER
 	/// <summary>
 	/// Creates a new <see cref="MySqlDataSource"/> object.
 	/// </summary>
 	/// <param name="connectionString">The connection string.</param>
-	public override DbDataSource CreateDataSource(string connectionString) => new MySqlDataSource(connectionString);
+	public
+#if NET7_0_OR_GREATER
+	override
 #endif
+	DbDataSource CreateDataSource(string connectionString) => new MySqlDataSource(connectionString);
+#pragma warning restore CA1822 // Mark members as static
 
 	private MySqlConnectorFactory()
 	{
diff --git a/src/MySqlConnector/MySqlConnectorTracingOptions.cs b/src/MySqlConnector/MySqlConnectorTracingOptions.cs
new file mode 100644
index 000000000..8bc0d71f5
--- /dev/null
+++ b/src/MySqlConnector/MySqlConnectorTracingOptions.cs
@@ -0,0 +1,11 @@
+namespace MySqlConnector;
+
+internal sealed class MySqlConnectorTracingOptions
+{
+	public bool EnableResultSetHeaderEvent { get; set; }
+
+	public static MySqlConnectorTracingOptions Default { get; } = new()
+	{
+		EnableResultSetHeaderEvent = true,
+	};
+}
diff --git a/src/MySqlConnector/MySqlConnectorTracingOptionsBuilder.cs b/src/MySqlConnector/MySqlConnectorTracingOptionsBuilder.cs
new file mode 100644
index 000000000..dfc0432b0
--- /dev/null
+++ b/src/MySqlConnector/MySqlConnectorTracingOptionsBuilder.cs
@@ -0,0 +1,25 @@
+namespace MySqlConnector;
+
+/// <summary>
+/// <see cref="MySqlConnectorTracingOptionsBuilder"/> provides an API for configuring OpenTelemetry tracing options.
+/// </summary>
+public sealed class MySqlConnectorTracingOptionsBuilder
+{
+	/// <summary>
+	/// Gets or sets a value indicating whether to enable the "time-to-first-read" event.
+	/// Default is true to preserve existing behavior.
+	/// </summary>
+	public MySqlConnectorTracingOptionsBuilder EnableResultSetHeaderEvent(bool enable = true)
+	{
+		m_enableResultSetHeaderEvent = enable;
+		return this;
+	}
+
+	internal MySqlConnectorTracingOptions Build() =>
+		new()
+		{
+			EnableResultSetHeaderEvent = m_enableResultSetHeaderEvent,
+		};
+
+	private bool m_enableResultSetHeaderEvent = MySqlConnectorTracingOptions.Default.EnableResultSetHeaderEvent;
+}
diff --git a/src/MySqlConnector/MySqlConversionException.cs b/src/MySqlConnector/MySqlConversionException.cs
index 130c3cc54..1e8a4ad67 100644
--- a/src/MySqlConnector/MySqlConversionException.cs
+++ b/src/MySqlConnector/MySqlConversionException.cs
@@ -17,6 +17,9 @@ internal MySqlConversionException(string message)
 	{
 	}
 
+#if NET8_0_OR_GREATER
+	[Obsolete(DiagnosticId = "SYSLIB0051")]
+#endif
 	private MySqlConversionException(SerializationInfo info, StreamingContext context)
 		: base(info, context)
 	{
diff --git a/src/MySqlConnector/MySqlDataAdapter.cs b/src/MySqlConnector/MySqlDataAdapter.cs
index ba67a5f88..91fa33ec2 100644
--- a/src/MySqlConnector/MySqlDataAdapter.cs
+++ b/src/MySqlConnector/MySqlDataAdapter.cs
@@ -158,7 +158,7 @@ protected override int ExecuteBatch()
 		}
 
 		// ensure that the VALUES(...) clause contained only parameters, and that all were consumed
-		var remainingValues = parser.CommandText.Substring(match.Index + 6).Trim();
+		var remainingValues = parser.CommandText[(match.Index + 6)..].Trim();
 		remainingValues = remainingValues.TrimEnd(';').Trim().TrimStart('(').TrimEnd(')');
 		remainingValues = remainingValues.Replace(",", "");
 		if (!string.IsNullOrWhiteSpace(remainingValues))
@@ -166,7 +166,7 @@ protected override int ExecuteBatch()
 
 		// build one INSERT statement with concatenated VALUES
 		var combinedCommand = new MySqlCommand();
-		var sqlBuilder = new StringBuilder(sql.Substring(0, match.Index + 6));
+		var sqlBuilder = new StringBuilder(sql[..(match.Index + 6)]);
 		var combinedParameterIndex = 0;
 		for (var i = 0; i < batch.BatchCommands.Count; i++)
 		{
@@ -202,7 +202,7 @@ public InsertSqlParser(IMySqlCommand command)
 		{
 			CommandText = command.CommandText!;
 			m_parameters = command.RawParameters;
-			ParameterIndexes = new();
+			ParameterIndexes = [];
 		}
 
 		public List<int> ParameterIndexes { get; }
@@ -219,7 +219,7 @@ protected override void OnNamedParameter(int index, int length)
 #if NETCOREAPP3_0_OR_GREATER
 			CommandText = string.Concat(CommandText.AsSpan(0, index), new string(' ', length), CommandText.AsSpan(index + length));
 #else
-			CommandText = CommandText.Substring(0, index) + new string(' ', length) + CommandText.Substring(index + length);
+			CommandText = CommandText[..index] + new string(' ', length) + CommandText[(index + length)..];
 #endif
 		}
 
@@ -231,7 +231,7 @@ protected override void OnPositionalParameter(int index)
 #if NETCOREAPP3_0_OR_GREATER
 			CommandText = string.Concat(CommandText.AsSpan(0, index), " ", CommandText.AsSpan(index + 1));
 #else
-			CommandText = CommandText.Substring(0, index) + " " + CommandText.Substring(index + 1);
+			CommandText = CommandText[..index] + " " + CommandText[(index + 1)..];
 #endif
 		}
 
diff --git a/src/MySqlConnector/MySqlDataReader.cs b/src/MySqlConnector/MySqlDataReader.cs
index 957b110e5..fc34ca365 100644
--- a/src/MySqlConnector/MySqlDataReader.cs
+++ b/src/MySqlConnector/MySqlDataReader.cs
@@ -1,6 +1,7 @@
 using System.Collections;
 using System.Collections.ObjectModel;
 using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using MySqlConnector.Core;
 using MySqlConnector.Logging;
@@ -11,7 +12,7 @@ namespace MySqlConnector;
 
 #pragma warning disable CA1010 // Generic interface should also be implemented
 
-#if NET461
+#if NET462
 public sealed class MySqlDataReader : DbDataReader
 #else
 public sealed class MySqlDataReader : DbDataReader, IDbColumnSchemaGenerator
@@ -27,7 +28,7 @@ public override bool Read()
 	{
 		VerifyNotDisposed();
 		Command!.CancellableCommand.ResetCommandTimeout();
-		return m_resultSet!.Read();
+		return m_resultSet.Read();
 	}
 
 	public override async Task<bool> ReadAsync(CancellationToken cancellationToken)
@@ -35,11 +36,11 @@ public override async Task<bool> ReadAsync(CancellationToken cancellationToken)
 		VerifyNotDisposed();
 		Command!.CancellableCommand.ResetCommandTimeout();
 		using var registration = Command.CancellableCommand.RegisterCancel(cancellationToken);
-		return await m_resultSet!.ReadAsync(cancellationToken).ConfigureAwait(false);
+		return await m_resultSet.ReadAsync(cancellationToken).ConfigureAwait(false);
 	}
 
 	internal Task<bool> ReadAsync(IOBehavior ioBehavior, CancellationToken cancellationToken) =>
-		m_resultSet!.ReadAsync(ioBehavior, cancellationToken);
+		m_resultSet.ReadAsync(ioBehavior, cancellationToken);
 
 	public override async Task<bool> NextResultAsync(CancellationToken cancellationToken)
 	{
@@ -58,7 +59,7 @@ internal async Task<bool> NextResultAsync(IOBehavior ioBehavior, CancellationTok
 			{
 				while (true)
 				{
-					await m_resultSet!.ReadEntireAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+					await m_resultSet.ReadEntireAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
 					await ScanResultSetAsync(ioBehavior, m_resultSet, cancellationToken).ConfigureAwait(false);
 					if (m_hasMoreResults && m_resultSet.ContainsCommandParameters)
 						await ReadOutParametersAsync(Command!, m_resultSet, ioBehavior, cancellationToken).ConfigureAwait(false);
@@ -68,11 +69,13 @@ internal async Task<bool> NextResultAsync(IOBehavior ioBehavior, CancellationTok
 
 				if (!m_hasMoreResults)
 				{
-					if (m_commandListPosition.CommandIndex < m_commandListPosition.Commands.Count)
+					if (m_commandListPosition.CommandIndex < m_commandListPosition.CommandCount)
 					{
-						Command = m_commandListPosition.Commands[m_commandListPosition.CommandIndex];
+						Command = m_commandListPosition.CommandAt(m_commandListPosition.CommandIndex);
 						using (Command.CancellableCommand.RegisterCancel(cancellationToken))
 						{
+							await m_payloadCreator!.SendCommandPrologueAsync(Command.Connection!, m_commandListPosition, ioBehavior, cancellationToken).ConfigureAwait(false);
+
 							var writer = new ByteBufferWriter();
 							if (!Command.Connection!.Session.IsCancelingQuery && m_payloadCreator.WriteQueryCommand(ref m_commandListPosition, m_cachedProcedures!, writer, false))
 							{
@@ -99,7 +102,7 @@ internal async Task<bool> NextResultAsync(IOBehavior ioBehavior, CancellationTok
 		}
 		catch (MySqlException)
 		{
-			m_resultSet!.Reset();
+			m_resultSet.Reset();
 			m_hasMoreResults = false;
 			m_schemaTable = null;
 			throw;
@@ -108,7 +111,7 @@ internal async Task<bool> NextResultAsync(IOBehavior ioBehavior, CancellationTok
 
 	private void ActivateResultSet(CancellationToken cancellationToken)
 	{
-		if (m_resultSet!.ReadResultSetHeaderException is not null)
+		if (m_resultSet.ReadResultSetHeaderException is not null)
 		{
 			var mySqlException = m_resultSet.ReadResultSetHeaderException.SourceException as MySqlException;
 
@@ -136,25 +139,20 @@ private void ActivateResultSet(CancellationToken cancellationToken)
 		m_hasWarnings = m_resultSet.WarningCount != 0;
 	}
 
-	private ValueTask ScanResultSetAsync(IOBehavior ioBehavior, ResultSet resultSet, CancellationToken cancellationToken)
+	private async ValueTask ScanResultSetAsync(IOBehavior ioBehavior, ResultSet resultSet, CancellationToken cancellationToken)
 	{
 		if (!m_hasMoreResults)
-			return default;
+			return;
 
 		if (resultSet.BufferState is ResultSetState.NoMoreData or ResultSetState.None)
 		{
 			m_hasMoreResults = false;
-			return default;
+			return;
 		}
 
 		if (resultSet.BufferState != ResultSetState.HasMoreData)
 			throw new InvalidOperationException($"Invalid state: {resultSet.BufferState}");
 
-		return new ValueTask(ScanResultSetAsyncAwaited(ioBehavior, resultSet, cancellationToken));
-	}
-
-	private async Task ScanResultSetAsyncAwaited(IOBehavior ioBehavior, ResultSet resultSet, CancellationToken cancellationToken)
-	{
 		using (Command!.CancellableCommand.RegisterCancel(cancellationToken))
 		{
 			try
@@ -182,9 +180,9 @@ public override int FieldCount
 		get
 		{
 			VerifyNotDisposed();
-			if (m_resultSet is null)
-				throw new InvalidOperationException("There is no current result set.");
-			return m_resultSet.ContainsCommandParameters ? 0 : m_resultSet.FieldCount;
+			return m_resultSet is null ? throw new InvalidOperationException("There is no current result set.") :
+				m_resultSet.ContainsCommandParameters ? 0 :
+				m_resultSet.FieldCount;
 		}
 	}
 
@@ -197,9 +195,8 @@ public override bool HasRows
 		get
 		{
 			VerifyNotDisposed();
-			if (m_resultSet is null)
-				throw new InvalidOperationException("There is no current result set.");
-			return !m_resultSet.ContainsCommandParameters && m_resultSet.HasRows;
+			return m_resultSet is null ? throw new InvalidOperationException("There is no current result set.") :
+				!m_resultSet.ContainsCommandParameters && m_resultSet.HasRows;
 		}
 	}
 
@@ -251,6 +248,10 @@ public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int
 	public override string GetDataTypeName(int ordinal) => GetResultSet().GetDataTypeName(ordinal);
 
 	public Type GetFieldType(string name) => GetFieldType(GetOrdinal(name));
+
+#if NET6_0_OR_GREATER
+	[UnconditionalSuppressMessage("ILLink", "IL2093", Justification = "This method is provided to implement the DbDataReader API. We do not want to retain all public methods of Types just used as sentinel values for field mapping.")]
+#endif
 	public override Type GetFieldType(int ordinal) => GetResultSet().GetFieldType(ordinal);
 
 	public override object GetValue(int ordinal) => GetResultSet().GetCurrentRow().GetValue(ordinal);
@@ -350,12 +351,16 @@ public override long GetChars(int ordinal, long dataOffset, char[]? buffer, int
 	/// <returns>A <see cref="System.Collections.ObjectModel.ReadOnlyCollection{DbColumn}"/> containing metadata about the result set.</returns>
 	public ReadOnlyCollection<DbColumn> GetColumnSchema()
 	{
-		var columnDefinitions = m_resultSet?.ColumnDefinitions;
-		var hasNoSchema = columnDefinitions is null || m_resultSet!.ContainsCommandParameters;
-		return hasNoSchema ? new List<DbColumn>().AsReadOnly() :
-			columnDefinitions!
-				.Select((c, n) => (DbColumn) new MySqlDbColumn(n, c, Connection!.AllowZeroDateTime, GetResultSet().ColumnTypes![n]))
-				.ToList().AsReadOnly();
+		var hasNoSchema = !m_resultSet.HasResultSet || m_resultSet.ContainsCommandParameters;
+		if (hasNoSchema)
+			return new ReadOnlyCollection<DbColumn>([]);
+
+		var columnDefinitions = m_resultSet.ColumnDefinitions;
+		var resultSet = GetResultSet();
+		var schema = new List<DbColumn>(columnDefinitions.Length);
+		for (var n = 0; n < columnDefinitions.Length; n++)
+			schema.Add(new MySqlDbColumn(n, columnDefinitions[n], Connection!.AllowZeroDateTime, resultSet.GetColumnType(n)));
+		return schema.AsReadOnly();
 	}
 
 	/// <summary>
@@ -451,30 +456,46 @@ protected override void Dispose(bool disposing)
 	public Task DisposeAsync() => DisposeAsync(Connection?.AsyncIOBehavior ?? IOBehavior.Asynchronous, CancellationToken.None);
 #endif
 
-	internal Activity? Activity { get; }
+	internal Activity? Activity { get; private set; }
 	internal IMySqlCommand? Command { get; private set; }
 	internal MySqlConnection? Connection => Command?.Connection;
 	internal ulong? RealRecordsAffected { get; set; }
 	internal ServerSession? Session => Command?.Connection!.Session;
 
-	internal static async Task<MySqlDataReader> CreateAsync(CommandListPosition commandListPosition, ICommandPayloadCreator payloadCreator, IDictionary<string, CachedProcedure?>? cachedProcedures, IMySqlCommand command, CommandBehavior behavior, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken)
+	internal async Task InitAsync(CommandListPosition commandListPosition, ICommandPayloadCreator payloadCreator, IDictionary<string, CachedProcedure?>? cachedProcedures, IMySqlCommand command, CommandBehavior behavior, Activity? activity, IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
-		var dataReader = new MySqlDataReader(commandListPosition, payloadCreator, cachedProcedures, command, behavior, activity);
-		command.Connection!.SetActiveReader(dataReader);
+		// reset fields from last use of this MySqlDataReader
+		if (m_hasMoreResults)
+			throw new InvalidOperationException("Expected m_hasMoreResults to be false");
+		if (m_resultSet.BufferState != ResultSetState.None || m_resultSet.State != ResultSetState.None)
+			throw new InvalidOperationException("Expected BufferState and State to be ResultSetState.None.");
+		m_closed = false;
+		m_hasWarnings = false;
+		RealRecordsAffected = null;
+
+		// initialize for new command
+		m_commandListPosition = commandListPosition;
+		m_payloadCreator = payloadCreator;
+		m_cachedProcedures = cachedProcedures;
+		Command = command;
+		m_behavior = behavior;
+		Activity = activity;
+
+		command.Connection!.SetActiveReader(this);
 
 		try
 		{
-			await dataReader.m_resultSet!.ReadResultSetHeaderAsync(ioBehavior).ConfigureAwait(false);
-			dataReader.ActivateResultSet(cancellationToken);
-			dataReader.m_hasMoreResults = true;
+			await m_resultSet.ReadResultSetHeaderAsync(ioBehavior).ConfigureAwait(false);
+			ActivateResultSet(cancellationToken);
+			m_hasMoreResults = true;
 
-			if (dataReader.m_resultSet.ContainsCommandParameters)
-				await ReadOutParametersAsync(dataReader.Command!, dataReader.m_resultSet, ioBehavior, cancellationToken).ConfigureAwait(false);
+			if (m_resultSet.ContainsCommandParameters)
+				await ReadOutParametersAsync(command, m_resultSet, ioBehavior, cancellationToken).ConfigureAwait(false);
 
 			// if the command list has multiple commands, keep reading until a result set is found
-			while (dataReader.m_resultSet.State == ResultSetState.NoMoreData && commandListPosition.CommandIndex < commandListPosition.Commands.Count)
+			while (m_resultSet.State == ResultSetState.NoMoreData && commandListPosition.CommandIndex < commandListPosition.CommandCount)
 			{
-				await dataReader.NextResultAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+				_ = await NextResultAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
 			}
 		}
 		catch (Exception ex)
@@ -484,22 +505,21 @@ internal static async Task<MySqlDataReader> CreateAsync(CommandListPosition comm
 				activity.SetException(ex);
 				activity.Stop();
 			}
-			dataReader.m_creationFailed = true;
-			dataReader.Dispose();
+			Dispose();
 			throw;
 		}
-
-		return dataReader;
 	}
 
 	internal DataTable? BuildSchemaTable()
 	{
-		var columnDefinitions = m_resultSet?.ColumnDefinitions;
-		if (columnDefinitions is null || m_resultSet!.ContainsCommandParameters)
+		if (!m_resultSet.HasResultSet || m_resultSet.ContainsCommandParameters)
 			return null;
 
-		var schemaTable = new DataTable("SchemaTable") { Locale = CultureInfo.InvariantCulture };
-		schemaTable.MinimumCapacity = columnDefinitions.Length;
+		var schemaTable = new DataTable("SchemaTable")
+		{
+			Locale = CultureInfo.InvariantCulture,
+			MinimumCapacity = m_resultSet.ColumnDefinitions.Length,
+		};
 
 		var columnName = new DataColumn(SchemaTableColumn.ColumnName, typeof(string));
 		var ordinal = new DataColumn(SchemaTableColumn.ColumnOrdinal, typeof(int));
@@ -583,15 +603,9 @@ internal static async Task<MySqlDataReader> CreateAsync(CommandListPosition comm
 		return schemaTable;
 	}
 
-	private MySqlDataReader(CommandListPosition commandListPosition, ICommandPayloadCreator payloadCreator, IDictionary<string, CachedProcedure?>? cachedProcedures, IMySqlCommand command, CommandBehavior behavior, Activity? activity)
+	internal MySqlDataReader()
 	{
-		m_commandListPosition = commandListPosition;
-		m_payloadCreator = payloadCreator;
-		m_cachedProcedures = cachedProcedures;
-		Command = command;
-		m_behavior = behavior;
 		m_resultSet = new(this);
-		Activity = activity;
 	}
 
 #if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
@@ -617,9 +631,8 @@ internal async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancel
 				{
 					// ignore "Query execution was interrupted" exceptions when closing a data reader; log other exceptions
 					if (ex.ErrorCode != MySqlErrorCode.QueryInterrupted)
-						Log.Warn("Session{0} ignoring exception in MySqlDataReader.DisposeAsync. Message: {1}. CommandText: {2}", Command.Connection.Session.Id, ex.Message, Command.CommandText);
+						Log.IgnoringExceptionInDisposeAsync(Command.Logger, ex, Command.Connection.Session.Id, ex.Message, Command.CommandText!);
 				}
-				m_resultSet = null;
 			}
 
 			m_hasMoreResults = false;
@@ -628,13 +641,17 @@ internal async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancel
 			Command.CancellableCommand.SetTimeout(Constants.InfiniteTimeout);
 			connection.FinishQuerying(m_hasWarnings);
 
-			if (!m_creationFailed)
-				Activity?.SetSuccess();
 			Activity?.Stop();
+			Activity = null;
 
 			if ((m_behavior & CommandBehavior.CloseConnection) != 0)
 				await connection.CloseAsync(ioBehavior).ConfigureAwait(false);
+
+			// clear fields (so that MySqlConnection can be GCed if the user doesn't hold a reference to it)
 			Command = null;
+			m_commandListPosition = default;
+			m_payloadCreator = null;
+			m_cachedProcedures = null;
 		}
 	}
 
@@ -643,7 +660,7 @@ internal async Task DisposeAsync(IOBehavior ioBehavior, CancellationToken cancel
 	// execution. These values will be stored in the parameters of the associated command.
 	private static async Task ReadOutParametersAsync(IMySqlCommand command, ResultSet resultSet, IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
-		await resultSet.ReadAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
+		_ = await resultSet.ReadAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
 
 		var row = resultSet.GetCurrentRow();
 		if (row.GetString(0) != SingleCommandPayloadCreator.OutParameterSentinelColumnName)
@@ -656,7 +673,7 @@ private static async Task ReadOutParametersAsync(IMySqlCommand command, ResultSe
 			if (param.HasSetDbType && !row.IsDBNull(columnIndex))
 			{
 				var dbTypeMapping = TypeMapper.Instance.GetDbTypeMapping(param.DbType);
-				if (dbTypeMapping is not null)
+				if (dbTypeMapping is not null && param.DbType is not DbType.Object)
 				{
 					param.Value = dbTypeMapping.DoConversion(row.GetValue(columnIndex));
 					continue;
@@ -675,24 +692,23 @@ private void VerifyNotDisposed()
 			throw new InvalidOperationException("Can't call this method when MySqlDataReader is closed.");
 	}
 
+	internal PreparedStatement? LastUsedPreparedStatement => m_commandListPosition.LastUsedPreparedStatement;
+
 	private ResultSet GetResultSet()
 	{
 		VerifyNotDisposed();
-		if (m_resultSet is null || m_resultSet.ContainsCommandParameters)
-			throw new InvalidOperationException("There is no current result set.");
-		return m_resultSet;
+		return m_resultSet is null || m_resultSet.ContainsCommandParameters ?
+			throw new InvalidOperationException("There is no current result set.") :
+			m_resultSet;
 	}
 
-	private static readonly IMySqlConnectorLogger Log = MySqlConnectorLogManager.CreateLogger(nameof(MySqlDataReader));
-
-	private readonly CommandBehavior m_behavior;
-	private readonly ICommandPayloadCreator m_payloadCreator;
-	private readonly IDictionary<string, CachedProcedure?>? m_cachedProcedures;
+	private readonly ResultSet m_resultSet;
+	private CommandBehavior m_behavior;
+	private ICommandPayloadCreator? m_payloadCreator;
+	private IDictionary<string, CachedProcedure?>? m_cachedProcedures;
 	private CommandListPosition m_commandListPosition;
 	private bool m_closed;
 	private bool m_hasWarnings;
 	private bool m_hasMoreResults;
-	private bool m_creationFailed;
-	private ResultSet? m_resultSet;
 	private DataTable? m_schemaTable;
 }
diff --git a/src/MySqlConnector/MySqlDataSource.cs b/src/MySqlConnector/MySqlDataSource.cs
index 492206e73..5b5b38436 100644
--- a/src/MySqlConnector/MySqlDataSource.cs
+++ b/src/MySqlConnector/MySqlDataSource.cs
@@ -1,12 +1,15 @@
-#if NET7_0_OR_GREATER
+using System.Net.Security;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.Extensions.Logging;
 using MySqlConnector.Core;
 using MySqlConnector.Logging;
+using MySqlConnector.Plugins;
 using MySqlConnector.Protocol.Serialization;
 
 namespace MySqlConnector;
 
 /// <summary>
-/// <see cref="MySqlDataSource"/> implements a MySQL data source which can be used to obtain open connections, and against which commands can be executed directly.
+/// <see cref="MySqlDataSource"/> implements a MySQL data source which can be used to obtain open connections.
 /// </summary>
 public sealed class MySqlDataSource : DbDataSource
 {
@@ -16,19 +19,60 @@ public sealed class MySqlDataSource : DbDataSource
 	/// <param name="connectionString">The connection string for the MySQL Server. This parameter is required.</param>
 	/// <exception cref="ArgumentNullException">Thrown if <paramref name="connectionString"/> is <c>null</c>.</exception>
 	public MySqlDataSource(string connectionString)
+		: this(connectionString ?? throw new ArgumentNullException(nameof(connectionString)), MySqlConnectorLoggingConfiguration.NullConfiguration, null, null, null, null, null, default, default, default, default)
+	{
+	}
+
+	internal MySqlDataSource(string connectionString,
+		MySqlConnectorLoggingConfiguration loggingConfiguration,
+		MySqlConnectorTracingOptions? tracingOptions,
+		string? name,
+		Func<X509CertificateCollection, ValueTask>? clientCertificatesCallback,
+		RemoteCertificateValidationCallback? remoteCertificateValidationCallback,
+		Func<MySqlProvidePasswordContext, CancellationToken, ValueTask<string>>? periodicPasswordProvider,
+		TimeSpan periodicPasswordProviderSuccessRefreshInterval,
+		TimeSpan periodicPasswordProviderFailureRefreshInterval,
+		ZstandardPlugin? zstandardPlugin,
+		MySqlConnectionOpenedCallback? connectionOpenedCallback)
 	{
-		m_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
-		Pool = ConnectionPool.CreatePool(m_connectionString);
+		m_connectionString = connectionString;
+		LoggingConfiguration = loggingConfiguration;
+		TracingOptions = tracingOptions ?? MySqlConnectorTracingOptions.Default;
+		Name = name;
+		m_clientCertificatesCallback = clientCertificatesCallback;
+		m_remoteCertificateValidationCallback = remoteCertificateValidationCallback;
+		m_logger = loggingConfiguration.DataSourceLogger;
+		m_zstandardPlugin = zstandardPlugin;
+		m_connectionOpenedCallback = connectionOpenedCallback;
+
+		Pool = ConnectionPool.CreatePool(m_connectionString, LoggingConfiguration, name);
 		m_id = Interlocked.Increment(ref s_lastId);
-		m_logArguments = new object?[2] { m_id, null };
-		if (Pool is not null)
-		{
-			m_logArguments[1] = Pool.Id;
-			Log.Info("DataSource{0} created with Pool {1}", m_logArguments);
-		}
+		if (Pool is not null && Name is not null)
+			Log.DataSourceCreatedWithPoolWithName(m_logger, m_id, Pool.Id, Name);
+		else if (Pool is not null)
+			Log.DataSourceCreatedWithPoolWithoutName(m_logger, m_id, Pool.Id);
+		else if (Name is not null)
+			Log.DataSourceCreatedWithoutPoolWithName(m_logger, m_id, Name);
 		else
+			Log.DataSourceCreatedWithoutPoolWithoutName(m_logger, m_id);
+
+		if (periodicPasswordProvider is not null)
 		{
-			Log.Info("DataSource{0} created with no pool", m_logArguments);
+			m_periodicPasswordProvider = periodicPasswordProvider;
+			m_periodicPasswordProviderSuccessRefreshInterval = periodicPasswordProviderSuccessRefreshInterval;
+			m_periodicPasswordProviderFailureRefreshInterval = periodicPasswordProviderFailureRefreshInterval;
+
+			m_passwordProviderTimerCancellationTokenSource = new();
+			var csb = new MySqlConnectionStringBuilder(m_connectionString);
+			m_providePasswordContext = new(csb.Server, (int) csb.Port, csb.UserID, csb.Database);
+
+			// create the timer but don't start it; the manual run below will will schedule the first refresh
+			// see https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md#timer-callbacks for code pattern
+			m_passwordProviderTimer = new Timer(_ => _ = RefreshPassword(), null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
+
+			// trigger the first refresh attempt right now, outside the timer; this allows us to capture the Task so it can be observed in ComplexGetPassword
+			m_initialPasswordRefreshTask = Task.Run(RefreshPassword);
+			m_providePasswordCallback = ProvidePasswordFromInitialRefreshTask;
 		}
 	}
 
@@ -66,11 +110,44 @@ public MySqlDataSource(string connectionString)
 	/// </summary>
 	public override string ConnectionString => m_connectionString;
 
+#pragma warning disable CA1044 // Properties should not be write only
+	/// <summary>
+	/// Sets the password that will be used by the next <see cref="MySqlConnection"/> created from this <see cref="MySqlDataSource"/>.
+	/// </summary>
+	/// <remarks>
+	/// <para>This can be used to update the password for database servers that periodically rotate authentication tokens, without
+	/// affecting connection pooling. The <see cref="MySqlConnectionStringBuilder.Password"/> property must not be specified in
+	/// order for this field to be used.</para>
+	/// <para>Consider using <see cref="MySqlDataSourceBuilder.UsePeriodicPasswordProvider"/> instead.</para>
+	/// </remarks>
+	public string Password
+	{
+		set
+		{
+			if (m_periodicPasswordProvider is not null)
+				throw new InvalidOperationException("Cannot set Password when this MySqlDataSource is configured with a PeriodicPasswordProvider.");
+
+			m_password = value ?? throw new ArgumentNullException(nameof(value));
+			m_providePasswordCallback = ProvidePasswordFromField;
+		}
+	}
+#pragma warning restore CA1044 // Properties should not be write only
+
 	protected override DbConnection CreateDbConnection()
 	{
+#if NET7_0_OR_GREATER
+		ObjectDisposedException.ThrowIf(m_isDisposed, this);
+#else
 		if (m_isDisposed)
 			throw new ObjectDisposedException(nameof(MySqlDataSource));
-		return new MySqlConnection(this);
+#endif
+		return new MySqlConnection(this)
+		{
+			ProvideClientCertificatesCallback = m_clientCertificatesCallback,
+			ProvidePasswordCallback = m_providePasswordCallback,
+			RemoteCertificateValidationCallback = m_remoteCertificateValidationCallback,
+			ConnectionOpenedCallback = m_connectionOpenedCallback,
+		};
 	}
 
 	protected override void Dispose(bool disposing)
@@ -92,6 +169,11 @@ protected override void Dispose(bool disposing)
 
 	private async ValueTask DisposeAsync(IOBehavior ioBehavior)
 	{
+		if (m_passwordProviderTimerCancellationTokenSource is { } cts)
+		{
+			cts.Cancel();
+			cts.Dispose();
+		}
 		if (Pool is not null)
 		{
 			await Pool.ClearAsync(ioBehavior, default).ConfigureAwait(false);
@@ -100,14 +182,62 @@ private async ValueTask DisposeAsync(IOBehavior ioBehavior)
 		m_isDisposed = true;
 	}
 
+	private async Task RefreshPassword()
+	{
+		try
+		{
+			// set the password from the callback, then queue another refresh after the 'success' interval
+			m_password = await m_periodicPasswordProvider!(m_providePasswordContext!, m_passwordProviderTimerCancellationTokenSource!.Token).ConfigureAwait(false);
+			m_providePasswordCallback = ProvidePasswordFromField;
+			_ = m_passwordProviderTimer!.Change(m_periodicPasswordProviderSuccessRefreshInterval, Timeout.InfiniteTimeSpan);
+		}
+		catch (Exception e)
+		{
+			// queue a refresh after the 'failure' interval
+			Log.PeriodicPasswordProviderFailed(m_logger, e, m_id, e.Message);
+			_ = m_passwordProviderTimer!.Change(m_periodicPasswordProviderFailureRefreshInterval, Timeout.InfiniteTimeSpan);
+			throw new MySqlException("The periodic password provider failed", e);
+		}
+	}
+
 	internal ConnectionPool? Pool { get; }
 
-	private static readonly IMySqlConnectorLogger Log = MySqlConnectorLogManager.CreateLogger(nameof(MySqlDataSource));
+	internal MySqlConnectorLoggingConfiguration LoggingConfiguration { get; }
+
+	internal MySqlConnectorTracingOptions TracingOptions { get; }
+
+	internal string? Name { get; }
+
+	private string ProvidePasswordFromField(MySqlProvidePasswordContext context) => m_password!;
+
+	private string ProvidePasswordFromInitialRefreshTask(MySqlProvidePasswordContext context)
+	{
+		if (m_password is null)
+		{
+			// password hasn't been set up, so wait (synchronously) for the task to complete the first time
+			m_initialPasswordRefreshTask!.GetAwaiter().GetResult();
+			m_providePasswordCallback = ProvidePasswordFromField;
+		}
+		return m_password!;
+	}
+
 	private static int s_lastId;
 
+	private readonly ILogger m_logger;
 	private readonly int m_id;
-	private readonly object?[] m_logArguments;
 	private readonly string m_connectionString;
+	private readonly Func<X509CertificateCollection, ValueTask>? m_clientCertificatesCallback;
+	private readonly RemoteCertificateValidationCallback? m_remoteCertificateValidationCallback;
+	private readonly Func<MySqlProvidePasswordContext, CancellationToken, ValueTask<string>>? m_periodicPasswordProvider;
+	private readonly TimeSpan m_periodicPasswordProviderSuccessRefreshInterval;
+	private readonly TimeSpan m_periodicPasswordProviderFailureRefreshInterval;
+	private readonly ZstandardPlugin? m_zstandardPlugin;
+	private readonly MySqlConnectionOpenedCallback? m_connectionOpenedCallback;
+	private readonly MySqlProvidePasswordContext? m_providePasswordContext;
+	private readonly CancellationTokenSource? m_passwordProviderTimerCancellationTokenSource;
+	private readonly Timer? m_passwordProviderTimer;
+	private readonly Task? m_initialPasswordRefreshTask;
 	private bool m_isDisposed;
+	private string? m_password;
+	private Func<MySqlProvidePasswordContext, string>? m_providePasswordCallback;
 }
-#endif
diff --git a/src/MySqlConnector/MySqlDataSourceBuilder.cs b/src/MySqlConnector/MySqlDataSourceBuilder.cs
new file mode 100644
index 000000000..5ea16dce0
--- /dev/null
+++ b/src/MySqlConnector/MySqlDataSourceBuilder.cs
@@ -0,0 +1,163 @@
+using System.Net.Security;
+using System.Security.Cryptography.X509Certificates;
+using Microsoft.Extensions.Logging;
+using MySqlConnector.Logging;
+using MySqlConnector.Plugins;
+
+namespace MySqlConnector;
+
+/// <summary>
+/// <see cref="MySqlDataSourceBuilder"/> provides an API for configuring and creating a <see cref="MySqlDataSource"/>,
+/// from which <see cref="MySqlConnection"/> objects can be obtained.
+/// </summary>
+public sealed class MySqlDataSourceBuilder
+{
+	/// <summary>
+	/// Initializes a new <see cref="MySqlDataSourceBuilder"/> with the specified connection string.
+	/// </summary>
+	/// <param name="connectionString">The optional connection string to use.</param>
+	public MySqlDataSourceBuilder(string? connectionString = null)
+	{
+		ConnectionStringBuilder = new(connectionString ?? "");
+	}
+
+	/// <summary>
+	/// Configures OpenTelemetry tracing options.
+	/// </summary>
+	/// <returns>This builder, so that method calls can be chained.</returns>
+	public MySqlDataSourceBuilder ConfigureTracing(Action<MySqlConnectorTracingOptionsBuilder> configureAction)
+	{
+#if NET6_0_OR_GREATER
+		ArgumentNullException.ThrowIfNull(configureAction);
+#else
+		if (configureAction is null)
+			throw new ArgumentNullException(nameof(configureAction));
+#endif
+		m_tracingOptionsBuilderCallbacks ??= [];
+		m_tracingOptionsBuilderCallbacks.Add(configureAction);
+		return this;
+	}
+
+	/// <summary>
+	/// Sets the <see cref="ILoggerFactory"/> that will be used for logging.
+	/// </summary>
+	/// <param name="loggerFactory">The logger factory.</param>
+	/// <returns>This builder, so that method calls can be chained.</returns>
+	public MySqlDataSourceBuilder UseLoggerFactory(ILoggerFactory? loggerFactory)
+	{
+		m_loggerFactory = loggerFactory;
+		return this;
+	}
+
+	/// <summary>
+	/// Sets the name of the <see cref="MySqlDataSource"/> that will be created.
+	/// </summary>
+	/// <param name="name">The data source name.</param>
+	/// <returns>This builder, so that method calls can be chained.</returns>
+	/// <remarks>The connection pool name is used to set the <c>program_name</c> connection attribute
+	/// (which is visible to some diagnostic tools) and the <c>pool.name</c> tag supplied with
+	/// <a href="https://mysqlconnector.net/diagnostics/metrics/">connection pool metrics</a>.</remarks>
+	public MySqlDataSourceBuilder UseName(string? name)
+	{
+		m_name = name;
+		return this;
+	}
+
+	/// <summary>
+	/// Sets the callback used to provide client certificates for connecting to a server.
+	/// </summary>
+	/// <param name="callback">The callback that will provide client certificates. The <see cref="X509CertificateCollection"/>
+	/// provided to the callback should be filled with the client certificate(s) needed to connect to the server.</param>
+	/// <returns>This builder, so that method calls can be chained.</returns>
+	public MySqlDataSourceBuilder UseClientCertificatesCallback(Func<X509CertificateCollection, ValueTask>? callback)
+	{
+		m_clientCertificatesCallback = callback;
+		return this;
+	}
+
+	/// <summary>
+	/// Configures a periodic password provider, which is automatically called by the data source at some regular interval. This is the
+	/// recommended way to fetch a rotating access token.
+	/// </summary>
+	/// <param name="passwordProvider">A callback which returns the password to be used by any new MySQL connections that are made.</param>
+	/// <param name="successRefreshInterval">How long to cache the password before re-invoking the callback.</param>
+	/// <param name="failureRefreshInterval">How long to wait before re-invoking the callback on failure. This should
+	/// typically be much shorter than <paramref name="successRefreshInterval"/>.</param>
+	/// <returns>This builder, so that method calls can be chained.</returns>
+	public MySqlDataSourceBuilder UsePeriodicPasswordProvider(Func<MySqlProvidePasswordContext, CancellationToken, ValueTask<string>>? passwordProvider, TimeSpan successRefreshInterval, TimeSpan failureRefreshInterval)
+	{
+		m_periodicPasswordProvider = passwordProvider;
+		m_periodicPasswordProviderSuccessRefreshInterval = successRefreshInterval;
+		m_periodicPasswordProviderFailureRefreshInterval = failureRefreshInterval;
+		return this;
+	}
+
+	/// <summary>
+	/// Sets the callback used to verify that the server's certificate is valid.
+	/// </summary>
+	/// <param name="callback">The callback used to verify that the server's certificate is valid.</param>
+	/// <returns>This builder, so that method calls can be chained.</returns>
+	/// <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 MySqlDataSourceBuilder UseRemoteCertificateValidationCallback(RemoteCertificateValidationCallback callback)
+	{
+		m_remoteCertificateValidationCallback = callback;
+		return this;
+	}
+
+	/// <summary>
+	/// Adds a callback that is invoked when a new <see cref="MySqlConnection"/> is opened.
+	/// </summary>
+	/// <param name="callback">The callback to invoke.</param>
+	/// <returns>This builder, so that method calls can be chained.</returns>
+	public MySqlDataSourceBuilder UseConnectionOpenedCallback(MySqlConnectionOpenedCallback callback)
+	{
+		m_connectionOpenedCallback += callback;
+		return this;
+	}
+
+	/// <summary>
+	/// Builds a <see cref="MySqlDataSource"/> which is ready for use.
+	/// </summary>
+	/// <returns>A new <see cref="MySqlDataSource"/> with the settings configured through this <see cref="MySqlDataSourceBuilder"/>.</returns>
+	public MySqlDataSource Build()
+	{
+		var loggingConfiguration = m_loggerFactory is null ? MySqlConnectorLoggingConfiguration.NullConfiguration : new(m_loggerFactory);
+
+		var tracingOptionsBuilder = new MySqlConnectorTracingOptionsBuilder();
+		foreach (var callback in m_tracingOptionsBuilderCallbacks ?? (IEnumerable<Action<MySqlConnectorTracingOptionsBuilder>>) [])
+			callback.Invoke(tracingOptionsBuilder);
+		var tracingOptions = tracingOptionsBuilder.Build();
+
+		return new(ConnectionStringBuilder.ConnectionString,
+			loggingConfiguration,
+			tracingOptions,
+			m_name,
+			m_clientCertificatesCallback,
+			m_remoteCertificateValidationCallback,
+			m_periodicPasswordProvider,
+			m_periodicPasswordProviderSuccessRefreshInterval,
+			m_periodicPasswordProviderFailureRefreshInterval,
+			ZstandardPlugin,
+			m_connectionOpenedCallback
+			);
+	}
+
+	/// <summary>
+	/// A <see cref="MySqlConnectionStringBuilder"/> that can be used to configure the connection string on this <see cref="MySqlDataSourceBuilder"/>.
+	/// </summary>
+	public MySqlConnectionStringBuilder ConnectionStringBuilder { get; }
+
+	internal ZstandardPlugin? ZstandardPlugin { get; set; }
+
+	private ILoggerFactory? m_loggerFactory;
+	private string? m_name;
+	private Func<X509CertificateCollection, ValueTask>? m_clientCertificatesCallback;
+	private RemoteCertificateValidationCallback? m_remoteCertificateValidationCallback;
+	private Func<MySqlProvidePasswordContext, CancellationToken, ValueTask<string>>? m_periodicPasswordProvider;
+	private TimeSpan m_periodicPasswordProviderSuccessRefreshInterval;
+	private TimeSpan m_periodicPasswordProviderFailureRefreshInterval;
+	private MySqlConnectionOpenedCallback? m_connectionOpenedCallback;
+	private List<Action<MySqlConnectorTracingOptionsBuilder>>? m_tracingOptionsBuilderCallbacks;
+}
diff --git a/src/MySqlConnector/MySqlDateTime.cs b/src/MySqlConnector/MySqlDateTime.cs
index f468842d3..3b1533f1d 100644
--- a/src/MySqlConnector/MySqlDateTime.cs
+++ b/src/MySqlConnector/MySqlDateTime.cs
@@ -7,29 +7,15 @@ namespace MySqlConnector;
 /// as <c>0000-00-00</c> that can be stored in MySQL (when <see cref="MySqlConnectionStringBuilder.AllowZeroDateTime"/>
 /// is true) but can't be stored in a <see cref="DateTime"/> value.
 /// </summary>
-public struct MySqlDateTime : IComparable, IComparable<MySqlDateTime>, IConvertible, IEquatable<MySqlDateTime>
+/// <param name="year">The year.</param>
+/// <param name="month">The (one-based) month.</param>
+/// <param name="day">The (one-based) day of the month.</param>
+/// <param name="hour">The hour.</param>
+/// <param name="minute">The minute.</param>
+/// <param name="second">The second.</param>
+/// <param name="microsecond">The microsecond.</param>
+public struct MySqlDateTime(int year, int month, int day, int hour, int minute, int second, int microsecond) : IComparable, IComparable<MySqlDateTime>, IConvertible, IEquatable<MySqlDateTime>
 {
-	/// <summary>
-	/// Initializes a new instance of <see cref="MySqlDateTime"/>.
-	/// </summary>
-	/// <param name="year">The year.</param>
-	/// <param name="month">The (one-based) month.</param>
-	/// <param name="day">The (one-based) day of the month.</param>
-	/// <param name="hour">The hour.</param>
-	/// <param name="minute">The minute.</param>
-	/// <param name="second">The second.</param>
-	/// <param name="microsecond">The microsecond.</param>
-	public MySqlDateTime(int year, int month, int day, int hour, int minute, int second, int microsecond)
-	{
-		Year = year;
-		Month = month;
-		Day = day;
-		Hour = hour;
-		Minute = minute;
-		Second = second;
-		Microsecond = microsecond;
-	}
-
 	/// <summary>
 	/// Initializes a new instance of <see cref="MySqlDateTime"/> from a <see cref="DateTime"/>.
 	/// </summary>
@@ -44,14 +30,8 @@ public MySqlDateTime(DateTime dt)
 	/// </summary>
 	/// <param name="other">The <see cref="MySqlDateTime"/> whose values will be copied.</param>
 	public MySqlDateTime(MySqlDateTime other)
+		: this(other.Year, other.Month, other.Day, other.Hour, other.Minute, other.Second, other.Microsecond)
 	{
-		Year = other.Year;
-		Month = other.Month;
-		Day = other.Day;
-		Hour = other.Hour;
-		Minute = other.Minute;
-		Second = other.Second;
-		Microsecond = other.Microsecond;
 	}
 
 	/// <summary>
@@ -62,37 +42,37 @@ public MySqlDateTime(MySqlDateTime other)
 	/// <summary>
 	/// Gets or sets the year.
 	/// </summary>
-	public int Year { get; set; }
+	public int Year { get; set; } = year;
 
 	/// <summary>
 	/// Gets or sets the month.
 	/// </summary>
-	public int Month { get; set; }
+	public int Month { get; set; } = month;
 
 	/// <summary>
 	/// Gets or sets the day of the month.
 	/// </summary>
-	public int Day { get; set; }
+	public int Day { get; set; } = day;
 
 	/// <summary>
 	/// Gets or sets the hour.
 	/// </summary>
-	public int Hour { get; set; }
+	public int Hour { get; set; } = hour;
 
 	/// <summary>
 	/// Gets or sets the minute.
 	/// </summary>
-	public int Minute { get; set; }
+	public int Minute { get; set; } = minute;
 
 	/// <summary>
 	/// Gets or sets the second.
 	/// </summary>
-	public int Second { get; set; }
+	public int Second { get; set; } = second;
 
 	/// <summary>
 	/// Gets or sets the microseconds.
 	/// </summary>
-	public int Microsecond { get; set; }
+	public int Microsecond { get; set; } = microsecond;
 
 	/// <summary>
 	/// Gets or sets the milliseconds.
@@ -202,7 +182,7 @@ readonly int IComparable<MySqlDateTime>.CompareTo(MySqlDateTime other)
 	string IConvertible.ToString(IFormatProvider? provider) => IsValidDateTime ? GetDateTime().ToString(provider) : "0000-00-00";
 
 	object IConvertible.ToType(Type conversionType, IFormatProvider? provider) =>
-		conversionType == typeof(DateTime) ? (object) GetDateTime() :
+		conversionType == typeof(DateTime) ? GetDateTime() :
 		conversionType == typeof(string) ? ((IConvertible) this).ToString(provider) :
 		throw new InvalidCastException();
 
diff --git a/src/MySqlConnector/MySqlDbColumn.cs b/src/MySqlConnector/MySqlDbColumn.cs
index 9710a8035..9696951ed 100644
--- a/src/MySqlConnector/MySqlDbColumn.cs
+++ b/src/MySqlConnector/MySqlDbColumn.cs
@@ -15,6 +15,7 @@ internal MySqlDbColumn(int ordinal, ColumnDefinitionPayload column, bool allowZe
 		var type = columnTypeMetadata.DbTypeMapping.ClrType;
 		var columnSize = type == typeof(string) || type == typeof(Guid) ?
 			column.ColumnLength / ProtocolUtility.GetBytesPerCharacter(column.CharacterSet) :
+			column.ColumnType == ColumnType.Vector ? column.ColumnLength / 4 :
 			column.ColumnLength;
 
 		AllowDBNull = (column.ColumnFlags & ColumnFlags.NotNull) == 0;
@@ -48,7 +49,13 @@ internal MySqlDbColumn(int ordinal, ColumnDefinitionPayload column, bool allowZe
 		}
 		NumericScale = column.Decimals;
 		ProviderType = mySqlDbType;
+		TableName = column.Table;
 	}
 
 	public MySqlDbType ProviderType { get; }
+
+	/// <summary>
+	/// Gets the name of the table that the column belongs to. This will be the alias if the table is aliased in the query.
+	/// </summary>
+	public string TableName { get; }
 }
diff --git a/src/MySqlConnector/MySqlDbType.cs b/src/MySqlConnector/MySqlDbType.cs
index 77f990383..f1d0708a0 100644
--- a/src/MySqlConnector/MySqlDbType.cs
+++ b/src/MySqlConnector/MySqlDbType.cs
@@ -37,6 +37,7 @@ public enum MySqlDbType
 	VarChar,
 	String,
 	Geometry,
+	Vector = 242,
 	UByte = 501,
 	UInt16,
 	UInt32,
diff --git a/src/MySqlConnector/MySqlDecimal.cs b/src/MySqlConnector/MySqlDecimal.cs
index 45b8afc18..b798578af 100644
--- a/src/MySqlConnector/MySqlDecimal.cs
+++ b/src/MySqlConnector/MySqlDecimal.cs
@@ -46,7 +46,7 @@ internal MySqlDecimal(string value)
 		throw new FormatException($"Could not parse the value as a MySqlDecimal: {value}");
 	}
 
-	private static readonly Regex s_pattern = new(@"^-?([1-9][0-9]*|0)(\.([0-9]+))?$");
+	private static readonly Regex s_pattern = new(@"^-?([0-9]+)(\.([0-9]+))?$");
 
 	private readonly string m_value;
 }
diff --git a/src/MySqlConnector/MySqlEndOfStreamException.cs b/src/MySqlConnector/MySqlEndOfStreamException.cs
new file mode 100644
index 000000000..a446584cd
--- /dev/null
+++ b/src/MySqlConnector/MySqlEndOfStreamException.cs
@@ -0,0 +1,15 @@
+namespace MySqlConnector;
+
+[Serializable]
+public sealed class MySqlEndOfStreamException : EndOfStreamException
+{
+	internal MySqlEndOfStreamException(int expectedByteCount, int readByteCount)
+		: base("An incomplete response was received from the server")
+	{
+		ExpectedByteCount = expectedByteCount;
+		ReadByteCount = readByteCount;
+	}
+
+	public int ExpectedByteCount { get; }
+	public int ReadByteCount { get; }
+}
diff --git a/src/MySqlConnector/MySqlException.cs b/src/MySqlConnector/MySqlException.cs
index 89c9aa5e1..0032f7a7e 100644
--- a/src/MySqlConnector/MySqlException.cs
+++ b/src/MySqlConnector/MySqlException.cs
@@ -39,6 +39,9 @@ public sealed class MySqlException : DbException
 	public bool IsTransient => IsErrorTransient(ErrorCode);
 #endif
 
+#if NET8_0_OR_GREATER
+	[Obsolete(DiagnosticId = "SYSLIB0051")]
+#endif
 	private MySqlException(SerializationInfo info, StreamingContext context)
 		: base(info, context)
 	{
@@ -52,6 +55,9 @@ private MySqlException(SerializationInfo info, StreamingContext context)
 	/// </summary>
 	/// <param name="info">The <see cref="SerializationInfo"/> that will be set.</param>
 	/// <param name="context">The context.</param>
+#if NET8_0_OR_GREATER
+	[Obsolete("Do not use legacy serialization infrastructure APIs", DiagnosticId = "SYSLIB0051")]
+#endif
 	public override void GetObjectData(SerializationInfo info, StreamingContext context)
 	{
 		base.GetObjectData(info, context);
diff --git a/src/MySqlConnector/MySqlGeometry.cs b/src/MySqlConnector/MySqlGeometry.cs
index 0c8e0b821..b5af9cf83 100644
--- a/src/MySqlConnector/MySqlGeometry.cs
+++ b/src/MySqlConnector/MySqlGeometry.cs
@@ -27,7 +27,7 @@ public static MySqlGeometry FromWkb(int srid, ReadOnlySpan<byte> wkb)
 	/// <param name="value">The raw bytes of MySQL's internal GEOMETRY format.</param>
 	/// <returns>A new <see cref="MySqlGeometry"/> containing the specified geometry.</returns>
 	/// <remarks>See <a href="https://dev.mysql.com/doc/refman/8.0/en/gis-data-formats.html#gis-internal-format">Internal Geometry Storage Format</a>.</remarks>
-	public static MySqlGeometry FromMySql(ReadOnlySpan<byte> value) => new MySqlGeometry(value.ToArray());
+	public static MySqlGeometry FromMySql(ReadOnlySpan<byte> value) => new(value.ToArray());
 
 	/// <summary>
 	/// The Spatial Reference System ID of this geometry.
diff --git a/src/MySqlConnector/MySqlHelper.cs b/src/MySqlConnector/MySqlHelper.cs
index a5fcdb09b..e78090a9c 100644
--- a/src/MySqlConnector/MySqlHelper.cs
+++ b/src/MySqlConnector/MySqlHelper.cs
@@ -12,8 +12,12 @@ public sealed class MySqlHelper
 	/// </summary>
 	public static string EscapeString(string value)
 	{
+#if NET6_0_OR_GREATER
+		ArgumentNullException.ThrowIfNull(value);
+#else
 		if (value is null)
 			throw new ArgumentNullException(nameof(value));
+#endif
 
 		StringBuilder? sb = null;
 		int last = -1;
diff --git a/src/MySqlConnector/MySqlParameter.cs b/src/MySqlConnector/MySqlParameter.cs
index 70fa0608c..b217eb814 100644
--- a/src/MySqlConnector/MySqlParameter.cs
+++ b/src/MySqlConnector/MySqlParameter.cs
@@ -1,9 +1,15 @@
+using System.Buffers.Binary;
 using System.Buffers.Text;
+using System.Data.Common;
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 using System.Globalization;
 using System.Numerics;
+using System.Runtime.InteropServices;
 using System.Text;
+#if NET8_0_OR_GREATER
+using System.Text.Unicode;
+#endif
 using MySqlConnector.Core;
 using MySqlConnector.Protocol.Serialization;
 using MySqlConnector.Utilities;
@@ -69,6 +75,7 @@ public override DbType DbType
 		}
 	}
 
+	[DbProviderSpecificTypeProperty(true)]
 	public MySqlDbType MySqlDbType
 	{
 		get => m_mySqlDbType;
@@ -85,8 +92,8 @@ public override ParameterDirection Direction
 		get => m_direction.GetValueOrDefault(ParameterDirection.Input);
 		set
 		{
-			if (value != ParameterDirection.Input && value != ParameterDirection.Output &&
-				value != ParameterDirection.InputOutput && value != ParameterDirection.ReturnValue)
+			if (value is not (ParameterDirection.Input or ParameterDirection.Output or
+				ParameterDirection.InputOutput or ParameterDirection.ReturnValue))
 			{
 				throw new ArgumentOutOfRangeException(nameof(value), $"{value} is not a supported value for ParameterDirection");
 			}
@@ -149,11 +156,11 @@ public override void ResetDbType()
 		HasSetDbType = false;
 	}
 
-	public MySqlParameter Clone() => new MySqlParameter(this);
+	public MySqlParameter Clone() => new(this);
 
 	object ICloneable.Clone() => Clone();
 
-	internal MySqlParameter WithParameterName(string parameterName) => new MySqlParameter(this, parameterName);
+	internal MySqlParameter WithParameterName(string parameterName) => new(this, parameterName);
 
 	private MySqlParameter(MySqlParameter other)
 	{
@@ -176,7 +183,13 @@ private MySqlParameter(MySqlParameter other)
 	private MySqlParameter(MySqlParameter other, string parameterName)
 		: this(other)
 	{
-		ParameterName = parameterName ?? throw new ArgumentNullException(nameof(parameterName));
+#if NET6_0_OR_GREATER
+		ArgumentNullException.ThrowIfNull(parameterName);
+#else
+		if (parameterName is null)
+			throw new ArgumentNullException(nameof(parameterName));
+#endif
+		ParameterName = parameterName;
 	}
 
 	internal bool HasSetDirection => m_direction.HasValue;
@@ -193,41 +206,40 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions
 
 		if (Value is null || Value == DBNull.Value)
 		{
-			ReadOnlySpan<byte> nullBytes = "NULL"u8;
-			writer.Write(nullBytes);
+			writer.Write("NULL"u8);
 		}
 		else if (Value is string stringValue)
 		{
-			WriteString(writer, noBackslashEscapes, writeDelimiters: true, stringValue.AsSpan());
+			WriteString(writer, noBackslashEscapes, stringValue.AsSpan());
 		}
 		else if (Value is ReadOnlyMemory<char> readOnlyMemoryChar)
 		{
-			WriteString(writer, noBackslashEscapes, writeDelimiters: true, readOnlyMemoryChar.Span);
+			WriteString(writer, noBackslashEscapes, readOnlyMemoryChar.Span);
 		}
 		else if (Value is Memory<char> memoryChar)
 		{
-			WriteString(writer, noBackslashEscapes, writeDelimiters: true, memoryChar.Span);
+			WriteString(writer, noBackslashEscapes, memoryChar.Span);
 		}
 		else if (Value is char charValue)
 		{
 			writer.Write((byte) '\'');
 			switch (charValue)
 			{
-			case '\0' when !noBackslashEscapes:
-				writer.Write((ushort) 0x305C); // \0
-				break;
+				case '\0' when !noBackslashEscapes:
+					writer.Write((ushort) 0x305C); // \0
+					break;
 
-			case '\'':
-				writer.Write((ushort) 0x2727); // ''
-				break;
+				case '\'':
+					writer.Write((ushort) 0x2727); // ''
+					break;
 
-			case '\\' when !noBackslashEscapes:
-				writer.Write((ushort) 0x5C5C); // \\
-				break;
+				case '\\' when !noBackslashEscapes:
+					writer.Write((ushort) 0x5C5C); // \\
+					break;
 
-			default:
-				writer.Write(charValue.ToString());
-				break;
+				default:
+					writer.Write(charValue.ToString());
+					break;
 			}
 			writer.Write((byte) '\'');
 		}
@@ -243,7 +255,12 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions
 		}
 		else if (Value is decimal decimalValue)
 		{
-			writer.Write(decimalValue.ToString(CultureInfo.InvariantCulture));
+#if NET8_0_OR_GREATER
+			decimalValue.TryFormat(writer.GetSpan(31), out var bytesWritten, default, CultureInfo.InvariantCulture);
+			writer.Advance(bytesWritten);
+#else
+			writer.WriteAscii(decimalValue.ToString(CultureInfo.InvariantCulture));
+#endif
 		}
 		else if (Value is short shortValue)
 		{
@@ -269,7 +286,7 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions
 		{
 			writer.WriteString(ulongValue);
 		}
-		else if (Value is byte[] or ReadOnlyMemory<byte> or Memory<byte> or ArraySegment<byte> or MySqlGeometry or MemoryStream)
+		else if (Value is byte[] or ReadOnlyMemory<byte> or Memory<byte> or ArraySegment<byte> or MySqlGeometry or MemoryStream or float[] or ReadOnlyMemory<float> or Memory<float>)
 		{
 			var inputSpan = Value switch
 			{
@@ -278,6 +295,9 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions
 				Memory<byte> memory => memory.Span,
 				MySqlGeometry geometry => geometry.ValueSpan,
 				MemoryStream memoryStream => memoryStream.TryGetBuffer(out var streamBuffer) ? streamBuffer.AsSpan() : memoryStream.ToArray().AsSpan(),
+				float[] floatArray => ConvertFloatsToBytes(floatArray.AsSpan()),
+				Memory<float> memory => ConvertFloatsToBytes(memory.Span),
+				ReadOnlyMemory<float> memory => ConvertFloatsToBytes(memory.Span),
 				_ => ((ReadOnlyMemory<byte>) Value).Span,
 			};
 
@@ -311,40 +331,57 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions
 			Debug.Assert(index == length, "index == length");
 			writer.Advance(index);
 		}
+		else if (Value is Stream)
+		{
+			throw new NotSupportedException($"Parameter type {Value.GetType().Name} can only be used after calling MySqlCommand.Prepare.");
+		}
 		else if (Value is bool boolValue)
 		{
-			ReadOnlySpan<byte> trueBytes = "true"u8;
-			ReadOnlySpan<byte> falseBytes = "false"u8;
-			writer.Write(boolValue ? trueBytes : falseBytes);
+			writer.Write(boolValue ? "true"u8 : "false"u8);
 		}
 		else if (Value is float floatValue)
 		{
+#if NET8_0_OR_GREATER
+			floatValue.TryFormat(writer.GetSpan(14), out var bytesWritten, "R", CultureInfo.InvariantCulture);
+			writer.Advance(bytesWritten);
+#else
 			// NOTE: Utf8Formatter doesn't support "R"
-			writer.Write(floatValue.ToString("R", CultureInfo.InvariantCulture));
+			writer.WriteAscii(floatValue.ToString("R", CultureInfo.InvariantCulture));
+#endif
 		}
 		else if (Value is double doubleValue)
 		{
+#if NET8_0_OR_GREATER
+			doubleValue.TryFormat(writer.GetSpan(24), out var bytesWritten, "R", CultureInfo.InvariantCulture);
+			writer.Advance(bytesWritten);
+#else
 			// NOTE: Utf8Formatter doesn't support "R"
-			writer.Write(doubleValue.ToString("R", CultureInfo.InvariantCulture));
+			writer.WriteAscii(doubleValue.ToString("R", CultureInfo.InvariantCulture));
+#endif
 		}
 		else if (Value is BigInteger bigInteger)
 		{
-			writer.Write(bigInteger.ToString(CultureInfo.InvariantCulture));
+			writer.WriteAscii(bigInteger.ToString(CultureInfo.InvariantCulture));
 		}
 		else if (Value is MySqlDecimal mySqlDecimal)
 		{
-			writer.Write(mySqlDecimal.ToString());
+			writer.WriteAscii(mySqlDecimal.ToString());
 		}
 		else if (Value is MySqlDateTime mySqlDateTimeValue)
 		{
 			if (mySqlDateTimeValue.IsValidDateTime)
 			{
+#if NET8_0_OR_GREATER
+				Utf8.TryWrite(writer.GetSpan(39), CultureInfo.InvariantCulture, $"timestamp('{mySqlDateTimeValue.GetDateTime():yyyy'-'MM'-'dd' 'HH':'mm':'ss'.'ffffff}')", out var bytesWritten);
+				writer.Advance(bytesWritten);
+#else
 #if NET6_0_OR_GREATER
 				var str = string.Create(CultureInfo.InvariantCulture, stackalloc char[39], $"timestamp('{mySqlDateTimeValue.GetDateTime():yyyy'-'MM'-'dd' 'HH':'mm':'ss'.'ffffff}')");
 #else
 				var str = FormattableString.Invariant($"timestamp('{mySqlDateTimeValue.GetDateTime():yyyy'-'MM'-'dd' 'HH':'mm':'ss'.'ffffff}')");
 #endif
-				writer.Write(str);
+				writer.WriteAscii(str);
+#endif
 			}
 			else
 			{
@@ -354,7 +391,12 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions
 #if NET6_0_OR_GREATER
 		else if (Value is DateOnly dateOnlyValue)
 		{
-			writer.Write(string.Create(CultureInfo.InvariantCulture, stackalloc char[23], $"timestamp('{dateOnlyValue:yyyy'-'MM'-'dd}')"));
+#if NET8_0_OR_GREATER
+			Utf8.TryWrite(writer.GetSpan(23), CultureInfo.InvariantCulture, $"timestamp('{dateOnlyValue:yyyy'-'MM'-'dd}')", out var bytesWritten);
+			writer.Advance(bytesWritten);
+#else
+			writer.WriteAscii(string.Create(CultureInfo.InvariantCulture, stackalloc char[23], $"timestamp('{dateOnlyValue:yyyy'-'MM'-'dd}')"));
+#endif
 		}
 #endif
 		else if (Value is DateTime dateTimeValue)
@@ -364,27 +406,42 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions
 			else if ((options & StatementPreparerOptions.DateTimeLocal) != 0 && dateTimeValue.Kind == DateTimeKind.Utc)
 				throw new MySqlException($"DateTime.Kind must not be Utc when DateTimeKind setting is Local (parameter name: {ParameterName})");
 
+#if NET8_0_OR_GREATER
+			Utf8.TryWrite(writer.GetSpan(39), CultureInfo.InvariantCulture, $"timestamp('{dateTimeValue:yyyy'-'MM'-'dd' 'HH':'mm':'ss'.'ffffff}')", out var bytesWritten);
+			writer.Advance(bytesWritten);
+#else
 #if NET6_0_OR_GREATER
 			var str = string.Create(CultureInfo.InvariantCulture, stackalloc char[39], $"timestamp('{dateTimeValue:yyyy'-'MM'-'dd' 'HH':'mm':'ss'.'ffffff}')");
 #else
 			var str = FormattableString.Invariant($"timestamp('{dateTimeValue:yyyy'-'MM'-'dd' 'HH':'mm':'ss'.'ffffff}')");
 #endif
-			writer.Write(str);
+			writer.WriteAscii(str);
+#endif
 		}
 		else if (Value is DateTimeOffset dateTimeOffsetValue)
 		{
 			// store as UTC as it will be read as such when deserialized from a timespan column
+#if NET8_0_OR_GREATER
+			Utf8.TryWrite(writer.GetSpan(39), CultureInfo.InvariantCulture, $"timestamp('{dateTimeOffsetValue.UtcDateTime:yyyy'-'MM'-'dd' 'HH':'mm':'ss'.'ffffff}')", out var bytesWritten);
+			writer.Advance(bytesWritten);
+#else
 #if NET6_0_OR_GREATER
 			var str = string.Create(CultureInfo.InvariantCulture, stackalloc char[39], $"timestamp('{dateTimeOffsetValue.UtcDateTime:yyyy'-'MM'-'dd' 'HH':'mm':'ss'.'ffffff}')");
 #else
 			var str = FormattableString.Invariant($"timestamp('{dateTimeOffsetValue.UtcDateTime:yyyy'-'MM'-'dd' 'HH':'mm':'ss'.'ffffff}')");
 #endif
-			writer.Write(str);
+			writer.WriteAscii(str);
+#endif
 		}
 #if NET6_0_OR_GREATER
 		else if (Value is TimeOnly timeOnlyValue)
 		{
-			writer.Write(string.Create(CultureInfo.InvariantCulture, stackalloc char[22], $"time '{timeOnlyValue:HH':'mm':'ss'.'ffffff}'"));
+#if NET8_0_OR_GREATER
+			Utf8.TryWrite(writer.GetSpan(22), CultureInfo.InvariantCulture, $"time '{timeOnlyValue:HH':'mm':'ss'.'ffffff}'", out var bytesWritten);
+			writer.Advance(bytesWritten);
+#else
+			writer.WriteAscii(string.Create(CultureInfo.InvariantCulture, stackalloc char[22], $"time '{timeOnlyValue:HH':'mm':'ss'.'ffffff}'"));
+#endif
 		}
 #endif
 		else if (Value is TimeSpan ts)
@@ -395,25 +452,41 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions
 				writer.Write((byte) '-');
 				ts = TimeSpan.FromTicks(-ts.Ticks);
 			}
+#if NET8_0_OR_GREATER
+			Utf8.TryWrite(writer.GetSpan(17), CultureInfo.InvariantCulture, $"{ts.Days * 24 + ts.Hours}:{ts:mm':'ss'.'ffffff}'", out var bytesWritten);
+			writer.Advance(bytesWritten);
+#else
 #if NET6_0_OR_GREATER
 			var str = string.Create(CultureInfo.InvariantCulture, stackalloc char[17], $"{ts.Days * 24 + ts.Hours}:{ts:mm':'ss'.'ffffff}'");
 #else
 			var str = FormattableString.Invariant($"{ts.Days * 24 + ts.Hours}:{ts:mm':'ss'.'ffffff}'");
 #endif
-			writer.Write(str);
+			writer.WriteAscii(str);
+#endif
 		}
 		else if (Value is Guid guidValue)
 		{
 			StatementPreparerOptions guidOptions = options & StatementPreparerOptions.GuidFormatMask;
 			if (guidOptions is StatementPreparerOptions.GuidFormatBinary16 or StatementPreparerOptions.GuidFormatTimeSwapBinary16 or StatementPreparerOptions.GuidFormatLittleEndianBinary16)
 			{
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER
+				Span<byte> bytes = stackalloc byte[16];
+#if NET8_0_OR_GREATER
+				guidValue.TryWriteBytes(bytes, bigEndian: guidOptions != StatementPreparerOptions.GuidFormatLittleEndianBinary16, out _);
+#else
+				guidValue.TryWriteBytes(bytes);
+#endif
+#else
 				var bytes = guidValue.ToByteArray();
+#endif
 				if (guidOptions != StatementPreparerOptions.GuidFormatLittleEndianBinary16)
 				{
+#if !NET8_0_OR_GREATER
 					Utility.SwapBytes(bytes, 0, 3);
 					Utility.SwapBytes(bytes, 1, 2);
 					Utility.SwapBytes(bytes, 4, 5);
 					Utility.SwapBytes(bytes, 6, 7);
+#endif
 
 					if (guidOptions == StatementPreparerOptions.GuidFormatTimeSwapBinary16)
 					{
@@ -447,17 +520,27 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions
 		}
 		else if (Value is StringBuilder stringBuilder)
 		{
-#if NETCOREAPP3_1 || NET5_0_OR_GREATER
+#if NETCOREAPP3_1_OR_GREATER
 			writer.Write((byte) '\'');
 			foreach (var chunk in stringBuilder.GetChunks())
-				WriteString(writer, noBackslashEscapes, writeDelimiters: false, chunk.Span);
+				WriteStringChunk(writer, noBackslashEscapes, chunk.Span);
 			if (stringBuilder.Length != 0)
 				writer.Write("".AsSpan(), flush: true);
 			writer.Write((byte) '\'');
 #else
-			WriteString(writer, noBackslashEscapes, writeDelimiters: true, stringBuilder.ToString().AsSpan());
+			WriteString(writer, noBackslashEscapes, stringBuilder.ToString().AsSpan());
 #endif
 		}
+		else if ((MySqlDbType is MySqlDbType.String or MySqlDbType.VarChar) && HasSetDbType && Value is Enum stringEnumValue)
+		{
+			writer.Write((byte) '\'');
+			writer.Write(stringEnumValue.ToString("G"));
+			writer.Write((byte) '\'');
+		}
+		else if (Value is Enum enumValue)
+		{
+			writer.Write(enumValue.ToString("d"));
+		}
 		else if (MySqlDbType == MySqlDbType.Int16)
 		{
 			writer.WriteString((short) Value);
@@ -482,25 +565,14 @@ internal void AppendSqlString(ByteBufferWriter writer, StatementPreparerOptions
 		{
 			writer.WriteString((ulong) Value);
 		}
-		else if ((MySqlDbType is MySqlDbType.String or MySqlDbType.VarChar) && HasSetDbType && Value is Enum stringEnumValue)
-		{
-			writer.Write((byte) '\'');
-			writer.Write(stringEnumValue.ToString("G"));
-			writer.Write((byte) '\'');
-		}
-		else if (Value is Enum enumValue)
-		{
-			writer.Write(enumValue.ToString("d"));
-		}
 		else
 		{
-			throw new NotSupportedException($"Parameter type {Value.GetType().Name} is not supported; see https://fl.vu/mysql-param-type. Value: {Value}");
+			throw new NotSupportedException($"Parameter type {Value.GetType().Name} is not supported; see https://mysqlconnector.net/param-type. Value: {Value}");
 		}
 
-		static void WriteString(ByteBufferWriter writer, bool noBackslashEscapes, bool writeDelimiters, ReadOnlySpan<char> value)
+		static void WriteString(ByteBufferWriter writer, bool noBackslashEscapes, ReadOnlySpan<char> value)
 		{
-			if (writeDelimiters)
-				writer.Write((byte) '\'');
+			writer.Write((byte) '\'');
 
 			var charsWritten = 0;
 			while (charsWritten < value.Length)
@@ -510,13 +582,13 @@ static void WriteString(ByteBufferWriter writer, bool noBackslashEscapes, bool w
 				if (nextDelimiterIndex == -1)
 				{
 					// write the rest of the string
-					writer.Write(remainingValue, flush: writeDelimiters);
+					writer.Write(remainingValue);
 					charsWritten += remainingValue.Length;
 				}
 				else
 				{
 					// write up to (and including) the delimiter, then double it
-					writer.Write(remainingValue[..nextDelimiterIndex], flush: true);
+					writer.Write(remainingValue[..nextDelimiterIndex]);
 					if (remainingValue[nextDelimiterIndex] == '\\' && !noBackslashEscapes)
 						writer.Write((ushort) 0x5C5C); // \\
 					else if (remainingValue[nextDelimiterIndex] == '\\' && noBackslashEscapes)
@@ -531,9 +603,43 @@ static void WriteString(ByteBufferWriter writer, bool noBackslashEscapes, bool w
 				}
 			}
 
-			if (writeDelimiters)
-				writer.Write((byte) '\'');
+			writer.Write((byte) '\'');
 		}
+
+#if NETCOREAPP3_1_OR_GREATER
+		// Writes a partial chunk of a string (that may end with half of a surrogate pair), escaping any delimiter characters.
+		static void WriteStringChunk(ByteBufferWriter writer, bool noBackslashEscapes, ReadOnlySpan<char> value)
+		{
+			var charsWritten = 0;
+			while (charsWritten < value.Length)
+			{
+				var remainingValue = value[charsWritten..];
+				var nextDelimiterIndex = remainingValue.IndexOfAny('\0', '\'', '\\');
+				if (nextDelimiterIndex == -1)
+				{
+					// write the rest of the string
+					writer.Write(remainingValue, flush: false);
+					charsWritten += remainingValue.Length;
+				}
+				else
+				{
+					// write up to (and including) the delimiter, then double it
+					writer.Write(remainingValue[..nextDelimiterIndex], flush: true);
+					if (remainingValue[nextDelimiterIndex] == '\\' && !noBackslashEscapes)
+						writer.Write((ushort) 0x5C5C); // \\
+					else if (remainingValue[nextDelimiterIndex] == '\\' && noBackslashEscapes)
+						writer.Write((byte) 0x5C); // \
+					else if (remainingValue[nextDelimiterIndex] == '\'')
+						writer.Write((ushort) 0x2727); // ''
+					else if (remainingValue[nextDelimiterIndex] == '\0' && !noBackslashEscapes)
+						writer.Write((ushort) 0x305C); // \0
+					else if (remainingValue[nextDelimiterIndex] == '\0' && noBackslashEscapes)
+						writer.Write((byte) 0x00); // (nul)
+					charsWritten += nextDelimiterIndex + 1;
+				}
+			}
+		}
+#endif
 	}
 
 	internal void AppendBinary(ByteBufferWriter writer, StatementPreparerOptions options)
@@ -542,116 +648,169 @@ internal void AppendBinary(ByteBufferWriter writer, StatementPreparerOptions opt
 		{
 			// stored in "null bitmap" only
 		}
-		else if (Value is string stringValue)
+		else
+		{
+			AppendBinary(writer, Value, options);
+		}
+	}
+
+	private void AppendBinary(ByteBufferWriter writer, object value, StatementPreparerOptions options)
+	{
+		if (value is string stringValue)
 		{
 			writer.WriteLengthEncodedString(stringValue);
 		}
-		else if (Value is char charValue)
+		else if (value is char charValue)
 		{
 			writer.WriteLengthEncodedString(charValue.ToString());
 		}
-		else if (Value is sbyte sbyteValue)
+		else if (value is sbyte sbyteValue)
 		{
 			writer.Write(unchecked((byte) sbyteValue));
 		}
-		else if (Value is byte byteValue)
+		else if (value is byte byteValue)
 		{
 			writer.Write(byteValue);
 		}
-		else if (Value is bool boolValue)
+		else if (value is bool boolValue)
 		{
 			writer.Write((byte) (boolValue ? 1 : 0));
 		}
-		else if (Value is short shortValue)
+		else if (value is short shortValue)
 		{
 			writer.Write(unchecked((ushort) shortValue));
 		}
-		else if (Value is ushort ushortValue)
+		else if (value is ushort ushortValue)
 		{
 			writer.Write(ushortValue);
 		}
-		else if (Value is int intValue)
+		else if (value is int intValue)
 		{
 			writer.Write(intValue);
 		}
-		else if (Value is uint uintValue)
+		else if (value is uint uintValue)
 		{
 			writer.Write(uintValue);
 		}
-		else if (Value is long longValue)
+		else if (value is long longValue)
 		{
 			writer.Write(unchecked((ulong) longValue));
 		}
-		else if (Value is ulong ulongValue)
+		else if (value is ulong ulongValue)
 		{
 			writer.Write(ulongValue);
 		}
-		else if (Value is byte[] byteArrayValue)
+		else if (value is byte[] byteArrayValue)
 		{
 			writer.WriteLengthEncodedInteger(unchecked((ulong) byteArrayValue.Length));
 			writer.Write(byteArrayValue);
 		}
-		else if (Value is ReadOnlyMemory<byte> readOnlyMemoryValue)
+		else if (value is ReadOnlyMemory<byte> readOnlyMemoryValue)
 		{
 			writer.WriteLengthEncodedInteger(unchecked((ulong) readOnlyMemoryValue.Length));
 			writer.Write(readOnlyMemoryValue.Span);
 		}
-		else if (Value is Memory<byte> memoryValue)
+		else if (value is Memory<byte> memoryValue)
 		{
 			writer.WriteLengthEncodedInteger(unchecked((ulong) memoryValue.Length));
 			writer.Write(memoryValue.Span);
 		}
-		else if (Value is ArraySegment<byte> arraySegmentValue)
+		else if (value is ArraySegment<byte> arraySegmentValue)
 		{
 			writer.WriteLengthEncodedInteger(unchecked((ulong) arraySegmentValue.Count));
 			writer.Write(arraySegmentValue);
 		}
-		else if (Value is MySqlGeometry geometry)
+		else if (value is MySqlGeometry geometry)
 		{
 			writer.WriteLengthEncodedInteger(unchecked((ulong) geometry.ValueSpan.Length));
 			writer.Write(geometry.ValueSpan);
 		}
-		else if (Value is MemoryStream memoryStream)
+		else if (value is MemoryStream memoryStream)
 		{
 			if (!memoryStream.TryGetBuffer(out var streamBuffer))
 				streamBuffer = new ArraySegment<byte>(memoryStream.ToArray());
 			writer.WriteLengthEncodedInteger(unchecked((ulong) streamBuffer.Count));
 			writer.Write(streamBuffer);
 		}
-		else if (Value is float floatValue)
+		else if (value is Stream)
 		{
-			writer.Write(BitConverter.GetBytes(floatValue));
+			// do nothing; this will be sent via CommandKind.StatementSendLongData
 		}
-		else if (Value is double doubleValue)
+		else if (value is float floatValue)
 		{
-			writer.Write(unchecked((ulong) BitConverter.DoubleToInt64Bits(doubleValue)));
+#if NET5_0_OR_GREATER
+			Span<byte> bytes = stackalloc byte[4];
+			BinaryPrimitives.WriteSingleLittleEndian(bytes, floatValue);
+			writer.Write(bytes);
+#else
+			// convert float to bytes with correct endianness (MySQL uses little-endian)
+			var bytes = BitConverter.GetBytes(floatValue);
+			if (!BitConverter.IsLittleEndian)
+				Array.Reverse(bytes);
+			writer.Write(bytes);
+#endif
 		}
-		else if (Value is decimal decimalValue)
+		else if (value is double doubleValue)
 		{
-			writer.WriteLengthEncodedString(decimalValue.ToString(CultureInfo.InvariantCulture));
+#if NET5_0_OR_GREATER
+			Span<byte> bytes = stackalloc byte[8];
+			BinaryPrimitives.WriteDoubleLittleEndian(bytes, doubleValue);
+			writer.Write(bytes);
+#else
+			if (BitConverter.IsLittleEndian)
+			{
+				writer.Write(unchecked((ulong) BitConverter.DoubleToInt64Bits(doubleValue)));
+			}
+			else
+			{
+				// convert double to bytes with correct endianness (MySQL uses little-endian)
+				var bytes = BitConverter.GetBytes(doubleValue);
+				Array.Reverse(bytes);
+				writer.Write(bytes);
+			}
+#endif
 		}
-		else if (Value is BigInteger bigInteger)
+		else if (value is float[] floatArrayValue)
 		{
-			writer.WriteLengthEncodedString(bigInteger.ToString(CultureInfo.InvariantCulture));
+			writer.WriteLengthEncodedInteger(unchecked((ulong) floatArrayValue.Length * 4));
+			writer.Write(ConvertFloatsToBytes(floatArrayValue.AsSpan()));
 		}
-		else if (Value is MySqlDateTime mySqlDateTimeValue)
+		else if (value is Memory<float> floatMemory)
+		{
+			writer.WriteLengthEncodedInteger(unchecked((ulong) floatMemory.Length * 4));
+			writer.Write(ConvertFloatsToBytes(floatMemory.Span));
+		}
+		else if (value is ReadOnlyMemory<float> floatReadOnlyMemory)
+		{
+			writer.WriteLengthEncodedInteger(unchecked((ulong) floatReadOnlyMemory.Length * 4));
+			writer.Write(ConvertFloatsToBytes(floatReadOnlyMemory.Span));
+		}
+		else if (value is decimal decimalValue)
+		{
+			writer.WriteLengthEncodedAsciiString(decimalValue.ToString(CultureInfo.InvariantCulture));
+		}
+		else if (value is BigInteger bigInteger)
+		{
+			writer.WriteLengthEncodedAsciiString(bigInteger.ToString(CultureInfo.InvariantCulture));
+		}
+		else if (value is MySqlDateTime mySqlDateTimeValue)
 		{
 			if (mySqlDateTimeValue.IsValidDateTime)
 				WriteDateTime(writer, mySqlDateTimeValue.GetDateTime());
 			else
 				writer.Write((byte) 0);
 		}
-		else if (Value is MySqlDecimal mySqlDecimal)
+		else if (value is MySqlDecimal mySqlDecimal)
 		{
-			writer.WriteLengthEncodedString(mySqlDecimal.ToString());
+			writer.WriteLengthEncodedAsciiString(mySqlDecimal.ToString());
 		}
 #if NET6_0_OR_GREATER
-		else if (Value is DateOnly dateOnlyValue)
+		else if (value is DateOnly dateOnlyValue)
 		{
 			WriteDateOnly(writer, dateOnlyValue);
 		}
 #endif
-		else if (Value is DateTime dateTimeValue)
+		else if (value is DateTime dateTimeValue)
 		{
 			if ((options & StatementPreparerOptions.DateTimeUtc) != 0 && dateTimeValue.Kind == DateTimeKind.Local)
 				throw new MySqlException($"DateTime.Kind must not be Local when DateTimeKind setting is Utc (parameter name: {ParameterName})");
@@ -660,33 +819,44 @@ internal void AppendBinary(ByteBufferWriter writer, StatementPreparerOptions opt
 
 			WriteDateTime(writer, dateTimeValue);
 		}
-		else if (Value is DateTimeOffset dateTimeOffsetValue)
+		else if (value is DateTimeOffset dateTimeOffsetValue)
 		{
 			// store as UTC as it will be read as such when deserialized from a timespan column
 			WriteDateTime(writer, dateTimeOffsetValue.UtcDateTime);
 		}
 #if NET6_0_OR_GREATER
-		else if (Value is TimeOnly timeOnlyValue)
+		else if (value is TimeOnly timeOnlyValue)
 		{
 			WriteTime(writer, timeOnlyValue.ToTimeSpan());
 		}
 #endif
-		else if (Value is TimeSpan ts)
+		else if (value is TimeSpan ts)
 		{
 			WriteTime(writer, ts);
 		}
-		else if (Value is Guid guidValue)
+		else if (value is Guid guidValue)
 		{
 			StatementPreparerOptions guidOptions = options & StatementPreparerOptions.GuidFormatMask;
 			if (guidOptions is StatementPreparerOptions.GuidFormatBinary16 or StatementPreparerOptions.GuidFormatTimeSwapBinary16 or StatementPreparerOptions.GuidFormatLittleEndianBinary16)
 			{
+#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER
+				Span<byte> bytes = stackalloc byte[16];
+#if NET8_0_OR_GREATER
+				guidValue.TryWriteBytes(bytes, bigEndian: guidOptions != StatementPreparerOptions.GuidFormatLittleEndianBinary16, out _);
+#else
+				guidValue.TryWriteBytes(bytes);
+#endif
+#else
 				var bytes = guidValue.ToByteArray();
+#endif
 				if (guidOptions != StatementPreparerOptions.GuidFormatLittleEndianBinary16)
 				{
+#if !NET8_0_OR_GREATER
 					Utility.SwapBytes(bytes, 0, 3);
 					Utility.SwapBytes(bytes, 1, 2);
 					Utility.SwapBytes(bytes, 4, 5);
 					Utility.SwapBytes(bytes, 6, 7);
+#endif
 
 					if (guidOptions == StatementPreparerOptions.GuidFormatTimeSwapBinary16)
 					{
@@ -711,53 +881,55 @@ internal void AppendBinary(ByteBufferWriter writer, StatementPreparerOptions opt
 				writer.Advance(guidLength);
 			}
 		}
-		else if (Value is ReadOnlyMemory<char> readOnlyMemoryChar)
+		else if (value is ReadOnlyMemory<char> readOnlyMemoryChar)
 		{
 			writer.WriteLengthEncodedString(readOnlyMemoryChar.Span);
 		}
-		else if (Value is Memory<char> memoryChar)
+		else if (value is Memory<char> memoryChar)
 		{
 			writer.WriteLengthEncodedString(memoryChar.Span);
 		}
-		else if (Value is StringBuilder stringBuilder)
+		else if (value is StringBuilder stringBuilder)
 		{
 			writer.WriteLengthEncodedString(stringBuilder);
 		}
+		else if ((MySqlDbType is MySqlDbType.String or MySqlDbType.VarChar) && HasSetDbType && value is Enum stringEnumValue)
+		{
+			writer.WriteLengthEncodedString(stringEnumValue.ToString("G"));
+		}
+		else if (value is Enum)
+		{
+			// using the underlying type matches the log in TypeMapper.GetDbTypeMapping, which controls the column type value that was sent to the server
+			var underlyingValue = Convert.ChangeType(value, Enum.GetUnderlyingType(value.GetType()), CultureInfo.InvariantCulture);
+			AppendBinary(writer, underlyingValue, options);
+		}
 		else if (MySqlDbType == MySqlDbType.Int16)
 		{
-			writer.Write((ushort) (short) Value);
+			writer.Write((ushort) (short) value);
 		}
 		else if (MySqlDbType == MySqlDbType.UInt16)
 		{
-			writer.Write((ushort) Value);
+			writer.Write((ushort) value);
 		}
 		else if (MySqlDbType == MySqlDbType.Int32)
 		{
-			writer.Write((int) Value);
+			writer.Write((int) value);
 		}
 		else if (MySqlDbType == MySqlDbType.UInt32)
 		{
-			writer.Write((uint) Value);
+			writer.Write((uint) value);
 		}
 		else if (MySqlDbType == MySqlDbType.Int64)
 		{
-			writer.Write((ulong) (long) Value);
+			writer.Write((ulong) (long) value);
 		}
 		else if (MySqlDbType == MySqlDbType.UInt64)
 		{
-			writer.Write((ulong) Value);
-		}
-		else if ((MySqlDbType is MySqlDbType.String or MySqlDbType.VarChar) && HasSetDbType && Value is Enum stringEnumValue)
-		{
-			writer.WriteLengthEncodedString(stringEnumValue.ToString("G"));
-		}
-		else if (Value is Enum)
-		{
-			writer.Write(Convert.ToInt32(Value, CultureInfo.InvariantCulture));
+			writer.Write((ulong) value);
 		}
 		else
 		{
-			throw new NotSupportedException($"Parameter type {Value.GetType().Name} is not supported; see https://fl.vu/mysql-param-type. Value: {Value}");
+			throw new NotSupportedException($"Parameter type {value.GetType().Name} is not supported; see https://mysqlconnector.net/param-type. Value: {value}");
 		}
 	}
 
@@ -830,6 +1002,32 @@ private static void WriteTime(ByteBufferWriter writer, TimeSpan timeSpan)
 		}
 	}
 
+	private static ReadOnlySpan<byte> ConvertFloatsToBytes(ReadOnlySpan<float> floats)
+	{
+		if (BitConverter.IsLittleEndian)
+		{
+			return MemoryMarshal.AsBytes(floats);
+		}
+		else
+		{
+			// for big-endian platforms, we need to convert each float individually
+			var bytes = new byte[floats.Length * 4];
+
+			for (var i = 0; i < floats.Length; i++)
+			{
+#if NET5_0_OR_GREATER
+				BinaryPrimitives.WriteSingleLittleEndian(bytes.AsSpan(i * 4), floats[i]);
+#else
+				var floatBytes = BitConverter.GetBytes(floats[i]);
+				Array.Reverse(floatBytes);
+				floatBytes.CopyTo(bytes, i * 4);
+#endif
+			}
+
+			return bytes;
+		}
+	}
+
 	private static ReadOnlySpan<byte> BinaryBytes => "_binary'"u8;
 
 	private DbType m_dbType;
diff --git a/src/MySqlConnector/MySqlParameterCollection.cs b/src/MySqlConnector/MySqlParameterCollection.cs
index 655778dc6..e9012d37e 100644
--- a/src/MySqlConnector/MySqlParameterCollection.cs
+++ b/src/MySqlConnector/MySqlParameterCollection.cs
@@ -8,7 +8,7 @@ public sealed class MySqlParameterCollection : DbParameterCollection, IEnumerabl
 {
 	internal MySqlParameterCollection()
 	{
-		m_parameters = new();
+		m_parameters = [];
 		m_nameToIndex = new(StringComparer.OrdinalIgnoreCase);
 	}
 
@@ -25,13 +25,25 @@ public MySqlParameter Add(string parameterName, DbType dbType)
 
 	public override int Add(object value)
 	{
-		AddParameter((MySqlParameter) (value ?? throw new ArgumentNullException(nameof(value))), m_parameters.Count);
+#if NET6_0_OR_GREATER
+		ArgumentNullException.ThrowIfNull(value);
+#else
+		if (value is null)
+			throw new ArgumentNullException(nameof(value));
+#endif
+		AddParameter((MySqlParameter) value, m_parameters.Count);
 		return m_parameters.Count - 1;
 	}
 
 	public MySqlParameter Add(MySqlParameter parameter)
 	{
-		AddParameter(parameter ?? throw new ArgumentNullException(nameof(parameter)), m_parameters.Count);
+#if NET6_0_OR_GREATER
+		ArgumentNullException.ThrowIfNull(parameter);
+#else
+		if (parameter is null)
+			throw new ArgumentNullException(nameof(parameter));
+#endif
+		AddParameter(parameter, m_parameters.Count);
 		return parameter;
 	}
 
@@ -123,7 +135,13 @@ public override void RemoveAt(int index)
 
 	protected override void SetParameter(int index, DbParameter value)
 	{
-		var newParameter = (MySqlParameter) (value ?? throw new ArgumentNullException(nameof(value)));
+#if NET6_0_OR_GREATER
+		ArgumentNullException.ThrowIfNull(value);
+#else
+		if (value is null)
+			throw new ArgumentNullException(nameof(value));
+#endif
+		var newParameter = (MySqlParameter) value;
 		var oldParameter = m_parameters[index];
 		if (oldParameter.NormalizedParameterName is not null)
 			m_nameToIndex.Remove(oldParameter.NormalizedParameterName);
diff --git a/src/MySqlConnector/MySqlProtocolException.cs b/src/MySqlConnector/MySqlProtocolException.cs
index dd51a6e76..894534ca6 100644
--- a/src/MySqlConnector/MySqlProtocolException.cs
+++ b/src/MySqlConnector/MySqlProtocolException.cs
@@ -15,8 +15,11 @@ public sealed class MySqlProtocolException : InvalidOperationException
 	/// <param name="packetSequenceNumber">The actual packet sequence number.</param>
 	/// <returns>A new <see cref="MySqlProtocolException"/>.</returns>
 	internal static MySqlProtocolException CreateForPacketOutOfOrder(int expectedSequenceNumber, int packetSequenceNumber) =>
-		new MySqlProtocolException($"Packet received out-of-order. Expected {expectedSequenceNumber:d}; got {packetSequenceNumber:d}.");
+		new($"Packet received out-of-order. Expected {expectedSequenceNumber:d}; got {packetSequenceNumber:d}.");
 
+#if NET8_0_OR_GREATER
+	[Obsolete(DiagnosticId = "SYSLIB0051")]
+#endif
 	private MySqlProtocolException(SerializationInfo info, StreamingContext context)
 		: base(info, context)
 	{
diff --git a/src/MySqlConnector/MySqlTransaction.cs b/src/MySqlConnector/MySqlTransaction.cs
index ded7bfa59..4896d357c 100644
--- a/src/MySqlConnector/MySqlTransaction.cs
+++ b/src/MySqlConnector/MySqlTransaction.cs
@@ -1,4 +1,6 @@
 using System.Net.Sockets;
+using Microsoft.Extensions.Logging;
+using MySqlConnector.Logging;
 using MySqlConnector.Protocol.Serialization;
 using MySqlConnector.Utilities;
 
@@ -30,13 +32,14 @@ private async Task CommitAsync(IOBehavior ioBehavior, CancellationToken cancella
 		VerifyValid();
 
 		using var activity = Connection!.Session.StartActivity("Commit");
+		Log.CommittingTransaction(m_logger, Connection.Session.Id);
 		try
 		{
 			using (var cmd = new MySqlCommand("commit", Connection, this) { NoActivity = true })
 				await cmd.ExecuteNonQueryAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
 			Connection!.CurrentTransaction = null;
+			Log.CommittedTransaction(m_logger, Connection.Session.Id);
 			Connection = null;
-			activity?.SetSuccess();
 		}
 		catch (Exception ex) when (activity is { IsAllDataRequested: true })
 		{
@@ -150,10 +153,18 @@ private async Task ExecuteSavepointAsync(string command, string savepointName, I
 	{
 		VerifyValid();
 
+#if NET6_0_OR_GREATER
+		ArgumentNullException.ThrowIfNull(savepointName);
+#else
 		if (savepointName is null)
 			throw new ArgumentNullException(nameof(savepointName));
+#endif
+#if NET8_0_OR_GREATER
+		ArgumentException.ThrowIfNullOrEmpty(savepointName);
+#else
 		if (savepointName.Length == 0)
 			throw new ArgumentException("savepointName must not be empty", nameof(savepointName));
+#endif
 
 		using var cmd = new MySqlCommand(command + "savepoint " + QuoteIdentifier(savepointName), Connection, this) { NoActivity = true };
 		await cmd.ExecuteNonQueryAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
@@ -247,20 +258,24 @@ private async Task DoDisposeAsync(IOBehavior ioBehavior, CancellationToken cance
 		Connection = null;
 	}
 
-	internal MySqlTransaction(MySqlConnection connection, IsolationLevel isolationLevel)
+	internal MySqlTransaction(MySqlConnection connection, IsolationLevel isolationLevel, ILogger logger)
 	{
 		Connection = connection;
 		IsolationLevel = isolationLevel;
+		m_logger = logger;
+
+		Log.StartedTransaction(m_logger, Connection.Session.Id);
 	}
 
 	private async Task DoRollback(IOBehavior ioBehavior, CancellationToken cancellationToken)
 	{
 		using var activity = Connection!.Session.StartActivity("Rollback");
+		Log.RollingBackTransaction(m_logger, Connection.Session.Id);
 		try
 		{
 			using var cmd = new MySqlCommand("rollback", Connection, this) { NoActivity = true };
 			await cmd.ExecuteNonQueryAsync(ioBehavior, cancellationToken).ConfigureAwait(false);
-			activity?.SetSuccess();
+			Log.RolledBackTransaction(m_logger, Connection.Session.Id);
 		}
 		catch (Exception ex) when (activity is { IsAllDataRequested: true })
 		{
@@ -271,8 +286,12 @@ private async Task DoRollback(IOBehavior ioBehavior, CancellationToken cancellat
 
 	private void VerifyValid()
 	{
+#if NET7_0_OR_GREATER
+		ObjectDisposedException.ThrowIf(m_isDisposed, this);
+#else
 		if (m_isDisposed)
 			throw new ObjectDisposedException(nameof(MySqlTransaction));
+#endif
 		if (Connection is null)
 			throw new InvalidOperationException("Already committed or rolled back.");
 		if (Connection.CurrentTransaction is null)
@@ -283,5 +302,6 @@ private void VerifyValid()
 
 	private static string QuoteIdentifier(string identifier) => "`" + identifier.Replace("`", "``") + "`";
 
+	private readonly ILogger m_logger;
 	private bool m_isDisposed;
 }
diff --git a/src/MySqlConnector/Plugins/ZstandardPlugin.cs b/src/MySqlConnector/Plugins/ZstandardPlugin.cs
new file mode 100644
index 000000000..3696067c3
--- /dev/null
+++ b/src/MySqlConnector/Plugins/ZstandardPlugin.cs
@@ -0,0 +1,9 @@
+using MySqlConnector.Protocol.Serialization;
+
+namespace MySqlConnector.Plugins;
+
+internal abstract class ZstandardPlugin
+{
+	public abstract IPayloadHandler CreatePayloadHandler(IByteHandler byteHandler);
+	public abstract int CompressionLevel { get; }
+}
diff --git a/src/MySqlConnector/Protocol/CharacterSet.cs b/src/MySqlConnector/Protocol/CharacterSet.cs
index b2833f5b7..4bdbf4fb2 100644
--- a/src/MySqlConnector/Protocol/CharacterSet.cs
+++ b/src/MySqlConnector/Protocol/CharacterSet.cs
@@ -296,9 +296,11 @@ internal enum CharacterSet : ushort
 	Utf8Mb3CroatianCaseInsensitiveMariaDb = 576,
 	Utf8Mb3MyanmarCaseInsensitive = 577,
 	Utf8Mb3ThaiUnicode520Weight2 = 578,
+	Utf8Mb3General1400AccentSensitiveCaseInsensitive = 579,
 	Utf8Mb4CroatianCaseInsensitiveMariaDb = 608,
 	Utf8Mb4MyanmarCaseInsensitive = 609,
 	Utf8Mb4ThaiUnicode520Weight2 = 610,
+	Utf8Mb4General1400AccentSensitiveCaseInsensitive = 611,
 	Ucs2CroatianCaseInsensitiveMariaDb = 640,
 	Ucs2MyanmarCaseInsensitive = 641,
 	Ucs2ThaiUnicode520Weight2 = 642,
diff --git a/src/MySqlConnector/Protocol/ColumnType.cs b/src/MySqlConnector/Protocol/ColumnType.cs
index 4b3701106..b031a41a5 100644
--- a/src/MySqlConnector/Protocol/ColumnType.cs
+++ b/src/MySqlConnector/Protocol/ColumnType.cs
@@ -24,6 +24,7 @@ internal enum ColumnType
 	Bit = 16,
 	Timestamp2 = 17,
 	DateTime2 = 18,
+	Vector = 242,
 	Json = 0xF5,
 	NewDecimal = 0xF6,
 	Enum = 0xF7,
diff --git a/src/MySqlConnector/Protocol/CommandKind.cs b/src/MySqlConnector/Protocol/CommandKind.cs
index 8b232707f..0d546721c 100644
--- a/src/MySqlConnector/Protocol/CommandKind.cs
+++ b/src/MySqlConnector/Protocol/CommandKind.cs
@@ -9,6 +9,6 @@ internal enum CommandKind
 	ChangeUser = 17,
 	StatementPrepare = 22,
 	StatementExecute = 23,
+	StatementSendLongData = 24,
 	ResetConnection = 31,
-	Multi = 254,
 }
diff --git a/src/MySqlConnector/Protocol/Payloads/AuthenticationMethodSwitchRequestPayload.cs b/src/MySqlConnector/Protocol/Payloads/AuthenticationMethodSwitchRequestPayload.cs
index dc2ec9233..275daa133 100644
--- a/src/MySqlConnector/Protocol/Payloads/AuthenticationMethodSwitchRequestPayload.cs
+++ b/src/MySqlConnector/Protocol/Payloads/AuthenticationMethodSwitchRequestPayload.cs
@@ -4,7 +4,7 @@
 
 namespace MySqlConnector.Protocol.Payloads;
 
-internal sealed class AuthenticationMethodSwitchRequestPayload
+internal readonly struct AuthenticationMethodSwitchRequestPayload
 {
 	public string Name { get; }
 	public byte[] Data { get; }
@@ -22,7 +22,7 @@ public static AuthenticationMethodSwitchRequestPayload Create(ReadOnlySpan<byte>
 			// if the packet is just the header byte (0xFE), it's an "Old Authentication Method Switch Request Packet"
 			// (possibly sent by a server that doesn't support CLIENT_PLUGIN_AUTH)
 			name = "mysql_old_password";
-			data = Array.Empty<byte>();
+			data = [];
 		}
 		else
 		{
diff --git a/src/MySqlConnector/Protocol/Payloads/AuthenticationMoreDataPayload.cs b/src/MySqlConnector/Protocol/Payloads/AuthenticationMoreDataPayload.cs
index 0346ed443..c5e27f9a9 100644
--- a/src/MySqlConnector/Protocol/Payloads/AuthenticationMoreDataPayload.cs
+++ b/src/MySqlConnector/Protocol/Payloads/AuthenticationMoreDataPayload.cs
@@ -2,7 +2,7 @@
 
 namespace MySqlConnector.Protocol.Payloads;
 
-internal sealed class AuthenticationMoreDataPayload
+internal readonly struct AuthenticationMoreDataPayload
 {
 	public byte[] Data { get; }
 
diff --git a/src/MySqlConnector/Protocol/Payloads/CachingSha2ServerResponsePayload.cs b/src/MySqlConnector/Protocol/Payloads/CachingSha2ServerResponsePayload.cs
index 24a27cdc1..698eaba75 100644
--- a/src/MySqlConnector/Protocol/Payloads/CachingSha2ServerResponsePayload.cs
+++ b/src/MySqlConnector/Protocol/Payloads/CachingSha2ServerResponsePayload.cs
@@ -2,7 +2,7 @@
 
 namespace MySqlConnector.Protocol.Payloads;
 
-internal sealed class CachingSha2ServerResponsePayload
+internal readonly struct CachingSha2ServerResponsePayload
 {
 	public const byte Signature = 0x01;
 
diff --git a/src/MySqlConnector/Protocol/Payloads/ColumnCountPayload.cs b/src/MySqlConnector/Protocol/Payloads/ColumnCountPayload.cs
new file mode 100644
index 000000000..8e407ac89
--- /dev/null
+++ b/src/MySqlConnector/Protocol/Payloads/ColumnCountPayload.cs
@@ -0,0 +1,28 @@
+using MySqlConnector.Protocol.Serialization;
+
+namespace MySqlConnector.Protocol.Payloads;
+
+/// <summary>
+///     Helper class to parse Column count packet.
+///     https://mariadb.com/kb/en/result-set-packets/#column-count-packet
+///     Packet contains a columnCount, and - if capability MARIADB_CLIENT_CACHE_METADATA is set - a flag to indicate if metadata follows
+/// </summary>
+internal readonly struct ColumnCountPayload
+{
+	private ColumnCountPayload(int columnCount, bool metadataFollows)
+	{
+		ColumnCount = columnCount;
+		MetadataFollows = metadataFollows;
+	}
+
+	public int ColumnCount { get; }
+	public bool MetadataFollows { get; }
+
+	public static ColumnCountPayload Create(ReadOnlySpan<byte> span, bool supportsOptionalMetadata)
+	{
+		var reader = new ByteArrayReader(span);
+		var columnCount = (int) reader.ReadLengthEncodedInteger();
+		var metadataFollows = !supportsOptionalMetadata || reader.BytesRemaining == 0 || reader.ReadByte() == 1;
+		return new ColumnCountPayload(columnCount, metadataFollows);
+	}
+}
diff --git a/src/MySqlConnector/Protocol/Payloads/ColumnDefinitionPayload.cs b/src/MySqlConnector/Protocol/Payloads/ColumnDefinitionPayload.cs
index 7673c854c..3bbc54a73 100644
--- a/src/MySqlConnector/Protocol/Payloads/ColumnDefinitionPayload.cs
+++ b/src/MySqlConnector/Protocol/Payloads/ColumnDefinitionPayload.cs
@@ -16,13 +16,13 @@ public string Name
 		}
 	}
 
-	public CharacterSet CharacterSet { get; }
+	public CharacterSet CharacterSet { get; private set; }
 
-	public uint ColumnLength { get; }
+	public uint ColumnLength { get; private set; }
 
-	public ColumnType ColumnType { get; }
+	public ColumnType ColumnType { get; private set; }
 
-	public ColumnFlags ColumnFlags { get; }
+	public ColumnFlags ColumnFlags { get; private set; }
 
 	public string SchemaName
 	{
@@ -74,11 +74,18 @@ public string PhysicalName
 		}
 	}
 
-	public byte Decimals { get; }
+	public byte Decimals { get; private set; }
 
-	public static ColumnDefinitionPayload Create(ResizableArraySegment<byte> arraySegment)
+	public static void Initialize(ref ColumnDefinitionPayload payload, ResizableArraySegment<byte> arraySegment)
 	{
-		var reader = new ByteArrayReader(arraySegment);
+		payload ??= new ColumnDefinitionPayload();
+		payload.Initialize(arraySegment);
+	}
+
+	private void Initialize(ResizableArraySegment<byte> originalData)
+	{
+		m_originalData = originalData;
+		var reader = new ByteArrayReader(originalData);
 		SkipLengthEncodedByteString(ref reader); // catalog
 		SkipLengthEncodedByteString(ref reader); // schema
 		SkipLengthEncodedByteString(ref reader); // table
@@ -86,15 +93,24 @@ public static ColumnDefinitionPayload Create(ResizableArraySegment<byte> arraySe
 		SkipLengthEncodedByteString(ref reader); // name
 		SkipLengthEncodedByteString(ref reader); // physical name
 		reader.ReadByte(0x0C); // length of fixed-length fields, always 0x0C
-		var characterSet = (CharacterSet) reader.ReadUInt16();
-		var columnLength = reader.ReadUInt32();
-		var columnType = (ColumnType) reader.ReadByte();
-		var columnFlags = (ColumnFlags) reader.ReadUInt16();
-		var decimals = reader.ReadByte(); // 0x00 for integers and static strings, 0x1f for dynamic strings, double, float, 0x00 to 0x51 for decimals
+		CharacterSet = (CharacterSet) reader.ReadUInt16();
+		ColumnLength = reader.ReadUInt32();
+		ColumnType = (ColumnType) reader.ReadByte();
+		ColumnFlags = (ColumnFlags) reader.ReadUInt16();
+		Decimals = reader.ReadByte(); // 0x00 for integers and static strings, 0x1f for dynamic strings, double, float, 0x00 to 0x51 for decimals
 		reader.ReadByte(0); // reserved byte 1
 		reader.ReadByte(0); // reserved byte 2
 
-		return new ColumnDefinitionPayload(arraySegment, characterSet, columnLength, columnType, columnFlags, decimals);
+		if (m_readNames)
+		{
+			m_catalogName = null;
+			m_schemaName = null;
+			m_table = null;
+			m_physicalTable = null;
+			m_name = null;
+			m_physicalName = null;
+			m_readNames = false;
+		}
 	}
 
 	private static void SkipLengthEncodedByteString(ref ByteArrayReader reader)
@@ -103,19 +119,13 @@ private static void SkipLengthEncodedByteString(ref ByteArrayReader reader)
 		reader.Offset += length;
 	}
 
-	private ColumnDefinitionPayload(ResizableArraySegment<byte> originalData, CharacterSet characterSet, uint columnLength, ColumnType columnType, ColumnFlags columnFlags, byte decimals)
+	private ColumnDefinitionPayload()
 	{
-		OriginalData = originalData;
-		CharacterSet = characterSet;
-		ColumnLength = columnLength;
-		ColumnType = columnType;
-		ColumnFlags = columnFlags;
-		Decimals = decimals;
 	}
 
 	private void ReadNames()
 	{
-		var reader = new ByteArrayReader(OriginalData);
+		var reader = new ByteArrayReader(m_originalData);
 		m_catalogName = Encoding.UTF8.GetString(reader.ReadLengthEncodedByteString());
 		m_schemaName = Encoding.UTF8.GetString(reader.ReadLengthEncodedByteString());
 		m_table = Encoding.UTF8.GetString(reader.ReadLengthEncodedByteString());
@@ -125,8 +135,7 @@ private void ReadNames()
 		m_readNames = true;
 	}
 
-	private ResizableArraySegment<byte> OriginalData { get; }
-
+	private ResizableArraySegment<byte> m_originalData;
 	private bool m_readNames;
 	private string? m_name;
 	private string? m_schemaName;
diff --git a/src/MySqlConnector/Protocol/Payloads/EmptyPayload.cs b/src/MySqlConnector/Protocol/Payloads/EmptyPayload.cs
index 66a6e503f..6a01a808c 100644
--- a/src/MySqlConnector/Protocol/Payloads/EmptyPayload.cs
+++ b/src/MySqlConnector/Protocol/Payloads/EmptyPayload.cs
@@ -1,8 +1,6 @@
-using MySqlConnector.Utilities;
-
 namespace MySqlConnector.Protocol.Payloads;
 
 internal static class EmptyPayload
 {
-	public static PayloadData Instance { get; } = new(Array.Empty<byte>());
+	public static PayloadData Instance { get; } = new([]);
 }
diff --git a/src/MySqlConnector/Protocol/Payloads/EofPayload.cs b/src/MySqlConnector/Protocol/Payloads/EofPayload.cs
index f58d57960..79fff182b 100644
--- a/src/MySqlConnector/Protocol/Payloads/EofPayload.cs
+++ b/src/MySqlConnector/Protocol/Payloads/EofPayload.cs
@@ -3,7 +3,7 @@
 namespace MySqlConnector.Protocol.Payloads;
 
 // See https://dev.mysql.com/doc/internals/en/packet-EOF_Packet.html
-internal sealed class EofPayload
+internal readonly struct EofPayload
 {
 	public int WarningCount { get; }
 	public ServerStatus ServerStatus { get; }
diff --git a/src/MySqlConnector/Protocol/Payloads/ErrorPayload.cs b/src/MySqlConnector/Protocol/Payloads/ErrorPayload.cs
index 628e47d2d..23c2b2274 100644
--- a/src/MySqlConnector/Protocol/Payloads/ErrorPayload.cs
+++ b/src/MySqlConnector/Protocol/Payloads/ErrorPayload.cs
@@ -5,13 +5,13 @@
 namespace MySqlConnector.Protocol.Payloads;
 
 // See https://dev.mysql.com/doc/internals/en/packet-ERR_Packet.html
-internal sealed class ErrorPayload
+internal readonly struct ErrorPayload
 {
 	public int ErrorCode { get; }
 	public string State { get; }
 	public string Message { get; }
 
-	public MySqlException ToException() => new MySqlException((MySqlErrorCode) ErrorCode, State, Message);
+	public MySqlException ToException() => new((MySqlErrorCode) ErrorCode, State, Message);
 
 	public static ErrorPayload Create(ReadOnlySpan<byte> span)
 	{
diff --git a/src/MySqlConnector/Protocol/Payloads/HandshakeResponse41Payload.cs b/src/MySqlConnector/Protocol/Payloads/HandshakeResponse41Payload.cs
index 1acb02c9d..d817e0290 100644
--- a/src/MySqlConnector/Protocol/Payloads/HandshakeResponse41Payload.cs
+++ b/src/MySqlConnector/Protocol/Payloads/HandshakeResponse41Payload.cs
@@ -1,15 +1,16 @@
 using MySqlConnector.Core;
 using MySqlConnector.Protocol.Serialization;
+using MySqlConnector.Utilities;
 
 namespace MySqlConnector.Protocol.Payloads;
 
 internal static class HandshakeResponse41Payload
 {
-	private static ByteBufferWriter CreateCapabilitiesPayload(ProtocolCapabilities serverCapabilities, ConnectionSettings cs, bool useCompression, CharacterSet characterSet, ProtocolCapabilities additionalCapabilities = 0)
+	private static ByteBufferWriter CreateCapabilitiesPayload(ProtocolCapabilities serverCapabilities, ConnectionSettings cs, CompressionMethod compressionMethod, CharacterSet characterSet, ProtocolCapabilities additionalCapabilities = 0)
 	{
 		var writer = new ByteBufferWriter();
 
-		writer.Write((int) (
+		var clientCapabilities =
 			ProtocolCapabilities.Protocol41 |
 			(cs.InteractiveSession ? (serverCapabilities & ProtocolCapabilities.Interactive) : 0) |
 			(serverCapabilities & ProtocolCapabilities.LongPassword) |
@@ -22,23 +23,27 @@ private static ByteBufferWriter CreateCapabilitiesPayload(ProtocolCapabilities s
 			(cs.AllowLoadLocalInfile ? ProtocolCapabilities.LocalFiles : 0) |
 			(string.IsNullOrWhiteSpace(cs.Database) ? 0 : ProtocolCapabilities.ConnectWithDatabase) |
 			(cs.UseAffectedRows ? 0 : ProtocolCapabilities.FoundRows) |
-			(useCompression ? ProtocolCapabilities.Compress : ProtocolCapabilities.None) |
+			(compressionMethod == CompressionMethod.Zlib ? ProtocolCapabilities.Compress : ProtocolCapabilities.None) |
 			(serverCapabilities & ProtocolCapabilities.ConnectionAttributes) |
 			(serverCapabilities & ProtocolCapabilities.SessionTrack) |
 			(serverCapabilities & ProtocolCapabilities.DeprecateEof) |
+			(compressionMethod == CompressionMethod.Zstandard ? ProtocolCapabilities.ZstandardCompressionAlgorithm : 0) |
 			(serverCapabilities & ProtocolCapabilities.QueryAttributes) |
-			additionalCapabilities));
+			(serverCapabilities & ProtocolCapabilities.MariaDbCacheMetadata) |
+			additionalCapabilities;
+
+		writer.Write((int) clientCapabilities);
 		writer.Write(0x4000_0000);
 		writer.Write((byte) characterSet);
 
 		// NOTE: not new byte[19]; see https://github.com/dotnet/roslyn/issues/33088
-		ReadOnlySpan<byte> padding = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
+		ReadOnlySpan<byte> padding = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
 		writer.Write(padding);
 
 		if ((serverCapabilities & ProtocolCapabilities.LongPassword) == 0)
 		{
 			// MariaDB writes extended capabilities at the end of the padding
-			writer.Write((int) (((long) (serverCapabilities & ProtocolCapabilities.MariaDbComMulti)) >> 32));
+			writer.Write((int) ((ulong) clientCapabilities >> 32));
 		}
 		else
 		{
@@ -48,15 +53,15 @@ private static ByteBufferWriter CreateCapabilitiesPayload(ProtocolCapabilities s
 		return writer;
 	}
 
-	public static PayloadData CreateWithSsl(ProtocolCapabilities serverCapabilities, ConnectionSettings cs, bool useCompression, CharacterSet characterSet) =>
-		CreateCapabilitiesPayload(serverCapabilities, cs, useCompression, characterSet, ProtocolCapabilities.Ssl).ToPayloadData();
+	public static PayloadData CreateWithSsl(ProtocolCapabilities serverCapabilities, ConnectionSettings cs, CompressionMethod compressionMethod, CharacterSet characterSet) =>
+		CreateCapabilitiesPayload(serverCapabilities, cs, compressionMethod, characterSet, ProtocolCapabilities.Ssl).ToPayloadData();
 
-	public static PayloadData Create(InitialHandshakePayload handshake, ConnectionSettings cs, string password, bool useCompression, CharacterSet characterSet, byte[]? connectionAttributes)
+	public static PayloadData Create(InitialHandshakePayload handshake, ConnectionSettings cs, byte[] authenticationResponse, CompressionMethod compressionMethod, int? compressionLevel, CharacterSet characterSet, byte[]? connectionAttributes)
 	{
 		// TODO: verify server capabilities
-		var writer = CreateCapabilitiesPayload(handshake.ProtocolCapabilities, cs, useCompression, characterSet);
+		var writer = CreateCapabilitiesPayload(handshake.ProtocolCapabilities, cs, compressionMethod, characterSet);
 		writer.WriteNullTerminatedString(cs.UserID);
-		var authenticationResponse = AuthenticationUtility.CreateAuthenticationResponse(handshake.AuthPluginData, password);
+
 		writer.Write((byte) authenticationResponse.Length);
 		writer.Write(authenticationResponse);
 
@@ -64,11 +69,15 @@ public static PayloadData Create(InitialHandshakePayload handshake, ConnectionSe
 			writer.WriteNullTerminatedString(cs.Database);
 
 		if ((handshake.ProtocolCapabilities & ProtocolCapabilities.PluginAuth) != 0)
-			writer.Write("mysql_native_password\0"u8);
+			writer.Write(handshake.AuthPluginName == "caching_sha2_password" ? "caching_sha2_password\0"u8 : "mysql_native_password\0"u8);
 
 		if (connectionAttributes is not null)
 			writer.Write(connectionAttributes);
 
+		// Zstandard compression level
+		if (compressionMethod == CompressionMethod.Zstandard)
+			writer.Write((byte) (compressionLevel ?? 10));
+
 		return writer.ToPayloadData();
 	}
 }
diff --git a/src/MySqlConnector/Protocol/Payloads/InitialHandshakePayload.cs b/src/MySqlConnector/Protocol/Payloads/InitialHandshakePayload.cs
index e9c6f53fc..2beed8ed3 100644
--- a/src/MySqlConnector/Protocol/Payloads/InitialHandshakePayload.cs
+++ b/src/MySqlConnector/Protocol/Payloads/InitialHandshakePayload.cs
@@ -42,9 +42,11 @@ public static InitialHandshakePayload Create(ReadOnlySpan<byte> span)
 			if ((protocolCapabilities & ProtocolCapabilities.SecureConnection) != 0)
 			{
 				var authPluginData2 = reader.ReadByteString(Math.Max(13, authPluginDataLength - 8));
+
+				// TODO: authPluginData = [..authPluginData1, ..authPluginData2]; when codegen doesn't use an intermediate List<byte>
 				authPluginData = new byte[authPluginData1.Length + authPluginData2.Length];
 				authPluginData1.CopyTo(authPluginData);
-				authPluginData2.CopyTo(new Span<byte>(authPluginData)[authPluginData1.Length..]);
+				authPluginData2.CopyTo(authPluginData.AsSpan(authPluginData1.Length));
 			}
 			if ((protocolCapabilities & ProtocolCapabilities.PluginAuth) != 0)
 				authPluginName = Encoding.UTF8.GetString(reader.ReadNullOrEofTerminatedByteString());
diff --git a/src/MySqlConnector/Protocol/Payloads/LocalInfilePayload.cs b/src/MySqlConnector/Protocol/Payloads/LocalInfilePayload.cs
index e68068b4c..72badfcbd 100644
--- a/src/MySqlConnector/Protocol/Payloads/LocalInfilePayload.cs
+++ b/src/MySqlConnector/Protocol/Payloads/LocalInfilePayload.cs
@@ -5,7 +5,7 @@
 namespace MySqlConnector.Protocol.Payloads;
 
 // See https://dev.mysql.com/doc/internals/en/com-query-response.html#local-infile-request
-internal sealed class LocalInfilePayload
+internal readonly struct LocalInfilePayload
 {
 	public const byte Signature = 0xFB;
 
diff --git a/src/MySqlConnector/Protocol/Payloads/OkPayload.cs b/src/MySqlConnector/Protocol/Payloads/OkPayload.cs
index 431557666..b1bc70139 100644
--- a/src/MySqlConnector/Protocol/Payloads/OkPayload.cs
+++ b/src/MySqlConnector/Protocol/Payloads/OkPayload.cs
@@ -1,4 +1,6 @@
+using System.Buffers.Text;
 using System.Text;
+using MySqlConnector.Core;
 using MySqlConnector.Protocol.Serialization;
 using MySqlConnector.Utilities;
 
@@ -10,8 +12,11 @@ internal sealed class OkPayload
 	public ulong LastInsertId { get; }
 	public ServerStatus ServerStatus { get; }
 	public int WarningCount { get; }
-	public string? StatusInfo { get; }
+	public byte[]? StatusInfo { get; }
 	public string? NewSchema { get; }
+	public CharacterSet? NewCharacterSet { get; }
+	public int? NewConnectionId { get; }
+	public string? RedirectionUrl { get; }
 
 	public const byte Signature = 0x00;
 
@@ -20,33 +25,57 @@ internal sealed class OkPayload
 	 * https://mariadb.com/kb/en/the-mariadb-library/resultset/
 	 * https://github.com/MariaDB/mariadb-connector-j/blob/5fa814ac6e1b4c9cb6d141bd221cbd5fc45c8a78/src/main/java/org/mariadb/jdbc/internal/com/read/resultset/SelectResultSet.java#L443-L444
 	 */
-	public static bool IsOk(ReadOnlySpan<byte> span, bool deprecateEof) =>
+	public static bool IsOk(ReadOnlySpan<byte> span, IServerCapabilities serverCapabilities) =>
 		span.Length > 0 &&
 			(span.Length > 6 && span[0] == Signature ||
-			 deprecateEof && span.Length < 0xFF_FFFF && span[0] == EofPayload.Signature);
+			 serverCapabilities.SupportsDeprecateEof && span.Length < 0xFF_FFFF && span[0] == EofPayload.Signature);
 
-	public static OkPayload Create(ReadOnlySpan<byte> span, bool deprecateEof, bool clientSessionTrack)
+	/// <summary>
+	/// Creates an <see cref="OkPayload"/> from the given <paramref name="span"/>, or throws <see cref="FormatException"/>
+	/// if the bytes do not represent a valid <see cref="OkPayload"/>.
+	/// </summary>
+	/// <param name="span">The bytes from which to read an OK packet.</param>
+	/// <param name="serverCapabilities">The server capabilities.</param>
+	/// <returns>A <see cref="OkPayload"/> with the contents of the OK packet.</returns>
+	/// <exception cref="FormatException">Thrown when the bytes are not a valid OK packet.</exception>
+	public static OkPayload Create(ReadOnlySpan<byte> span, IServerCapabilities serverCapabilities) =>
+		Read(span, serverCapabilities, true)!;
+
+	/// <summary>
+	/// Verifies that the bytes in the given <paramref name="span"/> form a valid <see cref="OkPayload"/>, or throws
+	/// <see cref="FormatException"/> if they do not.
+	/// </summary>
+	/// <param name="span">The bytes from which to read an OK packet.</param>
+	/// <param name="serverCapabilities">The server capabilities.</param>
+	/// <exception cref="FormatException">Thrown when the bytes are not a valid OK packet.</exception>
+	public static void Verify(ReadOnlySpan<byte> span, IServerCapabilities serverCapabilities) =>
+		Read(span, serverCapabilities, createPayload: false);
+
+	private static OkPayload? Read(ReadOnlySpan<byte> span, IServerCapabilities serverCapabilities, bool createPayload)
 	{
 		var reader = new ByteArrayReader(span);
 		var signature = reader.ReadByte();
-		if (signature != Signature && (!deprecateEof || signature != EofPayload.Signature))
+		if (signature != Signature && (!serverCapabilities.SupportsDeprecateEof || signature != EofPayload.Signature))
 			throw new FormatException($"Expected to read 0x00 or 0xFE but got 0x{signature:X2}");
 		var affectedRowCount = reader.ReadLengthEncodedInteger();
 		var lastInsertId = reader.ReadLengthEncodedInteger();
 		var serverStatus = (ServerStatus) reader.ReadUInt16();
 		var warningCount = (int) reader.ReadUInt16();
 		string? newSchema = null;
+		CharacterSet clientCharacterSet = default;
+		CharacterSet connectionCharacterSet = default;
+		CharacterSet resultsCharacterSet = default;
+		string? redirectionUrl = default;
+		int? connectionId = null;
 		ReadOnlySpan<byte> statusBytes;
 
-		if (clientSessionTrack)
+		if (serverCapabilities.SupportsSessionTrack)
 		{
-			if (reader.BytesRemaining > 0)
+			if (reader.BytesRemaining > 0 && createPayload)
 			{
 				statusBytes = reader.ReadLengthEncodedByteString(); // human-readable info
-
-				if ((serverStatus & ServerStatus.SessionStateChanged) == ServerStatus.SessionStateChanged && reader.BytesRemaining > 0)
+				while (reader.BytesRemaining > 0)
 				{
-					// implies ProtocolCapabilities.SessionTrack
 					var sessionStateChangeDataLength = checked((int) reader.ReadLengthEncodedInteger());
 					var endOffset = reader.Offset + sessionStateChangeDataLength;
 					while (reader.Offset < endOffset)
@@ -55,13 +84,52 @@ public static OkPayload Create(ReadOnlySpan<byte> span, bool deprecateEof, bool
 						var dataLength = (int) reader.ReadLengthEncodedInteger();
 						switch (kind)
 						{
-						case SessionTrackKind.Schema:
-							newSchema = Encoding.UTF8.GetString(reader.ReadLengthEncodedByteString());
-							break;
+							case SessionTrackKind.Schema:
+								newSchema = Encoding.UTF8.GetString(reader.ReadLengthEncodedByteString());
+								break;
 
-						default:
-							reader.Offset += dataLength;
-							break;
+							case SessionTrackKind.SystemVariables:
+								var systemVariablesEndOffset = reader.Offset + dataLength;
+								do
+								{
+									var systemVariableName = reader.ReadLengthEncodedByteString();
+									var systemVariableValueLength = reader.ReadLengthEncodedIntegerOrNull();
+									var systemVariableValue = systemVariableValueLength == -1 ? default : reader.ReadByteString(systemVariableValueLength);
+									if (systemVariableName.SequenceEqual("character_set_client"u8) && systemVariableValueLength != 0)
+									{
+										clientCharacterSet = systemVariableValue.SequenceEqual("utf8mb4"u8) ? CharacterSet.Utf8Mb4Binary :
+											systemVariableValue.SequenceEqual("utf8"u8) ? CharacterSet.Utf8Mb3Binary :
+											CharacterSet.None;
+									}
+									else if (systemVariableName.SequenceEqual("character_set_connection"u8) && systemVariableValueLength != 0)
+									{
+										connectionCharacterSet = systemVariableValue.SequenceEqual("utf8mb4"u8) ? CharacterSet.Utf8Mb4Binary :
+											systemVariableValue.SequenceEqual("utf8"u8) ? CharacterSet.Utf8Mb3Binary :
+											CharacterSet.None;
+									}
+									else if (systemVariableName.SequenceEqual("character_set_results"u8) && systemVariableValueLength != 0)
+									{
+										resultsCharacterSet = systemVariableValue.SequenceEqual("utf8mb4"u8) ? CharacterSet.Utf8Mb4Binary :
+											systemVariableValue.SequenceEqual("utf8"u8) ? CharacterSet.Utf8Mb3Binary :
+											CharacterSet.None;
+									}
+									else if (systemVariableName.SequenceEqual("connection_id"u8))
+									{
+										connectionId = Utf8Parser.TryParse(systemVariableValue, out int parsedConnectionId, out var bytesConsumed) && bytesConsumed == systemVariableValue.Length ? parsedConnectionId : default(int?);
+									}
+									else if (systemVariableName.SequenceEqual("redirect_url"u8))
+									{
+										if (systemVariableValue.Length > 0)
+										{
+											redirectionUrl = Encoding.UTF8.GetString(systemVariableValue);
+										}
+									}
+								} while (reader.Offset < systemVariablesEndOffset);
+								break;
+
+							default:
+								reader.Offset += dataLength;
+								break;
 						}
 					}
 				}
@@ -82,20 +150,32 @@ public static OkPayload Create(ReadOnlySpan<byte> span, bool deprecateEof, bool
 				statusBytes = statusBytes[1..];
 		}
 
-		var statusInfo = statusBytes.Length == 0 ? null : Encoding.UTF8.GetString(statusBytes);
+		if (createPayload)
+		{
+			var statusInfo = statusBytes.Length == 0 ? null : statusBytes.ToArray();
+
+			// detect the connection character set as utf8mb4 (or utf8) if all three system variables are set to the same value
+			var characterSet = clientCharacterSet == CharacterSet.Utf8Mb4Binary && connectionCharacterSet == CharacterSet.Utf8Mb4Binary && resultsCharacterSet == CharacterSet.Utf8Mb4Binary ? CharacterSet.Utf8Mb4Binary :
+				clientCharacterSet == CharacterSet.Utf8Mb3Binary && connectionCharacterSet == CharacterSet.Utf8Mb3Binary && resultsCharacterSet == CharacterSet.Utf8Mb3Binary ? CharacterSet.Utf8Mb3Binary :
+				CharacterSet.None;
 
-		if (affectedRowCount == 0 && lastInsertId == 0 && warningCount == 0 && statusInfo is null && newSchema is null)
+			if (affectedRowCount == 0 && lastInsertId == 0 && warningCount == 0 && statusInfo is null && newSchema is null && clientCharacterSet is CharacterSet.None && connectionId is null && redirectionUrl is null)
+			{
+				if (serverStatus == ServerStatus.AutoCommit)
+					return s_autoCommitOk;
+				if (serverStatus == (ServerStatus.AutoCommit | ServerStatus.SessionStateChanged))
+					return s_autoCommitSessionStateChangedOk;
+			}
+
+			return new OkPayload(affectedRowCount, lastInsertId, serverStatus, warningCount, statusInfo, newSchema, characterSet, connectionId, redirectionUrl);
+		}
+		else
 		{
-			if (serverStatus == ServerStatus.AutoCommit)
-				return s_autoCommitOk;
-			if (serverStatus == (ServerStatus.AutoCommit | ServerStatus.SessionStateChanged))
-				return s_autoCommitSessionStateChangedOk;
+			return null;
 		}
-
-		return new OkPayload(affectedRowCount, lastInsertId, serverStatus, warningCount, statusInfo, newSchema);
 	}
 
-	private OkPayload(ulong affectedRowCount, ulong lastInsertId, ServerStatus serverStatus, int warningCount, string? statusInfo, string? newSchema)
+	private OkPayload(ulong affectedRowCount, ulong lastInsertId, ServerStatus serverStatus, int warningCount, byte[]? statusInfo, string? newSchema, CharacterSet newCharacterSet, int? connectionId, string? redirectionUrl)
 	{
 		AffectedRowCount = affectedRowCount;
 		LastInsertId = lastInsertId;
@@ -103,8 +183,11 @@ private OkPayload(ulong affectedRowCount, ulong lastInsertId, ServerStatus serve
 		WarningCount = warningCount;
 		StatusInfo = statusInfo;
 		NewSchema = newSchema;
+		NewCharacterSet = newCharacterSet;
+		NewConnectionId = connectionId;
+		RedirectionUrl = redirectionUrl;
 	}
 
-	private static readonly OkPayload s_autoCommitOk = new(0, 0, ServerStatus.AutoCommit, 0, null, null);
-	private static readonly OkPayload s_autoCommitSessionStateChangedOk = new(0, 0, ServerStatus.AutoCommit | ServerStatus.SessionStateChanged, 0, null, null);
+	private static readonly OkPayload s_autoCommitOk = new(0, 0, ServerStatus.AutoCommit, 0, default, default, default, default, default);
+	private static readonly OkPayload s_autoCommitSessionStateChangedOk = new(0, 0, ServerStatus.AutoCommit | ServerStatus.SessionStateChanged, 0, default, default, default, default, default);
 }
diff --git a/src/MySqlConnector/Protocol/Payloads/PingPayload.cs b/src/MySqlConnector/Protocol/Payloads/PingPayload.cs
index 9b9eee3d1..a03ed5597 100644
--- a/src/MySqlConnector/Protocol/Payloads/PingPayload.cs
+++ b/src/MySqlConnector/Protocol/Payloads/PingPayload.cs
@@ -2,5 +2,5 @@ namespace MySqlConnector.Protocol.Payloads;
 
 internal static class PingPayload
 {
-	public static PayloadData Instance { get; } = new(new[] { (byte) CommandKind.Ping });
+	public static PayloadData Instance { get; } = new([(byte) CommandKind.Ping]);
 }
diff --git a/src/MySqlConnector/Protocol/Payloads/QueryPayload.cs b/src/MySqlConnector/Protocol/Payloads/QueryPayload.cs
index 71714d74f..8299536bf 100644
--- a/src/MySqlConnector/Protocol/Payloads/QueryPayload.cs
+++ b/src/MySqlConnector/Protocol/Payloads/QueryPayload.cs
@@ -1,5 +1,3 @@
-using System.Text;
-
 namespace MySqlConnector.Protocol.Payloads;
 
 internal static class QueryPayload
diff --git a/src/MySqlConnector/Protocol/Payloads/QuitPayload.cs b/src/MySqlConnector/Protocol/Payloads/QuitPayload.cs
index 72f83b5e0..ce10d022f 100644
--- a/src/MySqlConnector/Protocol/Payloads/QuitPayload.cs
+++ b/src/MySqlConnector/Protocol/Payloads/QuitPayload.cs
@@ -2,5 +2,5 @@ namespace MySqlConnector.Protocol.Payloads;
 
 internal static class QuitPayload
 {
-	public static PayloadData Instance { get; } = new(new[] { (byte) CommandKind.Quit });
+	public static PayloadData Instance { get; } = new([(byte) CommandKind.Quit]);
 }
diff --git a/src/MySqlConnector/Protocol/Payloads/ResetConnectionPayload.cs b/src/MySqlConnector/Protocol/Payloads/ResetConnectionPayload.cs
index c2285511e..bd9deb61a 100644
--- a/src/MySqlConnector/Protocol/Payloads/ResetConnectionPayload.cs
+++ b/src/MySqlConnector/Protocol/Payloads/ResetConnectionPayload.cs
@@ -2,5 +2,5 @@ namespace MySqlConnector.Protocol.Payloads;
 
 internal static class ResetConnectionPayload
 {
-	public static PayloadData Instance { get; } = new(new[] { (byte) CommandKind.ResetConnection });
+	public static PayloadData Instance { get; } = new([(byte) CommandKind.ResetConnection]);
 }
diff --git a/src/MySqlConnector/Protocol/Payloads/StatementPrepareResponsePayload.cs b/src/MySqlConnector/Protocol/Payloads/StatementPrepareResponsePayload.cs
index 197598453..da77783e9 100644
--- a/src/MySqlConnector/Protocol/Payloads/StatementPrepareResponsePayload.cs
+++ b/src/MySqlConnector/Protocol/Payloads/StatementPrepareResponsePayload.cs
@@ -2,7 +2,7 @@
 
 namespace MySqlConnector.Protocol.Payloads;
 
-internal sealed class StatementPrepareResponsePayload
+internal readonly struct StatementPrepareResponsePayload
 {
 	public int StatementId { get; }
 	public int ColumnCount { get; }
diff --git a/src/MySqlConnector/Protocol/ProtocolCapabilities.cs b/src/MySqlConnector/Protocol/ProtocolCapabilities.cs
index 5b0466f8d..9351dcc85 100644
--- a/src/MySqlConnector/Protocol/ProtocolCapabilities.cs
+++ b/src/MySqlConnector/Protocol/ProtocolCapabilities.cs
@@ -128,6 +128,18 @@ internal enum ProtocolCapabilities : ulong
 	/// </summary>
 	DeprecateEof = 0x100_0000,
 
+	/// <summary>
+	/// The client can handle optional metadata information in the resultset.
+	/// </summary>
+	/// <remarks>Corresponds to CLIENT_OPTIONAL_RESULTSET_METADATA.</remarks>
+	ClientOptionalResultsetMetadata = 0x200_0000,
+
+	/// <summary>
+	/// The client supports the Zstandard compression algorithm.
+	/// </summary>
+	/// <remarks>Corresponds to CLIENT_ZSTD_COMPRESSION_ALGORITHM.</remarks>
+	ZstandardCompressionAlgorithm = 0x400_0000,
+
 	/// <summary>
 	/// Supports query attributes (CLIENT_QUERY_ATTRIBUTES).
 	/// </summary>
@@ -147,4 +159,14 @@ internal enum ProtocolCapabilities : ulong
 	/// Support of array binding.
 	/// </summary>
 	MariaDbStatementBulkOperations = 0x4_0000_0000,
+
+	/// <summary>
+	/// metadata extended information
+	/// </summary>
+	MariaDbExtendedTypeInfo = 0x8_0000_0000,
+
+	/// <summary>
+	/// Client supports caching metadata for prepared statements (MARIADB_CLIENT_CACHE_METADATA).
+	/// </summary>
+	MariaDbCacheMetadata = 0x10_0000_0000,
 }
diff --git a/src/MySqlConnector/Protocol/Serialization/AuthGSSAPI.cs b/src/MySqlConnector/Protocol/Serialization/AuthGSSAPI.cs
index 812b2b0dd..412c6360e 100644
--- a/src/MySqlConnector/Protocol/Serialization/AuthGSSAPI.cs
+++ b/src/MySqlConnector/Protocol/Serialization/AuthGSSAPI.cs
@@ -56,10 +56,10 @@ public NegotiateToMySqlConverterStream(ServerSession serverSession, IOBehavior i
 	private static void CreateNegotiateStreamMessageHeader(byte[] buffer, int offset, byte messageId, long payloadLength)
 	{
 		buffer[offset] = messageId;
-		buffer[offset+1] = NegotiateStreamConstants.MajorVersion;
-		buffer[offset+2] = NegotiateStreamConstants.MinorVersion;
-		buffer[offset+3] = (byte) (payloadLength >> 8);
-		buffer[offset+4] = (byte) (payloadLength & 0xff);
+		buffer[offset + 1] = NegotiateStreamConstants.MajorVersion;
+		buffer[offset + 2] = NegotiateStreamConstants.MinorVersion;
+		buffer[offset + 3] = (byte) (payloadLength >> 8);
+		buffer[offset + 4] = (byte) (payloadLength & 0xff);
 	}
 	public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
 	{
@@ -90,13 +90,13 @@ public override async Task<int> ReadAsync(byte[] buffer, int offset, int count,
 			// during pluggable authentication packet exchanges.
 			switch (payloadMemory.Span[0])
 			{
-			case 0x0:
-				MySQLProtocolPayload = payload;
-				CreateNegotiateStreamMessageHeader(buffer, offset, NegotiateStreamConstants.HandshakeDone, 0);
-				return NegotiateStreamConstants.HeaderLength;
-			case 0x1:
-				payloadMemory = payloadMemory[1..];
-				break;
+				case 0x0:
+					MySQLProtocolPayload = payload;
+					CreateNegotiateStreamMessageHeader(buffer, offset, NegotiateStreamConstants.HandshakeDone, 0);
+					return NegotiateStreamConstants.HeaderLength;
+				case 0x1:
+					payloadMemory = payloadMemory[1..];
+					break;
 			}
 
 			m_readBuffer = new(payloadMemory.ToArray());
@@ -125,25 +125,25 @@ public override async Task WriteAsync(byte[] buffer, int offset, int count, Canc
 				throw new InvalidDataException("Cannot parse NegotiateStream handshake message header");
 
 			// Parse NegotiateStream handshake header
-			var messageId = buffer[offset+0];
-			var majorProtocolVersion = buffer[offset+1];
-			var minorProtocolVersion = buffer[offset+2];
-			var payloadSizeLow = buffer[offset+4];
-			var payloadSizeHigh = buffer[offset+3];
+			var messageId = buffer[offset + 0];
+			var majorProtocolVersion = buffer[offset + 1];
+			var minorProtocolVersion = buffer[offset + 2];
+			var payloadSizeLow = buffer[offset + 4];
+			var payloadSizeHigh = buffer[offset + 3];
 
 			if (majorProtocolVersion != NegotiateStreamConstants.MajorVersion ||
 				minorProtocolVersion != NegotiateStreamConstants.MinorVersion)
 			{
 				throw new FormatException($"Unknown version of NegotiateStream protocol {majorProtocolVersion:d}.{minorProtocolVersion:d}, expected {NegotiateStreamConstants.MajorVersion:d}.{NegotiateStreamConstants.MinorVersion:d}");
 			}
-			if (messageId != NegotiateStreamConstants.HandshakeDone &&
-				messageId != NegotiateStreamConstants.HandshakeError &&
-				messageId != NegotiateStreamConstants.HandshakeInProgress)
+			if (messageId is not NegotiateStreamConstants.HandshakeDone and
+				not NegotiateStreamConstants.HandshakeError and
+				not NegotiateStreamConstants.HandshakeInProgress)
 			{
 				throw new FormatException($"Invalid NegotiateStream MessageId 0x{messageId:X2}");
 			}
 
-			m_writePayloadLength = (int) payloadSizeLow + ((int) payloadSizeHigh << 8);
+			m_writePayloadLength = payloadSizeLow + (payloadSizeHigh << 8);
 			if (messageId == NegotiateStreamConstants.HandshakeDone)
 				m_clientHandshakeDone = true;
 
diff --git a/src/MySqlConnector/Protocol/Serialization/AuthenticationUtility.cs b/src/MySqlConnector/Protocol/Serialization/AuthenticationUtility.cs
index 5c90acfe9..d1f325682 100644
--- a/src/MySqlConnector/Protocol/Serialization/AuthenticationUtility.cs
+++ b/src/MySqlConnector/Protocol/Serialization/AuthenticationUtility.cs
@@ -1,7 +1,11 @@
+#if NET5_0_OR_GREATER
 using System.Runtime.CompilerServices;
+#endif
 using System.Security.Cryptography;
 using System.Text;
+#if !NETCOREAPP2_1_OR_GREATER && !NETSTANDARD2_1_OR_GREATER
 using MySqlConnector.Utilities;
+#endif
 
 namespace MySqlConnector.Protocol.Serialization;
 
@@ -9,26 +13,41 @@ namespace MySqlConnector.Protocol.Serialization;
 
 internal static class AuthenticationUtility
 {
-	public static byte[] CreateAuthenticationResponse(ReadOnlySpan<byte> challenge, string password) =>
-		string.IsNullOrEmpty(password) ? Array.Empty<byte>() : HashPassword(challenge, password);
+	/// <summary>
+	/// Returns the UTF-8 bytes for <paramref name="password"/>, followed by a null byte.
+	/// </summary>
+	public static byte[] GetNullTerminatedPasswordBytes(string password)
+	{
+		var passwordByteCount = Encoding.UTF8.GetByteCount(password);
+		var passwordBytes = new byte[passwordByteCount + 1];
+		Encoding.UTF8.GetBytes(password.AsSpan(), passwordBytes);
+		return passwordBytes;
+	}
 
 	/// <summary>
 	/// Hashes a password with the "Secure Password Authentication" method.
 	/// </summary>
-	/// <param name="challenge">The 20-byte random challenge (from the "auth-plugin-data" in the initial handshake).</param>
 	/// <param name="password">The password to hash.</param>
-	/// <returns>A 20-byte password hash.</returns>
+	/// <param name="authenticationData">The 20-byte random challenge (from the "auth-plugin-data" in the initial handshake).</param>
+	/// <param name="authenticationResponse">The authentication response.</param>
+	/// <param name="passwordHash">The twice-hashed password.</param>
 	/// <remarks>See <a href="https://dev.mysql.com/doc/internals/en/secure-password-authentication.html">Secure Password Authentication</a>.</remarks>
 #if NET5_0_OR_GREATER
 	[SkipLocalsInit]
 #endif
-	public static byte[] HashPassword(ReadOnlySpan<byte> challenge, string password)
+	public static void CreateResponseAndPasswordHash(string password, ReadOnlySpan<byte> authenticationData, out byte[] authenticationResponse, out byte[] passwordHash)
 	{
+		if (string.IsNullOrEmpty(password))
+		{
+			authenticationResponse = [];
+			passwordHash = [];
+			return;
+		}
+
 #if !NET5_0_OR_GREATER
 		using var sha1 = SHA1.Create();
 #endif
 		Span<byte> combined = stackalloc byte[40];
-		challenge.CopyTo(combined);
 
 		var passwordByteCount = Encoding.UTF8.GetByteCount(password);
 		Span<byte> passwordBytes = stackalloc byte[passwordByteCount];
@@ -39,9 +58,11 @@ public static byte[] HashPassword(ReadOnlySpan<byte> challenge, string password)
 		SHA1.TryHashData(hashedPassword, combined[20..], out _);
 #else
 		sha1.TryComputeHash(passwordBytes, hashedPassword, out _);
-		sha1.TryComputeHash(hashedPassword, combined.Slice(20), out _);
+		sha1.TryComputeHash(hashedPassword, combined[20..], out _);
 #endif
+		passwordHash = combined[20..].ToArray();
 
+		authenticationData[..20].CopyTo(combined);
 		Span<byte> xorBytes = stackalloc byte[20];
 #if NET5_0_OR_GREATER
 		SHA1.TryHashData(combined, xorBytes, out _);
@@ -51,17 +72,11 @@ public static byte[] HashPassword(ReadOnlySpan<byte> challenge, string password)
 		for (var i = 0; i < hashedPassword.Length; i++)
 			hashedPassword[i] ^= xorBytes[i];
 
-		return hashedPassword.ToArray();
+		authenticationResponse = hashedPassword.ToArray();
 	}
 
-	public static byte[] CreateScrambleResponse(ReadOnlySpan<byte> nonce, string password)
-	{
-		var scrambleResponse = string.IsNullOrEmpty(password)
-			? Array.Empty<byte>()
-			: HashPasswordWithNonce(nonce, password);
-
-		return scrambleResponse;
-	}
+	public static byte[] CreateScrambleResponse(ReadOnlySpan<byte> nonce, string password) =>
+		string.IsNullOrEmpty(password) ? [] : HashPasswordWithNonce(nonce, password);
 
 #if NET5_0_OR_GREATER
 	[SkipLocalsInit]
diff --git a/src/MySqlConnector/Protocol/Serialization/ByteArrayReader.cs b/src/MySqlConnector/Protocol/Serialization/ByteArrayReader.cs
index fa597c38a..080cb8555 100644
--- a/src/MySqlConnector/Protocol/Serialization/ByteArrayReader.cs
+++ b/src/MySqlConnector/Protocol/Serialization/ByteArrayReader.cs
@@ -14,7 +14,17 @@ public ByteArrayReader(ReadOnlySpan<byte> buffer)
 	public int Offset
 	{
 		readonly get => m_offset;
-		set => m_offset = value >= 0 && value <= m_maxOffset ? value : throw new ArgumentOutOfRangeException(nameof(value), $"value must be between 0 and {m_maxOffset:d}");
+		set
+		{
+#if NET8_0_OR_GREATER
+			ArgumentOutOfRangeException.ThrowIfNegative(value);
+			ArgumentOutOfRangeException.ThrowIfGreaterThan(value, m_maxOffset);
+#else
+			if (value < 0 || value > m_maxOffset)
+				throw new ArgumentOutOfRangeException(nameof(value), $"value must be between 0 and {m_maxOffset:d}");
+#endif
+			m_offset = value;
+		}
 	}
 
 	public byte ReadByte()
@@ -63,8 +73,13 @@ public uint ReadUInt32()
 
 	public uint ReadFixedLengthUInt32(int length)
 	{
+#if NET8_0_OR_GREATER
+		ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(length, 0);
+		ArgumentOutOfRangeException.ThrowIfGreaterThan(length, 4);
+#else
 		if (length is <= 0 or > 4)
 			throw new ArgumentOutOfRangeException(nameof(length));
+#endif
 		VerifyRead(length);
 		uint result = 0;
 		for (var i = 0; i < length; i++)
@@ -75,8 +90,13 @@ public uint ReadFixedLengthUInt32(int length)
 
 	public ulong ReadFixedLengthUInt64(int length)
 	{
+#if NET8_0_OR_GREATER
+		ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(length, 0);
+		ArgumentOutOfRangeException.ThrowIfGreaterThan(length, 8);
+#else
 		if (length is <= 0 or > 8)
 			throw new ArgumentOutOfRangeException(nameof(length));
+#endif
 		VerifyRead(length);
 		ulong result = 0;
 		for (var i = 0; i < length; i++)
diff --git a/src/MySqlConnector/Protocol/Serialization/ByteBufferWriter.cs b/src/MySqlConnector/Protocol/Serialization/ByteBufferWriter.cs
index fd6e2d389..9017404bb 100644
--- a/src/MySqlConnector/Protocol/Serialization/ByteBufferWriter.cs
+++ b/src/MySqlConnector/Protocol/Serialization/ByteBufferWriter.cs
@@ -17,9 +17,9 @@ public ByteBufferWriter(int capacity = 0)
 
 	public int Position => m_buffer.Length - m_output.Length;
 
-	public ArraySegment<byte> ArraySegment => new ArraySegment<byte>(m_buffer, 0, Position);
+	public ArraySegment<byte> ArraySegment => new(m_buffer, 0, Position);
 
-	public PayloadData ToPayloadData() => new PayloadData(ArraySegment, isPooled: true);
+	public PayloadData ToPayloadData() => new(ArraySegment, isPooled: true);
 
 	public Memory<byte> GetMemory(int sizeHint = 0)
 	{
@@ -99,8 +99,22 @@ public void Write(ReadOnlySpan<byte> span)
 		m_output = m_output[span.Length..];
 	}
 
-	public void Write(string value) => Write(value.AsSpan(), flush: true);
-	public void Write(string value, int offset, int length) => Write(value.AsSpan(offset, length), flush: true);
+	public void Write(string value) => Write(value.AsSpan());
+
+	public void WriteAscii(string value) => WriteAscii(value.AsSpan());
+
+	public void Write(string value, int offset, int length) => Write(value.AsSpan(offset, length));
+
+	public void Write(ReadOnlySpan<char> chars)
+	{
+		if (m_output.Length < chars.Length * 3)
+		{
+			var neededBytes = Encoding.UTF8.GetByteCount(chars);
+			if (m_output.Length < neededBytes)
+				Reallocate(neededBytes);
+		}
+		m_output = m_output[Encoding.UTF8.GetBytes(chars, m_output.Span)..];
+	}
 
 	public void Write(ReadOnlySpan<char> chars, bool flush)
 	{
@@ -126,6 +140,18 @@ public void Write(ReadOnlySpan<char> chars, bool flush)
 		}
 	}
 
+	public void WriteAscii(ReadOnlySpan<char> chars)
+	{
+		if (m_output.Length < chars.Length)
+			Reallocate(chars.Length);
+#if NET8_0_OR_GREATER
+		Ascii.FromUtf16(chars, m_output.Span, out var bytesWritten);
+		m_output = m_output[bytesWritten..];
+#else
+		m_output = m_output[Encoding.ASCII.GetBytes(chars, m_output.Span)..];
+#endif
+	}
+
 	public void WriteLengthEncodedString(StringBuilder stringBuilder)
 	{
 #if NETCOREAPP3_0_OR_GREATER
@@ -170,49 +196,37 @@ public void WriteLengthEncodedString(StringBuilder stringBuilder)
 
 	public void WriteString(short value)
 	{
-		int bytesWritten;
-		while (!Utf8Formatter.TryFormat(value, m_output.Span, out bytesWritten))
-			Reallocate();
+		Utf8Formatter.TryFormat(value, GetSpan(6), out var bytesWritten);
 		m_output = m_output[bytesWritten..];
 	}
 
 	public void WriteString(ushort value)
 	{
-		int bytesWritten;
-		while (!Utf8Formatter.TryFormat(value, m_output.Span, out bytesWritten))
-			Reallocate();
+		Utf8Formatter.TryFormat(value, GetSpan(5), out var bytesWritten);
 		m_output = m_output[bytesWritten..];
 	}
 
 	public void WriteString(int value)
 	{
-		int bytesWritten;
-		while (!Utf8Formatter.TryFormat(value, m_output.Span, out bytesWritten))
-			Reallocate();
+		Utf8Formatter.TryFormat(value, GetSpan(11), out var bytesWritten);
 		m_output = m_output[bytesWritten..];
 	}
 
 	public void WriteString(uint value)
 	{
-		int bytesWritten;
-		while (!Utf8Formatter.TryFormat(value, m_output.Span, out bytesWritten))
-			Reallocate();
+		Utf8Formatter.TryFormat(value, GetSpan(10), out var bytesWritten);
 		m_output = m_output[bytesWritten..];
 	}
 
 	public void WriteString(long value)
 	{
-		int bytesWritten;
-		while (!Utf8Formatter.TryFormat(value, m_output.Span, out bytesWritten))
-			Reallocate();
+		Utf8Formatter.TryFormat(value, GetSpan(20), out var bytesWritten);
 		m_output = m_output[bytesWritten..];
 	}
 
 	public void WriteString(ulong value)
 	{
-		int bytesWritten;
-		while (!Utf8Formatter.TryFormat(value, m_output.Span, out bytesWritten))
-			Reallocate();
+		Utf8Formatter.TryFormat(value, GetSpan(20), out var bytesWritten);
 		m_output = m_output[bytesWritten..];
 	}
 
@@ -220,7 +234,7 @@ private void Reallocate(int additional = 0)
 	{
 		var usedLength = Position;
 		var newBuffer = ArrayPool<byte>.Shared.Rent(Math.Max(usedLength + additional, m_buffer.Length * 2));
-		Buffer.BlockCopy(m_buffer, 0, newBuffer, 0, usedLength);
+		m_buffer.AsSpan(0, usedLength).CopyTo(newBuffer);
 		ArrayPool<byte>.Shared.Return(m_buffer);
 		m_buffer = newBuffer;
 		m_output = new(m_buffer, usedLength, m_buffer.Length - usedLength);
@@ -237,23 +251,23 @@ public static void WriteLengthEncodedInteger(this ByteBufferWriter writer, ulong
 	{
 		switch (value)
 		{
-		case < 251:
-			writer.Write((byte) value);
-			break;
-
-		case < 65536:
-			writer.Write((byte) 0xfc);
-			writer.Write((ushort) value);
-			break;
-
-		case < 16777216:
-			writer.Write((uint) ((value << 8) | 0xfd));
-			break;
-
-		default:
-			writer.Write((byte) 0xfe);
-			writer.Write(value);
-			break;
+			case < 251:
+				writer.Write((byte) value);
+				break;
+
+			case < 65536:
+				writer.Write((byte) 0xfc);
+				writer.Write((ushort) value);
+				break;
+
+			case < 16777216:
+				writer.Write((uint) ((value << 8) | 0xfd));
+				break;
+
+			default:
+				writer.Write((byte) 0xfe);
+				writer.Write(value);
+				break;
 		}
 	}
 
@@ -263,7 +277,13 @@ public static void WriteLengthEncodedString(this ByteBufferWriter writer, ReadOn
 	{
 		var byteCount = Encoding.UTF8.GetByteCount(value);
 		writer.WriteLengthEncodedInteger((ulong) byteCount);
-		writer.Write(value, flush: true);
+		writer.Write(value);
+	}
+
+	public static void WriteLengthEncodedAsciiString(this ByteBufferWriter writer, string value)
+	{
+		writer.WriteLengthEncodedInteger((ulong) value.Length);
+		writer.WriteAscii(value.AsSpan());
 	}
 
 	public static void WriteNullTerminatedString(this ByteBufferWriter writer, string value)
diff --git a/src/MySqlConnector/Protocol/Serialization/CompressedPayloadHandler.cs b/src/MySqlConnector/Protocol/Serialization/CompressedPayloadHandler.cs
index c9e455d51..101bbeaea 100644
--- a/src/MySqlConnector/Protocol/Serialization/CompressedPayloadHandler.cs
+++ b/src/MySqlConnector/Protocol/Serialization/CompressedPayloadHandler.cs
@@ -130,7 +130,8 @@ private async ValueTask<int> ReadBytesAsync(Memory<byte> buffer, ProtocolErrorBe
 			} while (bytesRead > 0);
 #endif
 			if (totalBytesRead != uncompressedLength && protocolErrorBehavior == ProtocolErrorBehavior.Throw)
-				throw new InvalidOperationException($"Expected to read {uncompressedLength:d} uncompressed bytes but only read {totalBytesRead:d}");
+				throw new MySqlEndOfStreamException(uncompressedLength, totalBytesRead);
+
 			m_remainingData = new(uncompressedData, 0, totalBytesRead);
 #else
 			// check CMF (Compression Method and Flags) and FLG (Flags) bytes for expected values
@@ -160,7 +161,7 @@ private async ValueTask<int> ReadBytesAsync(Memory<byte> buffer, ProtocolErrorBe
 				totalBytesRead += bytesRead;
 			} while (bytesRead > 0);
 			if (totalBytesRead != uncompressedLength && protocolErrorBehavior == ProtocolErrorBehavior.Throw)
-				throw new InvalidOperationException($"Expected to read {uncompressedLength:d} uncompressed bytes but only read {totalBytesRead:d}");
+				throw new MySqlEndOfStreamException(uncompressedLength, totalBytesRead);
 			m_remainingData = new(uncompressedData, 0, totalBytesRead);
 
 			var checksum = Adler32.Calculate(uncompressedData.AsSpan(0, totalBytesRead));
diff --git a/src/MySqlConnector/Protocol/Serialization/ProtocolUtility.cs b/src/MySqlConnector/Protocol/Serialization/ProtocolUtility.cs
index 8d819291b..53625a351 100644
--- a/src/MySqlConnector/Protocol/Serialization/ProtocolUtility.cs
+++ b/src/MySqlConnector/Protocol/Serialization/ProtocolUtility.cs
@@ -1,6 +1,5 @@
 using System.Buffers;
 using MySqlConnector.Protocol.Payloads;
-using MySqlConnector.Utilities;
 
 namespace MySqlConnector.Protocol.Serialization;
 
@@ -10,554 +9,493 @@ public static int GetBytesPerCharacter(CharacterSet characterSet)
 	{
 		switch (characterSet)
 		{
-		case CharacterSet.Latin2CzechCaseSensitive:
-		case CharacterSet.Dec8SwedishCaseInsensitive:
-		case CharacterSet.Cp850GeneralCaseInsensitive:
-		case CharacterSet.Latin1German1CaseInsensitive:
-		case CharacterSet.Hp8EnglishCaseInsensitive:
-		case CharacterSet.Koi8rGeneralCaseInsensitive:
-		case CharacterSet.Latin1SwedishCaseInsensitive:
-		case CharacterSet.Latin2GeneralCaseInsensitive:
-		case CharacterSet.Swe7SwedishCaseInsensitive:
-		case CharacterSet.AsciiGeneralCaseInsensitive:
-		case CharacterSet.Cp1251BulgarianCaseInsensitive:
-		case CharacterSet.Latin1DanishCaseInsensitive:
-		case CharacterSet.HebrewGeneralCaseInsensitive:
-		case CharacterSet.Tis620ThaiCaseInsensitive:
-		case CharacterSet.Latin7EstonianCaseSensitive:
-		case CharacterSet.Latin2HungarianCaseInsensitive:
-		case CharacterSet.Koi8uGeneralCaseInsensitive:
-		case CharacterSet.Cp1251UkrainianCaseInsensitive:
-		case CharacterSet.GreekGeneralCaseInsensitive:
-		case CharacterSet.Cp1250GeneralCaseInsensitive:
-		case CharacterSet.Latin2CroatianCaseInsensitive:
-		case CharacterSet.Cp1257LithuanianCaseInsensitive:
-		case CharacterSet.Latin5TurkishCaseInsensitive:
-		case CharacterSet.Latin1German2CaseInsensitive:
-		case CharacterSet.Armscii8GeneralCaseInsensitive:
-		case CharacterSet.Cp1250CzechCaseSensitive:
-		case CharacterSet.Cp866GeneralCaseInsensitive:
-		case CharacterSet.Keybcs2GeneralCaseInsensitive:
-		case CharacterSet.MacceGeneralCaseInsensitive:
-		case CharacterSet.MacromanGeneralCaseInsensitive:
-		case CharacterSet.Cp852GeneralCaseInsensitive:
-		case CharacterSet.Latin7GeneralCaseInsensitive:
-		case CharacterSet.Latin7GeneralCaseSensitive:
-		case CharacterSet.MacceBinary:
-		case CharacterSet.Cp1250CroatianCaseInsensitive:
-		case CharacterSet.Latin1Binary:
-		case CharacterSet.Latin1GeneralCaseInsensitive:
-		case CharacterSet.Latin1GeneralCaseSensitive:
-		case CharacterSet.Cp1251Binary:
-		case CharacterSet.Cp1251GeneralCaseInsensitive:
-		case CharacterSet.Cp1251GeneralCaseSensitive:
-		case CharacterSet.MacromanBinary:
-		case CharacterSet.Cp1256GeneralCaseInsensitive:
-		case CharacterSet.Cp1257Binary:
-		case CharacterSet.Cp1257GeneralCaseInsensitive:
-		case CharacterSet.Binary:
-		case CharacterSet.Armscii8Binary:
-		case CharacterSet.AsciiBinary:
-		case CharacterSet.Cp1250Binary:
-		case CharacterSet.Cp1256Binary:
-		case CharacterSet.Cp866Binary:
-		case CharacterSet.Dec8Binary:
-		case CharacterSet.GreekBinary:
-		case CharacterSet.HebrewBinary:
-		case CharacterSet.Hp8Binary:
-		case CharacterSet.Keybcs2Binary:
-		case CharacterSet.Koi8rBinary:
-		case CharacterSet.Koi8uBinary:
-		case CharacterSet.Latin2Binary:
-		case CharacterSet.Latin5Binary:
-		case CharacterSet.Latin7Binary:
-		case CharacterSet.Cp850Binary:
-		case CharacterSet.Cp852Binary:
-		case CharacterSet.Swe7Binary:
-		case CharacterSet.Tis620Binary:
-		case CharacterSet.Geostd8GeneralCaseInsensitive:
-		case CharacterSet.Geostd8Binary:
-		case CharacterSet.Latin1SpanishCaseInsensitive:
-		case CharacterSet.Cp1250PolishCaseInsensitive:
-		case CharacterSet.Dec8SwedishNoPadCaseInsensitive:
-		case CharacterSet.Cp850GeneralNoPadCaseInsensitive:
-		case CharacterSet.Hp8EnglishNoPadCaseInsensitive:
-		case CharacterSet.Koi8rGeneralNoPadCaseInsensitive:
-		case CharacterSet.Latin1SwedishNoPadCaseInsensitive:
-		case CharacterSet.Latin2GeneralNoPadCaseInsensitive:
-		case CharacterSet.Swe7SwedishNoPadCaseInsensitive:
-		case CharacterSet.AsciiGeneralNoPadCaseInsensitive:
-		case CharacterSet.HebrewGeneralNoPadCaseInsensitive:
-		case CharacterSet.Tis620ThaiNoPadCaseInsensitive:
-		case CharacterSet.Koi8uGeneralNoPadCaseInsensitive:
-		case CharacterSet.GreekGeneralNoPadCaseInsensitive:
-		case CharacterSet.Cp1250GeneralNoPadCaseInsensitive:
-		case CharacterSet.Latin5TurkishNoPadCaseInsensitive:
-		case CharacterSet.Armscii8GeneralNoPadCaseInsensitive:
-		case CharacterSet.Cp866GeneralNoPadCaseInsensitive:
-		case CharacterSet.Keybcs2GeneralNoPadCaseInsensitive:
-		case CharacterSet.MacCentralEuropeanGeneralNoPadCaseInsensitive:
-		case CharacterSet.MacRomanGeneralNoPadCaseInsensitive:
-		case CharacterSet.Cp852GeneralNoPadCaseInsensitive:
-		case CharacterSet.Latin7GeneralNoPadCaseInsensitive:
-		case CharacterSet.MacCentralEuropeanNoPadBinary:
-		case CharacterSet.Latin1NoPadBinary:
-		case CharacterSet.Cp1251NoPadBinary:
-		case CharacterSet.Cp1251GeneralNoPadCaseInsensitive:
-		case CharacterSet.MacRomanNoPadBinary:
-		case CharacterSet.Cp1256GeneralNoPadCaseInsensitive:
-		case CharacterSet.Cp1257NoPadBinary:
-		case CharacterSet.Cp1257GeneralNoPadCaseInsensitive:
-		case CharacterSet.Armscii8NoPadBinary:
-		case CharacterSet.AsciiNoPadBinary:
-		case CharacterSet.Cp1250NoPadBinary:
-		case CharacterSet.Cp1256NoPadBinary:
-		case CharacterSet.Cp866NoPadBinary:
-		case CharacterSet.Dec8NoPadBinary:
-		case CharacterSet.GreekNoPadBinary:
-		case CharacterSet.HebrewNoPadBinary:
-		case CharacterSet.Hp8NoPadBinary:
-		case CharacterSet.Keybcs2NoPadBinary:
-		case CharacterSet.Koi8rNoPadBinary:
-		case CharacterSet.Koi8uNoPadBinary:
-		case CharacterSet.Latin2NoPadBinary:
-		case CharacterSet.Latin5NoPadBinary:
-		case CharacterSet.Latin7NoPadBinary:
-		case CharacterSet.Cp850NoPadBinary:
-		case CharacterSet.Cp852NoPadBinary:
-		case CharacterSet.Swe7NoPadBinary:
-		case CharacterSet.Tis620NoPadBinary:
-		case CharacterSet.Geostd8GeneralNoPadCaseInsensitive:
-		case CharacterSet.Geostd8NoPadBinary:
-			return 1;
-
-		case CharacterSet.Big5ChineseCaseInsensitive:
-		case CharacterSet.SjisJapaneseCaseInsensitive:
-		case CharacterSet.EuckrKoreanCaseInsensitive:
-		case CharacterSet.Gb2312ChineseCaseInsensitive:
-		case CharacterSet.GbkChineseCaseInsensitive:
-		case CharacterSet.Ucs2GeneralCaseInsensitive:
-		case CharacterSet.Big5Binary:
-		case CharacterSet.EuckrBinary:
-		case CharacterSet.Gb2312Binary:
-		case CharacterSet.GbkBinary:
-		case CharacterSet.SjisBinary:
-		case CharacterSet.Ucs2Binary:
-		case CharacterSet.Cp932JapaneseCaseInsensitive:
-		case CharacterSet.Cp932Binary:
-		case CharacterSet.Ucs2UnicodeCaseInsensitive:
-		case CharacterSet.Ucs2IcelandicCaseInsensitive:
-		case CharacterSet.Ucs2LatvianCaseInsensitive:
-		case CharacterSet.Ucs2RomanianCaseInsensitive:
-		case CharacterSet.Ucs2SlovenianCaseInsensitive:
-		case CharacterSet.Ucs2PolishCaseInsensitive:
-		case CharacterSet.Ucs2EstonianCaseInsensitive:
-		case CharacterSet.Ucs2SpanishCaseInsensitive:
-		case CharacterSet.Ucs2SwedishCaseInsensitive:
-		case CharacterSet.Ucs2TurkishCaseInsensitive:
-		case CharacterSet.Ucs2CzechCaseInsensitive:
-		case CharacterSet.Ucs2DanishCaseInsensitive:
-		case CharacterSet.Ucs2LithuanianCaseInsensitive:
-		case CharacterSet.Ucs2SlovakCaseInsensitive:
-		case CharacterSet.Ucs2Spanish2CaseInsensitive:
-		case CharacterSet.Ucs2RomanCaseInsensitive:
-		case CharacterSet.Ucs2PersianCaseInsensitive:
-		case CharacterSet.Ucs2EsperantoCaseInsensitive:
-		case CharacterSet.Ucs2HungarianCaseInsensitive:
-		case CharacterSet.Ucs2SinhalaCaseInsensitive:
-		case CharacterSet.Ucs2German2CaseInsensitive:
-		case CharacterSet.Ucs2CroatianCaseInsensitive:
-		case CharacterSet.Ucs2Unicode520CaseInsensitive:
-		case CharacterSet.Ucs2VietnameseCaseInsensitive:
-		case CharacterSet.Ucs2GeneralMySql500CaseInsensitive:
-		case CharacterSet.Ucs2CroatianCaseInsensitiveMariaDb:
-		case CharacterSet.Ucs2MyanmarCaseInsensitive:
-		case CharacterSet.Ucs2ThaiUnicode520Weight2:
-		case CharacterSet.Big5ChineseNoPadCaseInsensitive:
-		case CharacterSet.SjisJapaneseNoPadCaseInsensitive:
-		case CharacterSet.EuckrKoreanNoPadCaseInsensitive:
-		case CharacterSet.Gb2312ChineseNoPadCaseInsensitive:
-		case CharacterSet.GbkChineseNoPadCaseInsensitive:
-		case CharacterSet.Ucs2GeneralNoPadCaseInsensitive:
-		case CharacterSet.Big5NoPadBinary:
-		case CharacterSet.EuckrNoPadBinary:
-		case CharacterSet.Gb2312NoPadBinary:
-		case CharacterSet.GbkNoPadBinary:
-		case CharacterSet.SjisNoPadBinary:
-		case CharacterSet.Ucs2NoPadBinary:
-		case CharacterSet.Cp932JapaneseNoPadCaseInsensitive:
-		case CharacterSet.Cp932NoPadBinary:
-		case CharacterSet.Ucs2UnicodeNoPadCaseInsensitive:
-		case CharacterSet.Ucs2Unicode520NoPadCaseInsensitive:
-			return 2;
-
-		case CharacterSet.UjisJapaneseCaseInsensitive:
-		case CharacterSet.Utf8Mb3GeneralCaseInsensitive:
-		case CharacterSet.Utf8Mb3ToLowerCaseInsensitive:
-		case CharacterSet.Utf8Mb3Binary:
-		case CharacterSet.UjisBinary:
-		case CharacterSet.EucjpmsJapaneseCaseInsensitive:
-		case CharacterSet.EucjpmsBinary:
-		case CharacterSet.Utf8Mb3UnicodeCaseInsensitive:
-		case CharacterSet.Utf8Mb3IcelandicCaseInsensitive:
-		case CharacterSet.Utf8Mb3LatvianCaseInsensitive:
-		case CharacterSet.Utf8Mb3RomanianCaseInsensitive:
-		case CharacterSet.Utf8Mb3SlovenianCaseInsensitive:
-		case CharacterSet.Utf8Mb3PolishCaseInsensitive:
-		case CharacterSet.Utf8Mb3EstonianCaseInsensitive:
-		case CharacterSet.Utf8Mb3SpanishCaseInsensitive:
-		case CharacterSet.Utf8Mb3SwedishCaseInsensitive:
-		case CharacterSet.Utf8Mb3TurkishCaseInsensitive:
-		case CharacterSet.Utf8Mb3CzechCaseInsensitive:
-		case CharacterSet.Utf8Mb3DanishCaseInsensitive:
-		case CharacterSet.Utf8Mb3LithuanianCaseInsensitive:
-		case CharacterSet.Utf8Mb3SlovakCaseInsensitive:
-		case CharacterSet.Utf8Mb3Spanish2CaseInsensitive:
-		case CharacterSet.Utf8Mb3RomanCaseInsensitive:
-		case CharacterSet.Utf8Mb3PersianCaseInsensitive:
-		case CharacterSet.Utf8Mb3EsperantoCaseInsensitive:
-		case CharacterSet.Utf8Mb3HungarianCaseInsensitive:
-		case CharacterSet.Utf8Mb3SinhalaCaseInsensitive:
-		case CharacterSet.Utf8Mb3German2CaseInsensitive:
-		case CharacterSet.Utf8Mb3CroatianCaseInsensitive:
-		case CharacterSet.Utf8Mb3Unicode520CaseInsensitive:
-		case CharacterSet.Utf8Mb3VietnameseCaseInsensitive:
-		case CharacterSet.Utf8Mb3GeneralMySql500CaseInsensitive:
-		case CharacterSet.Utf8Mb3CroatianCaseInsensitiveMariaDb:
-		case CharacterSet.Utf8Mb3MyanmarCaseInsensitive:
-		case CharacterSet.Utf8Mb3ThaiUnicode520Weight2:
-		case CharacterSet.UjisJapaneseNoPadCaseInsensitive:
-		case CharacterSet.Utf8Mb3GeneralNoPadCaseInsensitive:
-		case CharacterSet.Utf8Mb3NoPadBinary:
-		case CharacterSet.UjisNoPadBinary:
-		case CharacterSet.EucjpmsJapaneseNoPadCaseInsensitive:
-		case CharacterSet.EucjpmsNoPadBinary:
-		case CharacterSet.Utf8Mb3UnicodeNoPadCaseInsensitive:
-		case CharacterSet.Utf8Mb3Unicode520NoPadCaseInsensitive:
-			return 3;
-
-		case CharacterSet.Utf8Mb4GeneralCaseInsensitive:
-		case CharacterSet.Utf8Mb4Binary:
-		case CharacterSet.Utf16GeneralCaseInsensitive:
-		case CharacterSet.Utf16Binary:
-		case CharacterSet.Utf16leGeneralCaseInsensitive:
-		case CharacterSet.Utf32GeneralCaseInsensitive:
-		case CharacterSet.Utf32Binary:
-		case CharacterSet.Utf16leBinary:
-		case CharacterSet.Utf16UnicodeCaseInsensitive:
-		case CharacterSet.Utf16IcelandicCaseInsensitive:
-		case CharacterSet.Utf16LatvianCaseInsensitive:
-		case CharacterSet.Utf16RomanianCaseInsensitive:
-		case CharacterSet.Utf16SlovenianCaseInsensitive:
-		case CharacterSet.Utf16PolishCaseInsensitive:
-		case CharacterSet.Utf16EstonianCaseInsensitive:
-		case CharacterSet.Utf16SpanishCaseInsensitive:
-		case CharacterSet.Utf16SwedishCaseInsensitive:
-		case CharacterSet.Utf16TurkishCaseInsensitive:
-		case CharacterSet.Utf16CzechCaseInsensitive:
-		case CharacterSet.Utf16DanishCaseInsensitive:
-		case CharacterSet.Utf16LithuanianCaseInsensitive:
-		case CharacterSet.Utf16SlovakCaseInsensitive:
-		case CharacterSet.Utf16Spanish2CaseInsensitive:
-		case CharacterSet.Utf16RomanCaseInsensitive:
-		case CharacterSet.Utf16PersianCaseInsensitive:
-		case CharacterSet.Utf16EsperantoCaseInsensitive:
-		case CharacterSet.Utf16HungarianCaseInsensitive:
-		case CharacterSet.Utf16SinhalaCaseInsensitive:
-		case CharacterSet.Utf16German2CaseInsensitive:
-		case CharacterSet.Utf16CroatianCaseInsensitive:
-		case CharacterSet.Utf16Unicode520CaseInsensitive:
-		case CharacterSet.Utf16VietnameseCaseInsensitive:
-		case CharacterSet.Utf32UnicodeCaseInsensitive:
-		case CharacterSet.Utf32IcelandicCaseInsensitive:
-		case CharacterSet.Utf32LatvianCaseInsensitive:
-		case CharacterSet.Utf32RomanianCaseInsensitive:
-		case CharacterSet.Utf32SlovenianCaseInsensitive:
-		case CharacterSet.Utf32PolishCaseInsensitive:
-		case CharacterSet.Utf32EstonianCaseInsensitive:
-		case CharacterSet.Utf32SpanishCaseInsensitive:
-		case CharacterSet.Utf32SwedishCaseInsensitive:
-		case CharacterSet.Utf32TurkishCaseInsensitive:
-		case CharacterSet.Utf32CzechCaseInsensitive:
-		case CharacterSet.Utf32DanishCaseInsensitive:
-		case CharacterSet.Utf32LithuanianCaseInsensitive:
-		case CharacterSet.Utf32SlovakCaseInsensitive:
-		case CharacterSet.Utf32Spanish2CaseInsensitive:
-		case CharacterSet.Utf32RomanCaseInsensitive:
-		case CharacterSet.Utf32PersianCaseInsensitive:
-		case CharacterSet.Utf32EsperantoCaseInsensitive:
-		case CharacterSet.Utf32HungarianCaseInsensitive:
-		case CharacterSet.Utf32SinhalaCaseInsensitive:
-		case CharacterSet.Utf32German2CaseInsensitive:
-		case CharacterSet.Utf32CroatianCaseInsensitive:
-		case CharacterSet.Utf32Unicode520CaseInsensitive:
-		case CharacterSet.Utf32VietnameseCaseInsensitive:
-		case CharacterSet.Utf8Mb4UnicodeCaseInsensitive:
-		case CharacterSet.Utf8Mb4IcelandicCaseInsensitive:
-		case CharacterSet.Utf8Mb4LatvianCaseInsensitive:
-		case CharacterSet.Utf8Mb4RomanianCaseInsensitive:
-		case CharacterSet.Utf8Mb4SlovenianCaseInsensitive:
-		case CharacterSet.Utf8Mb4PolishCaseInsensitive:
-		case CharacterSet.Utf8Mb4EstonianCaseInsensitive:
-		case CharacterSet.Utf8Mb4SpanishCaseInsensitive:
-		case CharacterSet.Utf8Mb4SwedishCaseInsensitive:
-		case CharacterSet.Utf8Mb4TurkishCaseInsensitive:
-		case CharacterSet.Utf8Mb4CzechCaseInsensitive:
-		case CharacterSet.Utf8Mb4DanishCaseInsensitive:
-		case CharacterSet.Utf8Mb4LithuanianCaseInsensitive:
-		case CharacterSet.Utf8Mb4SlovakCaseInsensitive:
-		case CharacterSet.Utf8Mb4Spanish2CaseInsensitive:
-		case CharacterSet.Utf8Mb4RomanCaseInsensitive:
-		case CharacterSet.Utf8Mb4PersianCaseInsensitive:
-		case CharacterSet.Utf8Mb4EsperantoCaseInsensitive:
-		case CharacterSet.Utf8Mb4HungarianCaseInsensitive:
-		case CharacterSet.Utf8Mb4SinhalaCaseInsensitive:
-		case CharacterSet.Utf8Mb4German2CaseInsensitive:
-		case CharacterSet.Utf8Mb4CroatianCaseInsensitive:
-		case CharacterSet.Utf8Mb4Unicode520CaseInsensitive:
-		case CharacterSet.Utf8Mb4VietnameseCaseInsensitive:
-		case CharacterSet.Gb18030ChineseCaseInsensitive:
-		case CharacterSet.Gb18030Binary:
-		case CharacterSet.Gb18030Unicode520CaseInsensitive:
-		case CharacterSet.Utf8Mb4Uca900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4GermanPhonebookUca900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4IcelandicUca900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4LatvianUca900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4RomanianUca900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4SlovenianUca900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4PolishUca900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4EstonianUca900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4SpanishUca900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4SwedishUca900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4TurkishUca900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4CaseSensitiveUca900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4DanishUca900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4LithuanianUca900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4SlovakUca900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4TraditionalSpanishUca900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4LatinUca900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4EsperantoUca900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4HungarianUca900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4CroatianUca900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4VietnameseUca900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4Uca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4GermanPhonebookUca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4IcelandicUca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4LatvianUca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4RomanianUca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4SlovenianUca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4PolishUca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4EstonianUca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4SpanishUca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4SwedishUca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4TurkishUca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4CaseSensitiveUca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4DanishUca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4LithuanianUca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4SlovakUca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4TraditionalSpanishUca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4LatinUca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4EsperantoUca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4HungarianUca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4CroatianUca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4VietnameseUca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4JapaneseUca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4JapaneseUca900AccentSensitiveCaseSensitiveKanaSensitive:
-		case CharacterSet.Utf8Mb4Uca900AccentSensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4RussianUca900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4RussianUca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4ChineseUca900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4Uca900Binary:
-		case CharacterSet.Utf8Mb4NorwegianBokmal0900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4NorwegianBokmal0900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4NorwegianNynorsk0900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4NorwegianNynorsk0900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4SerbianLatin0900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4SerbianLatin0900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4Bosnian0900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4Bosnian0900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4Bulgarian0900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4Bulgarian0900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4Galician0900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4Galician0900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4MongolianCyrillic0900AccentInsensitiveCaseInsensitive:
-		case CharacterSet.Utf8Mb4MongolianCyrillic0900AccentSensitiveCaseSensitive:
-		case CharacterSet.Utf8Mb4CroatianCaseInsensitiveMariaDb:
-		case CharacterSet.Utf8Mb4MyanmarCaseInsensitive:
-		case CharacterSet.Utf8Mb4ThaiUnicode520Weight2:
-		case CharacterSet.Utf16CroatianCaseInsensitiveMariaDb:
-		case CharacterSet.Utf16MyanmarCaseInsensitive:
-		case CharacterSet.Utf16ThaiUnicode520Weight2:
-		case CharacterSet.Utf32CroatianCaseInsensitiveMariaDb:
-		case CharacterSet.Utf32MyanmarCaseInsensitive:
-		case CharacterSet.Utf32ThaiUnicode520Weight2:
-		case CharacterSet.Utf8Mb4GeneralNoPadCaseInsensitive:
-		case CharacterSet.Utf8Mb4NoPadBinary:
-		case CharacterSet.Utf16GeneralNoPadCaseInsensitive:
-		case CharacterSet.Utf16NoPadBinary:
-		case CharacterSet.Utf16leGeneralNoPadCaseInsensitive:
-		case CharacterSet.Utf32GeneralNoPadCaseInsensitive:
-		case CharacterSet.Utf32NoPadBinary:
-		case CharacterSet.Utf16leNoPadBinary:
-		case CharacterSet.Utf16UnicodeNoPadCaseInsensitive:
-		case CharacterSet.Utf16Unicode520NoPadCaseInsensitive:
-		case CharacterSet.Utf32UnicodeNoPadCaseInsensitive:
-		case CharacterSet.Utf32Unicode520NoPadCaseInsensitive:
-		case CharacterSet.Utf8Mb4UnicodeNoPadCaseInsensitive:
-		case CharacterSet.Utf8Mb4Unicode520NoPadCaseInsensitive:
-			return 4;
-
-		default:
-			throw new NotSupportedException($"Maximum byte length of character set {characterSet} is unknown.");
+			case CharacterSet.Latin2CzechCaseSensitive:
+			case CharacterSet.Dec8SwedishCaseInsensitive:
+			case CharacterSet.Cp850GeneralCaseInsensitive:
+			case CharacterSet.Latin1German1CaseInsensitive:
+			case CharacterSet.Hp8EnglishCaseInsensitive:
+			case CharacterSet.Koi8rGeneralCaseInsensitive:
+			case CharacterSet.Latin1SwedishCaseInsensitive:
+			case CharacterSet.Latin2GeneralCaseInsensitive:
+			case CharacterSet.Swe7SwedishCaseInsensitive:
+			case CharacterSet.AsciiGeneralCaseInsensitive:
+			case CharacterSet.Cp1251BulgarianCaseInsensitive:
+			case CharacterSet.Latin1DanishCaseInsensitive:
+			case CharacterSet.HebrewGeneralCaseInsensitive:
+			case CharacterSet.Tis620ThaiCaseInsensitive:
+			case CharacterSet.Latin7EstonianCaseSensitive:
+			case CharacterSet.Latin2HungarianCaseInsensitive:
+			case CharacterSet.Koi8uGeneralCaseInsensitive:
+			case CharacterSet.Cp1251UkrainianCaseInsensitive:
+			case CharacterSet.GreekGeneralCaseInsensitive:
+			case CharacterSet.Cp1250GeneralCaseInsensitive:
+			case CharacterSet.Latin2CroatianCaseInsensitive:
+			case CharacterSet.Cp1257LithuanianCaseInsensitive:
+			case CharacterSet.Latin5TurkishCaseInsensitive:
+			case CharacterSet.Latin1German2CaseInsensitive:
+			case CharacterSet.Armscii8GeneralCaseInsensitive:
+			case CharacterSet.Cp1250CzechCaseSensitive:
+			case CharacterSet.Cp866GeneralCaseInsensitive:
+			case CharacterSet.Keybcs2GeneralCaseInsensitive:
+			case CharacterSet.MacceGeneralCaseInsensitive:
+			case CharacterSet.MacromanGeneralCaseInsensitive:
+			case CharacterSet.Cp852GeneralCaseInsensitive:
+			case CharacterSet.Latin7GeneralCaseInsensitive:
+			case CharacterSet.Latin7GeneralCaseSensitive:
+			case CharacterSet.MacceBinary:
+			case CharacterSet.Cp1250CroatianCaseInsensitive:
+			case CharacterSet.Latin1Binary:
+			case CharacterSet.Latin1GeneralCaseInsensitive:
+			case CharacterSet.Latin1GeneralCaseSensitive:
+			case CharacterSet.Cp1251Binary:
+			case CharacterSet.Cp1251GeneralCaseInsensitive:
+			case CharacterSet.Cp1251GeneralCaseSensitive:
+			case CharacterSet.MacromanBinary:
+			case CharacterSet.Cp1256GeneralCaseInsensitive:
+			case CharacterSet.Cp1257Binary:
+			case CharacterSet.Cp1257GeneralCaseInsensitive:
+			case CharacterSet.Binary:
+			case CharacterSet.Armscii8Binary:
+			case CharacterSet.AsciiBinary:
+			case CharacterSet.Cp1250Binary:
+			case CharacterSet.Cp1256Binary:
+			case CharacterSet.Cp866Binary:
+			case CharacterSet.Dec8Binary:
+			case CharacterSet.GreekBinary:
+			case CharacterSet.HebrewBinary:
+			case CharacterSet.Hp8Binary:
+			case CharacterSet.Keybcs2Binary:
+			case CharacterSet.Koi8rBinary:
+			case CharacterSet.Koi8uBinary:
+			case CharacterSet.Latin2Binary:
+			case CharacterSet.Latin5Binary:
+			case CharacterSet.Latin7Binary:
+			case CharacterSet.Cp850Binary:
+			case CharacterSet.Cp852Binary:
+			case CharacterSet.Swe7Binary:
+			case CharacterSet.Tis620Binary:
+			case CharacterSet.Geostd8GeneralCaseInsensitive:
+			case CharacterSet.Geostd8Binary:
+			case CharacterSet.Latin1SpanishCaseInsensitive:
+			case CharacterSet.Cp1250PolishCaseInsensitive:
+			case CharacterSet.Dec8SwedishNoPadCaseInsensitive:
+			case CharacterSet.Cp850GeneralNoPadCaseInsensitive:
+			case CharacterSet.Hp8EnglishNoPadCaseInsensitive:
+			case CharacterSet.Koi8rGeneralNoPadCaseInsensitive:
+			case CharacterSet.Latin1SwedishNoPadCaseInsensitive:
+			case CharacterSet.Latin2GeneralNoPadCaseInsensitive:
+			case CharacterSet.Swe7SwedishNoPadCaseInsensitive:
+			case CharacterSet.AsciiGeneralNoPadCaseInsensitive:
+			case CharacterSet.HebrewGeneralNoPadCaseInsensitive:
+			case CharacterSet.Tis620ThaiNoPadCaseInsensitive:
+			case CharacterSet.Koi8uGeneralNoPadCaseInsensitive:
+			case CharacterSet.GreekGeneralNoPadCaseInsensitive:
+			case CharacterSet.Cp1250GeneralNoPadCaseInsensitive:
+			case CharacterSet.Latin5TurkishNoPadCaseInsensitive:
+			case CharacterSet.Armscii8GeneralNoPadCaseInsensitive:
+			case CharacterSet.Cp866GeneralNoPadCaseInsensitive:
+			case CharacterSet.Keybcs2GeneralNoPadCaseInsensitive:
+			case CharacterSet.MacCentralEuropeanGeneralNoPadCaseInsensitive:
+			case CharacterSet.MacRomanGeneralNoPadCaseInsensitive:
+			case CharacterSet.Cp852GeneralNoPadCaseInsensitive:
+			case CharacterSet.Latin7GeneralNoPadCaseInsensitive:
+			case CharacterSet.MacCentralEuropeanNoPadBinary:
+			case CharacterSet.Latin1NoPadBinary:
+			case CharacterSet.Cp1251NoPadBinary:
+			case CharacterSet.Cp1251GeneralNoPadCaseInsensitive:
+			case CharacterSet.MacRomanNoPadBinary:
+			case CharacterSet.Cp1256GeneralNoPadCaseInsensitive:
+			case CharacterSet.Cp1257NoPadBinary:
+			case CharacterSet.Cp1257GeneralNoPadCaseInsensitive:
+			case CharacterSet.Armscii8NoPadBinary:
+			case CharacterSet.AsciiNoPadBinary:
+			case CharacterSet.Cp1250NoPadBinary:
+			case CharacterSet.Cp1256NoPadBinary:
+			case CharacterSet.Cp866NoPadBinary:
+			case CharacterSet.Dec8NoPadBinary:
+			case CharacterSet.GreekNoPadBinary:
+			case CharacterSet.HebrewNoPadBinary:
+			case CharacterSet.Hp8NoPadBinary:
+			case CharacterSet.Keybcs2NoPadBinary:
+			case CharacterSet.Koi8rNoPadBinary:
+			case CharacterSet.Koi8uNoPadBinary:
+			case CharacterSet.Latin2NoPadBinary:
+			case CharacterSet.Latin5NoPadBinary:
+			case CharacterSet.Latin7NoPadBinary:
+			case CharacterSet.Cp850NoPadBinary:
+			case CharacterSet.Cp852NoPadBinary:
+			case CharacterSet.Swe7NoPadBinary:
+			case CharacterSet.Tis620NoPadBinary:
+			case CharacterSet.Geostd8GeneralNoPadCaseInsensitive:
+			case CharacterSet.Geostd8NoPadBinary:
+				return 1;
+
+			case CharacterSet.Big5ChineseCaseInsensitive:
+			case CharacterSet.SjisJapaneseCaseInsensitive:
+			case CharacterSet.EuckrKoreanCaseInsensitive:
+			case CharacterSet.Gb2312ChineseCaseInsensitive:
+			case CharacterSet.GbkChineseCaseInsensitive:
+			case CharacterSet.Ucs2GeneralCaseInsensitive:
+			case CharacterSet.Big5Binary:
+			case CharacterSet.EuckrBinary:
+			case CharacterSet.Gb2312Binary:
+			case CharacterSet.GbkBinary:
+			case CharacterSet.SjisBinary:
+			case CharacterSet.Ucs2Binary:
+			case CharacterSet.Cp932JapaneseCaseInsensitive:
+			case CharacterSet.Cp932Binary:
+			case CharacterSet.Ucs2UnicodeCaseInsensitive:
+			case CharacterSet.Ucs2IcelandicCaseInsensitive:
+			case CharacterSet.Ucs2LatvianCaseInsensitive:
+			case CharacterSet.Ucs2RomanianCaseInsensitive:
+			case CharacterSet.Ucs2SlovenianCaseInsensitive:
+			case CharacterSet.Ucs2PolishCaseInsensitive:
+			case CharacterSet.Ucs2EstonianCaseInsensitive:
+			case CharacterSet.Ucs2SpanishCaseInsensitive:
+			case CharacterSet.Ucs2SwedishCaseInsensitive:
+			case CharacterSet.Ucs2TurkishCaseInsensitive:
+			case CharacterSet.Ucs2CzechCaseInsensitive:
+			case CharacterSet.Ucs2DanishCaseInsensitive:
+			case CharacterSet.Ucs2LithuanianCaseInsensitive:
+			case CharacterSet.Ucs2SlovakCaseInsensitive:
+			case CharacterSet.Ucs2Spanish2CaseInsensitive:
+			case CharacterSet.Ucs2RomanCaseInsensitive:
+			case CharacterSet.Ucs2PersianCaseInsensitive:
+			case CharacterSet.Ucs2EsperantoCaseInsensitive:
+			case CharacterSet.Ucs2HungarianCaseInsensitive:
+			case CharacterSet.Ucs2SinhalaCaseInsensitive:
+			case CharacterSet.Ucs2German2CaseInsensitive:
+			case CharacterSet.Ucs2CroatianCaseInsensitive:
+			case CharacterSet.Ucs2Unicode520CaseInsensitive:
+			case CharacterSet.Ucs2VietnameseCaseInsensitive:
+			case CharacterSet.Ucs2GeneralMySql500CaseInsensitive:
+			case CharacterSet.Ucs2CroatianCaseInsensitiveMariaDb:
+			case CharacterSet.Ucs2MyanmarCaseInsensitive:
+			case CharacterSet.Ucs2ThaiUnicode520Weight2:
+			case CharacterSet.Big5ChineseNoPadCaseInsensitive:
+			case CharacterSet.SjisJapaneseNoPadCaseInsensitive:
+			case CharacterSet.EuckrKoreanNoPadCaseInsensitive:
+			case CharacterSet.Gb2312ChineseNoPadCaseInsensitive:
+			case CharacterSet.GbkChineseNoPadCaseInsensitive:
+			case CharacterSet.Ucs2GeneralNoPadCaseInsensitive:
+			case CharacterSet.Big5NoPadBinary:
+			case CharacterSet.EuckrNoPadBinary:
+			case CharacterSet.Gb2312NoPadBinary:
+			case CharacterSet.GbkNoPadBinary:
+			case CharacterSet.SjisNoPadBinary:
+			case CharacterSet.Ucs2NoPadBinary:
+			case CharacterSet.Cp932JapaneseNoPadCaseInsensitive:
+			case CharacterSet.Cp932NoPadBinary:
+			case CharacterSet.Ucs2UnicodeNoPadCaseInsensitive:
+			case CharacterSet.Ucs2Unicode520NoPadCaseInsensitive:
+				return 2;
+
+			case CharacterSet.UjisJapaneseCaseInsensitive:
+			case CharacterSet.Utf8Mb3GeneralCaseInsensitive:
+			case CharacterSet.Utf8Mb3ToLowerCaseInsensitive:
+			case CharacterSet.Utf8Mb3Binary:
+			case CharacterSet.UjisBinary:
+			case CharacterSet.EucjpmsJapaneseCaseInsensitive:
+			case CharacterSet.EucjpmsBinary:
+			case CharacterSet.Utf8Mb3UnicodeCaseInsensitive:
+			case CharacterSet.Utf8Mb3IcelandicCaseInsensitive:
+			case CharacterSet.Utf8Mb3LatvianCaseInsensitive:
+			case CharacterSet.Utf8Mb3RomanianCaseInsensitive:
+			case CharacterSet.Utf8Mb3SlovenianCaseInsensitive:
+			case CharacterSet.Utf8Mb3PolishCaseInsensitive:
+			case CharacterSet.Utf8Mb3EstonianCaseInsensitive:
+			case CharacterSet.Utf8Mb3SpanishCaseInsensitive:
+			case CharacterSet.Utf8Mb3SwedishCaseInsensitive:
+			case CharacterSet.Utf8Mb3TurkishCaseInsensitive:
+			case CharacterSet.Utf8Mb3CzechCaseInsensitive:
+			case CharacterSet.Utf8Mb3DanishCaseInsensitive:
+			case CharacterSet.Utf8Mb3LithuanianCaseInsensitive:
+			case CharacterSet.Utf8Mb3SlovakCaseInsensitive:
+			case CharacterSet.Utf8Mb3Spanish2CaseInsensitive:
+			case CharacterSet.Utf8Mb3RomanCaseInsensitive:
+			case CharacterSet.Utf8Mb3PersianCaseInsensitive:
+			case CharacterSet.Utf8Mb3EsperantoCaseInsensitive:
+			case CharacterSet.Utf8Mb3HungarianCaseInsensitive:
+			case CharacterSet.Utf8Mb3SinhalaCaseInsensitive:
+			case CharacterSet.Utf8Mb3German2CaseInsensitive:
+			case CharacterSet.Utf8Mb3CroatianCaseInsensitive:
+			case CharacterSet.Utf8Mb3Unicode520CaseInsensitive:
+			case CharacterSet.Utf8Mb3VietnameseCaseInsensitive:
+			case CharacterSet.Utf8Mb3GeneralMySql500CaseInsensitive:
+			case CharacterSet.Utf8Mb3CroatianCaseInsensitiveMariaDb:
+			case CharacterSet.Utf8Mb3MyanmarCaseInsensitive:
+			case CharacterSet.Utf8Mb3ThaiUnicode520Weight2:
+			case CharacterSet.Utf8Mb3General1400AccentSensitiveCaseInsensitive:
+			case CharacterSet.UjisJapaneseNoPadCaseInsensitive:
+			case CharacterSet.Utf8Mb3GeneralNoPadCaseInsensitive:
+			case CharacterSet.Utf8Mb3NoPadBinary:
+			case CharacterSet.UjisNoPadBinary:
+			case CharacterSet.EucjpmsJapaneseNoPadCaseInsensitive:
+			case CharacterSet.EucjpmsNoPadBinary:
+			case CharacterSet.Utf8Mb3UnicodeNoPadCaseInsensitive:
+			case CharacterSet.Utf8Mb3Unicode520NoPadCaseInsensitive:
+				return 3;
+
+			case CharacterSet.Utf8Mb4GeneralCaseInsensitive:
+			case CharacterSet.Utf8Mb4Binary:
+			case CharacterSet.Utf16GeneralCaseInsensitive:
+			case CharacterSet.Utf16Binary:
+			case CharacterSet.Utf16leGeneralCaseInsensitive:
+			case CharacterSet.Utf32GeneralCaseInsensitive:
+			case CharacterSet.Utf32Binary:
+			case CharacterSet.Utf16leBinary:
+			case CharacterSet.Utf16UnicodeCaseInsensitive:
+			case CharacterSet.Utf16IcelandicCaseInsensitive:
+			case CharacterSet.Utf16LatvianCaseInsensitive:
+			case CharacterSet.Utf16RomanianCaseInsensitive:
+			case CharacterSet.Utf16SlovenianCaseInsensitive:
+			case CharacterSet.Utf16PolishCaseInsensitive:
+			case CharacterSet.Utf16EstonianCaseInsensitive:
+			case CharacterSet.Utf16SpanishCaseInsensitive:
+			case CharacterSet.Utf16SwedishCaseInsensitive:
+			case CharacterSet.Utf16TurkishCaseInsensitive:
+			case CharacterSet.Utf16CzechCaseInsensitive:
+			case CharacterSet.Utf16DanishCaseInsensitive:
+			case CharacterSet.Utf16LithuanianCaseInsensitive:
+			case CharacterSet.Utf16SlovakCaseInsensitive:
+			case CharacterSet.Utf16Spanish2CaseInsensitive:
+			case CharacterSet.Utf16RomanCaseInsensitive:
+			case CharacterSet.Utf16PersianCaseInsensitive:
+			case CharacterSet.Utf16EsperantoCaseInsensitive:
+			case CharacterSet.Utf16HungarianCaseInsensitive:
+			case CharacterSet.Utf16SinhalaCaseInsensitive:
+			case CharacterSet.Utf16German2CaseInsensitive:
+			case CharacterSet.Utf16CroatianCaseInsensitive:
+			case CharacterSet.Utf16Unicode520CaseInsensitive:
+			case CharacterSet.Utf16VietnameseCaseInsensitive:
+			case CharacterSet.Utf32UnicodeCaseInsensitive:
+			case CharacterSet.Utf32IcelandicCaseInsensitive:
+			case CharacterSet.Utf32LatvianCaseInsensitive:
+			case CharacterSet.Utf32RomanianCaseInsensitive:
+			case CharacterSet.Utf32SlovenianCaseInsensitive:
+			case CharacterSet.Utf32PolishCaseInsensitive:
+			case CharacterSet.Utf32EstonianCaseInsensitive:
+			case CharacterSet.Utf32SpanishCaseInsensitive:
+			case CharacterSet.Utf32SwedishCaseInsensitive:
+			case CharacterSet.Utf32TurkishCaseInsensitive:
+			case CharacterSet.Utf32CzechCaseInsensitive:
+			case CharacterSet.Utf32DanishCaseInsensitive:
+			case CharacterSet.Utf32LithuanianCaseInsensitive:
+			case CharacterSet.Utf32SlovakCaseInsensitive:
+			case CharacterSet.Utf32Spanish2CaseInsensitive:
+			case CharacterSet.Utf32RomanCaseInsensitive:
+			case CharacterSet.Utf32PersianCaseInsensitive:
+			case CharacterSet.Utf32EsperantoCaseInsensitive:
+			case CharacterSet.Utf32HungarianCaseInsensitive:
+			case CharacterSet.Utf32SinhalaCaseInsensitive:
+			case CharacterSet.Utf32German2CaseInsensitive:
+			case CharacterSet.Utf32CroatianCaseInsensitive:
+			case CharacterSet.Utf32Unicode520CaseInsensitive:
+			case CharacterSet.Utf32VietnameseCaseInsensitive:
+			case CharacterSet.Utf8Mb4UnicodeCaseInsensitive:
+			case CharacterSet.Utf8Mb4IcelandicCaseInsensitive:
+			case CharacterSet.Utf8Mb4LatvianCaseInsensitive:
+			case CharacterSet.Utf8Mb4RomanianCaseInsensitive:
+			case CharacterSet.Utf8Mb4SlovenianCaseInsensitive:
+			case CharacterSet.Utf8Mb4PolishCaseInsensitive:
+			case CharacterSet.Utf8Mb4EstonianCaseInsensitive:
+			case CharacterSet.Utf8Mb4SpanishCaseInsensitive:
+			case CharacterSet.Utf8Mb4SwedishCaseInsensitive:
+			case CharacterSet.Utf8Mb4TurkishCaseInsensitive:
+			case CharacterSet.Utf8Mb4CzechCaseInsensitive:
+			case CharacterSet.Utf8Mb4DanishCaseInsensitive:
+			case CharacterSet.Utf8Mb4LithuanianCaseInsensitive:
+			case CharacterSet.Utf8Mb4SlovakCaseInsensitive:
+			case CharacterSet.Utf8Mb4Spanish2CaseInsensitive:
+			case CharacterSet.Utf8Mb4RomanCaseInsensitive:
+			case CharacterSet.Utf8Mb4PersianCaseInsensitive:
+			case CharacterSet.Utf8Mb4EsperantoCaseInsensitive:
+			case CharacterSet.Utf8Mb4HungarianCaseInsensitive:
+			case CharacterSet.Utf8Mb4SinhalaCaseInsensitive:
+			case CharacterSet.Utf8Mb4German2CaseInsensitive:
+			case CharacterSet.Utf8Mb4CroatianCaseInsensitive:
+			case CharacterSet.Utf8Mb4Unicode520CaseInsensitive:
+			case CharacterSet.Utf8Mb4VietnameseCaseInsensitive:
+			case CharacterSet.Gb18030ChineseCaseInsensitive:
+			case CharacterSet.Gb18030Binary:
+			case CharacterSet.Gb18030Unicode520CaseInsensitive:
+			case CharacterSet.Utf8Mb4Uca900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4GermanPhonebookUca900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4IcelandicUca900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4LatvianUca900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4RomanianUca900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4SlovenianUca900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4PolishUca900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4EstonianUca900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4SpanishUca900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4SwedishUca900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4TurkishUca900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4CaseSensitiveUca900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4DanishUca900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4LithuanianUca900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4SlovakUca900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4TraditionalSpanishUca900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4LatinUca900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4EsperantoUca900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4HungarianUca900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4CroatianUca900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4VietnameseUca900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4Uca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4GermanPhonebookUca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4IcelandicUca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4LatvianUca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4RomanianUca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4SlovenianUca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4PolishUca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4EstonianUca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4SpanishUca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4SwedishUca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4TurkishUca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4CaseSensitiveUca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4DanishUca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4LithuanianUca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4SlovakUca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4TraditionalSpanishUca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4LatinUca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4EsperantoUca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4HungarianUca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4CroatianUca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4VietnameseUca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4JapaneseUca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4JapaneseUca900AccentSensitiveCaseSensitiveKanaSensitive:
+			case CharacterSet.Utf8Mb4Uca900AccentSensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4RussianUca900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4RussianUca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4ChineseUca900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4Uca900Binary:
+			case CharacterSet.Utf8Mb4NorwegianBokmal0900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4NorwegianBokmal0900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4NorwegianNynorsk0900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4NorwegianNynorsk0900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4SerbianLatin0900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4SerbianLatin0900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4Bosnian0900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4Bosnian0900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4Bulgarian0900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4Bulgarian0900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4Galician0900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4Galician0900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4MongolianCyrillic0900AccentInsensitiveCaseInsensitive:
+			case CharacterSet.Utf8Mb4MongolianCyrillic0900AccentSensitiveCaseSensitive:
+			case CharacterSet.Utf8Mb4CroatianCaseInsensitiveMariaDb:
+			case CharacterSet.Utf8Mb4MyanmarCaseInsensitive:
+			case CharacterSet.Utf8Mb4ThaiUnicode520Weight2:
+			case CharacterSet.Utf8Mb4General1400AccentSensitiveCaseInsensitive:
+			case CharacterSet.Utf16CroatianCaseInsensitiveMariaDb:
+			case CharacterSet.Utf16MyanmarCaseInsensitive:
+			case CharacterSet.Utf16ThaiUnicode520Weight2:
+			case CharacterSet.Utf32CroatianCaseInsensitiveMariaDb:
+			case CharacterSet.Utf32MyanmarCaseInsensitive:
+			case CharacterSet.Utf32ThaiUnicode520Weight2:
+			case CharacterSet.Utf8Mb4GeneralNoPadCaseInsensitive:
+			case CharacterSet.Utf8Mb4NoPadBinary:
+			case CharacterSet.Utf16GeneralNoPadCaseInsensitive:
+			case CharacterSet.Utf16NoPadBinary:
+			case CharacterSet.Utf16leGeneralNoPadCaseInsensitive:
+			case CharacterSet.Utf32GeneralNoPadCaseInsensitive:
+			case CharacterSet.Utf32NoPadBinary:
+			case CharacterSet.Utf16leNoPadBinary:
+			case CharacterSet.Utf16UnicodeNoPadCaseInsensitive:
+			case CharacterSet.Utf16Unicode520NoPadCaseInsensitive:
+			case CharacterSet.Utf32UnicodeNoPadCaseInsensitive:
+			case CharacterSet.Utf32Unicode520NoPadCaseInsensitive:
+			case CharacterSet.Utf8Mb4UnicodeNoPadCaseInsensitive:
+			case CharacterSet.Utf8Mb4Unicode520NoPadCaseInsensitive:
+				return 4;
+
+			default:
+				throw new NotSupportedException($"Maximum byte length of character set {characterSet} is unknown.");
 		}
 	}
 
-	private static ValueTask<Packet> ReadPacketAsync(BufferedByteReader bufferedByteReader, IByteHandler byteHandler, Func<int> getNextSequenceNumber, ProtocolErrorBehavior protocolErrorBehavior, IOBehavior ioBehavior)
-	{
-		var headerBytesTask = bufferedByteReader.ReadBytesAsync(byteHandler, 4, ioBehavior);
-		if (headerBytesTask.IsCompletedSuccessfully)
-			return ReadPacketAfterHeader(headerBytesTask.Result, bufferedByteReader, byteHandler, getNextSequenceNumber, protocolErrorBehavior, ioBehavior);
-		return AddContinuation(headerBytesTask, bufferedByteReader, byteHandler, getNextSequenceNumber, protocolErrorBehavior, ioBehavior);
-
-		static async ValueTask<Packet> AddContinuation(ValueTask<ArraySegment<byte>> headerBytes, BufferedByteReader bufferedByteReader, IByteHandler byteHandler, Func<int> getNextSequenceNumber, ProtocolErrorBehavior protocolErrorBehavior, IOBehavior ioBehavior) =>
-			await ReadPacketAfterHeader(await headerBytes.ConfigureAwait(false), bufferedByteReader, byteHandler, getNextSequenceNumber, protocolErrorBehavior, ioBehavior).ConfigureAwait(false);
-	}
-
-	private static ValueTask<Packet> ReadPacketAfterHeader(ReadOnlySpan<byte> headerBytes, BufferedByteReader bufferedByteReader, IByteHandler byteHandler, Func<int> getNextSequenceNumber, ProtocolErrorBehavior protocolErrorBehavior, IOBehavior ioBehavior)
+	public static async ValueTask<ArraySegment<byte>> ReadPayloadAsync(BufferedByteReader bufferedByteReader, IByteHandler byteHandler, Func<int> getNextSequenceNumber, ArraySegmentHolder<byte> previousPayloads, ProtocolErrorBehavior protocolErrorBehavior, IOBehavior ioBehavior)
 	{
-		if (headerBytes.Length < 4)
+		previousPayloads.Clear();
+		while (true)
 		{
-			return protocolErrorBehavior == ProtocolErrorBehavior.Ignore ? default :
-				ValueTaskExtensions.FromException<Packet>(new EndOfStreamException($"Expected to read 4 header bytes but only received {headerBytes.Length:d}."));
-		}
-
-		var payloadLength = (int) SerializationUtility.ReadUInt32(headerBytes[..3]);
-		int packetSequenceNumber = headerBytes[3];
-
-		Exception? packetOutOfOrderException = null;
-		var expectedSequenceNumber = getNextSequenceNumber() % 256;
-		if (expectedSequenceNumber != -1 && packetSequenceNumber != expectedSequenceNumber)
-			packetOutOfOrderException = MySqlProtocolException.CreateForPacketOutOfOrder(expectedSequenceNumber, packetSequenceNumber);
+			// read the packet header
+			var headerBytes = await bufferedByteReader.ReadBytesAsync(byteHandler, 4, ioBehavior).ConfigureAwait(false);
+			if (headerBytes.Count < 4)
+			{
+				return protocolErrorBehavior == ProtocolErrorBehavior.Ignore ? default :
+					throw new MySqlEndOfStreamException(4, headerBytes.Count);
+			}
 
-		var payloadBytesTask = bufferedByteReader.ReadBytesAsync(byteHandler, payloadLength, ioBehavior);
-		if (payloadBytesTask.IsCompletedSuccessfully)
-			return CreatePacketFromPayload(payloadBytesTask.Result, payloadLength, protocolErrorBehavior, packetOutOfOrderException);
-		return AddContinuation(payloadBytesTask, payloadLength, protocolErrorBehavior, packetOutOfOrderException);
+			// read values from the header before the memory is potentially overwritten by ReadBytesAsync
+			var payloadLength = (int) SerializationUtility.ReadUInt32(headerBytes.AsSpan()[..3]);
+			int packetSequenceNumber = headerBytes.AsSpan()[3];
+			var expectedSequenceNumber = getNextSequenceNumber() % 256;
 
-		static async ValueTask<Packet> AddContinuation(ValueTask<ArraySegment<byte>> payloadBytesTask, int payloadLength, ProtocolErrorBehavior protocolErrorBehavior, Exception? packetOutOfOrderException) =>
-			await CreatePacketFromPayload(await payloadBytesTask.ConfigureAwait(false), payloadLength, protocolErrorBehavior, packetOutOfOrderException).ConfigureAwait(false);
-	}
-
-	private static ValueTask<Packet> CreatePacketFromPayload(ArraySegment<byte> payloadBytes, int payloadLength, ProtocolErrorBehavior protocolErrorBehavior, Exception? exception)
-	{
-		if (exception is not null)
-		{
-			if (protocolErrorBehavior == ProtocolErrorBehavior.Ignore)
-				return default;
+			// read the packet payload
+			var payloadBytes = await bufferedByteReader.ReadBytesAsync(byteHandler, payloadLength, ioBehavior).ConfigureAwait(false);
 
+			Packet packet;
+			if (expectedSequenceNumber != -1 && packetSequenceNumber != expectedSequenceNumber)
+			{
+				if (protocolErrorBehavior == ProtocolErrorBehavior.Ignore)
+					packet = default;
 #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
-			if (payloadBytes is [ ErrorPayload.Signature, .. ])
+				else if (payloadBytes is [ErrorPayload.Signature, ..])
 #else
-			if (payloadBytes.Count > 0 && payloadBytes.AsSpan()[0] == ErrorPayload.Signature)
+				else if (payloadBytes.Count > 0 && payloadBytes.AsSpan()[0] == ErrorPayload.Signature)
 #endif
-				return new ValueTask<Packet>(new Packet(payloadBytes));
-
-			return ValueTaskExtensions.FromException<Packet>(exception);
-		}
-
-		return payloadBytes.Count >= payloadLength ? new ValueTask<Packet>(new Packet(payloadBytes)) :
-			protocolErrorBehavior == ProtocolErrorBehavior.Throw ? ValueTaskExtensions.FromException<Packet>(new EndOfStreamException($"Expected to read {payloadLength:d} payload bytes but only received {payloadBytes.Count:d}.")) :
-			default;
-	}
-
-	public static ValueTask<ArraySegment<byte>> ReadPayloadAsync(BufferedByteReader bufferedByteReader, IByteHandler byteHandler, Func<int> getNextSequenceNumber, ArraySegmentHolder<byte> cache, ProtocolErrorBehavior protocolErrorBehavior, IOBehavior ioBehavior)
-	{
-		cache.Clear();
-		return DoReadPayloadAsync(bufferedByteReader, byteHandler, getNextSequenceNumber, cache, protocolErrorBehavior, ioBehavior);
-	}
-
-	private static ValueTask<ArraySegment<byte>> DoReadPayloadAsync(BufferedByteReader bufferedByteReader, IByteHandler byteHandler, Func<int> getNextSequenceNumber, ArraySegmentHolder<byte> previousPayloads, ProtocolErrorBehavior protocolErrorBehavior, IOBehavior ioBehavior)
-	{
-		var readPacketTask = ReadPacketAsync(bufferedByteReader, byteHandler, getNextSequenceNumber, protocolErrorBehavior, ioBehavior);
-		while (readPacketTask.IsCompletedSuccessfully)
-		{
-			if (HasReadPayload(previousPayloads, readPacketTask.Result, out var result))
-				return result;
-
-			readPacketTask = ReadPacketAsync(bufferedByteReader, byteHandler, getNextSequenceNumber, protocolErrorBehavior, ioBehavior);
-		}
-
-		return AddContinuation(readPacketTask, bufferedByteReader, byteHandler, getNextSequenceNumber, previousPayloads, protocolErrorBehavior, ioBehavior);
-
-		static async ValueTask<ArraySegment<byte>> AddContinuation(ValueTask<Packet> readPacketTask, BufferedByteReader bufferedByteReader, IByteHandler byteHandler, Func<int> getNextSequenceNumber, ArraySegmentHolder<byte> previousPayloads, ProtocolErrorBehavior protocolErrorBehavior, IOBehavior ioBehavior)
-		{
-			var packet = await readPacketTask.ConfigureAwait(false);
-			var resultTask = HasReadPayload(previousPayloads, packet, out var result) ? result :
-				DoReadPayloadAsync(bufferedByteReader, byteHandler, getNextSequenceNumber, previousPayloads, protocolErrorBehavior, ioBehavior);
-			return await resultTask.ConfigureAwait(false);
-		}
-	}
+					packet = new(payloadBytes);
+				else
+					throw MySqlProtocolException.CreateForPacketOutOfOrder(expectedSequenceNumber, packetSequenceNumber);
+			}
+			else
+			{
+				packet = payloadBytes.Count >= payloadLength ? new(payloadBytes) :
+					protocolErrorBehavior == ProtocolErrorBehavior.Throw ? throw new MySqlEndOfStreamException(payloadLength, payloadBytes.Count) :
+					default;
+			}
 
-	private static bool HasReadPayload(ArraySegmentHolder<byte> previousPayloads, Packet packet, out ValueTask<ArraySegment<byte>> result)
-	{
-		if (previousPayloads.Count == 0 && packet.Contents.Count < MaxPacketSize)
-		{
-			result = new(packet.Contents);
-			return true;
-		}
+			// if this is a complete packet, return it
+			if (previousPayloads.Count == 0 && packet.Contents.Count < MaxPacketSize)
+				return packet.Contents;
 
-		var previousPayloadsArray = previousPayloads.Array;
-		if (previousPayloadsArray is null)
-			previousPayloadsArray = new byte[ProtocolUtility.MaxPacketSize + 1];
-		else if (previousPayloads.Offset + previousPayloads.Count + packet.Contents.Count > previousPayloadsArray.Length)
-			Array.Resize(ref previousPayloadsArray, previousPayloadsArray.Length * 2);
+			// resize the buffer of previous payloads if necessary, then append this payload to it
+			var previousPayloadsArray = previousPayloads.Array;
+			if (previousPayloadsArray is null)
+				previousPayloadsArray = new byte[ProtocolUtility.MaxPacketSize + 1];
+			else if (previousPayloads.Offset + previousPayloads.Count + packet.Contents.Count > previousPayloadsArray.Length)
+				Array.Resize(ref previousPayloadsArray, previousPayloadsArray.Length * 2);
 
-		Buffer.BlockCopy(packet.Contents.Array!, packet.Contents.Offset, previousPayloadsArray, previousPayloads.Offset + previousPayloads.Count, packet.Contents.Count);
-		previousPayloads.ArraySegment = new(previousPayloadsArray, previousPayloads.Offset, previousPayloads.Count + packet.Contents.Count);
+			packet.Contents.AsSpan().CopyTo(previousPayloadsArray.AsSpan(previousPayloads.Offset + previousPayloads.Count));
+			previousPayloads.ArraySegment = new(previousPayloadsArray, previousPayloads.Offset, previousPayloads.Count + packet.Contents.Count);
 
-		if (packet.Contents.Count < ProtocolUtility.MaxPacketSize)
-		{
-			result = new(previousPayloads.ArraySegment);
-			return true;
+			if (packet.Contents.Count < ProtocolUtility.MaxPacketSize)
+				return previousPayloads.ArraySegment;
 		}
-
-		result = default;
-		return false;
 	}
 
-	public static ValueTask WritePayloadAsync(IByteHandler byteHandler, Func<int> getNextSequenceNumber, ReadOnlyMemory<byte> payload, IOBehavior ioBehavior)
+	public static async ValueTask WritePayloadAsync(IByteHandler byteHandler, Func<int> getNextSequenceNumber, ReadOnlyMemory<byte> payload, IOBehavior ioBehavior)
 	{
-		return payload.Length <= MaxPacketSize ? WritePacketAsync(byteHandler, getNextSequenceNumber(), payload, ioBehavior) :
-			WritePayloadAsyncAwaited(byteHandler, getNextSequenceNumber, payload, ioBehavior);
-
-		static async ValueTask WritePayloadAsyncAwaited(IByteHandler byteHandler, Func<int> getNextSequenceNumber, ReadOnlyMemory<byte> payload, IOBehavior ioBehavior)
+		var buffer = ArrayPool<byte>.Shared.Rent(Math.Min(MaxPacketSize, payload.Length) + 4);
+		try
 		{
-			for (var bytesSent = 0; bytesSent < payload.Length; bytesSent += MaxPacketSize)
+			var bytesSent = 0;
+			do
 			{
 				var contents = payload.Slice(bytesSent, Math.Min(MaxPacketSize, payload.Length - bytesSent));
-				await WritePacketAsync(byteHandler, getNextSequenceNumber(), contents, ioBehavior).ConfigureAwait(false);
-			}
-		}
-	}
+				var bufferLength = contents.Length + 4;
 
-	private static ValueTask WritePacketAsync(IByteHandler byteHandler, int sequenceNumber, ReadOnlyMemory<byte> contents, IOBehavior ioBehavior)
-	{
-		var bufferLength = contents.Length + 4;
-		var buffer = ArrayPool<byte>.Shared.Rent(bufferLength);
-		SerializationUtility.WriteUInt32((uint) contents.Length, buffer, 0, 3);
-		buffer[3] = (byte) sequenceNumber;
-		contents.CopyTo(buffer.AsMemory()[4..]);
-		var task = byteHandler.WriteBytesAsync(new ArraySegment<byte>(buffer, 0, bufferLength), ioBehavior);
-		if (task.IsCompletedSuccessfully)
-		{
-			ArrayPool<byte>.Shared.Return(buffer);
-			return default;
-		}
-		return WritePacketAsyncAwaited(task, buffer);
+				SerializationUtility.WriteUInt32((uint) contents.Length, buffer, 0, 3);
+				buffer[3] = (byte) getNextSequenceNumber();
+				contents.CopyTo(buffer.AsMemory(4));
 
-		static async ValueTask WritePacketAsyncAwaited(ValueTask task, byte[] buffer)
+				await byteHandler.WriteBytesAsync(new ArraySegment<byte>(buffer, 0, bufferLength), ioBehavior).ConfigureAwait(false);
+				bytesSent += contents.Length;
+			}
+			while (bytesSent < payload.Length);
+		}
+		finally
 		{
-			await task.ConfigureAwait(false);
 			ArrayPool<byte>.Shared.Return(buffer);
 		}
 	}
diff --git a/src/MySqlConnector/Protocol/Serialization/StandardPayloadHandler.cs b/src/MySqlConnector/Protocol/Serialization/StandardPayloadHandler.cs
index 337418667..6650dc563 100644
--- a/src/MySqlConnector/Protocol/Serialization/StandardPayloadHandler.cs
+++ b/src/MySqlConnector/Protocol/Serialization/StandardPayloadHandler.cs
@@ -26,14 +26,24 @@ public IByteHandler ByteHandler
 	{
 		get
 		{
+#if NET7_0_OR_GREATER
+			ObjectDisposedException.ThrowIf(m_byteHandler is null, this);
+#else
 			if (m_byteHandler is null)
 				throw new ObjectDisposedException(nameof(StandardPayloadHandler));
+#endif
 			return m_byteHandler;
 		}
 		set
 		{
 			var oldByteHandler = m_byteHandler;
-			m_byteHandler = value ?? throw new ArgumentNullException(nameof(value));
+#if NET6_0_OR_GREATER
+			ArgumentNullException.ThrowIfNull(value);
+#else
+			if (value is null)
+				throw new ArgumentNullException(nameof(value));
+#endif
+			m_byteHandler = value;
 			oldByteHandler?.Dispose();
 			m_bufferedByteReader = new();
 		}
diff --git a/src/MySqlConnector/Protocol/Serialization/StreamByteHandler.cs b/src/MySqlConnector/Protocol/Serialization/StreamByteHandler.cs
index 9dc68a06e..6e61538a2 100644
--- a/src/MySqlConnector/Protocol/Serialization/StreamByteHandler.cs
+++ b/src/MySqlConnector/Protocol/Serialization/StreamByteHandler.cs
@@ -18,8 +18,9 @@ public StreamByteHandler(Stream stream)
 
 	public ValueTask<int> ReadBytesAsync(Memory<byte> buffer, IOBehavior ioBehavior)
 	{
-		return ioBehavior == IOBehavior.Asynchronous ? new ValueTask<int>(DoReadBytesAsync(buffer)) :
+		return
 			RemainingTimeout <= 0 ? ValueTaskExtensions.FromException<int>(MySqlException.CreateForTimeout()) :
+			ioBehavior == IOBehavior.Asynchronous ? new ValueTask<int>(DoReadBytesAsync(buffer)) :
 			m_stream.CanTimeout ? DoReadBytesSync(buffer) :
 			DoReadBytesSyncOverAsync(buffer);
 
@@ -34,9 +35,9 @@ ValueTask<int> DoReadBytesSync(Memory<byte> buffer)
 			}
 			catch (Exception ex)
 			{
-				if (RemainingTimeout != Constants.InfiniteTimeout && ex is IOException { InnerException: SocketException { SocketErrorCode: SocketError.TimedOut } })
-					return ValueTaskExtensions.FromException<int>(MySqlException.CreateForTimeout(ex));
-				return ValueTaskExtensions.FromException<int>(ex);
+				return RemainingTimeout != Constants.InfiniteTimeout && ex is IOException { InnerException: SocketException { SocketErrorCode: SocketError.TimedOut } } ?
+					ValueTaskExtensions.FromException<int>(MySqlException.CreateForTimeout(ex)) :
+					ValueTaskExtensions.FromException<int>(ex);
 			}
 			if (RemainingTimeout != Constants.InfiniteTimeout)
 				RemainingTimeout -= unchecked(Environment.TickCount - startTime);
diff --git a/src/MySqlConnector/Shims/DbColumn.cs b/src/MySqlConnector/Shims/DbColumn.cs
index 7aa45e216..814d125bb 100644
--- a/src/MySqlConnector/Shims/DbColumn.cs
+++ b/src/MySqlConnector/Shims/DbColumn.cs
@@ -1,4 +1,4 @@
-#if NET461
+#if NET462
 #pragma warning disable CA1716 // Don't use reserved language keywords
 namespace System.Data.Common;
 
diff --git a/src/MySqlConnector/Shims/DbDataSource.cs b/src/MySqlConnector/Shims/DbDataSource.cs
new file mode 100644
index 000000000..5152dcb37
--- /dev/null
+++ b/src/MySqlConnector/Shims/DbDataSource.cs
@@ -0,0 +1,93 @@
+#if !NET7_0_OR_GREATER
+namespace System.Data.Common;
+
+public abstract class DbDataSource
+#if NETCOREAPP3_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+	: IDisposable, IAsyncDisposable
+#else
+	: IDisposable
+#endif
+{
+	public abstract string ConnectionString { get; }
+
+	public DbConnection CreateConnection() => CreateDbConnection();
+
+	public DbConnection OpenConnection() => OpenDbConnection();
+
+	public ValueTask<DbConnection> OpenConnectionAsync(CancellationToken cancellationToken = default) =>
+		OpenDbConnectionAsync(cancellationToken);
+
+	public DbCommand CreateCommand(string? commandText = null) => CreateDbCommand(commandText);
+
+#if NET6_0_OR_GREATER
+	public DbBatch CreateBatch() => CreateDbBatch();
+#endif
+
+	public void Dispose()
+	{
+		Dispose(disposing: true);
+		GC.SuppressFinalize(this);
+	}
+
+	public async ValueTask DisposeAsync()
+	{
+		await DisposeAsyncCore().ConfigureAwait(false);
+
+		Dispose(disposing: false);
+#pragma warning disable CA1816 // Call GC.SuppressFinalize correctly
+		GC.SuppressFinalize(this);
+#pragma warning restore CA1816
+	}
+
+	protected abstract DbConnection CreateDbConnection();
+
+	protected virtual DbConnection OpenDbConnection()
+	{
+		var connection = CreateDbConnection();
+
+		try
+		{
+			connection.Open();
+			return connection;
+		}
+		catch
+		{
+			connection.Dispose();
+			throw;
+		}
+	}
+
+	protected virtual async ValueTask<DbConnection> OpenDbConnectionAsync(CancellationToken cancellationToken = default)
+	{
+		var connection = CreateDbConnection();
+
+		try
+		{
+			await connection.OpenAsync(cancellationToken).ConfigureAwait(false);
+			return connection;
+		}
+		catch
+		{
+#if NET5_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+			await connection.DisposeAsync().ConfigureAwait(false);
+#else
+			connection.Dispose();
+#endif
+			throw;
+		}
+	}
+
+	// The shim doesn't support these methods; to use the full DbDataSource the client needs to be on .NET 7.0.
+	protected virtual DbCommand CreateDbCommand(string? commandText = null) => throw new NotSupportedException();
+
+#if NET6_0_OR_GREATER
+	protected virtual DbBatch CreateDbBatch() => throw new NotSupportedException();
+#endif
+
+	protected virtual void Dispose(bool disposing)
+	{
+	}
+
+	protected virtual ValueTask DisposeAsyncCore() => default;
+}
+#endif
diff --git a/src/MySqlConnector/Utilities/ActivitySourceHelper.cs b/src/MySqlConnector/Utilities/ActivitySourceHelper.cs
index 6cfe95bf6..1f2c15a0d 100644
--- a/src/MySqlConnector/Utilities/ActivitySourceHelper.cs
+++ b/src/MySqlConnector/Utilities/ActivitySourceHelper.cs
@@ -1,8 +1,7 @@
-using System.Collections.Generic;
 using System.Diagnostics;
+using System.Diagnostics.Metrics;
 using System.Globalization;
 using System.Reflection;
-using MySqlConnector.Core;
 
 namespace MySqlConnector.Utilities;
 
@@ -18,7 +17,6 @@ internal static class ActivitySourceHelper
 	public const string NetPeerNameTagName = "net.peer.name";
 	public const string NetPeerPortTagName = "net.peer.port";
 	public const string NetTransportTagName = "net.transport";
-	public const string StatusCodeTagName = "otel.status_code";
 	public const string ThreadIdTagName = "thread.id";
 
 	public const string DatabaseSystemValue = "mysql";
@@ -37,18 +35,10 @@ internal static class ActivitySourceHelper
 		return activity;
 	}
 
-	public static void SetSuccess(this Activity activity)
-	{
-		activity.SetStatus(ActivityStatusCode.Ok);
-		activity.SetTag(StatusCodeTagName, "OK");
-	}
-
 	public static void SetException(this Activity activity, Exception exception)
 	{
 		var description = exception is MySqlException mySqlException ? mySqlException.ErrorCode.ToString() : exception.Message;
 		activity.SetStatus(ActivityStatusCode.Error, description);
-		activity.SetTag(StatusCodeTagName, "ERROR");
-		activity.SetTag("otel.status_description", description);
 		activity.AddEvent(new ActivityEvent("exception", tags: new ActivityTagsCollection
 		{
 			{ "exception.type", exception.GetType().FullName },
@@ -66,12 +56,10 @@ public static void CopyTags(IEnumerable<KeyValuePair<string, object?>> tags, Act
 		}
 	}
 
-	private static ActivitySource ActivitySource { get; } = CreateActivitySource();
+	public static Meter Meter { get; } = new("MySqlConnector", GetVersion());
 
-	private static ActivitySource CreateActivitySource()
-	{
-		var assembly = typeof(ActivitySourceHelper).Assembly;
-		var version = assembly.GetCustomAttribute<AssemblyFileVersionAttribute>()!.Version;
-		return new("MySqlConnector", version);
-	}
+	private static ActivitySource ActivitySource { get; } = new("MySqlConnector", GetVersion());
+
+	private static string GetVersion() =>
+		typeof(ActivitySourceHelper).Assembly.GetCustomAttribute<AssemblyFileVersionAttribute>()!.Version;
 }
diff --git a/src/MySqlConnector/Utilities/NullableAttributes.cs b/src/MySqlConnector/Utilities/NullableAttributes.cs
index 833e9b0ec..b444c36c0 100644
--- a/src/MySqlConnector/Utilities/NullableAttributes.cs
+++ b/src/MySqlConnector/Utilities/NullableAttributes.cs
@@ -1,7 +1,7 @@
 // Supports using nullable attributes on older frameworks.
 // Copied from https://github.com/dotnet/corefx/blob/master/src/Common/src/CoreLib/System/Diagnostics/CodeAnalysis/NullableAttributes.cs
 
-#if NET461 || NET471 || NETSTANDARD2_0 || NETCOREAPP2_1
+#if NET462 || NET471 || NET48 || NETSTANDARD2_0 || NETCOREAPP2_1
 namespace System.Diagnostics.CodeAnalysis;
 
 /// <summary>Specifies that null is allowed as an input even if the corresponding type disallows it.</summary>
diff --git a/src/MySqlConnector/Utilities/ResizableArraySegment.cs b/src/MySqlConnector/Utilities/ResizableArraySegment.cs
index d2a0d09b7..d3ae564cb 100644
--- a/src/MySqlConnector/Utilities/ResizableArraySegment.cs
+++ b/src/MySqlConnector/Utilities/ResizableArraySegment.cs
@@ -3,19 +3,12 @@ namespace MySqlConnector.Utilities;
 /// <summary>
 /// An <see cref="ArraySegment{T}"/> that supports having its underlying array reallocated and resized.
 /// </summary>
-internal readonly struct ResizableArraySegment<T>
+internal readonly struct ResizableArraySegment<T>(ResizableArray<T> array, int offset, int count)
 	where T : notnull
 {
-	public ResizableArraySegment(ResizableArray<T> array, int offset, int count)
-	{
-		Array = array;
-		Offset = offset;
-		Count = count;
-	}
+	public ResizableArray<T> Array { get; } = array;
+	public int Offset { get; } = offset;
+	public int Count { get; } = count;
 
-	public ResizableArray<T> Array { get; }
-	public int Offset { get; }
-	public int Count { get; }
-
-	public static implicit operator ReadOnlySpan<T>(ResizableArraySegment<T> segment) => new ReadOnlySpan<T>(segment.Array.Array, segment.Offset, segment.Count);
+	public static implicit operator ReadOnlySpan<T>(ResizableArraySegment<T> segment) => new(segment.Array.Array, segment.Offset, segment.Count);
 }
diff --git a/src/MySqlConnector/Utilities/TimerQueue.cs b/src/MySqlConnector/Utilities/TimerQueue.cs
index 44ee1f8e0..3b120264e 100644
--- a/src/MySqlConnector/Utilities/TimerQueue.cs
+++ b/src/MySqlConnector/Utilities/TimerQueue.cs
@@ -14,8 +14,12 @@ internal sealed class TimerQueue
 	/// <returns>A timer ID that can be passed to <see cref="Remove"/> to cancel the timer.</returns>
 	public uint Add(int delay, Action action)
 	{
+#if NET8_0_OR_GREATER
+		ArgumentOutOfRangeException.ThrowIfNegative(delay);
+#else
 		if (delay < 0)
 			throw new ArgumentOutOfRangeException(nameof(delay), $"delay must not be negative: {delay}");
+#endif
 
 		var current = Environment.TickCount;
 		lock (m_lock)
@@ -64,7 +68,7 @@ private TimerQueue()
 	{
 		m_lock = new();
 		m_timer = new(Callback, this, -1, -1);
-		m_timeoutActions = new();
+		m_timeoutActions = [];
 	}
 
 	private void Callback(object? obj)
@@ -127,7 +131,11 @@ public Data(uint id, int time, Action action)
 		public Action Action { get; }
 	}
 
+#if NET9_0_OR_GREATER
+	private readonly Lock m_lock;
+#else
 	private readonly object m_lock;
+#endif
 	private readonly Timer m_timer;
 	private readonly List<Data> m_timeoutActions;
 	private uint m_counter;
diff --git a/src/MySqlConnector/Utilities/UnixDomainSocketEndPoint.cs b/src/MySqlConnector/Utilities/UnixDomainSocketEndPoint.cs
index 0e87bfa89..eb5f5fa09 100644
--- a/src/MySqlConnector/Utilities/UnixDomainSocketEndPoint.cs
+++ b/src/MySqlConnector/Utilities/UnixDomainSocketEndPoint.cs
@@ -45,9 +45,9 @@ internal sealed class UnixDomainSocketEndPoint : EndPoint
 	public UnixDomainSocketEndPoint(string filename)
 	{
 		if (filename is null)
-			throw new ArgumentNullException (nameof(filename));
+			throw new ArgumentNullException(nameof(filename));
 		if (filename.Length == 0)
-			throw new ArgumentException ("Cannot be empty.", nameof(filename));
+			throw new ArgumentException("Cannot be empty.", nameof(filename));
 		Filename = filename;
 	}
 
diff --git a/src/MySqlConnector/Utilities/Utility.cs b/src/MySqlConnector/Utilities/Utility.cs
index b15dd479b..a62931e5a 100644
--- a/src/MySqlConnector/Utilities/Utility.cs
+++ b/src/MySqlConnector/Utilities/Utility.cs
@@ -1,9 +1,14 @@
+#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
 using System.Buffers;
+#endif
 using System.Buffers.Text;
+using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
-using System.Globalization;
+#if NET462
 using System.Net;
 using System.Reflection;
+#endif
+using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
 using System.Security.Authentication;
 using System.Security.Cryptography;
@@ -92,6 +97,11 @@ public static void LoadRsaParameters(string key, RSA rsa)
 	public static RSAParameters GetRsaParameters(string key)
 #endif
 	{
+#if NET5_0_OR_GREATER
+		if (!PemEncoding.TryFind(key, out var pemFields))
+			throw new FormatException(string.Concat("Unrecognized PEM data: ", key.AsSpan(0, Math.Min(key.Length, 80))));
+		var isPrivate = key.AsSpan()[pemFields.Label].SequenceEqual("RSA PRIVATE KEY");
+#else
 		const string beginRsaPrivateKey = "-----BEGIN RSA PRIVATE KEY-----";
 		const string endRsaPrivateKey = "-----END RSA PRIVATE KEY-----";
 		const string beginPublicKey = "-----BEGIN PUBLIC KEY-----";
@@ -118,7 +128,7 @@ public static RSAParameters GetRsaParameters(string key)
 #if NETCOREAPP3_0_OR_GREATER
 			throw new FormatException(string.Concat("Unrecognized PEM header: ", key.AsSpan(0, Math.Min(key.Length, 80))));
 #else
-			throw new FormatException("Unrecognized PEM header: " + key.Substring(0, Math.Min(key.Length, 80)));
+			throw new FormatException("Unrecognized PEM header: " + key[..Math.Min(key.Length, 80)]);
 #endif
 		}
 
@@ -128,18 +138,21 @@ public static RSAParameters GetRsaParameters(string key)
 #if NETCOREAPP3_0_OR_GREATER
 			throw new FormatException(string.Concat("Missing expected '", pemFooter, "' PEM footer: ", key.AsSpan(Math.Max(key.Length - 80, 0))));
 #else
-			throw new FormatException($"Missing expected '{pemFooter}' PEM footer: " + key.Substring(Math.Max(key.Length - 80, 0)));
+			throw new FormatException($"Missing expected '{pemFooter}' PEM footer: " + key[Math.Max(key.Length - 80, 0)..]);
+#endif
 #endif
 
 #if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+#if NET5_0_OR_GREATER
+		var keyChars = key.AsSpan()[pemFields.Base64Data];
+#else
 		var keyChars = key.AsSpan()[keyStartIndex..keyEndIndex];
+#endif
 		var bufferLength = keyChars.Length / 4 * 3;
 		byte[]? buffer = null;
-		scoped Span<byte> bufferBytes;
-		if (bufferLength > 1024)
-			bufferBytes = buffer = ArrayPool<byte>.Shared.Rent(bufferLength);
-		else
-			 bufferBytes = stackalloc byte[bufferLength];
+		Span<byte> bufferBytes = bufferLength > 1024 ?
+			(Span<byte>) (buffer = ArrayPool<byte>.Shared.Rent(bufferLength)) :
+			stackalloc byte[bufferLength];
 		try
 		{
 			if (!System.Convert.TryFromBase64Chars(keyChars, bufferBytes, out var bytesWritten))
@@ -159,7 +172,7 @@ public static RSAParameters GetRsaParameters(string key)
 				ArrayPool<byte>.Shared.Return(buffer);
 		}
 #else
-		key = key.Substring(keyStartIndex, keyEndIndex - keyStartIndex);
+		key = key[keyStartIndex..keyEndIndex];
 		return GetRsaParameters(System.Convert.FromBase64String(key), isPrivate);
 #endif
 	}
@@ -171,57 +184,57 @@ private static RSAParameters GetRsaParameters(ReadOnlySpan<byte> data, bool isPr
 		// read header (30 81 xx, or 30 82 xx xx)
 		if (data[0] != 0x30)
 			throw new FormatException($"Expected 0x30 but read 0x{data[0]:X2}");
-		data = data.Slice(1);
+		data = data[1..];
 
 		if (!TryReadAsnLength(data, out var length, out var bytesConsumed))
 			throw new FormatException("Couldn't read key length");
-		data = data.Slice(bytesConsumed);
+		data = data[bytesConsumed..];
 
 		if (!isPrivate)
 		{
 			// encoded OID sequence for  PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1"
-			ReadOnlySpan<byte> rsaOid = new byte[] { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 };
-			if (!data.Slice(0, rsaOid.Length).SequenceEqual(rsaOid))
-				throw new FormatException($"Expected RSA OID but read {BitConverter.ToString(data.Slice(0, 15).ToArray())}");
-			data = data.Slice(rsaOid.Length);
+			ReadOnlySpan<byte> rsaOid = [0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00];
+			if (!data[..rsaOid.Length].SequenceEqual(rsaOid))
+				throw new FormatException($"Expected RSA OID but read {BitConverter.ToString(data[..15].ToArray())}");
+			data = data[rsaOid.Length..];
 
 			// BIT STRING (0x03) followed by length
 			if (data[0] != 0x03)
 				throw new FormatException($"Expected 0x03 but read 0x{data[0]:X2}");
-			data = data.Slice(1);
+			data = data[1..];
 
 			if (!TryReadAsnLength(data, out length, out bytesConsumed))
 				throw new FormatException("Couldn't read length");
-			data = data.Slice(bytesConsumed);
+			data = data[bytesConsumed..];
 
 			// skip NULL byte
 			if (data[0] != 0x00)
 				throw new FormatException($"Expected 0x00 but read 0x{data[0]:X2}");
-			data = data.Slice(1);
+			data = data[1..];
 
 			// skip next header (30 81 xx, or 30 82 xx xx)
 			if (data[0] != 0x30)
 				throw new FormatException($"Expected 0x30 but read 0x{data[0]:X2}");
-			data = data.Slice(1);
+			data = data[1..];
 
 			if (!TryReadAsnLength(data, out length, out bytesConsumed))
 				throw new FormatException("Couldn't read length");
-			data = data.Slice(bytesConsumed);
+			data = data[bytesConsumed..];
 		}
 		else
 		{
 			if (!TryReadAsnInteger(data, out var zero, out bytesConsumed) || zero.Length != 1 || zero[0] != 0)
 				throw new FormatException("Couldn't read zero.");
-			data = data.Slice(bytesConsumed);
+			data = data[bytesConsumed..];
 		}
 
 		if (!TryReadAsnInteger(data, out var modulus, out bytesConsumed))
 			throw new FormatException("Couldn't read modulus");
-		data = data.Slice(bytesConsumed);
+		data = data[bytesConsumed..];
 
 		if (!TryReadAsnInteger(data, out var exponent, out bytesConsumed))
 			throw new FormatException("Couldn't read exponent");
-		data = data.Slice(bytesConsumed);
+		data = data[bytesConsumed..];
 
 		if (!isPrivate)
 		{
@@ -234,27 +247,27 @@ private static RSAParameters GetRsaParameters(ReadOnlySpan<byte> data, bool isPr
 
 		if (!TryReadAsnInteger(data, out var d, out bytesConsumed))
 			throw new FormatException("Couldn't read D");
-		data = data.Slice(bytesConsumed);
+		data = data[bytesConsumed..];
 
 		if (!TryReadAsnInteger(data, out var p, out bytesConsumed))
 			throw new FormatException("Couldn't read P");
-		data = data.Slice(bytesConsumed);
+		data = data[bytesConsumed..];
 
 		if (!TryReadAsnInteger(data, out var q, out bytesConsumed))
 			throw new FormatException("Couldn't read Q");
-		data = data.Slice(bytesConsumed);
+		data = data[bytesConsumed..];
 
 		if (!TryReadAsnInteger(data, out var dp, out bytesConsumed))
 			throw new FormatException("Couldn't read DP");
-		data = data.Slice(bytesConsumed);
+		data = data[bytesConsumed..];
 
 		if (!TryReadAsnInteger(data, out var dq, out bytesConsumed))
 			throw new FormatException("Couldn't read DQ");
-		data = data.Slice(bytesConsumed);
+		data = data[bytesConsumed..];
 
 		if (!TryReadAsnInteger(data, out var iq, out bytesConsumed))
 			throw new FormatException("Couldn't read IQ");
-		data = data.Slice(bytesConsumed);
+		data = data[bytesConsumed..];
 
 		return new RSAParameters
 		{
@@ -278,7 +291,7 @@ private static RSAParameters GetRsaParameters(ReadOnlySpan<byte> data, bool isPr
 	/// <param name="index">The non-negative, zero-based starting index of the new slice (relative to <see cref="ArraySegment{T}.Offset"/> of <paramref name="arraySegment"/>.</param>
 	/// <returns>A new <see cref="ArraySegment{T}"/> starting at the <paramref name="index"/>th element of <paramref name="arraySegment"/> and continuing to the end of <paramref name="arraySegment"/>.</returns>
 	public static ArraySegment<T> Slice<T>(this ArraySegment<T> arraySegment, int index) =>
-		new ArraySegment<T>(arraySegment.Array!, arraySegment.Offset + index, arraySegment.Count - index);
+		new(arraySegment.Array!, arraySegment.Offset + index, arraySegment.Count - index);
 
 	/// <summary>
 	/// Returns a new <see cref="ArraySegment{T}"/> that starts at index <paramref name="index"/> into <paramref name="arraySegment"/> and has a length of <paramref name="length"/>.
@@ -288,7 +301,7 @@ public static ArraySegment<T> Slice<T>(this ArraySegment<T> arraySegment, int in
 	/// <param name="length">The non-negative length of the new slice.</param>
 	/// <returns>A new <see cref="ArraySegment{T}"/> of length <paramref name="length"/>, starting at the <paramref name="index"/>th element of <paramref name="arraySegment"/>.</returns>
 	public static ArraySegment<T> Slice<T>(this ArraySegment<T> arraySegment, int index, int length) =>
-		new ArraySegment<T>(arraySegment.Array!, arraySegment.Offset + index, length);
+		new(arraySegment.Array!, arraySegment.Offset + index, length);
 #endif
 
 #if !NET5_0_OR_GREATER
@@ -318,7 +331,7 @@ public static byte[] ArraySlice(byte[] input, int offset, int length)
 	/// <returns>The offset of <paramref name="pattern"/> within <paramref name="data"/>, or <c>-1</c> if <paramref name="pattern"/> was not found.</returns>
 	public static int FindNextIndex(ReadOnlySpan<byte> data, int offset, ReadOnlySpan<byte> pattern)
 	{
-		var index = MemoryExtensions.IndexOf(data.Slice(offset), pattern);
+		var index = MemoryExtensions.IndexOf(data[offset..], pattern);
 		return index == -1 ? -1 : offset + index;
 	}
 
@@ -333,68 +346,45 @@ public static void Resize<T>([NotNull] ref ResizableArray<T>? resizableArray, in
 		resizableArray.DoResize(newLength);
 	}
 
-	public static bool TryParseRedirectionHeader(string header, out string host, out int port, out string user)
+	public static bool TryParseRedirectionHeader(string redirectUrl, string initialUser, out string host, out int port, out string user)
 	{
 		host = "";
 		port = 0;
 		user = "";
 
-		if (!header.StartsWith("Location: mysql://", StringComparison.Ordinal) || header.Length < 22)
+		// "mariadb/mysql://[{user}[:{password}]@]{host}[:{port}]/[{db}[?{opt1}={value1}[&{opt2}={value2}]]]']"
+		if (!redirectUrl.StartsWith("mysql://", StringComparison.Ordinal) && !redirectUrl.StartsWith("mariadb://", StringComparison.Ordinal))
 			return false;
 
-		bool isCommunityFormat;
-		int portIndex;
-		if (header[18] == '[')
-		{
-			// Community protocol:
-			// Location: mysql://[redirectedHostName]:redirectedPort/?user=redirectedUser&ttl=%d\n
-			isCommunityFormat = true;
-
-			var hostIndex = 19;
-			var closeSquareBracketIndex = header.IndexOf(']', hostIndex);
-			if (closeSquareBracketIndex == -1)
-				return false;
-
-			host = header.Substring(hostIndex, closeSquareBracketIndex - hostIndex);
-			if (header.Length <= closeSquareBracketIndex + 2)
-				return false;
-			if (header[closeSquareBracketIndex + 1] != ':')
-				return false;
-			portIndex = closeSquareBracketIndex + 2;
-		}
-		else
+		try
 		{
-			// Azure protocol:
-			// Location: mysql://redirectedHostName:redirectedPort/user=redirectedUser&ttl=%d (where ttl is optional)
-			isCommunityFormat = false;
-
-			var hostIndex = 18;
-			var colonIndex = header.IndexOf(':', hostIndex);
-			if (colonIndex == -1)
-				return false;
+			var uri = new Uri(redirectUrl);
+			host = uri.Host;
+			if (string.IsNullOrEmpty(host)) return false;
+			if (host.StartsWith('[') && host.EndsWith("]", StringComparison.Ordinal)) host = host.Substring(1, host.Length - 2);
+
+			port = uri.Port;
+			user = Uri.UnescapeDataString(uri.UserInfo.Split(':')[0]);
+			if (string.IsNullOrEmpty(user) && !string.IsNullOrEmpty(uri.Query))
+			{
+				// query format "?{opt1}={value1}[&{opt2}={value2}]"
+				var q = uri.Query.Substring(1);
+				foreach (var token in q.Split('&'))
+				{
+					if (token.StartsWith("user=", StringComparison.Ordinal))
+					{
+						user = Uri.UnescapeDataString(token.Substring(5));
+					}
+				}
+			}
 
-			host = header.Substring(hostIndex, colonIndex - hostIndex);
-			portIndex = colonIndex + 1;
+			if (string.IsNullOrEmpty(user)) user = initialUser;
+			return true;
 		}
-
-		var userIndex = header.IndexOf(isCommunityFormat ? "/?user=" : "/user=", StringComparison.Ordinal);
-		if (userIndex == -1)
-			return false;
-
-#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER
-		if (!int.TryParse(header.AsSpan(portIndex, userIndex - portIndex), out port) || port <= 0)
-#else
-		if (!int.TryParse(header.Substring(portIndex, userIndex - portIndex), out port) || port <= 0)
-#endif
+		catch (UriFormatException)
+		{
 			return false;
-
-		userIndex += isCommunityFormat ? 7 : 6;
-		var ampersandIndex = header.IndexOf('&', userIndex);
-		var newlineIndex = header.IndexOf('\n', userIndex);
-		var terminatorIndex = ampersandIndex == -1 ? (newlineIndex == -1 ? header.Length : newlineIndex) :
-			(newlineIndex == -1 ? ampersandIndex : Math.Min(ampersandIndex, newlineIndex));
-		user = header.Substring(userIndex, terminatorIndex - userIndex);
-		return user.Length != 0;
+		}
 	}
 
 	public static TimeSpan ParseTimeSpan(ReadOnlySpan<byte> value)
@@ -403,7 +393,7 @@ public static TimeSpan ParseTimeSpan(ReadOnlySpan<byte> value)
 
 		// parse (optional) leading minus sign
 		var isNegative = false;
-		if (value is [ 0x2D, .. ])
+		if (value is [0x2D, ..])
 		{
 			isNegative = true;
 			value = value[1..];
@@ -414,7 +404,7 @@ public static TimeSpan ParseTimeSpan(ReadOnlySpan<byte> value)
 			goto InvalidTimeSpan;
 		if (value.Length == bytesConsumed || value[bytesConsumed] != 58)
 			goto InvalidTimeSpan;
-		value = value.Slice(bytesConsumed + 1);
+		value = value[(bytesConsumed + 1)..];
 
 		// parse minutes (0-59)
 		if (!Utf8Parser.TryParse(value, out int minutes, out bytesConsumed) || bytesConsumed != 2 || minutes < 0 || minutes > 59)
@@ -450,7 +440,9 @@ public static TimeSpan ParseTimeSpan(ReadOnlySpan<byte> value)
 			seconds = -seconds;
 			microseconds = -microseconds;
 		}
-#if NET7_0_OR_GREATER
+#if NET9_0_OR_GREATER
+		return TimeSpan.FromHours(hours, minutes, seconds, microseconds: microseconds);
+#elif NET7_0_OR_GREATER
 		return new TimeSpan(0, hours, minutes, seconds, microseconds / 1000, microseconds % 1000);
 #else
 		return new TimeSpan(0, hours, minutes, seconds, microseconds / 1000) + TimeSpan.FromTicks(microseconds % 1000 * 10);
@@ -473,36 +465,36 @@ public static bool TryComputeHash(this HashAlgorithm hashAlgorithm, ReadOnlySpan
 
 	public static byte[] TrimZeroByte(byte[] value)
 	{
-		if (value is [ .., 0 ])
+		if (value is [.., 0])
 			Array.Resize(ref value, value.Length - 1);
 		return value;
 	}
 
 	public static ReadOnlySpan<byte> TrimZeroByte(ReadOnlySpan<byte> value) =>
-		value is [ .., 0 ] ? value[..^1] : value;
+		value is [.., 0] ? value[..^1] : value;
 
 #if !NETCOREAPP2_1_OR_GREATER && !NETSTANDARD2_1_OR_GREATER
 	public static int Read(this Stream stream, Memory<byte> buffer)
 	{
-		MemoryMarshal.TryGetArray<byte>(buffer, out var arraySegment);
+		_ = MemoryMarshal.TryGetArray<byte>(buffer, out var arraySegment);
 		return stream.Read(arraySegment.Array, arraySegment.Offset, arraySegment.Count);
 	}
 
 	public static Task<int> ReadAsync(this Stream stream, Memory<byte> buffer)
 	{
-		MemoryMarshal.TryGetArray<byte>(buffer, out var arraySegment);
+		_ = MemoryMarshal.TryGetArray<byte>(buffer, out var arraySegment);
 		return stream.ReadAsync(arraySegment.Array, arraySegment.Offset, arraySegment.Count);
 	}
 
 	public static void Write(this Stream stream, ReadOnlyMemory<byte> data)
 	{
-		MemoryMarshal.TryGetArray(data, out var arraySegment);
+		_ = MemoryMarshal.TryGetArray(data, out var arraySegment);
 		stream.Write(arraySegment.Array, arraySegment.Offset, arraySegment.Count);
 	}
 
 	public static Task WriteAsync(this Stream stream, ReadOnlyMemory<byte> data)
 	{
-		MemoryMarshal.TryGetArray(data, out var arraySegment);
+		_ = MemoryMarshal.TryGetArray(data, out var arraySegment);
 		return stream.WriteAsync(arraySegment.Array, arraySegment.Offset, arraySegment.Count);
 	}
 #else
@@ -511,14 +503,21 @@ public static Task WriteAsync(this Stream stream, ReadOnlyMemory<byte> data)
 	public static void Write(this Stream stream, ReadOnlyMemory<byte> data) => stream.Write(data.Span);
 #endif
 
-	public static void SwapBytes(byte[] bytes, int offset1, int offset2)
+#if !NETCOREAPP2_0_OR_GREATER && !NETSTANDARD2_1_OR_GREATER
+	public static bool StartsWith(this string str, char value) => !string.IsNullOrEmpty(str) && str[0] == value;
+#endif
+
+	public static void SwapBytes(Span<byte> bytes, int offset1, int offset2)
 	{
-		byte swap = bytes[offset1];
-		bytes[offset1] = bytes[offset2];
-		bytes[offset2] = swap;
+#if NET8_0_OR_GREATER
+		ref var first = ref Unsafe.AsRef(ref bytes[0]);
+#else
+		ref var first = ref Unsafe.AsRef(bytes[0]);
+#endif
+		(Unsafe.Add(ref first, offset2), Unsafe.Add(ref first, offset1)) = (Unsafe.Add(ref first, offset1), Unsafe.Add(ref first, offset2));
 	}
 
-#if NET461
+#if NET462
 	public static bool IsWindows() => Environment.OSVersion.Platform == PlatformID.Win32NT;
 
 	public static void GetOSDetails(out string? os, out string osDescription, out string architecture)
@@ -557,13 +556,33 @@ public static void GetOSDetails(out string? os, out string osDescription, out st
 	}
 #endif
 
-#if NET461
+	/// <summary>
+	/// Gets the elapsed time (in milliseconds) since the specified <paramref name="startingTimestamp"/> (which must be a value returned from <see cref="Stopwatch.GetTimestamp"/>.
+	/// </summary>
+	public static int GetElapsedMilliseconds(long startingTimestamp) =>
+#if NET7_0_OR_GREATER
+		(int) Stopwatch.GetElapsedTime(startingTimestamp).TotalMilliseconds;
+#else
+		(int) ((Stopwatch.GetTimestamp() - startingTimestamp) * 1000L / Stopwatch.Frequency);
+#endif
+
+	/// <summary>
+	/// Gets the elapsed time (in seconds) between the specified <paramref name="startingTimestamp"/> and <paramref name="endingTimestamp"/>. (These must be values returned from <see cref="Stopwatch.GetTimestamp"/>.)
+	/// </summary>
+	public static double GetElapsedSeconds(long startingTimestamp, long endingTimestamp) =>
+#if NET7_0_OR_GREATER
+		Stopwatch.GetElapsedTime(startingTimestamp, endingTimestamp).TotalSeconds;
+#else
+		(endingTimestamp - startingTimestamp) / (double) Stopwatch.Frequency;
+#endif
+
+#if NET462
 	public static SslProtocols GetDefaultSslProtocols()
 	{
 		if (!s_defaultSslProtocols.HasValue)
 		{
 			// Prior to .NET Framework 4.7, SslProtocols.None is not a valid argument to SslStream.AuthenticateAsClientAsync.
-			// If the NET461 build is loaded by an application that targets .NET 4.7 (or later), or if app.config has set
+			// If the NET462 build is loaded by an application that targets .NET 4.7 (or later), or if app.config has set
 			// Switch.System.Net.DontEnableSystemDefaultTlsVersions to false, then SslProtocols.None will work; otherwise,
 			// if the application targets .NET 4.6.2 or earlier and hasn't changed the AppContext switch, then it will
 			// fail at runtime. We attempt to determine if it will fail by accessing the internal static
@@ -614,7 +633,7 @@ private static bool TryReadAsnLength(ReadOnlySpan<byte> data, out int length, ou
 
 		if (leadByte == 0x82)
 		{
-			length = data[1] * 256 + data[2];
+			length = (data[1] * 256) + data[2];
 			bytesConsumed = 3;
 			return true;
 		}
@@ -628,7 +647,7 @@ private static bool TryReadAsnLength(ReadOnlySpan<byte> data, out int length, ou
 	private static bool TryReadAsnInteger(ReadOnlySpan<byte> data, out ReadOnlySpan<byte> number, out int bytesConsumed)
 	{
 		// integer tag is 2
-		if (data is not [ 0x02, .. ])
+		if (data is not [0x02, ..])
 		{
 			number = default;
 			bytesConsumed = 0;
@@ -649,7 +668,7 @@ private static bool TryReadAsnInteger(ReadOnlySpan<byte> data, out ReadOnlySpan<
 		bytesConsumed = lengthBytesConsumed + length + 1;
 
 		// trim leading zero bytes
-		while (number is [ 0, _, .. ])
+		while (number is [0, _, ..])
 			number = number[1..];
 
 		return true;
diff --git a/src/MySqlConnector/Utilities/ValueTaskExtensions.cs b/src/MySqlConnector/Utilities/ValueTaskExtensions.cs
index 7baf4e620..77c10c876 100644
--- a/src/MySqlConnector/Utilities/ValueTaskExtensions.cs
+++ b/src/MySqlConnector/Utilities/ValueTaskExtensions.cs
@@ -1,8 +1,22 @@
+using System.Runtime.CompilerServices;
+
 namespace MySqlConnector.Utilities;
 
 internal static class ValueTaskExtensions
 {
-	public static ValueTask FromException(Exception exception) => new(Task.FromException(exception));
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	public static ValueTask FromException(Exception exception) =>
+#if NET5_0_OR_GREATER
+		ValueTask.FromException(exception);
+#else
+		new(Task.FromException(exception));
+#endif
 
-	public static ValueTask<T> FromException<T>(Exception exception) => new(Task.FromException<T>(exception));
+	[MethodImpl(MethodImplOptions.AggressiveInlining)]
+	public static ValueTask<T> FromException<T>(Exception exception) =>
+#if NET5_0_OR_GREATER
+		ValueTask.FromException<T>(exception);
+#else
+		new(Task.FromException<T>(exception));
+#endif
 }
diff --git a/src/MySqlConnector/docs/README.md b/src/MySqlConnector/docs/README.md
index 959bbcb90..855145b43 100644
--- a/src/MySqlConnector/docs/README.md
+++ b/src/MySqlConnector/docs/README.md
@@ -1,6 +1,6 @@
 ## About
 
-MySqlConnector is an [ADO.NET](https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/) data provider for [MySQL](https://www.mysql.com/), [MariaDB](https://mariadb.org/), [Amazon Aurora](https://aws.amazon.com/rds/aurora/), [Azure Database for MySQL](https://azure.microsoft.com/en-us/services/mysql/) and other MySQL-compatible databases.
+MySqlConnector is a C# [ADO.NET](https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/) driver for [MySQL](https://www.mysql.com/), [MariaDB](https://mariadb.org/), [Amazon Aurora](https://aws.amazon.com/rds/aurora/), [Azure Database for MySQL](https://azure.microsoft.com/en-us/services/mysql/) and other MySQL-compatible databases.
 
 More documentation is available at the [MySqlConnector website](https://mysqlconnector.net/).
 
@@ -35,6 +35,31 @@ while (reader.Read())
 }
 ```
 
+### ASP.NET
+
+For ASP.NET, use the [MySqlConnector.DependencyInjection package](https://www.nuget.org/packages/MySqlConnector.DependencyInjection/) to integrate with dependency injection and logging.
+
+```csharp
+var builder = WebApplication.CreateBuilder(args);
+
+// use AddMySqlDataSource to configure MySqlConnector
+builder.Services.AddMySqlDataSource(builder.Configuration.GetConnectionString("Default"));
+
+var app = builder.Build();
+
+// use dependency injection to get a MySqlConnection in minimal APIs or in controllers
+app.MapGet("/", async (MySqlConnection connection) =>
+{
+    // open and use the connection here
+    await connection.OpenAsync();
+    await using var command = connection.CreateCommand();
+    command.CommandText = "SELECT name FROM users LIMIT 1";
+    return "Hello World: " + await command.ExecuteScalarAsync();
+});
+
+app.Run();
+```
+
 ## Key Features
 
 * Full support for async I/O
@@ -58,8 +83,8 @@ The main types provided by this library are:
 
 ## Related Packages
 
+* Dependency Injection: [MySqlConnector.DependencyInjection](https://www.nuget.org/packages/MySqlConnector.DependencyInjection/)
 * Entity Framework Core: [Pomelo.EntityFrameworkCore.MySql](https://www.nuget.org/packages/Pomelo.EntityFrameworkCore.MySql/)
-* Logging: [log4net](https://www.nuget.org/packages/MySqlConnector.Logging.log4net/), [Microsoft.Extensions.Logging](https://www.nuget.org/packages/MySqlConnector.Logging.Microsoft.Extensions.Logging/), [NLog](https://www.nuget.org/packages/MySqlConnector.Logging.NLog/), [Serilog](https://www.nuget.org/packages/MySqlConnector.Logging.Serilog/)
 
 ## Feedback
 
diff --git a/tests/Benchmark/Benchmark.csproj b/tests/Benchmark/Benchmark.csproj
index 3ddca3f04..0fc9f463a 100644
--- a/tests/Benchmark/Benchmark.csproj
+++ b/tests/Benchmark/Benchmark.csproj
@@ -1,12 +1,18 @@
 <Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFramework>net7.0</TargetFramework>
-    <LangVersion>10.0</LangVersion>
+    <TargetFramework>net8.0</TargetFramework>
+    <LangVersion>12.0</LangVersion>
+    <ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
   </PropertyGroup>
   <ItemGroup>
-    <PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
-    <PackageReference Include="MySqlConnector" Version="2.2.0-rc.1" />
-    <PackageReference Include="MySql.Data" Version="8.0.31" />
+    <PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
+    <PackageReference Include="MySqlConnector" Version="2.4.0" />
+    <PackageReference Include="MySql.Data" Version="9.4.0" />
   </ItemGroup>
+
+<!--  <ItemGroup>-->
+<!--    <ProjectReference Include="..\..\src\MySqlConnector\MySqlConnector.csproj"/>-->
+<!--    <Using Include="MySqlConnector.Core"/>-->
+<!--  </ItemGroup>-->
 </Project>
diff --git a/tests/Benchmark/Benchmark.sln b/tests/Benchmark/Benchmark.sln
deleted file mode 100644
index 882832779..000000000
--- a/tests/Benchmark/Benchmark.sln
+++ /dev/null
@@ -1,28 +0,0 @@
-﻿
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.26403.7
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Benchmark", "Benchmark.csproj", "{102C06D1-ADB7-4661-9EC5-F6460BFEDB2A}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MySqlConnector", "..\..\src\MySqlConnector\MySqlConnector.csproj", "{6B4FEA15-4504-4BF7-B157-CEF9B60CFE13}"
-EndProject
-Global
-	GlobalSection(SolutionConfigurationPlatforms) = preSolution
-		Debug|Any CPU = Debug|Any CPU
-		Release|Any CPU = Release|Any CPU
-	EndGlobalSection
-	GlobalSection(ProjectConfigurationPlatforms) = postSolution
-		{102C06D1-ADB7-4661-9EC5-F6460BFEDB2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{102C06D1-ADB7-4661-9EC5-F6460BFEDB2A}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{102C06D1-ADB7-4661-9EC5-F6460BFEDB2A}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{102C06D1-ADB7-4661-9EC5-F6460BFEDB2A}.Release|Any CPU.Build.0 = Release|Any CPU
-		{6B4FEA15-4504-4BF7-B157-CEF9B60CFE13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{6B4FEA15-4504-4BF7-B157-CEF9B60CFE13}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{6B4FEA15-4504-4BF7-B157-CEF9B60CFE13}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{6B4FEA15-4504-4BF7-B157-CEF9B60CFE13}.Release|Any CPU.Build.0 = Release|Any CPU
-	EndGlobalSection
-	GlobalSection(SolutionProperties) = preSolution
-		HideSolutionNode = FALSE
-	EndGlobalSection
-EndGlobal
diff --git a/tests/Benchmark/Benchmark.slnx b/tests/Benchmark/Benchmark.slnx
new file mode 100644
index 000000000..e8c8f4395
--- /dev/null
+++ b/tests/Benchmark/Benchmark.slnx
@@ -0,0 +1,4 @@
+<Solution>
+  <Project Path="../../src/MySqlConnector/MySqlConnector.csproj" />
+  <Project Path="Benchmark.csproj" />
+</Solution>
diff --git a/tests/Benchmark/Program.cs b/tests/Benchmark/Program.cs
index 47760b498..1b7863028 100644
--- a/tests/Benchmark/Program.cs
+++ b/tests/Benchmark/Program.cs
@@ -23,7 +23,7 @@ static void Main()
 			.AddValidator(JitOptimizationsValidator.FailOnError)
 			.AddDiagnoser(MemoryDiagnoser.Default)
 			.AddColumn(StatisticColumn.AllStatistics)
-			.AddJob(Job.Default.WithRuntime(CoreRuntime.Core70))
+			.AddJob(Job.Default.WithRuntime(CoreRuntime.Core80))
 			.AddExporter(DefaultExporters.Csv);
 
 		var summary = BenchmarkRunner.Run<MySqlClient>(customConfig);
diff --git a/tests/Conformance.Tests/Conformance.Tests.csproj b/tests/Conformance.Tests/Conformance.Tests.csproj
index 7622a4ea1..e2a4ca928 100644
--- a/tests/Conformance.Tests/Conformance.Tests.csproj
+++ b/tests/Conformance.Tests/Conformance.Tests.csproj
@@ -1,20 +1,23 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
  <PropertyGroup>
-    <TargetFrameworks>net7.0</TargetFrameworks>
+    <TargetFrameworks>net9.0</TargetFrameworks>
     <VersionPrefix>0.1.0</VersionPrefix>
     <SignAssembly>true</SignAssembly>
     <PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
     <AssemblyOriginatorKeyFile>..\..\MySqlConnector.snk</AssemblyOriginatorKeyFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
-   <LangVersion>preview</LangVersion>
+   <LangVersion>11.0</LangVersion>
   </PropertyGroup>
 
  <ItemGroup>
-    <PackageReference Include="AdoNet.Specification.Tests" Version="2.0.0-beta.1" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
-    <PackageReference Include="xunit" Version="2.4.2" />
-    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
+    <PackageReference Include="AdoNet.Specification.Tests" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" />
+    <PackageReference Include="xunit" />
+    <PackageReference Include="xunit.runner.visualstudio">
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      <PrivateAssets>all</PrivateAssets>
+    </PackageReference>
     <ProjectReference Include="..\..\src\MySqlConnector\MySqlConnector.csproj" />
   </ItemGroup>
 
diff --git a/tests/Conformance.Tests/ConnectionStringBuilderTests.cs b/tests/Conformance.Tests/ConnectionStringBuilderTests.cs
index 51b026177..5aaf6ef2a 100644
--- a/tests/Conformance.Tests/ConnectionStringBuilderTests.cs
+++ b/tests/Conformance.Tests/ConnectionStringBuilderTests.cs
@@ -9,4 +9,3 @@ public ConnectionStringBuilderTests(DbFactoryFixture fixture)
 	{
 	}
 }
-
diff --git a/tests/Conformance.Tests/DataReaderTests.cs b/tests/Conformance.Tests/DataReaderTests.cs
index 41bcbdad3..4618d5f35 100644
--- a/tests/Conformance.Tests/DataReaderTests.cs
+++ b/tests/Conformance.Tests/DataReaderTests.cs
@@ -11,5 +11,7 @@ public DataReaderTests(SelectValueFixture fixture)
 	}
 
 	[Fact(Skip = "Deliberately throws InvalidCastException")]
-	public override void GetTextReader_returns_empty_for_null_String() { }
+	public override void GetTextReader_returns_empty_for_null_String()
+	{
+	}
 }
diff --git a/tests/Conformance.Tests/DbFactoryFixture.cs b/tests/Conformance.Tests/DbFactoryFixture.cs
index 358226069..4ecaaa8af 100644
--- a/tests/Conformance.Tests/DbFactoryFixture.cs
+++ b/tests/Conformance.Tests/DbFactoryFixture.cs
@@ -6,12 +6,12 @@
 namespace Conformance.Tests;
 
 public class DbFactoryFixture : IDbFactoryFixture
+{
+	public DbFactoryFixture()
 	{
-		public DbFactoryFixture()
-		{
-			ConnectionString = Environment.GetEnvironmentVariable("CONNECTION_STRING") ?? "Server=localhost;User Id=mysqltest;Password=test;SSL Mode=None";
-		}
-
-		public string ConnectionString { get; }
-		public DbProviderFactory Factory => MySqlConnectorFactory.Instance;
+		ConnectionString = Environment.GetEnvironmentVariable("CONNECTION_STRING") ?? "Server=localhost;User Id=mysqltest;Password=test;SSL Mode=None;AllowPublicKeyRetrieval=true";
 	}
+
+	public string ConnectionString { get; }
+	public DbProviderFactory Factory => MySqlConnectorFactory.Instance;
+}
diff --git a/tests/Conformance.Tests/GetValueConversionTests.cs b/tests/Conformance.Tests/GetValueConversionTests.cs
index c2fbbbedd..6cbe2b90f 100644
--- a/tests/Conformance.Tests/GetValueConversionTests.cs
+++ b/tests/Conformance.Tests/GetValueConversionTests.cs
@@ -270,7 +270,6 @@ public GetValueConversionTests(SelectValueFixture fixture)
 	public override void GetByte_throws_for_zero_Decimal_with_GetFieldValue() => TestGetValue(DbType.Decimal, ValueKind.Zero, x => x.GetFieldValue<byte>(0), (byte) 0);
 	public override Task GetByte_throws_for_zero_Decimal_with_GetFieldValueAsync() => TestGetValueAsync(DbType.Decimal, ValueKind.Zero, x => x.GetFieldValueAsync<byte>(0), (byte) 0);
 
-
 	// the minimum date permitted by MySQL is 1000-01-01; override the minimum value for DateTime tests
 	public override void GetDateTime_for_minimum_Date() => TestGetValue(DbType.Date, ValueKind.Minimum, x => x.GetDateTime(0), new DateTime(1000, 1, 1));
 	public override void GetDateTime_for_minimum_Date_with_GetFieldValue() => TestGetValue(DbType.Date, ValueKind.Minimum, x => x.GetFieldValue<DateTime>(0), new DateTime(1000, 1, 1));
diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props
new file mode 100644
index 000000000..78b9e666c
--- /dev/null
+++ b/tests/Directory.Build.props
@@ -0,0 +1,8 @@
+<Project>
+  <Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />
+
+  <PropertyGroup>
+    <NoWarn>$(NoWarn);SA0001</NoWarn>
+  </PropertyGroup>
+
+</Project>
diff --git a/tests/IntegrationTests/ActivityTests.cs b/tests/IntegrationTests/ActivityTests.cs
index 65d8e22fc..e486494bf 100644
--- a/tests/IntegrationTests/ActivityTests.cs
+++ b/tests/IntegrationTests/ActivityTests.cs
@@ -38,12 +38,9 @@ public void OpenTags()
 		Assert.NotNull(activity);
 		Assert.Equal(ActivityKind.Client, activity.Kind);
 		Assert.Equal("Open", activity.OperationName);
-#if NET6_0_OR_GREATER
-		Assert.Equal(ActivityStatusCode.Ok, activity.Status);
-#endif
+		Assert.Equal(ActivityStatusCode.Unset, activity.Status);
 
 		AssertTags(activity.Tags, csb);
-		AssertTag(activity.Tags, "otel.status_code", "OK");
 	}
 
 	[Fact]
@@ -74,7 +71,6 @@ public void OpenFromPoolTags()
 				connection.Open();
 
 			Assert.NotNull(activities[i]);
-			AssertTag(activities[i].Tags, "otel.status_code", "OK");
 		}
 
 		// activities should have the same connection ID
@@ -104,12 +100,9 @@ public void OpenFailedTags()
 		Assert.NotNull(activity);
 		Assert.Equal(ActivityKind.Client, activity.Kind);
 		Assert.Equal("Open", activity.OperationName);
-#if NET6_0_OR_GREATER
 		Assert.Equal(ActivityStatusCode.Error, activity.Status);
-#endif
 
 		AssertTags(activity.Tags, csb);
-		AssertTag(activity.Tags, "otel.status_code", "ERROR");
 	}
 
 	[Fact]
@@ -140,14 +133,53 @@ public void SelectTags()
 		Assert.NotNull(activity);
 		Assert.Equal(ActivityKind.Client, activity.Kind);
 		Assert.Equal("Execute", activity.OperationName);
-#if NET6_0_OR_GREATER
-		Assert.Equal(ActivityStatusCode.Ok, activity.Status);
-#endif
+		Assert.Equal(ActivityStatusCode.Unset, activity.Status);
 
 		AssertTags(activity.Tags, csb);
 		AssertTag(activity.Tags, "db.connection_id", connection.ServerThread.ToString(CultureInfo.InvariantCulture));
 		AssertTag(activity.Tags, "db.statement", "SELECT 1;");
-		AssertTag(activity.Tags, "otel.status_code", "OK");
+	}
+
+	[Theory]
+	[InlineData(true)]
+	[InlineData(false)]
+	public void ReadResultSetHeaderEvent(bool enableEvent)
+	{
+		var dataSourceBuilder = new MySqlDataSourceBuilder(AppConfig.ConnectionString)
+			.ConfigureTracing(o => o.EnableResultSetHeaderEvent(enableEvent));
+		using var dataSource = dataSourceBuilder.Build();
+		using var connection = dataSource.OpenConnection();
+
+		using var parentActivity = new Activity(nameof(ReadResultSetHeaderEvent));
+		parentActivity.Start();
+
+		Activity activity = null;
+		using var listener = new ActivityListener
+		{
+			ShouldListenTo = x => x.Name == "MySqlConnector",
+			Sample = (ref ActivityCreationOptions<ActivityContext> options) =>
+				options.TraceId == parentActivity.TraceId ? ActivitySamplingResult.AllData : ActivitySamplingResult.None,
+			ActivityStopped = x => activity = x,
+		};
+		ActivitySource.AddActivityListener(listener);
+
+		using (var command = new MySqlCommand("SELECT 1;", connection))
+		{
+			command.ExecuteScalar();
+		}
+
+		Assert.NotNull(activity);
+		Assert.Equal(ActivityKind.Client, activity.Kind);
+		Assert.Equal("Execute", activity.OperationName);
+		if (enableEvent)
+		{
+			var activityEvent = Assert.Single(activity.Events);
+			Assert.Equal("read-result-set-header", activityEvent.Name);
+		}
+		else
+		{
+			Assert.Empty(activity.Events);
+		}
 	}
 
 	private void AssertTags(IEnumerable<KeyValuePair<string, string>> tags, MySqlConnectionStringBuilder csb)
@@ -165,7 +197,7 @@ private void AssertTag(IEnumerable<KeyValuePair<string, string>> tags, string ex
 	{
 		var tag = tags.SingleOrDefault(x => x.Key == expectedTag);
 		if (tag.Key is null)
-			Assert.True(false, $"tags did not contain '{expectedTag}'");
+			Assert.Fail($"tags did not contain '{expectedTag}'");
 		Assert.Equal(expectedValue, tag.Value);
 	}
 }
diff --git a/tests/IntegrationTests/AppConfig.cs b/tests/IntegrationTests/AppConfig.cs
index 995cbc497..5ffa32192 100644
--- a/tests/IntegrationTests/AppConfig.cs
+++ b/tests/IntegrationTests/AppConfig.cs
@@ -1,6 +1,9 @@
-using System.Reflection;
 using Microsoft.Extensions.Configuration;
 
+#if MYSQL_DATA
+[assembly: CollectionBehavior(DisableTestParallelization = true)]
+#endif
+
 namespace IntegrationTests;
 
 public static class AppConfig
@@ -49,7 +52,7 @@ private static IConfiguration BuildConfiguration()
 	public static string MySqlBulkLoaderTsvFile => Config.GetValue<string>("Data:MySqlBulkLoaderTsvFile");
 	public static string MySqlBulkLoaderLocalTsvFile => Config.GetValue<string>("Data:MySqlBulkLoaderLocalTsvFile");
 
-	public static MySqlConnectionStringBuilder CreateConnectionStringBuilder() => new MySqlConnectionStringBuilder(ConnectionString);
+	public static MySqlConnectionStringBuilder CreateConnectionStringBuilder() => new(ConnectionString);
 
 	public static MySqlConnectionStringBuilder CreateSha256ConnectionStringBuilder()
 	{
diff --git a/tests/IntegrationTests/BatchTests.cs b/tests/IntegrationTests/BatchTests.cs
index f62af6e49..3a62700ee 100644
--- a/tests/IntegrationTests/BatchTests.cs
+++ b/tests/IntegrationTests/BatchTests.cs
@@ -8,6 +8,18 @@ public BatchTests(DatabaseFixture database)
 	{
 	}
 
+	[Fact]
+	public void CanCreateParameter()
+	{
+		Assert.True(new MySqlBatchCommand().CanCreateParameter);
+	}
+
+	[Fact]
+	public void CreateParameter()
+	{
+		Assert.IsType<MySqlParameter>(new MySqlBatchCommand().CreateParameter());
+	}
+
 	[Fact]
 	public void NeedsConnection()
 	{
@@ -180,7 +192,7 @@ public void ExecuteBatch(string suffix)
 		{
 			BatchCommands =
 			{
-				new MySqlBatchCommand("SELECT 1" + suffix),
+				new MySqlBatchCommand("SELECT 10; SELECT 1" + suffix),
 				new MySqlBatchCommand("SELECT 2" + suffix),
 				new MySqlBatchCommand("SELECT 3" + suffix),
 			},
@@ -201,32 +213,14 @@ public void ExecuteBatch(string suffix)
 		Assert.True(reader.Read());
 		total += reader.GetInt32(0);
 		Assert.False(reader.Read());
-		Assert.False(reader.NextResult());
-
-		Assert.Equal(6, total);
-	}
+		Assert.True(reader.NextResult());
 
-	[Fact(Skip = "COM_MULTI")]
-	public void ExecuteInvalidSqlBatch()
-	{
-		using var connection = new MySqlConnection(AppConfig.ConnectionString);
-		connection.Open();
-		using var batch = new MySqlBatch(connection)
-		{
-			BatchCommands =
-			{
-				new MySqlBatchCommand("SELECT 1;"),
-				new MySqlBatchCommand("SELECT 2 /* incomplete"),
-				new MySqlBatchCommand("SELECT 3;"),
-			},
-		};
-		using var reader = batch.ExecuteReader();
 		Assert.True(reader.Read());
-		Assert.Equal(1, reader.GetInt32(0));
+		total += reader.GetInt32(0);
 		Assert.False(reader.Read());
+		Assert.False(reader.NextResult());
 
-		var ex = Assert.Throws<MySqlException>(() => reader.NextResult());
-		Assert.Equal(MySqlErrorCode.ParseError, ex.ErrorCode);
+		Assert.Equal(16, total);
 	}
 
 	[Theory]
@@ -247,7 +241,7 @@ public void SingleRow(bool prepare)
 		{
 			BatchCommands =
 			{
-				new MySqlBatchCommand("SELECT id FROM batch_single_row ORDER BY id"),
+				new MySqlBatchCommand("SELECT id FROM batch_single_row ORDER BY id; SELECT 10"),
 				new MySqlBatchCommand("SELECT id FROM batch_single_row ORDER BY id"),
 			},
 		};
@@ -255,12 +249,35 @@ public void SingleRow(bool prepare)
 		if (prepare)
 			batch.Prepare();
 
-		using var reader = batch.ExecuteReader(CommandBehavior.SingleRow);
-		Assert.True(reader.Read());
-		Assert.Equal(1, reader.GetInt32(0));
-		Assert.False(reader.Read());
+		using (var reader = batch.ExecuteReader(CommandBehavior.SingleRow))
+		{
+			Assert.True(reader.Read());
+			Assert.Equal(1, reader.GetInt32(0));
+			Assert.False(reader.Read());
+			Assert.False(reader.NextResult());
+		}
 
-		Assert.False(reader.NextResult());
+		using (var reader = batch.ExecuteReader())
+		{
+			Assert.True(reader.Read());
+			Assert.Equal(1, reader.GetInt32(0));
+			Assert.True(reader.Read());
+			Assert.Equal(2, reader.GetInt32(0));
+			Assert.True(reader.Read());
+			Assert.Equal(3, reader.GetInt32(0));
+			Assert.False(reader.Read());
+			Assert.True(reader.NextResult());
+			Assert.True(reader.Read());
+			Assert.Equal(10, reader.GetInt32(0));
+			Assert.True(reader.NextResult());
+			Assert.True(reader.Read());
+			Assert.Equal(1, reader.GetInt32(0));
+			Assert.True(reader.Read());
+			Assert.Equal(2, reader.GetInt32(0));
+			Assert.True(reader.Read());
+			Assert.Equal(3, reader.GetInt32(0));
+			Assert.False(reader.Read());
+		}
 	}
 
 	[Fact]
@@ -326,7 +343,7 @@ public void PrepareNeedsCommandsWithText()
 	private static string GetIgnoreCommandTransactionConnectionString() =>
 		new MySqlConnectionStringBuilder(AppConfig.ConnectionString)
 		{
-			IgnoreCommandTransaction = true
+			IgnoreCommandTransaction = true,
 		}.ConnectionString;
 }
 #endif
diff --git a/tests/IntegrationTests/BulkLoaderAsync.cs b/tests/IntegrationTests/BulkLoaderAsync.cs
index 104e1f3df..ddd11c4a4 100644
--- a/tests/IntegrationTests/BulkLoaderAsync.cs
+++ b/tests/IntegrationTests/BulkLoaderAsync.cs
@@ -34,9 +34,11 @@ one int primary key
 	public async Task BulkLoadTsvFile()
 	{
 		using var connection = new MySqlConnection(GetConnectionString());
-		MySqlBulkLoader bl = new MySqlBulkLoader(connection);
-		bl.FileName = AppConfig.MySqlBulkLoaderTsvFile;
-		bl.TableName = m_testTable;
+		MySqlBulkLoader bl = new MySqlBulkLoader(connection)
+		{
+			FileName = AppConfig.MySqlBulkLoaderTsvFile,
+			TableName = m_testTable,
+		};
 		bl.Columns.AddRange(new string[] { "one", "two", "three", "four", "five" });
 		bl.NumberOfLinesToSkip = 1;
 		bl.Expressions.Add("five = UNHEX(five)");
@@ -49,9 +51,11 @@ public async Task BulkLoadTsvFile()
 	public async Task BulkLoadLocalTsvFile()
 	{
 		using var connection = new MySqlConnection(GetLocalConnectionString());
-		MySqlBulkLoader bl = new MySqlBulkLoader(connection);
-		bl.FileName = AppConfig.MySqlBulkLoaderLocalTsvFile;
-		bl.TableName = m_testTable;
+		MySqlBulkLoader bl = new MySqlBulkLoader(connection)
+		{
+			FileName = AppConfig.MySqlBulkLoaderLocalTsvFile,
+			TableName = m_testTable,
+		};
 		bl.Columns.AddRange(new string[] { "one", "two", "three", "four", "five" });
 		bl.NumberOfLinesToSkip = 1;
 		bl.Expressions.Add("five = UNHEX(five)");
@@ -64,9 +68,11 @@ public async Task BulkLoadLocalTsvFile()
 	public async Task BulkLoadCsvFile()
 	{
 		using var connection = new MySqlConnection(GetConnectionString());
-		MySqlBulkLoader bl = new MySqlBulkLoader(connection);
-		bl.FileName = AppConfig.MySqlBulkLoaderCsvFile;
-		bl.TableName = m_testTable;
+		MySqlBulkLoader bl = new MySqlBulkLoader(connection)
+		{
+			FileName = AppConfig.MySqlBulkLoaderCsvFile,
+			TableName = m_testTable,
+		};
 		bl.Columns.AddRange(new string[] { "one", "two", "three", "four", "five" });
 		bl.NumberOfLinesToSkip = 1;
 		bl.FieldTerminator = ",";
@@ -83,10 +89,12 @@ public async Task BulkLoadLocalCsvFile()
 	{
 		using var connection = new MySqlConnection(GetLocalConnectionString());
 		await connection.OpenAsync();
-		MySqlBulkLoader bl = new MySqlBulkLoader(connection);
-		bl.FileName = AppConfig.MySqlBulkLoaderLocalCsvFile;
-		bl.TableName = m_testTable;
-		bl.CharacterSet = "UTF8";
+		MySqlBulkLoader bl = new MySqlBulkLoader(connection)
+		{
+			FileName = AppConfig.MySqlBulkLoaderLocalCsvFile,
+			TableName = m_testTable,
+			CharacterSet = "UTF8",
+		};
 		bl.Columns.AddRange(new string[] { "one", "two", "three", "four", "five" });
 		bl.NumberOfLinesToSkip = 1;
 		bl.FieldTerminator = ",";
@@ -108,10 +116,12 @@ public async Task BulkLoadCsvFileNotFound()
 		if (string.IsNullOrEmpty(secureFilePath) || secureFilePath == "NULL")
 			return;
 
-		MySqlBulkLoader bl = new MySqlBulkLoader(connection);
-		bl.FileName = Path.Combine(secureFilePath, AppConfig.MySqlBulkLoaderCsvFile + "-junk");
-		bl.TableName = m_testTable;
-		bl.CharacterSet = "UTF8";
+		MySqlBulkLoader bl = new MySqlBulkLoader(connection)
+		{
+			FileName = Path.Combine(secureFilePath, AppConfig.MySqlBulkLoaderCsvFile + "-junk"),
+			TableName = m_testTable,
+			CharacterSet = "UTF8",
+		};
 		bl.Columns.AddRange(new string[] { "one", "two", "three", "four", "five" });
 		bl.NumberOfLinesToSkip = 1;
 		bl.FieldTerminator = ",";
@@ -148,11 +158,13 @@ public async Task BulkLoadLocalCsvFileNotFound()
 	{
 		using var connection = new MySqlConnection(GetLocalConnectionString());
 		await connection.OpenAsync();
-		MySqlBulkLoader bl = new MySqlBulkLoader(connection);
-		bl.Timeout = 3; //Set a short timeout for this test because the file not found exception takes a long time otherwise, the timeout does not change the result
-		bl.FileName = AppConfig.MySqlBulkLoaderLocalCsvFile + "-junk";
-		bl.TableName = m_testTable;
-		bl.CharacterSet = "UTF8";
+		MySqlBulkLoader bl = new MySqlBulkLoader(connection)
+		{
+			Timeout = 3, // Set a short timeout for this test because the file not found exception takes a long time otherwise, the timeout does not change the result
+			FileName = AppConfig.MySqlBulkLoaderLocalCsvFile + "-junk",
+			TableName = m_testTable,
+			CharacterSet = "UTF8",
+		};
 		bl.Columns.AddRange(new string[] { "one", "two", "three", "four", "five" });
 		bl.NumberOfLinesToSkip = 1;
 		bl.FieldTerminator = ",";
@@ -185,7 +197,7 @@ public async Task BulkLoadLocalCsvFileNotFound()
 		}
 		catch (Exception exception)
 		{
-			//We know that the exception is not a MySqlException, just use the assertion to fail the test
+			// We know that the exception is not a MySqlException, just use the assertion to fail the test
 			Assert.IsType<MySqlException>(exception);
 		}
 	}
@@ -255,8 +267,10 @@ public async Task BulkLoadMissingFileName()
 	{
 		using var connection = new MySqlConnection(GetConnectionString());
 		await connection.OpenAsync();
-		MySqlBulkLoader bl = new MySqlBulkLoader(connection);
-		bl.TableName = m_testTable;
+		MySqlBulkLoader bl = new MySqlBulkLoader(connection)
+		{
+			TableName = m_testTable,
+		};
 		bl.Columns.AddRange(new string[] { "one", "two", "three", "four", "five" });
 		bl.NumberOfLinesToSkip = 1;
 		bl.FieldTerminator = ",";
@@ -282,8 +296,10 @@ public async Task BulkLoadMissingTableName()
 	{
 		using var connection = new MySqlConnection(GetConnectionString());
 		await connection.OpenAsync();
-		MySqlBulkLoader bl = new MySqlBulkLoader(connection);
-		bl.FileName = AppConfig.MySqlBulkLoaderLocalCsvFile;
+		MySqlBulkLoader bl = new MySqlBulkLoader(connection)
+		{
+			FileName = AppConfig.MySqlBulkLoaderLocalCsvFile,
+		};
 		bl.Columns.AddRange(new string[] { "one", "two", "three", "four", "five" });
 		bl.NumberOfLinesToSkip = 1;
 		bl.FieldTerminator = ",";
@@ -304,14 +320,15 @@ await Assert.ThrowsAsync<MySqlException>(async () =>
 #endif
 	}
 
-#if !MYSQL_DATA
 	[SkippableFact(ConfigSettings.LocalCsvFile)]
 	public async Task BulkLoadFileStreamInvalidOperation()
 	{
 		using var connection = new MySqlConnection(GetConnectionString());
 		var bl = new MySqlBulkLoader(connection);
 		using var fileStream = new FileStream(AppConfig.MySqlBulkLoaderLocalCsvFile, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true);
+#if !MYSQL_DATA
 		bl.SourceStream = fileStream;
+#endif
 		bl.TableName = m_testTable;
 		bl.Columns.AddRange(new string[] { "one", "two", "three", "four", "five" });
 		bl.NumberOfLinesToSkip = 1;
@@ -320,10 +337,11 @@ public async Task BulkLoadFileStreamInvalidOperation()
 		bl.FieldQuotationOptional = true;
 		bl.Expressions.Add("five = UNHEX(five)");
 		bl.Local = false;
-		await Assert.ThrowsAsync<System.InvalidOperationException>(async () =>
-		{
-			int rowCount = await bl.LoadAsync();
-		});
+#if !MYSQL_DATA
+		await Assert.ThrowsAsync<InvalidOperationException>(async () => { var rowCount = await bl.LoadAsync(); });
+#else
+		await Assert.ThrowsAsync<MySqlException>(async () => { var rowCount = await bl.LoadAsync(fileStream); });
+#endif
 	}
 
 	[SkippableFact(ConfigSettings.LocalCsvFile)]
@@ -333,7 +351,9 @@ public async Task BulkLoadLocalFileStream()
 		await connection.OpenAsync();
 		MySqlBulkLoader bl = new MySqlBulkLoader(connection);
 		using var fileStream = new FileStream(AppConfig.MySqlBulkLoaderLocalCsvFile, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true);
+#if !MYSQL_DATA
 		bl.SourceStream = fileStream;
+#endif
 		bl.TableName = m_testTable;
 		bl.Columns.AddRange(new string[] { "one", "two", "three", "four", "five" });
 		bl.NumberOfLinesToSkip = 1;
@@ -342,7 +362,11 @@ public async Task BulkLoadLocalFileStream()
 		bl.FieldQuotationOptional = true;
 		bl.Expressions.Add("five = UNHEX(five)");
 		bl.Local = true;
+#if !MYSQL_DATA
 		int rowCount = await bl.LoadAsync();
+#else
+		int rowCount = await bl.LoadAsync(fileStream);
+#endif
 		Assert.Equal(20, rowCount);
 	}
 
@@ -353,7 +377,9 @@ public async Task BulkLoadMemoryStreamInvalidOperation()
 		await connection.OpenAsync();
 		MySqlBulkLoader bl = new MySqlBulkLoader(connection);
 		using var memoryStream = new MemoryStream(m_memoryStreamBytes, false);
+#if !MYSQL_DATA
 		bl.SourceStream = memoryStream;
+#endif
 		bl.TableName = m_testTable;
 		bl.Columns.AddRange(new string[] { "one", "two", "three" });
 		bl.NumberOfLinesToSkip = 0;
@@ -361,10 +387,11 @@ public async Task BulkLoadMemoryStreamInvalidOperation()
 		bl.FieldQuotationCharacter = '"';
 		bl.FieldQuotationOptional = true;
 		bl.Local = false;
-		await Assert.ThrowsAsync<System.InvalidOperationException>(async () =>
-		{
-			int rowCount = await bl.LoadAsync();
-		});
+#if !MYSQL_DATA
+		await Assert.ThrowsAsync<InvalidOperationException>(async () => { var rowCount = await bl.LoadAsync(); });
+#else
+		await Assert.ThrowsAsync<MySqlException>(async () => { var rowCount = await bl.LoadAsync(memoryStream); });
+#endif
 	}
 
 	[Fact]
@@ -374,7 +401,9 @@ public async Task BulkLoadLocalMemoryStream()
 		await connection.OpenAsync();
 		MySqlBulkLoader bl = new MySqlBulkLoader(connection);
 		using var memoryStream = new MemoryStream(m_memoryStreamBytes, false);
+#if !MYSQL_DATA
 		bl.SourceStream = memoryStream;
+#endif
 		bl.TableName = m_testTable;
 		bl.Columns.AddRange(new string[] { "one", "two", "three" });
 		bl.NumberOfLinesToSkip = 0;
@@ -382,10 +411,15 @@ public async Task BulkLoadLocalMemoryStream()
 		bl.FieldQuotationCharacter = '"';
 		bl.FieldQuotationOptional = true;
 		bl.Local = true;
+#if !MYSQL_DATA
 		int rowCount = await bl.LoadAsync();
+#else
+		int rowCount = await bl.LoadAsync(memoryStream);
+#endif
 		Assert.Equal(5, rowCount);
 	}
 
+#if !MYSQL_DATA
 	[Fact]
 	public async Task BulkCopyDataReader()
 	{
@@ -425,12 +459,12 @@ public async Task BulkCopyDataReader()
 	}
 
 	[Fact]
-	public void BulkCopyNullDataTable()
+	public async Task BulkCopyNullDataTable()
 	{
 		using var connection = new MySqlConnection(GetLocalConnectionString());
-		connection.Open();
+		await connection.OpenAsync();
 		var bulkCopy = new MySqlBulkCopy(connection);
-		Assert.ThrowsAsync<ArgumentNullException>(async () => await bulkCopy.WriteToServerAsync(default(DataTable)));
+		await Assert.ThrowsAsync<ArgumentNullException>(async () => await bulkCopy.WriteToServerAsync(default(DataTable)));
 	}
 
 	[Fact]
@@ -594,18 +628,18 @@ public async Task BulkCopyDataTableWithWarnings()
 	}
 
 	[Fact]
-	public void BulkCopyNullDataReader()
+	public async Task BulkCopyNullDataReader()
 	{
 		using var connection = new MySqlConnection(GetLocalConnectionString());
-		connection.Open();
+		await connection.OpenAsync();
 		var bulkCopy = new MySqlBulkCopy(connection);
-		Assert.ThrowsAsync<ArgumentNullException>(async () => await bulkCopy.WriteToServerAsync(default(DbDataReader)));
+		await Assert.ThrowsAsync<ArgumentNullException>(async () => await bulkCopy.WriteToServerAsync(default(DbDataReader)));
 	}
 #endif
 
 	private static string GetConnectionString() => BulkLoaderSync.GetConnectionString();
 	private static string GetLocalConnectionString() => BulkLoaderSync.GetLocalConnectionString();
 
-	readonly string m_testTable;
-	readonly byte[] m_memoryStreamBytes;
+	private readonly string m_testTable;
+	private readonly byte[] m_memoryStreamBytes;
 }
diff --git a/tests/IntegrationTests/BulkLoaderSync.cs b/tests/IntegrationTests/BulkLoaderSync.cs
index 4dc65ca38..ddcb849cc 100644
--- a/tests/IntegrationTests/BulkLoaderSync.cs
+++ b/tests/IntegrationTests/BulkLoaderSync.cs
@@ -51,9 +51,11 @@ public void BulkLoadTsvFile()
 	{
 		using var connection = new MySqlConnection(GetConnectionString());
 		connection.Open();
-		MySqlBulkLoader bl = new MySqlBulkLoader(connection);
-		bl.FileName = AppConfig.MySqlBulkLoaderTsvFile;
-		bl.TableName = m_testTable;
+		MySqlBulkLoader bl = new MySqlBulkLoader(connection)
+		{
+			FileName = AppConfig.MySqlBulkLoaderTsvFile,
+			TableName = m_testTable,
+		};
 		bl.Columns.AddRange(new string[] { "one", "two", "three", "four", "five" });
 		bl.NumberOfLinesToSkip = 1;
 		bl.Expressions.Add("five = UNHEX(five)");
@@ -67,9 +69,11 @@ public void BulkLoadLocalTsvFile()
 	{
 		using var connection = new MySqlConnection(GetLocalConnectionString());
 		connection.Open();
-		MySqlBulkLoader bl = new MySqlBulkLoader(connection);
-		bl.FileName = AppConfig.MySqlBulkLoaderLocalTsvFile;
-		bl.TableName = m_testTable;
+		MySqlBulkLoader bl = new MySqlBulkLoader(connection)
+		{
+			FileName = AppConfig.MySqlBulkLoaderLocalTsvFile,
+			TableName = m_testTable,
+		};
 		bl.Columns.AddRange(new string[] { "one", "two", "three", "four", "five" });
 		bl.NumberOfLinesToSkip = 1;
 		bl.Expressions.Add("five = UNHEX(five)");
@@ -83,10 +87,12 @@ public void BulkLoadCsvFile()
 	{
 		using var connection = new MySqlConnection(GetConnectionString());
 		connection.Open();
-		MySqlBulkLoader bl = new MySqlBulkLoader(connection);
-		bl.FileName = AppConfig.MySqlBulkLoaderCsvFile;
-		bl.TableName = m_testTable;
-		bl.CharacterSet = "UTF8";
+		MySqlBulkLoader bl = new MySqlBulkLoader(connection)
+		{
+			FileName = AppConfig.MySqlBulkLoaderCsvFile,
+			TableName = m_testTable,
+			CharacterSet = "UTF8",
+		};
 		bl.Columns.AddRange(new string[] { "one", "two", "three", "four", "five" });
 		bl.NumberOfLinesToSkip = 1;
 		bl.FieldTerminator = ",";
@@ -103,10 +109,12 @@ public void BulkLoadLocalCsvFile()
 	{
 		using var connection = new MySqlConnection(GetLocalConnectionString());
 		connection.Open();
-		MySqlBulkLoader bl = new MySqlBulkLoader(connection);
-		bl.FileName = AppConfig.MySqlBulkLoaderLocalCsvFile;
-		bl.TableName = m_testTable;
-		bl.CharacterSet = "UTF8";
+		MySqlBulkLoader bl = new MySqlBulkLoader(connection)
+		{
+			FileName = AppConfig.MySqlBulkLoaderLocalCsvFile,
+			TableName = m_testTable,
+			CharacterSet = "UTF8",
+		};
 		bl.Columns.AddRange(new string[] { "one", "two", "three", "four", "five" });
 		bl.NumberOfLinesToSkip = 1;
 		bl.FieldTerminator = ",";
@@ -127,10 +135,12 @@ public void BulkLoadCsvFileNotFound()
 		if (string.IsNullOrEmpty(secureFilePath) || secureFilePath == "NULL")
 			return;
 
-		MySqlBulkLoader bl = new MySqlBulkLoader(connection);
-		bl.FileName = Path.Combine(secureFilePath, AppConfig.MySqlBulkLoaderCsvFile + "-junk");
-		bl.TableName = m_testTable;
-		bl.CharacterSet = "UTF8";
+		MySqlBulkLoader bl = new MySqlBulkLoader(connection)
+		{
+			FileName = Path.Combine(secureFilePath, AppConfig.MySqlBulkLoaderCsvFile + "-junk"),
+			TableName = m_testTable,
+			CharacterSet = "UTF8",
+		};
 		bl.Columns.AddRange(new string[] { "one", "two", "three", "four", "five" });
 		bl.NumberOfLinesToSkip = 1;
 		bl.FieldTerminator = ",";
@@ -167,11 +177,13 @@ public void BulkLoadLocalCsvFileNotFound()
 	{
 		using var connection = new MySqlConnection(GetLocalConnectionString());
 		connection.Open();
-		MySqlBulkLoader bl = new MySqlBulkLoader(connection);
-		bl.Timeout = 3; //Set a short timeout for this test because the file not found exception takes a long time otherwise, the timeout does not change the result
-		bl.FileName = AppConfig.MySqlBulkLoaderLocalCsvFile + "-junk";
-		bl.TableName = m_testTable;
-		bl.CharacterSet = "UTF8";
+		MySqlBulkLoader bl = new MySqlBulkLoader(connection)
+		{
+			Timeout = 3, // Set a short timeout for this test because the file not found exception takes a long time otherwise, the timeout does not change the result
+			FileName = AppConfig.MySqlBulkLoaderLocalCsvFile + "-junk",
+			TableName = m_testTable,
+			CharacterSet = "UTF8",
+		};
 		bl.Columns.AddRange(new string[] { "one", "two", "three", "four", "five" });
 		bl.NumberOfLinesToSkip = 1;
 		bl.FieldTerminator = ",";
@@ -204,7 +216,7 @@ public void BulkLoadLocalCsvFileNotFound()
 		}
 		catch (Exception exception)
 		{
-			//We know that the exception is not a MySqlException, just use the assertion to fail the test
+			// We know that the exception is not a MySqlException, just use the assertion to fail the test
 			Assert.IsType<MySqlException>(exception);
 		}
 	}
@@ -334,9 +346,11 @@ public void BulkLoadMissingFileName()
 	{
 		using var connection = new MySqlConnection(GetConnectionString());
 		connection.Open();
-		MySqlBulkLoader bl = new MySqlBulkLoader(connection);
-		bl.TableName = m_testTable;
-		bl.CharacterSet = "UTF8";
+		MySqlBulkLoader bl = new MySqlBulkLoader(connection)
+		{
+			TableName = m_testTable,
+			CharacterSet = "UTF8",
+		};
 		bl.Columns.AddRange(new string[] { "one", "two", "three", "four", "five" });
 		bl.NumberOfLinesToSkip = 1;
 		bl.FieldTerminator = ",";
@@ -345,15 +359,9 @@ public void BulkLoadMissingFileName()
 		bl.Expressions.Add("five = UNHEX(five)");
 		bl.Local = false;
 #if MYSQL_DATA
-		Assert.Throws<System.NullReferenceException>(() =>
-		{
-			int rowCount = bl.Load();
-		});
+		Assert.Throws<MySqlException>(() => { var rowCount = bl.Load(); });
 #else
-		Assert.Throws<System.InvalidOperationException>(() =>
-		{
-			int rowCount = bl.Load();
-		});
+		Assert.Throws<InvalidOperationException>(() => { var rowCount = bl.Load(); });
 #endif
 	}
 
@@ -362,8 +370,10 @@ public void BulkLoadMissingTableName()
 	{
 		using var connection = new MySqlConnection(GetConnectionString());
 		connection.Open();
-		MySqlBulkLoader bl = new MySqlBulkLoader(connection);
-		bl.FileName = AppConfig.MySqlBulkLoaderLocalCsvFile;
+		MySqlBulkLoader bl = new MySqlBulkLoader(connection)
+		{
+			FileName = AppConfig.MySqlBulkLoaderLocalCsvFile,
+		};
 		bl.Columns.AddRange(new string[] { "one", "two", "three", "four", "five" });
 		bl.NumberOfLinesToSkip = 1;
 		bl.FieldTerminator = ",";
@@ -384,7 +394,6 @@ public void BulkLoadMissingTableName()
 #endif
 	}
 
-#if !MYSQL_DATA
 	[SkippableFact(ConfigSettings.LocalCsvFile)]
 	public void BulkLoadFileStreamInvalidOperation()
 	{
@@ -392,7 +401,9 @@ public void BulkLoadFileStreamInvalidOperation()
 		connection.Open();
 		MySqlBulkLoader bl = new MySqlBulkLoader(connection);
 		using var fileStream = new FileStream(AppConfig.MySqlBulkLoaderLocalCsvFile, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true);
+#if !MYSQL_DATA
 		bl.SourceStream = fileStream;
+#endif
 		bl.TableName = m_testTable;
 		bl.CharacterSet = "UTF8";
 		bl.Columns.AddRange(new string[] { "one", "two", "three", "four", "five" });
@@ -402,10 +413,11 @@ public void BulkLoadFileStreamInvalidOperation()
 		bl.FieldQuotationOptional = true;
 		bl.Expressions.Add("five = UNHEX(five)");
 		bl.Local = false;
-		Assert.Throws<System.InvalidOperationException>(() =>
-		{
-			int rowCount = bl.Load();
-		});
+#if !MYSQL_DATA
+		Assert.Throws<InvalidOperationException>(() => { int rowCount = bl.Load(); });
+#else
+		Assert.Throws<MySqlException>(() => { int rowCount = bl.Load(fileStream); });
+#endif
 	}
 
 	[SkippableFact(ConfigSettings.LocalCsvFile)]
@@ -415,7 +427,9 @@ public void BulkLoadLocalFileStream()
 		connection.Open();
 		MySqlBulkLoader bl = new MySqlBulkLoader(connection);
 		using var fileStream = new FileStream(AppConfig.MySqlBulkLoaderLocalCsvFile, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true);
+#if !MYSQL_DATA
 		bl.SourceStream = fileStream;
+#endif
 		bl.TableName = m_testTable;
 		bl.CharacterSet = "UTF8";
 		bl.Columns.AddRange(new string[] { "one", "two", "three", "four", "five" });
@@ -425,7 +439,11 @@ public void BulkLoadLocalFileStream()
 		bl.FieldQuotationOptional = true;
 		bl.Expressions.Add("five = UNHEX(five)");
 		bl.Local = true;
+#if !MYSQL_DATA
 		int rowCount = bl.Load();
+#else
+		int rowCount = bl.Load(fileStream);
+#endif
 		Assert.Equal(20, rowCount);
 	}
 
@@ -436,7 +454,9 @@ public void BulkLoadMemoryStreamInvalidOperation()
 		connection.Open();
 		MySqlBulkLoader bl = new MySqlBulkLoader(connection);
 		using var memoryStream = new MemoryStream(m_memoryStreamBytes, false);
+#if !MYSQL_DATA
 		bl.SourceStream = memoryStream;
+#endif
 		bl.TableName = m_testTable;
 		bl.CharacterSet = "UTF8";
 		bl.Columns.AddRange(new string[] { "one", "two", "three" });
@@ -445,7 +465,11 @@ public void BulkLoadMemoryStreamInvalidOperation()
 		bl.FieldQuotationCharacter = '"';
 		bl.FieldQuotationOptional = true;
 		bl.Local = false;
-		Assert.Throws<System.InvalidOperationException>(() => bl.Load());
+#if !MYSQL_DATA
+		Assert.Throws<InvalidOperationException>(() => bl.Load());
+#else
+		Assert.Throws<MySqlException>(() => bl.Load(memoryStream));
+#endif
 	}
 
 	[Fact]
@@ -455,7 +479,9 @@ public void BulkLoadLocalMemoryStream()
 		connection.Open();
 		MySqlBulkLoader bl = new MySqlBulkLoader(connection);
 		using var memoryStream = new MemoryStream(m_memoryStreamBytes, false);
+#if !MYSQL_DATA
 		bl.SourceStream = memoryStream;
+#endif
 		bl.TableName = m_testTable;
 		bl.CharacterSet = "UTF8";
 		bl.Columns.AddRange(new string[] { "one", "two", "three" });
@@ -464,10 +490,15 @@ public void BulkLoadLocalMemoryStream()
 		bl.FieldQuotationCharacter = '"';
 		bl.FieldQuotationOptional = true;
 		bl.Local = true;
+#if !MYSQL_DATA
 		int rowCount = bl.Load();
+#else
+		int rowCount = bl.Load(memoryStream);
+#endif
 		Assert.Equal(5, rowCount);
 	}
 
+#if !MYSQL_DATA
 	[Fact]
 	public void BulkCopyDataReader()
 	{
@@ -515,7 +546,6 @@ public void BulkCopyNullDataTable()
 		Assert.Throws<ArgumentNullException>(() => bulkCopy.WriteToServer(default(DataTable)));
 	}
 
-#if !MYSQL_DATA
 	[Fact]
 	public void BulkCopyDataTableWithMySqlDecimal()
 	{
@@ -640,7 +670,6 @@ public void BulkCopyDataTableWithTimeOnly()
 			Assert.Equal(new TimeOnly(1, 2, 3, 456), reader.GetTimeOnly(2));
 		}
 	}
-#endif
 #endif
 
 	public static IEnumerable<object[]> GetBulkCopyData() =>
@@ -698,6 +727,50 @@ public void BulkCopyDataTable(string columnType, object[] rows)
 		}
 	}
 
+	[Fact]
+	public void BulkCopyToColumnNeedingQuoting()
+	{
+		var dataTable = new DataTable()
+		{
+			Columns =
+			{
+				new DataColumn("id", typeof(int)),
+				new DataColumn("@a", typeof(string)),
+			},
+		};
+		dataTable.Rows.Add(2, "two");
+
+		using var connection = new MySqlConnection(GetLocalConnectionString());
+		connection.Open();
+		using (var cmd = new MySqlCommand($"""
+			drop table if exists bulk_load_quoted_identifier;
+			create table bulk_load_quoted_identifier(id int, `@a` text);
+			insert into bulk_load_quoted_identifier values (1, 'one');
+			""", connection))
+		{
+			cmd.ExecuteNonQuery();
+		}
+
+		var bulkCopy = new MySqlBulkCopy(connection)
+		{
+			DestinationTableName = "bulk_load_quoted_identifier",
+		};
+		var result = bulkCopy.WriteToServer(dataTable);
+		Assert.Equal(1, result.RowsInserted);
+		Assert.Empty(result.Warnings);
+
+		using (var cmd = new MySqlCommand(@"select `@a` from bulk_load_quoted_identifier order by id;", connection))
+		{
+			using var reader = cmd.ExecuteReader();
+			Assert.True(reader.Read());
+			Assert.Equal("one", reader.GetString(0));
+			Assert.True(reader.Read());
+			Assert.Equal("two", reader.GetString(0));
+			Assert.False(reader.Read());
+			Assert.False(reader.NextResult());
+		}
+	}
+
 	[Fact]
 	public void BulkCopyDataTableWithLongBlob()
 	{
@@ -975,7 +1048,7 @@ public void BulkCopyColumnMappings()
 				new object[] { 1, 100, "a", "A", new byte[] { 0x33, 0x30 } },
 				new object[] { 2, 200, "bb", "BB", new byte[] { 0x33, 0x31 } },
 				new object[] { 3, 300, "ccc", "CCC", new byte[] { 0x33, 0x32 } },
-			}
+			},
 		};
 
 		var result = bulkCopy.WriteToServer(dataTable);
@@ -1029,7 +1102,7 @@ public void BulkCopyColumnMappingsInvalidSourceOrdinal()
 				new object[] { 1 },
 				new object[] { 2 },
 				new object[] { 3 },
-			}
+			},
 		};
 
 		Assert.Throws<InvalidOperationException>(() => bulkCopy.WriteToServer(dataTable));
@@ -1066,7 +1139,7 @@ public void BulkCopyColumnMappingsInvalidDestinationColumn()
 				new object[] { 1 },
 				new object[] { 2 },
 				new object[] { 3 },
-			}
+			},
 		};
 
 		Assert.Throws<InvalidOperationException>(() => bulkCopy.WriteToServer(dataTable));
@@ -1083,7 +1156,7 @@ public void BulkCopyDoesNotInsertAllRows()
 
 		var bcp = new MySqlBulkCopy(connection)
 		{
-			DestinationTableName = "bulk_copy_duplicate_pk"
+			DestinationTableName = "bulk_copy_duplicate_pk",
 		};
 
 		var dataTable = new DataTable()
@@ -1098,7 +1171,7 @@ public void BulkCopyDoesNotInsertAllRows()
 				new object[] { 1, "a" },
 				new object[] { 1, "b" },
 				new object[] { 3, "c" },
-			}
+			},
 		};
 
 		var ex = Assert.Throws<MySqlException>(() => bcp.WriteToServer(dataTable));
@@ -1186,26 +1259,26 @@ public void BulkCopyDataTableConflictOption(MySqlBulkLoaderConflictOption confli
 
 		switch (conflictOption)
 		{
-		case MySqlBulkLoaderConflictOption.None:
-			var exception = Assert.Throws<MySqlException>(() => bulkCopy.WriteToServer(dataTable));
-			Assert.Equal(MySqlErrorCode.BulkCopyFailed, exception.ErrorCode);
-			break;
-
-		case MySqlBulkLoaderConflictOption.Replace:
-			var replaceResult = bulkCopy.WriteToServer(dataTable);
-			Assert.Equal(expectedRowsInserted, replaceResult.RowsInserted);
-			Assert.Empty(replaceResult.Warnings);
-			break;
-
-		case MySqlBulkLoaderConflictOption.Ignore:
-			var ignoreResult = bulkCopy.WriteToServer(dataTable);
-			Assert.Equal(expectedRowsInserted, ignoreResult.RowsInserted);
-			if (!connection.ServerVersion.StartsWith("5.6.", StringComparison.Ordinal))
-			{
-				var error = Assert.Single(ignoreResult.Warnings);
-				Assert.Equal(MySqlErrorCode.DuplicateKeyEntry, error.ErrorCode);
-			}
-			break;
+			case MySqlBulkLoaderConflictOption.None:
+				var exception = Assert.Throws<MySqlException>(() => bulkCopy.WriteToServer(dataTable));
+				Assert.Equal(MySqlErrorCode.BulkCopyFailed, exception.ErrorCode);
+				break;
+
+			case MySqlBulkLoaderConflictOption.Replace:
+				var replaceResult = bulkCopy.WriteToServer(dataTable);
+				Assert.Equal(expectedRowsInserted, replaceResult.RowsInserted);
+				Assert.Empty(replaceResult.Warnings);
+				break;
+
+			case MySqlBulkLoaderConflictOption.Ignore:
+				var ignoreResult = bulkCopy.WriteToServer(dataTable);
+				Assert.Equal(expectedRowsInserted, ignoreResult.RowsInserted);
+				if (!connection.ServerVersion.StartsWith("5.6.", StringComparison.Ordinal))
+				{
+					var error = Assert.Single(ignoreResult.Warnings);
+					Assert.Equal(MySqlErrorCode.DuplicateKeyEntry, error.ErrorCode);
+				}
+				break;
 		}
 
 		using (var cmd = new MySqlCommand("select b from bulk_load_data_table;", connection))
@@ -1222,6 +1295,6 @@ internal static string GetLocalConnectionString()
 		return csb.ConnectionString;
 	}
 
-	readonly string m_testTable;
-	readonly byte[] m_memoryStreamBytes;
+	private readonly string m_testTable;
+	private readonly byte[] m_memoryStreamBytes;
 }
diff --git a/tests/IntegrationTests/CancelTests.cs b/tests/IntegrationTests/CancelTests.cs
index 8c0a2b295..74cc640de 100644
--- a/tests/IntegrationTests/CancelTests.cs
+++ b/tests/IntegrationTests/CancelTests.cs
@@ -21,7 +21,7 @@ public void NoCancel()
 		Assert.InRange(stopwatch.ElapsedMilliseconds, 100, 1000);
 	}
 
-	[SkippableFact(ServerFeatures.CancelSleepSuccessfully)]
+	[Fact]
 	public void CancelCommand()
 	{
 		using var cmd = new MySqlCommand("SELECT SLEEP(5)", m_database.Connection);
@@ -32,14 +32,16 @@ public void CancelCommand()
 		});
 
 		var stopwatch = Stopwatch.StartNew();
-		TestUtilities.AssertIsOne(cmd.ExecuteScalar());
+		TestUtilities.AssertExecuteScalarReturnsOneOrIsCanceled(cmd);
 		Assert.InRange(stopwatch.ElapsedMilliseconds, 250, 2500);
 
+#pragma warning disable xUnit1031 // Do not use blocking task operations in test method
 		task.Wait(); // shouldn't throw
+#pragma warning restore xUnit1031 // Do not use blocking task operations in test method
 	}
 
 #if !MYSQL_DATA
-	[SkippableFact(ServerFeatures.CancelSleepSuccessfully | ServerFeatures.Timeout)]
+	[SkippableFact(ServerFeatures.Timeout)]
 	public async Task CancelCommandWithPasswordCallback()
 	{
 		var csb = AppConfig.CreateConnectionStringBuilder();
@@ -58,13 +60,13 @@ public async Task CancelCommandWithPasswordCallback()
 		});
 
 		var stopwatch = Stopwatch.StartNew();
-		TestUtilities.AssertIsOne(await command.ExecuteScalarAsync());
+		await TestUtilities.AssertExecuteScalarReturnsOneOrIsCanceledAsync(command);
 		Assert.InRange(stopwatch.ElapsedMilliseconds, 250, 2500);
 
 		task.Wait(); // shouldn't throw
 	}
 
-	[SkippableFact(ServerFeatures.CancelSleepSuccessfully | ServerFeatures.Timeout)]
+	[SkippableFact(ServerFeatures.Timeout)]
 	public async Task CancelCommandCancellationTokenWithPasswordCallback()
 	{
 		var csb = AppConfig.CreateConnectionStringBuilder();
@@ -78,7 +80,7 @@ public async Task CancelCommandCancellationTokenWithPasswordCallback()
 		using var command = new MySqlCommand("SELECT SLEEP(5)", connection);
 		using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(500));
 		var stopwatch = Stopwatch.StartNew();
-		TestUtilities.AssertIsOne(await command.ExecuteScalarAsync(cts.Token));
+		await TestUtilities.AssertExecuteScalarReturnsOneOrIsCanceledAsync(command, cts.Token);
 		Assert.InRange(stopwatch.ElapsedMilliseconds, 250, 2500);
 	}
 #endif
@@ -517,50 +519,6 @@ public void CancelBatchBeforeRead()
 		Assert.InRange(rows, 0, 10000000);
 	}
 
-	[SkippableFact(ServerFeatures.StreamingResults | ServerFeatures.Timeout, Skip = "COM_MULTI")]
-	public void CancelMultiCommandBatchReader()
-	{
-		using var barrier = new Barrier(2);
-		using var batch = new MySqlBatch(m_database.Connection)
-		{
-			BatchCommands =
-			{
-				new MySqlBatchCommand(c_hugeQuery),
-				new MySqlBatchCommand(c_hugeQuery),
-				new MySqlBatchCommand(c_hugeQuery),
-			},
-		};
-		var task = Task.Run(() =>
-		{
-			barrier.SignalAndWait();
-			batch.Cancel();
-		});
-
-		int rows = 0;
-		using (var reader = batch.ExecuteReader())
-		{
-			Assert.True(reader.Read());
-
-			barrier.SignalAndWait();
-			try
-			{
-				while (reader.Read())
-					rows++;
-			}
-			catch (MySqlException ex)
-			{
-				Assert.Equal(MySqlErrorCode.QueryInterrupted, ex.ErrorCode);
-			}
-
-			// query returns 25 billion rows; we shouldn't have read many of them
-			Assert.InRange(rows, 0, 10000000);
-
-			Assert.False(reader.NextResult());
-		}
-
-		task.Wait(); // shouldn't throw
-	}
-
 	[Fact]
 	public async Task CancelBatchWithTokenBeforeExecuteScalar()
 	{
@@ -686,41 +644,6 @@ public async Task CancelHugeQueryBatchWithTokenAfterExecuteReader()
 		Assert.False(reader.NextResult());
 	}
 
-	[SkippableFact(ServerFeatures.StreamingResults | ServerFeatures.Timeout, Skip = "COM_MULTI")]
-	public async Task CancelHugeQueryBatchWithTokenInNextResult()
-	{
-		using var batch = new MySqlBatch(m_database.Connection)
-		{
-			BatchCommands =
-			{
-				new MySqlBatchCommand(c_hugeQuery),
-				new MySqlBatchCommand("select 1, 2, 3;"),
-			},
-		};
-		using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(0.5));
-		using var reader = await batch.ExecuteReaderAsync(cts.Token);
-
-		// read first result set
-		Assert.True(await reader.ReadAsync(cts.Token));
-
-		try
-		{
-			// skip to the next result set
-			Assert.True(await reader.NextResultAsync(cts.Token));
-
-			// shouldn't get here
-			Assert.True(false);
-		}
-		catch (OperationCanceledException ex)
-		{
-			Assert.Equal(cts.Token, ex.CancellationToken);
-		}
-
-		// no more result sets
-		Assert.False(reader.Read());
-		Assert.False(reader.NextResult());
-	}
-
 	[SkippableFact(ServerFeatures.Timeout)]
 	public async Task CancelSlowQueryBatchWithTokenAfterExecuteReader()
 	{
@@ -794,39 +717,7 @@ public async Task CancelSlowQueryBatchWithTokenAfterNextResult()
 
 		Assert.False(await reader.NextResultAsync());
 	}
-
-	[SkippableFact(ServerFeatures.StreamingResults | ServerFeatures.Timeout, Skip = "COM_MULTI")]
-	public async Task CancelMultiStatementBatchInRead()
-	{
-		using var batch = new MySqlBatch(m_database.Connection)
-		{
-			BatchCommands =
-			{
-				new MySqlBatchCommand(c_hugeQuery),
-				new MySqlBatchCommand(c_hugeQuery),
-				new MySqlBatchCommand(c_hugeQuery),
-			},
-		};
-		using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(0.5));
-		using var reader = await batch.ExecuteReaderAsync();
-		var rows = 0;
-		try
-		{
-			while (await reader.ReadAsync(cts.Token))
-				rows++;
-
-			Assert.True(false);
-		}
-		catch (OperationCanceledException ex)
-		{
-			Assert.Equal(cts.Token, ex.CancellationToken);
-			Assert.InRange(rows, 0, 10000000);
-		}
-
-		// no more result sets; the whole command was cancelled
-		Assert.False(reader.Read());
-		Assert.False(reader.NextResult());
-	}
+#endif
 #endif
 
 	private static CancellationToken GetCanceledToken()
@@ -836,15 +727,14 @@ private static CancellationToken GetCanceledToken()
 		return cts.Token;
 	}
 
-	static readonly CancellationToken s_canceledToken = GetCanceledToken();
-#endif
-
 	// returns billions of rows
-	const string c_hugeQuery = @"select * from integers a join integers b join integers c join integers d join integers e join integers f join integers g join integers h;";
+	private const string c_hugeQuery = @"select * from integers a join integers b join integers c join integers d join integers e join integers f join integers g join integers h;";
 
 	// takes a long time to return any rows
-	const string c_slowQuery = @"select * from integers a join integers b join integers c join integers d join integers e join integers f join integers g join integers h
+	private const string c_slowQuery = @"select * from integers a join integers b join integers c join integers d join integers e join integers f join integers g join integers h
 where sqrt(a.value) + sqrt(b.value) + sqrt(c.value) + sqrt(d.value) + sqrt(e.value) + sqrt(f.value) + sqrt(g.value) + sqrt(h.value) = 20;";
 
-	readonly DatabaseFixture m_database;
+	private static readonly CancellationToken s_canceledToken = GetCanceledToken();
+
+	private readonly DatabaseFixture m_database;
 }
diff --git a/tests/IntegrationTests/CharacterSetTests.cs b/tests/IntegrationTests/CharacterSetTests.cs
index 2c2971f7e..e64f31b4b 100644
--- a/tests/IntegrationTests/CharacterSetTests.cs
+++ b/tests/IntegrationTests/CharacterSetTests.cs
@@ -78,9 +78,11 @@ public void CollationConnection(bool reopenConnection)
 		}
 
 		var collation = connection.Query<string>(@"select @@collation_connection;").Single();
-		var expected = connection.ServerVersion.StartsWith("8.0") ? "utf8mb4_0900_ai_ci" : "utf8mb4_general_ci";
+		var expected = connection.ServerVersion.Substring(0, 2) is "8." or "9." ? "utf8mb4_0900_ai_ci" :
+			connection.ServerVersion.StartsWith("11.4.", StringComparison.Ordinal) || connection.ServerVersion.StartsWith("11.8.", StringComparison.Ordinal) ? "utf8mb4_uca1400_ai_ci" :
+			"utf8mb4_general_ci";
 		Assert.Equal(expected, collation);
 	}
 
-	readonly DatabaseFixture m_database;
+	private readonly DatabaseFixture m_database;
 }
diff --git a/tests/IntegrationTests/ChunkStream.cs b/tests/IntegrationTests/ChunkStream.cs
new file mode 100644
index 000000000..9c44d3ede
--- /dev/null
+++ b/tests/IntegrationTests/ChunkStream.cs
@@ -0,0 +1,130 @@
+namespace IntegrationTests;
+
+internal sealed class ChunkStream : Stream
+{
+	public ChunkStream(byte[] data, int chunkLength)
+	{
+		if (data is null)
+			throw new ArgumentNullException(nameof(data));
+		if (chunkLength <= 0)
+			throw new ArgumentOutOfRangeException(nameof(chunkLength));
+
+		m_data = data;
+		m_chunkLength = chunkLength;
+		m_position = 0;
+	}
+
+	public override bool CanRead => true;
+	public override bool CanSeek => false;
+	public override bool CanWrite => false;
+	public override long Length => m_data.Length;
+	public override long Position
+	{
+		get => m_position;
+		set => throw new NotSupportedException();
+	}
+
+	public override int Read(byte[] buffer, int offset, int count)
+	{
+		if (buffer is null)
+			throw new ArgumentNullException(nameof(buffer));
+		if (offset < 0 || offset > buffer.Length)
+			throw new ArgumentOutOfRangeException(nameof(offset));
+		if (count < 0 || offset + count > buffer.Length)
+			throw new ArgumentOutOfRangeException(nameof(count));
+
+		return Read(buffer.AsSpan(offset, count));
+	}
+
+	public
+#if NETSTANDARD2_1 || NETCOREAPP2_1_OR_GREATER
+		override
+#endif
+		int Read(Span<byte> buffer)
+	{
+		if (m_position >= m_data.Length)
+			return 0;
+
+		// Read at most chunkLength bytes
+		var bytesToRead = Math.Min(buffer.Length, Math.Min(m_chunkLength, m_data.Length - m_position));
+
+		// Copy data from the actual data array
+		m_data.AsSpan(m_position, bytesToRead).CopyTo(buffer);
+
+		m_position += bytesToRead;
+		return bytesToRead;
+	}
+
+	public override int ReadByte()
+	{
+		Span<byte> buffer = stackalloc byte[1];
+		var bytesRead = Read(buffer);
+		return bytesRead == 0 ? -1 : buffer[0];
+	}
+
+	public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+	{
+		if (buffer is null)
+			throw new ArgumentNullException(nameof(buffer));
+		if (offset < 0 || offset > buffer.Length)
+			throw new ArgumentOutOfRangeException(nameof(offset));
+		if (count < 0 || offset + count > buffer.Length)
+			throw new ArgumentOutOfRangeException(nameof(count));
+
+		if (cancellationToken.IsCancellationRequested)
+			return Task.FromCanceled<int>(cancellationToken);
+
+		try
+		{
+			return Task.FromResult(Read(buffer.AsSpan(offset, count)));
+		}
+		catch (Exception ex)
+		{
+			return Task.FromException<int>(ex);
+		}
+	}
+
+	public
+#if NETSTANDARD2_1 || NETCOREAPP2_1_OR_GREATER
+		override
+#endif
+		ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
+	{
+		if (cancellationToken.IsCancellationRequested)
+			return new(Task.FromCanceled<int>(cancellationToken));
+
+		try
+		{
+			return new(Read(buffer.Span));
+		}
+		catch (Exception ex)
+		{
+			return new(Task.FromException<int>(ex));
+		}
+	}
+
+	public override void Write(byte[] buffer, int offset, int count) =>
+		throw new NotSupportedException();
+
+	public override void WriteByte(byte value) =>
+		throw new NotSupportedException();
+
+	public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) =>
+		throw new NotSupportedException();
+
+	public override void SetLength(long value) =>
+		throw new NotSupportedException();
+
+	public override long Seek(long offset, SeekOrigin origin) =>
+		throw new NotSupportedException();
+
+	public override void Flush() =>
+		throw new NotSupportedException();
+
+	public override Task FlushAsync(CancellationToken cancellationToken) =>
+		throw new NotSupportedException();
+
+	private readonly byte[] m_data;
+	private readonly int m_chunkLength;
+	private int m_position;
+}
diff --git a/tests/IntegrationTests/ClientFactoryTests.cs b/tests/IntegrationTests/ClientFactoryTests.cs
index bebd2f5e0..01ba718c0 100644
--- a/tests/IntegrationTests/ClientFactoryTests.cs
+++ b/tests/IntegrationTests/ClientFactoryTests.cs
@@ -24,7 +24,6 @@ public void CreateConnectionStringBuilder()
 		Assert.IsType<MySqlConnectionStringBuilder>(MySqlConnectorFactory.Instance.CreateConnectionStringBuilder());
 	}
 
-
 	[Fact]
 	public void CreateParameter()
 	{
@@ -51,7 +50,7 @@ public void DbProviderFactoriesGetFactory()
 #else
 		var providerInvariantName = "MySqlConnector";
 #endif
-#if !NET462 && !NET472
+#if !NETFRAMEWORK
 		DbProviderFactories.RegisterFactory(providerInvariantName, MySqlConnectorFactory.Instance);
 #endif
 		var factory = DbProviderFactories.GetFactory(providerInvariantName);
diff --git a/tests/IntegrationTests/CommandBuilderTests.cs b/tests/IntegrationTests/CommandBuilderTests.cs
index 73b109876..584c8fe30 100644
--- a/tests/IntegrationTests/CommandBuilderTests.cs
+++ b/tests/IntegrationTests/CommandBuilderTests.cs
@@ -122,11 +122,11 @@ value varchar(100)
 	[Theory]
 	[InlineData("test", "`test`")]
 	[InlineData("te`st", "`te``st`")]
-	[InlineData("`test`", "```test```"
 #if MYSQL_DATA
-		, Skip = "Doesn't quote leading quotes"
+	[InlineData("`test`", "```test```", Skip = "Doesn't quote leading quotes")]
+#else
+	[InlineData("`test`", "```test```")]
 #endif
-	)]
 	public void QuoteIdentifier(string input, string expected)
 	{
 		var cb = new MySqlCommandBuilder();
@@ -144,5 +144,5 @@ public void UnquoteIdentifier(string input, string expected)
 		Assert.Equal(expected, cb.UnquoteIdentifier(input));
 	}
 
-	readonly DatabaseFixture m_database;
+	private readonly DatabaseFixture m_database;
 }
diff --git a/tests/IntegrationTests/CommandTests.cs b/tests/IntegrationTests/CommandTests.cs
index 1414042db..2543cc367 100644
--- a/tests/IntegrationTests/CommandTests.cs
+++ b/tests/IntegrationTests/CommandTests.cs
@@ -44,6 +44,114 @@ public void CreateCommandSetsConnection()
 		Assert.Equal(m_database.Connection, command.Connection);
 	}
 
+#if !MYSQL_DATA
+	[Fact]
+	public void SingleQueryWithPrepareReuse()
+	{
+		using var connection = new MySqlConnection(m_database.Connection.ConnectionString);
+		connection.Open();
+		using (var cmd = connection.CreateCommand())
+		{
+			cmd.CommandText = "select ?";
+			cmd.Prepare();
+			cmd.Parameters.Add(new() { Value = 1 });
+			using (var reader = cmd.ExecuteReader())
+			{
+				Assert.True(reader.Read());
+				Assert.Equal(1, reader.GetInt32(0));
+				Assert.False(reader.Read());
+			}
+
+			cmd.Parameters.Clear();
+			cmd.Parameters.Add(new() { Value = 100 });
+			using (var reader = cmd.ExecuteReader())
+			{
+				Assert.True(reader.Read());
+				Assert.Equal(100, reader.GetInt32(0));
+				Assert.False(reader.Read());
+			}
+		}
+
+		using (var cmd = connection.CreateCommand())
+		{
+			cmd.CommandText = "select ?";
+			cmd.Prepare();
+			cmd.Parameters.Add(new() { Value = 2 });
+			using var reader = cmd.ExecuteReader();
+			Assert.True(reader.Read());
+			Assert.Equal(2, reader.GetInt32(0));
+			Assert.False(reader.Read());
+		}
+	}
+
+	[Fact]
+	public void MultiQueryWithPrepareReuse()
+	{
+		using var connection = new MySqlConnection(m_database.Connection.ConnectionString);
+		connection.Open();
+		using (var cmd = connection.CreateCommand())
+		{
+			cmd.CommandText = "select ?; select ? + 2";
+			cmd.Prepare();
+			cmd.Parameters.Add(new() { Value = 1 });
+			cmd.Parameters.Add(new() { Value = 4 });
+			using (var reader = cmd.ExecuteReader())
+			{
+				Assert.True(reader.Read());
+				Assert.Equal(1, reader.GetInt32(0));
+				Assert.False(reader.Read());
+
+				Assert.True(reader.NextResult());
+
+				Assert.True(reader.Read());
+				Assert.Equal(6, reader.GetInt32(0));
+				Assert.False(reader.Read());
+
+				Assert.False(reader.NextResult());
+			}
+
+			cmd.Parameters.Clear();
+			cmd.Parameters.Add(new() { Value = 100 });
+			cmd.Parameters.Add(new() { Value = 400 });
+			using (var reader = cmd.ExecuteReader())
+			{
+				Assert.True(reader.Read());
+				Assert.Equal(100, reader.GetInt32(0));
+				Assert.False(reader.Read());
+
+				Assert.True(reader.NextResult());
+
+				Assert.True(reader.Read());
+				Assert.Equal(402, reader.GetInt32(0));
+				Assert.False(reader.Read());
+
+				Assert.False(reader.NextResult());
+			}
+		}
+
+		using (var cmd = connection.CreateCommand())
+		{
+			cmd.CommandText = "select ?; select ? + 2";
+			cmd.Prepare();
+			cmd.Parameters.Add(new() { Value = 2 });
+			cmd.Parameters.Add(new() { Value = 5 });
+			using var reader = cmd.ExecuteReader();
+
+			Assert.True(reader.Read());
+			Assert.Equal(2, reader.GetInt32(0));
+			Assert.False(reader.Read());
+
+			Assert.True(reader.NextResult());
+
+			Assert.True(reader.Read());
+			Assert.Equal(7, reader.GetInt32(0));
+			Assert.False(reader.Read());
+
+			Assert.False(reader.NextResult());
+		}
+	}
+#endif
+
 	[Fact]
 	public void CreateCommandDoesNotSetTransaction()
 	{
@@ -118,7 +226,7 @@ public void ExecuteNonQueryForSelectReturnsNegativeOne()
 		Assert.Equal(-1, command.ExecuteNonQuery());
 	}
 
-[Fact]
+	[Fact]
 	public async Task ExecuteNonQueryReturnValue()
 	{
 		using var connection = new MySqlConnection(m_database.Connection.ConnectionString);
@@ -159,7 +267,7 @@ public void IgnoreCommandTransactionIgnoresNull()
 	{
 		using var connection = new MySqlConnection(GetIgnoreCommandTransactionConnectionString());
 		connection.Open();
-		using var _ = connection.BeginTransaction();
+		using var ignoredTransaction = connection.BeginTransaction();
 		using var command = connection.CreateCommand();
 		command.CommandText = "SELECT 1;";
 		TestUtilities.AssertIsOne(command.ExecuteScalar());
@@ -295,7 +403,7 @@ public void CancelEmptyCommandIsNoop()
 		cmd.Cancel();
 	}
 
-	[SkippableFact(MySqlData = "https://bugs.mysql.com/bug.php?id=101507")]
+	[Fact]
 	public void CancelCommandForClosedConnectionIsNoop()
 	{
 		using var connection = new MySqlConnection(AppConfig.ConnectionString);
@@ -305,7 +413,7 @@ public void CancelCommandForClosedConnectionIsNoop()
 		cmd.Cancel();
 	}
 
-	[SkippableFact(MySqlData = "https://bugs.mysql.com/bug.php?id=101507")]
+	[Fact]
 	public void CancelCommandForDisposedConnectionIsNoop()
 	{
 		using var connection = new MySqlConnection(AppConfig.ConnectionString);
@@ -359,10 +467,10 @@ private static string GetIgnoreCommandTransactionConnectionString()
 #else
 		return new MySqlConnectionStringBuilder(AppConfig.ConnectionString)
 		{
-			IgnoreCommandTransaction = true
+			IgnoreCommandTransaction = true,
 		}.ConnectionString;
 #endif
 	}
 
-	readonly DatabaseFixture m_database;
+	private readonly DatabaseFixture m_database;
 }
diff --git a/tests/IntegrationTests/CommandTimeoutTests.cs b/tests/IntegrationTests/CommandTimeoutTests.cs
index 3806b3193..d49246e4e 100644
--- a/tests/IntegrationTests/CommandTimeoutTests.cs
+++ b/tests/IntegrationTests/CommandTimeoutTests.cs
@@ -46,7 +46,7 @@ public void LargeCommandTimeoutIsCoerced()
 		Assert.Equal(2_147_483, command.CommandTimeout);
 	}
 
-	[SkippableFact(ServerFeatures.CancelSleepSuccessfully)]
+	[Fact]
 	public void CommandTimeoutWithSleepSync()
 	{
 		var connectionState = m_connection.State;
@@ -59,11 +59,7 @@ public void CommandTimeoutWithSleepSync()
 			Assert.Contains("fatal error", ex.Message, StringComparison.OrdinalIgnoreCase);
 			connectionState = ConnectionState.Closed;
 #else
-			using (var reader = cmd.ExecuteReader())
-			{
-				Assert.True(reader.Read());
-				Assert.Equal(1, reader.GetInt32(0));
-			}
+			TestUtilities.AssertExecuteScalarReturnsOneOrTimesOut(cmd);
 #endif
 			sw.Stop();
 			TestUtilities.AssertDuration(sw, cmd.CommandTimeout * 1000 - 100, 500);
@@ -72,7 +68,7 @@ public void CommandTimeoutWithSleepSync()
 		Assert.Equal(connectionState, m_connection.State);
 	}
 
-	[SkippableFact(ServerFeatures.CancelSleepSuccessfully | ServerFeatures.Timeout)]
+	[SkippableFact(ServerFeatures.Timeout, MySqlData = "https://bugs.mysql.com/bug.php?id=110790")]
 	public async Task CommandTimeoutWithSleepAsync()
 	{
 		var connectionState = m_connection.State;
@@ -85,11 +81,7 @@ public async Task CommandTimeoutWithSleepAsync()
 			Assert.Contains("fatal error", exception.Message, StringComparison.OrdinalIgnoreCase);
 			connectionState = ConnectionState.Closed;
 #else
-			using (var reader = await cmd.ExecuteReaderAsync())
-			{
-				Assert.True(await reader.ReadAsync());
-				Assert.Equal(1, reader.GetInt32(0));
-			}
+			await TestUtilities.AssertExecuteScalarReturnsOneOrTimesOutAsync(cmd);
 #endif
 			sw.Stop();
 			TestUtilities.AssertDuration(sw, cmd.CommandTimeout * 1000 - 100, 700);
@@ -98,7 +90,7 @@ public async Task CommandTimeoutWithSleepAsync()
 		Assert.Equal(connectionState, m_connection.State);
 	}
 
-	[SkippableTheory(ServerFeatures.CancelSleepSuccessfully)]
+	[Theory]
 	[InlineData(true)]
 	[InlineData(false)]
 	public void CommandTimeoutWithStoredProcedureSleepSync(bool pooling)
@@ -126,11 +118,7 @@ create procedure sleep_sproc(IN seconds INT)
 		var ex = Assert.Throws<MySqlException>(cmd.ExecuteReader);
 		Assert.Contains("fatal error", ex.Message, StringComparison.OrdinalIgnoreCase);
 #else
-		using (var reader = cmd.ExecuteReader())
-		{
-			Assert.True(reader.Read());
-			Assert.Equal(1, reader.GetInt32(0));
-		}
+		TestUtilities.AssertExecuteScalarReturnsOneOrTimesOut(cmd);
 #endif
 		sw.Stop();
 		TestUtilities.AssertDuration(sw, cmd.CommandTimeout * 1000 - 100, 500);
@@ -170,7 +158,7 @@ public void MultipleCommandTimeoutWithSleepSync()
 		Assert.Equal(connectionState, m_connection.State);
 	}
 
-	[SkippableFact(ServerFeatures.CancelSleepSuccessfully | ServerFeatures.Timeout)]
+	[SkippableFact(ServerFeatures.CancelSleepSuccessfully | ServerFeatures.Timeout, MySqlData = "https://bugs.mysql.com/bug.php?id=110790")]
 	public async Task MultipleCommandTimeoutWithSleepAsync()
 	{
 		var connectionState = m_connection.State;
@@ -245,8 +233,7 @@ public async Task CommandTimeoutResetsOnReadAsync()
 		Assert.Equal(ConnectionState.Open, m_connection.State);
 	}
 
-
-	[SkippableFact(ServerFeatures.CancelSleepSuccessfully)]
+	[Fact]
 	public void TransactionCommandTimeoutWithSleepSync()
 	{
 		var connectionState = m_connection.State;
@@ -260,11 +247,7 @@ public void TransactionCommandTimeoutWithSleepSync()
 			Assert.Contains("fatal error", ex.Message, StringComparison.OrdinalIgnoreCase);
 			connectionState = ConnectionState.Closed;
 #else
-			using (var reader = cmd.ExecuteReader())
-			{
-				Assert.True(reader.Read());
-				Assert.Equal(1, reader.GetInt32(0));
-			}
+			TestUtilities.AssertExecuteScalarReturnsOneOrTimesOut(cmd);
 #endif
 			sw.Stop();
 			TestUtilities.AssertDuration(sw, cmd.CommandTimeout * 1000 - 100, 500);
@@ -273,7 +256,7 @@ public void TransactionCommandTimeoutWithSleepSync()
 		Assert.Equal(connectionState, m_connection.State);
 	}
 
-	[SkippableFact(ServerFeatures.CancelSleepSuccessfully)]
+	[SkippableFact(MySqlData = "https://bugs.mysql.com/bug.php?id=110790")]
 	public async Task TransactionCommandTimeoutWithSleepAsync()
 	{
 		var connectionState = m_connection.State;
@@ -287,11 +270,7 @@ public async Task TransactionCommandTimeoutWithSleepAsync()
 			Assert.Contains("fatal error", ex.Message, StringComparison.OrdinalIgnoreCase);
 			connectionState = ConnectionState.Closed;
 #else
-			using (var reader = await cmd.ExecuteReaderAsync())
-			{
-				Assert.True(await reader.ReadAsync());
-				Assert.Equal(1, reader.GetInt32(0));
-			}
+			await TestUtilities.AssertExecuteScalarReturnsOneOrTimesOutAsync(cmd);
 #endif
 			sw.Stop();
 		}
@@ -299,6 +278,6 @@ public async Task TransactionCommandTimeoutWithSleepAsync()
 		Assert.Equal(connectionState, m_connection.State);
 	}
 
-	readonly DatabaseFixture m_database;
-	readonly MySqlConnection m_connection;
+	private readonly DatabaseFixture m_database;
+	private readonly MySqlConnection m_connection;
 }
diff --git a/tests/IntegrationTests/ConfigSettings.cs b/tests/IntegrationTests/ConfigSettings.cs
index fea01b55b..7215e534b 100644
--- a/tests/IntegrationTests/ConfigSettings.cs
+++ b/tests/IntegrationTests/ConfigSettings.cs
@@ -16,5 +16,6 @@ public enum ConfigSettings
 	SecondaryDatabase = 0x400,
 	KnownClientCertificate = 0x800,
 	GSSAPIUser = 0x1000,
-	HasKerberos = 0x2000
+	HasKerberos = 0x2000,
+	UserHasPassword = 0x4000,
 }
diff --git a/tests/IntegrationTests/ConnectAsync.cs b/tests/IntegrationTests/ConnectAsync.cs
index 342e5697b..eb5a99b76 100644
--- a/tests/IntegrationTests/ConnectAsync.cs
+++ b/tests/IntegrationTests/ConnectAsync.cs
@@ -1,7 +1,4 @@
 using System.Security.Authentication;
-#if !MYSQL_DATA
-using MySqlConnector.Authentication.Ed25519;
-#endif
 
 namespace IntegrationTests;
 
@@ -51,18 +48,14 @@ public async Task ConnectBadPassword()
 		Assert.Equal(ConnectionState.Closed, connection.State);
 	}
 
-	[Fact]
+	[SkippableFact(MySqlData = "https://bugs.mysql.com/bug.php?id=110791")]
 	public async Task ConnectCanceled()
 	{
 		using var cts = new CancellationTokenSource();
 		cts.Cancel();
 
 		using var connection = new MySqlConnection(AppConfig.ConnectionString);
-#if MYSQL_DATA
-		await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await connection.OpenAsync(cts.Token));
-#else
 		await Assert.ThrowsAsync<OperationCanceledException>(async () => await connection.OpenAsync(cts.Token));
-#endif
 		await connection.OpenAsync();
 		Assert.Equal(ConnectionState.Open, connection.State);
 	}
@@ -135,7 +128,6 @@ public async Task ConnectTimeoutAsync()
 		TestUtilities.AssertDuration(stopwatch, 1900, 1500);
 	}
 
-
 	[SkippableFact(ServerFeatures.Timeout, MySqlData = "https://bugs.mysql.com/bug.php?id=94760")]
 	public async Task ConnectTimeoutAsyncCancellationToken()
 	{
@@ -186,7 +178,7 @@ public async Task UsePasswordProvider()
 		Assert.True(wasCalled);
 	}
 
-	[Fact]
+	[SkippableFact(ConfigSettings.UserHasPassword)]
 	public async Task UsePasswordProviderPasswordTakesPrecedence()
 	{
 		var csb = AppConfig.CreateConnectionStringBuilder();
@@ -196,7 +188,11 @@ public async Task UsePasswordProviderPasswordTakesPrecedence()
 		await MySqlConnection.ClearPoolAsync(connection);
 
 		var wasCalled = false;
-		connection.ProvidePasswordCallback = _ => { wasCalled = true; return password; };
+		connection.ProvidePasswordCallback = _ =>
+		{
+			wasCalled = true;
+			return password;
+		};
 
 		await connection.OpenAsync();
 		Assert.False(wasCalled);
@@ -430,7 +426,7 @@ public async Task CachingSha2WithoutSecureConnection()
 	[SkippableFact(ServerFeatures.Ed25519)]
 	public async Task Ed25519Authentication()
 	{
-		Ed25519AuthenticationPlugin.Install();
+		MySqlConnector.Authentication.Ed25519.Ed25519AuthenticationPlugin.Install();
 
 		var csb = AppConfig.CreateConnectionStringBuilder();
 		csb.UserID = "ed25519user";
@@ -439,6 +435,30 @@ public async Task Ed25519Authentication()
 		using var connection = new MySqlConnection(csb.ConnectionString);
 		await connection.OpenAsync();
 	}
+
+	[SkippableFact(ServerFeatures.Ed25519)]
+	public async Task MultiAuthentication()
+	{
+		MySqlConnector.Authentication.Ed25519.Ed25519AuthenticationPlugin.Install();
+		var csb = AppConfig.CreateConnectionStringBuilder();
+		csb.UserID = "multiAuthUser";
+		csb.Password = "secret";
+		csb.Database = null;
+		using var connection = new MySqlConnection(csb.ConnectionString);
+		await connection.OpenAsync();
+	}
+
+	[SkippableFact(ServerFeatures.ParsecAuthentication)]
+	public async Task Parsec()
+	{
+		MySqlConnector.Authentication.Ed25519.ParsecAuthenticationPlugin.Install();
+		var csb = AppConfig.CreateConnectionStringBuilder();
+		csb.UserID = "parsec-user";
+		csb.Password = "P@rs3c-Pa55";
+		csb.Database = null;
+		using var connection = new MySqlConnection(csb.ConnectionString);
+		await connection.OpenAsync();
+	}
 #endif
 
 	// To create a MariaDB GSSAPI user for a current user
@@ -466,6 +486,7 @@ public async Task GoodServerSPN()
 	{
 		var csb = AppConfig.CreateGSSAPIConnectionStringBuilder();
 		string serverSPN;
+
 		// Use server's variable gssapi_principal_name as SPN
 		using (var connection = new MySqlConnection(csb.ConnectionString))
 		{
@@ -524,7 +545,7 @@ public async Task UnixDomainSocket()
 		Assert.Equal(ConnectionState.Open, connection.State);
 	}
 
-	[Fact]
+	[SkippableFact(MySqlData = "Not raised")]
 	public async Task DisposeAsyncRaisesDisposed()
 	{
 		var disposedCount = 0;
@@ -535,13 +556,5 @@ public async Task DisposeAsyncRaisesDisposed()
 		Assert.Equal(1, disposedCount);
 	}
 
-	readonly DatabaseFixture m_database;
+	private readonly DatabaseFixture m_database;
 }
-
-#if MYSQL_DATA
-internal static class MySqlDataConnectionHelpers
-{
-	// MySql.Data capitalizes the 'B' in 'Database'
-	public static Task ChangeDatabaseAsync(this MySqlConnection connection, string databaseName) => connection.ChangeDataBaseAsync(databaseName);
-}
-#endif
diff --git a/tests/IntegrationTests/ConnectSync.cs b/tests/IntegrationTests/ConnectSync.cs
index 5b09ef908..3137d9c0c 100644
--- a/tests/IntegrationTests/ConnectSync.cs
+++ b/tests/IntegrationTests/ConnectSync.cs
@@ -60,10 +60,8 @@ public void ConnectBadDatabase()
 		csb.Database = "wrong_database";
 		using var connection = new MySqlConnection(csb.ConnectionString);
 		var ex = Assert.Throws<MySqlException>(connection.Open);
-#if !MYSQL_DATA // https://bugs.mysql.com/bug.php?id=78426
-		if (AppConfig.SupportedFeatures.HasFlag(ServerFeatures.ErrorCodes) || ex.ErrorCode != default)
-			Assert.Equal(MySqlErrorCode.UnknownDatabase, ex.ErrorCode);
-#endif
+		if (AppConfig.SupportedFeatures.HasFlag(ServerFeatures.ErrorCodes) || ex.Number != 0)
+			Assert.Equal((int) MySqlErrorCode.UnknownDatabase, ex.Number);
 		Assert.Equal(ConnectionState.Closed, connection.State);
 	}
 
@@ -74,7 +72,7 @@ public void ConnectBadPassword()
 		csb.Password = "wrong";
 		using var connection = new MySqlConnection(csb.ConnectionString);
 		var ex = Assert.Throws<MySqlException>(connection.Open);
-#if !MYSQL_DATA // https://bugs.mysql.com/bug.php?id=78426
+#if !MYSQL_DATA
 		if (AppConfig.SupportedFeatures.HasFlag(ServerFeatures.ErrorCodes) || ex.ErrorCode != default)
 			Assert.Equal(MySqlErrorCode.AccessDenied, ex.ErrorCode);
 #endif
@@ -100,7 +98,7 @@ public void NonExistentPipe()
 			PipeName = "nonexistingpipe",
 			ConnectionProtocol = MySqlConnectionProtocol.NamedPipe,
 			Server = ".",
-			ConnectionTimeout = 1
+			ConnectionTimeout = 1,
 		};
 
 		var sw = Stopwatch.StartNew();
@@ -257,7 +255,7 @@ public void UsePasswordProvider()
 		Assert.True(wasCalled);
 	}
 
-	[Fact]
+	[SkippableFact(ConfigSettings.UserHasPassword)]
 	public void UsePasswordProviderPasswordTakesPrecedence()
 	{
 		var csb = AppConfig.CreateConnectionStringBuilder();
@@ -267,7 +265,11 @@ public void UsePasswordProviderPasswordTakesPrecedence()
 		MySqlConnection.ClearPool(connection);
 
 		var wasCalled = false;
-		connection.ProvidePasswordCallback = _ => { wasCalled = true; return password; };
+		connection.ProvidePasswordCallback = _ =>
+		{
+			wasCalled = true;
+			return password;
+		};
 
 		connection.Open();
 		Assert.False(wasCalled);
@@ -600,5 +602,5 @@ public void DisposeRaisesDisposed()
 		Assert.Equal(1, disposedCount);
 	}
 
-	readonly DatabaseFixture m_database;
+	private readonly DatabaseFixture m_database;
 }
diff --git a/tests/IntegrationTests/ConnectionPool.cs b/tests/IntegrationTests/ConnectionPool.cs
index 27af0d2ba..330582f58 100644
--- a/tests/IntegrationTests/ConnectionPool.cs
+++ b/tests/IntegrationTests/ConnectionPool.cs
@@ -76,7 +76,7 @@ public async Task ExhaustConnectionPool()
 			Assert.InRange(stopwatch.ElapsedMilliseconds, 4500, 7500);
 		}
 
-		closeTask.Wait();
+		await closeTask.ConfigureAwait(true);
 
 		foreach (var connection in connections)
 			connection.Dispose();
@@ -313,5 +313,5 @@ private Task ClearPoolAsync(MySqlConnection connection)
 	}
 
 	private static HashSet<long> GetConnectionIds(IEnumerable<MySqlConnection> connections)
-		=> new HashSet<long>(connections.Select(x => (long) x.ServerThread));
+		=> new(connections.Select(x => (long) x.ServerThread));
 }
diff --git a/tests/IntegrationTests/ConnectionTests.cs b/tests/IntegrationTests/ConnectionTests.cs
index 1b9336333..0f8941f36 100644
--- a/tests/IntegrationTests/ConnectionTests.cs
+++ b/tests/IntegrationTests/ConnectionTests.cs
@@ -188,7 +188,7 @@ public void CloneIsClosed()
 		Assert.Equal(ConnectionState.Closed, connection2.State);
 	}
 
-	[SkippableFact(MySqlData = "https://bugs.mysql.com/bug.php?id=97473")]
+	[Fact]
 	public void CloneDoesNotDisclosePassword()
 	{
 		using var connection = new MySqlConnection(AppConfig.ConnectionString);
@@ -218,8 +218,10 @@ public void CloneWithUsesExistingPassword()
 		var newConnectionString = "user=root;server=example.com;database=test";
 		using var connection2 = connection.CloneWith(newConnectionString);
 
-		var builder = new MySqlConnectionStringBuilder(newConnectionString);
-		builder.Password = AppConfig.CreateConnectionStringBuilder().Password;
+		var builder = new MySqlConnectionStringBuilder(newConnectionString)
+		{
+			Password = AppConfig.CreateConnectionStringBuilder().Password,
+		};
 		Assert.Equal(builder.ConnectionString, connection2.ConnectionString);
 	}
 
@@ -249,8 +251,10 @@ public void CloneWithDoesDiscloseExistingPasswordIfPersistSecurityInfo(bool pers
 		var newConnectionString = "user=root;server=example.com;database=test;Persist Security Info=" + persistSecurityInfo;
 		using var connection2 = connection.CloneWith(newConnectionString);
 
-		var builder = new MySqlConnectionStringBuilder(newConnectionString);
-		builder.Password = AppConfig.CreateConnectionStringBuilder().Password;
+		var builder = new MySqlConnectionStringBuilder(newConnectionString)
+		{
+			Password = AppConfig.CreateConnectionStringBuilder().Password,
+		};
 		Assert.Equal(builder.ConnectionString, connection2.ConnectionString);
 	}
 
diff --git a/tests/IntegrationTests/DataAdapterTests.cs b/tests/IntegrationTests/DataAdapterTests.cs
index 6f60955ba..7cee96197 100644
--- a/tests/IntegrationTests/DataAdapterTests.cs
+++ b/tests/IntegrationTests/DataAdapterTests.cs
@@ -205,7 +205,6 @@ public void BatchUpdate()
 		Assert.Equal(new[] { "two", "three", "four" }, m_connection.Query<string>("SELECT text_value FROM data_adapter ORDER BY id"));
 	}
 
-
 	[Fact]
 	public void BatchInsert()
 	{
@@ -341,5 +340,5 @@ public void ConvertBatchToCommandParameters()
 	}
 #endif
 
-	readonly MySqlConnection m_connection;
+	private readonly MySqlConnection m_connection;
 }
diff --git a/tests/IntegrationTests/DataTypes.cs b/tests/IntegrationTests/DataTypes.cs
index cee22e809..c105cf7ff 100644
--- a/tests/IntegrationTests/DataTypes.cs
+++ b/tests/IntegrationTests/DataTypes.cs
@@ -1,5 +1,6 @@
 using System.Globalization;
-using System.Reflection;
+using System.Runtime.InteropServices;
+
 #if MYSQL_DATA
 using MySql.Data.Types;
 #endif
@@ -9,17 +10,17 @@
 // However, DbDataReader.GetString etc. are documented as throwing InvalidCastException: https://msdn.microsoft.com/en-us/library/system.data.common.dbdatareader.getstring.aspx
 // Additionally, that is what DbDataReader.GetFieldValue<T> throws. For consistency, we prefer InvalidCastException.
 #if MYSQL_DATA
-using GetValueWhenNullException = System.Data.SqlTypes.SqlNullValueException;
-using GetGuidWhenNullException = MySql.Data.MySqlClient.MySqlException;
 using GetBytesWhenNullException = System.NullReferenceException;
 using GetGeometryWhenNullException = System.Exception;
+using GetGuidWhenNullException = MySql.Data.MySqlClient.MySqlException;
 using GetStreamWhenNullException = System.ArgumentNullException;
+using GetValueWhenNullException = System.Data.SqlTypes.SqlNullValueException;
 #else
-using GetValueWhenNullException = System.InvalidCastException;
-using GetGuidWhenNullException = System.InvalidCastException;
 using GetBytesWhenNullException = System.InvalidCastException;
 using GetGeometryWhenNullException = System.InvalidCastException;
+using GetGuidWhenNullException = System.InvalidCastException;
 using GetStreamWhenNullException = System.InvalidCastException;
+using GetValueWhenNullException = System.InvalidCastException;
 #endif
 
 namespace IntegrationTests;
@@ -156,19 +157,19 @@ private async Task DoGetValue<T>(string column, Func<MySqlDataReader, int, T> ge
 			Assert.True(await reader.ReadAsync().ConfigureAwait(false));
 			switch (flags[i])
 			{
-			case 0: // normal
-				Assert.Equal(values[i], getInt(reader, 0));
-				Assert.Equal(values[i], getIntByName(reader, column));
-				break;
-
-			case 1: // null
-				Assert.True(await reader.IsDBNullAsync(0).ConfigureAwait(false));
-				break;
-
-			case 2: // overflow
-				Assert.Throws<OverflowException>(() => getInt(reader, 0));
-				Assert.Throws<OverflowException>(() => getIntByName(reader, column));
-				break;
+				case 0: // normal
+					Assert.Equal(values[i], getInt(reader, 0));
+					Assert.Equal(values[i], getIntByName(reader, column));
+					break;
+
+				case 1: // null
+					Assert.True(await reader.IsDBNullAsync(0).ConfigureAwait(false));
+					break;
+
+				case 2: // overflow
+					Assert.Throws<OverflowException>(() => getInt(reader, 0));
+					Assert.Throws<OverflowException>(() => getIntByName(reader, column));
+					break;
 			}
 		}
 		Assert.False(await reader.ReadAsync().ConfigureAwait(false));
@@ -222,7 +223,7 @@ public void QueryTinyIntSbyte(string column, string dataTypeName, object[] expec
 		DoQuery("bools", column, dataTypeName, expected, reader => reader.GetSByte(0), mySqlDataCoercedNullValue: default(sbyte), connection: connection);
 	}
 
-	[Theory()]
+	[Theory]
 	[InlineData("TinyInt1U", "TINYINT", new object[] { null, (byte) 0, (byte) 1, (byte) 0, (byte) 1, (byte) 255, (byte) 123 })]
 	public void QueryTinyInt1Unsigned(string column, string dataTypeName, object[] expected)
 	{
@@ -314,8 +315,8 @@ public void QueryDouble(string column, string dataTypeName, object[] expected)
 	[Theory]
 	[InlineData("SmallDecimal", new object[] { null, "0", "-999.99", "-0.01", "999.99", "0.01" })]
 	[InlineData("MediumDecimal", new object[] { null, "0", "-999999999999.99999999", "-0.00000001", "999999999999.99999999", "0.00000001" })]
-	// value exceeds the range of a decimal and cannot be deserialized
-	// [InlineData("BigDecimal", new object[] { null, "0", "-99999999999999999999.999999999999999999999999999999", "-0.000000000000000000000000000001", "99999999999999999999.999999999999999999999999999999", "0.000000000000000000000000000001" })]
+	//// value exceeds the range of a decimal and cannot be deserialized
+	//// [InlineData("BigDecimal", new object[] { null, "0", "-99999999999999999999.999999999999999999999999999999", "-0.000000000000000000000000000001", "99999999999999999999.999999999999999999999999999999", "0.000000000000000000000000000001" })]
 	public void QueryDecimal(string column, object[] expected)
 	{
 		for (int i = 0; i < expected.Length; i++)
@@ -330,6 +331,8 @@ public void QueryDecimal(string column, object[] expected)
 	[InlineData("latin1", new[] { null, "", "ASCII", "Lãtïñ", c_251ByteString })]
 	[InlineData("latin1bin", new[] { null, "", "ASCII", "Lãtïñ", c_251ByteString })]
 	[InlineData("cp1251", new[] { null, "", "ASCII", "АБВГабвг", c_251ByteString })]
+	[InlineData("nonguid_utf8", new[] { null, "", "ASCII", "Ũńıċōđĕ", "This string has 36 characters in it." })]
+	[InlineData("nonguid_latin1", new[] { null, "", "ASCII", "Lãtïñ", "This string has 36 characters in it." })]
 	public void QueryString(string column, string[] expected)
 	{
 		DoQuery("strings", column, "VARCHAR", expected, reader => reader.GetString(0));
@@ -341,7 +344,7 @@ public void QueryString(string column, string[] expected)
 		}, getFieldValueType: typeof(TextReader));
 #endif
 	}
-	const string c_251ByteString = "This string has exactly 251 characters in it. The encoded length is stored as 0xFC 0xFB 0x00. 0xFB (i.e., 251) is the sentinel byte indicating \"this field is null\". Incorrectly interpreting the (decoded) length as the sentinel byte would corrupt data.";
+	private const string c_251ByteString = "This string has exactly 251 characters in it. The encoded length is stored as 0xFC 0xFB 0x00. 0xFB (i.e., 251) is the sentinel byte indicating \"this field is null\". Incorrectly interpreting the (decoded) length as the sentinel byte would corrupt data.";
 
 	[Theory]
 	[InlineData("guid", "CHAR(36)", new object[] { null, "00000000-0000-0000-0000-000000000000", "00000000-0000-0000-c000-000000000046", "fd24a0e8-c3f2-4821-a456-35da2dc4bb8f", "6A0E0A40-6228-11D3-A996-0050041896C8" })]
@@ -449,7 +452,7 @@ public async Task QueryWithGuidParameter(bool oldGuids)
 			Assert.Equal(oldGuids ? 0L : 1L, (await connection.QueryAsync<long>(@"select count(*) from datatypes_strings where guid = @guid", new { guid = new Guid("fd24a0e8-c3f2-4821-a456-35da2dc4bb8f") }).ConfigureAwait(false)).SingleOrDefault());
 			Assert.Equal(oldGuids ? 0L : 1L, (await connection.QueryAsync<long>(@"select count(*) from datatypes_strings where guidbin = @guid", new { guid = new Guid("fd24a0e8-c3f2-4821-a456-35da2dc4bb8f") }).ConfigureAwait(false)).SingleOrDefault());
 		}
-		catch (MySqlException ex) when (oldGuids && ex.Number is 1300 or 3854) // InvalidCharacterString, CannotConvertString
+		catch (MySqlException ex) when (oldGuids && ex.Number is 1300 or 3854) //// InvalidCharacterString, CannotConvertString
 		{
 			// new error in MySQL 8.0.24, MariaDB 10.5
 		}
@@ -563,14 +566,14 @@ insert into guid_format(c36, c32, b16, tsb16, leb16, t, b) values(
 				new() { Value = guidAsLittleEndianBinary16 },
 				new() { Value = guidAsChar36 },
 				new() { Value = isBinary16 ? guidAsBinary16 : isTimeSwapBinary16 ? guidAsTimeSwapBinary16 : guidAsLittleEndianBinary16 },
-				new() { Value = isChar36 ? (object) guid : guidAsChar36 },
-				new() { Value = isChar32 ? (object) guid : guidAsChar32 },
-				new() { Value = isBinary16 ? (object) guid : guidAsBinary16 },
-				new() { Value = isTimeSwapBinary16 ? (object) guid : guidAsTimeSwapBinary16 },
-				new() { Value = isLittleEndianBinary16 ? (object) guid : guidAsLittleEndianBinary16 },
+				new() { Value = isChar36 ? guid : guidAsChar36 },
+				new() { Value = isChar32 ? guid : guidAsChar32 },
+				new() { Value = isBinary16 ? guid : guidAsBinary16 },
+				new() { Value = isTimeSwapBinary16 ? guid : guidAsTimeSwapBinary16 },
+				new() { Value = isLittleEndianBinary16 ? guid : guidAsLittleEndianBinary16 },
 				new() { Value = guidAsChar32 },
 				new() { Value = isBinary16 ? guidAsBinary16 : isTimeSwapBinary16 ? guidAsTimeSwapBinary16 : guidAsLittleEndianBinary16 },
-			}
+			},
 		};
 		cmd.ExecuteNonQuery();
 		cmd.CommandText = "select c36, c32, b16, tsb16, leb16, t, b from guid_format;";
@@ -670,7 +673,7 @@ public void QueryZeroDateTime(bool convertZeroDateTime)
 		using var cmd = connection.CreateCommand();
 		cmd.CommandText = @"select cast(0 as date), cast(0 as datetime);";
 
-		using var reader = (MySqlDataReader) cmd.ExecuteReader();
+		using var reader = cmd.ExecuteReader();
 		Assert.True(reader.Read());
 		if (convertZeroDateTime)
 		{
@@ -737,7 +740,7 @@ insert into date_time_kind(d, dt0, dt1, dt2, dt3, dt4, dt5, dt6) values(?, ?, ?,
 				new() { Value = dateTimeIn },
 				new() { Value = dateTimeIn },
 				new() { Value = dateTimeIn },
-			}
+			},
 		};
 		if (success)
 		{
@@ -1070,6 +1073,7 @@ public void QueryGeometry(string columnName, string dataTypeName, byte[] expecte
 			expected,
 		};
 
+#pragma warning disable SA1111 // Closing parenthesis should be on line of last parameter
 		DoQuery("geometry", columnName, dataTypeName, geometryData.ToArray(),
 #if !MYSQL_DATA
 			GetBytes
@@ -1092,6 +1096,7 @@ public void QueryGeometry(string columnName, string dataTypeName, byte[] expecte
 			assertEqual: (x, y) => Assert.Equal(((MySqlGeometry) x)?.Value.ToArray(), ((MySqlGeometry) y)?.Value.ToArray())
 #endif
 			);
+#pragma warning restore SA1111 // Closing parenthesis should be on line of last parameter
 	}
 
 	private static object CreateGeometry(byte[] data)
@@ -1142,6 +1147,11 @@ private static object CreateGeometry(byte[] data)
 	[InlineData("Int64", "datatypes_integers", MySqlDbType.Int64, 20, typeof(long), "N", 0, 0)]
 	[InlineData("UInt64", "datatypes_integers", MySqlDbType.UInt64, 20, typeof(ulong), "N", 0, 0)]
 	[InlineData("value", "datatypes_json_core", MySqlDbType.JSON, int.MaxValue, typeof(string), "LN", 0, 0)]
+#if MYSQL_DATA
+	[InlineData("value", "datatypes_vector", MySqlDbType.Vector, 12, typeof(byte[]), "N", 0, 31)]
+#else
+	[InlineData("value", "datatypes_vector", MySqlDbType.Vector, 3, typeof(ReadOnlyMemory<float>), "N", 0, 31)]
+#endif
 	[InlineData("Single", "datatypes_reals", MySqlDbType.Float, 12, typeof(float), "N", 0, 31)]
 	[InlineData("Double", "datatypes_reals", MySqlDbType.Double, 22, typeof(double), "N", 0, 31)]
 	[InlineData("SmallDecimal", "datatypes_reals", MySqlDbType.NewDecimal, 7, typeof(decimal), "N", 5, 2)]
@@ -1194,6 +1204,17 @@ private void DoGetSchemaTable(string column, string table, MySqlDbType mySqlDbTy
 	{
 		if (table == "datatypes_json_core" && !AppConfig.SupportsJson)
 			return;
+		if (table == "datatypes_vector" && !AppConfig.SupportedFeatures.HasFlag(ServerFeatures.Vector))
+			return;
+
+		// adjust for databases that don't have a dedicated on-the-wire type for VECTOR(n)
+		if (mySqlDbType == MySqlDbType.Vector && !AppConfig.SupportedFeatures.HasFlag(ServerFeatures.VectorType))
+		{
+			mySqlDbType = MySqlDbType.VarBinary;
+			columnSize *= 4;
+			dataType = typeof(byte[]);
+			scale = 0;
+		}
 
 		var isAutoIncrement = flags.IndexOf('A') != -1;
 		var isKey = flags.IndexOf('K') != -1;
@@ -1327,6 +1348,8 @@ public void GetSchemaTableAfterNextResult()
 	[InlineData("cp1251", "datatypes_strings", MySqlDbType.VarChar, "VARCHAR", 300, typeof(string), "N", -1, 0)]
 	[InlineData("guid", "datatypes_strings", MySqlDbType.Guid, "CHAR(36)", 36, typeof(Guid), "N", -1, 0)]
 	[InlineData("guidbin", "datatypes_strings", MySqlDbType.Guid, "CHAR(36)", 36, typeof(Guid), "N", -1, 0)]
+	[InlineData("nonguid_utf8", "datatypes_strings", MySqlDbType.VarChar, "VARCHAR", 36, typeof(string), "N", -1, 0)]
+	[InlineData("nonguid_latin1", "datatypes_strings", MySqlDbType.VarChar, "VARCHAR", 36, typeof(string), "N", -1, 0)]
 	[InlineData("Date", "datatypes_times", MySqlDbType.Date, "DATE", 10, typeof(DateTime), "N", -1, 0)]
 	[InlineData("DateTime", "datatypes_times", MySqlDbType.DateTime, "DATETIME", 26, typeof(DateTime), "N", -1, 6)]
 	[InlineData("Timestamp", "datatypes_times", MySqlDbType.Timestamp, "TIMESTAMP", 26, typeof(DateTime), "N", -1, 6)]
@@ -1419,6 +1442,8 @@ public void GetColumnSchema(string column, string table, MySqlDbType mySqlDbType
 	[InlineData("BigDecimal", "datatypes_reals", MySqlDbType.NewDecimal, "DECIMAL(50,30)", typeof(decimal), 3, null)]
 	[InlineData("utf8", "datatypes_strings", MySqlDbType.VarChar, "VARCHAR(300)", typeof(string), 3, "ASCII")]
 	[InlineData("latin1", "datatypes_strings", MySqlDbType.VarChar, "VARCHAR(300)", typeof(string), 3, "ASCII")]
+	[InlineData("nonguid_utf8", "datatypes_strings", MySqlDbType.VarChar, "VARCHAR(36)", typeof(string), 3, "ASCII")]
+	[InlineData("nonguid_latin1", "datatypes_strings", MySqlDbType.VarChar, "VARCHAR(36)", typeof(string), 3, "ASCII")]
 	[InlineData("Date", "datatypes_times", MySqlDbType.Date, "DATE", typeof(DateTime), 2, null)]
 	[InlineData("DateTime", "datatypes_times", MySqlDbType.DateTime, "DATETIME", typeof(DateTime), 2, null)]
 	[InlineData("Timestamp", "datatypes_times", MySqlDbType.Timestamp, "TIMESTAMP", typeof(DateTime), 2, null)]
@@ -1514,7 +1539,7 @@ public void StoredProcedureOutParameter(string column, string table, MySqlDbType
 	[InlineData("DateTime", "datatypes_times", "DATETIME(6)")]
 	[InlineData("Timestamp", "datatypes_times", "TIMESTAMP(6)")]
 	[InlineData("Time", "datatypes_times", "TIME(6)")]
-	// [InlineData("Year", "datatypes_times", "YEAR")]
+	//// [InlineData("Year", "datatypes_times", "YEAR")]
 	[InlineData("value", "datatypes_json_core", "JSON")]
 	[InlineData("Geometry", "datatypes_geometry", "GEOMETRY")]
 	public void BulkCopyDataReader(string column, string table, string dataTypeName)
@@ -1598,6 +1623,43 @@ public void QueryJson(string column, string[] expected)
 		DoQuery("json_core", column, dataTypeName, expected, reader => reader.GetString(0), omitWhereTest: true);
 	}
 
+	[SkippableTheory(ServerFeatures.Vector)]
+	[InlineData("value", new[] { null, "0,0,0", "1,1,1", "1,2,3", "-1,-1,-1" })]
+	public void QueryVector(string column, string[] expected)
+	{
+		var hasVectorType = AppConfig.SupportedFeatures.HasFlag(ServerFeatures.VectorType);
+		string dataTypeName = hasVectorType ? "VECTOR" : "BLOB";
+		DoQuery("vector", column, dataTypeName,
+			expected.Select(x =>
+#if !MYSQL_DATA
+				hasVectorType ? (GetFloatArray(x) is float[] a ? (object) new ReadOnlyMemory<float>(a) : null) : GetByteArray(x))
+#else
+				// Connector/NET returns the float array as a byte[]
+				GetByteArray(x))
+#endif
+			.ToArray(),
+#if !MYSQL_DATA
+			x => hasVectorType ? (ReadOnlyMemory<float>) x.GetValue(0) : (byte[]) x.GetValue(0),
+#else
+			// NOTE: Connector/NET returns 'null' for NULL so simulate an exception for the tests
+			x => x.IsDBNull(0) ? throw new GetValueWhenNullException() : x.GetValue(0),
+#endif
+			assertEqual: (l, r) =>
+			{
+				if (l is ReadOnlyMemory<float> roml)
+					l = roml.ToArray();
+				if (r is ReadOnlyMemory<float> romr)
+					r = romr.ToArray();
+				Assert.Equal(l, r);
+			},
+			omitWhereTest: true);
+
+		static float[] GetFloatArray(string value) => value?.Split(',').Select(x => float.Parse(x, CultureInfo.InvariantCulture)).ToArray();
+
+		static byte[] GetByteArray(string value) =>
+			GetFloatArray(value) is { } floats ? MemoryMarshal.AsBytes<float>(floats).ToArray() : null;
+	}
+
 	[SkippableTheory(MySqlData = "https://bugs.mysql.com/bug.php?id=97067")]
 	[InlineData(false, "MIN", 0)]
 	[InlineData(false, "MAX", uint.MaxValue)]
@@ -1847,6 +1909,5 @@ private static int[] SplitAndParse(object obj)
 
 	private MySqlConnection Connection { get; }
 
-	private MySqlConnectionStringBuilder CreateConnectionStringBuilder() => new MySqlConnectionStringBuilder(AppConfig.ConnectionString);
+	private MySqlConnectionStringBuilder CreateConnectionStringBuilder() => new(AppConfig.ConnectionString);
 }
-
diff --git a/tests/IntegrationTests/DataTypesFixture.cs b/tests/IntegrationTests/DataTypesFixture.cs
index f9ad28a38..de346a0f1 100644
--- a/tests/IntegrationTests/DataTypesFixture.cs
+++ b/tests/IntegrationTests/DataTypesFixture.cs
@@ -121,21 +121,23 @@ latin1 varchar(300) character set 'latin1' null,
   latin1bin varchar(300) character set latin1 collate latin1_bin null,
   cp1251 varchar(300) character set 'cp1251' null,
   guid char(36) null,
-  guidbin char(36) binary null
+  guidbin char(36) binary null,
+  nonguid_utf8 varchar(36) character set 'utf8mb4' null,
+  nonguid_latin1 varchar(36) character set 'latin1' null
 );
 
-insert into datatypes_strings(utf8, utf8bin, latin1, latin1bin, cp1251, guid, guidbin)
+insert into datatypes_strings(utf8, utf8bin, latin1, latin1bin, cp1251, guid, guidbin, nonguid_utf8, nonguid_latin1)
 values
-  (null, null, null, null, null, null, null),
-  ('', '', '', '', '', '00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000000'),
-  ('ASCII', 'ASCII', 'ASCII', 'ASCII', 'ASCII', '00000000-0000-0000-c000-000000000046', '00000000-0000-0000-c000-000000000046'),
-  ('Ũńıċōđĕ', 'Ũńıċōđĕ', 'Lãtïñ', 'Lãtïñ', 'АБВГабвг', 'fd24a0e8-c3f2-4821-a456-35da2dc4bb8f', 'fd24a0e8-c3f2-4821-a456-35da2dc4bb8f'),
+  (null, null, null, null, null, null, null, null, null),
+  ('', '', '', '', '', '00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000000', '', ''),
+  ('ASCII', 'ASCII', 'ASCII', 'ASCII', 'ASCII', '00000000-0000-0000-c000-000000000046', '00000000-0000-0000-c000-000000000046', 'ASCII', 'ASCII'),
+  ('Ũńıċōđĕ', 'Ũńıċōđĕ', 'Lãtïñ', 'Lãtïñ', 'АБВГабвг', 'fd24a0e8-c3f2-4821-a456-35da2dc4bb8f', 'fd24a0e8-c3f2-4821-a456-35da2dc4bb8f', 'Ũńıċōđĕ', 'Lãtïñ'),
   ('This string has exactly 251 characters in it. The encoded length is stored as 0xFC 0xFB 0x00. 0xFB (i.e., 251) is the sentinel byte indicating ""this field is null"". Incorrectly interpreting the (decoded) length as the sentinel byte would corrupt data.',
    'This string has exactly 251 characters in it. The encoded length is stored as 0xFC 0xFB 0x00. 0xFB (i.e., 251) is the sentinel byte indicating ""this field is null"". Incorrectly interpreting the (decoded) length as the sentinel byte would corrupt data.',
    'This string has exactly 251 characters in it. The encoded length is stored as 0xFC 0xFB 0x00. 0xFB (i.e., 251) is the sentinel byte indicating ""this field is null"". Incorrectly interpreting the (decoded) length as the sentinel byte would corrupt data.',
    'This string has exactly 251 characters in it. The encoded length is stored as 0xFC 0xFB 0x00. 0xFB (i.e., 251) is the sentinel byte indicating ""this field is null"". Incorrectly interpreting the (decoded) length as the sentinel byte would corrupt data.',
    'This string has exactly 251 characters in it. The encoded length is stored as 0xFC 0xFB 0x00. 0xFB (i.e., 251) is the sentinel byte indicating ""this field is null"". Incorrectly interpreting the (decoded) length as the sentinel byte would corrupt data.',
-   '6a0e0a40-6228-11d3-a996-0050041896c8', '6a0e0a40-6228-11d3-a996-0050041896c8');
+   '6a0e0a40-6228-11d3-a996-0050041896c8', '6a0e0a40-6228-11d3-a996-0050041896c8', 'This string has 36 characters in it.', 'This string has 36 characters in it.');
 
 drop table if exists datatypes_blobs;
 create table datatypes_blobs(
@@ -242,6 +244,28 @@ insert into datatypes_json_core (value)
   ('{""a"": ""b""}');
 ");
 		}
+
+		if (AppConfig.SupportedFeatures.HasFlag(ServerFeatures.Vector))
+		{
+			// create a helper function for MariaDB 11.7+
+			if (Connection.ServerVersion.StartsWith("11.8.", StringComparison.Ordinal))
+				Connection.Execute("create function if not exists STRING_TO_VECTOR(s text) returns vector(3) deterministic return Vec_FromText(s);");
+
+			Connection.Execute("""
+				drop table if exists datatypes_vector;
+				create table datatypes_vector (
+					rowid integer not null primary key auto_increment,
+					value vector(3) null
+				);
+				insert into datatypes_vector (value)
+				values
+					(null),
+					(STRING_TO_VECTOR('[0, 0, 0]')),
+					(STRING_TO_VECTOR('[1, 1, 1]')),
+					(STRING_TO_VECTOR('[1, 2, 3]')),
+					(STRING_TO_VECTOR('[-1, -1, -1]'));
+				""");
+		}
 		Connection.Close();
 	}
 }
diff --git a/tests/IntegrationTests/DatabaseFixture.cs b/tests/IntegrationTests/DatabaseFixture.cs
index eb53da67c..857e460af 100644
--- a/tests/IntegrationTests/DatabaseFixture.cs
+++ b/tests/IntegrationTests/DatabaseFixture.cs
@@ -53,6 +53,6 @@ protected virtual void Dispose(bool disposing)
 		}
 	}
 
-	static object s_lock = new object();
-	static bool s_isInitialized;
+	private static readonly object s_lock = new();
+	private static bool s_isInitialized;
 }
diff --git a/tests/IntegrationTests/InsertTests.cs b/tests/IntegrationTests/InsertTests.cs
index 15b4366fb..63191eacd 100644
--- a/tests/IntegrationTests/InsertTests.cs
+++ b/tests/IntegrationTests/InsertTests.cs
@@ -1,3 +1,4 @@
+using System.Globalization;
 using System.Numerics;
 #if MYSQL_DATA
 using MySql.Data.Types;
@@ -133,7 +134,7 @@ public async Task LastInsertedIdTwoInserts()
 		}
 	}
 
-	[SkippableFact(MySqlData = "https://bugs.mysql.com/bug.php?id=97061")]
+	[Fact]
 	public async Task LastInsertedIdLockTables()
 	{
 		await m_database.Connection.ExecuteAsync(@"drop table if exists insert_ai;
@@ -154,7 +155,7 @@ public async Task LastInsertedIdLockTables()
 		}
 	}
 
-	[SkippableFact(MySqlData = "https://bugs.mysql.com/bug.php?id=97061")]
+	[Fact]
 	public async Task LastInsertedIdInsertForeignKey()
 	{
 		await m_database.Connection.ExecuteAsync(@"drop table if exists TestTableWithForeignKey;
@@ -294,7 +295,7 @@ public void InsertDateTimeOffset()
 	{
 		m_database.Connection.Execute(@"drop table if exists insert_datetimeoffset;
 create table insert_datetimeoffset(rowid integer not null primary key auto_increment, datetimeoffset1 datetime null);");
-		var value = new DateTimeOffsetValues { datetimeoffset1 = new DateTimeOffset(2017, 1, 2, 3, 4, 5, TimeSpan.FromMinutes(678)) };
+		var value = new DateTimeOffsetValues { DateTimeOffset1 = new DateTimeOffset(2017, 1, 2, 3, 4, 5, TimeSpan.FromMinutes(678)) };
 
 		m_database.Connection.Open();
 		try
@@ -305,7 +306,7 @@ public void InsertDateTimeOffset()
 			{
 				ParameterName = "@datetimeoffset1",
 				DbType = DbType.DateTimeOffset,
-				Value = value.datetimeoffset1
+				Value = value.DateTimeOffset1,
 			});
 			Assert.Equal(1, cmd.ExecuteNonQuery());
 		}
@@ -318,7 +319,7 @@ public void InsertDateTimeOffset()
 
 		DateTime.SpecifyKind(datetime, DateTimeKind.Utc);
 
-		Assert.Equal(value.datetimeoffset1.Value.UtcDateTime, datetime);
+		Assert.Equal(value.DateTimeOffset1.Value.UtcDateTime, datetime);
 	}
 
 	[SkippableFact(MySqlData = "https://bugs.mysql.com/bug.php?id=91199")]
@@ -326,7 +327,7 @@ public void InsertMySqlDateTime()
 	{
 		m_database.Connection.Execute(@"drop table if exists insert_mysqldatetime;
 create table insert_mysqldatetime(rowid integer not null primary key auto_increment, ts timestamp(6) null);");
-		var value = new DateTimeOffsetValues { datetimeoffset1 = new DateTimeOffset(2017, 1, 2, 3, 4, 5, TimeSpan.FromMinutes(678)) };
+		var value = new DateTimeOffsetValues { DateTimeOffset1 = new DateTimeOffset(2017, 1, 2, 3, 4, 5, TimeSpan.FromMinutes(678)) };
 
 		m_database.Connection.Open();
 		try
@@ -387,12 +388,7 @@ public void InsertStringBuilder(bool prepare)
 
 		var value = new StringBuilder("\aAB\\12'ab\\'\\'");
 		for (var i = 0; i < 100; i++)
-#if !NETCOREAPP3_1
 			value.Append("\U0001F600\uD800\'\U0001F601\uD800");
-#else
-			// the netcoreapp3.1 implementation is broken when handling an unpaired surrogate; probably https://github.com/dotnet/runtime/issues/33817
-			value.Append("\U0001F600\U0001F601");
-#endif
 
 		using var cmd = connection.CreateCommand();
 		cmd.CommandText = @"insert into insert_string_builder(str) values(@str);";
@@ -476,9 +472,133 @@ public void InsertMySqlDecimalAsDecimal(bool prepare)
 
 		using var reader = connection.ExecuteReader(@"select value from insert_mysql_decimal order by rowid;");
 		Assert.True(reader.Read());
-		var val = reader.GetValue(0).ToString();
+		var val = ((decimal) reader.GetValue(0)).ToString(CultureInfo.InvariantCulture);
 		Assert.Equal(value, val);
 	}
+
+	[Theory]
+	[InlineData(1_000_000, 1024, true)]
+	[InlineData(1_000_000, 1024, false)]
+	[InlineData(1_000_000, int.MaxValue, true)]
+	[InlineData(1_000_000, int.MaxValue, false)]
+	[InlineData(0xff_fff8, 299593, true)]
+	[InlineData(0xff_fff8, 299593, false)]
+	[InlineData(0xff_fff8, 300000, true)]
+	[InlineData(0xff_fff8, 300000, false)]
+	[InlineData(0xff_fff8, int.MaxValue, true)]
+	[InlineData(0xff_fff8, int.MaxValue, false)]
+	[InlineData(0xff_fff9, int.MaxValue, true)]
+	[InlineData(0xff_fff9, int.MaxValue, false)]
+	[InlineData(0x1ff_fff0, 299593, true)]
+	[InlineData(0x1ff_fff0, 299593, false)]
+	[InlineData(0x1ff_fff0, 300000, true)]
+	[InlineData(0x1ff_fff0, 300000, false)]
+	[InlineData(15_999_999, int.MaxValue, true)]
+	[InlineData(15_999_999, int.MaxValue, false)]
+	[InlineData(16_000_000, int.MaxValue, true)]
+	[InlineData(16_000_000, int.MaxValue, false)]
+	[InlineData(16_000_001, int.MaxValue, true)]
+	[InlineData(16_000_001, int.MaxValue, false)]
+	[InlineData(31_999_999, 999_999, true)]
+	[InlineData(31_999_999, 1_000_000, false)]
+	[InlineData(32_000_000, 1_000_001, true)]
+	[InlineData(32_000_000, 1_000_002, false)]
+	[InlineData(32_000_001, 1_000_003, true)]
+	[InlineData(32_000_001, 1_000_004, false)]
+	public async Task SendLongData(int dataLength, int chunkLength, bool isAsync)
+	{
+		using MySqlConnection connection = new MySqlConnection(AppConfig.ConnectionString);
+		connection.Open();
+		connection.Execute("""
+			drop table if exists insert_mysql_long_data;
+			create table insert_mysql_long_data(rowid integer not null primary key auto_increment, value longblob);
+			""");
+
+		var random = new Random(dataLength);
+		var data = new byte[dataLength];
+		random.NextBytes(data);
+
+		using var chunkStream = new ChunkStream(data, chunkLength);
+
+		using var writeCommand = new MySqlCommand("""
+			insert into insert_mysql_long_data(value) values(@value);
+			select length(value) from insert_mysql_long_data order by rowid;
+			""", connection);
+		writeCommand.Parameters.AddWithValue("@value", chunkStream);
+		writeCommand.Prepare();
+		using (var reader = isAsync ? await writeCommand.ExecuteReaderAsync().ConfigureAwait(true) : writeCommand.ExecuteReader())
+		{
+			Assert.True(reader.Read());
+			Assert.Equal(1, reader.FieldCount);
+			Assert.Equal(dataLength, reader.GetInt32(0));
+			Assert.False(reader.Read());
+		}
+
+		using var readCommand = new MySqlCommand("select value from insert_mysql_long_data order by rowid;", connection);
+		using (var reader = readCommand.ExecuteReader())
+		{
+			Assert.True(reader.Read());
+			var readData = (byte[]) reader.GetValue(0);
+			Assert.True(data.AsSpan().SequenceEqual(readData)); // much faster than Assert.Equal
+		}
+	}
+
+	[Theory]
+	[InlineData(false)]
+	[InlineData(true)]
+	public async Task SendLongDataMultipleStatements(bool isAsync)
+	{
+		using MySqlConnection connection = new MySqlConnection(AppConfig.ConnectionString);
+		connection.Open();
+		connection.Execute("""
+			drop table if exists insert_mysql_long_data;
+			create table insert_mysql_long_data(rowid integer not null primary key auto_increment, value longblob);
+			""");
+
+		var data1 = new byte[1000];
+		var data2 = new byte[2000];
+		var data3 = new byte[3000];
+		var random = new Random(1);
+		random.NextBytes(data1);
+		random.NextBytes(data2);
+		random.NextBytes(data3);
+
+		using var chunkStream1 = new ChunkStream(data1, int.MaxValue);
+		using var chunkStream2 = new ChunkStream(data2, int.MaxValue);
+		using var chunkStream3 = new ChunkStream(data3, int.MaxValue);
+
+		using var writeCommand = new MySqlCommand("""
+			insert into insert_mysql_long_data(rowid, value) values(1, @value1);
+			insert into insert_mysql_long_data(rowid, value) values(2, @value2);
+			insert into insert_mysql_long_data(rowid, value) values(3, @value3);
+			""", connection);
+		writeCommand.Parameters.AddWithValue("@value1", chunkStream1);
+		writeCommand.Parameters.AddWithValue("@value2", chunkStream2);
+		writeCommand.Parameters.AddWithValue("@value3", chunkStream3);
+		writeCommand.Prepare();
+		if (isAsync)
+			await writeCommand.ExecuteNonQueryAsync();
+		else
+			writeCommand.ExecuteNonQuery();
+
+		using var readCommand = new MySqlCommand("select value from insert_mysql_long_data order by rowid;", connection);
+		using (var reader = readCommand.ExecuteReader())
+		{
+			Assert.True(reader.Read());
+			var readData = (byte[]) reader.GetValue(0);
+			Assert.True(data1.AsSpan().SequenceEqual(readData));
+
+			Assert.True(reader.Read());
+			readData = (byte[]) reader.GetValue(0);
+			Assert.True(data2.AsSpan().SequenceEqual(readData));
+
+			Assert.True(reader.Read());
+			readData = (byte[]) reader.GetValue(0);
+			Assert.True(data3.AsSpan().SequenceEqual(readData));
+
+			Assert.False(reader.Read());
+		}
+	}
 #endif
 
 	[Theory]
@@ -518,6 +638,40 @@ public void ReadMySqlDecimalUsingReader(bool prepare)
 #endif
 	}
 
+	[Theory]
+	[InlineData(false)]
+	[InlineData(true)]
+	public void ReadMySqlDecimalZeroFill(bool prepare)
+	{
+		using MySqlConnection connection = new MySqlConnection(AppConfig.ConnectionString);
+		connection.Open();
+		connection.Execute("""
+			drop table if exists mysql_decimal_zerofill;
+			create table mysql_decimal_zerofill(rowid integer not null primary key auto_increment, value decimal(20, 10) zerofill);
+			insert into mysql_decimal_zerofill(value) values(0),(1),(0.1);
+			""");
+
+		using var cmd = connection.CreateCommand();
+		cmd.CommandText = @"select value from mysql_decimal_zerofill order by rowid;";
+		if (prepare)
+			cmd.Prepare();
+		using var reader = cmd.ExecuteReader();
+
+		Assert.True(reader.Read());
+		Assert.Equal("0000000000.0000000000", reader.GetMySqlDecimal("value").ToString());
+		Assert.Equal(0m, reader.GetDecimal(0));
+
+		Assert.True(reader.Read());
+		Assert.Equal("0000000001.0000000000", reader.GetMySqlDecimal("value").ToString());
+		Assert.Equal(1m, reader.GetDecimal(0));
+
+		Assert.True(reader.Read());
+		Assert.Equal("0000000000.1000000000", reader.GetMySqlDecimal("value").ToString());
+		Assert.Equal(0.1m, reader.GetDecimal(0));
+
+		Assert.False(reader.Read());
+	}
+
 	[Theory]
 	[InlineData(false)]
 	[InlineData(true)]
@@ -613,40 +767,39 @@ public async Task EnumParametersAreParsedCorrectly()
 		{
 			m_database.Connection.Close();
 		}
-
 	}
 
-	enum Enum16 : short
+	private enum Enum16 : short
 	{
 		Off,
 		On,
 	}
 
-	enum Enum32 : int
+	private enum Enum32 : int
 	{
 		Off,
 		On,
 	}
 
-	enum Enum64 : long
+	private enum Enum64 : long
 	{
 		Off,
 		On,
 	}
 
-	class DateTimeOffsetValues
+	private class DateTimeOffsetValues
 	{
-		public DateTimeOffset? datetimeoffset1 { get; set; }
+		public DateTimeOffset? DateTimeOffset1 { get; set; }
 	}
 
-	class ColorEnumValues
+	private class ColorEnumValues
 	{
 		public string Varchar { get; set; }
 		public string String { get; set; }
 		public int Int { get; set; }
 	}
 
-	class EnumValues
+	private class EnumValues
 	{
 		public Enum16? Enum16 { get; set; }
 		public Enum32? Enum32 { get; set; }
@@ -667,17 +820,17 @@ color enum('red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet') not n
 		Assert.Equal(new[] { "blue" }, m_database.Connection.Query<string>(@"select color from insert_mysql_enums"));
 	}
 
-	enum MySqlSize
+	private enum MySqlSize
 	{
 		None,
 		XSmall,
 		Small,
 		Medium,
 		Large,
-		XLarge
+		XLarge,
 	}
 
-	enum MySqlColor
+	private enum MySqlColor
 	{
 		None,
 		Red,
@@ -686,7 +839,7 @@ enum MySqlColor
 		Green,
 		Blue,
 		Indigo,
-		Violet
+		Violet,
 	}
 
 	[Fact]
@@ -727,7 +880,6 @@ public static IEnumerable<object[]> GetChars() =>
 		new[] { '\0', 'a', '\'', '\"', '\\', 'A', '\b', '\n', '\r', '\t', '\x1A' }
 			.SelectMany(x => new[] { false, true }.Select(y => new object[] { x, y }));
 
-
 #if !MYSQL_DATA
 	[Theory]
 	[MemberData(nameof(GetBlobs))]
@@ -748,7 +900,7 @@ value mediumblob null
 				cmd.Prepare();
 			cmd.ExecuteNonQuery();
 		}
-		Assert.Equal(new byte[] { 1, 0, 2, 39, 3, 92, 4, 34, 5, 6  }, connection.Query<byte[]>(@"select value from insert_mysql_blob;").Single());
+		Assert.Equal(new byte[] { 1, 0, 2, 39, 3, 92, 4, 34, 5, 6 }, connection.Query<byte[]>(@"select value from insert_mysql_blob;").Single());
 	}
 
 	public static IEnumerable<object[]> GetBlobs()
@@ -767,5 +919,5 @@ public static IEnumerable<object[]> GetBlobs()
 	}
 #endif
 
-	readonly DatabaseFixture m_database;
+	private readonly DatabaseFixture m_database;
 }
diff --git a/tests/IntegrationTests/IntegrationTests.csproj b/tests/IntegrationTests/IntegrationTests.csproj
index 19516d32a..bb388f662 100644
--- a/tests/IntegrationTests/IntegrationTests.csproj
+++ b/tests/IntegrationTests/IntegrationTests.csproj
@@ -1,13 +1,14 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup Condition=" '$(Configuration)' != 'MySqlData' ">
-    <TargetFrameworks>net462;net472;netcoreapp3.1;net6.0;net7.0</TargetFrameworks>
+    <TargetFrameworks>net462;net481;net6.0;net8.0;net9.0</TargetFrameworks>
     <CheckEolTargetFramework>false</CheckEolTargetFramework>
   </PropertyGroup>
 
   <PropertyGroup Condition=" '$(Configuration)' == 'MySqlData' ">
-    <TargetFrameworks>net6.0</TargetFrameworks>
+    <TargetFrameworks>net9.0</TargetFrameworks>
     <DefineConstants>MYSQL_DATA</DefineConstants>
+    <NoWarn>$(NoWarn);MSB3246</NoWarn>
   </PropertyGroup>
 
   <PropertyGroup>
@@ -16,25 +17,31 @@
     <PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
     <AssemblyOriginatorKeyFile>..\..\MySqlConnector.snk</AssemblyOriginatorKeyFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+    <SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
     <PreserveCompilationContext>true</PreserveCompilationContext>
     <AssemblyName>IntegrationTests</AssemblyName>
     <ServerGarbageCollection>true</ServerGarbageCollection>
     <ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
     <ThreadPoolMinThreads>64</ThreadPoolMinThreads>
-    <LangVersion>preview</LangVersion>
+    <LangVersion>11.0</LangVersion>
     <ImplicitUsings>enable</ImplicitUsings>
+    <NoWarn>$(NoWarn);SA0001;SA1021;SA1133;xUnit1030</NoWarn>
   </PropertyGroup>
 
   <ItemGroup>
     <!--testing packages-->
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
-    <PackageReference Include="xunit" Version="2.4.2" />
-    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" />
+    <PackageReference Include="xunit" />
+    <PackageReference Include="xunit.runner.visualstudio">
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      <PrivateAssets>all</PrivateAssets>
+    </PackageReference>
     <!--app packages-->
-    <PackageReference Include="Dapper.StrongName" Version="2.0.123" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.1" />
-    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
+    <PackageReference Include="Dapper.StrongName" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Json" />
+    <PackageReference Include="System.Text.Json" />
     <Using Include="System" />
     <Using Include="System.Data" />
     <Using Include="System.Data.Common" />
@@ -51,11 +58,11 @@
   </ItemGroup>
 
   <ItemGroup Condition=" '$(Configuration)' == 'MySqlData' ">
-    <PackageReference Include="MySql.Data" Version="8.0.31" />
+    <PackageReference Include="MySql.Data" />
     <Using Include="MySql.Data.MySqlClient" />
   </ItemGroup>
 
-  <ItemGroup Condition=" '$(TargetFramework)' == 'net462' OR '$(TargetFramework)' == 'net472' ">
+  <ItemGroup Condition=" '$(TargetFramework)' == 'net462' OR '$(TargetFramework)' == 'net481' ">
     <Reference Include="System" />
     <Reference Include="System.Net.Http" />
     <Reference Include="System.Transactions" />
diff --git a/tests/IntegrationTests/LoadDataInfileAsync.cs b/tests/IntegrationTests/LoadDataInfileAsync.cs
index 52bd089f0..e14f761bd 100644
--- a/tests/IntegrationTests/LoadDataInfileAsync.cs
+++ b/tests/IntegrationTests/LoadDataInfileAsync.cs
@@ -30,7 +30,8 @@ public async Task CommandLoadCsvFile()
 	{
 		var insertInlineCommand = string.Format(m_loadDataInfileCommand, "", AppConfig.MySqlBulkLoaderCsvFile.Replace("\\", "\\\\"));
 		using var command = new MySqlCommand(insertInlineCommand, m_database.Connection);
-		if (m_database.Connection.State != ConnectionState.Open) await m_database.Connection.OpenAsync();
+		if (m_database.Connection.State != ConnectionState.Open)
+			await m_database.Connection.OpenAsync();
 		var rowCount = await command.ExecuteNonQueryAsync();
 		m_database.Connection.Close();
 		Assert.Equal(20, rowCount);
@@ -65,7 +66,7 @@ public async Task ThrowsNotSupportedExceptionForNotTrustedHostAndNotStream()
 		await Assert.ThrowsAsync<MySqlException>(async () => await command.ExecuteNonQueryAsync());
 	}
 
-	readonly DatabaseFixture m_database;
-	readonly string m_testTable;
-	readonly string m_loadDataInfileCommand;
+	private readonly DatabaseFixture m_database;
+	private readonly string m_testTable;
+	private readonly string m_loadDataInfileCommand;
 }
diff --git a/tests/IntegrationTests/LoadDataInfileSync.cs b/tests/IntegrationTests/LoadDataInfileSync.cs
index 2eee12ffe..2d75556db 100644
--- a/tests/IntegrationTests/LoadDataInfileSync.cs
+++ b/tests/IntegrationTests/LoadDataInfileSync.cs
@@ -30,7 +30,8 @@ public void CommandLoadCsvFile()
 	{
 		var insertInlineCommand = string.Format(m_loadDataInfileCommand, "", AppConfig.MySqlBulkLoaderCsvFile.Replace("\\", "\\\\"));
 		using var command = new MySqlCommand(insertInlineCommand, m_database.Connection);
-		if (m_database.Connection.State != ConnectionState.Open) m_database.Connection.Open();
+		if (m_database.Connection.State != ConnectionState.Open)
+			m_database.Connection.Open();
 		var rowCount = command.ExecuteNonQuery();
 		m_database.Connection.Close();
 		Assert.Equal(20, rowCount);
@@ -41,7 +42,8 @@ public void CommandLoadLocalCsvFile()
 	{
 		var insertInlineCommand = string.Format(m_loadDataInfileCommand, " LOCAL", AppConfig.MySqlBulkLoaderLocalCsvFile.Replace("\\", "\\\\"));
 		using var command = new MySqlCommand(insertInlineCommand, m_database.Connection);
-		if (m_database.Connection.State != ConnectionState.Open) m_database.Connection.Open();
+		if (m_database.Connection.State != ConnectionState.Open)
+			m_database.Connection.Open();
 		var rowCount = command.ExecuteNonQuery();
 		m_database.Connection.Close();
 		Assert.Equal(20, rowCount);
@@ -61,7 +63,7 @@ public void ThrowsNotSupportedExceptionForNotTrustedHostAndNotStream()
 		m_database.Connection.Close();
 	}
 
-	readonly DatabaseFixture m_database;
-	readonly string m_testTable;
-	readonly string m_loadDataInfileCommand;
+	private readonly DatabaseFixture m_database;
+	private readonly string m_testTable;
+	private readonly string m_loadDataInfileCommand;
 }
diff --git a/tests/IntegrationTests/MySqlDataSourceTests.cs b/tests/IntegrationTests/MySqlDataSourceTests.cs
index c8c8bfd76..0d8f9a72f 100644
--- a/tests/IntegrationTests/MySqlDataSourceTests.cs
+++ b/tests/IntegrationTests/MySqlDataSourceTests.cs
@@ -1,16 +1,9 @@
 #if !MYSQL_DATA
-#if NET7_0_OR_GREATER
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
 namespace IntegrationTests;
 
 public class MySqlDataSourceTests : IClassFixture<DatabaseFixture>
 {
-	public MySqlDataSourceTests(DatabaseFixture _)
+	public MySqlDataSourceTests(DatabaseFixture ignoredFixture)
 	{
 	}
 
@@ -32,6 +25,17 @@ public void OpenConnection()
 		Assert.Equal(ConnectionState.Open, connection.State);
 	}
 
+	[Fact]
+	public void DoubleDispose()
+	{
+		using var dbSource = new MySqlDataSource(AppConfig.ConnectionString);
+		using (var connection = dbSource.OpenConnection())
+		{
+			Assert.Equal(ConnectionState.Open, connection.State);
+		}
+		dbSource.Dispose();
+	}
+
 	[Fact]
 	public async Task OpenConnectionAsync()
 	{
@@ -57,6 +61,22 @@ public void OpenConnectionReusesConnection()
 		}
 	}
 
+	[Fact]
+	public void CloneReusesDataSource()
+	{
+		using var dbSource = new MySqlDataSource(AppConfig.ConnectionString);
+
+		using var connection1 = dbSource.CreateConnection();
+		using var connection2 = connection1.Clone();
+
+		connection1.Open();
+		var serverThread = connection1.ServerThread;
+		connection1.Close();
+
+		connection2.Open();
+		Assert.Equal(serverThread, connection2.ServerThread);
+	}
+
 	[Fact]
 	public void MultipleDataSourcesHaveDifferentPools()
 	{
@@ -101,6 +121,115 @@ public void CreateFromDbFactory()
 		Assert.IsType<MySqlDataSource>(dbSource);
 		Assert.Equal(AppConfig.ConnectionString, dbSource.ConnectionString);
 	}
+
+	[Fact]
+	public void CreateFromDataSourceBuilder()
+	{
+		var connectionString = AppConfig.CreateConnectionStringBuilder().ConnectionString;
+		var builder = new MySqlDataSourceBuilder(connectionString);
+		using var dataSource = builder.Build();
+		Assert.Equal(connectionString, dataSource.ConnectionString);
+		using var connection = dataSource.OpenConnection();
+		Assert.Equal(ConnectionState.Open, connection.State);
+	}
+
+	[SkippableTheory(ServerFeatures.KnownCertificateAuthority, ConfigSettings.RequiresSsl)]
+	[InlineData(MySqlSslMode.VerifyCA, false, false)]
+	[InlineData(MySqlSslMode.VerifyCA, true, false)]
+	[InlineData(MySqlSslMode.Required, true, true)]
+	public async Task ConnectSslRemoteCertificateValidationCallback(MySqlSslMode sslMode, bool clearCA, bool expectedSuccess)
+	{
+		var builder = new MySqlDataSourceBuilder(AppConfig.ConnectionString)
+			.UseRemoteCertificateValidationCallback((s, c, h, e) => true);
+		builder.ConnectionStringBuilder.CertificateFile = Path.Combine(AppConfig.CertsPath, "ssl-client.pfx");
+		builder.ConnectionStringBuilder.SslMode = sslMode;
+		builder.ConnectionStringBuilder.SslCa = clearCA ? "" : Path.Combine(AppConfig.CertsPath, "non-ca-client-cert.pem");
+
+		using var dataSource = builder.Build();
+		using var connection = dataSource.CreateConnection();
+		if (expectedSuccess || AppConfig.SupportedFeatures.HasFlag(ServerFeatures.TlsFingerprintValidation))
+			await connection.OpenAsync();
+		else
+			await Assert.ThrowsAsync<MySqlException>(connection.OpenAsync);
+	}
+
+	[Fact]
+	public void PasswordPropertyIsUsed()
+	{
+		var csb = AppConfig.CreateConnectionStringBuilder();
+		var password = csb.Password;
+		csb.Password = "";
+
+		using var dataSource = new MySqlDataSource(csb.ConnectionString)
+		{
+			Password = password,
+		};
+		using var connection = dataSource.OpenConnection();
+		Assert.Equal(ConnectionState.Open, connection.State);
+	}
+
+	[Fact]
+	public void PasswordProviderCallbackIsInvoked()
+	{
+		var csb = AppConfig.CreateConnectionStringBuilder();
+		var password = csb.Password;
+		csb.Password = "";
+
+		using var dataSource = new MySqlDataSourceBuilder(csb.ConnectionString)
+			.UsePeriodicPasswordProvider((_, _) => new ValueTask<string>(password), TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(1))
+			.Build();
+		using var connection = dataSource.OpenConnection();
+		Assert.Equal(ConnectionState.Open, connection.State);
+	}
+
+	[Fact]
+	public void PasswordProviderCallbackIsWaitedFor()
+	{
+		var csb = AppConfig.CreateConnectionStringBuilder();
+		var password = csb.Password;
+		csb.Password = "";
+
+		using var dataSource = new MySqlDataSourceBuilder(csb.ConnectionString)
+			.UsePeriodicPasswordProvider(async (_, _) =>
+			{
+				await Task.Delay(TimeSpan.FromSeconds(0.5));
+				return password;
+			}, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(1))
+			.Build();
+		using var connection = dataSource.OpenConnection();
+		Assert.Equal(ConnectionState.Open, connection.State);
+	}
+
+	[Fact]
+	public void PasswordProviderFailureIsRetried()
+	{
+		var csb = AppConfig.CreateConnectionStringBuilder();
+		var password = csb.Password;
+		csb.Password = "";
+
+		using var barrier = new Barrier(2);
+
+		var count = 0;
+		using var dataSource = new MySqlDataSourceBuilder(csb.ConnectionString)
+			.UsePeriodicPasswordProvider((_, _) =>
+			{
+				if (count++ == 0)
+					throw new ApplicationException("First-time failure");
+				barrier.SignalAndWait();
+				return new ValueTask<string>(password);
+			}, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(0.5))
+			.Build();
+
+		// throws the first time
+		var exception = Assert.Throws<MySqlException>(dataSource.OpenConnection); // Failed to obtain password via ProvidePasswordCallback
+		Assert.IsType<MySqlException>(exception.InnerException); // The periodic password provider failed
+		Assert.IsType<ApplicationException>(exception.InnerException.InnerException); // First-time failure
+
+		// succeeds after failure retry
+		barrier.SignalAndWait();
+		Thread.Sleep(10);
+		using var connection = dataSource.OpenConnection();
+		Assert.Equal(ConnectionState.Open, connection.State);
+	}
 }
 #endif
-#endif
diff --git a/tests/IntegrationTests/ParameterCollection.cs b/tests/IntegrationTests/ParameterCollection.cs
index 62227b5d7..c19206465 100644
--- a/tests/IntegrationTests/ParameterCollection.cs
+++ b/tests/IntegrationTests/ParameterCollection.cs
@@ -85,8 +85,8 @@ public void FindByNameIgnoringCase(string parameterName)
 	[Theory]
 	[InlineData("@test")]
 	[InlineData("@TEST")]
-#if !MYSQL_DATA
-	[InlineData("test")] // https://bugs.mysql.com/bug.php?id=93370
+	[InlineData("test")]
+#if !MYSQL_DATA // doesn't support quoted parameter names
 	[InlineData("@'test'")]
 	[InlineData("@`TEST`")]
 #endif
@@ -299,6 +299,6 @@ public void SetTwoParametersToSameNAme()
 #endif
 	}
 
-	MySqlCommand m_command;
-	MySqlParameterCollection m_parameterCollection;
+	private readonly MySqlCommand m_command;
+	private readonly MySqlParameterCollection m_parameterCollection;
 }
diff --git a/tests/IntegrationTests/PreparedCommandTests.cs b/tests/IntegrationTests/PreparedCommandTests.cs
index 7f25508f2..e61ac87f0 100644
--- a/tests/IntegrationTests/PreparedCommandTests.cs
+++ b/tests/IntegrationTests/PreparedCommandTests.cs
@@ -373,12 +373,7 @@ public static IEnumerable<object[]> GetInsertAndQueryData()
 			yield return new object[] { isPrepared, "TINYINT UNSIGNED", (byte) 123, MySqlDbType.UByte };
 			yield return new object[] { isPrepared, "SMALLINT", (short) -12345, MySqlDbType.Int16 };
 			yield return new object[] { isPrepared, "SMALLINT UNSIGNED", (ushort) 12345, MySqlDbType.UInt16 };
-#if !MYSQL_DATA
 			yield return new object[] { isPrepared, "MEDIUMINT", -1234567, MySqlDbType.Int24 };
-#else
-			// https://bugs.mysql.com/bug.php?id=108756
-			yield return new object[] { isPrepared, "MEDIUMINT", -1234567, MySqlDbType.Int32 };
-#endif
 			yield return new object[] { isPrepared, "MEDIUMINT UNSIGNED", 1234567u, MySqlDbType.UInt24 };
 			yield return new object[] { isPrepared, "INT", -123456789, MySqlDbType.Int32 };
 			yield return new object[] { isPrepared, "INT UNSIGNED", 123456789u, MySqlDbType.UInt32 };
@@ -397,10 +392,6 @@ public static IEnumerable<object[]> GetInsertAndQueryData()
 			yield return new object[] { isPrepared, "DATE", new DateTime(2018, 7, 23), MySqlDbType.Date };
 			yield return new object[] { isPrepared, "DATETIME(3)", new DateTime(2018, 7, 23, 20, 46, 52, 123), MySqlDbType.DateTime };
 			yield return new object[] { isPrepared, "ENUM('small', 'medium', 'large')", "medium", MySqlDbType.Enum };
-#if !MYSQL_DATA
-			// https://bugs.mysql.com/bug.php?id=106247
-			yield return new object[] { isPrepared, "SET('one','two','four','eight')", "two,eight", MySqlDbType.Set };
-#endif
 #if !MYSQL_DATA
 			yield return new object[] { isPrepared, "BOOL", true, MySqlDbType.Bool };
 #else
@@ -442,6 +433,62 @@ public static IEnumerable<object[]> GetDifferentTypeInsertAndQueryData()
 		}
 	}
 
+	[Theory, MemberData(nameof(GetDifferentWidthData))]
+	public void InsertDifferentWidthParameters(string dataType1, object value1, string dataType2, object value2)
+	{
+		using var connection = CreateConnection();
+		using var command = connection.CreateCommand();
+		command.CommandText = $"""
+			drop table if exists parameter_width;
+			create table parameter_width(value1 {dataType1} not null, value2 {dataType2} not null);
+			""";
+		command.ExecuteNonQuery();
+
+		command.CommandText = "insert into parameter_width(value1, value2) values(@value1, @value2);";
+		command.Parameters.AddWithValue("@value1", value1);
+		command.Parameters.AddWithValue("@value2", value2);
+		command.Prepare();
+		command.ExecuteNonQuery();
+
+		using var queryCommand = connection.CreateCommand();
+		queryCommand.CommandText = "select value1, value2 from parameter_width;";
+		using var reader = queryCommand.ExecuteReader();
+		Assert.True(reader.Read());
+		Assert.Equal(Convert.ToInt32(value1), reader.GetInt32(0));
+		Assert.Equal(Convert.ToInt32(value2), reader.GetInt32(1));
+		Assert.False(reader.Read());
+	}
+
+	public static IEnumerable<object[]> GetDifferentWidthData()
+	{
+		var dataTypes = new string[] { "TINYINT", "SMALLINT", "INT" };
+		var values = new object[] { (sbyte) 100, (short) 110, 120, OneByteEnum.Value, TwoByteEnum.Value };
+
+		foreach (var dataType1 in dataTypes)
+		{
+			foreach (var value1 in values)
+			{
+				foreach (var dataType2 in dataTypes)
+				{
+					foreach (var value2 in values)
+					{
+						yield return new object[] { dataType1, value1, dataType2, value2 };
+					}
+				}
+			}
+		}
+	}
+
+	private enum OneByteEnum : sbyte
+	{
+		Value = 101,
+	}
+
+	private enum TwoByteEnum : short
+	{
+		Value = 111,
+	}
+
 	private static MySqlConnection CreateConnection()
 	{
 		var connection = new MySqlConnection(AppConfig.ConnectionString);
diff --git a/tests/IntegrationTests/QueryTests.cs b/tests/IntegrationTests/QueryTests.cs
index 022c524c2..eae6175da 100644
--- a/tests/IntegrationTests/QueryTests.cs
+++ b/tests/IntegrationTests/QueryTests.cs
@@ -1,3 +1,5 @@
+using System.Runtime.InteropServices;
+
 namespace IntegrationTests;
 
 public class QueryTests : IClassFixture<DatabaseFixture>, IDisposable
@@ -19,8 +21,10 @@ public void Dispose()
 	[InlineData(false, false, true)]
 	public void Bug1096(bool sprocBeforeInsert, bool sprocAfterInsert, bool sprocTwiceBeforeInsert)
 	{
-		var csb = new MySqlConnectionStringBuilder(AppConfig.ConnectionString);
-		csb.UseAffectedRows = true;
+		var csb = new MySqlConnectionStringBuilder(AppConfig.ConnectionString)
+		{
+			UseAffectedRows = true,
+		};
 		using var connection = new MySqlConnection(csb.ConnectionString);
 		connection.Open();
 
@@ -122,7 +126,7 @@ public void GetOrdinalAfterNextResult()
 	public void GetOrdinalBeforeAndAfterRead()
 	{
 		using var cmd = m_database.Connection.CreateCommand();
-		cmd.CommandText = "select 0 as zero, 1 as one;";
+		cmd.CommandText = "select 0 as zero, 1 as one; select 2 as two;";
 
 		using var reader = cmd.ExecuteReader();
 		Assert.Equal(1, reader.GetOrdinal("one"));
@@ -130,6 +134,13 @@ public void GetOrdinalBeforeAndAfterRead()
 		Assert.Equal(1, reader.GetOrdinal("one"));
 		Assert.False(reader.Read());
 		Assert.Equal(1, reader.GetOrdinal("one"));
+
+		Assert.True(reader.NextResult());
+		Assert.Equal(0, reader.GetOrdinal("two"));
+		Assert.True(reader.Read());
+		Assert.Equal(0, reader.GetOrdinal("two"));
+		Assert.False(reader.Read());
+		Assert.Equal(0, reader.GetOrdinal("two"));
 	}
 
 	[Fact]
@@ -727,6 +738,44 @@ public void UseReaderWithoutDisposing()
 			throw ex;
 	}
 
+	[Fact]
+	public void GetNameAfterNextResult()
+	{
+		using var cmd = m_database.Connection.CreateCommand();
+		cmd.CommandText = "select 1 as a;";
+		using var reader = cmd.ExecuteReader();
+		Assert.False(reader.NextResult());
+#if MYSQL_DATA
+		Assert.Throws<IndexOutOfRangeException>(() => reader.GetName(0));
+#else
+		Assert.Throws<InvalidOperationException>(() => reader.GetName(0));
+#endif
+	}
+
+	[Fact]
+	public void GetNameBeforeAndAfterRead()
+	{
+		using var cmd = m_database.Connection.CreateCommand();
+		cmd.CommandText = "select '1' as a; select 1.0 as b;";
+		using var reader = cmd.ExecuteReader();
+		Assert.Equal("a", reader.GetName(0));
+
+		Assert.True(reader.Read());
+		Assert.Equal("a", reader.GetName(0));
+
+		Assert.False(reader.Read());
+		Assert.Equal("a", reader.GetName(0));
+
+		Assert.True(reader.NextResult());
+		Assert.Equal("b", reader.GetName(0));
+
+		Assert.True(reader.Read());
+		Assert.Equal("b", reader.GetName(0));
+
+		Assert.False(reader.Read());
+		Assert.Equal("b", reader.GetName(0));
+	}
+
 	[Theory]
 #if MYSQL_DATA
 	[InlineData("null", typeof(string))]
@@ -778,6 +827,30 @@ public void GetFieldTypeAfterNextResult()
 #endif
 	}
 
+	[Fact]
+	public void GetFieldTypeBeforeAndAfterRead()
+	{
+		using var cmd = m_database.Connection.CreateCommand();
+		cmd.CommandText = "select '1' as a; select 1.0 as b;";
+		using var reader = cmd.ExecuteReader();
+		Assert.Equal(typeof(string), reader.GetFieldType(0));
+
+		Assert.True(reader.Read());
+		Assert.Equal(typeof(string), reader.GetFieldType(0));
+
+		Assert.False(reader.Read());
+		Assert.Equal(typeof(string), reader.GetFieldType(0));
+
+		Assert.True(reader.NextResult());
+		Assert.Equal(typeof(decimal), reader.GetFieldType(0));
+
+		Assert.True(reader.Read());
+		Assert.Equal(typeof(decimal), reader.GetFieldType(0));
+
+		Assert.False(reader.Read());
+		Assert.Equal(typeof(decimal), reader.GetFieldType(0));
+	}
+
 	[Theory]
 #if MYSQL_DATA
 	[InlineData("null", "VARCHAR")]
@@ -828,6 +901,30 @@ public void GetDataTypeNameAfterNextResult()
 #endif
 	}
 
+	[Fact]
+	public void GetDataTypeNameBeforeAndAfterRead()
+	{
+		using var cmd = m_database.Connection.CreateCommand();
+		cmd.CommandText = "select '1' as a; select 1.0 as b;";
+		using var reader = cmd.ExecuteReader();
+		Assert.Equal("VARCHAR", reader.GetDataTypeName(0));
+
+		Assert.True(reader.Read());
+		Assert.Equal("VARCHAR", reader.GetDataTypeName(0));
+
+		Assert.False(reader.Read());
+		Assert.Equal("VARCHAR", reader.GetDataTypeName(0));
+
+		Assert.True(reader.NextResult());
+		Assert.Equal("DECIMAL", reader.GetDataTypeName(0));
+
+		Assert.True(reader.Read());
+		Assert.Equal("DECIMAL", reader.GetDataTypeName(0));
+
+		Assert.False(reader.Read());
+		Assert.Equal("DECIMAL", reader.GetDataTypeName(0));
+	}
+
 #if !MYSQL_DATA
 	[Fact]
 	public void GetColumnSchemaAfterNextResult()
@@ -838,6 +935,48 @@ public void GetColumnSchemaAfterNextResult()
 		Assert.False(reader.NextResult());
 		Assert.Empty(reader.GetColumnSchema());
 	}
+
+	[Fact]
+	public void GetColumnSchemaForTableAlias()
+	{
+		using var cmd = m_database.Connection.CreateCommand();
+		cmd.CommandText = """
+			drop table if exists column_schema_table2;
+			drop table if exists column_schema_table1;
+			create table column_schema_table1(id int not null primary key);
+			create table column_schema_table2(id int not null primary key,
+				table1_id1 int not null,
+				table1_id2 int not null,
+				foreign key (table1_id1) references column_schema_table1(id),
+				foreign key (table1_id2) references column_schema_table1(id));
+			insert into column_schema_table1(id) values(1),(2);
+			insert into column_schema_table2(id, table1_id1, table1_id2) values(3, 1, 2);
+			""";
+		cmd.ExecuteNonQuery();
+
+		cmd.CommandText = """
+			select t2.id as id2, t1a.id as ida, t1b.id as idb
+			from column_schema_table2 t2
+			join column_schema_table1 t1a on t2.table1_id1 = t1a.id
+			join column_schema_table1 t1b on t2.table1_id2 = t1b.id;
+			""";
+		using var reader = cmd.ExecuteReader();
+		Assert.True(reader.Read());
+		var schema = reader.GetColumnSchema();
+		Assert.Equal(3, schema.Count);
+		Assert.Equal("id2", schema[0].ColumnName);
+		Assert.Equal("ida", schema[1].ColumnName);
+		Assert.Equal("idb", schema[2].ColumnName);
+		Assert.Equal("id", schema[0].BaseColumnName);
+		Assert.Equal("id", schema[1].BaseColumnName);
+		Assert.Equal("id", schema[2].BaseColumnName);
+		Assert.Equal("column_schema_table2", schema[0].BaseTableName);
+		Assert.Equal("column_schema_table1", schema[1].BaseTableName);
+		Assert.Equal("column_schema_table1", schema[2].BaseTableName);
+		Assert.Equal("t2", ((MySqlDbColumn) schema[0]).TableName);
+		Assert.Equal("t1a", ((MySqlDbColumn) schema[1]).TableName);
+		Assert.Equal("t1b", ((MySqlDbColumn) schema[2]).TableName);
+	}
 #endif
 
 	private void UseReaderWithoutDisposingThread(object obj)
@@ -893,11 +1032,15 @@ public void OutputParameter()
 		{
 			ParameterName = "@param",
 			Direction = ParameterDirection.Output,
+			MySqlDbType = MySqlDbType.Int32,
 		});
 
+#if MYSQL_DATA
+		cmd.ExecuteNonQuery();
+		Assert.Equal(1234, cmd.Parameters["@param"].Value);
+#else
 		Assert.Throws<MySqlException>(() => cmd.ExecuteNonQuery());
-
-		// Issue #231: Assert.Equal(1234, cmd.Parameters["@param"].Value);
+#endif
 	}
 
 	[Fact]
@@ -984,6 +1127,33 @@ public void Int64EnumParameter()
 		Assert.False(reader.Read());
 	}
 
+	[Theory]
+	[InlineData(false)]
+	[InlineData(true)]
+	public void QueryEnumWithMySqlDbTypeInt16(bool prepare)
+	{
+		m_database.Connection.Execute(@"drop table if exists bug_1384;
+create table bug_1384(name text not null, enum1 smallint not null, enum2 smallint not null);
+insert into bug_1384 values('CreateNew', 1, 1), ('OpenOrCreate', 4, 4);
+");
+
+		using var command = new MySqlCommand("select * from bug_1384 where enum1 = @enum1 or enum2 = @enum2;", m_database.Connection)
+		{
+			Parameters =
+			{
+				new MySqlParameter("@enum1", MySqlDbType.Int16) { Value = (short) FileMode.OpenOrCreate },
+				new MySqlParameter("@enum2", MySqlDbType.Int16) { Value = FileMode.OpenOrCreate },
+			},
+		};
+		if (prepare)
+			command.Prepare();
+
+		using var reader = command.ExecuteReader();
+		Assert.True(reader.Read());
+		Assert.Equal("OpenOrCreate", reader.GetValue(0));
+		Assert.False(reader.Read());
+	}
+
 	[Fact]
 	public void ReturnDerivedTypes()
 	{
@@ -1074,6 +1244,9 @@ public void HasRowsRepeated()
 		Assert.True(reader.HasRows);
 		Assert.True(reader.HasRows);
 		Assert.True(reader.HasRows);
+
+		Assert.False(reader.NextResult());
+		Assert.False(reader.HasRows);
 	}
 
 	[Fact]
@@ -1236,6 +1409,7 @@ END AS value
 		Assert.True(await reader.NextResultAsync());
 
 		Assert.True(await reader.ReadAsync());
+
 		// MySQL returns ulong, MariaDB returns decimal; GetBoolean will coerce both
 		Assert.True(reader.GetBoolean(0));
 		Assert.False(await reader.ReadAsync());
@@ -1246,8 +1420,10 @@ END AS value
 	[Theory]
 	[InlineData(false, false)]
 	[InlineData(false, true)]
+#if !MYSQL_DATA
 	[InlineData(true, false)]
 	[InlineData(true, true)]
+#endif
 	public void GetIntForTinyInt1(bool treatTinyAsBoolean, bool prepare)
 	{
 		var csb = AppConfig.CreateConnectionStringBuilder();
@@ -1270,19 +1446,13 @@ public void GetIntForTinyInt1(bool treatTinyAsBoolean, bool prepare)
 		for (int i = 0; i < expected.Length; i++)
 		{
 			Assert.True(reader.Read());
-#if !MYSQL_DATA
-			// https://bugs.mysql.com/bug.php?id=99091
 			Assert.Equal((sbyte) expected[i], reader.GetSByte(0));
 			if (treatTinyAsBoolean)
 				Assert.Equal((byte) expected[i], reader.GetByte(0));
-#endif
 			Assert.Equal((short) expected[i], reader.GetInt16(0));
 			Assert.Equal(expected[i], reader.GetInt32(0));
-			Assert.Equal((long) expected[i], reader.GetInt64(0));
-#if !MYSQL_DATA
-			// https://bugs.mysql.com/bug.php?id=99091
+			Assert.Equal(expected[i], reader.GetInt64(0));
 			Assert.Equal(expected[i], reader.GetFieldValue<int>(0));
-#endif
 		}
 
 		Assert.False(reader.Read());
@@ -1520,13 +1690,75 @@ public void GetBytesByName()
 	}
 #endif
 
-	class BoolTest
+	[SkippableTheory(ServerFeatures.Vector)]
+	[InlineData(false, 0)]
+	[InlineData(false, 1)]
+	[InlineData(false, 2)]
+	[InlineData(true, 0)]
+	[InlineData(true, 1)]
+	[InlineData(true, 2)]
+	public void QueryVector(bool prepare, int dataFormat)
+	{
+		using var connection = new MySqlConnection(AppConfig.ConnectionString);
+		connection.Open();
+
+		connection.Execute("""
+			drop table if exists test_vector;
+			create table test_vector(id int auto_increment not null primary key, vec vector(3) not null);
+			""");
+
+		using var cmd = m_database.Connection.CreateCommand();
+		cmd.CommandText = "INSERT INTO test_vector(vec) VALUES(@vec)";
+		cmd.Parameters.Add(new MySqlParameter
+		{
+			ParameterName = "@vec",
+			MySqlDbType = MySqlDbType.Vector,
+		});
+
+		var floatArray = new[] { 1.2f, 3.4f, 5.6f };
+#if MYSQL_DATA
+		// Connector/NET requires the float vector to be passed as a byte array
+		cmd.Parameters[0].Value = MemoryMarshal.AsBytes<float>(floatArray).ToArray();
+		Assert.InRange(dataFormat, 0, 2);
+#else
+		cmd.Parameters[0].Value = dataFormat switch
+		{
+			0 => floatArray,
+			1 => new Memory<float>(floatArray),
+			2 => new ReadOnlyMemory<float>(floatArray),
+			_ => throw new NotSupportedException(),
+		};
+#endif
+
+		if (prepare)
+			cmd.Prepare();
+		cmd.ExecuteNonQuery();
+
+		// Select and verify the value
+		cmd.CommandText = "SELECT vec FROM test_vector";
+		if (prepare)
+			cmd.Prepare();
+
+		using var reader = cmd.ExecuteReader();
+		Assert.True(reader.Read());
+		var value = reader.GetValue(0);
+
+#if MYSQL_DATA
+		var result = MemoryMarshal.Cast<byte, float>((byte[]) value).ToArray();
+#else
+		var result = AppConfig.SupportedFeatures.HasFlag(ServerFeatures.VectorType) ? (ReadOnlyMemory<float>) value :
+			MemoryMarshal.Cast<byte, float>((byte[]) value).ToArray();
+#endif
+		Assert.Equal(floatArray, result);
+	}
+
+	private class BoolTest
 	{
 		public int Id { get; set; }
 		public bool? IsBold { get; set; }
 	}
 
-	class UseReaderWithoutDisposingThreadData
+	private class UseReaderWithoutDisposingThreadData
 	{
 		public UseReaderWithoutDisposingThreadData(List<Exception> exceptions, MySqlConnectionStringBuilder csb)
 		{
@@ -1539,10 +1771,10 @@ public UseReaderWithoutDisposingThreadData(List<Exception> exceptions, MySqlConn
 		public MySqlConnectionStringBuilder ConnectionStringBuilder { get; }
 	}
 
-	enum TestLongEnum : long
+	private enum TestLongEnum : long
 	{
 		Value = long.MaxValue,
 	}
 
-	readonly DatabaseFixture m_database;
+	private readonly DatabaseFixture m_database;
 }
diff --git a/tests/IntegrationTests/RedirectionTests.cs b/tests/IntegrationTests/RedirectionTests.cs
new file mode 100644
index 000000000..7d1155782
--- /dev/null
+++ b/tests/IntegrationTests/RedirectionTests.cs
@@ -0,0 +1,197 @@
+#if !MYSQL_DATA
+using System.Net;
+using System.Net.Sockets;
+
+namespace IntegrationTests;
+
+public class RedirectionTests : IClassFixture<DatabaseFixture>, IDisposable
+{
+	public RedirectionTests(DatabaseFixture database)
+	{
+		m_database = database;
+		m_database.Connection.Open();
+	}
+
+	public void Dispose()
+	{
+		m_database.Connection.Close();
+	}
+
+	[SkippableFact(ServerFeatures.Redirection)]
+	public void RedirectionTest()
+	{
+		StartProxy();
+
+		// wait for proxy to launch
+		Thread.Sleep(50);
+		var csb = AppConfig.CreateConnectionStringBuilder();
+		var initialServer = csb.Server;
+		var initialPort = csb.Port;
+		m_database.Connection.Execute($"set @@global.redirect_url=\"mariadb://{initialServer}:{initialPort}\"");
+
+		try
+		{
+			// changing to proxy port
+			csb.Server = "localhost";
+			csb.Port = (uint)proxy.ListenPort;
+			csb.ServerRedirectionMode = MySqlServerRedirectionMode.Preferred;
+
+			// ensure that connection has been redirected
+			using (var db = new MySqlConnection(csb.ConnectionString))
+			{
+				db.Open();
+				using (var cmd = db.CreateCommand())
+				{
+					cmd.CommandText = "SELECT 1";
+					cmd.ExecuteNonQuery();
+				}
+
+				Assert.Equal((int) initialPort, db.SessionEndPoint!.Port);
+				db.Close();
+			}
+
+			// ensure that connection has been redirected with Required
+			csb.ServerRedirectionMode = MySqlServerRedirectionMode.Required;
+			using (var db = new MySqlConnection(csb.ConnectionString))
+			{
+				db.Open();
+				using (var cmd = db.CreateCommand())
+				{
+					cmd.CommandText = "SELECT 1";
+					cmd.ExecuteNonQuery();
+				}
+
+				Assert.Equal((int) initialPort, db.SessionEndPoint!.Port);
+				db.Close();
+			}
+
+			// ensure that redirection is not done
+			csb.ServerRedirectionMode = MySqlServerRedirectionMode.Disabled;
+			using (var db = new MySqlConnection(csb.ConnectionString))
+			{
+				db.Open();
+				using (var cmd = db.CreateCommand())
+				{
+					cmd.CommandText = "SELECT 1";
+					cmd.ExecuteNonQuery();
+				}
+
+				Assert.Equal(proxy.ListenPort, db.SessionEndPoint!.Port);
+				db.Close();
+			}
+		}
+		finally
+		{
+			m_database.Connection.Execute($"set @@global.redirect_url=\"\"");
+		}
+		MySqlConnection.ClearAllPools();
+
+		// ensure that when required, throwing error if no redirection
+		csb.ServerRedirectionMode = MySqlServerRedirectionMode.Required;
+		using (var db = new MySqlConnection(csb.ConnectionString))
+		{
+			var exception = Assert.Throws<MySqlException>(db.Open);
+			Assert.Equal(MySqlErrorCode.UnableToConnectToHost, exception.ErrorCode);
+		}
+
+		StopProxy();
+	}
+
+	protected void StartProxy()
+	{
+		var csb = AppConfig.CreateConnectionStringBuilder();
+		proxy = new ServerConfiguration( csb.Server, (int)csb.Port );
+		Thread serverThread = new Thread( ServerThread );
+		serverThread.Start( proxy );
+	}
+
+	protected void StopProxy()
+	{
+		proxy.RunServer = false;
+		proxy.ServerSocket.Close();
+	}
+
+	private class ServerConfiguration
+	{
+		public IPAddress RemoteAddress { get; set; }
+		public int RemotePort { get; set; }
+		public int ListenPort { get; set; }
+		public Socket ServerSocket { get; set; }
+		public ServerConfiguration(string remoteAddress, int remotePort)
+		{
+			var ipHostEntry = Dns.GetHostEntry(remoteAddress);
+			RemoteAddress = ipHostEntry.AddressList[0];
+			RemotePort = remotePort;
+			ListenPort = 0;
+		}
+		public bool RunServer { get; set; } = true;
+	}
+
+	private static void ServerThread(object configObj)
+	{
+		ServerConfiguration config = (ServerConfiguration)configObj;
+		Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
+
+		serverSocket.Bind( new IPEndPoint( IPAddress.Any, 0 ) );
+		serverSocket.Listen(1);
+		config.ListenPort = ((IPEndPoint) serverSocket.LocalEndPoint).Port;
+		config.ServerSocket = serverSocket;
+		while (config.RunServer)
+		{
+			try
+			{
+				Socket client = serverSocket.Accept();
+				Thread clientThread = new Thread(ClientThread);
+				clientThread.Start(new ClientContext() { Config = config, Client = client });
+			}
+			catch (SocketException) when (!config.RunServer)
+			{
+				return;
+			}
+		}
+	}
+
+	private class ClientContext
+	{
+		public ServerConfiguration Config { get; set; }
+		public Socket Client { get; set; }
+	}
+
+	private static void ClientThread(object contextObj)
+	{
+		ClientContext context = (ClientContext) contextObj;
+		Socket client = context.Client;
+		ServerConfiguration config = context.Config;
+		IPEndPoint remoteEndPoint = new IPEndPoint(config.RemoteAddress, config.RemotePort);
+		Socket remote = new Socket(remoteEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
+		remote.Connect(remoteEndPoint);
+		var buffer = new byte[4096];
+		while (true)
+		{
+			if (!config.RunServer)
+			{
+				remote.Close();
+				client.Close();
+				return;
+			}
+			if (client.Available > 0)
+			{
+				var count = client.Receive(buffer);
+				if (count == 0)
+					return;
+				remote.Send(buffer, count, SocketFlags.None);
+			}
+			if (remote.Available > 0)
+			{
+				var count = remote.Receive(buffer);
+				if (count == 0)
+					return;
+				client.Send(buffer, count, SocketFlags.None);
+			}
+		}
+	}
+
+	private readonly DatabaseFixture m_database;
+	private ServerConfiguration proxy;
+}
+#endif
diff --git a/tests/IntegrationTests/SchemaProviderFixture.cs b/tests/IntegrationTests/SchemaProviderFixture.cs
new file mode 100644
index 000000000..85eba866f
--- /dev/null
+++ b/tests/IntegrationTests/SchemaProviderFixture.cs
@@ -0,0 +1,32 @@
+namespace IntegrationTests;
+
+public class SchemaProviderFixture : DatabaseFixture
+{
+	public SchemaProviderFixture()
+	{
+		Connection.Execute("""
+			DROP TABLE IF EXISTS fk_test;
+			DROP TABLE IF EXISTS pk_test;
+
+			CREATE TABLE pk_test
+			(
+				a INT NOT NULL,
+				b INT NOT NULL,
+				c INT NOT NULL,
+				d INT NOT NULL,
+				e INT NOT NULL,
+				CONSTRAINT pk_test_pk PRIMARY KEY (a, b),
+				CONSTRAINT pk_test_uq UNIQUE INDEX (c, d, e),
+				INDEX pk_test_ix (c, d)
+			);
+
+			CREATE TABLE fk_test
+			(
+				g INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
+				h INT NOT NULL,
+				i INT NOT NULL,
+				CONSTRAINT fk_test_fk FOREIGN KEY (h, i) REFERENCES pk_test(a, b)
+			);
+			""");
+	}
+}
diff --git a/tests/IntegrationTests/SchemaProviderTests.cs b/tests/IntegrationTests/SchemaProviderTests.cs
index aba11872d..d4c589ab7 100644
--- a/tests/IntegrationTests/SchemaProviderTests.cs
+++ b/tests/IntegrationTests/SchemaProviderTests.cs
@@ -1,8 +1,9 @@
+#nullable enable
 namespace IntegrationTests;
 
-public class SchemaProviderTests : IClassFixture<DatabaseFixture>, IDisposable
+public class SchemaProviderTests : IClassFixture<SchemaProviderFixture>, IDisposable
 {
-	public SchemaProviderTests(DatabaseFixture database)
+	public SchemaProviderTests(SchemaProviderFixture database)
 	{
 		m_database = database;
 		m_database.Connection.Open();
@@ -118,10 +119,12 @@ public void MetaDataCollectionsRestriction() =>
 	[InlineData("DATABASES", "Databases")]
 	[InlineData("DataTypes")]
 	[InlineData("datatypes", "DataTypes")]
-	// only in 8.0 - [InlineData("KeyWords")]
+	[InlineData("Indexes")]
+	[InlineData("IndexColumns")]
+	//// only in 8.0 - [InlineData("KeyWords")]
 	[InlineData("MetaDataCollections")]
 	[InlineData("Procedures")]
-	// only in 8.0 - [InlineData("ResourceGroups")]
+	//// only in 8.0 - [InlineData("ResourceGroups")]
 	[InlineData("Tables")]
 	[InlineData("Triggers")]
 	[InlineData("Views")]
@@ -130,6 +133,7 @@ public void MetaDataCollectionsRestriction() =>
 	[InlineData("CollationCharacterSetApplicability")]
 	[InlineData("Collations")]
 	[InlineData("Engines")]
+	[InlineData("Foreign Keys")]
 	[InlineData("KeyColumnUsage")]
 	[InlineData("Parameters")]
 	[InlineData("Partitions")]
@@ -140,10 +144,9 @@ public void MetaDataCollectionsRestriction() =>
 	[InlineData("SchemaPrivileges")]
 	[InlineData("TableConstraints")]
 	[InlineData("TablePrivileges")]
-	[InlineData("TableSpaces")]
 	[InlineData("UserPrivileges")]
 #endif
-	public void GetSchema(string schemaName, string expectedSchemaName = null)
+	public void GetSchema(string schemaName, string? expectedSchemaName = null)
 	{
 		var table = m_database.Connection.GetSchema(schemaName);
 		Assert.NotNull(table);
@@ -177,5 +180,83 @@ public void GetCollationsSchema()
 	}
 #endif
 
-	readonly DatabaseFixture m_database;
+	[Fact]
+	public void ForeignKeys()
+	{
+		var schemaName = m_database.Connection.Database;
+		var table = m_database.Connection.GetSchema("Foreign Keys", new[] { null, schemaName, "fk_test" });
+		var row = table.Rows.Cast<DataRow>().Single();
+		foreach (var (column, value) in new[]
+		{
+			("CONSTRAINT_CATALOG", "def"),
+			("CONSTRAINT_SCHEMA", schemaName),
+			("CONSTRAINT_NAME", "fk_test_fk"),
+			("TABLE_CATALOG", "def"),
+			("TABLE_SCHEMA", schemaName),
+			("TABLE_NAME", "fk_test"),
+			("MATCH_OPTION", "NONE"),
+			("REFERENCED_TABLE_CATALOG", null),
+			("REFERENCED_TABLE_SCHEMA", schemaName),
+			("REFERENCED_TABLE_NAME", "pk_test"),
+		})
+		{
+			Assert.Equal(value, row[column] is DBNull ? null : (string?) row[column]);
+		}
+	}
+
+	[Fact]
+	public void Indexes()
+	{
+		var schemaName = m_database.Connection.Database;
+		var table = m_database.Connection.GetSchema("Indexes", new[] { null, schemaName, "pk_test" });
+		var actual = table.Rows
+			.Cast<DataRow>()
+			.OrderBy(x => (string) x["INDEX_NAME"])
+			.Select(x => ((string) x["INDEX_SCHEMA"], (string) x["INDEX_NAME"], (string) x["TABLE_NAME"], (bool) x["UNIQUE"], (bool) x["PRIMARY"], (string) x["TYPE"]));
+		var expected = new[]
+		{
+			(schemaName, "pk_test_ix", "pk_test", false, false, "BTREE"),
+			(schemaName, "pk_test_uq", "pk_test", true, false, "BTREE"),
+			(schemaName, "PRIMARY", "pk_test", true, true, "BTREE"),
+		};
+		Assert.Equal(expected, actual);
+	}
+
+	[Fact]
+	public void IndexColumns()
+	{
+		var schemaName = m_database.Connection.Database;
+		var table = m_database.Connection.GetSchema("IndexColumns", new[] { null, schemaName, "pk_test", "pk_test_uq" });
+		var actual = table.Rows
+			.Cast<DataRow>()
+			.OrderBy(x => (string) x["INDEX_NAME"])
+			.ThenBy(x => (int) x["ORDINAL_POSITION"])
+			.Select(x => ((string) x["INDEX_SCHEMA"], (string) x["INDEX_NAME"], (string) x["TABLE_NAME"], (string) x["COLUMN_NAME"], (int) x["ORDINAL_POSITION"]));
+		var expected = new[]
+		{
+			(schemaName, "pk_test_uq", "pk_test", "c", 1),
+			(schemaName, "pk_test_uq", "pk_test", "d", 2),
+			(schemaName, "pk_test_uq", "pk_test", "e", 3),
+		};
+		Assert.Equal(expected, actual);
+	}
+
+	[Fact]
+	public void IndexColumnsWithColumnName()
+	{
+		var schemaName = m_database.Connection.Database;
+		var table = m_database.Connection.GetSchema("IndexColumns", new[] { null, schemaName, "pk_test", "pk_test_uq", "d" });
+		var actual = table.Rows
+			.Cast<DataRow>()
+			.OrderBy(x => (string) x["INDEX_NAME"])
+			.ThenBy(x => (int) x["ORDINAL_POSITION"])
+			.Select(x => ((string) x["INDEX_SCHEMA"], (string) x["INDEX_NAME"], (string) x["TABLE_NAME"], (string) x["COLUMN_NAME"], (int) x["ORDINAL_POSITION"]));
+		var expected = new[]
+		{
+			(schemaName, "pk_test_uq", "pk_test", "d", 2),
+		};
+		Assert.Equal(expected, actual);
+	}
+
+	private readonly DatabaseFixture m_database;
 }
diff --git a/tests/IntegrationTests/ServerFeatures.cs b/tests/IntegrationTests/ServerFeatures.cs
index cefe5d377..2ab997c65 100644
--- a/tests/IntegrationTests/ServerFeatures.cs
+++ b/tests/IntegrationTests/ServerFeatures.cs
@@ -35,4 +35,29 @@ public enum ServerFeatures
 	/// A "SLEEP" command produces a result set when it is cancelled, not an error payload.
 	/// </summary>
 	CancelSleepSuccessfully = 0x40_0000,
+
+	/// <summary>
+	/// Server permits redirection (sent as a server variable in first OK packet).
+	/// </summary>
+	Redirection = 0x80_0000,
+
+	/// <summary>
+	/// Server provides hash of TLS certificate in first OK packet.
+	/// </summary>
+	TlsFingerprintValidation = 0x100_0000,
+
+	/// <summary>
+	/// Server supports the 'parsec' authentication plugin.
+	/// </summary>
+	ParsecAuthentication = 0x200_0000,
+
+	/// <summary>
+	/// Server supports the <c>VECTOR</c> SQL type.
+	/// </summary>
+	Vector = 0x400_0000,
+
+	/// <summary>
+	/// Server has a dedicated type on the wire for <c>VECTOR</c>.
+	/// </summary>
+	VectorType = 0x800_0000,
 }
diff --git a/tests/IntegrationTests/SslTests.cs b/tests/IntegrationTests/SslTests.cs
index df3042d71..be0dcf3da 100644
--- a/tests/IntegrationTests/SslTests.cs
+++ b/tests/IntegrationTests/SslTests.cs
@@ -62,19 +62,15 @@ public async Task ConnectSslClientCertificateCallback(string certificateFile, st
 		var certificateFilePath = Path.Combine(AppConfig.CertsPath, certificateFile);
 
 		using var connection = new MySqlConnection(csb.ConnectionString);
-#if NETFRAMEWORK
 		connection.ProvideClientCertificatesCallback = x =>
 		{
-			x.Add(new X509Certificate2(certificateFilePath, certificateFilePassword));
-			return Task.CompletedTask;
-		};
+#if NET9_0_OR_GREATER
+			x.Add(X509CertificateLoader.LoadPkcs12FromFile(certificateFilePath, certificateFilePassword));
 #else
-		connection.ProvideClientCertificatesCallback = async x =>
-		{
-			var certificateBytes = await File.ReadAllBytesAsync(certificateFilePath);
-			x.Add(new X509Certificate2(certificateBytes, certificateFilePassword));
-		};
+			x.Add(new X509Certificate2(certificateFilePath, certificateFilePassword));
 #endif
+			return default;
+		};
 
 		await connection.OpenAsync();
 		Assert.True(connection.SslIsEncrypted);
@@ -111,7 +107,9 @@ private async Task DoTestSsl(string connectionString)
 		Assert.True(connection.SslIsEncrypted);
 		Assert.True(connection.SslIsSigned);
 		Assert.True(connection.SslIsAuthenticated);
+#if !NET9_0_OR_GREATER
 		Assert.True(connection.SslIsMutuallyAuthenticated);
+#endif
 #endif
 		cmd.CommandText = "SHOW SESSION STATUS LIKE 'Ssl_version'";
 		var sslVersion = (string) await cmd.ExecuteScalarAsync();
@@ -125,7 +123,11 @@ public async Task ConnectSslClientCertificateFromCertificateStore(string certFil
 		// Create a mock of certificate store
 		var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
 		store.Open(OpenFlags.ReadWrite);
+#if NET9_0_OR_GREATER
+		var certificate = X509CertificateLoader.LoadPkcs12FromFile(Path.Combine(AppConfig.CertsPath, certFile), null);
+#else
 		var certificate = new X509Certificate2(Path.Combine(AppConfig.CertsPath, certFile));
+#endif
 		store.Add(certificate);
 
 		var csb = AppConfig.CreateConnectionStringBuilder();
@@ -141,7 +143,9 @@ public async Task ConnectSslClientCertificateFromCertificateStore(string certFil
 			Assert.True(connection.SslIsEncrypted);
 			Assert.True(connection.SslIsSigned);
 			Assert.True(connection.SslIsAuthenticated);
+#if !NET9_0_OR_GREATER
 			Assert.True(connection.SslIsMutuallyAuthenticated);
+#endif
 #endif
 			cmd.CommandText = "SHOW SESSION STATUS LIKE 'Ssl_version'";
 			var sslVersion = (string) await cmd.ExecuteScalarAsync();
@@ -189,7 +193,10 @@ public async Task ConnectSslBadCaCertificate()
 		csb.SslMode = MySqlSslMode.VerifyCA;
 		csb.SslCa = Path.Combine(AppConfig.CertsPath, "non-ca-client-cert.pem");
 		using var connection = new MySqlConnection(csb.ConnectionString);
-		await Assert.ThrowsAsync<MySqlException>(async () => await connection.OpenAsync());
+		if (AppConfig.SupportedFeatures.HasFlag(ServerFeatures.TlsFingerprintValidation))
+			await connection.OpenAsync();
+		else
+			await Assert.ThrowsAsync<MySqlException>(async () => await connection.OpenAsync());
 	}
 
 #if !MYSQL_DATA
@@ -206,13 +213,58 @@ public async Task ConnectSslRemoteCertificateValidationCallback(MySqlSslMode ssl
 		using var connection = new MySqlConnection(csb.ConnectionString);
 		connection.RemoteCertificateValidationCallback = (s, c, h, e) => true;
 
-		if (expectedSuccess)
+		if (expectedSuccess || AppConfig.SupportedFeatures.HasFlag(ServerFeatures.TlsFingerprintValidation))
 			await connection.OpenAsync();
 		else
 			await Assert.ThrowsAsync<MySqlException>(async () => await connection.OpenAsync());
 	}
 #endif
 
+	[SkippableFact(ServerFeatures.TlsFingerprintValidation)]
+	public async Task ConnectZeroConfigurationSslNative()
+	{
+		// permit connection without any Ssl configuration.
+		// reference https://mariadb.org/mission-impossible-zero-configuration-ssl/
+		var csb = AppConfig.CreateConnectionStringBuilder();
+		csb.CertificateFile = null;
+		csb.SslMode = MySqlSslMode.VerifyFull;
+		csb.SslCa = "";
+		csb.UserID = "ssltest";
+		csb.Password = "test";
+		using var connection = new MySqlConnection(csb.ConnectionString);
+		await connection.OpenAsync();
+	}
+
+#if !MYSQL_DATA
+	[SkippableFact(ServerFeatures.TlsFingerprintValidation | ServerFeatures.Ed25519)]
+	public async Task ConnectZeroConfigurationSslEd25519()
+	{
+		MySqlConnector.Authentication.Ed25519.Ed25519AuthenticationPlugin.Install();
+		var csb = AppConfig.CreateConnectionStringBuilder();
+		csb.CertificateFile = null;
+		csb.SslMode = MySqlSslMode.VerifyFull;
+		csb.SslCa = "";
+		csb.UserID = "ed25519user";
+		csb.Password = "Ed255!9";
+		using var connection = new MySqlConnection(csb.ConnectionString);
+		await connection.OpenAsync();
+	}
+
+	[SkippableFact(ServerFeatures.TlsFingerprintValidation | ServerFeatures.ParsecAuthentication)]
+	public async Task ConnectZeroConfigurationSslParsec()
+	{
+		MySqlConnector.Authentication.Ed25519.ParsecAuthenticationPlugin.Install();
+		var csb = AppConfig.CreateConnectionStringBuilder();
+		csb.CertificateFile = null;
+		csb.SslMode = MySqlSslMode.VerifyFull;
+		csb.SslCa = "";
+		csb.UserID = "parsec-user";
+		csb.Password = "P@rs3c-Pa55";
+		using var connection = new MySqlConnection(csb.ConnectionString);
+		await connection.OpenAsync();
+	}
+#endif
+
 	[SkippableFact(ConfigSettings.RequiresSsl)]
 	public async Task ConnectSslTlsVersion()
 	{
@@ -268,24 +320,5 @@ public async Task ForceTls11()
 	}
 #endif
 
-#if NETCOREAPP3_1
-	[SkippableFact(ConfigSettings.RequiresSsl)]
-	public async Task RequireTls13()
-	{
-		if (AppConfig.SupportedFeatures.HasFlag(ServerFeatures.Tls13))
-			return;
-
-		var csb = AppConfig.CreateConnectionStringBuilder();
-		csb.TlsVersion = "TLS 1.3";
-
-		using var connection = new MySqlConnection(csb.ConnectionString);
-#if !MYSQL_DATA
-		await Assert.ThrowsAsync<MySqlException>(async () => await connection.OpenAsync());
-#else
-		await Assert.ThrowsAsync<Win32Exception>(async () => await connection.OpenAsync());
-#endif
-	}
-#endif
-
-	readonly DatabaseFixture m_database;
+	private readonly DatabaseFixture m_database;
 }
diff --git a/tests/IntegrationTests/StoredProcedureTests.cs b/tests/IntegrationTests/StoredProcedureTests.cs
index f3fa66341..bbbdec53d 100644
--- a/tests/IntegrationTests/StoredProcedureTests.cs
+++ b/tests/IntegrationTests/StoredProcedureTests.cs
@@ -1,3 +1,5 @@
+using System.Runtime.InteropServices;
+
 namespace IntegrationTests;
 
 public class StoredProcedureTests : IClassFixture<StoredProcedureFixture>
@@ -443,18 +445,18 @@ private async Task<object> ExecuteCommandAsync(DbCommand cmd, string executorTyp
 	{
 		switch (executorType)
 		{
-		case "NonQuery":
-			await cmd.ExecuteNonQueryAsync();
-			return null;
-		case "Scalar":
-			return await cmd.ExecuteScalarAsync();
-		default:
-			using (var reader = await cmd.ExecuteReaderAsync())
-			{
-				if (await reader.ReadAsync())
-					return reader.GetValue(0);
+			case "NonQuery":
+				await cmd.ExecuteNonQueryAsync();
 				return null;
-			}
+			case "Scalar":
+				return await cmd.ExecuteScalarAsync();
+			default:
+				using (var reader = await cmd.ExecuteReaderAsync())
+				{
+					if (await reader.ReadAsync())
+						return reader.GetValue(0);
+					return null;
+				}
 		}
 	}
 
@@ -532,7 +534,7 @@ public async Task InOut(bool prepare)
 			ParameterName = "high",
 			DbType = DbType.Int32,
 			Direction = ParameterDirection.InputOutput,
-			Value = 1
+			Value = 1,
 		};
 		while ((int) parameter.Value < 8)
 		{
@@ -664,21 +666,23 @@ OUT param2 VARCHAR(100))
 	}
 
 	[Theory]
-	[InlineData("bit(1)", 1)]
-	[InlineData("bit(10)", 10)]
 #if !MYSQL_DATA
 	[InlineData("bool", 1)]
 	[InlineData("tinyint(1)", 1)]
 	[InlineData("decimal(10)", 10)]
-#endif
-    [InlineData("char(30)", 30)]
+
+	// https://bugs.mysql.com/bug.php?id=112088
+	[InlineData("bit(1)", 1)]
+	[InlineData("bit(10)", 10)]
 	[InlineData("mediumtext", 0)]
+#endif
+	[InlineData("char(30)", 30)]
 	[InlineData("varchar(50)", 50)]
-	// These return nonzero sizes for some versions of MySQL Server 8.0
-	// [InlineData("bit", 0)]
-	// [InlineData("tinyint", 0)]
-	// [InlineData("bigint", 0)]
-	// [InlineData("bigint unsigned", 0)]
+	//// These return nonzero sizes for some versions of MySQL Server 8.0
+	//// [InlineData("bit", 0)]
+	//// [InlineData("tinyint", 0)]
+	//// [InlineData("bigint", 0)]
+	//// [InlineData("bigint unsigned", 0)]
 	public void DeriveParametersParameterSize(string parameterType, int expectedSize)
 	{
 		var csb = AppConfig.CreateConnectionStringBuilder();
@@ -759,6 +763,39 @@ public void PassJsonParameter()
 		Assert.False(reader.Read());
 	}
 
+	[SkippableTheory(ServerFeatures.Vector | ServerFeatures.VectorType)]
+	[InlineData(false)]
+	[InlineData(true)]
+	public void VectorOutputParameter(bool prepare)
+	{
+		using var cmd = m_database.Connection.CreateCommand();
+		cmd.CommandText = """
+			DROP PROCEDURE IF EXISTS sp_vector_out;
+			CREATE PROCEDURE sp_vector_out (OUT vec VECTOR)
+			BEGIN
+				SELECT STRING_TO_VECTOR('[1.2, 3.4, 5.6]') INTO vec;
+			END;
+			""";
+		cmd.ExecuteNonQuery();
+
+		cmd.CommandText = "sp_vector_out";
+		cmd.CommandType = CommandType.StoredProcedure;
+		cmd.Parameters.Add(new MySqlParameter
+		{
+			Direction = ParameterDirection.Output,
+			MySqlDbType = MySqlDbType.Vector,
+			ParameterName = "@vec",
+		});
+
+		if (prepare)
+			cmd.Prepare();
+		cmd.ExecuteNonQuery();
+
+		var value = cmd.Parameters[0].Value;
+		var result = Assert.IsType<byte[]>(value);
+		Assert.Equal(new float[] { 1.2f, 3.4f, 5.6f }, MemoryMarshal.Cast<byte, float>(result).ToArray());
+	}
+
 	private static Action<MySqlParameter> AssertParameter(string name, ParameterDirection direction, MySqlDbType mySqlDbType)
 	{
 		return x =>
@@ -872,6 +909,60 @@ public void SprocNameSpecialCharacters(string sprocName)
 		}
 	}
 
+#if !MYSQL_DATA
+	[Theory]
+	[InlineData(MySqlGuidFormat.Binary16, "BINARY(16)", "X'BABD8384C908499C9D95C02ADA94A970'", false, false)]
+	[InlineData(MySqlGuidFormat.Binary16, "BINARY(16)", "X'BABD8384C908499C9D95C02ADA94A970'", false, true)]
+	[InlineData(MySqlGuidFormat.Binary16, "BINARY(16)", "X'BABD8384C908499C9D95C02ADA94A970'", true, false)]
+	[InlineData(MySqlGuidFormat.Binary16, "BINARY(16)", "X'BABD8384C908499C9D95C02ADA94A970'", true, true)]
+	[InlineData(MySqlGuidFormat.Char32, "CHAR(32)", "'BABD8384C908499C9D95C02ADA94A970'", false, false)]
+	[InlineData(MySqlGuidFormat.Char32, "CHAR(32)", "'BABD8384C908499C9D95C02ADA94A970'", false, true)]
+	[InlineData(MySqlGuidFormat.Char32, "CHAR(32)", "'BABD8384C908499C9D95C02ADA94A970'", true, false)]
+	[InlineData(MySqlGuidFormat.Char32, "CHAR(32)", "'BABD8384C908499C9D95C02ADA94A970'", true, true)]
+	[InlineData(MySqlGuidFormat.Char36, "CHAR(36)", "'BABD8384-C908-499C-9D95-C02ADA94A970'", false, false)]
+	[InlineData(MySqlGuidFormat.Char36, "CHAR(36)", "'BABD8384-C908-499C-9D95-C02ADA94A970'", false, true)]
+	[InlineData(MySqlGuidFormat.Char36, "CHAR(36)", "'BABD8384-C908-499C-9D95-C02ADA94A970'", true, false)]
+	[InlineData(MySqlGuidFormat.Char36, "CHAR(36)", "'BABD8384-C908-499C-9D95-C02ADA94A970'", true, true)]
+	public void StoredProcedureReturnsGuid(MySqlGuidFormat guidFormat, string columnDefinition, string columnValue, bool setMySqlDbType, bool prepare)
+	{
+		var csb = AppConfig.CreateConnectionStringBuilder();
+		csb.GuidFormat = guidFormat;
+		csb.Pooling = false;
+		using var connection = new MySqlConnection(csb.ConnectionString);
+		connection.Open();
+
+		using (var command = new MySqlCommand($"""
+			DROP TABLE IF EXISTS out_guid_table;
+			CREATE TABLE out_guid_table (id INT PRIMARY KEY AUTO_INCREMENT, guid {columnDefinition});
+			INSERT INTO out_guid_table (guid) VALUES ({columnValue});
+			DROP PROCEDURE IF EXISTS out_guid;
+			CREATE PROCEDURE out_guid
+			(
+				OUT out_name {columnDefinition}
+			)
+			BEGIN
+				SELECT guid INTO out_name FROM out_guid_table;
+			END;
+			""", connection))
+		{
+			command.ExecuteNonQuery();
+		}
+
+		using (var command = new MySqlCommand("out_guid", connection))
+		{
+			command.CommandType = CommandType.StoredProcedure;
+			var param = new MySqlParameter("out_name", null) { Direction = ParameterDirection.Output };
+			if (setMySqlDbType)
+				param.MySqlDbType = MySqlDbType.Guid;
+			command.Parameters.Add(param);
+			command.ExecuteNonQuery();
+			if (prepare)
+				command.Prepare();
+			Assert.Equal(new Guid("BABD8384C908499C9D95C02ADA94A970"), param.Value);
+		}
+	}
+#endif
+
 	private static string NormalizeSpaces(string input)
 	{
 		input = input.Replace('\r', ' ');
@@ -893,5 +984,5 @@ private static MySqlConnection CreateOpenConnection()
 		return connection;
 	}
 
-	readonly DatabaseFixture m_database;
+	private readonly DatabaseFixture m_database;
 }
diff --git a/tests/IntegrationTests/TestUtilities.cs b/tests/IntegrationTests/TestUtilities.cs
index 3593dc35b..c7ae8c696 100644
--- a/tests/IntegrationTests/TestUtilities.cs
+++ b/tests/IntegrationTests/TestUtilities.cs
@@ -18,23 +18,80 @@ public static void AssertEqual(byte[] expected, byte[] actual)
 	}
 
 	/// <summary>
-	/// Verifies that <paramref name="value"/> is an integer (<see cref="Int32"/> or <see cref="Int64"/>) with the value <code>1</code>.
+	/// Verifies that <paramref name="value"/> is an integer (<see cref="int"/> or <see cref="long"/>) with the value <code>1</code>.
 	/// </summary>
 	public static void AssertIsOne(object value)
 	{
 		switch (value)
 		{
-		case int intValue:
-			Assert.Equal(1, intValue);
-			break;
+			case int intValue:
+				Assert.Equal(1, intValue);
+				break;
 
-		case long longValue:
-			Assert.Equal(1L, longValue);
-			break;
+			case long longValue:
+				Assert.Equal(1L, longValue);
+				break;
 
-		default:
-			Assert.Equal(1L, value);
-			break;
+			default:
+				Assert.Equal(1L, value);
+				break;
+		}
+	}
+
+	public static void AssertExecuteScalarReturnsOneOrIsCanceled(MySqlCommand command) =>
+		AssertExecuteScalarReturnsOneOrThrowsException(command, MySqlErrorCode.QueryInterrupted);
+
+#if !MYSQL_DATA
+	public static void AssertExecuteScalarReturnsOneOrTimesOut(MySqlCommand command) =>
+		AssertExecuteScalarReturnsOneOrThrowsException(command, MySqlErrorCode.CommandTimeoutExpired);
+#endif
+
+	private static void AssertExecuteScalarReturnsOneOrThrowsException(MySqlCommand command, MySqlErrorCode expectedCode)
+	{
+		if (AppConfig.SupportedFeatures.HasFlag(ServerFeatures.CancelSleepSuccessfully))
+		{
+			AssertIsOne(command.ExecuteScalar());
+		}
+		else
+		{
+			var ex = Assert.Throws<MySqlException>(command.ExecuteScalar);
+#if MYSQL_DATA
+			Assert.Equal((int) expectedCode, ex.Number);
+#else
+			Assert.Equal(expectedCode, ex.ErrorCode);
+#endif
+		}
+	}
+
+	public static async Task AssertExecuteScalarReturnsOneOrIsCanceledAsync(MySqlCommand command, CancellationToken token = default) =>
+		await AssertExecuteScalarReturnsOneOrThrowsExceptionAsync(command, MySqlErrorCode.QueryInterrupted, token);
+
+#if !MYSQL_DATA
+	public static async Task AssertExecuteScalarReturnsOneOrTimesOutAsync(MySqlCommand command, CancellationToken token = default) =>
+		await AssertExecuteScalarReturnsOneOrThrowsExceptionAsync(command, MySqlErrorCode.CommandTimeoutExpired, token);
+#endif
+
+	private static async Task AssertExecuteScalarReturnsOneOrThrowsExceptionAsync(MySqlCommand command, MySqlErrorCode expectedCode, CancellationToken token)
+	{
+		if (AppConfig.SupportedFeatures.HasFlag(ServerFeatures.CancelSleepSuccessfully))
+		{
+			AssertIsOne(await command.ExecuteScalarAsync(token));
+		}
+		else
+		{
+			var ex = await Assert.ThrowsAnyAsync<Exception>(async () => await command.ExecuteScalarAsync(token));
+			MySqlException exception = ex as MySqlException;
+			while (exception is null && ex is not null)
+			{
+				ex = ex.InnerException;
+				exception = ex as MySqlException;
+			}
+			Assert.NotNull(exception);
+#if MYSQL_DATA
+			Assert.Equal((int) expectedCode, exception.Number);
+#else
+			Assert.Equal(expectedCode, exception.ErrorCode);
+#endif
 		}
 	}
 
@@ -89,6 +146,9 @@ public static string GetSkipReason(ServerFeatures serverFeatures, ConfigSettings
 		if (configSettings.HasFlag(ConfigSettings.PasswordlessUser) && string.IsNullOrWhiteSpace(AppConfig.PasswordlessUser))
 			return "Requires PasswordlessUser in config.json";
 
+		if (configSettings.HasFlag(ConfigSettings.UserHasPassword) && csb.Password.Length == 0)
+			return "Requires password in connection string";
+
 		if (configSettings.HasFlag(ConfigSettings.GSSAPIUser) && string.IsNullOrWhiteSpace(AppConfig.GSSAPIUser))
 			return "Requires GSSAPIUser in config.json";
 
diff --git a/tests/IntegrationTests/Transaction.cs b/tests/IntegrationTests/Transaction.cs
index f1f2abdcf..4fb94d316 100644
--- a/tests/IntegrationTests/Transaction.cs
+++ b/tests/IntegrationTests/Transaction.cs
@@ -169,7 +169,7 @@ public async Task ReadWriteTransactionAsync()
 		Assert.Equal(new[] { 1, 2 }, results);
 	}
 
-#if !NET462 && !NET472
+#if !NETFRAMEWORK
 	[Fact]
 	public async Task DbConnectionCommitAsync()
 	{
@@ -241,7 +241,7 @@ public async Task RollbackDisposeAsync()
 		Assert.Equal(new int[0], results);
 	}
 
-#if !NET462 && !NET472
+#if !NETFRAMEWORK
 	[Fact]
 	public async Task DbConnectionRollbackAsync()
 	{
@@ -399,7 +399,7 @@ public void TransactionHoldsLocks()
 				""";
 			command.ExecuteNonQuery();
 		}
-		
+
 		using var barrier = new Barrier(2);
 		using var barrier2 = new Barrier(2);
 
@@ -449,6 +449,6 @@ public void TransactionHoldsLocks()
 		task2.GetAwaiter().GetResult();
 	}
 
-	readonly TransactionFixture m_database;
-	readonly MySqlConnection m_connection;
+	private readonly TransactionFixture m_database;
+	private readonly MySqlConnection m_connection;
 }
diff --git a/tests/IntegrationTests/TransactionScopeTests.cs b/tests/IntegrationTests/TransactionScopeTests.cs
index 3bf1ea086..1104a7015 100644
--- a/tests/IntegrationTests/TransactionScopeTests.cs
+++ b/tests/IntegrationTests/TransactionScopeTests.cs
@@ -9,7 +9,7 @@ public TransactionScopeTests(DatabaseFixture database)
 		m_database = database;
 	}
 
-	public static IEnumerable<object[]> ConnectionStrings = new[]
+	public static IEnumerable<object[]> ConnectionStrings { get; } = new[]
 	{
 #if MYSQL_DATA
 		new object[] { "" },
@@ -274,7 +274,7 @@ public void UsingSequentialConnectionsInOneTransactionDoesNotDeadlock(string con
 		var transactionOptions = new TransactionOptions
 		{
 			IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted,
-			Timeout = TransactionManager.MaximumTimeout
+			Timeout = TransactionManager.MaximumTimeout,
 		};
 		using (var scope = new TransactionScope(TransactionScopeOption.Required, transactionOptions, TransactionScopeAsyncFlowOption.Enabled))
 		{
@@ -316,7 +316,7 @@ public void UsingSequentialConnectionsInOneTransactionDoesNotDeadlockWithoutComp
 		var transactionOptions = new TransactionOptions
 		{
 			IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted,
-			Timeout = TransactionManager.MaximumTimeout
+			Timeout = TransactionManager.MaximumTimeout,
 		};
 		using (new TransactionScope(TransactionScopeOption.Required, transactionOptions, TransactionScopeAsyncFlowOption.Enabled))
 		{
@@ -454,7 +454,7 @@ public void ReusingConnectionInOneTransactionDoesNotDeadlock(string connectionSt
 		var transactionOptions = new TransactionOptions
 		{
 			IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted,
-			Timeout = TransactionManager.MaximumTimeout
+			Timeout = TransactionManager.MaximumTimeout,
 		};
 		using (var scope = new TransactionScope(TransactionScopeOption.Required, transactionOptions, TransactionScopeAsyncFlowOption.Enabled))
 		{
@@ -494,7 +494,7 @@ public void ReusingConnectionInOneTransactionDoesNotDeadlockWithoutComplete(stri
 		var transactionOptions = new TransactionOptions
 		{
 			IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted,
-			Timeout = TransactionManager.MaximumTimeout
+			Timeout = TransactionManager.MaximumTimeout,
 		};
 		using (new TransactionScope(TransactionScopeOption.Required, transactionOptions, TransactionScopeAsyncFlowOption.Enabled))
 		{
@@ -766,7 +766,9 @@ PRIMARY KEY (`id`)
 
 		var task = Task.Run(() => UseTransaction());
 		UseTransaction();
+#pragma warning disable xUnit1031 // Do not use blocking task operations in test method
 		task.Wait();
+#pragma warning restore xUnit1031 // Do not use blocking task operations in test method
 
 		void UseTransaction()
 		{
@@ -840,8 +842,85 @@ public void CannotMixNonXaAndXaTransactions()
 			Assert.Throws<NotSupportedException>(() => conn2.Open());
 		}
 	}
+
+	[Fact]
+	public void Bug1348()
+	{
+		var xid = string.Empty;
+
+		// TransactionAbortedException、MySqlException
+		Assert.ThrowsAny<Exception>(() =>
+		{
+			using (TransactionScope scope = new())
+			{
+				xid = System.Transactions.Transaction.Current.TransactionInformation.LocalIdentifier;
+				using var conn1 = new MySqlConnection(AppConfig.ConnectionString);
+				conn1.Open();
+
+				using var conn2 = new MySqlConnection(AppConfig.ConnectionString);
+				conn2.Open();
+
+				// Rolling back the second branch transaction early so that it has an exception in the preparation phase
+				var command2 = conn2.CreateCommand();
+				command2.CommandText = $"XA END '{xid}','2';XA ROLLBACK '{xid}','2'";
+				command2.ExecuteNonQuery();
+
+				scope.Complete();
+			}
+		});
+
+		// Asserts whether the first branch transaction is rolled back
+		using var conn = new MySqlConnection(AppConfig.ConnectionString);
+		conn.Open();
+		var command = conn.CreateCommand();
+		command.CommandText = $"XA RECOVER;";
+		using var reader = command.ExecuteReader();
+		var rollbacked = true;
+		while (reader.Read())
+		{
+			var branchID = reader.GetString(3);
+			if (branchID == $"{xid}1")
+			{
+				rollbacked = false;
+				break;
+			}
+		}
+
+		Assert.True(rollbacked, $"First branch transaction '{xid}1' not rolled back");
+	}
+
+	[Fact]
+	public void ConnectionOpenedCallbackAutoEnlistInTransaction()
+	{
+		var connectionOpenedCallbackCount = 0;
+		var connectionOpenedConditions = MySqlConnectionOpenedConditions.None;
+		using var dataSource = new MySqlDataSourceBuilder(AppConfig.ConnectionString)
+			.UseConnectionOpenedCallback((ctx, token) =>
+			{
+				connectionOpenedCallbackCount++;
+				connectionOpenedConditions = ctx.Conditions;
+				return default;
+			})
+			.Build();
+
+		using (var transactionScope = new TransactionScope())
+		{
+			using (var conn = dataSource.OpenConnection())
+			{
+				Assert.Equal(1, connectionOpenedCallbackCount);
+				Assert.Equal(MySqlConnectionOpenedConditions.New, connectionOpenedConditions);
+			}
+
+			using (var conn = dataSource.OpenConnection())
+			{
+				Assert.Equal(2, connectionOpenedCallbackCount);
+				Assert.Equal(MySqlConnectionOpenedConditions.None, connectionOpenedConditions);
+			}
+
+			transactionScope.Complete();
+		}
+	}
 #endif
 
-	DatabaseFixture m_database;
+	private readonly DatabaseFixture m_database;
 }
-
diff --git a/tests/IntegrationTests/UpdateTests.cs b/tests/IntegrationTests/UpdateTests.cs
index 046d6c6b8..9aa336cf4 100644
--- a/tests/IntegrationTests/UpdateTests.cs
+++ b/tests/IntegrationTests/UpdateTests.cs
@@ -161,5 +161,5 @@ public void UpdateFieldCount()
 		}
 	}
 
-	readonly DatabaseFixture m_database;
+	private readonly DatabaseFixture m_database;
 }
diff --git a/tests/IntegrationTests/app.config b/tests/IntegrationTests/app.config
index fc35e1bc3..15ee770dd 100644
--- a/tests/IntegrationTests/app.config
+++ b/tests/IntegrationTests/app.config
@@ -3,7 +3,8 @@
   <system.data>
     <DbProviderFactories>
       <add name="MySqlConnector" invariant="MySqlConnector" description="Async MySQL ADO.NET Connector" type="MySqlConnector.MySqlConnectorFactory, MySqlConnector, Culture=neutral, PublicKeyToken=d33d3e53aa5f8c92" />
-      <add name="MySQL Data Provider" invariant="MySql.Data.MySqlClient" description=".Net Framework Data Provider for MySQL" type="MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=8.0.31.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d" />
+      <remove invariant="MySql.Data.MySqlClient" />
+      <add name="MySQL Data Provider" invariant="MySql.Data.MySqlClient" description=".Net Framework Data Provider for MySQL" type="MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=9.4.0.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d" />
     </DbProviderFactories>
   </system.data>
 </configuration>
diff --git a/tests/MySqlConnector.DependencyInjection.Tests/DependencyInjectionTests.cs b/tests/MySqlConnector.DependencyInjection.Tests/DependencyInjectionTests.cs
new file mode 100644
index 000000000..7942994d3
--- /dev/null
+++ b/tests/MySqlConnector.DependencyInjection.Tests/DependencyInjectionTests.cs
@@ -0,0 +1,219 @@
+namespace MySqlConnector.DependencyInjection.Tests;
+
+public class DependencyInjectionTests
+{
+	[Fact]
+	public async Task MySqlDataSourceIsRegistered()
+	{
+		var serviceCollection = new ServiceCollection();
+		serviceCollection.AddMySqlDataSource(c_connectionString);
+
+		await using var serviceProvider = serviceCollection.BuildServiceProvider();
+
+		var dataSource = serviceProvider.GetRequiredService<MySqlDataSource>();
+		await using var connection = dataSource.CreateConnection();
+		Assert.Equal(c_connectionString, connection.ConnectionString);
+	}
+
+	[Fact]
+	public async Task MySqlConnectionIsRegistered()
+	{
+		var serviceCollection = new ServiceCollection();
+		serviceCollection.AddMySqlDataSource(c_connectionString);
+
+		await using var serviceProvider = serviceCollection.BuildServiceProvider();
+
+		await using var connection = serviceProvider.GetRequiredService<MySqlConnection>();
+		Assert.Equal(c_connectionString, connection.ConnectionString);
+	}
+
+	[Fact]
+	public async Task DbConnectionIsRegistered()
+	{
+		var serviceCollection = new ServiceCollection();
+		serviceCollection.AddMySqlDataSource(c_connectionString);
+
+		await using var serviceProvider = serviceCollection.BuildServiceProvider();
+
+		await using var connection = serviceProvider.GetRequiredService<DbConnection>();
+		Assert.IsAssignableFrom<MySqlConnection>(connection);
+		Assert.Equal(c_connectionString, connection.ConnectionString);
+	}
+
+	[Fact]
+	public async Task DbDataSourceIsRegistered()
+	{
+		var serviceCollection = new ServiceCollection();
+		serviceCollection.AddMySqlDataSource(c_connectionString);
+
+		await using var serviceProvider = serviceCollection.BuildServiceProvider();
+
+		await using var dataSource = serviceProvider.GetRequiredService<DbDataSource>();
+		Assert.IsAssignableFrom<MySqlDataSource>(dataSource);
+		await using var connection = dataSource.CreateConnection();
+		Assert.IsAssignableFrom<MySqlConnection>(connection);
+		Assert.Equal(c_connectionString, connection.ConnectionString);
+	}
+
+	[Fact]
+	public async Task MySqlDataSourceCanSetName()
+	{
+		var serviceCollection = new ServiceCollection();
+
+		serviceCollection.AddMySqlDataSource(c_connectionString, builder =>
+		{
+			builder.UseName("MyName");
+		});
+
+		await using var serviceProvider = serviceCollection.BuildServiceProvider();
+		var dataSource = serviceProvider.GetRequiredService<MySqlDataSource>();
+		Assert.Equal("MyName", dataSource.Name);
+	}
+
+	[Fact]
+	public async Task MySqlDataSourceCanSetNameFromServiceProvider()
+	{
+		var serviceCollection = new ServiceCollection();
+
+		serviceCollection.AddSingleton("MyName");
+		serviceCollection.AddMySqlDataSource(c_connectionString, (sp, builder) =>
+		{
+			builder.UseName(sp.GetRequiredService<string>());
+		});
+
+		await using var serviceProvider = serviceCollection.BuildServiceProvider();
+		var dataSource = serviceProvider.GetRequiredService<MySqlDataSource>();
+		Assert.Equal("MyName", dataSource.Name);
+	}
+
+	[Fact]
+	public async Task KeyedMySqlDataSourceIsRegistered()
+	{
+		var serviceCollection = new ServiceCollection();
+		serviceCollection.AddKeyedMySqlDataSource(KeyedService.AnyKey, c_connectionString);
+
+		await using var serviceProvider = serviceCollection.BuildServiceProvider();
+
+		var dataSource = serviceProvider.GetRequiredKeyedService<MySqlDataSource>(new object());
+		Assert.Null(dataSource.Name);
+		await using var connection = dataSource.CreateConnection();
+		Assert.Equal(c_connectionString, connection.ConnectionString);
+	}
+
+	[Fact]
+	public async Task StringKeyedMySqlDataSourceHasNameSet()
+	{
+		var serviceCollection = new ServiceCollection();
+		serviceCollection.AddKeyedMySqlDataSource("key", c_connectionString);
+
+		await using var serviceProvider = serviceCollection.BuildServiceProvider();
+
+		var dataSource = serviceProvider.GetRequiredKeyedService<MySqlDataSource>("key");
+		Assert.Equal("key", dataSource.Name);
+		await using var connection = dataSource.CreateConnection();
+		Assert.Equal(c_connectionString, connection.ConnectionString);
+	}
+
+	[Fact]
+	public async Task KeyedMySqlDataSourceCanSetName()
+	{
+		var serviceCollection = new ServiceCollection();
+		serviceCollection.AddKeyedMySqlDataSource("key", c_connectionString, builder => builder.UseName("MyName"));
+
+		await using var serviceProvider = serviceCollection.BuildServiceProvider();
+
+		var dataSource = serviceProvider.GetRequiredKeyedService<MySqlDataSource>("key");
+		Assert.Equal("MyName", dataSource.Name);
+		await using var connection = dataSource.CreateConnection();
+		Assert.Equal(c_connectionString, connection.ConnectionString);
+	}
+
+	[Fact]
+	public async Task KeyedMySqlDataSourceCanSetNameFromServiceProvider()
+	{
+		var serviceCollection = new ServiceCollection();
+		serviceCollection.AddSingleton("MyName");
+		serviceCollection.AddKeyedMySqlDataSource("key", c_connectionString, (sp, builder) => builder.UseName(sp.GetRequiredService<string>()));
+
+		await using var serviceProvider = serviceCollection.BuildServiceProvider();
+
+		var dataSource = serviceProvider.GetRequiredKeyedService<MySqlDataSource>("key");
+		Assert.Equal("MyName", dataSource.Name);
+		await using var connection = dataSource.CreateConnection();
+		Assert.Equal(c_connectionString, connection.ConnectionString);
+	}
+
+	[Fact]
+	public async Task KeyedMySqlDataSourceRetrievedWithStringKeyHasName()
+	{
+		var serviceCollection = new ServiceCollection();
+		serviceCollection.AddKeyedMySqlDataSource(KeyedService.AnyKey, c_connectionString);
+
+		await using var serviceProvider = serviceCollection.BuildServiceProvider();
+
+		var dataSource = serviceProvider.GetRequiredKeyedService<MySqlDataSource>("key");
+		Assert.Equal("key", dataSource.Name);
+		await using var connection = dataSource.CreateConnection();
+		Assert.Equal(c_connectionString, connection.ConnectionString);
+	}
+
+	[Fact]
+	public async Task KeyedMySqlConnectionIsRegistered()
+	{
+		var serviceCollection = new ServiceCollection();
+		serviceCollection.AddKeyedMySqlDataSource("key", c_connectionString);
+
+		await using var serviceProvider = serviceCollection.BuildServiceProvider();
+
+		await using var connection = serviceProvider.GetRequiredKeyedService<MySqlConnection>("key");
+		Assert.Equal(c_connectionString, connection.ConnectionString);
+	}
+
+	[Fact]
+	public async Task TwoKeyedMySqlDataConnectionsAreRegistered()
+	{
+		const string c_connectionString2 = c_connectionString + ";Database=test";
+
+		var serviceCollection = new ServiceCollection();
+		serviceCollection.AddKeyedMySqlDataSource(KeyedService.AnyKey, c_connectionString);
+		serviceCollection.AddKeyedMySqlDataSource("key2", c_connectionString2);
+
+		await using var serviceProvider = serviceCollection.BuildServiceProvider();
+
+		await using var connection1 = serviceProvider.GetRequiredKeyedService<MySqlConnection>("key");
+		Assert.Equal(c_connectionString, connection1.ConnectionString);
+
+		await using var connection2 = serviceProvider.GetRequiredKeyedService<MySqlConnection>("key2");
+		Assert.Equal(c_connectionString2, connection2.ConnectionString);
+	}
+
+	[Fact]
+	public async Task KeyedDbConnectionIsRegistered()
+	{
+		var serviceCollection = new ServiceCollection();
+		serviceCollection.AddKeyedMySqlDataSource("key", c_connectionString);
+
+		await using var serviceProvider = serviceCollection.BuildServiceProvider();
+
+		await using var connection = serviceProvider.GetRequiredKeyedService<DbConnection>("key");
+		Assert.IsAssignableFrom<MySqlConnection>(connection);
+		Assert.Equal(c_connectionString, connection.ConnectionString);
+	}
+
+	[Fact]
+	public async Task KeyedDbDataSourceIsRegistered()
+	{
+		var serviceCollection = new ServiceCollection();
+		serviceCollection.AddKeyedMySqlDataSource("key", c_connectionString);
+
+		await using var serviceProvider = serviceCollection.BuildServiceProvider();
+
+		await using var dataSource = serviceProvider.GetRequiredKeyedService<DbDataSource>("key");
+		Assert.IsAssignableFrom<MySqlDataSource>(dataSource);
+		await using var connection = dataSource.CreateConnection();
+		Assert.IsAssignableFrom<MySqlConnection>(connection);
+		Assert.Equal(c_connectionString, connection.ConnectionString);
+	}
+
+	private const string c_connectionString = "Server=localhost;User ID=root;Password=pass";
+}
diff --git a/tests/MySqlConnector.DependencyInjection.Tests/MySqlConnector.DependencyInjection.Tests.csproj b/tests/MySqlConnector.DependencyInjection.Tests/MySqlConnector.DependencyInjection.Tests.csproj
new file mode 100644
index 000000000..ebeeef845
--- /dev/null
+++ b/tests/MySqlConnector.DependencyInjection.Tests/MySqlConnector.DependencyInjection.Tests.csproj
@@ -0,0 +1,33 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFrameworks>net9.0</TargetFrameworks>
+    <SignAssembly>true</SignAssembly>
+    <PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
+    <AssemblyOriginatorKeyFile>..\..\MySqlConnector.snk</AssemblyOriginatorKeyFile>
+    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+    <Nullable>enable</Nullable>
+    <ImplicitUsings>enable</ImplicitUsings>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" />
+    <PackageReference Include="xunit" />
+    <PackageReference Include="xunit.runner.visualstudio">
+      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+      <PrivateAssets>all</PrivateAssets>
+    </PackageReference>
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="../../src/MySqlConnector.DependencyInjection/MySqlConnector.DependencyInjection.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Using Include="Microsoft.Extensions.DependencyInjection" />
+    <Using Include="System.Data.Common" />
+    <Using Include="Xunit" />
+  </ItemGroup>
+
+</Project>
diff --git a/tests/MySqlConnector.NativeAot.Tests/MySqlConnector.NativeAot.Tests.csproj b/tests/MySqlConnector.NativeAot.Tests/MySqlConnector.NativeAot.Tests.csproj
new file mode 100644
index 000000000..3797dedb8
--- /dev/null
+++ b/tests/MySqlConnector.NativeAot.Tests/MySqlConnector.NativeAot.Tests.csproj
@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFrameworks>net8.0;net9.0</TargetFrameworks>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>enable</Nullable>
+    <PublishAot>true</PublishAot>
+    <InvariantGlobalization>true</InvariantGlobalization>
+    <StripSymbols>true</StripSymbols>
+    <UseSystemResourceKeys>true</UseSystemResourceKeys>
+    <IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\..\src\MySqlConnector\MySqlConnector.csproj" />
+    <ProjectReference Include="..\..\src\MySqlConnector.DependencyInjection\MySqlConnector.DependencyInjection.csproj" />
+  </ItemGroup>
+
+</Project>
diff --git a/tests/MySqlConnector.NativeAot.Tests/Program.cs b/tests/MySqlConnector.NativeAot.Tests/Program.cs
new file mode 100644
index 000000000..f72c65d71
--- /dev/null
+++ b/tests/MySqlConnector.NativeAot.Tests/Program.cs
@@ -0,0 +1,26 @@
+using MySqlConnector;
+
+var connectionString = Environment.GetEnvironmentVariable("CONNECTION_STRING") ?? "Server=localhost;Username=root;Password=pass";
+
+await using var dataSource = new MySqlDataSourceBuilder(connectionString).Build();
+
+await using var conn = dataSource.CreateConnection();
+await conn.OpenAsync();
+await using var cmd = new MySqlCommand("SELECT 'Hello World'", conn);
+await using var reader = await cmd.ExecuteReaderAsync();
+if (!await reader.ReadAsync())
+	throw new Exception("ReadAsync returned false");
+
+var value = reader.GetFieldValue<string>(0);
+if (value != "Hello World")
+	throw new Exception($"Expected 'Hello World'; got '{value}'");
+
+var schema = reader.GetColumnSchema();
+if (schema.Count != 1)
+	throw new Exception($"Expected 1 column, got {schema.Count}");
+if (((MySqlDbColumn) schema[0]).ProviderType != MySqlDbType.VarChar)
+	throw new Exception($"Expected column type to be MySqlDbType.VarChar, got {((MySqlDbColumn) schema[0]).ProviderType}");
+if (reader.GetFieldType(0) != typeof(string))
+	throw new Exception($"Expected column type to be System.String, got {reader.GetFieldType(0)}");
+
+Console.WriteLine("Success");
diff --git a/tests/MySqlConnector.Tests/ByteBufferWriterTests.cs b/tests/MySqlConnector.Tests/ByteBufferWriterTests.cs
index 0906f5623..01beb3aba 100644
--- a/tests/MySqlConnector.Tests/ByteBufferWriterTests.cs
+++ b/tests/MySqlConnector.Tests/ByteBufferWriterTests.cs
@@ -1,5 +1,4 @@
 using MySqlConnector.Protocol.Serialization;
-using Xunit;
 
 namespace MySqlConnector.Tests;
 
diff --git a/tests/MySqlConnector.Tests/CachedProcedureTests.cs b/tests/MySqlConnector.Tests/CachedProcedureTests.cs
index 1a590e7b9..78484d00d 100644
--- a/tests/MySqlConnector.Tests/CachedProcedureTests.cs
+++ b/tests/MySqlConnector.Tests/CachedProcedureTests.cs
@@ -1,16 +1,12 @@
-using System.Collections.Generic;
-using MySqlConnector.Core;
-using Xunit;
-
 namespace MySqlConnector.Tests;
 
 public class CachedProcedureTests
 {
 	[Theory]
 	[MemberData(nameof(CreateParseableParameters))]
-	public void ParseParameters(string sql, object[] expected)
+	public void ParseParameters(string sql, MySqlGuidFormat guidFormat, object[] expected)
 	{
-		var actual = CachedProcedure.ParseParameters(sql);
+		var actual = CachedProcedure.ParseParameters(sql, guidFormat);
 		Assert.Equal(expected.Length, actual.Count);
 		for (int i = 0; i < expected.Length; i++)
 		{
@@ -29,120 +25,121 @@ public static IEnumerable<object[]> CreateParseableParameters()
 		{
 			new object[]
 			{
-				"", new object[0],
-			},
-			new object[]
-			{
-				"/* no, parameters */", new object[0],
+				"", MySqlGuidFormat.Binary16, new object[0],
 			},
-			new object[]
-			{
-				"IN test INT", new object[]
+			[
+				"/* no, parameters */", MySqlGuidFormat.Binary16, new object[0],
+			],
+			[
+				"IN test INT", MySqlGuidFormat.Binary16, new object[]
 				{
-					new CachedParameter(1, "IN", "test", "INT", false, 0),
+					new CachedParameter(1, "IN", "test", "INT", false, 0, MySqlGuidFormat.Binary16),
 				}
-			},
-			new object[]
-			{
-				"IN test INT UNSIGNED", new object[]
+			],
+			[
+				"IN test INT UNSIGNED", MySqlGuidFormat.Binary16, new object[]
 				{
-					new CachedParameter(1, "IN", "test", "INT", true, 0),
+					new CachedParameter(1, "IN", "test", "INT", true, 0, MySqlGuidFormat.Binary16),
 				}
-			},
-			new object[]
-			{
-				"-- IN ignored INT UNSIGNED,\r\nIN notignored INT", new object[]
+			],
+			[
+				"-- IN ignored INT UNSIGNED,\r\nIN notignored INT", MySqlGuidFormat.Binary16, new object[]
 				{
-					new CachedParameter(1, "IN", "notignored", "INT", false, 0),
+					new CachedParameter(1, "IN", "notignored", "INT", false, 0, MySqlGuidFormat.Binary16),
 				}
-			},
-			new object[]
-			{
-				"IN param1 INT,\r\nIN param2 INT", new object[]
+			],
+			[
+				"IN param1 INT,\r\nIN param2 INT", MySqlGuidFormat.Binary16, new object[]
 				{
-					new CachedParameter(1, "IN", "param1", "INT", false, 0),
-					new CachedParameter(2, "IN", "param2", "INT", false, 0),
+					new CachedParameter(1, "IN", "param1", "INT", false, 0, MySqlGuidFormat.Binary16),
+					new CachedParameter(2, "IN", "param2", "INT", false, 0, MySqlGuidFormat.Binary16),
 				}
-			},
-			new object[]
-			{
-				"IN /* ignored BIGINT,\r\nIN*/ param1 INT", new object[]
+			],
+			[
+				"IN /* ignored BIGINT,\r\nIN*/ param1 INT", MySqlGuidFormat.Binary16, new object[]
 				{
-					new CachedParameter(1, "IN", "param1", "INT", false, 0),
+					new CachedParameter(1, "IN", "param1", "INT", false, 0, MySqlGuidFormat.Binary16),
 				}
-			},
-			new object[]
-			{
-				"IN param1 INT(11)", new object[]
+			],
+			[
+				"IN param1 INT(11)", MySqlGuidFormat.Binary16, new object[]
 				{
-					new CachedParameter(1, "IN", "param1", "INT", false, 11),
+					new CachedParameter(1, "IN", "param1", "INT", false, 11, MySqlGuidFormat.Binary16),
 				}
-			},
-			new object[]
-			{
-				"param1 BIGINT(21) UNSIGNED ZEROFILL", new object[]
+			],
+			[
+				"param1 BIGINT(21) UNSIGNED ZEROFILL", MySqlGuidFormat.Binary16, new object[]
 				{
-					new CachedParameter(1, "IN", "param1", "BIGINT", true, 21),
+					new CachedParameter(1, "IN", "param1", "BIGINT", true, 21, MySqlGuidFormat.Binary16),
 				}
-			},
-			new object[]
-			{
-				"param1 VARCHAR(63)", new object[]
+			],
+			[
+				"param1 VARCHAR(63)", MySqlGuidFormat.Binary16, new object[]
 				{
-					new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63),
+					new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63, MySqlGuidFormat.Binary16),
 				}
-			},
-			new object[]
-			{
-				"param1 VARCHAR(63) CHARSET latin1", new object[]
+			],
+			[
+				"param1 VARCHAR(63) CHARSET latin1", MySqlGuidFormat.Binary16, new object[]
 				{
-					new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63),
+					new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63, MySqlGuidFormat.Binary16),
 				}
-			},
-			new object[]
-			{
-				"param1 VARCHAR(63) COLLATE utf8bin", new object[]
+			],
+			[
+				"param1 VARCHAR(63) COLLATE utf8bin", MySqlGuidFormat.Binary16, new object[]
 				{
-					new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63),
+					new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63, MySqlGuidFormat.Binary16),
 				}
-			},
-			new object[]
-			{
-				"param1 VARCHAR(63) CHARACTER SET latin1 COLLATE latin1_bin", new object[]
+			],
+			[
+				"param1 VARCHAR(63) CHARACTER SET latin1 COLLATE latin1_bin", MySqlGuidFormat.Binary16, new object[]
 				{
-					new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63),
+					new CachedParameter(1, "IN", "param1", "VARCHAR", false, 63, MySqlGuidFormat.Binary16),
 				}
-			},
-			new object[]
-			{
-				"`par``am` INT", new object[]
+			],
+			[
+				"`par``am` INT", MySqlGuidFormat.Binary16, new object[]
 				{
-					new CachedParameter(1, "IN", "par`am", "INT", false, 0),
+					new CachedParameter(1, "IN", "par`am", "INT", false, 0, MySqlGuidFormat.Binary16),
 				}
-			},
-			new object[]
-			{
-				"IN input enum ('One', 'Two', 'Three')", new object[]
+			],
+			[
+				"IN input enum ('One', 'Two', 'Three')", MySqlGuidFormat.Binary16, new object[]
 				{
-					new CachedParameter(1, "IN", "input", "ENUM", false, 0),
+					new CachedParameter(1, "IN", "input", "ENUM", false, 0, MySqlGuidFormat.Binary16),
 				}
-			},
-			new object[]
-			{
-				"OUT param DECIMAL(10,5)", new object[]
+			],
+			[
+				"OUT param DECIMAL(10,5)", MySqlGuidFormat.Binary16, new object[]
 				{
-					new CachedParameter(1, "OUT", "param", "DECIMAL", false, 0),
+					new CachedParameter(1, "OUT", "param", "DECIMAL", false, 0, MySqlGuidFormat.Binary16),
 				}
-			},
-			new object[]
-			{
-				"INOUT param LONGTEXT", new object[]
+			],
+			[
+				"INOUT param LONGTEXT", MySqlGuidFormat.Binary16, new object[]
 				{
-					new CachedParameter(1, "INOUT", "param", "LONGTEXT", false, 0),
+					new CachedParameter(1, "INOUT", "param", "LONGTEXT", false, 0, MySqlGuidFormat.Binary16),
 				}
-			},
-			new object[]
-			{
+			],
+			[
+				"OUT param1 BINARY(16)", MySqlGuidFormat.Binary16, new object[]
+				{
+					new CachedParameter(1, "OUT", "param1", "BINARY", false, 16, MySqlGuidFormat.Binary16),
+				}
+			],
+			[
+				"OUT param1 CHAR(36)", MySqlGuidFormat.Char36, new object[]
+				{
+					new CachedParameter(1, "OUT", "param1", "CHAR", false, 36, MySqlGuidFormat.Char36),
+				}
+			],
+			[
+				"ColSet set('set1','set2','set3')", MySqlGuidFormat.Binary16, new object[]
+				{
+					new CachedParameter(1, "IN", "ColSet", "SET", false, 0, MySqlGuidFormat.Binary16),
+				}
+			],
+			[
 				@"IN param1 DATETIME(6),
 -- ignored1
 OUT param2 /* ignore */ INT,
@@ -150,18 +147,17 @@ param3 DECIMAL(20,10),
 inout param4 VARCHAR(63) CHARSET latin1,
 param5 bigint(20) unsigned zerofill,
 out param6 bool",
-				new object[]
+				MySqlGuidFormat.Binary16, new object[]
 				{
-					new CachedParameter(1, "IN", "param1", "DATETIME", false, 6),
-					new CachedParameter(2, "OUT", "param2", "INT", false, 0),
-					new CachedParameter(3, "IN", "param3", "DECIMAL", false, 0),
-					new CachedParameter(4, "INOUT", "param4", "VARCHAR", false, 63),
-					new CachedParameter(5, "IN", "param5", "BIGINT", true, 20),
-					new CachedParameter(6, "OUT", "param6", "TINYINT", false, 1),
+					new CachedParameter(1, "IN", "param1", "DATETIME", false, 6, MySqlGuidFormat.Binary16),
+					new CachedParameter(2, "OUT", "param2", "INT", false, 0, MySqlGuidFormat.Binary16),
+					new CachedParameter(3, "IN", "param3", "DECIMAL", false, 0, MySqlGuidFormat.Binary16),
+					new CachedParameter(4, "INOUT", "param4", "VARCHAR", false, 63, MySqlGuidFormat.Binary16),
+					new CachedParameter(5, "IN", "param5", "BIGINT", true, 20, MySqlGuidFormat.Binary16),
+					new CachedParameter(6, "OUT", "param6", "TINYINT", false, 1, MySqlGuidFormat.Binary16),
 				}
-			},
-			new object[]
-			{
+			],
+			[
 				@"
 param1 boolean,
 param2 nvarchar,
@@ -169,14 +165,14 @@ param3 real(20,10),
 -- ignored INT
 param4 INTEGER(3)
 ",
-				new object[]
+				MySqlGuidFormat.Binary16, new object[]
 				{
-					new CachedParameter(1, "IN", "param1", "TINYINT", false, 1),
-					new CachedParameter(2, "IN", "param2", "VARCHAR", false, 0),
-					new CachedParameter(3, "IN", "param3", "DOUBLE", false, 20),
-					new CachedParameter(4, "IN", "param4", "INT", false, 3),
+					new CachedParameter(1, "IN", "param1", "TINYINT", false, 1, MySqlGuidFormat.Binary16),
+					new CachedParameter(2, "IN", "param2", "VARCHAR", false, 0, MySqlGuidFormat.Binary16),
+					new CachedParameter(3, "IN", "param3", "DOUBLE", false, 20, MySqlGuidFormat.Binary16),
+					new CachedParameter(4, "IN", "param4", "INT", false, 3, MySqlGuidFormat.Binary16),
 				}
-			},
+			],
 		};
 	}
 
@@ -211,6 +207,7 @@ param4 INTEGER(3)
 	[InlineData("NUMERIC(12)", "DECIMAL", false, 12)]
 	[InlineData("FIXED(12)", "DECIMAL", false, 12)]
 	[InlineData("ENUM('a','b','c')", "ENUM", false, 0)]
+	[InlineData("SET('a','b','c')", "SET", false, 0)]
 	public void ParseDataType(string sql, string expectedDataType, bool expectedUnsigned, int expectedLength)
 	{
 		var dataType = CachedProcedure.ParseDataType(sql, out var unsigned, out var length);
diff --git a/tests/MySqlConnector.Tests/CancellationTests.cs b/tests/MySqlConnector.Tests/CancellationTests.cs
index de904b738..e70967e4a 100644
--- a/tests/MySqlConnector.Tests/CancellationTests.cs
+++ b/tests/MySqlConnector.Tests/CancellationTests.cs
@@ -1,11 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Data;
-using System.Diagnostics;
 using System.Net.Sockets;
-using System.Threading;
-using System.Threading.Tasks;
-using Xunit;
 
 namespace MySqlConnector.Tests;
 
@@ -25,7 +18,7 @@ public CancellationTests()
 
 	public void Dispose() => m_server.Stop();
 
-	// NOTE: Multiple nested classes in order to force tests to run in parallel against each other
+	//// NOTE: Multiple nested classes in order to force tests to run in parallel against each other
 
 	public class CancelWithCommandTimeout : CancellationTests
 	{
@@ -407,7 +400,7 @@ public async Task ExecuteAsync(int step, int method)
 
 	public static IEnumerable<object[]> GetSyncMethodSteps()
 	{
-		for (var step = 1; step <=  12; step++)
+		for (var step = 1; step <= 12; step++)
 		{
 			for (var method = 0; method < s_executeMethods.Length; method++)
 			{
@@ -427,13 +420,24 @@ public static IEnumerable<object[]> GetAsyncMethodSteps()
 		}
 	}
 
-	private static readonly Func<MySqlCommand, int>[] s_executeMethods = new Func<MySqlCommand, int>[] { ExecuteScalar, ExecuteNonQuery, ExecuteReader };
-	private static readonly Func<MySqlCommand, CancellationToken, Task<int>>[] s_executeAsyncMethods = new Func<MySqlCommand, CancellationToken, Task<int>>[] { ExecuteScalarAsync, ExecuteNonQueryAsync, ExecuteReaderAsync };
+	private static readonly Func<MySqlCommand, int>[] s_executeMethods = [ExecuteScalar, ExecuteNonQuery, ExecuteReader];
+	private static readonly Func<MySqlCommand, CancellationToken, Task<int>>[] s_executeAsyncMethods = [ExecuteScalarAsync, ExecuteNonQueryAsync, ExecuteReaderAsync];
 
 	private static int ExecuteScalar(MySqlCommand command) => (int) command.ExecuteScalar();
 	private static async Task<int> ExecuteScalarAsync(MySqlCommand command, CancellationToken token) => (int) await command.ExecuteScalarAsync(token);
-	private static int ExecuteNonQuery(MySqlCommand command) { command.ExecuteNonQuery(); return 0; }
-	private static async Task<int> ExecuteNonQueryAsync(MySqlCommand command, CancellationToken token) { await command.ExecuteNonQueryAsync(token); return 0; }
+
+	private static int ExecuteNonQuery(MySqlCommand command)
+	{
+		command.ExecuteNonQuery();
+		return 0;
+	}
+
+	private static async Task<int> ExecuteNonQueryAsync(MySqlCommand command, CancellationToken token)
+	{
+		await command.ExecuteNonQueryAsync(token);
+		return 0;
+	}
+
 	private static int ExecuteReader(MySqlCommand command)
 	{
 		using var reader = command.ExecuteReader();
@@ -457,6 +461,6 @@ private static async Task<int> ExecuteReaderAsync(MySqlCommand command, Cancella
 		return value.Value;
 	}
 
-	readonly FakeMySqlServer m_server;
-	readonly MySqlConnectionStringBuilder m_csb;
+	private readonly FakeMySqlServer m_server;
+	private readonly MySqlConnectionStringBuilder m_csb;
 }
diff --git a/tests/MySqlConnector.Tests/ColumnCountPayloadTests.cs b/tests/MySqlConnector.Tests/ColumnCountPayloadTests.cs
new file mode 100644
index 000000000..010d6793c
--- /dev/null
+++ b/tests/MySqlConnector.Tests/ColumnCountPayloadTests.cs
@@ -0,0 +1,21 @@
+﻿using MySqlConnector.Protocol.Payloads;
+
+namespace MySqlConnector.Tests;
+
+public class ColumnCountPayloadTests
+{
+	[Theory]
+	[InlineData(new byte[] { 1 }, false, 1, true)]
+	[InlineData(new byte[] { 1, 1 }, true, 1, true)]
+	[InlineData(new byte[] { 1, 0 }, true, 1, false)]
+	[InlineData(new byte[] { 2 }, false, 2, true)]
+	[InlineData(new byte[] { 2, 1 }, true, 2, true)]
+	[InlineData(new byte[] { 2, 0 }, true, 2, false)]
+	[InlineData(new byte[] { 2 }, true, 2, true)] // this seems like invalid data, but a server may return this
+	public void ParseResultSetHeader(byte[] span, bool supportsOptionalMetadata, int expectedColumnCount, bool expectedMetadataFollows)
+	{
+		var columnCountPayload = ColumnCountPayload.Create(span.AsSpan(), supportsOptionalMetadata);
+		Assert.Equal(expectedColumnCount, columnCountPayload.ColumnCount);
+		Assert.Equal(expectedMetadataFollows, columnCountPayload.MetadataFollows);
+	}
+}
diff --git a/tests/MySqlConnector.Tests/ColumnReaderTests.cs b/tests/MySqlConnector.Tests/ColumnReaderTests.cs
new file mode 100644
index 000000000..88ba92d51
--- /dev/null
+++ b/tests/MySqlConnector.Tests/ColumnReaderTests.cs
@@ -0,0 +1,92 @@
+using System.Text;
+using MySqlConnector.ColumnReaders;
+
+namespace MySqlConnector.Tests;
+
+public class ColumnReaderTests
+{
+	[Theory]
+	[InlineData("0001-01-01", "0001-01-01T00:00:00.0000000")]
+	[InlineData("2345-12-31", "2345-12-31T00:00:00.0000000")]
+	[InlineData("2345-12-31 12:34:56", "2345-12-31T12:34:56.0000000")]
+	[InlineData("2345-12-31 12:34:56.1", "2345-12-31T12:34:56.1000000")]
+	[InlineData("2345-12-31 12:34:56.12", "2345-12-31T12:34:56.1200000")]
+	[InlineData("2345-12-31 12:34:56.123", "2345-12-31T12:34:56.1230000")]
+	[InlineData("2345-12-31 12:34:56.1234", "2345-12-31T12:34:56.1234000")]
+	[InlineData("2345-12-31 12:34:56.12345", "2345-12-31T12:34:56.1234500")]
+	[InlineData("2345-12-31 12:34:56.123456", "2345-12-31T12:34:56.1234560")]
+	[InlineData("2345-12-31 12:34:56.1234567", "2345-12-31T12:34:56.1234567")]
+	[InlineData("2345-12-31 12:34:56.01", "2345-12-31T12:34:56.0100000")]
+	[InlineData("2345-12-31 12:34:56.001", "2345-12-31T12:34:56.0010000")]
+	[InlineData("2345-12-31 12:34:56.0001", "2345-12-31T12:34:56.0001000")]
+	[InlineData("2345-12-31 12:34:56.00001", "2345-12-31T12:34:56.0000100")]
+	[InlineData("2345-12-31 12:34:56.000001", "2345-12-31T12:34:56.0000010")]
+	[InlineData("2345-12-31 12:34:56.0000001", "2345-12-31T12:34:56.0000001")]
+	[InlineData("9999-12-31 23:59:59.999999", "9999-12-31T23:59:59.9999990")]
+	[InlineData("9999-12-31 23:59:59.9999999", "9999-12-31T23:59:59.9999999")]
+	public void ParseDateTime(string input, string expected)
+	{
+		var actual = (DateTime) TextDateTimeColumnReader.ParseDateTime(Encoding.UTF8.GetBytes(input), convertZeroDateTime: false, allowZeroDateTime: false, dateTimeKind: DateTimeKind.Unspecified);
+		Assert.Equal(expected, actual.ToString("O"));
+	}
+
+	[Theory]
+	[InlineData("0001-01-01", "0001-01-01T00:00:00.0000000")]
+	[InlineData("2345-12-31", "2345-12-31T00:00:00.0000000")]
+	[InlineData("2345-12-31 12:34:56", "2345-12-31T12:34:56.0000000")]
+	[InlineData("2345-12-31 12:34:56.1", "2345-12-31T12:34:56.1000000")]
+	[InlineData("2345-12-31 12:34:56.12", "2345-12-31T12:34:56.1200000")]
+	[InlineData("2345-12-31 12:34:56.123", "2345-12-31T12:34:56.1230000")]
+	[InlineData("2345-12-31 12:34:56.1234", "2345-12-31T12:34:56.1234000")]
+	[InlineData("2345-12-31 12:34:56.12345", "2345-12-31T12:34:56.1234500")]
+	[InlineData("2345-12-31 12:34:56.123456", "2345-12-31T12:34:56.1234560")]
+	[InlineData("2345-12-31 12:34:56.01", "2345-12-31T12:34:56.0100000")]
+	[InlineData("2345-12-31 12:34:56.001", "2345-12-31T12:34:56.0010000")]
+	[InlineData("2345-12-31 12:34:56.0001", "2345-12-31T12:34:56.0001000")]
+	[InlineData("2345-12-31 12:34:56.00001", "2345-12-31T12:34:56.0000100")]
+	[InlineData("2345-12-31 12:34:56.000001", "2345-12-31T12:34:56.0000010")]
+	[InlineData("9999-12-31 23:59:59.999999", "9999-12-31T23:59:59.9999990")]
+	public void ParseMySqlDateTime(string input, string expected)
+	{
+		var actual = (MySqlDateTime) TextDateTimeColumnReader.ParseDateTime(Encoding.UTF8.GetBytes(input), convertZeroDateTime: false, allowZeroDateTime: true, dateTimeKind: DateTimeKind.Unspecified);
+		Assert.Equal(expected, actual.GetDateTime().ToString("O"));
+	}
+
+	[Fact]
+	public void ConvertZeroDateTime()
+	{
+		var dateTime = (DateTime) TextDateTimeColumnReader.ParseDateTime("0000-00-00"u8, convertZeroDateTime: true, allowZeroDateTime: false, DateTimeKind.Utc);
+		Assert.Equal(DateTime.MinValue, dateTime);
+	}
+
+	[Fact]
+	public void AllowZeroDateTime()
+	{
+		var mySqlDateTime = (MySqlDateTime) TextDateTimeColumnReader.ParseDateTime("0000-00-00"u8, convertZeroDateTime: false, allowZeroDateTime: true, DateTimeKind.Utc);
+		Assert.Equal(default, mySqlDateTime);
+	}
+
+	[Theory]
+	[InlineData("2")]
+	[InlineData("20")]
+	[InlineData("200")]
+	[InlineData("2000")]
+	[InlineData("2000-1")]
+	[InlineData("2000-1-1")]
+	[InlineData("2000-00-01")]
+	[InlineData("2000-13-01")]
+	[InlineData("2000-01-00")]
+	[InlineData("2000-01-32")]
+	[InlineData("10000-01-01")]
+	[InlineData("2000-01-01 01")]
+	[InlineData("2000-01-01 01:01")]
+	[InlineData("2000-01-01 01:01:60")]
+	[InlineData("2000-01-01T01:01:01")]
+	[InlineData("2000-01-01 01-01-01")]
+	[InlineData("2000-01-01 01:01:01.12345678")]
+	[InlineData("2000-01-01 01:01:01.123456789")]
+	public void InvalidDateTime(string input)
+	{
+		Assert.Throws<FormatException>(() => TextDateTimeColumnReader.ParseDateTime(Encoding.UTF8.GetBytes(input), convertZeroDateTime: false, allowZeroDateTime: false, dateTimeKind: DateTimeKind.Unspecified));
+	}
+}
diff --git a/tests/MySqlConnector.Tests/ConnectionOpenedCallbackTests.cs b/tests/MySqlConnector.Tests/ConnectionOpenedCallbackTests.cs
new file mode 100644
index 000000000..bf9b72e84
--- /dev/null
+++ b/tests/MySqlConnector.Tests/ConnectionOpenedCallbackTests.cs
@@ -0,0 +1,140 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MySqlConnector.Tests;
+
+public class ConnectionOpenedCallbackTests : IDisposable
+{
+	public ConnectionOpenedCallbackTests()
+	{
+		m_server = new();
+		m_server.Start();
+
+		m_csb = new MySqlConnectionStringBuilder()
+		{
+			Server = "localhost",
+			Port = (uint) m_server.Port,
+		};
+		m_dataSource = new MySqlDataSourceBuilder(m_csb.ConnectionString)
+			.UseConnectionOpenedCallback(OnConnectionOpenedAsync)
+			.Build();
+	}
+
+	public void Dispose()
+	{
+		m_dataSource.Dispose();
+		m_server.Stop();
+	}
+
+	[Fact]
+	public void CallbackIsInvoked()
+	{
+		using (var connection = m_dataSource.CreateConnection())
+		{
+			Assert.Equal(0, m_connectionOpenedCount);
+			Assert.Equal(MySqlConnectionOpenedConditions.None, m_connectionOpenedConditions);
+
+			connection.Open();
+
+			Assert.Equal(1, m_connectionOpenedCount);
+			Assert.Equal(MySqlConnectionOpenedConditions.New, m_connectionOpenedConditions);
+		}
+	}
+
+	[Fact]
+	public void CallbackIsInvokedForPooledConnection()
+	{
+		using (var connection = m_dataSource.CreateConnection())
+		{
+			Assert.Equal(0, m_connectionOpenedCount);
+			Assert.Equal(MySqlConnectionOpenedConditions.None, m_connectionOpenedConditions);
+
+			connection.Open();
+
+			Assert.Equal(1, m_connectionOpenedCount);
+			Assert.Equal(MySqlConnectionOpenedConditions.New, m_connectionOpenedConditions);
+		}
+
+		using (var connection = m_dataSource.OpenConnection())
+		{
+			Assert.Equal(2, m_connectionOpenedCount);
+			Assert.Equal(MySqlConnectionOpenedConditions.Reset, m_connectionOpenedConditions);
+		}
+
+		using (var connection = m_dataSource.OpenConnection())
+		{
+			Assert.Equal(3, m_connectionOpenedCount);
+			Assert.Equal(MySqlConnectionOpenedConditions.Reset, m_connectionOpenedConditions);
+		}
+	}
+
+	[Fact]
+	public void CallbackIsInvokedForNonPooledConnection()
+	{
+		var csb = new MySqlConnectionStringBuilder(m_csb.ConnectionString)
+		{
+			Pooling = false,
+		};
+		using var dataSource = new MySqlDataSourceBuilder(csb.ConnectionString)
+			.UseConnectionOpenedCallback(OnConnectionOpenedAsync)
+			.Build();
+
+		using (var connection = dataSource.OpenConnection())
+		{
+			Assert.Equal(1, m_connectionOpenedCount);
+			Assert.Equal(MySqlConnectionOpenedConditions.New, m_connectionOpenedConditions);
+		}
+
+		using (var connection = dataSource.OpenConnection())
+		{
+			Assert.Equal(2, m_connectionOpenedCount);
+			Assert.Equal(MySqlConnectionOpenedConditions.New, m_connectionOpenedConditions);
+		}
+
+		using (var connection = dataSource.OpenConnection())
+		{
+			Assert.Equal(3, m_connectionOpenedCount);
+			Assert.Equal(MySqlConnectionOpenedConditions.New, m_connectionOpenedConditions);
+		}
+	}
+
+	[Fact]
+	public void ConditionsForNonResetConnection()
+	{
+		var csb = new MySqlConnectionStringBuilder(m_csb.ConnectionString)
+		{
+			ConnectionReset = false,
+		};
+		using var dataSource = new MySqlDataSourceBuilder(csb.ConnectionString)
+			.UseConnectionOpenedCallback(OnConnectionOpenedAsync)
+			.Build();
+
+		using (var connection = dataSource.OpenConnection())
+		{
+			Assert.Equal(1, m_connectionOpenedCount);
+			Assert.Equal(MySqlConnectionOpenedConditions.New, m_connectionOpenedConditions);
+		}
+		using (var connection = dataSource.OpenConnection())
+		{
+			Assert.Equal(2, m_connectionOpenedCount);
+			Assert.Equal(MySqlConnectionOpenedConditions.None, m_connectionOpenedConditions);
+		}
+	}
+
+	private ValueTask OnConnectionOpenedAsync(MySqlConnectionOpenedContext context, CancellationToken cancellationToken)
+	{
+		m_connectionOpenedCount++;
+		m_connectionOpenedConditions = context.Conditions;
+		return default;
+	}
+
+	private readonly FakeMySqlServer m_server;
+	private readonly MySqlConnectionStringBuilder m_csb;
+	private readonly MySqlDataSource m_dataSource;
+
+	private int m_connectionOpenedCount;
+	private MySqlConnectionOpenedConditions m_connectionOpenedConditions;
+}
diff --git a/tests/MySqlConnector.Tests/ConnectionTests.cs b/tests/MySqlConnector.Tests/ConnectionTests.cs
index 42c621346..abe4cf99c 100644
--- a/tests/MySqlConnector.Tests/ConnectionTests.cs
+++ b/tests/MySqlConnector.Tests/ConnectionTests.cs
@@ -1,10 +1,3 @@
-using System;
-using System.Data;
-using System.Diagnostics;
-using System.Threading;
-using System.Threading.Tasks;
-using Xunit;
-
 namespace MySqlConnector.Tests;
 
 public class ConnectionTests : IDisposable
@@ -186,9 +179,11 @@ public void PingWhenClosed()
 	[Fact]
 	public void ConnectionTimeout()
 	{
-		m_server.BlockOnConnect = true;
-		var csb = new MySqlConnectionStringBuilder(m_csb.ConnectionString);
-		csb.ConnectionTimeout = 4;
+		m_server.ConnectDelay = TimeSpan.FromSeconds(10);
+		var csb = new MySqlConnectionStringBuilder(m_csb.ConnectionString)
+		{
+			ConnectionTimeout = 4,
+		};
 		using var connection = new MySqlConnection(csb.ConnectionString);
 		var stopwatch = Stopwatch.StartNew();
 		try
@@ -206,8 +201,10 @@ public void ConnectionTimeout()
 	[Fact]
 	public void ResetConnectionTimeout()
 	{
-		var csb = new MySqlConnectionStringBuilder(m_csb.ConnectionString);
-		csb.ConnectionTimeout = 4;
+		var csb = new MySqlConnectionStringBuilder(m_csb.ConnectionString)
+		{
+			ConnectionTimeout = 4,
+		};
 		using var connection = new MySqlConnection(csb.ConnectionString);
 		connection.Open();
 		connection.Close();
@@ -237,6 +234,49 @@ public void ReadInfinity()
 		Assert.False(reader.Read());
 	}
 
+	[Fact]
+	public void ReplaceActiveReader()
+	{
+		var connection = new MySqlConnection(m_csb.ConnectionString);
+		connection.Open();
+		using (var command = connection.CreateCommand())
+		{
+			command.CommandText = "select disconnect";
+			command.CommandTimeout = 600;
+			var reader = command.ExecuteReader();
+			Assert.True(reader.Read());
+			Assert.Equal(1, reader.GetInt32(0));
+		}
+
+		try
+		{
+			connection.Close();
+		}
+		catch (MySqlEndOfStreamException)
+		{
+		}
+
+		connection.Open();
+		using (var command = connection.CreateCommand())
+		{
+			command.CommandText = "SELECT 1;";
+			var reader = command.ExecuteReader();
+			Assert.True(reader.Read());
+			Assert.Equal(1, reader.GetInt32(0));
+		}
+		connection.Close();
+
+		connection.Open();
+		using (var command = connection.CreateCommand())
+		{
+			command.CommandText = "SELECT 2;";
+			var reader = command.ExecuteReader();
+			Assert.True(reader.Read());
+			Assert.Equal(2, reader.GetInt32(0));
+		}
+		connection.Close();
+	}
+
 	private static async Task WaitForConditionAsync<T>(T expected, Func<T> getValue)
 	{
 		var sw = Stopwatch.StartNew();
@@ -245,6 +285,6 @@ private static async Task WaitForConditionAsync<T>(T expected, Func<T> getValue)
 		Assert.Equal(expected, getValue());
 	}
 
-	readonly FakeMySqlServer m_server;
-	readonly MySqlConnectionStringBuilder m_csb;
+	private readonly FakeMySqlServer m_server;
+	private readonly MySqlConnectionStringBuilder m_csb;
 }
diff --git a/tests/MySqlConnector.Tests/DbProviderFactoryTests.cs b/tests/MySqlConnector.Tests/DbProviderFactoryTests.cs
index 3b28ac22e..0b85f6a3e 100644
--- a/tests/MySqlConnector.Tests/DbProviderFactoryTests.cs
+++ b/tests/MySqlConnector.Tests/DbProviderFactoryTests.cs
@@ -1,8 +1,6 @@
 #if MYSQL_DATA
-using MySql.Data.MySqlClient;
 using MySqlConnectorFactory = MySql.Data.MySqlClient.MySqlClientFactory;
 #endif
-using Xunit;
 
 namespace MySqlConnector.Tests;
 
diff --git a/tests/MySqlConnector.Tests/DummyEnum.cs b/tests/MySqlConnector.Tests/DummyEnum.cs
index 70b580905..e0e61ff1b 100644
--- a/tests/MySqlConnector.Tests/DummyEnum.cs
+++ b/tests/MySqlConnector.Tests/DummyEnum.cs
@@ -3,5 +3,53 @@ namespace MySqlConnector.Tests;
 internal enum DummyEnum
 {
 	FirstValue,
-	SecondValue
-} 
+	SecondValue,
+}
+
+internal enum DummyByteEnum : byte
+{
+	FirstValue,
+	SecondValue = 0x11,
+}
+
+internal enum DummySByteEnum : sbyte
+{
+	FirstValue,
+	SecondValue = 0x11,
+}
+
+internal enum DummyShortEnum : short
+{
+	FirstValue,
+	SecondValue = 0x1122,
+}
+
+internal enum DummyUShortEnum : ushort
+{
+	FirstValue,
+	SecondValue = 0x1122,
+}
+
+internal enum DummyIntEnum : int
+{
+	FirstValue,
+	SecondValue = 0x11223344,
+}
+
+internal enum DummyUIntEnum : uint
+{
+	FirstValue,
+	SecondValue = 0x11223344,
+}
+
+internal enum DummyLongEnum : long
+{
+	FirstValue,
+	SecondValue = 0x11223344_55667788,
+}
+
+internal enum DummyULongEnum : ulong
+{
+	FirstValue,
+	SecondValue = 0x11223344_55667788,
+}
diff --git a/tests/MySqlConnector.Tests/FakeMySqlServer.cs b/tests/MySqlConnector.Tests/FakeMySqlServer.cs
index aebe0a5f4..a5e61d82a 100644
--- a/tests/MySqlConnector.Tests/FakeMySqlServer.cs
+++ b/tests/MySqlConnector.Tests/FakeMySqlServer.cs
@@ -1,9 +1,5 @@
-using System;
-using System.Collections.Generic;
 using System.Net;
 using System.Net.Sockets;
-using System.Threading;
-using System.Threading.Tasks;
 
 namespace MySqlConnector.Tests;
 
@@ -13,8 +9,8 @@ public FakeMySqlServer()
 	{
 		m_tcpListener = new(IPAddress.Any, 0);
 		m_lock = new();
-		m_connections = new();
-		m_tasks = new();
+		m_connections = [];
+		m_tasks = [];
 	}
 
 	public void Start()
@@ -40,6 +36,9 @@ public void Stop()
 			}
 			m_connections.Clear();
 			m_tasks.Clear();
+#if NET8_0_OR_GREATER
+			m_tcpListener.Dispose();
+#endif
 			m_cts.Dispose();
 			m_cts = null;
 		}
@@ -53,7 +52,7 @@ public void Stop()
 
 	public bool SuppressAuthPluginNameTerminatingNull { get; set; }
 	public bool SendIncompletePostHandshakeResponse { get; set; }
-	public bool BlockOnConnect { get; set; }
+	public TimeSpan? ConnectDelay { get; set; }
 	public TimeSpan? ResetDelay { get; set; }
 
 	internal void CancelQuery(int connectionId)
@@ -82,10 +81,10 @@ private async Task AcceptConnectionsAsync()
 		}
 	}
 
-	readonly object m_lock;
-	readonly TcpListener m_tcpListener;
-	readonly List<FakeMySqlServerConnection> m_connections;
-	readonly List<Task> m_tasks;
-	CancellationTokenSource m_cts;
-	int m_activeConnections;
+	private readonly object m_lock;
+	private readonly TcpListener m_tcpListener;
+	private readonly List<FakeMySqlServerConnection> m_connections;
+	private readonly List<Task> m_tasks;
+	private CancellationTokenSource m_cts;
+	private int m_activeConnections;
 }
diff --git a/tests/MySqlConnector.Tests/FakeMySqlServerConnection.cs b/tests/MySqlConnector.Tests/FakeMySqlServerConnection.cs
index 637b7171e..ef117f28e 100644
--- a/tests/MySqlConnector.Tests/FakeMySqlServerConnection.cs
+++ b/tests/MySqlConnector.Tests/FakeMySqlServerConnection.cs
@@ -1,11 +1,7 @@
-using System;
 using System.IO;
-using System.Linq;
 using System.Net.Sockets;
 using System.Text;
 using System.Text.RegularExpressions;
-using System.Threading;
-using System.Threading.Tasks;
 using MySqlConnector.Protocol;
 
 namespace MySqlConnector.Tests;
@@ -29,15 +25,15 @@ public async Task RunAsync(TcpClient client, CancellationToken token)
 			using (client)
 			using (var stream = client.GetStream())
 			{
-				if (m_server.BlockOnConnect)
-					Thread.Sleep(TimeSpan.FromSeconds(10));
+				if (m_server.ConnectDelay is { } connectDelay)
+					await Task.Delay(connectDelay);
 
 				await SendAsync(stream, 0, WriteInitialHandshake);
 				await ReadPayloadAsync(stream, token); // handshake response
 
 				if (m_server.SendIncompletePostHandshakeResponse)
 				{
-					await stream.WriteAsync(new byte[] { 1, 0, 0, 2 }, 0, 4);
+					await stream.WriteAsync([1, 0, 0, 2], 0, 4);
 					return;
 				}
 
@@ -58,141 +54,155 @@ public async Task RunAsync(TcpClient client, CancellationToken token)
 
 					switch ((CommandKind) bytes[0])
 					{
-					case CommandKind.Quit:
-						await SendAsync(stream, 1, WriteOk);
-						keepRunning = false;
-						break;
-
-					case CommandKind.Ping:
-						await SendAsync(stream, 1, WriteOk);
-						break;
-
-					case CommandKind.ResetConnection:
-						if (m_server.ResetDelay is { } resetDelay)
-							await Task.Delay(resetDelay);
-						await SendAsync(stream, 1, WriteOk);
-						break;
+						case CommandKind.Quit:
+							await SendAsync(stream, 1, WriteOk);
+							keepRunning = false;
+							break;
 
-					case CommandKind.Query:
-						var query = Encoding.UTF8.GetString(bytes, 1, bytes.Length - 1);
-						Match match;
-						if (query == "SET NAMES utf8mb4;")
-						{
+						case CommandKind.Ping:
 							await SendAsync(stream, 1, WriteOk);
-						}
-						else if ((match = Regex.Match(query, @"^SELECT ([0-9])(;|$)")).Success)
-						{
-							var number = match.Groups[1].Value;
-							var data = new byte[number.Length + 1];
-							data[0] = (byte) number.Length;
-							Encoding.UTF8.GetBytes(number, 0, number.Length, data, 1);
+							break;
 
-							await SendAsync(stream, 1, x => x.Write((byte) 1)); // one column
-							await SendAsync(stream, 2, x => x.Write(new byte[] { 3, 0x64, 0x65, 0x66, 0, 0, 0, 1, 0x5F, 0, 0x0c, 0x3f, 0, 1, 0, 0, 0, 3, 0x81, 0, 0, 0, 0 })); // column definition
-							await SendAsync(stream, 3, x => x.Write(new byte[] { 0xFE, 0, 0, 2, 0 })); // EOF
-							await SendAsync(stream, 4, x => x.Write(data));
-							await SendAsync(stream, 5, x => x.Write(new byte[] { 0xFE, 0, 0, 2, 0 })); // EOF
-						}
-						else if ((match = Regex.Match(query, @"^SELECT ([0-9]+), ([0-9]+), ([0-9-]+), ([0-9]+)(;|$)")).Success)
-						{
-							// command is "SELECT {value}, {delay}, {pauseStep}, {flags}"
-							var number = match.Groups[1].Value;
-							var value = int.Parse(number);
-							var delay = int.Parse(match.Groups[2].Value);
-							var pauseStep = int.Parse(match.Groups[3].Value);
-							var flags = int.Parse(match.Groups[4].Value);
-							var ignoreCancellation = (flags & 1) == 1;
-							var bufferOutput = (flags & 2) == 2;
+						case CommandKind.ResetConnection:
+							if (m_server.ResetDelay is { } resetDelay)
+								await Task.Delay(resetDelay);
+							await SendAsync(stream, 1, WriteOk);
+							break;
 
-							var data = new byte[number.Length + 1];
-							data[0] = (byte) number.Length;
-							Encoding.UTF8.GetBytes(number, 0, number.Length, data, 1);
-							
-							var negativeOne = new byte[] { 2, 0x2D, 0x31 };
-							var packets = new[]
+						case CommandKind.Query:
+							var query = Encoding.UTF8.GetString(bytes, 1, bytes.Length - 1);
+							Match match;
+							if (query == "SET NAMES utf8mb4;")
 							{
-								new byte[] { 0xFF, 0x25, 0x05, 0x23, 0x37, 0x30, 0x31, 0x30, 0x30 }.Concat(Encoding.ASCII.GetBytes("Query execution was interrupted")).ToArray(), // error
-								new byte[] { 1 }, // one column
-								new byte[] { 3, 0x64, 0x65, 0x66, 0, 0, 0, 1, 0x5F, 0, 0x0c, 0x3f, 0, 1, 0, 0, 0, 3, 0x81, 0, 0, 0, 0 }, // column definition
-								new byte[] { 0xFE, 0, 0, 2, 0 }, // EOF
-								data,
-								negativeOne,
-								negativeOne,
-								new byte[] { 0xFE, 0, 0, 10, 0 }, // EOF, more results exist
-								new byte[] { 1 }, // one column
-								new byte[] { 3, 0x64, 0x65, 0x66, 0, 0, 0, 1, 0x5F, 0, 0x0c, 0x3f, 0, 1, 0, 0, 0, 3, 0x81, 0, 0, 0, 0 }, // column definition
-								new byte[] { 0xFE, 0, 0, 2, 0 }, // EOF
-								negativeOne,
-								new byte[] { 0xFE, 0, 0, 2, 0 }, // EOF
-							};
-
-							if (bufferOutput)
+								await SendAsync(stream, 1, WriteOk);
+							}
+							else if ((match = Regex.Match(query, @"^SELECT ([0-9])(;|$)")).Success)
 							{
-								// if 'bufferOutput' is set, perform the delay immediately then send all the output afterwards, as though it were buffered on the server
-								var queryInterrupted = false;
-								if (ignoreCancellation)
-									await Task.Delay(delay, token);
-								else
-									queryInterrupted = CancelQueryEvent.Wait(delay, token);
+								var number = match.Groups[1].Value;
+								var data = new byte[number.Length + 1];
+								data[0] = (byte) number.Length;
+								Encoding.UTF8.GetBytes(number, 0, number.Length, data, 1);
 
-								for (var step = 1; step < pauseStep; step++)
-									await SendAsync(stream, step, x => x.Write(packets[step]));
-								await SendAsync(stream, pauseStep, x => x.Write(packets[queryInterrupted ? 0 : pauseStep]));
+								await SendAsync(stream, 1, x => x.Write((byte) 1)); // one column
+								await SendAsync(stream, 2, x => x.Write(new byte[] { 3, 0x64, 0x65, 0x66, 0, 0, 0, 1, 0x5F, 0, 0x0c, 0x3f, 0, 1, 0, 0, 0, 3, 0x81, 0, 0, 0, 0 })); // column definition
+								await SendAsync(stream, 3, x => x.Write(new byte[] { 0xFE, 0, 0, 2, 0 })); // EOF
+								await SendAsync(stream, 4, x => x.Write(data));
+								await SendAsync(stream, 5, x => x.Write(new byte[] { 0xFE, 0, 0, 2, 0 })); // EOF
 							}
-							else
+							else if ((match = Regex.Match(query, @"^SELECT ([0-9]+), ([0-9]+), ([0-9-]+), ([0-9]+)(;|$)")).Success)
 							{
-								var queryInterrupted = false;
-								for (var step = 1; step < packets.Length && !queryInterrupted; step++)
+								// command is "SELECT {value}, {delay}, {pauseStep}, {flags}"
+								var number = match.Groups[1].Value;
+								var value = int.Parse(number);
+								var delay = int.Parse(match.Groups[2].Value);
+								var pauseStep = int.Parse(match.Groups[3].Value);
+								var flags = int.Parse(match.Groups[4].Value);
+								var ignoreCancellation = (flags & 1) == 1;
+								var bufferOutput = (flags & 2) == 2;
+
+								var data = new byte[number.Length + 1];
+								data[0] = (byte) number.Length;
+								Encoding.UTF8.GetBytes(number, 0, number.Length, data, 1);
+
+								var negativeOne = new byte[] { 2, 0x2D, 0x31 };
+								var packets = new[]
+								{
+									new byte[] { 0xFF, 0x25, 0x05, 0x23, 0x37, 0x30, 0x31, 0x30, 0x30 }.Concat(Encoding.ASCII.GetBytes("Query execution was interrupted")).ToArray(), // error
+									[1], // one column
+									[3, 0x64, 0x65, 0x66, 0, 0, 0, 1, 0x5F, 0, 0x0c, 0x3f, 0, 1, 0, 0, 0, 3, 0x81, 0, 0, 0, 0], // column definition
+									[0xFE, 0, 0, 2, 0], // EOF
+									data,
+									negativeOne,
+									negativeOne,
+									[0xFE, 0, 0, 10, 0], // EOF, more results exist
+									[1], // one column
+									[3, 0x64, 0x65, 0x66, 0, 0, 0, 1, 0x5F, 0, 0x0c, 0x3f, 0, 1, 0, 0, 0, 3, 0x81, 0, 0, 0, 0], // column definition
+									[0xFE, 0, 0, 2, 0], // EOF
+									negativeOne,
+									[0xFE, 0, 0, 2, 0], // EOF
+								};
+
+								if (bufferOutput)
+								{
+									// if 'bufferOutput' is set, perform the delay immediately then send all the output afterwards, as though it were buffered on the server
+									var queryInterrupted = false;
+									if (ignoreCancellation)
+										await Task.Delay(delay, token);
+									else
+										queryInterrupted = CancelQueryEvent.Wait(delay, token);
+
+									for (var step = 1; step < pauseStep; step++)
+										await SendAsync(stream, step, x => x.Write(packets[step]));
+									await SendAsync(stream, pauseStep, x => x.Write(packets[queryInterrupted ? 0 : pauseStep]));
+								}
+								else
 								{
-									if (pauseStep == step || pauseStep == -1)
+									var queryInterrupted = false;
+									for (var step = 1; step < packets.Length && !queryInterrupted; step++)
 									{
-										if (ignoreCancellation)
-											await Task.Delay(delay, token);
-										else
-											queryInterrupted = CancelQueryEvent.Wait(delay, token);
-									}
+										if (pauseStep == step || pauseStep == -1)
+										{
+											if (ignoreCancellation)
+												await Task.Delay(delay, token);
+											else
+												queryInterrupted = CancelQueryEvent.Wait(delay, token);
+										}
 
-									await SendAsync(stream, step, x => x.Write(packets[queryInterrupted ? 0 : step]));
+										await SendAsync(stream, step, x => x.Write(packets[queryInterrupted ? 0 : step]));
+									}
 								}
 							}
-						}
-						else if ((match = Regex.Match(query, @"^KILL QUERY ([0-9]+)(;|$)", RegexOptions.IgnoreCase)).Success)
-						{
-							var connectionId = int.Parse(match.Groups[1].Value);
-							m_server.CancelQuery(connectionId);
-							await SendAsync(stream, 1, WriteOk);
-						}
-						else if (query == "SELECT SLEEP(0) INTO @\uE001MySqlConnector\uE001Sleep;")
-						{
-							var wasSet = CancelQueryEvent.Wait(0, token);
-							await SendAsync(stream, 1, WriteOk);
-						}
-						else if (query == "select infinity")
-						{
-							var packets = new[]
+							else if ((match = Regex.Match(query, @"^KILL QUERY ([0-9]+)(;|$)", RegexOptions.IgnoreCase)).Success)
 							{
-								new byte[] { 2 }, // two columns
-								new byte[] { 3, 0x64, 0x65, 0x66, 0, 0, 0, 1, 0x46, 0, 0x0c, 0x3f, 0, 1, 0, 0, 0, 4, 0x01, 0, 0x1F, 0, 0 }, // column definition (float)
-								new byte[] { 3, 0x64, 0x65, 0x66, 0, 0, 0, 1, 0x44, 0, 0x0c, 0x3f, 0, 1, 0, 0, 0, 5, 0x01, 0, 0x1F, 0, 0 }, // column definition (double)
-								new byte[] { 0xFE, 0, 0, 2, 0 }, // EOF
-								new byte[] { 3, 0x6e, 0x61, 0x6e, 3, 0x6e, 0x61, 0x6e }, // nan
-								new byte[] { 3, 0x69, 0x6e, 0x66, 3, 0x69, 0x6e, 0x66 }, // inf
-								new byte[] { 4, 0x2d, 0x69, 0x6e, 0x66, 4, 0x2d, 0x69, 0x6e, 0x66 }, // -inf
-								new byte[] { 0xFE, 0, 0, 2, 0 }, // EOF
-							};
-							for (var packetIndex = 0; packetIndex < packets.Length; packetIndex++)
-								await SendAsync(stream, packetIndex + 1, x => x.Write(packets[packetIndex]));
-						}
-						else
-						{
-							await SendAsync(stream, 1, x => WriteError(x, "Unhandled query: " + query));
-						}
-						break;
+								var connectionId = int.Parse(match.Groups[1].Value);
+								m_server.CancelQuery(connectionId);
+								await SendAsync(stream, 1, WriteOk);
+							}
+							else if (query == "SELECT SLEEP(0) INTO @__MySqlConnector__Sleep;")
+							{
+								var wasSet = CancelQueryEvent.Wait(0, token);
+								await SendAsync(stream, 1, WriteOk);
+							}
+							else if (query == "select infinity")
+							{
+								var packets = new[]
+								{
+									new byte[] { 2 }, // two columns
+									[3, 0x64, 0x65, 0x66, 0, 0, 0, 1, 0x46, 0, 0x0c, 0x3f, 0, 1, 0, 0, 0, 4, 0x01, 0, 0x1F, 0, 0], // column definition (float)
+									[3, 0x64, 0x65, 0x66, 0, 0, 0, 1, 0x44, 0, 0x0c, 0x3f, 0, 1, 0, 0, 0, 5, 0x01, 0, 0x1F, 0, 0], // column definition (double)
+									[0xFE, 0, 0, 2, 0], // EOF
+									[3, 0x6e, 0x61, 0x6e, 3, 0x6e, 0x61, 0x6e], // nan
+									[3, 0x69, 0x6e, 0x66, 3, 0x69, 0x6e, 0x66], // inf
+									[4, 0x2d, 0x69, 0x6e, 0x66, 4, 0x2d, 0x69, 0x6e, 0x66], // -inf
+									[0xFE, 0, 0, 2, 0], // EOF
+								};
+								for (var packetIndex = 0; packetIndex < packets.Length; packetIndex++)
+									await SendAsync(stream, packetIndex + 1, x => x.Write(packets[packetIndex]));
+							}
+							else if (query == "select disconnect")
+							{
+								var packets = new[]
+								{
+									new byte[] { 1 }, // one column
+									[3, 0x64, 0x65, 0x66, 0, 0, 0, 1, 0x41, 0, 0x0c, 0x3f, 0, 1, 0, 0, 0, 3, 0x01, 0, 0x1F, 0, 0], // column definition (int)
+									[0xFE, 0, 0, 2, 0], // EOF
+									[1, 0x31], // 1
+								};
+								for (var packetIndex = 0; packetIndex < packets.Length; packetIndex++)
+									await SendAsync(stream, packetIndex + 1, x => x.Write(packets[packetIndex]));
+								stream.Close();
+								client.Close();
+							}
+							else
+							{
+								await SendAsync(stream, 1, x => WriteError(x, "Unhandled query: " + query));
+							}
+							break;
 
-					default:
-						Console.WriteLine("** UNHANDLED ** {0}", (CommandKind) bytes[0]);
-						await SendAsync(stream, 1, x => WriteError(x));
-						break;
+						default:
+							Console.WriteLine("** UNHANDLED ** {0}", (CommandKind) bytes[0]);
+							await SendAsync(stream, 1, x => WriteError(x));
+							break;
 					}
 				}
 			}
@@ -298,6 +308,6 @@ private static void WriteError(BinaryWriter writer, string message = "An unknown
 		writer.WriteRaw(message);
 	}
 
-	readonly FakeMySqlServer m_server;
-	readonly int m_connectionId;
+	private readonly FakeMySqlServer m_server;
+	private readonly int m_connectionId;
 }
diff --git a/tests/MySqlConnector.Tests/LoadBalancerTests.cs b/tests/MySqlConnector.Tests/LoadBalancerTests.cs
index cbf18bd27..0dcfa86cb 100644
--- a/tests/MySqlConnector.Tests/LoadBalancerTests.cs
+++ b/tests/MySqlConnector.Tests/LoadBalancerTests.cs
@@ -1,7 +1,3 @@
-using System.Linq;
-using MySqlConnector.Core;
-using Xunit;
-
 namespace MySqlConnector.Tests;
 
 public class LoadBalancerTests
diff --git a/tests/MySqlConnector.Tests/Metrics/ConnectTimeoutTests.cs b/tests/MySqlConnector.Tests/Metrics/ConnectTimeoutTests.cs
new file mode 100644
index 000000000..405ce03d9
--- /dev/null
+++ b/tests/MySqlConnector.Tests/Metrics/ConnectTimeoutTests.cs
@@ -0,0 +1,50 @@
+namespace MySqlConnector.Tests.Metrics;
+
+public class ConnectTimeoutTests : MetricsTestsBase
+{
+	[Fact(Skip = MetricsSkip)]
+	public async Task ConnectTimeout()
+	{
+		var csb = CreateConnectionStringBuilder();
+		csb.ConnectionTimeout = 1;
+		csb.Server = "www.example.com";
+		PoolName = csb.GetConnectionString(includePassword: false);
+
+		using var connection = new MySqlConnection(csb.ConnectionString);
+		await Assert.ThrowsAsync<MySqlException>(connection.OpenAsync);
+
+		AssertMeasurement("db.client.connections.timeouts", 1);
+	}
+
+	[Fact(Skip = MetricsSkip)]
+	public async Task DataSourceConnectTimeout()
+	{
+		var csb = CreateConnectionStringBuilder();
+		csb.ConnectionTimeout = 1;
+		csb.Server = "www.example.com";
+
+		PoolName = "timeout";
+		using var dataSource = new MySqlDataSourceBuilder(csb.ConnectionString)
+			.UseName(PoolName)
+			.Build();
+
+		await Assert.ThrowsAsync<MySqlException>(async () => await dataSource.OpenConnectionAsync());
+
+		AssertMeasurement("db.client.connections.timeouts", 1);
+	}
+
+	[Fact(Skip = MetricsSkip)]
+	public async Task NoPoolConnectTimeout()
+	{
+		var csb = CreateConnectionStringBuilder();
+		csb.ConnectionTimeout = 1;
+		csb.Server = "www.example.com";
+		csb.Pooling = false;
+		PoolName = csb.GetConnectionString(includePassword: false);
+
+		using var connection = new MySqlConnection(csb.ConnectionString);
+		await Assert.ThrowsAsync<MySqlException>(connection.OpenAsync);
+
+		AssertMeasurement("db.client.connections.timeouts", 1);
+	}
+}
diff --git a/tests/MySqlConnector.Tests/Metrics/ConnectionTimeTests.cs b/tests/MySqlConnector.Tests/Metrics/ConnectionTimeTests.cs
new file mode 100644
index 000000000..4a919f8ee
--- /dev/null
+++ b/tests/MySqlConnector.Tests/Metrics/ConnectionTimeTests.cs
@@ -0,0 +1,92 @@
+namespace MySqlConnector.Tests.Metrics;
+
+public class ConnectionTimeTests : MetricsTestsBase
+{
+	[Fact(Skip = MetricsSkip)]
+	public async Task ConnectionTime()
+	{
+		var csb = CreateConnectionStringBuilder();
+		PoolName = csb.GetConnectionString(includePassword: false);
+		using var connection = new MySqlConnection(csb.ConnectionString);
+		await connection.OpenAsync();
+		var measurements = GetAndClearMeasurements("db.client.connections.create_time");
+		var time = Assert.Single(measurements);
+		Assert.InRange(time, 0, 0.3);
+	}
+
+	[Fact(Skip = MetricsSkip)]
+	public async Task ConnectionTimeWithDelay()
+	{
+		var csb = CreateConnectionStringBuilder();
+		PoolName = csb.GetConnectionString(includePassword: false);
+		Server.ConnectDelay = TimeSpan.FromSeconds(1);
+
+		using var connection = new MySqlConnection(csb.ConnectionString);
+		await connection.OpenAsync();
+		var measurements = GetAndClearMeasurements("db.client.connections.create_time");
+		var time = Assert.Single(measurements);
+		Assert.InRange(time, 1.0, 1.3);
+	}
+
+	[Fact(Skip = MetricsSkip)]
+	public async Task OpenFromPoolTime()
+	{
+		var csb = CreateConnectionStringBuilder();
+		PoolName = csb.GetConnectionString(includePassword: false);
+
+		using var connection = new MySqlConnection(csb.ConnectionString);
+		await connection.OpenAsync();
+		connection.Close();
+
+		await connection.OpenAsync();
+		var measurements = GetAndClearMeasurements("db.client.connections.wait_time");
+		var time = Assert.Single(measurements);
+		Assert.InRange(time, 0, 0.2);
+	}
+
+	[Fact(Skip = MetricsSkip)]
+	public async Task OpenFromPoolTimeWithDelay()
+	{
+		var csb = CreateConnectionStringBuilder();
+		PoolName = csb.GetConnectionString(includePassword: false);
+		Server.ResetDelay = TimeSpan.FromSeconds(1);
+
+		using var connection = new MySqlConnection(csb.ConnectionString);
+		await connection.OpenAsync();
+		connection.Close();
+
+		await connection.OpenAsync();
+		var measurements = GetAndClearMeasurements("db.client.connections.wait_time");
+		var time = Assert.Single(measurements);
+		Assert.InRange(time, 1.0, 1.2);
+	}
+
+	[Fact(Skip = MetricsSkip)]
+	public async Task UseTime()
+	{
+		var csb = CreateConnectionStringBuilder();
+		PoolName = csb.GetConnectionString(includePassword: false);
+
+		using var connection = new MySqlConnection(csb.ConnectionString);
+		await connection.OpenAsync();
+		connection.Close();
+
+		var time = Assert.Single(GetAndClearMeasurements("db.client.connections.use_time"));
+		Assert.InRange(time, 0, 0.1);
+	}
+
+	[Fact(Skip = MetricsSkip)]
+	public async Task UseTimeWithDelay()
+	{
+		var csb = CreateConnectionStringBuilder();
+		PoolName = csb.GetConnectionString(includePassword: false);
+
+		using var connection = new MySqlConnection(csb.ConnectionString);
+		await connection.OpenAsync();
+		await Task.Delay(500);
+		connection.Close();
+
+		var time = Assert.Single(GetAndClearMeasurements("db.client.connections.use_time"));
+		Assert.InRange(time, 0.5, 0.6);
+	}
+}
diff --git a/tests/MySqlConnector.Tests/Metrics/ConnectionsUsageTests.cs b/tests/MySqlConnector.Tests/Metrics/ConnectionsUsageTests.cs
new file mode 100644
index 000000000..a0e6bd8f2
--- /dev/null
+++ b/tests/MySqlConnector.Tests/Metrics/ConnectionsUsageTests.cs
@@ -0,0 +1,186 @@
+#nullable enable
+
+namespace MySqlConnector.Tests.Metrics;
+
+public class ConnectionsUsageTests : MetricsTestsBase
+{
+	[Theory(Skip = MetricsSkip)]
+	[InlineData("DataSource|true||")]
+	[InlineData("DataSource|true||app-name")]
+	[InlineData("DataSource|true|pool-name|")]
+	[InlineData("DataSource|true|pool-name|app-name")]
+	[InlineData("Plain|true|")]
+	[InlineData("Plain|true|app-name")]
+	public void ConnectionsWithPoolsHaveMetrics(string connectionCreatorSpec)
+	{
+		using var connectionCreator = CreateConnectionCreator(connectionCreatorSpec, CreateConnectionStringBuilder());
+		PoolName = connectionCreator.PoolName;
+
+		// no connections at beginning of test
+		AssertMeasurement("db.client.connections.usage", 0);
+		AssertMeasurement("db.client.connections.usage|idle", 0);
+		AssertMeasurement("db.client.connections.usage|used", 0);
+		Assert.Equal(0, Server.ActiveConnections);
+
+		// opening a connection creates a 'used' connection
+		using (var connection = connectionCreator.OpenConnection())
+		{
+			AssertMeasurement("db.client.connections.usage", 1);
+			AssertMeasurement("db.client.connections.usage|idle", 0);
+			AssertMeasurement("db.client.connections.usage|used", 1);
+			Assert.Equal(1, Server.ActiveConnections);
+		}
+
+		// closing it creates an 'idle' connection
+		AssertMeasurement("db.client.connections.usage", 1);
+		AssertMeasurement("db.client.connections.usage|idle", 1);
+		AssertMeasurement("db.client.connections.usage|used", 0);
+		Assert.Equal(1, Server.ActiveConnections);
+
+		// reopening the connection transitions it back to 'used'
+		using (var connection = connectionCreator.OpenConnection())
+		{
+			AssertMeasurement("db.client.connections.usage", 1);
+			AssertMeasurement("db.client.connections.usage|idle", 0);
+			AssertMeasurement("db.client.connections.usage|used", 1);
+		}
+		Assert.Equal(1, Server.ActiveConnections);
+
+		// opening a second connection creates a net new 'used' connection
+		using (var connection = connectionCreator.OpenConnection())
+		using (var connection2 = connectionCreator.OpenConnection())
+		{
+			AssertMeasurement("db.client.connections.usage", 2);
+			AssertMeasurement("db.client.connections.usage|idle", 0);
+			AssertMeasurement("db.client.connections.usage|used", 2);
+			Assert.Equal(2, Server.ActiveConnections);
+		}
+
+		AssertMeasurement("db.client.connections.usage", 2);
+		AssertMeasurement("db.client.connections.usage|idle", 2);
+		AssertMeasurement("db.client.connections.usage|used", 0);
+		Assert.Equal(2, Server.ActiveConnections);
+	}
+
+	[Theory(Skip = MetricsSkip)]
+	[InlineData("DataSource|false||")]
+	[InlineData("DataSource|false||app-name")]
+	[InlineData("DataSource|false|pool-name|")]
+	[InlineData("DataSource|false|pool-name|app-name")]
+	[InlineData("Plain|false|")]
+	[InlineData("Plain|false|app-name")]
+	public void ConnectionsWithoutPoolsHaveNoMetrics(string connectionCreatorSpec)
+	{
+		using var connectionCreator = CreateConnectionCreator(connectionCreatorSpec, CreateConnectionStringBuilder());
+		PoolName = connectionCreator.PoolName;
+
+		// no connections at beginning of test
+		AssertMeasurement("db.client.connections.usage", 0);
+		AssertMeasurement("db.client.connections.usage|idle", 0);
+		AssertMeasurement("db.client.connections.usage|used", 0);
+		Assert.Equal(0, Server.ActiveConnections);
+
+		// opening a connection doesn't change connection counts
+		using (var connection = connectionCreator.OpenConnection())
+		{
+			AssertMeasurement("db.client.connections.usage", 0);
+			AssertMeasurement("db.client.connections.usage|idle", 0);
+			AssertMeasurement("db.client.connections.usage|used", 0);
+			Assert.Equal(1, Server.ActiveConnections);
+		}
+
+		// closing it doesn't create an idle connection but closes it immediately
+		AssertMeasurement("db.client.connections.usage", 0);
+		AssertMeasurement("db.client.connections.usage|idle", 0);
+		AssertMeasurement("db.client.connections.usage|used", 0);
+
+		// disposing the connection sends a COM_QUIT packet and immediately returns; give the in-proc server a chance to process it
+		for (var retry = 0; retry < 20; retry++)
+		{
+			if (Server.ActiveConnections != 0)
+				Thread.Sleep(1);
+		}
+		Assert.Equal(0, Server.ActiveConnections);
+	}
+
+	[Fact(Skip = MetricsSkip)]
+	public void NamedDataSourceWithMinPoolSize()
+	{
+		var csb = CreateConnectionStringBuilder();
+		csb.MinimumPoolSize = 3;
+
+		PoolName = "minimum-pool-size";
+		using var dataSource = new MySqlDataSourceBuilder(csb.ConnectionString)
+			.UseName(PoolName)
+			.Build();
+
+		// minimum pool size is created lazily when the first connection is opened
+		AssertMeasurement("db.client.connections.usage", 0);
+		AssertMeasurement("db.client.connections.usage|idle", 0);
+		AssertMeasurement("db.client.connections.usage|used", 0);
+		Assert.Equal(0, Server.ActiveConnections);
+
+		// opening a connection creates the minimum connections then takes an idle one from the pool
+		using (var connection = dataSource.OpenConnection())
+		{
+			AssertMeasurement("db.client.connections.usage", 3);
+			AssertMeasurement("db.client.connections.usage|idle", 2);
+			AssertMeasurement("db.client.connections.usage|used", 1);
+			Assert.Equal(3, Server.ActiveConnections);
+		}
+
+		// closing puts it back to idle
+		AssertMeasurement("db.client.connections.usage", 3);
+		AssertMeasurement("db.client.connections.usage|idle", 3);
+		AssertMeasurement("db.client.connections.usage|used", 0);
+		Assert.Equal(3, Server.ActiveConnections);
+	}
+
+	[Fact(Skip = MetricsSkip)]
+	public async Task PendingRequestForCreation()
+	{
+		var csb = CreateConnectionStringBuilder();
+		PoolName = csb.GetConnectionString(includePassword: false);
+		Server.ConnectDelay = TimeSpan.FromSeconds(0.5);
+
+		AssertMeasurement("db.client.connections.pending_requests", 0);
+
+		using var connection = new MySqlConnection(csb.ConnectionString);
+		var openTask = connection.OpenAsync();
+		AssertMeasurement("db.client.connections.pending_requests", 1);
+		await openTask;
+
+		AssertMeasurement("db.client.connections.pending_requests", 0);
+	}
+
+	[Fact(Skip = MetricsSkip)]
+	public async Task PendingRequestForOpenFromPool()
+	{
+		var csb = CreateConnectionStringBuilder();
+		PoolName = csb.GetConnectionString(includePassword: false);
+		Server.ResetDelay = TimeSpan.FromSeconds(0.5);
+
+		using var connection = new MySqlConnection(csb.ConnectionString);
+		await connection.OpenAsync();
+		connection.Close();
+
+		AssertMeasurement("db.client.connections.pending_requests", 0);
+
+		var openTask = connection.OpenAsync();
+		AssertMeasurement("db.client.connections.pending_requests", 1);
+		await openTask;
+
+		AssertMeasurement("db.client.connections.pending_requests", 0);
+	}
+
+	private IConnectionCreator CreateConnectionCreator(string spec, MySqlConnectionStringBuilder connectionStringBuilder)
+	{
+		var parts = spec.Split('|');
+		return parts[0] switch
+		{
+			"DataSource" => new DataSourceConnectionCreator(bool.Parse(parts[1]), parts[2] == "" ? null : parts[2], parts[3] == "" ? null : parts[3], connectionStringBuilder),
+			"Plain" => new PlainConnectionCreator(bool.Parse(parts[1]), parts[2] == "" ? null : parts[2], connectionStringBuilder),
+			_ => throw new NotSupportedException(),
+		};
+	}
+}
diff --git a/tests/MySqlConnector.Tests/Metrics/DictionaryExtensions.cs b/tests/MySqlConnector.Tests/Metrics/DictionaryExtensions.cs
new file mode 100644
index 000000000..36dc93d74
--- /dev/null
+++ b/tests/MySqlConnector.Tests/Metrics/DictionaryExtensions.cs
@@ -0,0 +1,12 @@
+#nullable enable
+
+namespace MySqlConnector.Tests.Metrics;
+
+#if !NETSTANDARD2_1 && !NETCOREAPP3_1_OR_GREATER
+internal static class DictionaryExtensions
+{
+	public static TValue GetValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key)
+		where TValue : struct =>
+		dictionary.TryGetValue(key, out var value) ? value : default;
+}
+#endif
diff --git a/tests/MySqlConnector.Tests/Metrics/IConnectionCreator.cs b/tests/MySqlConnector.Tests/Metrics/IConnectionCreator.cs
new file mode 100644
index 000000000..4f8b51a63
--- /dev/null
+++ b/tests/MySqlConnector.Tests/Metrics/IConnectionCreator.cs
@@ -0,0 +1,54 @@
+#nullable enable
+
+namespace MySqlConnector.Tests.Metrics;
+
+public interface IConnectionCreator : IDisposable
+{
+	string PoolName { get; }
+	MySqlConnection OpenConnection();
+}
+
+internal sealed class DataSourceConnectionCreator : IConnectionCreator
+{
+	public DataSourceConnectionCreator(bool usePooling, string? poolName, string? applicationName, MySqlConnectionStringBuilder connectionStringBuilder)
+	{
+		connectionStringBuilder.Pooling = usePooling;
+		connectionStringBuilder.ApplicationName = applicationName;
+		m_dataSource = new MySqlDataSourceBuilder(connectionStringBuilder.ConnectionString)
+			.UseName(poolName)
+			.Build();
+		PoolName = poolName ?? applicationName ?? connectionStringBuilder.GetConnectionString(includePassword: false);
+	}
+
+	public MySqlConnection OpenConnection() => m_dataSource.OpenConnection();
+	public string PoolName { get; }
+	public void Dispose() => m_dataSource.Dispose();
+
+	private readonly MySqlDataSource m_dataSource;
+}
+
+internal sealed class PlainConnectionCreator : IConnectionCreator
+{
+	public PlainConnectionCreator(bool usePooling, string? applicationName, MySqlConnectionStringBuilder connectionStringBuilder)
+	{
+		connectionStringBuilder.Pooling = usePooling;
+		connectionStringBuilder.ApplicationName = applicationName;
+		m_connectionString = connectionStringBuilder.ConnectionString;
+		PoolName = applicationName ?? connectionStringBuilder.GetConnectionString(includePassword: false);
+	}
+
+	public MySqlConnection OpenConnection()
+	{
+		var connection = new MySqlConnection(m_connectionString);
+		connection.Open();
+		return connection;
+	}
+
+	public string PoolName { get; }
+
+	public void Dispose()
+	{
+	}
+
+	private readonly string m_connectionString;
+}
diff --git a/tests/MySqlConnector.Tests/Metrics/MetricsTestsBase.cs b/tests/MySqlConnector.Tests/Metrics/MetricsTestsBase.cs
new file mode 100644
index 000000000..51b912a1e
--- /dev/null
+++ b/tests/MySqlConnector.Tests/Metrics/MetricsTestsBase.cs
@@ -0,0 +1,130 @@
+#nullable enable
+
+using System.Diagnostics.Metrics;
+
+namespace MySqlConnector.Tests.Metrics;
+
+public abstract class MetricsTestsBase : IDisposable
+{
+	public MetricsTestsBase()
+	{
+		m_measurements = [];
+		m_timeMeasurements = [];
+
+		Server = new FakeMySqlServer();
+		Server.Start();
+
+		m_meterListener = new MeterListener
+		{
+			InstrumentPublished = (instrument, listener) =>
+			{
+				if (instrument.Meter.Name == "MySqlConnector")
+					listener.EnableMeasurementEvents(instrument);
+			},
+		};
+		m_meterListener.SetMeasurementEventCallback<int>(OnMeasurementRecorded);
+		m_meterListener.SetMeasurementEventCallback<double>(OnMeasurementRecorded);
+		m_meterListener.Start();
+	}
+
+	public void Dispose()
+	{
+		Dispose(disposing: true);
+		GC.SuppressFinalize(this);
+	}
+
+	protected virtual void Dispose(bool disposing)
+	{
+		if (disposing)
+		{
+			m_meterListener.Dispose();
+			Server.Stop();
+		}
+	}
+
+	protected string? PoolName { get; set; }
+	protected FakeMySqlServer Server { get; }
+
+	protected MySqlConnectionStringBuilder CreateConnectionStringBuilder() =>
+		new()
+		{
+			Server = "localhost",
+			Port = (uint) Server.Port,
+			UserID = "test",
+			Password = "test",
+		};
+
+	protected void AssertMeasurement(string name, int expected)
+	{
+		// clear cached measurements from observable counters
+		lock (m_measurements)
+		{
+			m_measurements.Remove("db.client.connections.idle.max");
+			m_measurements.Remove("db.client.connections.idle.min");
+			m_measurements.Remove("db.client.connections.max");
+		}
+		m_meterListener.RecordObservableInstruments();
+		lock (m_measurements)
+			Assert.Equal(expected, m_measurements.GetValueOrDefault(name));
+	}
+
+	protected List<double> GetAndClearMeasurements(string name)
+	{
+		if (!m_timeMeasurements.TryGetValue(name, out var list))
+			list = [];
+		m_timeMeasurements[name] = [];
+		return list;
+	}
+
+#if NO_METRICS_TESTS
+	protected const string MetricsSkip = "Metrics tests are skipped";
+#else
+	protected const string MetricsSkip = null;
+#endif
+
+	private void OnMeasurementRecorded(Instrument instrument, int measurement, ReadOnlySpan<KeyValuePair<string, object?>> tags, object? state)
+	{
+		var (poolName, stateTag) = GetTags(tags);
+		if (poolName != PoolName)
+			return;
+
+		lock (m_measurements)
+		{
+			m_measurements[instrument.Name] = m_measurements.GetValueOrDefault(instrument.Name) + measurement;
+			if (stateTag.Length != 0)
+				m_measurements[$"{instrument.Name}|{stateTag}"] = m_measurements.GetValueOrDefault($"{instrument.Name}|{stateTag}") + measurement;
+		}
+	}
+
+	private void OnMeasurementRecorded(Instrument instrument, double measurement, ReadOnlySpan<KeyValuePair<string, object?>> tags, object? state)
+	{
+		var (poolName, stateTag) = GetTags(tags);
+		if (poolName != PoolName)
+			return;
+
+		lock (m_timeMeasurements)
+		{
+			if (!m_timeMeasurements.TryGetValue(instrument.Name, out var list))
+				list = m_timeMeasurements[instrument.Name] = [];
+			list.Add(measurement);
+		}
+	}
+
+	private (string PoolName, string State) GetTags(ReadOnlySpan<KeyValuePair<string, object?>> tags)
+	{
+		var poolName = "";
+		var state = "";
+		foreach (var tag in tags)
+		{
+			if (tag.Key == "pool.name" && tag.Value is string s1)
+				poolName = s1;
+			else if (tag.Key == "state" && tag.Value is string s2)
+				state = s2;
+		}
+		return (poolName, state);
+	}
+
+	private readonly Dictionary<string, int> m_measurements;
+	private readonly Dictionary<string, List<double>> m_timeMeasurements;
+	private readonly MeterListener m_meterListener;
+}
diff --git a/tests/MySqlConnector.Tests/Metrics/MinMaxConnectionsTests.cs b/tests/MySqlConnector.Tests/Metrics/MinMaxConnectionsTests.cs
new file mode 100644
index 000000000..a5a4e658a
--- /dev/null
+++ b/tests/MySqlConnector.Tests/Metrics/MinMaxConnectionsTests.cs
@@ -0,0 +1,105 @@
+#nullable enable
+
+namespace MySqlConnector.Tests.Metrics;
+
+public class MinMaxConnectionsTests : MetricsTestsBase
+{
+	[Fact(Skip = MetricsSkip)]
+	public void SetsMinimumIdleToDefault()
+	{
+		var csb = CreateConnectionStringBuilder();
+
+		PoolName = "min-idle";
+		using var dataSource = new MySqlDataSourceBuilder(csb.ConnectionString)
+			.UseName(PoolName)
+			.Build();
+		using var connection = dataSource.OpenConnection();
+
+		AssertMeasurement("db.client.connections.idle.min", 0);
+	}
+
+	[Fact(Skip = MetricsSkip)]
+	public void SetsMaximumIdleToDefault()
+	{
+		var csb = CreateConnectionStringBuilder();
+
+		PoolName = "max-idle";
+		using var dataSource = new MySqlDataSourceBuilder(csb.ConnectionString)
+			.UseName(PoolName)
+			.Build();
+		using var connection = dataSource.OpenConnection();
+
+		AssertMeasurement("db.client.connections.idle.max", 100);
+	}
+
+	[Fact(Skip = MetricsSkip)]
+	public void SetsMaximumToDefault()
+	{
+		var csb = CreateConnectionStringBuilder();
+
+		PoolName = "max";
+		using var dataSource = new MySqlDataSourceBuilder(csb.ConnectionString)
+			.UseName(PoolName)
+			.Build();
+		using var connection = dataSource.OpenConnection();
+
+		AssertMeasurement("db.client.connections.max", 100);
+	}
+
+	[Fact(Skip = MetricsSkip)]
+	public void SetsMinimumIdleToCustom()
+	{
+		var csb = CreateConnectionStringBuilder();
+		csb.MinimumPoolSize = 3;
+
+		PoolName = "min-idle";
+		using (var dataSource = new MySqlDataSourceBuilder(csb.ConnectionString)
+			.UseName(PoolName)
+			.Build())
+		{
+			using var connection = dataSource.OpenConnection();
+
+			AssertMeasurement("db.client.connections.idle.min", 3);
+		}
+
+		AssertMeasurement("db.client.connections.idle.min", 0);
+	}
+
+	[Fact(Skip = MetricsSkip)]
+	public void SetsMaximumIdleToCustom()
+	{
+		var csb = CreateConnectionStringBuilder();
+		csb.MaximumPoolSize = 57;
+
+		PoolName = "max-idle";
+		using (var dataSource = new MySqlDataSourceBuilder(csb.ConnectionString)
+			.UseName(PoolName)
+			.Build())
+		{
+			using var connection = dataSource.OpenConnection();
+
+			AssertMeasurement("db.client.connections.idle.max", 57);
+		}
+
+		AssertMeasurement("db.client.connections.idle.max", 0);
+	}
+
+	[Fact(Skip = MetricsSkip)]
+	public void SetsMaximumToCustom()
+	{
+		var csb = CreateConnectionStringBuilder();
+		csb.MaximumPoolSize = 99;
+
+		PoolName = "max";
+		using (var dataSource = new MySqlDataSourceBuilder(csb.ConnectionString)
+			.UseName(PoolName)
+			.Build())
+		{
+			using var connection = dataSource.OpenConnection();
+
+			AssertMeasurement("db.client.connections.max", 99);
+		}
+
+		AssertMeasurement("db.client.connections.max", 0);
+	}
+}
diff --git a/tests/MySqlConnector.Tests/MySqlAttributeCollectionTests.cs b/tests/MySqlConnector.Tests/MySqlAttributeCollectionTests.cs
index e81091986..eae0484f1 100644
--- a/tests/MySqlConnector.Tests/MySqlAttributeCollectionTests.cs
+++ b/tests/MySqlConnector.Tests/MySqlAttributeCollectionTests.cs
@@ -1,9 +1,3 @@
-#if MYSQL_DATA
-using MySql.Data.MySqlClient;
-#endif
-using System;
-using Xunit;
-
 namespace MySqlConnector.Tests;
 
 public class MySqlAttributeCollectionTests
@@ -87,7 +81,7 @@ private MySqlAttribute AssertSingle()
 		Assert.Equal(1, m_collection.Count);
 		return m_collection[0];
 #else
-		return (MySqlAttribute) Assert.Single(m_collection);
+		return Assert.Single(m_collection);
 #endif
 	}
 
diff --git a/tests/MySqlConnector.Tests/MySqlAttributeTests.cs b/tests/MySqlConnector.Tests/MySqlAttributeTests.cs
index bb8f21c3a..a218caf1e 100644
--- a/tests/MySqlConnector.Tests/MySqlAttributeTests.cs
+++ b/tests/MySqlConnector.Tests/MySqlAttributeTests.cs
@@ -1,9 +1,3 @@
-#if MYSQL_DATA
-using MySql.Data.MySqlClient;
-#endif
-using System;
-using Xunit;
-
 namespace MySqlConnector.Tests;
 
 public class MySqlAttributeTests
diff --git a/tests/MySqlConnector.Tests/MySqlConnectionStringBuilderTests.cs b/tests/MySqlConnector.Tests/MySqlConnectionStringBuilderTests.cs
index 0f61413df..0601f6e3a 100644
--- a/tests/MySqlConnector.Tests/MySqlConnectionStringBuilderTests.cs
+++ b/tests/MySqlConnector.Tests/MySqlConnectionStringBuilderTests.cs
@@ -1,11 +1,5 @@
-using System;
 using System.ComponentModel;
 using System.Globalization;
-using System.Linq;
-#if MYSQL_DATA
-using MySql.Data.MySqlClient;
-#endif
-using Xunit;
 
 namespace MySqlConnector.Tests;
 
@@ -164,7 +158,7 @@ public void ParseConnectionString()
 				"ssl mode=verifyca;" +
 				"tls version=Tls12, TLS v1.3;" +
 				"Uid=username;" +
-				"useaffectedrows=true"
+				"useaffectedrows=true",
 		};
 		Assert.True(csb.AllowLoadLocalInfile);
 		Assert.True(csb.AllowPublicKeyRetrieval);
@@ -173,7 +167,7 @@ public void ParseConnectionString()
 		Assert.False(csb.AutoEnlist);
 #if !MYSQL_DATA
 		Assert.Equal(-1, csb.CancellationTimeout);
-		// Connector/NET treats "CertificateFile" (client certificate) and "SslCa" (server CA) as aliases
+		//// Connector/NET treats "CertificateFile" (client certificate) and "SslCa" (server CA) as aliases
 		Assert.Equal("file.pfx", csb.CertificateFile);
 #endif
 		Assert.Equal("Pass2345", csb.CertificatePassword);
@@ -452,7 +446,7 @@ public void ParseTlsVersion(string input, string expected)
 	{
 		var csb = new MySqlConnectionStringBuilder { TlsVersion = input };
 #if !MYSQL_DATA
-		string[] normalizedVersions = new[] { "TLS 1.0", "TLS 1.1", "TLS 1.2", "TLS 1.3" };
+		string[] normalizedVersions = ["TLS 1.0", "TLS 1.1", "TLS 1.2", "TLS 1.3"];
 #else
 		string[] normalizedVersions = new[] { "Tls", "Tls11", "Tls12", "Tls13" };
 #endif
@@ -589,4 +583,17 @@ public void NamedProperty(string propertyName, object value)
 			Assert.Equal(value, csb[propertyName]);
 		}
 	}
+
+	[Fact]
+	public void SpecialCharactersInPassword()
+	{
+		var builder = new MySqlConnectionStringBuilder
+		{
+			Password = "foo;=bar,baz",
+		};
+		Assert.Equal("Password=\"foo;=bar,baz\"", builder.ConnectionString, StringComparer.OrdinalIgnoreCase);
+#if !MYSQL_DATA // https://bugs.mysql.com/bug.php?id=111797
+		using var connection = new MySqlConnection(builder.ConnectionString);
+#endif
+	}
 }
diff --git a/tests/MySqlConnector.Tests/MySqlConnectionTests.cs b/tests/MySqlConnector.Tests/MySqlConnectionTests.cs
new file mode 100644
index 000000000..249fbe56b
--- /dev/null
+++ b/tests/MySqlConnector.Tests/MySqlConnectionTests.cs
@@ -0,0 +1,43 @@
+using System.Text;
+
+namespace MySqlConnector.Tests;
+
+public class MySqlConnectionTests
+{
+	[Theory]
+	[InlineData(IsolationLevel.ReadUncommitted, null, false, "\x3A\0\0\0\x03set session transaction isolation level read uncommitted;\x13\0\0\0\x03start transaction;")]
+	[InlineData(IsolationLevel.ReadUncommitted, null, true, "\x3C\0\0\0\x03\0\x01set session transaction isolation level read uncommitted;\x15\0\0\0\x03\0\x01start transaction;")]
+	[InlineData(IsolationLevel.ReadUncommitted, false, false, "\x3A\0\0\0\x03set session transaction isolation level read uncommitted;\x1E\0\0\0\x03start transaction read write;")]
+	[InlineData(IsolationLevel.ReadUncommitted, false, true, "\x3C\0\0\0\x03\0\x01set session transaction isolation level read uncommitted;\x20\0\0\0\x03\0\x01start transaction read write;")]
+	[InlineData(IsolationLevel.ReadUncommitted, true, false, "\x3A\0\0\0\x03set session transaction isolation level read uncommitted;\x1D\0\0\0\x03start transaction read only;")]
+	[InlineData(IsolationLevel.ReadUncommitted, true, true, "\x3C\0\0\0\x03\0\x01set session transaction isolation level read uncommitted;\x1F\0\0\0\x03\0\x01start transaction read only;")]
+	[InlineData(IsolationLevel.ReadCommitted, null, false, "\x38\0\0\0\x03set session transaction isolation level read committed;\x13\0\0\0\x03start transaction;")]
+	[InlineData(IsolationLevel.ReadCommitted, null, true, "\x3A\0\0\0\x03\0\x01set session transaction isolation level read committed;\x15\0\0\0\x03\0\x01start transaction;")]
+	[InlineData(IsolationLevel.ReadCommitted, false, false, "\x38\0\0\0\x03set session transaction isolation level read committed;\x1E\0\0\0\x03start transaction read write;")]
+	[InlineData(IsolationLevel.ReadCommitted, false, true, "\x3A\0\0\0\x03\0\x01set session transaction isolation level read committed;\x20\0\0\0\x03\0\x01start transaction read write;")]
+	[InlineData(IsolationLevel.ReadCommitted, true, false, "\x38\0\0\0\x03set session transaction isolation level read committed;\x1D\0\0\0\x03start transaction read only;")]
+	[InlineData(IsolationLevel.ReadCommitted, true, true, "\x3A\0\0\0\x03\0\x01set session transaction isolation level read committed;\x1F\0\0\0\x03\0\x01start transaction read only;")]
+	[InlineData(IsolationLevel.Serializable, null, false, "\x36\0\0\0\x03set session transaction isolation level serializable;\x13\0\0\0\x03start transaction;")]
+	[InlineData(IsolationLevel.Serializable, null, true, "\x38\0\0\0\x03\0\x01set session transaction isolation level serializable;\x15\0\0\0\x03\0\x01start transaction;")]
+	[InlineData(IsolationLevel.Serializable, false, false, "\x36\0\0\0\x03set session transaction isolation level serializable;\x1E\0\0\0\x03start transaction read write;")]
+	[InlineData(IsolationLevel.Serializable, false, true, "\x38\0\0\0\x03\0\x01set session transaction isolation level serializable;\x20\0\0\0\x03\0\x01start transaction read write;")]
+	[InlineData(IsolationLevel.Serializable, true, false, "\x36\0\0\0\x03set session transaction isolation level serializable;\x1D\0\0\0\x03start transaction read only;")]
+	[InlineData(IsolationLevel.Serializable, true, true, "\x38\0\0\0\x03\0\x01set session transaction isolation level serializable;\x1F\0\0\0\x03\0\x01start transaction read only;")]
+	[InlineData(IsolationLevel.RepeatableRead, null, false, "\x39\0\0\0\x03set session transaction isolation level repeatable read;\x13\0\0\0\x03start transaction;")]
+	[InlineData(IsolationLevel.RepeatableRead, null, true, "\x3B\0\0\0\x03\0\x01set session transaction isolation level repeatable read;\x15\0\0\0\x03\0\x01start transaction;")]
+	[InlineData(IsolationLevel.RepeatableRead, false, false, "\x39\0\0\0\x03set session transaction isolation level repeatable read;\x1E\0\0\0\x03start transaction read write;")]
+	[InlineData(IsolationLevel.RepeatableRead, false, true, "\x3B\0\0\0\x03\0\x01set session transaction isolation level repeatable read;\x20\0\0\0\x03\0\x01start transaction read write;")]
+	[InlineData(IsolationLevel.RepeatableRead, true, false, "\x39\0\0\0\x03set session transaction isolation level repeatable read;\x1D\0\0\0\x03start transaction read only;")]
+	[InlineData(IsolationLevel.RepeatableRead, true, true, "\x3B\0\0\0\x03\0\x01set session transaction isolation level repeatable read;\x1F\0\0\0\x03\0\x01start transaction read only;")]
+	[InlineData(IsolationLevel.Snapshot, null, false, "\x39\0\0\0\x03set session transaction isolation level repeatable read;\x2C\0\0\0\x03start transaction with consistent snapshot;")]
+	[InlineData(IsolationLevel.Snapshot, null, true, "\x3B\0\0\0\x03\0\x01set session transaction isolation level repeatable read;\x2E\0\0\0\x03\0\x01start transaction with consistent snapshot;")]
+	[InlineData(IsolationLevel.Snapshot, false, false, "\x39\0\0\0\x03set session transaction isolation level repeatable read;\x38\0\0\0\x03start transaction with consistent snapshot, read write;")]
+	[InlineData(IsolationLevel.Snapshot, false, true, "\x3B\0\0\0\x03\0\x01set session transaction isolation level repeatable read;\x3A\0\0\0\x03\0\x01start transaction with consistent snapshot, read write;")]
+	[InlineData(IsolationLevel.Snapshot, true, false, "\x39\0\0\0\x03set session transaction isolation level repeatable read;\x37\0\0\0\x03start transaction with consistent snapshot, read only;")]
+	[InlineData(IsolationLevel.Snapshot, true, true, "\x3B\0\0\0\x03\0\x01set session transaction isolation level repeatable read;\x39\0\0\0\x03\0\x01start transaction with consistent snapshot, read only;")]
+	public void GetStartTransactionPayload(IsolationLevel isolationLevel, bool? isReadOnly, bool supportsQueryAttributes, string expected)
+	{
+		var payload = MySqlConnection.GetStartTransactionPayload(isolationLevel, isReadOnly, supportsQueryAttributes);
+		Assert.Equal(expected, Encoding.ASCII.GetString(payload.Span.ToArray()));
+	}
+}
diff --git a/tests/MySqlConnector.Tests/MySqlConnector.Tests.csproj b/tests/MySqlConnector.Tests/MySqlConnector.Tests.csproj
index be2b5cac5..b375b14ae 100644
--- a/tests/MySqlConnector.Tests/MySqlConnector.Tests.csproj
+++ b/tests/MySqlConnector.Tests/MySqlConnector.Tests.csproj
@@ -1,43 +1,57 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup Condition=" '$(Configuration)' != 'MySqlData' ">
-    <TargetFrameworks>net481;net7.0</TargetFrameworks>
+    <TargetFrameworks>net481;net9.0</TargetFrameworks>
   </PropertyGroup>
 
  <PropertyGroup Condition=" '$(Configuration)' == 'MySqlData' ">
     <TargetFrameworks>net472</TargetFrameworks>
-    <DefineConstants>MYSQL_DATA</DefineConstants>
+    <DefineConstants>$(DefineConstants);MYSQL_DATA</DefineConstants>
+    <NoWarn>$(NoWarn);MSB3246</NoWarn>
   </PropertyGroup>
 
   <PropertyGroup>
-    <VersionPrefix>0.1.0</VersionPrefix>
     <SignAssembly>true</SignAssembly>
     <PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
     <AssemblyOriginatorKeyFile>..\..\MySqlConnector.snk</AssemblyOriginatorKeyFile>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
-    <AssemblyName>MySqlConnector.Tests</AssemblyName>
-    <PackageId>MySqlConnector.Tests</PackageId>
-    <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
-    <LangVersion>preview</LangVersion>
+    <LangVersion>12.0</LangVersion>
+    <NoWarn>$(NoWarn);SA1021</NoWarn>
+  </PropertyGroup>
+
+  <PropertyGroup Condition=" '$(GITHUB_ACTIONS)' == 'true' OR '$(APPVEYOR)' == 'True' OR '$(TF_BUILD)' == 'True' ">
+    <DefineConstants>$(DefineConstants);NO_METRICS_TESTS</DefineConstants>
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="xunit" Version="2.4.2" />
-    <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
-    <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3">
+    <PackageReference Include="xunit" />
+    <PackageReference Include="xunit.runner.visualstudio">
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
       <PrivateAssets>all</PrivateAssets>
     </PackageReference>
+    <PackageReference Include="Microsoft.NET.Test.Sdk" />
+    <Using Include="System" />
+    <Using Include="System.Collections.Generic" />
+    <Using Include="System.Data" />
+    <Using Include="System.Data.Common" />
+    <Using Include="System.Diagnostics" />
+    <Using Include="System.Linq" />
+    <Using Include="System.Threading" />
+    <Using Include="System.Threading.Tasks" />
+    <Using Include="Xunit" />
   </ItemGroup>
 
   <ItemGroup Condition=" '$(Configuration)' != 'MySqlData' ">
     <ProjectReference Include="..\..\src\MySqlConnector\MySqlConnector.csproj" />
+    <Using Include="MySqlConnector.Core" />
   </ItemGroup>
 
   <ItemGroup Condition=" '$(Configuration)' == 'MySqlData' ">
-    <PackageReference Include="MySql.Data" Version="8.0.31" />
-    <Compile Remove="ByteBufferWriterTests.cs;CachedProcedureTests.cs;CancellationTests.cs;ConnectionTests.cs;FakeMySqlServer.cs;FakeMySqlServerConnection.cs;LoadBalancerTests.cs;MySqlDecimalTests.cs;MySqlExceptionTests.cs;MySqlParameterCollectionNameToIndexTests.cs;NormalizeTests.cs;ServerVersionTests.cs;StatementPreparerTests.cs;TypeMapperTests.cs;UtilityTests.cs" />
+    <PackageReference Include="MySql.Data" />
+    <Compile Remove="ByteBufferWriterTests.cs;CachedProcedureTests.cs;CancellationTests.cs;ColumnCountPayloadTests.cs;ColumnReaderTests.cs;ConnectionOpenedCallbackTests.cs;ConnectionTests.cs;FakeMySqlServer.cs;FakeMySqlServerConnection.cs;LoadBalancerTests.cs;MySqlConnectionTests.cs;MySqlDecimalTests.cs;MySqlExceptionTests.cs;MySqlParameterAppendBinaryTests.cs;MySqlParameterCollectionNameToIndexTests.cs;NormalizeTests.cs;ServerVersionTests.cs;StatementPreparerTests.cs;TypeMapperTests.cs;UtilityTests.cs" />
+    <Compile Remove="Metrics\*.cs" />
+    <Using Include="MySql.Data.MySqlClient" />
+    <Using Include="MySql.Data.Types" />
   </ItemGroup>
 
 </Project>
diff --git a/tests/MySqlConnector.Tests/MySqlDateTimeTests.cs b/tests/MySqlConnector.Tests/MySqlDateTimeTests.cs
index 9638a5c45..3cb53ead7 100644
--- a/tests/MySqlConnector.Tests/MySqlDateTimeTests.cs
+++ b/tests/MySqlConnector.Tests/MySqlDateTimeTests.cs
@@ -1,9 +1,4 @@
-using System;
 using System.Globalization;
-#if MYSQL_DATA
-using MySql.Data.Types;
-#endif
-using Xunit;
 
 namespace MySqlConnector.Tests;
 
@@ -12,7 +7,7 @@ public class MySqlDateTimeTests
 	[Fact]
 	public void NewMySqlDateTimeIsNotValidDateTime()
 	{
-		var msdt = new MySqlDateTime();
+		var msdt = default(MySqlDateTime);
 		Assert.False(msdt.IsValidDateTime);
 	}
 
@@ -57,7 +52,7 @@ public void GetDateTime()
 	[Fact]
 	public void GetDateTimeForInvalidDate()
 	{
-		var msdt = new MySqlDateTime();
+		var msdt = default(MySqlDateTime);
 		Assert.False(msdt.IsValidDateTime);
 		Assert.Throws<MySqlConversionException>(() => msdt.GetDateTime());
 	}
@@ -65,7 +60,7 @@ public void GetDateTimeForInvalidDate()
 	[Fact]
 	public void SetMicrosecond()
 	{
-		var msdt = new MySqlDateTime();
+		var msdt = default(MySqlDateTime);
 		Assert.Equal(0, msdt.Microsecond);
 		msdt.Microsecond = 123456;
 		Assert.Equal(123, msdt.Millisecond);
@@ -98,7 +93,7 @@ public void ChangeTypeToDateTime()
 	[Fact]
 	public void NotConvertibleToDateTime()
 	{
-		IConvertible convertible = new MySqlDateTime();
+		IConvertible convertible = default(MySqlDateTime);
 #if !MYSQL_DATA
 		Assert.Throws<InvalidCastException>(() => convertible.ToDateTime(CultureInfo.InvariantCulture));
 #else
@@ -109,7 +104,7 @@ public void NotConvertibleToDateTime()
 	[Fact]
 	public void NotConvertToDateTime()
 	{
-		object obj = new MySqlDateTime();
+		object obj = default(MySqlDateTime);
 #if !MYSQL_DATA
 		Assert.Throws<InvalidCastException>(() => Convert.ToDateTime(obj));
 #else
@@ -120,7 +115,7 @@ public void NotConvertToDateTime()
 	[Fact]
 	public void NotChangeTypeToDateTime()
 	{
-		object obj = new MySqlDateTime();
+		object obj = default(MySqlDateTime);
 #if !MYSQL_DATA
 		Assert.Throws<InvalidCastException>(() => Convert.ChangeType(obj, TypeCode.DateTime));
 #else
@@ -139,7 +134,7 @@ public void ValidDateTimeConvertibleToString()
 	[Fact]
 	public void InvalidDateTimeConvertibleToString()
 	{
-		IConvertible convertible = new MySqlDateTime();
+		IConvertible convertible = default(MySqlDateTime);
 		Assert.Equal("0000-00-00", convertible.ToString(CultureInfo.InvariantCulture));
 	}
 #endif
@@ -260,6 +255,6 @@ public void Equal()
 	}
 #endif
 
-	static readonly MySqlDateTime s_mySqlDateTime = new(2018, 6, 9, 12, 34, 56, 123456);
-	static readonly DateTime s_dateTime = new DateTime(2018, 6, 9, 12, 34, 56, 123).AddTicks(4560);
+	private static readonly MySqlDateTime s_mySqlDateTime = new(2018, 6, 9, 12, 34, 56, 123456);
+	private static readonly DateTime s_dateTime = new DateTime(2018, 6, 9, 12, 34, 56, 123).AddTicks(4560);
 }
diff --git a/tests/MySqlConnector.Tests/MySqlDecimalTests.cs b/tests/MySqlConnector.Tests/MySqlDecimalTests.cs
index 03974650c..2d4bdc093 100644
--- a/tests/MySqlConnector.Tests/MySqlDecimalTests.cs
+++ b/tests/MySqlConnector.Tests/MySqlDecimalTests.cs
@@ -1,5 +1,4 @@
-using System;
-using Xunit;
+using System.Globalization;
 
 namespace MySqlConnector.Tests;
 
@@ -25,26 +24,12 @@ public void TestToDouble()
 	[Fact]
 	public void TestToDecimal()
 	{
-		decimal doubleVal = 1.2M ;
+		decimal doubleVal = 1.2M;
 		string stringVal = "1.2";
 		MySqlDecimal mySqlDecimal = new MySqlDecimal(stringVal);
 		Assert.Equal(doubleVal, mySqlDecimal.Value);
 	}
 
-	[Fact]
-	public void TestInvalidFormatWithDecimalPostive()
-	{
-		var invalidValue = "0323.323";
-		Assert.Throws<FormatException>(() => new MySqlDecimal(invalidValue));
-	}
-
-	[Fact]
-	public void TestInvalidFormatWithDecimalNegative()
-	{
-		var invalidValue = "-0323.323";
-		Assert.Throws<FormatException>(() => new MySqlDecimal(invalidValue));
-	}
-
 	[Fact]
 	public void TestValidFormatWithDecimalNegative68Length()
 	{
@@ -90,7 +75,7 @@ public void TestValidFormatWithDecimalNegativeZero()
 		var value = "-0.2342323";
 		var decimalVal = new MySqlDecimal(value);
 		Assert.Equal(value, decimalVal.ToString());
-		Assert.Equal(decimal.Parse(value), decimalVal.Value);
+		Assert.Equal(decimal.Parse(value, CultureInfo.InvariantCulture), decimalVal.Value);
 	}
 
 	[Fact]
@@ -110,6 +95,10 @@ public void TestValidFormatWithDecimalNegative67Length()
 	[InlineData("-0.1")]
 	[InlineData("1.0")]
 	[InlineData("1.23")]
+	[InlineData("00")]
+	[InlineData("01")]
+	[InlineData("0323.323")]
+	[InlineData("-0323.323")]
 	[InlineData("12345678901234567890123456789012345678901234567890123456789012345")]
 	[InlineData("-12345678901234567890123456789012345678901234567890123456789012345")]
 	[InlineData("12345678901234567890123456789012345.012345678901234567890123456789")]
@@ -121,8 +110,6 @@ public void ValidDecimalValues(string input) =>
 	[InlineData("")]
 	[InlineData("-0")]
 	[InlineData("-0.0")]
-	[InlineData("00")]
-	[InlineData("01")]
 	[InlineData("123456789012345678901234567890123456789012345678901234567890123456")]
 	[InlineData("-123456789012345678901234567890123456789012345678901234567890123456")]
 	[InlineData("123456789012345678901234567890123456.012345678901234567890123456789")]
diff --git a/tests/MySqlConnector.Tests/MySqlExceptionTests.cs b/tests/MySqlConnector.Tests/MySqlExceptionTests.cs
index f7d7b8778..e5ea7e766 100644
--- a/tests/MySqlConnector.Tests/MySqlExceptionTests.cs
+++ b/tests/MySqlConnector.Tests/MySqlExceptionTests.cs
@@ -1,8 +1,3 @@
-#if MYSQL_DATA
-using MySql.Data.MySqlClient;
-#endif
-using Xunit;
-
 namespace MySqlConnector.Tests;
 
 public class MySqlExceptionTests
diff --git a/tests/MySqlConnector.Tests/MySqlHelperTests.cs b/tests/MySqlConnector.Tests/MySqlHelperTests.cs
index 986815585..127d8273c 100644
--- a/tests/MySqlConnector.Tests/MySqlHelperTests.cs
+++ b/tests/MySqlConnector.Tests/MySqlHelperTests.cs
@@ -1,8 +1,3 @@
-#if MYSQL_DATA
-using MySql.Data.MySqlClient;
-#endif
-using Xunit;
-
 namespace MySqlConnector.Tests;
 
 public class MySqlHelperTests
diff --git a/tests/MySqlConnector.Tests/MySqlParameterAppendBinaryTests.cs b/tests/MySqlConnector.Tests/MySqlParameterAppendBinaryTests.cs
new file mode 100644
index 000000000..d5a9a3835
--- /dev/null
+++ b/tests/MySqlConnector.Tests/MySqlParameterAppendBinaryTests.cs
@@ -0,0 +1,27 @@
+using MySqlConnector.Protocol.Serialization;
+
+namespace MySqlConnector.Tests;
+
+public class MySqlParameterAppendBinaryTests
+{
+	[Theory]
+	[InlineData(DummySByteEnum.SecondValue, MySqlDbType.Byte, new byte[] { 0x11 })]
+	[InlineData(DummyByteEnum.SecondValue, MySqlDbType.UByte, new byte[] { 0x11 })]
+	[InlineData(DummyShortEnum.SecondValue, MySqlDbType.Int16, new byte[] { 0x22, 0x11 })]
+	[InlineData(DummyUShortEnum.SecondValue, MySqlDbType.UInt16, new byte[] { 0x22, 0x11 })]
+	[InlineData(DummyIntEnum.SecondValue, MySqlDbType.Int32, new byte[] { 0x44, 0x33, 0x22, 0x11 })]
+	[InlineData(DummyUIntEnum.SecondValue, MySqlDbType.UInt32, new byte[] { 0x44, 0x33, 0x22, 0x11 })]
+	[InlineData(DummyEnum.SecondValue, MySqlDbType.Int32, new byte[] { 0x01, 0x00, 0x00, 0x00 })]
+	[InlineData(DummyLongEnum.SecondValue, MySqlDbType.Int64, new byte[] { 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11 })]
+	[InlineData(DummyULongEnum.SecondValue, MySqlDbType.UInt64, new byte[] { 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11 })]
+	public void WriteBinaryEnumType(object value, MySqlDbType expectedMySqlDbType, byte[] expectedBinary)
+	{
+		var parameter = new MySqlParameter { Value = value };
+		var writer = new ByteBufferWriter();
+		parameter.AppendBinary(writer, StatementPreparerOptions.None);
+
+		Assert.Equal(parameter.MySqlDbType, expectedMySqlDbType);
+		Assert.Equal(writer.Position, expectedBinary.Length);
+		Assert.Equal(writer.ArraySegment.ToArray(), expectedBinary);
+	}
+}
diff --git a/tests/MySqlConnector.Tests/MySqlParameterCollectionNameToIndexTests.cs b/tests/MySqlConnector.Tests/MySqlParameterCollectionNameToIndexTests.cs
index 5ab63400b..fb289e30c 100644
--- a/tests/MySqlConnector.Tests/MySqlParameterCollectionNameToIndexTests.cs
+++ b/tests/MySqlConnector.Tests/MySqlParameterCollectionNameToIndexTests.cs
@@ -1,17 +1,15 @@
-using Xunit;
-
 namespace MySqlConnector.Tests;
 
 public class MySqlParameterCollectionNameToIndexTests
 {
 	public MySqlParameterCollectionNameToIndexTests()
 	{
-		m_collection = new MySqlParameterCollection
-		{
+		m_collection =
+		[
 			new MySqlParameter { ParameterName = "A", Value = 1 },
 			new MySqlParameter { ParameterName = "B", Value = 2 },
 			new MySqlParameter { ParameterName = "C", Value = 3 },
-		};
+		];
 	}
 
 	[Fact]
@@ -126,5 +124,5 @@ public void ChangeParameter()
 		Assert.Equal(1, m_collection.NormalizedIndexOf("D"));
 	}
 
-	MySqlParameterCollection m_collection;
+	private readonly MySqlParameterCollection m_collection;
 }
diff --git a/tests/MySqlConnector.Tests/MySqlParameterCollectionTests.cs b/tests/MySqlConnector.Tests/MySqlParameterCollectionTests.cs
index 80934c44b..23e816e66 100644
--- a/tests/MySqlConnector.Tests/MySqlParameterCollectionTests.cs
+++ b/tests/MySqlConnector.Tests/MySqlParameterCollectionTests.cs
@@ -1,9 +1,3 @@
-#if MYSQL_DATA
-using MySql.Data.MySqlClient;
-#endif
-using System;
-using Xunit;
-
 namespace MySqlConnector.Tests;
 
 public class MySqlParameterCollectionTests
@@ -25,5 +19,5 @@ public MySqlParameterCollectionTests()
 	[Fact]
 	public void RemoveAtEnd() => Assert.Throws<ArgumentOutOfRangeException>(() => m_collection.RemoveAt(0));
 
-	MySqlParameterCollection m_collection;
+	private readonly MySqlParameterCollection m_collection;
 }
diff --git a/tests/IntegrationTests/ParameterTests.cs b/tests/MySqlConnector.Tests/MySqlParameterTests.cs
similarity index 96%
rename from tests/IntegrationTests/ParameterTests.cs
rename to tests/MySqlConnector.Tests/MySqlParameterTests.cs
index 26b16f435..710c699cf 100644
--- a/tests/IntegrationTests/ParameterTests.cs
+++ b/tests/MySqlConnector.Tests/MySqlParameterTests.cs
@@ -1,6 +1,6 @@
-namespace IntegrationTests;
+namespace MySqlConnector.Tests;
 
-public class ParameterTests
+public class MySqlParameterTests
 {
 	[Theory]
 	[InlineData(DbType.Byte, MySqlDbType.UByte)]
@@ -226,7 +226,7 @@ public void CloneDirection()
 		Assert.Equal(parameter.Direction, clone.Direction);
 	}
 
-	[SkippableFact(MySqlData = "https://bugs.mysql.com/bug.php?id=92734")]
+	[Fact]
 	public void CloneIsNullable()
 	{
 		var parameter = new MySqlParameter { IsNullable = true };
@@ -234,7 +234,7 @@ public void CloneIsNullable()
 		Assert.Equal(parameter.IsNullable, clone.IsNullable);
 	}
 
-	[SkippableFact(MySqlData = "https://bugs.mysql.com/bug.php?id=92734")]
+	[Fact]
 	public void ClonePrecision()
 	{
 		var parameter = new MySqlParameter { Precision = 10 };
@@ -242,7 +242,7 @@ public void ClonePrecision()
 		Assert.Equal(parameter.Precision, clone.Precision);
 	}
 
-	[SkippableFact(MySqlData = "https://bugs.mysql.com/bug.php?id=92734")]
+	[Fact]
 	public void CloneScale()
 	{
 		var parameter = new MySqlParameter { Scale = 12 };
@@ -250,7 +250,7 @@ public void CloneScale()
 		Assert.Equal(parameter.Scale, clone.Scale);
 	}
 
-	[SkippableFact(MySqlData = "https://bugs.mysql.com/bug.php?id=92734")]
+	[Fact]
 	public void CloneSize()
 	{
 		var parameter = new MySqlParameter { Size = 8 };
@@ -266,7 +266,7 @@ public void CloneSourceColumn()
 		Assert.Equal(parameter.SourceColumn, clone.SourceColumn);
 	}
 
-	[SkippableFact(MySqlData = "https://bugs.mysql.com/bug.php?id=92734")]
+	[Fact]
 	public void CloneSourceColumnNullMapping()
 	{
 		var parameter = new MySqlParameter { SourceColumnNullMapping = true };
@@ -319,7 +319,6 @@ public void SetValueToByteArrayInfersType()
 		Assert.Equal(MySqlDbType.Blob, parameter.MySqlDbType);
 	}
 
-
 	[Fact]
 	public void SetValueDoesNotInferType()
 	{
diff --git a/tests/MySqlConnector.Tests/NormalizeTests.cs b/tests/MySqlConnector.Tests/NormalizeTests.cs
index 9f7527312..f0c365479 100644
--- a/tests/MySqlConnector.Tests/NormalizeTests.cs
+++ b/tests/MySqlConnector.Tests/NormalizeTests.cs
@@ -1,6 +1,3 @@
-using MySqlConnector.Core;
-using Xunit;
-
 namespace MySqlConnector.Tests;
 
 public class NormalizeTests
diff --git a/tests/MySqlConnector.Tests/ServerVersionTests.cs b/tests/MySqlConnector.Tests/ServerVersionTests.cs
index faaf0d11a..cb41661e9 100644
--- a/tests/MySqlConnector.Tests/ServerVersionTests.cs
+++ b/tests/MySqlConnector.Tests/ServerVersionTests.cs
@@ -1,7 +1,4 @@
-using System;
 using System.Text;
-using MySqlConnector.Core;
-using Xunit;
 
 namespace MySqlConnector.Tests;
 
diff --git a/tests/MySqlConnector.Tests/SkipCITheoryAttribute.cs b/tests/MySqlConnector.Tests/SkipCITheoryAttribute.cs
index 13eeac6a7..07544b7de 100644
--- a/tests/MySqlConnector.Tests/SkipCITheoryAttribute.cs
+++ b/tests/MySqlConnector.Tests/SkipCITheoryAttribute.cs
@@ -1,10 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Xunit;
-
 namespace MySqlConnector.Tests;
 
 public class SkipCITheoryAttribute : TheoryAttribute
diff --git a/tests/MySqlConnector.Tests/StatementPreparerTests.cs b/tests/MySqlConnector.Tests/StatementPreparerTests.cs
index 8805e1504..368c47645 100644
--- a/tests/MySqlConnector.Tests/StatementPreparerTests.cs
+++ b/tests/MySqlConnector.Tests/StatementPreparerTests.cs
@@ -1,12 +1,8 @@
-using System;
-using System.Collections.Generic;
 using System.Globalization;
 using System.IO;
 using System.Text;
-using MySqlConnector.Core;
 using MySqlConnector.Protocol.Serialization;
 using MySqlConnector.Utilities;
-using Xunit;
 
 namespace MySqlConnector.Tests;
 
@@ -146,48 +142,71 @@ public void FormatParameter(object parameterValue, string replacedValue, bool no
 		Assert.Equal(sql.Replace("@param", replacedValue), parsedSql);
 	}
 
+	[Theory]
+	[InlineData(true)]
+	[InlineData(false)]
+	public void AlterUserHardcoded(bool noBackslashEscapes)
+	{
+		const string sql = "ALTER USER 'root'@'%' IDENTIFIED BY 'password';";
+		var options = noBackslashEscapes ? StatementPreparerOptions.NoBackslashEscapes : StatementPreparerOptions.None;
+		var parsedSql = GetParsedSql(sql, options: options);
+		Assert.Equal(sql, parsedSql);
+	}
+
+	[Theory]
+	[InlineData(true)]
+	[InlineData(false)]
+	public void AlterUserWithParameters(bool noBackslashEscapes)
+	{
+		const string sql = "ALTER USER @user@'localhost' IDENTIFIED BY @password;";
+		var parameters = new MySqlParameterCollection { new("@user", "root"), new("@password", "P@ssw0rd") };
+		var options = noBackslashEscapes ? StatementPreparerOptions.NoBackslashEscapes : StatementPreparerOptions.None;
+		var parsedSql = GetParsedSql(sql, parameters, options);
+		Assert.Equal("ALTER USER 'root'@'localhost' IDENTIFIED BY 'P@ssw0rd';", parsedSql);
+	}
+
 	public static IEnumerable<object[]> FormatParameterData =>
 		new[]
 		{
 			new object[] { (byte) 200, "200" },
-			new object[] { (sbyte) -100, "-100" },
-			new object[] { (short) -12345, "-12345" },
-			new object[] { (ushort) 45678, "45678" },
-			new object[] { -1_234_567_890, "-1234567890" },
-			new object[] { 3_456_789_012u, "3456789012" },
-			new object[] { -12_345_678_901L, "-12345678901" },
-			new object[] { 12_345_678_901UL, "12345678901" },
+			[(sbyte) -100, "-100"],
+			[(short) -12345, "-12345"],
+			[(ushort) 45678, "45678"],
+			[-1_234_567_890, "-1234567890"],
+			[3_456_789_012u, "3456789012"],
+			[-12_345_678_901L, "-12345678901"],
+			[12_345_678_901UL, "12345678901"],
 #if NET481
-			new object[] { 1.0123456f, "1.01234555" },
+			[1.0123456f, "1.01234555"],
 #else
-			new object[] { 1.0123456f, "1.0123456" },
+			[1.0123456f, "1.0123456"],
 #endif
-			new object[] { 1.0123456789012346, "1.0123456789012346" },
-			new object[] { 123456789.123456789m, "123456789.123456789" },
-			new object[] { "1234", "'1234'" },
-			new object[] { "it's", "'it''s'" },
-			new object[] { "it's", "'it''s'", true },
-			new object[] { 'a', "'a'" },
-			new object[] { '\'', "''''" },
-			new object[] { '\'', "''''", true },
-			new object[] { '\\', "'\\\\'" },
-			new object[] { '\\', "'\\'", true },
-			new object[] { "\\'", "'\\\\'''" },
-			new object[] { "\\'", "'\\'''", true },
-			new object[] { 'ﬃ', "'ﬃ'" },
-			new object[] { new DateTime(1234, 12, 23, 12, 34, 56, 789), "timestamp('1234-12-23 12:34:56.789000')" },
-			new object[] { new DateTimeOffset(1234, 12, 23, 12, 34, 56, 789, TimeSpan.FromHours(2)), "timestamp('1234-12-23 10:34:56.789000')" },
-			new object[] { new TimeSpan(2, 3, 4, 5, 6), "time '51:04:05.006000'" },
-			new object[] { new TimeSpan(-2, -3, -4, -5, -6), "time '-51:04:05.006000'" },
-			new object[] { new Guid("00112233-4455-6677-8899-AABBCCDDEEFF"), "'00112233-4455-6677-8899-aabbccddeeff'" },
-			new object[] { new byte[] { 0x41, 0x42, 0x27, 0x61, 0x5c, 0x62 }, @"_binary'AB''a\\b'" },
-			new object[] { new byte[] { 0x41, 0x42, 0x27, 0x61, 0x5c, 0x62 }, @"_binary'AB''a\b'", true },
-			new object[] { new MemoryStream(new byte[] { 0x41, 0x42, 0x27, 0x61, 0x5c, 0x62 }), @"_binary'AB''a\\b'" },
-			new object[] { new MemoryStream(new byte[] { 0x41, 0x42, 0x27, 0x61, 0x5c, 0x62 }), @"_binary'AB''a\b'", true },
-			new object[] { new MemoryStream(new byte[] { 0, 0x41, 0x42, 0x27, 0x61, 0x5c, 0x62, 0x63 }, 1, 6, false, true), @"_binary'AB''a\\b'" },
-			new object[] { new MemoryStream(new byte[] { 0, 0x41, 0x42, 0x27, 0x61, 0x5c, 0x62, 0x63 }, 1, 6, false, true), @"_binary'AB''a\b'", true },
-			new object[] { "\"AB\\ab'".AsMemory(), @"'""AB\\ab'''" },
-			new object[] { new StringBuilder("\"AB\\ab'"), @"'""AB\\ab'''" },
+			[1.0123456789012346, "1.0123456789012346"],
+			[123456789.123456789m, "123456789.123456789"],
+			["1234", "'1234'"],
+			["it's", "'it''s'"],
+			["it's", "'it''s'", true],
+			['a', "'a'"],
+			['\'', "''''"],
+			['\'', "''''", true],
+			['\\', "'\\\\'"],
+			['\\', "'\\'", true],
+			["\\'", "'\\\\'''"],
+			["\\'", "'\\'''", true],
+			['ﬃ', "'ﬃ'"],
+			[new DateTime(1234, 12, 23, 12, 34, 56, 789), "timestamp('1234-12-23 12:34:56.789000')"],
+			[new DateTimeOffset(1234, 12, 23, 12, 34, 56, 789, TimeSpan.FromHours(2)), "timestamp('1234-12-23 10:34:56.789000')"],
+			[new TimeSpan(2, 3, 4, 5, 6), "time '51:04:05.006000'"],
+			[new TimeSpan(-2, -3, -4, -5, -6), "time '-51:04:05.006000'"],
+			[new Guid("00112233-4455-6677-8899-AABBCCDDEEFF"), "'00112233-4455-6677-8899-aabbccddeeff'"],
+			[new byte[] { 0x41, 0x42, 0x27, 0x61, 0x5c, 0x62 }, @"_binary'AB''a\\b'"],
+			[new byte[] { 0x41, 0x42, 0x27, 0x61, 0x5c, 0x62 }, @"_binary'AB''a\b'", true],
+			[new MemoryStream([0x41, 0x42, 0x27, 0x61, 0x5c, 0x62]), @"_binary'AB''a\\b'"],
+			[new MemoryStream([0x41, 0x42, 0x27, 0x61, 0x5c, 0x62]), @"_binary'AB''a\b'", true],
+			[new MemoryStream([0, 0x41, 0x42, 0x27, 0x61, 0x5c, 0x62, 0x63], 1, 6, false, true), @"_binary'AB''a\\b'"],
+			[new MemoryStream([0, 0x41, 0x42, 0x27, 0x61, 0x5c, 0x62, 0x63], 1, 6, false, true), @"_binary'AB''a\b'", true],
+			["\"AB\\ab'".AsMemory(), @"'""AB\\ab'''"],
+			[new StringBuilder("\"AB\\ab'"), @"'""AB\\ab'''"],
 		};
 
 	[Theory]
@@ -300,7 +319,7 @@ public void SplitStatement(string sql, string[] expectedStatements, string expec
 
 	private static string GetParsedSql(string input, MySqlParameterCollection parameters = null, StatementPreparerOptions options = StatementPreparerOptions.None)
 	{
-		var preparer = new StatementPreparer(input, parameters ?? new MySqlParameterCollection(), options);
+		var preparer = new StatementPreparer(input, parameters ?? [], options);
 		var writer = new ByteBufferWriter();
 		preparer.ParseAndBindParameters(writer);
 		using var payload = writer.ToPayloadData();
diff --git a/tests/MySqlConnector.Tests/TypeMapperTests.cs b/tests/MySqlConnector.Tests/TypeMapperTests.cs
index afb3d49dd..677e75e1d 100644
--- a/tests/MySqlConnector.Tests/TypeMapperTests.cs
+++ b/tests/MySqlConnector.Tests/TypeMapperTests.cs
@@ -1,9 +1,3 @@
-using System;
-using System.Data;
-using System.Linq;
-using MySqlConnector.Core;
-using Xunit;
-
 namespace MySqlConnector.Tests;
 
 public class TypeMapperTests
@@ -37,18 +31,18 @@ public void DbTypeMappingTest(Type clrType, DbType dbType)
 
 	[Theory]
 	[InlineData(1, DbType.Boolean, true)]
-	[InlineData(true, DbType.SByte, (sbyte)1)]
-	[InlineData((sbyte)1, DbType.Byte, (byte)1)]
-	[InlineData((byte)1, DbType.Int16, (short)1)]
-	[InlineData((short)1, DbType.UInt16, (ushort)1)]
-	[InlineData((ushort)1, DbType.Int32, 1)]
-	[InlineData(1, DbType.UInt32, (uint)1)]
-	[InlineData((uint)1, DbType.Int64, (long)1)]
-	[InlineData((long)1, DbType.UInt64, (ulong)1)]
-	[InlineData((ulong)1, DbType.String, "1")]
-	[InlineData((ulong)1, DbType.StringFixedLength, "1")]
-	[InlineData((ulong)1, DbType.AnsiString, "1")]
-	[InlineData((ulong)1, DbType.AnsiStringFixedLength, "1")]
+	[InlineData(true, DbType.SByte, (sbyte) 1)]
+	[InlineData((sbyte) 1, DbType.Byte, (byte) 1)]
+	[InlineData((byte) 1, DbType.Int16, (short) 1)]
+	[InlineData((short) 1, DbType.UInt16, (ushort) 1)]
+	[InlineData((ushort) 1, DbType.Int32, 1)]
+	[InlineData(1, DbType.UInt32, 1U)]
+	[InlineData(1U, DbType.Int64, 1L)]
+	[InlineData(1L, DbType.UInt64, 1UL)]
+	[InlineData(1UL, DbType.String, "1")]
+	[InlineData(1UL, DbType.StringFixedLength, "1")]
+	[InlineData(1UL, DbType.AnsiString, "1")]
+	[InlineData(1UL, DbType.AnsiStringFixedLength, "1")]
 	public void ConversionTest(object original, DbType dbType, object expected)
 	{
 		Assert.Equal(expected, TypeMapper.Instance.GetDbTypeMapping(dbType).DoConversion(original));
diff --git a/tests/MySqlConnector.Tests/UtilityTests.cs b/tests/MySqlConnector.Tests/UtilityTests.cs
index d31cade55..9b4fab4af 100644
--- a/tests/MySqlConnector.Tests/UtilityTests.cs
+++ b/tests/MySqlConnector.Tests/UtilityTests.cs
@@ -1,30 +1,24 @@
-using System;
 using System.Globalization;
 using System.Security.Cryptography;
 using System.Text;
 using MySqlConnector.Utilities;
-using Xunit;
 
 namespace MySqlConnector.Tests;
 
 public class UtilityTests
 {
 	[Theory]
-	[InlineData("Location: mysql://host.example.com:1234/user=user@host", "host.example.com", 1234, "user@host")]
-	[InlineData("Location: mysql://host.example.com:1234/user=user@host\n", "host.example.com", 1234, "user@host")]
-	[InlineData("Location: mysql://host.example.com:1234/user=user@host&ttl=60", "host.example.com", 1234, "user@host")]
-	[InlineData("Location: mysql://host.example.com:1234/user=user@host&ttl=60\n", "host.example.com", 1234, "user@host")]
-	[InlineData("Location: mysql://[host.example.com]:1234/?user=abcd", "host.example.com", 1234, "abcd")]
-	[InlineData("Location: mysql://[host.example.com]:1234/?user=abcd\n", "host.example.com", 1234, "abcd")]
-	[InlineData("Location: mysql://[host.example.com]:1234/?user=abcd&ttl=60", "host.example.com", 1234, "abcd")]
-	[InlineData("Location: mysql://[host.example.com]:1234/?user=abcd&ttl=60\n", "host.example.com", 1234, "abcd")]
-	[InlineData("Location: mysql://[2001:4860:4860::8888]:1234/?user=abcd", "2001:4860:4860::8888", 1234, "abcd")]
-	[InlineData("Location: mysql://[2001:4860:4860::8888]:1234/?user=abcd\n", "2001:4860:4860::8888", 1234, "abcd")]
-	[InlineData("Location: mysql://[2001:4860:4860::8888]:1234/?user=abcd&ttl=60", "2001:4860:4860::8888", 1234, "abcd")]
-	[InlineData("Location: mysql://[2001:4860:4860::8888]:1234/?user=abcd&ttl=60\n", "2001:4860:4860::8888", 1234, "abcd")]
+	[InlineData("mariadb://host.example.com:1234/?user=user@host", "host.example.com", 1234, "user@host")]
+	[InlineData("mariadb://user%40host:password@host.example.com:1234/", "host.example.com", 1234, "user@host")]
+	[InlineData("mariadb://host.example.com:1234/?user=user@host&ttl=60", "host.example.com", 1234, "user@host")]
+	[InlineData("mariadb://someuser:password@host.example.com:1234/?user=user@host&ttl=60\n", "host.example.com", 1234, "someuser")]
+	[InlineData("mysql://[2001:4860:4860::8888]:1234/?user=abcd", "2001:4860:4860::8888", 1234, "abcd")]
+	[InlineData("mysql://[2001:4860:4860::8888]:1234/?user=abcd\n", "2001:4860:4860::8888", 1234, "abcd")]
+	[InlineData("mysql://[2001:4860:4860::8888]:1234/?user=abcd&ttl=60", "2001:4860:4860::8888", 1234, "abcd")]
+	[InlineData("mysql://[2001:4860:4860::8888]:1234/?user=abcd&ttl=60\n", "2001:4860:4860::8888", 1234, "abcd")]
 	public void ParseRedirectionHeader(string input, string expectedHost, int expectedPort, string expectedUser)
 	{
-		Assert.True(Utility.TryParseRedirectionHeader(input, out var host, out var port, out var user));
+		Assert.True(Utility.TryParseRedirectionHeader(input, null, out var host, out var port, out var user));
 		Assert.Equal(expectedHost, host);
 		Assert.Equal(expectedPort, port);
 		Assert.Equal(expectedUser, user);
@@ -32,26 +26,14 @@ public void ParseRedirectionHeader(string input, string expectedHost, int expect
 
 	[Theory]
 	[InlineData("")]
-	[InlineData("Location: mysql")]
-	[InlineData("Location: mysql://host.example.com")]
-	[InlineData("Location: mysql://host.example.com:")]
-	[InlineData("Location: mysql://[host.example.com")]
-	[InlineData("Location: mysql://[host.example.com]")]
-	[InlineData("Location: mysql://[host.example.com]:")]
-	[InlineData("Location: mysql://host.example.com:123")]
-	[InlineData("Location: mysql://host.example.com:123/")]
-	[InlineData("Location: mysql://[host.example.com]:123")]
-	[InlineData("Location: mysql://[host.example.com]:123/")]
-	[InlineData("Location: mysql://host.example.com:/user=")]
-	[InlineData("Location: mysql://host.example.com:123/user=")]
-	[InlineData("Location: mysql://[host.example.com]:123/?user=")]
-	[InlineData("Location: mysql://host.example.com:/user=user@host")]
-	[InlineData("Location: mysql://host.example.com:-1/user=user@host")]
-	[InlineData("Location: mysql://host.example.com:0/user=user@host")]
-	[InlineData("Location: mysql://[host.example.com]:123/user=abcd")]
+	[InlineData("not formated")]
+	[InlineData("mysql")]
+	[InlineData("mysql://[host.example.com")]
+	[InlineData("mysql://host.example.com:-1/user=user@host")]
+	[InlineData("mysql://[host.example.com]:123/user=abcd")]
 	public void ParseRedirectionHeaderFails(string input)
 	{
-		Assert.False(Utility.TryParseRedirectionHeader(input, out _, out _, out _));
+		Assert.False(Utility.TryParseRedirectionHeader(input, null, out _, out _, out _));
 	}
 
 	[Theory]
@@ -105,9 +87,9 @@ public void ParseTimeSpanFails(string input)
 
 	[Theory]
 	[InlineData("", "")]
-	[InlineData("pre", "")]
-	[InlineData("", "post")]
-	[InlineData("pre", "post")]
+	[InlineData("pre\n", "")]
+	[InlineData("", "\npost")]
+	[InlineData("pre\n", "\npost")]
 	public void DecodePublicKey(string pre, string post)
 	{
 #if NET5_0_OR_GREATER
@@ -123,9 +105,9 @@ public void DecodePublicKey(string pre, string post)
 
 	[Theory]
 	[InlineData("", "")]
-	[InlineData("pre", "")]
-	[InlineData("", "post")]
-	[InlineData("pre", "post")]
+	[InlineData("pre\n", "")]
+	[InlineData("", "\npost")]
+	[InlineData("pre\n", "\npost")]
 	public void DecodePrivateKey(string pre, string post)
 	{
 #if NET5_0_OR_GREATER
@@ -145,7 +127,7 @@ public void DecodePrivateKey(string pre, string post)
 		Assert.Equal(s_iq, parameters.InverseQ);
 	}
 
-	const string c_publicKey = @"-----BEGIN PUBLIC KEY-----
+	private const string c_publicKey = @"-----BEGIN PUBLIC KEY-----
 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwmlDX62hQAQNvSJZ/HAO
 UjCbAiEPQquEyPpxjqDxyx1fVxL93U1au50xGk4sad4OH+GSZCChqj3kvwJhXc52
 iHdBzjQbucGYlC1wLNMc1F+H89vMjEq1ZexsRDWQSrgL1I6i9Mn5NFgS563yPBpO
@@ -155,7 +137,7 @@ public void DecodePrivateKey(string pre, string post)
 LwIDAQAB
 -----END PUBLIC KEY-----";
 
-	const string c_privateKey = @"-----BEGIN RSA PRIVATE KEY-----
+	private const string c_privateKey = @"-----BEGIN RSA PRIVATE KEY-----
 MIIEpAIBAAKCAQEAwmlDX62hQAQNvSJZ/HAOUjCbAiEPQquEyPpxjqDxyx1fVxL9
 3U1au50xGk4sad4OH+GSZCChqj3kvwJhXc52iHdBzjQbucGYlC1wLNMc1F+H89vM
 jEq1ZexsRDWQSrgL1I6i9Mn5NFgS563yPBpOyfYyGWCrL5w7yI+we3MCwy2q8JIO
@@ -183,12 +165,12 @@ public void DecodePrivateKey(string pre, string post)
 joMn7CFmwV9EYjPTxwkByKs5a8AbNYJDSfNCa3KoHTFjOvVMPec90Q==
 -----END RSA PRIVATE KEY-----";
 
-	static readonly byte[] s_modulus = new byte[] { 0xC2, 0x69, 0x43, 0x5F, 0xAD, 0xA1, 0x40, 0x04, 0x0D, 0xBD, 0x22, 0x59, 0xFC, 0x70, 0x0E, 0x52, 0x30, 0x9B, 0x02, 0x21, 0x0F, 0x42, 0xAB, 0x84, 0xC8, 0xFA, 0x71, 0x8E, 0xA0, 0xF1, 0xCB, 0x1D, 0x5F, 0x57, 0x12, 0xFD, 0xDD, 0x4D, 0x5A, 0xBB, 0x9D, 0x31, 0x1A, 0x4E, 0x2C, 0x69, 0xDE, 0x0E, 0x1F, 0xE1, 0x92, 0x64, 0x20, 0xA1, 0xAA, 0x3D, 0xE4, 0xBF, 0x02, 0x61, 0x5D, 0xCE, 0x76, 0x88, 0x77, 0x41, 0xCE, 0x34, 0x1B, 0xB9, 0xC1, 0x98, 0x94, 0x2D, 0x70, 0x2C, 0xD3, 0x1C, 0xD4, 0x5F, 0x87, 0xF3, 0xDB, 0xCC, 0x8C, 0x4A, 0xB5, 0x65, 0xEC, 0x6C, 0x44, 0x35, 0x90, 0x4A, 0xB8, 0x0B, 0xD4, 0x8E, 0xA2, 0xF4, 0xC9, 0xF9, 0x34, 0x58, 0x12, 0xE7, 0xAD, 0xF2, 0x3C, 0x1A, 0x4E, 0xC9, 0xF6, 0x32, 0x19, 0x60, 0xAB, 0x2F, 0x9C, 0x3B, 0xC8, 0x8F, 0xB0, 0x7B, 0x73, 0x02, 0xC3, 0x2D, 0xAA, 0xF0, 0x92, 0x0E, 0xC9, 0x97, 0xA0, 0xCE, 0x1E, 0xFA, 0x5B, 0xE7, 0xF7, 0x17, 0xDA, 0x45, 0x76, 0x77, 0xE3, 0x40, 0xEF, 0xAF, 0x8C, 0xFC, 0x8D, 0xBA, 0x99, 0xD3, 0x10, 0x3B, 0x58, 0x6C, 0x68, 0xC5, 0x61, 0xAA, 0x98, 0x21, 0x33, 0x67, 0x71, 0x9F, 0x1C, 0x2F, 0x52, 0xD8, 0x57, 0x0B, 0x94, 0x5D, 0x6D, 0x0F, 0x5B, 0xC6, 0xDF, 0x94, 0x47, 0x45, 0x77, 0xF0, 0x6B, 0xF1, 0xFC, 0xC2, 0x80, 0xAE, 0x08, 0x55, 0xD4, 0xA6, 0xE8, 0xE5, 0x4F, 0xCD, 0x26, 0x5A, 0xB5, 0xAC, 0x38, 0xDA, 0x37, 0x17, 0xDA, 0xD3, 0x53, 0xF1, 0xFD, 0xCF, 0xDC, 0x29, 0x40, 0xBC, 0xBD, 0x55, 0xDE, 0x3E, 0x04, 0x74, 0x21, 0xF2, 0x60, 0xDA, 0xCA, 0x26, 0xAC, 0x19, 0xEE, 0x74, 0xED, 0x8E, 0xA0, 0x41, 0x3E, 0xA3, 0x66, 0x36, 0x27, 0x11, 0xDD, 0xE3, 0x3C, 0xB2, 0xE1, 0xA3, 0x31, 0x6C, 0xA6, 0x2F };
-	static readonly byte[] s_exponent = new byte[] { 0x01, 0x00, 0x01 };
-	static readonly byte[] s_d = new byte[] { 0x11, 0xB3, 0x61, 0xD6, 0x01, 0x4A, 0x10, 0x39, 0x0E, 0x43, 0x2D, 0x30, 0x82, 0x42, 0x75, 0x9B, 0x58, 0x1F, 0x94, 0xE7, 0x0B, 0xAB, 0xA7, 0x50, 0x34, 0xB8, 0x50, 0xC4, 0x8A, 0xB4, 0xD9, 0x28, 0x78, 0x78, 0xC6, 0x1A, 0xE5, 0x1E, 0x58, 0xE7, 0x82, 0x1D, 0x69, 0x66, 0xBA, 0xB6, 0x7C, 0xE8, 0x4E, 0x50, 0xCC, 0x72, 0x5F, 0x62, 0x38, 0xCC, 0xDF, 0xD9, 0xE0, 0x4E, 0x9B, 0x2A, 0xE4, 0x31, 0xF2, 0xA1, 0xC4, 0x33, 0x8C, 0x5E, 0xB2, 0x58, 0xC1, 0x0D, 0x0E, 0x0C, 0x96, 0xC0, 0x26, 0x39, 0xF8, 0x77, 0xE4, 0x88, 0x50, 0x31, 0xB1, 0x28, 0x71, 0x89, 0x3E, 0x00, 0x9E, 0x7D, 0x9F, 0x1A, 0x3C, 0xA5, 0x2C, 0x03, 0x1B, 0xAC, 0x9B, 0xEE, 0x74, 0xF2, 0x31, 0x2D, 0x3F, 0xF9, 0xA9, 0x38, 0xCC, 0x38, 0xAD, 0x84, 0xD6, 0xB1, 0x44, 0x09, 0x51, 0x6E, 0x7B, 0xC4, 0x11, 0xA9, 0xAB, 0xB9, 0x80, 0xD4, 0xA8, 0x87, 0xEF, 0x2C, 0x76, 0x51, 0x64, 0x3E, 0xE5, 0x37, 0x7F, 0xF1, 0x26, 0xD7, 0xC0, 0x80, 0xEB, 0x96, 0xA9, 0x1A, 0x4D, 0xB8, 0xAF, 0xC9, 0xE5, 0xA2, 0x43, 0x7A, 0x6C, 0xEB, 0x9F, 0xF3, 0x8A, 0x72, 0xDD, 0xE7, 0xD2, 0xEB, 0x60, 0xD6, 0x01, 0xDD, 0x25, 0x40, 0x8B, 0x71, 0xBA, 0x0D, 0x49, 0x55, 0x3A, 0x1D, 0x9B, 0x60, 0x71, 0x2C, 0x37, 0xAF, 0xFB, 0xD9, 0x3C, 0xEF, 0x77, 0xF1, 0x43, 0xE9, 0x7F, 0xE9, 0x2C, 0xDC, 0x76, 0xDC, 0x34, 0x48, 0xFF, 0x4F, 0xB4, 0xBE, 0x93, 0x82, 0xC3, 0x14, 0x42, 0xC6, 0xE4, 0x01, 0xF5, 0xB0, 0xDE, 0xED, 0x64, 0x82, 0xB9, 0x1C, 0x17, 0x0A, 0x28, 0x89, 0x66, 0xC2, 0x45, 0xA3, 0x4E, 0xBC, 0x61, 0xB4, 0xD7, 0xCC, 0x47, 0xDC, 0x86, 0x8E, 0x1F, 0xEE, 0x8E, 0x0D, 0x0F, 0x51, 0xB1, 0xAC, 0x62, 0xEF, 0xDC, 0x6D, 0x90, 0x81 };
-	static readonly byte[] s_p = new byte[] { 0xFA, 0x63, 0x13, 0x19, 0xF3, 0x13, 0x7A, 0xC0, 0x70, 0x67, 0xAD, 0xE8, 0x05, 0x26, 0x70, 0x68, 0x9F, 0x30, 0xFB, 0x06, 0x9E, 0x45, 0xFF, 0x38, 0xF2, 0x56, 0xA3, 0x35, 0x4F, 0xAE, 0x21, 0x20, 0x94, 0x44, 0x80, 0x42, 0x61, 0x39, 0xC3, 0xEB, 0xA9, 0x8F, 0xD2, 0x61, 0x88, 0xE9, 0x6A, 0x62, 0x42, 0xF0, 0x18, 0x55, 0xDE, 0x50, 0x73, 0x1E, 0xFA, 0x70, 0xE5, 0x25, 0x73, 0x28, 0x6F, 0x29, 0x40, 0xE0, 0x09, 0x1A, 0xB6, 0x11, 0xF4, 0xB1, 0xEC, 0x89, 0x25, 0x66, 0x7D, 0x41, 0xC7, 0x53, 0xFA, 0x3A, 0x58, 0xF0, 0xB2, 0x27, 0x73, 0xE8, 0x63, 0x49, 0x06, 0x15, 0xF1, 0x4C, 0x35, 0x9D, 0x33, 0xE5, 0x28, 0x3E, 0xBF, 0x2F, 0x9F, 0xE4, 0x33, 0xE9, 0xCE, 0x6F, 0x91, 0x18, 0xAD, 0x95, 0xD4, 0xF5, 0x12, 0xD6, 0x92, 0xBD, 0xDA, 0xDF, 0x64, 0xA8, 0xB4, 0xEC, 0x7E, 0xD8, 0x1A, 0x41 };
-	static readonly byte[] s_q = new byte[] { 0xC6, 0xC4, 0xF4, 0x19, 0x1A, 0x74, 0xFB, 0xA1, 0x07, 0x3D, 0x8A, 0x80, 0x9A, 0x6F, 0xC5, 0x60, 0xCB, 0x68, 0x32, 0x14, 0xCA, 0x9D, 0x21, 0x59, 0x7C, 0xA5, 0xAC, 0x0B, 0x68, 0x0E, 0x8C, 0xE2, 0x82, 0x77, 0x6C, 0xBC, 0xC2, 0xA6, 0xB3, 0xF2, 0x8B, 0xE4, 0xC8, 0xDF, 0xF9, 0x7B, 0x92, 0x82, 0xE6, 0x20, 0x84, 0xCB, 0xE0, 0x46, 0x44, 0x0A, 0xB4, 0xD7, 0x48, 0x43, 0x29, 0x78, 0xE7, 0xCA, 0xE4, 0x8F, 0xEC, 0x52, 0x2A, 0x6F, 0xDA, 0x58, 0x8D, 0xAA, 0xEB, 0xC0, 0x5E, 0x4F, 0xC1, 0x70, 0x9A, 0x97, 0xCE, 0xE4, 0xFF, 0x70, 0xEC, 0xA1, 0x2A, 0xED, 0xD8, 0x7A, 0xA1, 0xA5, 0x1C, 0x79, 0x2A, 0x8E, 0xBC, 0x74, 0xA3, 0x45, 0x20, 0x27, 0x14, 0xE1, 0xB9, 0x16, 0x80, 0x26, 0x81, 0xC2, 0x17, 0x26, 0xBB, 0x83, 0x04, 0x88, 0xFB, 0x0B, 0xB2, 0x06, 0x49, 0x88, 0x9A, 0xC0, 0x44, 0x6F };
-	static readonly byte[] s_dp = new byte[] { 0xA9, 0x1C, 0xD1, 0x81, 0xED, 0x53, 0x72, 0xCD, 0x17, 0x1E, 0x6F, 0xAF, 0x0E, 0x0B, 0x70, 0x50, 0xB9, 0x74, 0x73, 0x97, 0x7C, 0xBE, 0xFA, 0x2A, 0x94, 0x43, 0x3F, 0xE8, 0x79, 0xF0, 0xBA, 0x1F, 0x2F, 0x05, 0x35, 0x3B, 0xA0, 0x3F, 0x4B, 0xBC, 0x97, 0xC1, 0xB3, 0xEE, 0x7C, 0x72, 0x6A, 0x90, 0x03, 0x75, 0xF6, 0x79, 0xC8, 0xB5, 0xBD, 0x8B, 0x66, 0xCB, 0x69, 0x79, 0x69, 0xD6, 0x1D, 0x7E, 0x57, 0x7A, 0xF9, 0xAB, 0x38, 0xDD, 0xD7, 0xCF, 0x74, 0x5B, 0x0D, 0x0F, 0xB3, 0xFA, 0x01, 0x8F, 0x0E, 0xAA, 0xE9, 0xF7, 0x67, 0xDC, 0x8D, 0xC4, 0x9F, 0x8A, 0x6A, 0xB9, 0xD7, 0x48, 0x09, 0xCE, 0x2B, 0x86, 0xD3, 0x2F, 0xCE, 0x21, 0x74, 0x74, 0xC5, 0x3B, 0x5D, 0x5A, 0xAD, 0x3D, 0x65, 0xC0, 0x95, 0x1D, 0x11, 0xA4, 0x2B, 0x68, 0x5F, 0xB4, 0x12, 0x60, 0x80, 0x12, 0x5B, 0x79, 0xCA, 0x01 };
-	static readonly byte[] s_dq = new byte[] { 0x99, 0x7F, 0xF2, 0x0E, 0x0C, 0xDB, 0x78, 0x93, 0x63, 0x97, 0x08, 0x05, 0xC6, 0xBE, 0x38, 0x4C, 0x46, 0xE9, 0x21, 0x7B, 0xE2, 0xF8, 0xB3, 0x8F, 0x7A, 0xEE, 0x2A, 0x4D, 0xE8, 0xBF, 0x0B, 0xD7, 0xC4, 0xEF, 0x5B, 0x3E, 0xEE, 0x87, 0x74, 0x82, 0x03, 0xBC, 0xDB, 0xCF, 0xF3, 0xC7, 0x95, 0x5E, 0x97, 0x3F, 0x57, 0xAE, 0x66, 0x75, 0x7D, 0x08, 0x53, 0x9D, 0xC9, 0x85, 0x6C, 0x5A, 0x9D, 0x8E, 0x97, 0x31, 0xFA, 0x4B, 0x7D, 0xD0, 0x41, 0x6B, 0x8F, 0x84, 0x06, 0x69, 0xD9, 0xAB, 0x77, 0xF4, 0x70, 0xBE, 0x79, 0x9D, 0x69, 0x13, 0x18, 0xFA, 0x61, 0xBF, 0xAA, 0x25, 0x11, 0xFE, 0x03, 0x5E, 0x23, 0x1A, 0x75, 0xB9, 0x11, 0x79, 0x49, 0x9A, 0x44, 0xCB, 0x08, 0x77, 0xB2, 0xE1, 0x89, 0xE5, 0x8C, 0xA1, 0x71, 0xBF, 0x7D, 0x5A, 0x17, 0x9D, 0x71, 0x8E, 0xDF, 0x6C, 0xAA, 0x2D, 0x7E, 0xEB };
-	static readonly byte[] s_iq = new byte[] { 0x2F, 0x91, 0x84, 0x58, 0xAB, 0x47, 0xAE, 0xE6, 0x92, 0xBC, 0xE4, 0x13, 0xBA, 0x9C, 0xDD, 0x66, 0xFF, 0x91, 0xA8, 0x94, 0x41, 0xBF, 0xD6, 0x54, 0x1B, 0x6D, 0x92, 0xC0, 0x67, 0xC6, 0x08, 0x23, 0xA4, 0x6D, 0x24, 0xA1, 0xC1, 0xBD, 0xCB, 0xC5, 0xAF, 0x4E, 0x96, 0x39, 0x46, 0x4A, 0x44, 0xD7, 0x10, 0x8F, 0x35, 0x76, 0xF5, 0xCB, 0x86, 0xE2, 0x9A, 0xAD, 0x32, 0xCD, 0x2C, 0xC0, 0xB8, 0x4A, 0x31, 0xB0, 0xAC, 0xF8, 0xFA, 0xAB, 0xEB, 0x0E, 0x8A, 0x7D, 0x92, 0x0D, 0xB5, 0x5C, 0x8B, 0x36, 0xD6, 0xC3, 0x56, 0x8F, 0x6B, 0xC0, 0x54, 0x5C, 0x8E, 0x83, 0x27, 0xEC, 0x21, 0x66, 0xC1, 0x5F, 0x44, 0x62, 0x33, 0xD3, 0xC7, 0x09, 0x01, 0xC8, 0xAB, 0x39, 0x6B, 0xC0, 0x1B, 0x35, 0x82, 0x43, 0x49, 0xF3, 0x42, 0x6B, 0x72, 0xA8, 0x1D, 0x31, 0x63, 0x3A, 0xF5, 0x4C, 0x3D, 0xE7, 0x3D, 0xD1 };
+	private static readonly byte[] s_modulus = [0xC2, 0x69, 0x43, 0x5F, 0xAD, 0xA1, 0x40, 0x04, 0x0D, 0xBD, 0x22, 0x59, 0xFC, 0x70, 0x0E, 0x52, 0x30, 0x9B, 0x02, 0x21, 0x0F, 0x42, 0xAB, 0x84, 0xC8, 0xFA, 0x71, 0x8E, 0xA0, 0xF1, 0xCB, 0x1D, 0x5F, 0x57, 0x12, 0xFD, 0xDD, 0x4D, 0x5A, 0xBB, 0x9D, 0x31, 0x1A, 0x4E, 0x2C, 0x69, 0xDE, 0x0E, 0x1F, 0xE1, 0x92, 0x64, 0x20, 0xA1, 0xAA, 0x3D, 0xE4, 0xBF, 0x02, 0x61, 0x5D, 0xCE, 0x76, 0x88, 0x77, 0x41, 0xCE, 0x34, 0x1B, 0xB9, 0xC1, 0x98, 0x94, 0x2D, 0x70, 0x2C, 0xD3, 0x1C, 0xD4, 0x5F, 0x87, 0xF3, 0xDB, 0xCC, 0x8C, 0x4A, 0xB5, 0x65, 0xEC, 0x6C, 0x44, 0x35, 0x90, 0x4A, 0xB8, 0x0B, 0xD4, 0x8E, 0xA2, 0xF4, 0xC9, 0xF9, 0x34, 0x58, 0x12, 0xE7, 0xAD, 0xF2, 0x3C, 0x1A, 0x4E, 0xC9, 0xF6, 0x32, 0x19, 0x60, 0xAB, 0x2F, 0x9C, 0x3B, 0xC8, 0x8F, 0xB0, 0x7B, 0x73, 0x02, 0xC3, 0x2D, 0xAA, 0xF0, 0x92, 0x0E, 0xC9, 0x97, 0xA0, 0xCE, 0x1E, 0xFA, 0x5B, 0xE7, 0xF7, 0x17, 0xDA, 0x45, 0x76, 0x77, 0xE3, 0x40, 0xEF, 0xAF, 0x8C, 0xFC, 0x8D, 0xBA, 0x99, 0xD3, 0x10, 0x3B, 0x58, 0x6C, 0x68, 0xC5, 0x61, 0xAA, 0x98, 0x21, 0x33, 0x67, 0x71, 0x9F, 0x1C, 0x2F, 0x52, 0xD8, 0x57, 0x0B, 0x94, 0x5D, 0x6D, 0x0F, 0x5B, 0xC6, 0xDF, 0x94, 0x47, 0x45, 0x77, 0xF0, 0x6B, 0xF1, 0xFC, 0xC2, 0x80, 0xAE, 0x08, 0x55, 0xD4, 0xA6, 0xE8, 0xE5, 0x4F, 0xCD, 0x26, 0x5A, 0xB5, 0xAC, 0x38, 0xDA, 0x37, 0x17, 0xDA, 0xD3, 0x53, 0xF1, 0xFD, 0xCF, 0xDC, 0x29, 0x40, 0xBC, 0xBD, 0x55, 0xDE, 0x3E, 0x04, 0x74, 0x21, 0xF2, 0x60, 0xDA, 0xCA, 0x26, 0xAC, 0x19, 0xEE, 0x74, 0xED, 0x8E, 0xA0, 0x41, 0x3E, 0xA3, 0x66, 0x36, 0x27, 0x11, 0xDD, 0xE3, 0x3C, 0xB2, 0xE1, 0xA3, 0x31, 0x6C, 0xA6, 0x2F];
+	private static readonly byte[] s_exponent = [0x01, 0x00, 0x01];
+	private static readonly byte[] s_d = [0x11, 0xB3, 0x61, 0xD6, 0x01, 0x4A, 0x10, 0x39, 0x0E, 0x43, 0x2D, 0x30, 0x82, 0x42, 0x75, 0x9B, 0x58, 0x1F, 0x94, 0xE7, 0x0B, 0xAB, 0xA7, 0x50, 0x34, 0xB8, 0x50, 0xC4, 0x8A, 0xB4, 0xD9, 0x28, 0x78, 0x78, 0xC6, 0x1A, 0xE5, 0x1E, 0x58, 0xE7, 0x82, 0x1D, 0x69, 0x66, 0xBA, 0xB6, 0x7C, 0xE8, 0x4E, 0x50, 0xCC, 0x72, 0x5F, 0x62, 0x38, 0xCC, 0xDF, 0xD9, 0xE0, 0x4E, 0x9B, 0x2A, 0xE4, 0x31, 0xF2, 0xA1, 0xC4, 0x33, 0x8C, 0x5E, 0xB2, 0x58, 0xC1, 0x0D, 0x0E, 0x0C, 0x96, 0xC0, 0x26, 0x39, 0xF8, 0x77, 0xE4, 0x88, 0x50, 0x31, 0xB1, 0x28, 0x71, 0x89, 0x3E, 0x00, 0x9E, 0x7D, 0x9F, 0x1A, 0x3C, 0xA5, 0x2C, 0x03, 0x1B, 0xAC, 0x9B, 0xEE, 0x74, 0xF2, 0x31, 0x2D, 0x3F, 0xF9, 0xA9, 0x38, 0xCC, 0x38, 0xAD, 0x84, 0xD6, 0xB1, 0x44, 0x09, 0x51, 0x6E, 0x7B, 0xC4, 0x11, 0xA9, 0xAB, 0xB9, 0x80, 0xD4, 0xA8, 0x87, 0xEF, 0x2C, 0x76, 0x51, 0x64, 0x3E, 0xE5, 0x37, 0x7F, 0xF1, 0x26, 0xD7, 0xC0, 0x80, 0xEB, 0x96, 0xA9, 0x1A, 0x4D, 0xB8, 0xAF, 0xC9, 0xE5, 0xA2, 0x43, 0x7A, 0x6C, 0xEB, 0x9F, 0xF3, 0x8A, 0x72, 0xDD, 0xE7, 0xD2, 0xEB, 0x60, 0xD6, 0x01, 0xDD, 0x25, 0x40, 0x8B, 0x71, 0xBA, 0x0D, 0x49, 0x55, 0x3A, 0x1D, 0x9B, 0x60, 0x71, 0x2C, 0x37, 0xAF, 0xFB, 0xD9, 0x3C, 0xEF, 0x77, 0xF1, 0x43, 0xE9, 0x7F, 0xE9, 0x2C, 0xDC, 0x76, 0xDC, 0x34, 0x48, 0xFF, 0x4F, 0xB4, 0xBE, 0x93, 0x82, 0xC3, 0x14, 0x42, 0xC6, 0xE4, 0x01, 0xF5, 0xB0, 0xDE, 0xED, 0x64, 0x82, 0xB9, 0x1C, 0x17, 0x0A, 0x28, 0x89, 0x66, 0xC2, 0x45, 0xA3, 0x4E, 0xBC, 0x61, 0xB4, 0xD7, 0xCC, 0x47, 0xDC, 0x86, 0x8E, 0x1F, 0xEE, 0x8E, 0x0D, 0x0F, 0x51, 0xB1, 0xAC, 0x62, 0xEF, 0xDC, 0x6D, 0x90, 0x81];
+	private static readonly byte[] s_p = [0xFA, 0x63, 0x13, 0x19, 0xF3, 0x13, 0x7A, 0xC0, 0x70, 0x67, 0xAD, 0xE8, 0x05, 0x26, 0x70, 0x68, 0x9F, 0x30, 0xFB, 0x06, 0x9E, 0x45, 0xFF, 0x38, 0xF2, 0x56, 0xA3, 0x35, 0x4F, 0xAE, 0x21, 0x20, 0x94, 0x44, 0x80, 0x42, 0x61, 0x39, 0xC3, 0xEB, 0xA9, 0x8F, 0xD2, 0x61, 0x88, 0xE9, 0x6A, 0x62, 0x42, 0xF0, 0x18, 0x55, 0xDE, 0x50, 0x73, 0x1E, 0xFA, 0x70, 0xE5, 0x25, 0x73, 0x28, 0x6F, 0x29, 0x40, 0xE0, 0x09, 0x1A, 0xB6, 0x11, 0xF4, 0xB1, 0xEC, 0x89, 0x25, 0x66, 0x7D, 0x41, 0xC7, 0x53, 0xFA, 0x3A, 0x58, 0xF0, 0xB2, 0x27, 0x73, 0xE8, 0x63, 0x49, 0x06, 0x15, 0xF1, 0x4C, 0x35, 0x9D, 0x33, 0xE5, 0x28, 0x3E, 0xBF, 0x2F, 0x9F, 0xE4, 0x33, 0xE9, 0xCE, 0x6F, 0x91, 0x18, 0xAD, 0x95, 0xD4, 0xF5, 0x12, 0xD6, 0x92, 0xBD, 0xDA, 0xDF, 0x64, 0xA8, 0xB4, 0xEC, 0x7E, 0xD8, 0x1A, 0x41];
+	private static readonly byte[] s_q = [0xC6, 0xC4, 0xF4, 0x19, 0x1A, 0x74, 0xFB, 0xA1, 0x07, 0x3D, 0x8A, 0x80, 0x9A, 0x6F, 0xC5, 0x60, 0xCB, 0x68, 0x32, 0x14, 0xCA, 0x9D, 0x21, 0x59, 0x7C, 0xA5, 0xAC, 0x0B, 0x68, 0x0E, 0x8C, 0xE2, 0x82, 0x77, 0x6C, 0xBC, 0xC2, 0xA6, 0xB3, 0xF2, 0x8B, 0xE4, 0xC8, 0xDF, 0xF9, 0x7B, 0x92, 0x82, 0xE6, 0x20, 0x84, 0xCB, 0xE0, 0x46, 0x44, 0x0A, 0xB4, 0xD7, 0x48, 0x43, 0x29, 0x78, 0xE7, 0xCA, 0xE4, 0x8F, 0xEC, 0x52, 0x2A, 0x6F, 0xDA, 0x58, 0x8D, 0xAA, 0xEB, 0xC0, 0x5E, 0x4F, 0xC1, 0x70, 0x9A, 0x97, 0xCE, 0xE4, 0xFF, 0x70, 0xEC, 0xA1, 0x2A, 0xED, 0xD8, 0x7A, 0xA1, 0xA5, 0x1C, 0x79, 0x2A, 0x8E, 0xBC, 0x74, 0xA3, 0x45, 0x20, 0x27, 0x14, 0xE1, 0xB9, 0x16, 0x80, 0x26, 0x81, 0xC2, 0x17, 0x26, 0xBB, 0x83, 0x04, 0x88, 0xFB, 0x0B, 0xB2, 0x06, 0x49, 0x88, 0x9A, 0xC0, 0x44, 0x6F];
+	private static readonly byte[] s_dp = [0xA9, 0x1C, 0xD1, 0x81, 0xED, 0x53, 0x72, 0xCD, 0x17, 0x1E, 0x6F, 0xAF, 0x0E, 0x0B, 0x70, 0x50, 0xB9, 0x74, 0x73, 0x97, 0x7C, 0xBE, 0xFA, 0x2A, 0x94, 0x43, 0x3F, 0xE8, 0x79, 0xF0, 0xBA, 0x1F, 0x2F, 0x05, 0x35, 0x3B, 0xA0, 0x3F, 0x4B, 0xBC, 0x97, 0xC1, 0xB3, 0xEE, 0x7C, 0x72, 0x6A, 0x90, 0x03, 0x75, 0xF6, 0x79, 0xC8, 0xB5, 0xBD, 0x8B, 0x66, 0xCB, 0x69, 0x79, 0x69, 0xD6, 0x1D, 0x7E, 0x57, 0x7A, 0xF9, 0xAB, 0x38, 0xDD, 0xD7, 0xCF, 0x74, 0x5B, 0x0D, 0x0F, 0xB3, 0xFA, 0x01, 0x8F, 0x0E, 0xAA, 0xE9, 0xF7, 0x67, 0xDC, 0x8D, 0xC4, 0x9F, 0x8A, 0x6A, 0xB9, 0xD7, 0x48, 0x09, 0xCE, 0x2B, 0x86, 0xD3, 0x2F, 0xCE, 0x21, 0x74, 0x74, 0xC5, 0x3B, 0x5D, 0x5A, 0xAD, 0x3D, 0x65, 0xC0, 0x95, 0x1D, 0x11, 0xA4, 0x2B, 0x68, 0x5F, 0xB4, 0x12, 0x60, 0x80, 0x12, 0x5B, 0x79, 0xCA, 0x01];
+	private static readonly byte[] s_dq = [0x99, 0x7F, 0xF2, 0x0E, 0x0C, 0xDB, 0x78, 0x93, 0x63, 0x97, 0x08, 0x05, 0xC6, 0xBE, 0x38, 0x4C, 0x46, 0xE9, 0x21, 0x7B, 0xE2, 0xF8, 0xB3, 0x8F, 0x7A, 0xEE, 0x2A, 0x4D, 0xE8, 0xBF, 0x0B, 0xD7, 0xC4, 0xEF, 0x5B, 0x3E, 0xEE, 0x87, 0x74, 0x82, 0x03, 0xBC, 0xDB, 0xCF, 0xF3, 0xC7, 0x95, 0x5E, 0x97, 0x3F, 0x57, 0xAE, 0x66, 0x75, 0x7D, 0x08, 0x53, 0x9D, 0xC9, 0x85, 0x6C, 0x5A, 0x9D, 0x8E, 0x97, 0x31, 0xFA, 0x4B, 0x7D, 0xD0, 0x41, 0x6B, 0x8F, 0x84, 0x06, 0x69, 0xD9, 0xAB, 0x77, 0xF4, 0x70, 0xBE, 0x79, 0x9D, 0x69, 0x13, 0x18, 0xFA, 0x61, 0xBF, 0xAA, 0x25, 0x11, 0xFE, 0x03, 0x5E, 0x23, 0x1A, 0x75, 0xB9, 0x11, 0x79, 0x49, 0x9A, 0x44, 0xCB, 0x08, 0x77, 0xB2, 0xE1, 0x89, 0xE5, 0x8C, 0xA1, 0x71, 0xBF, 0x7D, 0x5A, 0x17, 0x9D, 0x71, 0x8E, 0xDF, 0x6C, 0xAA, 0x2D, 0x7E, 0xEB];
+	private static readonly byte[] s_iq = [0x2F, 0x91, 0x84, 0x58, 0xAB, 0x47, 0xAE, 0xE6, 0x92, 0xBC, 0xE4, 0x13, 0xBA, 0x9C, 0xDD, 0x66, 0xFF, 0x91, 0xA8, 0x94, 0x41, 0xBF, 0xD6, 0x54, 0x1B, 0x6D, 0x92, 0xC0, 0x67, 0xC6, 0x08, 0x23, 0xA4, 0x6D, 0x24, 0xA1, 0xC1, 0xBD, 0xCB, 0xC5, 0xAF, 0x4E, 0x96, 0x39, 0x46, 0x4A, 0x44, 0xD7, 0x10, 0x8F, 0x35, 0x76, 0xF5, 0xCB, 0x86, 0xE2, 0x9A, 0xAD, 0x32, 0xCD, 0x2C, 0xC0, 0xB8, 0x4A, 0x31, 0xB0, 0xAC, 0xF8, 0xFA, 0xAB, 0xEB, 0x0E, 0x8A, 0x7D, 0x92, 0x0D, 0xB5, 0x5C, 0x8B, 0x36, 0xD6, 0xC3, 0x56, 0x8F, 0x6B, 0xC0, 0x54, 0x5C, 0x8E, 0x83, 0x27, 0xEC, 0x21, 0x66, 0xC1, 0x5F, 0x44, 0x62, 0x33, 0xD3, 0xC7, 0x09, 0x01, 0xC8, 0xAB, 0x39, 0x6B, 0xC0, 0x1B, 0x35, 0x82, 0x43, 0x49, 0xF3, 0x42, 0x6B, 0x72, 0xA8, 0x1D, 0x31, 0x63, 0x3A, 0xF5, 0x4C, 0x3D, 0xE7, 0x3D, 0xD1];
 }
diff --git a/tests/README.md b/tests/README.md
index 5e3db593d..4bc3082a9 100644
--- a/tests/README.md
+++ b/tests/README.md
@@ -7,8 +7,8 @@ It also verifies that MySqlConnector and MySQL Connector/NET (MySql.Data) have s
 
 The tests require a MySQL server. The simplest way to run one is with [Docker](https://www.docker.com/community-edition):
 
-    docker run -d --rm --name mysqlconnector -e MYSQL_ROOT_PASSWORD=pass -p 3306:3306 --tmpfs /var/lib/mysql mysql:8.0 --max-allowed-packet=96M --character-set-server=utf8mb4 --log-bin-trust-function-creators=1 --local-infile=1 --max-connections=250
-    docker exec mysqlconnector mysql -uroot -ppass -e "INSTALL COMPONENT 'file://component_query_attributes';"
+    docker run -d --rm --pull always --name mysqlconnector -e MYSQL_ROOT_PASSWORD=pass -p 3306:3306 --tmpfs /var/lib/mysql mysql:9.4 --max-allowed-packet=96M --character-set-server=utf8mb4 --disable-log-bin --local-infile=1 --max-connections=250
+    docker exec mysqlconnector mysql -uroot -ppass -e "INSTALL COMPONENT 'file://component_query_attributes'; CREATE USER 'caching-sha2-user'@'%' IDENTIFIED WITH caching_sha2_password BY 'Cach!ng-Sh@2-Pa55'; GRANT ALL PRIVILEGES ON *.* TO 'caching-sha2-user'@'%';"
 
 Copy the file `IntegrationTests/config.json.example` to `IntegrationTests/config.json`, then edit
 the `config.json` file in order to connect to your server. If you are using the Docker
@@ -27,6 +27,7 @@ Otherwise, set the following options appropriately:
   * `ErrorCodes`: server returns error codes in error packet (some MySQL proxies do not)
   * `Json`: the `JSON` data type (MySQL 5.7 and later)
   * `LargePackets`: large packets (over 4MB)
+  * `Redirection`: server supports sending redirection information in a server variable in the first OK packet
   * `RoundDateTime`: server rounds `datetime` values to the specified precision (not implemented in MariaDB)
   * `RsaEncryption`: server supports RSA public key encryption (for `sha256_password` and `caching_sha2_password`)
   * `SessionTrack`: server supports `CLIENT_SESSION_TRACK` capability (MySQL 5.7 and later)
@@ -36,6 +37,7 @@ Otherwise, set the following options appropriately:
   * `Tls11`: server supports TLS 1.1
   * `Tls12`: server supports TLS 1.2
   * `Tls13`: server supports TLS 1.3
+  * `TlsFingerprintValidation`: server provides a hash of the TLS certificate fingerprint in the first OK packet
   * `UnixDomainSocket`: server is accessible via a Unix domain socket
   * `UuidToBin`: server supports `UUID_TO_BIN` (MySQL 8.0 and later)
 
diff --git a/tools/SchemaCollectionGenerator/SchemaCollectionGenerator.cs b/tools/SchemaCollectionGenerator/SchemaCollectionGenerator.cs
index 77475e4ba..549267aa4 100644
--- a/tools/SchemaCollectionGenerator/SchemaCollectionGenerator.cs
+++ b/tools/SchemaCollectionGenerator/SchemaCollectionGenerator.cs
@@ -1,4 +1,6 @@
+using System.Globalization;
 using System.Reflection;
+using System.Text.RegularExpressions;
 using YamlDotNet.Serialization;
 using YamlDotNet.Serialization.NamingConventions;
 
@@ -10,7 +12,7 @@
 	.Build();
 var schemaCollections = deserializer.Deserialize<List<Schema>>(reader);
 
-using var codeWriter = new StreamWriter(@"..\..\..\..\..\src\MySqlConnector\Core\SchemaProvider.g.cs");
+using var codeWriter = new StreamWriter(@"..\..\src\MySqlConnector\Core\SchemaProvider.g.cs");
 codeWriter.WriteLine("""
 	// DO NOT EDIT - generated by SchemaCollectionGenerator.cs
 	#nullable enable
@@ -23,17 +25,22 @@ internal sealed partial class SchemaProvider
 	{
 		public async ValueTask<DataTable> GetSchemaAsync(IOBehavior ioBehavior, string collectionName, string?[]? restrictionValues, CancellationToken cancellationToken)
 		{
+	#if NET6_0_OR_GREATER
+			ArgumentNullException.ThrowIfNull(collectionName);
+	#else
 			if (collectionName is null)
 				throw new ArgumentNullException(nameof(collectionName));
+	#endif
 
 			var dataTable = new DataTable();
 	""");
 var elseIf = "if";
 foreach (var schema in schemaCollections)
 {
+	string methodSchemaName = Regexes.NonAsciiAlphaNumeric().Replace(schema.Name, "");
 	codeWriter.WriteLine($"""
 				{elseIf} (string.Equals(collectionName, "{schema.Name}", StringComparison.OrdinalIgnoreCase))
-					await Fill{schema.Name}Async(ioBehavior, dataTable, "{schema.Name}", restrictionValues, cancellationToken).ConfigureAwait(false);
+					await Fill{methodSchemaName}Async(ioBehavior, dataTable, "{schema.Name}", restrictionValues, cancellationToken).ConfigureAwait(false);
 		""");
 	elseIf = "else if";
 }
@@ -48,16 +55,17 @@ public async ValueTask<DataTable> GetSchemaAsync(IOBehavior ioBehavior, string c
 
 foreach (var schema in schemaCollections)
 {
-	var isAsync = schema.Table is not null;
+	var isAsync = schema.Table is not null || schema.Custom?.EndsWith("Async") is true;
+	string methodSchemaName = Regexes.NonAsciiAlphaNumeric().Replace(schema.Name, "");
 	var supportsRestrictions = schema.Restrictions is { Count: > 0 };
 	codeWriter.WriteLine($$"""
-			private {{(isAsync ? "async " : "")}}Task Fill{{schema.Name}}Async(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
+			private {{(isAsync ? "async " : "")}}Task Fill{{methodSchemaName}}Async(IOBehavior ioBehavior, DataTable dataTable, string tableName, string?[]? restrictionValues, CancellationToken cancellationToken)
 			{
 		""");
 	if (!supportsRestrictions)
 	{
 		codeWriter.WriteLine($"""
-					if (restrictionValues is not null)
+					if (restrictionValues is not null && restrictionValues.Length > 0)
 						throw new ArgumentException("restrictionValues is not supported for schema '{schema.Name}'.", nameof(restrictionValues));
 			""");
 	}
@@ -72,8 +80,8 @@ public async ValueTask<DataTable> GetSchemaAsync(IOBehavior ioBehavior, string c
 	codeWriter.WriteLine("""
 
 				dataTable.TableName = tableName;
-				dataTable.Columns.AddRange(new DataColumn[]
-				{
+				dataTable.Columns.AddRange(
+				[
 		""");
 	foreach (var column in schema.Columns)
 	{
@@ -82,7 +90,7 @@ public async ValueTask<DataTable> GetSchemaAsync(IOBehavior ioBehavior, string c
 			""");
 	}
 	codeWriter.WriteLine("""
-				});
+				]);
 
 		""");
 	if (schema.Table is string table)
@@ -140,9 +148,13 @@ public async ValueTask<DataTable> GetSchemaAsync(IOBehavior ioBehavior, string c
 	}
 	else
 	{
-		codeWriter.WriteLine($"""
-					{schema.Custom}(dataTable);
+		// custom function will be called here
+		string restrictions = (schema.Restrictions?.Count > 0) ? ", restrictionValues" : "";
+		codeWriter.Write($"""
+					{(isAsync ? "await " : "")}{schema.Custom}({(isAsync ? "ioBehavior, " : "")}dataTable{restrictions}{(isAsync ? ", cancellationToken" : "")}){(isAsync ? ".ConfigureAwait(false)" : "")};
 			""");
+		if (!isAsync)
+			codeWriter.WriteLine();
 	}
 
 	if (!isAsync)
@@ -164,11 +176,11 @@ public async ValueTask<DataTable> GetSchemaAsync(IOBehavior ioBehavior, string c
 	}
 	""");
 
-using var docWriter = new StreamWriter(@"..\..\..\..\..\docs\content\overview\schema-collections.md");
+using var docWriter = new StreamWriter(@"..\..\docs\content\overview\schema-collections.md");
 docWriter.Write($"""
 	---
 	date: 2021-04-24
-	lastmod: {DateTime.UtcNow.ToString("yyyy-MM-dd")}
+	lastmod: {DateTime.UtcNow.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}
 	menu:
 	  main:
 	    parent: getting started
@@ -185,17 +197,19 @@ public async ValueTask<DataTable> GetSchemaAsync(IOBehavior ioBehavior, string c
 
 
 	""");
+#pragma warning disable CA1308 // Normalize strings to uppercase
 foreach (var schema in schemaCollections)
 	docWriter.Write($@"* `{schema.Name}`{(schema.Description is not null ? "—[" + schema.Description + "](../schema/" + schema.Name.ToLowerInvariant() + "/)" : "")}
 ");
 
 foreach (var schema in schemaCollections.Where(x => x.Description is not null))
 {
-	using var schemaDocWriter = new StreamWriter($@"..\..\..\..\..\docs\content\overview\schema\{schema.Name.ToLowerInvariant()}.md");
+	using var schemaDocWriter = new StreamWriter($@"..\..\docs\content\overview\schema\{schema.Name.ToLowerInvariant()}.md");
+#pragma warning restore CA1308 // Normalize strings to uppercase
 	schemaDocWriter.Write($"""
 		---
 		date: 2022-07-10
-		lastmod: {DateTime.UtcNow.ToString("yyyy-MM-dd")}
+		lastmod: {DateTime.UtcNow.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)}
 		title: {schema.Name} Schema
 		---
 
@@ -226,7 +240,8 @@ Restriction Name | Restriction Default | Restriction Number
 	}
 }
 
-class Schema
+#pragma warning disable CA1812 // Avoid uninstantiated internal classes
+internal sealed class Schema
 {
 	public required string Name { get; init; }
 	public string? Description { get; init; }
@@ -237,7 +252,7 @@ class Schema
 	public List<Restriction>? Restrictions { get; init; }
 }
 
-class Column
+internal sealed class Column
 {
 	public required string Name { get; init; }
 	public required string Type { get; init; }
@@ -245,8 +260,14 @@ class Column
 	public bool Optional { get; init; }
 }
 
-class Restriction
+internal sealed class Restriction
 {
 	public required string Name { get; init; }
 	public required string Default { get; init; }
 }
+
+internal sealed partial class Regexes
+{
+	[GeneratedRegex("[^a-z0-9]", RegexOptions.IgnoreCase | RegexOptions.Compiled, "en-US")]
+	public static partial Regex NonAsciiAlphaNumeric();
+}
diff --git a/tools/SchemaCollectionGenerator/SchemaCollectionGenerator.csproj b/tools/SchemaCollectionGenerator/SchemaCollectionGenerator.csproj
index 5334b4ac3..85c1ed799 100644
--- a/tools/SchemaCollectionGenerator/SchemaCollectionGenerator.csproj
+++ b/tools/SchemaCollectionGenerator/SchemaCollectionGenerator.csproj
@@ -2,9 +2,10 @@
 
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFramework>net7.0</TargetFramework>
+    <TargetFramework>net9.0</TargetFramework>
     <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>enable</Nullable>
+    <GenerateDocumentationFile>true</GenerateDocumentationFile>
   </PropertyGroup>
 
   <ItemGroup>
@@ -12,7 +13,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="YamlDotNet" Version="12.3.1" />
+    <PackageReference Include="YamlDotNet" />
   </ItemGroup>
 
 </Project>
diff --git a/tools/SchemaCollectionGenerator/SchemaCollections.yaml b/tools/SchemaCollectionGenerator/SchemaCollections.yaml
index af385739e..47c464b7c 100644
--- a/tools/SchemaCollectionGenerator/SchemaCollections.yaml
+++ b/tools/SchemaCollectionGenerator/SchemaCollections.yaml
@@ -652,28 +652,6 @@
   - name: IS_GRANTABLE
     type: string
 
-- name: TableSpaces
-  table: TABLESPACES
-  columns:
-  - name: TABLESPACE_NAME
-    type: string
-  - name: ENGINE
-    type: string
-  - name: TABLESPACE_TYPE
-    type: string
-  - name: LOGFILE_GROUP_NAME
-    type: string
-  - name: EXTENT_SIZE
-    type: long
-  - name: AUTOEXTEND_SIZE
-    type: long
-  - name: MAXIMUM_SIZE
-    type: long
-  - name: NODEGROUP_ID
-    type: long
-  - name: TABLESPACE_COMMENT
-    type: string
-
 - name: Triggers
   table: TRIGGERS
   identifierPartCount: 3
@@ -759,3 +737,101 @@
     type: string
   - name: COLLATION_CONNECTION
     type: string
+
+- name: Foreign Keys
+  description: information about foreign keys in the server's SQL syntax
+  custom: DoFillForeignKeysAsync
+  restrictions:
+  - name: Catalog
+    default: TABLE_CATALOG
+  - name: Schema
+    default: TABLE_SCHEMA
+  - name: Table
+    default: TABLE_NAME
+  - name: Constraint Name
+    default: CONSTRAINT_NAME
+  columns:
+  - name: CONSTRAINT_CATALOG
+    type: string
+  - name: CONSTRAINT_SCHEMA
+    type: string
+  - name: CONSTRAINT_NAME
+    type: string
+  - name: TABLE_CATALOG
+    type: string
+  - name: TABLE_SCHEMA
+    type: string
+  - name: TABLE_NAME
+    type: string
+  - name: MATCH_OPTION
+    type: string
+  - name: UPDATE_RULE
+    type: string
+  - name: DELETE_RULE
+    type: string
+  - name: REFERENCED_TABLE_CATALOG
+    type: string
+  - name: REFERENCED_TABLE_SCHEMA
+    type: string
+  - name: REFERENCED_TABLE_NAME
+    type: string
+
+- name: Indexes
+  description: information about indexes in the server's SQL syntax
+  custom: DoFillIndexesAsync
+  restrictions:
+  - name: Catalog
+    default: TABLE_CATALOG
+  - name: Schema
+    default: TABLE_SCHEMA
+  - name: Table
+    default: TABLE_NAME
+  - name: Name
+    default: INDEX_NAME
+  columns:
+  - name: INDEX_CATALOG
+    type: string
+  - name: INDEX_SCHEMA # same as TABLE_SCHEMA, see https://mariadb.com/kb/en/information-schema-statistics-table/
+    type: string
+  - name: INDEX_NAME
+    type: string
+  - name: TABLE_NAME
+    type: string
+  - name: UNIQUE
+    type: bool
+  - name: PRIMARY
+    type: bool
+  - name: TYPE
+    type: string
+  - name: COMMENT
+    type: string
+
+- name: IndexColumns
+  description: information about indexes in the server's SQL syntax
+  custom: DoFillIndexColumnsAsync
+  restrictions:
+  - name: Catalog
+    default: TABLE_CATALOG
+  - name: Schema
+    default: TABLE_SCHEMA
+  - name: Table
+    default: TABLE_NAME
+  - name: Name
+    default: INDEX_NAME
+  - name: Column
+    default: COLUMN_NAME
+  columns:
+  - name: INDEX_CATALOG
+    type: string
+  - name: INDEX_SCHEMA # same as TABLE_SCHEMA, see https://mariadb.com/kb/en/information-schema-statistics-table/
+    type: string
+  - name: INDEX_NAME
+    type: string
+  - name: TABLE_NAME
+    type: string
+  - name: COLUMN_NAME
+    type: string
+  - name: ORDINAL_POSITION
+    type: int
+  - name: SORT_ORDER
+    type: string
