From 29a6c83efe99be0700d88ba570932746c18af837 Mon Sep 17 00:00:00 2001
From: Marc Gravell <marc.gravell@gmail.com>
Date: Thu, 12 Dec 2024 11:55:15 +0000
Subject: [PATCH] random experiment

---
 Dapper.StrongName/Dapper.StrongName.csproj    |    2 +-
 Dapper/CommandDefinition.cs                   |   10 +
 Dapper/Dapper.csproj                          |    3 +-
 Dapper/PublicAPI.Unshipped.txt                |    2 +-
 .../PublicAPI/net5.0/PublicAPI.Unshipped.txt  |    1 -
 .../PublicAPI/net7.0/PublicAPI.Unshipped.txt  |    1 -
 .../{net5.0 => net8.0}/PublicAPI.Shipped.txt  |    0
 .../PublicAPI/net8.0/PublicAPI.Unshipped.txt  |   15 +
 .../{net7.0 => net9.0}/PublicAPI.Shipped.txt  |    0
 .../PublicAPI/net9.0/PublicAPI.Unshipped.txt  |   15 +
 Dapper/SqlMapper.Async.cs                     |    4 +-
 Dapper/SqlMapper.SqlBuilder.cs                |  303 +++++
 Dapper/SqlMapper.cs                           |   81 +-
 tests/Dapper.Tests/SqlBuilderLiteralTests.cs  | 1173 +++++++++++++++++
 14 files changed, 1561 insertions(+), 49 deletions(-)
 delete mode 100644 Dapper/PublicAPI/net5.0/PublicAPI.Unshipped.txt
 delete mode 100644 Dapper/PublicAPI/net7.0/PublicAPI.Unshipped.txt
 rename Dapper/PublicAPI/{net5.0 => net8.0}/PublicAPI.Shipped.txt (100%)
 create mode 100644 Dapper/PublicAPI/net8.0/PublicAPI.Unshipped.txt
 rename Dapper/PublicAPI/{net7.0 => net9.0}/PublicAPI.Shipped.txt (100%)
 create mode 100644 Dapper/PublicAPI/net9.0/PublicAPI.Unshipped.txt
 create mode 100644 Dapper/SqlMapper.SqlBuilder.cs
 create mode 100644 tests/Dapper.Tests/SqlBuilderLiteralTests.cs

diff --git a/Dapper.StrongName/Dapper.StrongName.csproj b/Dapper.StrongName/Dapper.StrongName.csproj
index 770b7ce7a..3d4ec4db0 100644
--- a/Dapper.StrongName/Dapper.StrongName.csproj
+++ b/Dapper.StrongName/Dapper.StrongName.csproj
@@ -5,7 +5,7 @@
     <Title>Dapper (Strong Named)</Title>
     <Description>A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc. Major Sponsor: Dapper Plus from ZZZ Projects.</Description>
     <Authors>Sam Saffron;Marc Gravell;Nick Craver</Authors>
-    <TargetFrameworks>net461;netstandard2.0;net5.0;net7.0</TargetFrameworks>
+    <TargetFrameworks>net461;netstandard2.0;net6.0;net8.0</TargetFrameworks>
     <SignAssembly>true</SignAssembly>
     <PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
     <Nullable>enable</Nullable>
diff --git a/Dapper/CommandDefinition.cs b/Dapper/CommandDefinition.cs
index 19963ba67..113d5239c 100644
--- a/Dapper/CommandDefinition.cs
+++ b/Dapper/CommandDefinition.cs
@@ -1,5 +1,6 @@
 ﻿using System;
 using System.Data;
+using System.Data.Common;
 using System.Reflection;
 using System.Reflection.Emit;
 using System.Threading;
@@ -113,6 +114,15 @@ private CommandDefinition(object? parameters, CommandFlags flags) : this()
             CommandText = "";
         }
 
+        internal CommandDefinition(IDbCommand cmd, CommandFlags flags = CommandFlags.None)
+        {
+            CommandText = cmd.CommandText;
+            CommandTypeDirect = cmd.CommandType;
+            CommandTimeout = cmd.CommandTimeout;
+            Transaction = cmd.Transaction;
+            Flags = flags;
+        }
+
         /// <summary>
         /// For asynchronous operations, the cancellation-token
         /// </summary>
diff --git a/Dapper/Dapper.csproj b/Dapper/Dapper.csproj
index 28951f871..3a8ab454b 100644
--- a/Dapper/Dapper.csproj
+++ b/Dapper/Dapper.csproj
@@ -5,8 +5,9 @@
     <PackageTags>orm;sql;micro-orm</PackageTags>
     <Description>A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc. Major Sponsor: Dapper Plus from ZZZ Projects.</Description>
     <Authors>Sam Saffron;Marc Gravell;Nick Craver</Authors>
-    <TargetFrameworks>net461;netstandard2.0;net5.0;net6.0;net7.0</TargetFrameworks>
+    <TargetFrameworks>net461;netstandard2.0;net6.0;net8.0;net9.0</TargetFrameworks>
     <Nullable>enable</Nullable>
+    <LangVersion>13</LangVersion>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
   <ItemGroup>
diff --git a/Dapper/PublicAPI.Unshipped.txt b/Dapper/PublicAPI.Unshipped.txt
index 91b0e1a43..ab058de62 100644
--- a/Dapper/PublicAPI.Unshipped.txt
+++ b/Dapper/PublicAPI.Unshipped.txt
@@ -1 +1 @@
-﻿#nullable enable
\ No newline at end of file
+﻿#nullable enable
diff --git a/Dapper/PublicAPI/net5.0/PublicAPI.Unshipped.txt b/Dapper/PublicAPI/net5.0/PublicAPI.Unshipped.txt
deleted file mode 100644
index 91b0e1a43..000000000
--- a/Dapper/PublicAPI/net5.0/PublicAPI.Unshipped.txt
+++ /dev/null
@@ -1 +0,0 @@
-﻿#nullable enable
\ No newline at end of file
diff --git a/Dapper/PublicAPI/net7.0/PublicAPI.Unshipped.txt b/Dapper/PublicAPI/net7.0/PublicAPI.Unshipped.txt
deleted file mode 100644
index 91b0e1a43..000000000
--- a/Dapper/PublicAPI/net7.0/PublicAPI.Unshipped.txt
+++ /dev/null
@@ -1 +0,0 @@
-﻿#nullable enable
\ No newline at end of file
diff --git a/Dapper/PublicAPI/net5.0/PublicAPI.Shipped.txt b/Dapper/PublicAPI/net8.0/PublicAPI.Shipped.txt
similarity index 100%
rename from Dapper/PublicAPI/net5.0/PublicAPI.Shipped.txt
rename to Dapper/PublicAPI/net8.0/PublicAPI.Shipped.txt
diff --git a/Dapper/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/Dapper/PublicAPI/net8.0/PublicAPI.Unshipped.txt
new file mode 100644
index 000000000..5a5acf07e
--- /dev/null
+++ b/Dapper/PublicAPI/net8.0/PublicAPI.Unshipped.txt
@@ -0,0 +1,15 @@
+﻿#nullable enable
+Dapper.SqlMapper.SqlBuilder
+Dapper.SqlMapper.SqlBuilder.AppendFormatted(bool value, string! format = "", string! expression = "") -> void
+Dapper.SqlMapper.SqlBuilder.AppendFormatted<T>(T value, string! format = "", string! expression = "") -> void
+Dapper.SqlMapper.SqlBuilder.AppendLiteral(string! value) -> void
+Dapper.SqlMapper.SqlBuilder.Dispose() -> void
+Dapper.SqlMapper.SqlBuilder.GetCommandAndReset(System.Data.IDbTransaction? transaction, int? commandTimeout) -> System.Data.IDbCommand!
+Dapper.SqlMapper.SqlBuilder.SqlBuilder() -> void
+Dapper.SqlMapper.SqlBuilder.SqlBuilder(int literalLength, int formattedCount, System.Data.IDbConnection! connection) -> void
+static Dapper.SqlMapper.Execute(this System.Data.IDbConnection! connection, ref Dapper.SqlMapper.SqlBuilder sql, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null) -> int
+static Dapper.SqlMapper.ExecuteScalar(this System.Data.IDbConnection! connection, ref Dapper.SqlMapper.SqlBuilder sql, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null) -> object?
+static Dapper.SqlMapper.ExecuteScalar<T>(this System.Data.IDbConnection! connection, ref Dapper.SqlMapper.SqlBuilder sql, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null) -> T?
+static Dapper.SqlMapper.Query<T>(this System.Data.IDbConnection! connection, ref Dapper.SqlMapper.SqlBuilder sql, System.Data.IDbTransaction? transaction = null, bool buffered = false, int? commandTimeout = null) -> System.Collections.Generic.IEnumerable<T>!
+static Dapper.SqlMapper.QueryBuffered<T>(this System.Data.IDbConnection! connection, ref Dapper.SqlMapper.SqlBuilder sql, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null) -> System.Collections.Generic.List<T>!
+static Dapper.SqlMapper.QueryUnbuffered<T>(this System.Data.IDbConnection! connection, ref Dapper.SqlMapper.SqlBuilder sql, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null) -> System.Collections.Generic.IEnumerable<T>!
\ No newline at end of file
diff --git a/Dapper/PublicAPI/net7.0/PublicAPI.Shipped.txt b/Dapper/PublicAPI/net9.0/PublicAPI.Shipped.txt
similarity index 100%
rename from Dapper/PublicAPI/net7.0/PublicAPI.Shipped.txt
rename to Dapper/PublicAPI/net9.0/PublicAPI.Shipped.txt
diff --git a/Dapper/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/Dapper/PublicAPI/net9.0/PublicAPI.Unshipped.txt
new file mode 100644
index 000000000..256f267a6
--- /dev/null
+++ b/Dapper/PublicAPI/net9.0/PublicAPI.Unshipped.txt
@@ -0,0 +1,15 @@
+﻿#nullable enable
+Dapper.SqlMapper.SqlBuilder
+Dapper.SqlMapper.SqlBuilder.AppendFormatted(bool value, string! format = "", string! expression = "") -> void
+Dapper.SqlMapper.SqlBuilder.AppendFormatted<T>(T value, string! format = "", string! expression = "") -> void
+Dapper.SqlMapper.SqlBuilder.AppendLiteral(string! value) -> void
+Dapper.SqlMapper.SqlBuilder.Dispose() -> void
+Dapper.SqlMapper.SqlBuilder.GetCommandAndReset(System.Data.IDbTransaction? transaction, int? commandTimeout) -> System.Data.IDbCommand!
+Dapper.SqlMapper.SqlBuilder.SqlBuilder() -> void
+Dapper.SqlMapper.SqlBuilder.SqlBuilder(int literalLength, int formattedCount, System.Data.IDbConnection! connection) -> void
+static Dapper.SqlMapper.Execute(this System.Data.IDbConnection! connection, ref Dapper.SqlMapper.SqlBuilder sql, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null) -> int
+static Dapper.SqlMapper.ExecuteScalar(this System.Data.IDbConnection! connection, ref Dapper.SqlMapper.SqlBuilder sql, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null) -> object?
+static Dapper.SqlMapper.ExecuteScalar<T>(this System.Data.IDbConnection! connection, ref Dapper.SqlMapper.SqlBuilder sql, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null) -> T?
+static Dapper.SqlMapper.Query<T>(this System.Data.IDbConnection! connection, ref Dapper.SqlMapper.SqlBuilder sql, System.Data.IDbTransaction? transaction = null, bool buffered = false, int? commandTimeout = null) -> System.Collections.Generic.IEnumerable<T>!
+static Dapper.SqlMapper.QueryBuffered<T>(this System.Data.IDbConnection! connection, ref Dapper.SqlMapper.SqlBuilder sql, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null) -> System.Collections.Generic.List<T>!
+static Dapper.SqlMapper.QueryUnbuffered<T>(this System.Data.IDbConnection! connection, ref Dapper.SqlMapper.SqlBuilder sql, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null) -> System.Collections.Generic.IEnumerable<T>!
diff --git a/Dapper/SqlMapper.Async.cs b/Dapper/SqlMapper.Async.cs
index 9408d5735..e166aed90 100644
--- a/Dapper/SqlMapper.Async.cs
+++ b/Dapper/SqlMapper.Async.cs
@@ -493,7 +493,7 @@ private static async Task<T> QueryRowAsync<T>(this IDbConnection cnn, Row row, T
                 T result = default!;
                 if (await reader.ReadAsync(cancel).ConfigureAwait(false) && reader.FieldCount != 0)
                 {
-                    result = ReadRow<T>(info, identity, ref command, effectiveType, reader);
+                    result = ReadRow<T>(info, identity, in command, effectiveType, reader);
 
                     if ((row & Row.Single) != 0 && await reader.ReadAsync(cancel).ConfigureAwait(false)) ThrowMultipleRows(row);
                     while (await reader.ReadAsync(cancel).ConfigureAwait(false)) { /* ignore rows after the first */ }
@@ -1155,7 +1155,7 @@ public static Task<DbDataReader> ExecuteReaderAsync(this DbConnection cnn, Comma
 
         private static async Task<DbDataReader> ExecuteWrappedReaderImplAsync(IDbConnection cnn, CommandDefinition command, CommandBehavior commandBehavior)
         {
-            Action<IDbCommand, object?>? paramReader = GetParameterReader(cnn, ref command);
+            Action<IDbCommand, object?>? paramReader = GetParameterReader(cnn, in command);
 
             DbCommand? cmd = null;
             bool wasClosed = cnn.State == ConnectionState.Closed, disposeCommand = true;
diff --git a/Dapper/SqlMapper.SqlBuilder.cs b/Dapper/SqlMapper.SqlBuilder.cs
new file mode 100644
index 000000000..c39b1085e
--- /dev/null
+++ b/Dapper/SqlMapper.SqlBuilder.cs
@@ -0,0 +1,303 @@
+﻿#if NET8_0_OR_GREATER
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Data;
+using System.Data.Common;
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Text.RegularExpressions;
+
+namespace Dapper;
+
+public partial class SqlMapper
+{
+    /// <inheritdoc cref="SqlMapper.Execute(IDbConnection, string, object?, IDbTransaction?, int?, CommandType?)" />
+    [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous")]
+    public static int Execute(
+        this IDbConnection connection,
+        [InterpolatedStringHandlerArgument("connection")] ref SqlBuilder sql,
+        IDbTransaction? transaction = null,
+        int? commandTimeout = null)
+    {
+        var cmd = sql.GetCommandAndReset(transaction, commandTimeout);
+        return ExecuteImpl(connection, new(cmd), cmd);
+    }
+
+#pragma warning disable CS1591, RS0016 // Missing XML comment for publicly visible type or member
+    public static int ExecuteNQ(
+#pragma warning restore CS1591, RS0016 // Missing XML comment for publicly visible type or member
+    this IDbConnection connection,
+    [InterpolatedStringHandlerArgument("connection")] ref SqlBuilder sql,
+    IDbTransaction? transaction = null,
+    int? commandTimeout = null) => Execute(connection, ref sql, transaction, commandTimeout);
+
+    /// <inheritdoc cref="SqlMapper.Query{T}(IDbConnection, string, object?, IDbTransaction?, bool, int?, CommandType?)" />
+    [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous")]
+    public static IEnumerable<T> QueryUnbuffered<T>(
+        this IDbConnection connection,
+        [InterpolatedStringHandlerArgument("connection")] ref SqlBuilder sql,
+        IDbTransaction? transaction = null,
+        int? commandTimeout = null)
+    {
+        var cmd = sql.GetCommandAndReset(transaction, commandTimeout);
+        return QueryImpl<T>(connection, new(cmd), typeof(T), cmd);
+    }
+
+    /// <inheritdoc cref="SqlMapper.Query{T}(IDbConnection, string, object?, IDbTransaction?, bool, int?, CommandType?)" />
+    [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous")]
+    public static IEnumerable<T> Query<T>(
+        this IDbConnection connection,
+        [InterpolatedStringHandlerArgument("connection")] ref SqlBuilder sql,
+        IDbTransaction? transaction = null,
+        bool buffered = false,
+        int? commandTimeout = null)
+    {
+        var cmd = sql.GetCommandAndReset(transaction, commandTimeout);
+        var result = QueryImpl<T>(connection, new(cmd, buffered ? CommandFlags.Buffered : CommandFlags.None), typeof(T), cmd);
+        return buffered ? result.ToList() : result;
+    }
+
+    /// <inheritdoc cref="SqlMapper.Query{T}(IDbConnection, string, object?, IDbTransaction?, bool, int?, CommandType?)" />
+    [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous")]
+    public static List<T> QueryBuffered<T>(
+        this IDbConnection connection,
+        [InterpolatedStringHandlerArgument("connection")] ref SqlBuilder sql,
+        IDbTransaction? transaction = null,
+        int? commandTimeout = null)
+    {
+        var cmd = sql.GetCommandAndReset(transaction, commandTimeout);
+        return QueryImpl<T>(connection, new(cmd, CommandFlags.Buffered), typeof(T), cmd).ToList();
+    }
+
+    /// <inheritdoc cref="ExecuteScalar(IDbConnection, string, object?, IDbTransaction?, int?, CommandType?)"/>
+    [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous")]
+    public static object? ExecuteScalar(
+        this IDbConnection connection,
+        [InterpolatedStringHandlerArgument("connection")] ref SqlBuilder sql,
+        IDbTransaction? transaction = null,
+        int? commandTimeout = null)
+    {
+        var cmd = sql.GetCommandAndReset(transaction, commandTimeout);
+        return ExecuteScalarImpl<object>(connection, new(cmd), cmd);
+    }
+
+    /// <inheritdoc cref="ExecuteScalar{T}(IDbConnection, string, object?, IDbTransaction?, int?, CommandType?)"/>
+    [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous")]
+    public static T? ExecuteScalar<T>(
+        this IDbConnection connection,
+        [InterpolatedStringHandlerArgument("connection")] ref SqlBuilder sql,
+        IDbTransaction? transaction = null,
+        int? commandTimeout = null)
+    {
+        var cmd = sql.GetCommandAndReset(transaction, commandTimeout);
+        return ExecuteScalarImpl<T?>(connection, new(cmd), cmd);
+    }
+
+    /// <summary>
+    /// Allows efficient construction of SQL, intended for use with interpolated string literals.
+    /// </summary>
+    [InterpolatedStringHandler]
+    [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0064:Make readonly fields writable", Justification = "Only Dispose impacted")]
+    public ref struct SqlBuilder
+    {
+        DefaultInterpolatedStringHandler _handler;
+
+        private readonly IDbCommand _command;
+        private int _argIndex;
+#if NET9_0_OR_GREATER
+        private bool _formatted;
+#endif
+
+        /// <summary>
+        /// Create a new builder instance.
+        /// </summary>
+        public SqlBuilder(int literalLength, int formattedCount, IDbConnection connection)
+        {
+            _handler = new(literalLength, formattedCount, CultureInfo.InvariantCulture);
+            _command = connection.CreateCommand();
+            _command.CommandType = CommandType.Text;
+            if (_command.Connection is null) _command.Connection = connection;
+        }
+
+        /// <summary>
+        /// Returns the <see cref="DbCommand"/> for this operation with the <see cref="DbCommand.CommandText"/> set to the final SQL.
+        /// </summary>
+        public IDbCommand GetCommandAndReset(IDbTransaction? transaction, int? commandTimeout)
+        {
+            _command.Transaction = transaction;
+            var sql = GetText(ref _handler);
+            if (!sql.SequenceEqual(_command.CommandText))
+            {
+#if NET9_0_OR_GREATER
+                if (_formatted)
+                {
+                    // unique per-call
+                    _command.CommandText = sql.ToString();
+                }
+                else
+                {
+                    // repeated
+                    var cache = _sqlCache.GetAlternateLookup<ReadOnlySpan<char>>();
+                    if (!cache.TryGetValue(sql, out var sqlString))
+                    {
+                        sqlString = sql.ToString();
+                        cache[sqlString] = sqlString;
+                    }
+                    _command.CommandText = sqlString;
+                }
+#else
+                _command.CommandText = sql.ToString();
+#endif
+            }
+            if (commandTimeout.HasValue)
+            {
+                _command.CommandTimeout = commandTimeout.GetValueOrDefault();
+            }
+            else if (Settings.CommandTimeout is int globalTimeout)
+            {
+                _command.CommandTimeout = globalTimeout;
+            }
+            Debug.WriteLine($"built command with {_command.Parameters.Count} parameters: {_command.CommandText}");
+
+            var cmd = _command;
+            Dispose();
+            return cmd;
+        }
+
+        /// <inheritdoc cref="DefaultInterpolatedStringHandler.AppendLiteral(string)"/>
+        public void AppendLiteral(string value) => _handler.AppendLiteral(value);
+
+        // avoid allocating composed names repeatedly
+        private static readonly ConcurrentDictionary<(char token, string expression), string> _expressionCache = [];
+        private static readonly ConcurrentDictionary<(char token, int index), string> _indexCache = [];
+
+#if NET9_0_OR_GREATER
+        private static readonly ConcurrentDictionary<string, string> _sqlCache = [];
+#endif
+
+        string ProposeAndAppendName(char token, string expression)
+        {
+            // for simple names, use the expression to name the parameter, so @{name} becomes @name
+            // otherwise, invent, so @{id + 2} becomes @p0
+            if (Regex.IsMatch(expression, "^[a-zA-Z_][a-zA-Z0-9_]*$"))
+            {
+                _handler.AppendLiteral(expression);
+                var key = (token, expression);
+                if (!_expressionCache.TryGetValue(key, out var composed))
+                {
+                    _expressionCache[key] = composed = $"{key.token}{key.expression}";
+                }
+                return composed;
+            }
+            else
+            {
+                _handler.AppendLiteral("p");
+                _handler.AppendFormatted(_argIndex);
+                var key = (token, index: _argIndex++);
+                if (!_indexCache.TryGetValue(key, out var composed))
+                {
+                    _indexCache[key] = composed = $"{key.token}p{key.index}";
+                }
+                return composed;
+            }
+        }
+
+        [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_Text")]
+        private static extern ReadOnlySpan<char> GetText(ref DefaultInterpolatedStringHandler handler);
+
+        [UnsafeAccessor(UnsafeAccessorKind.Method)]
+        private static extern void Clear(ref DefaultInterpolatedStringHandler handler);
+
+        private bool IsParameter(out char prefix)
+        {
+            var sql = GetText(ref _handler);
+            if (!sql.IsEmpty)
+            {
+                prefix = sql[sql.Length - 1];
+                return prefix is '@' or ':' or '$';
+            }
+            prefix = default;
+            return false;
+        }
+
+        /// <inheritdoc cref="DefaultInterpolatedStringHandler.AppendFormatted{T}(T, string?)"/>
+        [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous")]
+        public void AppendFormatted(bool value, string format = "", [CallerArgumentExpression(nameof(value))] string expression = "")
+        {
+            if (string.IsNullOrWhiteSpace(format))
+            {
+                if (IsParameter(out var prefix))
+                {
+                    AppendParameter(value, expression, prefix);
+                }
+                else
+                {
+                    // treat as literal 0/1
+                    _handler.AppendLiteral(value ? "1" : "0");
+                }
+            }
+            else
+            {
+                _handler.AppendFormatted(value, format);
+#if NET9_0_OR_GREATER
+                _formatted = true;
+#endif
+            }
+        }
+
+        /// <inheritdoc cref="DefaultInterpolatedStringHandler.AppendFormatted{T}(T, string?)"/>
+        [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous")]
+        public void AppendFormatted<T>(T value, string format = "", [CallerArgumentExpression(nameof(value))] string expression = "")
+        {
+            if (string.IsNullOrWhiteSpace(format) && IsParameter(out var prefix))
+            {
+                AppendParameter(value, expression, prefix);
+            }
+            else
+            {
+                _handler.AppendFormatted(value, format);
+#if NET9_0_OR_GREATER
+                _formatted = true;
+#endif
+            }
+        }
+        private void AppendParameter<T>(T value, string expression, char prefix)
+        {
+            var fullName = ProposeAndAppendName(prefix, expression);
+            if (!HasParam(fullName))
+            {
+                var param = _command.CreateParameter();
+                param.ParameterName = fullName;
+                param.Value = (object?)value ?? DBNull.Value;
+                param.Direction = ParameterDirection.Input;
+                _command.Parameters.Add(param);
+            }
+        }
+
+        private bool HasParam(string name)
+        {
+            foreach (DbParameter param in _command.Parameters)
+            {
+                if (string.Equals(param.ParameterName, name, StringComparison.InvariantCultureIgnoreCase))
+                {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        /// <inheritdoc cref="IDisposable.Dispose"/>
+        public void Dispose()
+        {
+            var cmd = _command;
+            Clear(ref _handler);
+            cmd?.Dispose();
+            this = default;
+        }
+    }
+}
+#endif
diff --git a/Dapper/SqlMapper.cs b/Dapper/SqlMapper.cs
index d23e949a5..1c3cb0e3b 100644
--- a/Dapper/SqlMapper.cs
+++ b/Dapper/SqlMapper.cs
@@ -553,7 +553,7 @@ public static void SetDbType(IDataParameter parameter, object value)
         public static int Execute(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null)
         {
             var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered);
-            return ExecuteImpl(cnn, ref command);
+            return ExecuteImpl(cnn, in command);
         }
 
         /// <summary>
@@ -562,7 +562,7 @@ public static int Execute(this IDbConnection cnn, string sql, object? param = nu
         /// <param name="cnn">The connection to execute on.</param>
         /// <param name="command">The command to execute on this connection.</param>
         /// <returns>The number of rows affected.</returns>
-        public static int Execute(this IDbConnection cnn, CommandDefinition command) => ExecuteImpl(cnn, ref command);
+        public static int Execute(this IDbConnection cnn, CommandDefinition command) => ExecuteImpl(cnn, in command);
 
         /// <summary>
         /// Execute parameterized SQL that selects a single value.
@@ -577,7 +577,7 @@ public static int Execute(this IDbConnection cnn, string sql, object? param = nu
         public static object? ExecuteScalar(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null)
         {
             var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered);
-            return ExecuteScalarImpl<object>(cnn, ref command);
+            return ExecuteScalarImpl<object>(cnn, in command);
         }
 
         /// <summary>
@@ -594,7 +594,7 @@ public static int Execute(this IDbConnection cnn, string sql, object? param = nu
         public static T? ExecuteScalar<T>(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null)
         {
             var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered);
-            return ExecuteScalarImpl<T>(cnn, ref command);
+            return ExecuteScalarImpl<T>(cnn, in command);
         }
 
         /// <summary>
@@ -604,7 +604,7 @@ public static int Execute(this IDbConnection cnn, string sql, object? param = nu
         /// <param name="command">The command to execute.</param>
         /// <returns>The first cell selected as <see cref="object"/>.</returns>
         public static object? ExecuteScalar(this IDbConnection cnn, CommandDefinition command) =>
-            ExecuteScalarImpl<object>(cnn, ref command);
+            ExecuteScalarImpl<object>(cnn, in command);
 
         /// <summary>
         /// Execute parameterized SQL that selects a single value.
@@ -614,7 +614,7 @@ public static int Execute(this IDbConnection cnn, string sql, object? param = nu
         /// <param name="command">The command to execute.</param>
         /// <returns>The first cell selected as <typeparamref name="T"/>.</returns>
         public static T? ExecuteScalar<T>(this IDbConnection cnn, CommandDefinition command) =>
-            ExecuteScalarImpl<T>(cnn, ref command);
+            ExecuteScalarImpl<T>(cnn, in command);
 
         private static IEnumerable? GetMultiExec(object? param)
         {
@@ -627,7 +627,7 @@ public static int Execute(this IDbConnection cnn, string sql, object? param = nu
                 ) ? (IEnumerable)param : null;
         }
 
-        private static int ExecuteImpl(this IDbConnection cnn, ref CommandDefinition command)
+        private static int ExecuteImpl(this IDbConnection cnn, in CommandDefinition command, IDbCommand? cmd = null)
         {
             object? param = command.Parameters;
             IEnumerable? multiExec = GetMultiExec(param);
@@ -646,7 +646,7 @@ private static int ExecuteImpl(this IDbConnection cnn, ref CommandDefinition com
                 try
                 {
                     if (wasClosed) cnn.Open();
-                    using (var cmd = command.SetupCommand(cnn, null))
+                    using (cmd = command.SetupCommand(cnn, null))
                     {
                         string? masterSql = null;
                         foreach (var obj in multiExec)
@@ -682,7 +682,7 @@ private static int ExecuteImpl(this IDbConnection cnn, ref CommandDefinition com
                 identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, null, param.GetType());
                 info = GetCacheInfo(identity, param, command.AddToCache);
             }
-            return ExecuteCommand(cnn, ref command, param is null ? null : info!.ParamReader);
+            return ExecuteCommand(cnn, in command, param is null ? null : info!.ParamReader, cmd);
         }
 
         /// <summary>
@@ -713,7 +713,7 @@ private static int ExecuteImpl(this IDbConnection cnn, ref CommandDefinition com
         public static IDataReader ExecuteReader(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null)
         {
             var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered);
-            var reader = ExecuteReaderImpl(cnn, ref command, CommandBehavior.Default, out IDbCommand? dbcmd);
+            var reader = ExecuteReaderImpl(cnn, in command, CommandBehavior.Default, out IDbCommand? dbcmd);
             return DbWrappedReader.Create(dbcmd, reader);
         }
 
@@ -729,7 +729,7 @@ public static IDataReader ExecuteReader(this IDbConnection cnn, string sql, obje
         /// </remarks>
         public static IDataReader ExecuteReader(this IDbConnection cnn, CommandDefinition command)
         {
-            var reader = ExecuteReaderImpl(cnn, ref command, CommandBehavior.Default, out IDbCommand? dbcmd);
+            var reader = ExecuteReaderImpl(cnn, in command, CommandBehavior.Default, out IDbCommand? dbcmd);
             return DbWrappedReader.Create(dbcmd, reader);
         }
 
@@ -746,7 +746,7 @@ public static IDataReader ExecuteReader(this IDbConnection cnn, CommandDefinitio
         /// </remarks>
         public static IDataReader ExecuteReader(this IDbConnection cnn, CommandDefinition command, CommandBehavior commandBehavior)
         {
-            var reader = ExecuteReaderImpl(cnn, ref command, commandBehavior, out IDbCommand? dbcmd);
+            var reader = ExecuteReaderImpl(cnn, in command, commandBehavior, out IDbCommand? dbcmd);
             return DbWrappedReader.Create(dbcmd, reader);
         }
 
@@ -862,7 +862,7 @@ public static IEnumerable<T> Query<T>(this IDbConnection cnn, string sql, object
         public static T QueryFirst<T>(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null)
         {
             var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None);
-            return QueryRowImpl<T>(cnn, Row.First, ref command, typeof(T));
+            return QueryRowImpl<T>(cnn, Row.First, in command, typeof(T));
         }
 
         /// <summary>
@@ -883,7 +883,7 @@ public static T QueryFirst<T>(this IDbConnection cnn, string sql, object? param
         public static T? QueryFirstOrDefault<T>(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null)
         {
             var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None);
-            return QueryRowImpl<T>(cnn, Row.FirstOrDefault, ref command, typeof(T));
+            return QueryRowImpl<T>(cnn, Row.FirstOrDefault, in command, typeof(T));
         }
 
         /// <summary>
@@ -904,7 +904,7 @@ public static T QueryFirst<T>(this IDbConnection cnn, string sql, object? param
         public static T QuerySingle<T>(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null)
         {
             var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None);
-            return QueryRowImpl<T>(cnn, Row.Single, ref command, typeof(T));
+            return QueryRowImpl<T>(cnn, Row.Single, in command, typeof(T));
         }
 
         /// <summary>
@@ -925,7 +925,7 @@ public static T QuerySingle<T>(this IDbConnection cnn, string sql, object? param
         public static T? QuerySingleOrDefault<T>(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null)
         {
             var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None);
-            return QueryRowImpl<T>(cnn, Row.SingleOrDefault, ref command, typeof(T));
+            return QueryRowImpl<T>(cnn, Row.SingleOrDefault, in command, typeof(T));
         }
 
         /// <summary>
@@ -973,7 +973,7 @@ public static object QueryFirst(this IDbConnection cnn, Type type, string sql, o
         {
             if (type is null) throw new ArgumentNullException(nameof(type));
             var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None);
-            return QueryRowImpl<object>(cnn, Row.First, ref command, type);
+            return QueryRowImpl<object>(cnn, Row.First, in command, type);
         }
 
         /// <summary>
@@ -996,7 +996,7 @@ public static object QueryFirst(this IDbConnection cnn, Type type, string sql, o
         {
             if (type is null) throw new ArgumentNullException(nameof(type));
             var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None);
-            return QueryRowImpl<object>(cnn, Row.FirstOrDefault, ref command, type);
+            return QueryRowImpl<object>(cnn, Row.FirstOrDefault, in command, type);
         }
 
         /// <summary>
@@ -1019,7 +1019,7 @@ public static object QuerySingle(this IDbConnection cnn, Type type, string sql,
         {
             if (type is null) throw new ArgumentNullException(nameof(type));
             var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None);
-            return QueryRowImpl<object>(cnn, Row.Single, ref command, type);
+            return QueryRowImpl<object>(cnn, Row.Single, in command, type);
         }
 
         /// <summary>
@@ -1042,7 +1042,7 @@ public static object QuerySingle(this IDbConnection cnn, Type type, string sql,
         {
             if (type is null) throw new ArgumentNullException(nameof(type));
             var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.None);
-            return QueryRowImpl<object>(cnn, Row.SingleOrDefault, ref command, type);
+            return QueryRowImpl<object>(cnn, Row.SingleOrDefault, in command, type);
         }
 
         /// <summary>
@@ -1072,7 +1072,7 @@ public static IEnumerable<T> Query<T>(this IDbConnection cnn, CommandDefinition
         /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive).
         /// </returns>
         public static T QueryFirst<T>(this IDbConnection cnn, CommandDefinition command) =>
-            QueryRowImpl<T>(cnn, Row.First, ref command, typeof(T));
+            QueryRowImpl<T>(cnn, Row.First, in command, typeof(T));
 
         /// <summary>
         /// Executes a query, returning the data typed as <typeparamref name="T"/>.
@@ -1085,7 +1085,7 @@ public static T QueryFirst<T>(this IDbConnection cnn, CommandDefinition command)
         /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive).
         /// </returns>
         public static T? QueryFirstOrDefault<T>(this IDbConnection cnn, CommandDefinition command) =>
-            QueryRowImpl<T>(cnn, Row.FirstOrDefault, ref command, typeof(T));
+            QueryRowImpl<T>(cnn, Row.FirstOrDefault, in command, typeof(T));
 
         /// <summary>
         /// Executes a query, returning the data typed as <typeparamref name="T"/>.
@@ -1098,7 +1098,7 @@ public static T QueryFirst<T>(this IDbConnection cnn, CommandDefinition command)
         /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive).
         /// </returns>
         public static T QuerySingle<T>(this IDbConnection cnn, CommandDefinition command) =>
-            QueryRowImpl<T>(cnn, Row.Single, ref command, typeof(T));
+            QueryRowImpl<T>(cnn, Row.Single, in command, typeof(T));
 
         /// <summary>
         /// Executes a query, returning the data typed as <typeparamref name="T"/>.
@@ -1111,7 +1111,7 @@ public static T QuerySingle<T>(this IDbConnection cnn, CommandDefinition command
         /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive).
         /// </returns>
         public static T? QuerySingleOrDefault<T>(this IDbConnection cnn, CommandDefinition command) =>
-            QueryRowImpl<T>(cnn, Row.SingleOrDefault, ref command, typeof(T));
+            QueryRowImpl<T>(cnn, Row.SingleOrDefault, in command, typeof(T));
 
         /// <summary>
         /// Execute a command that returns multiple result sets, and access each in turn.
@@ -1125,7 +1125,7 @@ public static T QuerySingle<T>(this IDbConnection cnn, CommandDefinition command
         public static GridReader QueryMultiple(this IDbConnection cnn, string sql, object? param = null, IDbTransaction? transaction = null, int? commandTimeout = null, CommandType? commandType = null)
         {
             var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered);
-            return QueryMultipleImpl(cnn, ref command);
+            return QueryMultipleImpl(cnn, in command);
         }
 
         /// <summary>
@@ -1134,9 +1134,9 @@ public static GridReader QueryMultiple(this IDbConnection cnn, string sql, objec
         /// <param name="cnn">The connection to query on.</param>
         /// <param name="command">The command to execute for this query.</param>
         public static GridReader QueryMultiple(this IDbConnection cnn, CommandDefinition command) =>
-            QueryMultipleImpl(cnn, ref command);
+            QueryMultipleImpl(cnn, in command);
 
-        private static GridReader QueryMultipleImpl(this IDbConnection cnn, ref CommandDefinition command)
+        private static GridReader QueryMultipleImpl(this IDbConnection cnn, in CommandDefinition command)
         {
             object? param = command.Parameters;
             var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, typeof(GridReader), param?.GetType());
@@ -1195,19 +1195,18 @@ private static DbDataReader ExecuteReaderWithFlagsFallback(IDbCommand cmd, bool
             }
         }
 
-        private static IEnumerable<T> QueryImpl<T>(this IDbConnection cnn, CommandDefinition command, Type effectiveType)
+        private static IEnumerable<T> QueryImpl<T>(this IDbConnection cnn, CommandDefinition command, Type effectiveType, IDbCommand? cmd = null)
         {
             object? param = command.Parameters;
             var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, effectiveType, param?.GetType());
             var info = GetCacheInfo(identity, param, command.AddToCache);
 
-            IDbCommand? cmd = null;
             DbDataReader? reader = null;
 
             bool wasClosed = cnn.State == ConnectionState.Closed;
             try
             {
-                cmd = command.SetupCommand(cnn, info.ParamReader);
+                cmd ??= command.SetupCommand(cnn, info.ParamReader);
 
                 if (wasClosed) cnn.Open();
                 reader = ExecuteReaderWithFlagsFallback(cmd, wasClosed, CommandBehavior.SequentialAccess | CommandBehavior.SingleResult);
@@ -1288,7 +1287,7 @@ private static void ThrowZeroRows(Row row)
             };
         }
 
-        private static T QueryRowImpl<T>(IDbConnection cnn, Row row, ref CommandDefinition command, Type effectiveType)
+        private static T QueryRowImpl<T>(IDbConnection cnn, Row row, in CommandDefinition command, Type effectiveType)
         {
             object? param = command.Parameters;
             var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, effectiveType, param?.GetType());
@@ -1314,7 +1313,7 @@ private static T QueryRowImpl<T>(IDbConnection cnn, Row row, ref CommandDefiniti
                     // with the CloseConnection flag, so the reader will deal with the connection; we
                     // still need something in the "finally" to ensure that broken SQL still results
                     // in the connection closing itself
-                    result = ReadRow<T>(info, identity, ref command, effectiveType, reader);
+                    result = ReadRow<T>(info, identity, in command, effectiveType, reader);
 
                     if ((row & Row.Single) != 0 && reader.Read()) ThrowMultipleRows(row);
                     while (reader.Read()) { /* ignore subsequent rows */ }
@@ -1353,7 +1352,7 @@ private static T QueryRowImpl<T>(IDbConnection cnn, Row row, ref CommandDefiniti
         /// Shared value deserialization path for QueryRowImpl and QueryRowAsync
         /// </summary>
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        private static T ReadRow<T>(CacheInfo info, Identity identity, ref CommandDefinition command, Type effectiveType, DbDataReader reader)
+        private static T ReadRow<T>(CacheInfo info, Identity identity, in CommandDefinition command, Type effectiveType, DbDataReader reader)
         {
             var tuple = info.Deserializer;
             int hash = GetColumnHash(reader);
@@ -2983,13 +2982,12 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true
         private static readonly MethodInfo StringReplace = typeof(string).GetPublicInstanceMethod(nameof(string.Replace), [typeof(string), typeof(string)])!,
             InvariantCulture = typeof(CultureInfo).GetProperty(nameof(CultureInfo.InvariantCulture), BindingFlags.Public | BindingFlags.Static)!.GetGetMethod()!;
 
-        private static int ExecuteCommand(IDbConnection cnn, ref CommandDefinition command, Action<IDbCommand, object?>? paramReader)
+        private static int ExecuteCommand(IDbConnection cnn, in CommandDefinition command, Action<IDbCommand, object?>? paramReader, IDbCommand? cmd)
         {
-            IDbCommand? cmd = null;
             bool wasClosed = cnn.State == ConnectionState.Closed;
             try
             {
-                cmd = command.SetupCommand(cnn, paramReader);
+                cmd ??= command.SetupCommand(cnn, paramReader);
                 if (wasClosed) cnn.Open();
                 int result = cmd.ExecuteNonQuery();
                 command.OnCompleted();
@@ -3003,7 +3001,7 @@ private static int ExecuteCommand(IDbConnection cnn, ref CommandDefinition comma
             }
         }
 
-        private static T? ExecuteScalarImpl<T>(IDbConnection cnn, ref CommandDefinition command)
+        private static T? ExecuteScalarImpl<T>(IDbConnection cnn, in CommandDefinition command, IDbCommand? cmd = null)
         {
             Action<IDbCommand, object?>? paramReader = null;
             object? param = command.Parameters;
@@ -3013,12 +3011,11 @@ private static int ExecuteCommand(IDbConnection cnn, ref CommandDefinition comma
                 paramReader = GetCacheInfo(identity, command.Parameters, command.AddToCache).ParamReader;
             }
 
-            IDbCommand? cmd = null;
             bool wasClosed = cnn.State == ConnectionState.Closed;
             object? result;
             try
             {
-                cmd = command.SetupCommand(cnn, paramReader);
+                cmd ??= command.SetupCommand(cnn, paramReader);
                 if (wasClosed) cnn.Open();
                 result = cmd.ExecuteScalar();
                 command.OnCompleted();
@@ -3032,9 +3029,9 @@ private static int ExecuteCommand(IDbConnection cnn, ref CommandDefinition comma
             return Parse<T>(result);
         }
 
-        private static DbDataReader ExecuteReaderImpl(IDbConnection cnn, ref CommandDefinition command, CommandBehavior commandBehavior, out IDbCommand? cmd)
+        private static DbDataReader ExecuteReaderImpl(IDbConnection cnn, in CommandDefinition command, CommandBehavior commandBehavior, out IDbCommand? cmd)
         {
-            Action<IDbCommand, object?>? paramReader = GetParameterReader(cnn, ref command);
+            Action<IDbCommand, object?>? paramReader = GetParameterReader(cnn, in command);
             cmd = null;
             bool wasClosed = cnn.State == ConnectionState.Closed, disposeCommand = true;
             try
@@ -3058,7 +3055,7 @@ private static DbDataReader ExecuteReaderImpl(IDbConnection cnn, ref CommandDefi
             }
         }
 
-        private static Action<IDbCommand, object?>? GetParameterReader(IDbConnection cnn, ref CommandDefinition command)
+        private static Action<IDbCommand, object?>? GetParameterReader(IDbConnection cnn, in CommandDefinition command)
         {
             object? param = command.Parameters;
             IEnumerable? multiExec = GetMultiExec(param);
diff --git a/tests/Dapper.Tests/SqlBuilderLiteralTests.cs b/tests/Dapper.Tests/SqlBuilderLiteralTests.cs
new file mode 100644
index 000000000..d7c908d88
--- /dev/null
+++ b/tests/Dapper.Tests/SqlBuilderLiteralTests.cs
@@ -0,0 +1,1173 @@
+﻿#if NET8_0_OR_GREATER
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Data.Common;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.CSharp.RuntimeBinder;
+using Xunit;
+
+namespace Dapper.Tests
+{
+    [Collection("MiscTests")]
+    public sealed class SystemSqlClientSqlBuilderLiteralTests : SqlBuilderLiteralTests<SystemSqlClientProvider> { }
+#if MSSQLCLIENT
+    [Collection("MiscTests")]
+    public sealed class MicrosoftSqlClientSqlBuilderLiteralTests : SqlBuilderLiteralTests<MicrosoftSqlClientProvider> { }
+#endif
+    public abstract class SqlBuilderLiteralTests<TProvider> : TestBase<TProvider> where TProvider : DatabaseProvider
+    {
+        [Fact]
+        public void TestNullableGuidSupport()
+        {
+            var guid = connection.Query<Guid?>("select null").First();
+            Assert.Null(guid);
+
+            guid = Guid.NewGuid();
+            var guid2 = connection.Query<Guid?>($"select @{guid}").First();
+            Assert.Equal(guid, guid2);
+        }
+
+        [Fact]
+        public void TestNonNullableGuidSupport()
+        {
+            var guid = Guid.NewGuid();
+            var guid2 = connection.Query<Guid?>($"select @{guid}").First();
+            Assert.True(guid == guid2);
+        }
+
+        private struct Car
+        {
+            public enum TrapEnum : int
+            {
+                A = 1,
+                B = 2
+            }
+#pragma warning disable 0649
+            public string Name;
+#pragma warning restore 0649
+            public int Age { get; set; }
+            public TrapEnum Trap { get; set; }
+        }
+
+        private struct CarWithAllProps
+        {
+            public string Name { get; set; }
+            public int Age { get; init; }
+            public Car.TrapEnum Trap { get; init; }
+        }
+
+        private record PositionalCarRecord(int Age, Car.TrapEnum Trap, string? Name)
+        {
+            public PositionalCarRecord() : this(default, default, default) { }
+        }
+
+        private record NominalCarRecord
+        {
+            public int Age { get; init; }
+            public Car.TrapEnum Trap { get; init; }
+            public string? Name { get; init; }
+        }
+
+        [Fact]
+        public void TestStructAsParam()
+        {
+            var car1 = new CarWithAllProps { Name = "Ford", Age = 21, Trap = Car.TrapEnum.B };
+            // note Car has Name as a field; parameters only respect properties at the moment
+            var car2 = connection.Query<CarWithAllProps>($"select @{car1.Name} Name, @{car1.Age} Age, @{car1.Trap} Trap").First();
+
+            Assert.Equal(car2.Name, car1.Name);
+            Assert.Equal(car2.Age, car1.Age);
+            Assert.Equal(car2.Trap, car1.Trap);
+        }
+
+        private class NullTestType
+        {
+            public int Id { get; }
+            public int Foo { get; }
+        }
+
+        [Fact]
+        public void TestStrings()
+        {
+            Assert.Equal(new[] { "a", "b" }, connection.Query<string>("select 'a' a union select 'b'"));
+        }
+
+        // see https://stackoverflow.com/questions/16726709/string-format-with-sql-wildcard-causing-dapper-query-to-break
+        [Fact]
+        public void CheckComplexConcat()
+        {
+            const string term = "F"; // the term the user searched for
+
+            connection.ExecuteNQ($"""
+            create table #users16726709 (first_name varchar(200), last_name varchar(200))
+            insert #users16726709 values (@{"Fred"},@{"Bloggs"})
+            insert #users16726709 values (@{"Tony"},@{"Farcus"})
+            insert #users16726709 values (@{"Albert"},@{"TenoF"})
+            """);
+
+            // Using Dapper
+            Assert.Equal(2, connection.Query<object>($"""
+                SELECT * FROM #users16726709
+                WHERE (first_name LIKE CONCAT(@{term}, '%') OR last_name LIKE CONCAT(@{term}, '%'));
+                """).Count());
+            Assert.Equal(3, connection.Query<object>($"""
+                SELECT * FROM #users16726709
+                WHERE (first_name LIKE CONCAT('%', @{term}, '%') OR last_name LIKE CONCAT('%', @{term}, '%'));
+                """).Count());
+        }
+
+        [Fact]
+        public void TestExtraFields()
+        {
+            var guid = Guid.NewGuid();
+            var dog = connection.Query<Dog>($"select '' as Extra, 1 as Age, 0.1 as Name1 , Id = @{guid}");
+
+            Assert.Single(dog);
+            Assert.Equal(1, dog.First().Age);
+            Assert.Equal(dog.First().Id, guid);
+        }
+
+        [Fact]
+        public void TestStrongType()
+        {
+            var guid = Guid.NewGuid();
+            var dog = connection.Query<Dog>($"select Age = @{(int?)null}, Id = @{guid}");
+
+            Assert.Single(dog);
+            Assert.Null(dog.First().Age);
+            Assert.Equal(dog.First().Id, guid);
+        }
+
+        [Fact]
+        public void TestStringList()
+        {
+            Assert.Equal(
+                new[] { "a", "b", "c" },
+                connection.Query<string>("select * from (select 'a' as x union all select 'b' union all select 'c') as T where x in @strings", new { strings = new[] { "a", "b", "c" } })
+            );
+            Assert.Equal(
+                new string[0],
+                connection.Query<string>("select * from (select 'a' as x union all select 'b' union all select 'c') as T where x in @strings", new { strings = new string[0] })
+            );
+        }
+
+        [Fact]
+        public void TestExecuteCommand()
+        {
+            Assert.Equal(2, connection.Execute($"""
+    set nocount on 
+    create table #t(i int) 
+    set nocount off 
+    insert #t 
+    select @{1} a union all select @{2}
+    set nocount on 
+    drop table #t
+    """));
+        }
+
+        /*
+        [Fact]
+        public void TestExecuteMultipleCommand()
+        {
+            connection.Execute("create table #t(i int)");
+            try
+            {
+                int tally = connection.Execute("insert #t (i) values(@a)", new[] { new { a = 1 }, new { a = 2 }, new { a = 3 }, new { a = 4 } });
+                int sum = connection.Query<int>("select sum(i) from #t").First();
+                Assert.Equal(4, tally);
+                Assert.Equal(10, sum);
+            }
+            finally
+            {
+                connection.Execute("drop table #t");
+            }
+        }
+
+        private class Student
+        {
+            public string? Name { get; set; }
+            public int Age { get; set; }
+        }
+
+        [Fact]
+        public void TestExecuteMultipleCommandStrongType()
+        {
+            connection.Execute("create table #t(Name nvarchar(max), Age int)");
+            try
+            {
+                int tally = connection.Execute("insert #t (Name,Age) values(@Name, @Age)", new List<Student>(2)
+            {
+                new Student{Age = 1, Name = "sam"},
+                new Student{Age = 2, Name = "bob"}
+            });
+                int sum = connection.Query<int>("select sum(Age) from #t").First();
+                Assert.Equal(2, tally);
+                Assert.Equal(3, sum);
+            }
+            finally
+            {
+                connection.Execute("drop table #t");
+            }
+        }
+
+        [Fact]
+        public void TestExecuteMultipleCommandObjectArray()
+        {
+            connection.Execute("create table #t(i int)");
+            int tally = connection.Execute("insert #t (i) values(@a)", new object[] { new { a = 1 }, new { a = 2 }, new { a = 3 }, new { a = 4 } });
+            int sum = connection.Query<int>("select sum(i) from #t drop table #t").First();
+            Assert.Equal(4, tally);
+            Assert.Equal(10, sum);
+        }
+
+        private class TestObj
+        {
+            public int _internal;
+            internal int Internal
+            {
+                set { _internal = value; }
+            }
+
+            public int _priv;
+            private int Priv
+            {
+                set { _priv = value; }
+            }
+
+            private int PrivGet => _priv;
+        }
+
+        [Fact]
+        public void TestSetInternal()
+        {
+            Assert.Equal(10, connection.Query<TestObj>("select 10 as [Internal]").First()._internal);
+        }
+
+        [Fact]
+        public void TestSetPrivate()
+        {
+            Assert.Equal(10, connection.Query<TestObj>("select 10 as [Priv]").First()._priv);
+        }
+
+        [Fact]
+        public void TestExpandWithNullableFields()
+        {
+            var row = connection.Query("select null A, 2 B").Single();
+            Assert.Null((int?)row.A);
+            Assert.Equal(2, (int?)row.B);
+        }
+
+        [Fact]
+        public void TestEnumeration()
+        {
+            var en = connection.Query<int>("select 1 as one union all select 2 as one", buffered: false);
+            var i = en.GetEnumerator();
+            i.MoveNext();
+
+            bool gotException = false;
+            try
+            {
+                var x = connection.Query<int>("select 1 as one", buffered: false).First();
+            }
+            catch (Exception)
+            {
+                gotException = true;
+            }
+
+            while (i.MoveNext())
+            {
+            }
+
+            // should not exception, since enumerated
+            en = connection.Query<int>("select 1 as one", buffered: false);
+
+            Assert.True(gotException);
+        }
+
+        [Fact]
+        public void TestEnumerationDynamic()
+        {
+            var en = connection.Query("select 1 as one union all select 2 as one", buffered: false);
+            var i = en.GetEnumerator();
+            i.MoveNext();
+
+            bool gotException = false;
+            try
+            {
+                var x = connection.Query("select 1 as one", buffered: false).First();
+            }
+            catch (Exception)
+            {
+                gotException = true;
+            }
+
+            while (i.MoveNext())
+            {
+            }
+
+            // should not exception, since enumertated
+            en = connection.Query("select 1 as one", buffered: false);
+
+            Assert.True(gotException);
+        }
+
+        [Fact]
+        public void TestNakedBigInt()
+        {
+            const long foo = 12345;
+            var result = connection.Query<long>("select @foo", new { foo }).Single();
+            Assert.Equal(foo, result);
+        }
+
+        [Fact]
+        public void TestBigIntMember()
+        {
+            const long foo = 12345;
+            var result = connection.Query<WithBigInt>(@"
+declare @bar table(Value bigint)
+insert @bar values (@foo)
+select * from @bar", new { foo }).Single();
+            Assert.Equal(foo, result.Value);
+        }
+
+        private class WithBigInt
+        {
+            public long Value { get; set; }
+        }
+
+        [Fact]
+        public void TestFieldsAndPrivates()
+        {
+            var data = connection.Query<TestFieldCaseAndPrivatesEntity>(
+                "select a=1,b=2,c=3,d=4,f='5'").Single();
+            Assert.Equal(1, data.a);
+            Assert.Equal(2, data.GetB());
+            Assert.Equal(3, data.c);
+            Assert.Equal(4, data.GetD());
+            Assert.Equal(5, data.e);
+        }
+
+        private class TestFieldCaseAndPrivatesEntity
+        {
+#pragma warning disable IDE1006 // Naming Styles
+            public int a { get; set; }
+            private int b { get; set; }
+            public int GetB() { return b; }
+            public int c = 0;
+#pragma warning disable RCS1169 // Mark field as read-only.
+            private int d = 0;
+#pragma warning restore RCS1169 // Mark field as read-only.
+            public int GetD() { return d; }
+            public int e { get; set; }
+            private string f
+            {
+                get { return e.ToString(); }
+                set { e = int.Parse(value); }
+            }
+#pragma warning restore IDE1006 // Naming Styles
+        }
+
+        private class InheritanceTest1
+        {
+            public string? Base1 { get; set; }
+            public string? Base2 { get; private set; }
+        }
+
+        private class InheritanceTest2 : InheritanceTest1
+        {
+            public string? Derived1 { get; set; }
+            public string? Derived2 { get; private set; }
+        }
+
+        [Fact]
+        public void TestInheritance()
+        {
+            // Test that inheritance works.
+            var list = connection.Query<InheritanceTest2>("select 'One' as Derived1, 'Two' as Derived2, 'Three' as Base1, 'Four' as Base2");
+            Assert.Equal("One", list.First().Derived1);
+            Assert.Equal("Two", list.First().Derived2);
+            Assert.Equal("Three", list.First().Base1);
+            Assert.Equal("Four", list.First().Base2);
+        }
+
+        [Fact]
+        public void ExecuteReader()
+        {
+            var dt = new DataTable();
+            dt.Load(connection.ExecuteReader("select 3 as [three], 4 as [four]"));
+            Assert.Equal(2, dt.Columns.Count);
+            Assert.Equal("three", dt.Columns[0].ColumnName);
+            Assert.Equal("four", dt.Columns[1].ColumnName);
+            Assert.Equal(1, dt.Rows.Count);
+            Assert.Equal(3, (int)dt.Rows[0][0]);
+            Assert.Equal(4, (int)dt.Rows[0][1]);
+        }
+
+        [Fact]
+        public void TestDbString()
+        {
+            var obj = connection.Query("select datalength(@a) as a, datalength(@b) as b, datalength(@c) as c, datalength(@d) as d, datalength(@e) as e, datalength(@f) as f",
+                new
+                {
+                    a = new DbString { Value = "abcde", IsFixedLength = true, Length = 10, IsAnsi = true },
+                    b = new DbString { Value = "abcde", IsFixedLength = true, Length = 10, IsAnsi = false },
+                    c = new DbString { Value = "abcde", IsFixedLength = false, Length = 10, IsAnsi = true },
+                    d = new DbString { Value = "abcde", IsFixedLength = false, Length = 10, IsAnsi = false },
+                    e = new DbString { Value = "abcde", IsAnsi = true },
+                    f = new DbString { Value = "abcde", IsAnsi = false },
+                }).First();
+            Assert.Equal(10, (int)obj.a);
+            Assert.Equal(20, (int)obj.b);
+            Assert.Equal(5, (int)obj.c);
+            Assert.Equal(10, (int)obj.d);
+            Assert.Equal(5, (int)obj.e);
+            Assert.Equal(10, (int)obj.f);
+        }
+
+        [Fact]
+        public void DbStringNullHandling()
+        {
+            // without lengths
+            var obj = new { x = new DbString("abc"), y = (DbString?)new DbString(null) };
+            var row = connection.QuerySingle<(string? x,string? y)>("select @x as x, @y as y", obj);
+            Assert.Equal("abc", row.x);
+            Assert.Null(row.y);
+
+            // with lengths
+            obj = new { x = new DbString("abc", 200), y = (DbString?)new DbString(null, 200) };
+            row = connection.QuerySingle<(string? x, string? y)>("select @x as x, @y as y", obj);
+            Assert.Equal("abc", row.x);
+            Assert.Null(row.y);
+
+            // null raw value - give clear message, at least
+            obj = obj with { y = null };
+            var ex = Assert.Throws<InvalidOperationException>(() => connection.QuerySingle<(string? x, string? y)>("select @x as x, @y as y", obj));
+            Assert.Equal("Member 'y' is an ICustomQueryParameter and cannot be null", ex.Message);
+        }
+
+        [Fact]
+        public void TestDbStringToString()
+        {
+            Assert.Equal("Dapper.DbString (Value: 'abcde', Length: 10, IsAnsi: True, IsFixedLength: True)", 
+                new DbString { Value = "abcde", IsFixedLength = true, Length = 10, IsAnsi = true }.ToString());
+            Assert.Equal("Dapper.DbString (Value: 'abcde', Length: 10, IsAnsi: False, IsFixedLength: True)",
+                new DbString { Value = "abcde", IsFixedLength = true, Length = 10, IsAnsi = false }.ToString());
+            Assert.Equal("Dapper.DbString (Value: 'abcde', Length: 10, IsAnsi: True, IsFixedLength: False)",
+                new DbString { Value = "abcde", IsFixedLength = false, Length = 10, IsAnsi = true }.ToString());
+            Assert.Equal("Dapper.DbString (Value: 'abcde', Length: 10, IsAnsi: False, IsFixedLength: False)",
+                new DbString { Value = "abcde", IsFixedLength = false, Length = 10, IsAnsi = false }.ToString());
+            Assert.Equal("Dapper.DbString (Value: null, Length: -1, IsAnsi: False, IsFixedLength: False)",
+                new DbString { Value = null }.ToString());
+
+            Assert.Equal("Dapper.DbString (Value: 'abcde', Length: -1, IsAnsi: True, IsFixedLength: False)",
+                new DbString { Value = "abcde", IsAnsi = true }.ToString());
+            Assert.Equal("Dapper.DbString (Value: 'abcde', Length: -1, IsAnsi: False, IsFixedLength: False)",
+                new DbString { Value = "abcde", IsAnsi = false }.ToString());
+        }
+
+        [Fact]
+        public void TestDefaultDbStringDbType()
+        {
+            var origDefaultStringDbType = DbString.IsAnsiDefault;
+            try
+            {
+                DbString.IsAnsiDefault = true;
+                var a = new DbString { Value = "abcde" };
+                var b = new DbString { Value = "abcde", IsAnsi = false };
+                Assert.True(a.IsAnsi);
+                Assert.False(b.IsAnsi);
+            }
+            finally
+            {
+                DbString.IsAnsiDefault = origDefaultStringDbType;
+            }
+        }
+
+        [Fact]
+        public void TestFastExpandoSupportsIDictionary()
+        {
+            var row = connection.Query("select 1 A, 'two' B").First() as IDictionary<string, object>;
+            Assert.NotNull(row);
+            Assert.Equal(1, row["A"]);
+            Assert.Equal("two", row["B"]);
+        }
+
+        [Fact]
+        public void TestDapperSetsPrivates()
+        {
+            Assert.Equal(1, connection.Query<PrivateDan>("select 'one' ShadowInDB").First().Shadow);
+
+            Assert.Equal(1, connection.QueryFirstOrDefault<PrivateDan>("select 'one' ShadowInDB")?.Shadow);
+        }
+
+        private class PrivateDan
+        {
+            public int Shadow { get; set; }
+            private string ShadowInDB
+            {
+                set { Shadow = value == "one" ? 1 : 0; }
+            }
+        }
+
+        [Fact]
+        public void TestUnexpectedDataMessage()
+        {
+            string? msg = null;
+            try
+            {
+                connection.Query<int>("select count(1) where 1 = @Foo", new WithBizarreData { Foo = new GenericUriParser(GenericUriParserOptions.Default), Bar = 23 }).First();
+            }
+            catch (Exception ex)
+            {
+                msg = ex.Message;
+            }
+            Assert.Equal("The member Foo of type System.GenericUriParser cannot be used as a parameter value", msg);
+        }
+
+        [Fact]
+        public void TestUnexpectedButFilteredDataMessage()
+        {
+            int i = connection.Query<int>("select @Bar", new WithBizarreData { Foo = new GenericUriParser(GenericUriParserOptions.Default), Bar = 23 }).Single();
+
+            Assert.Equal(23, i);
+        }
+
+        private class WithBizarreData
+        {
+            public GenericUriParser? Foo { get; set; }
+            public int Bar { get; set; }
+        }
+
+        private class WithCharValue
+        {
+            public char Value { get; set; }
+            public char? ValueNullable { get; set; }
+        }
+
+        [Fact]
+        public void TestCharInputAndOutput()
+        {
+            const char test = '〠';
+            char c = connection.Query<char>("select @c", new { c = test }).Single();
+
+            Assert.Equal(test, c);
+
+            var obj = connection.Query<WithCharValue>("select @Value as Value", new WithCharValue { Value = c }).Single();
+
+            Assert.Equal(test, obj.Value);
+        }
+
+        [Fact]
+        public void TestNullableCharInputAndOutputNonNull()
+        {
+            char? test = '〠';
+            char? c = connection.Query<char?>("select @c", new { c = test }).Single();
+
+            Assert.Equal(c, test);
+
+            var obj = connection.Query<WithCharValue>("select @ValueNullable as ValueNullable", new WithCharValue { ValueNullable = c }).Single();
+
+            Assert.Equal(obj.ValueNullable, test);
+        }
+
+        [Fact]
+        public void TestNullableCharInputAndOutputNull()
+        {
+            char? test = null;
+            char? c = connection.Query<char?>("select @c", new { c = test }).Single();
+
+            Assert.Equal(c, test);
+
+            var obj = connection.Query<WithCharValue>("select @ValueNullable as ValueNullable", new WithCharValue { ValueNullable = c }).Single();
+
+            Assert.Equal(obj.ValueNullable, test);
+        }
+
+        [Fact]
+        public void WorkDespiteHavingWrongStructColumnTypes()
+        {
+            var hazInt = connection.Query<CanHazInt>("select cast(1 as bigint) Value").Single();
+            Assert.Equal(1, hazInt.Value);
+        }
+
+        private struct CanHazInt
+        {
+            public int Value { get; set; }
+        }
+
+        [Fact]
+        public void TestInt16Usage()
+        {
+            Assert.Equal((short)42, connection.Query<short>("select cast(42 as smallint)").Single());
+            Assert.Equal((short?)42, connection.Query<short?>("select cast(42 as smallint)").Single());
+            Assert.Equal((short?)null, connection.Query<short?>("select cast(null as smallint)").Single());
+
+            Assert.Equal((ShortEnum)42, connection.Query<ShortEnum>("select cast(42 as smallint)").Single());
+            Assert.Equal((ShortEnum?)42, connection.Query<ShortEnum?>("select cast(42 as smallint)").Single());
+            Assert.Equal((ShortEnum?)null, connection.Query<ShortEnum?>("select cast(null as smallint)").Single());
+
+            var row =
+                connection.Query<WithInt16Values>(
+                    "select cast(1 as smallint) as NonNullableInt16, cast(2 as smallint) as NullableInt16, cast(3 as smallint) as NonNullableInt16Enum, cast(4 as smallint) as NullableInt16Enum")
+                    .Single();
+            Assert.Equal((short)1, row.NonNullableInt16);
+            Assert.Equal((short)2, row.NullableInt16);
+            Assert.Equal(ShortEnum.Three, row.NonNullableInt16Enum);
+            Assert.Equal(ShortEnum.Four, row.NullableInt16Enum);
+
+            row =
+    connection.Query<WithInt16Values>(
+        "select cast(5 as smallint) as NonNullableInt16, cast(null as smallint) as NullableInt16, cast(6 as smallint) as NonNullableInt16Enum, cast(null as smallint) as NullableInt16Enum")
+        .Single();
+            Assert.Equal((short)5, row.NonNullableInt16);
+            Assert.Equal((short?)null, row.NullableInt16);
+            Assert.Equal(ShortEnum.Six, row.NonNullableInt16Enum);
+            Assert.Equal((ShortEnum?)null, row.NullableInt16Enum);
+        }
+
+        [Fact]
+        public void TestInt32Usage()
+        {
+            Assert.Equal((int)42, connection.Query<int>("select cast(42 as int)").Single());
+            Assert.Equal((int?)42, connection.Query<int?>("select cast(42 as int)").Single());
+            Assert.Equal((int?)null, connection.Query<int?>("select cast(null as int)").Single());
+
+            Assert.Equal((IntEnum)42, connection.Query<IntEnum>("select cast(42 as int)").Single());
+            Assert.Equal((IntEnum?)42, connection.Query<IntEnum?>("select cast(42 as int)").Single());
+            Assert.Equal((IntEnum?)null, connection.Query<IntEnum?>("select cast(null as int)").Single());
+
+            var row =
+                connection.Query<WithInt32Values>(
+                    "select cast(1 as int) as NonNullableInt32, cast(2 as int) as NullableInt32, cast(3 as int) as NonNullableInt32Enum, cast(4 as int) as NullableInt32Enum")
+                    .Single();
+            Assert.Equal((int)1, row.NonNullableInt32);
+            Assert.Equal((int)2, row.NullableInt32);
+            Assert.Equal(IntEnum.Three, row.NonNullableInt32Enum);
+            Assert.Equal(IntEnum.Four, row.NullableInt32Enum);
+
+            row =
+    connection.Query<WithInt32Values>(
+        "select cast(5 as int) as NonNullableInt32, cast(null as int) as NullableInt32, cast(6 as int) as NonNullableInt32Enum, cast(null as int) as NullableInt32Enum")
+        .Single();
+            Assert.Equal((int)5, row.NonNullableInt32);
+            Assert.Equal((int?)null, row.NullableInt32);
+            Assert.Equal(IntEnum.Six, row.NonNullableInt32Enum);
+            Assert.Equal((IntEnum?)null, row.NullableInt32Enum);
+        }
+
+        public class WithInt16Values
+        {
+            public short NonNullableInt16 { get; set; }
+            public short? NullableInt16 { get; set; }
+            public ShortEnum NonNullableInt16Enum { get; set; }
+            public ShortEnum? NullableInt16Enum { get; set; }
+        }
+
+        public class WithInt32Values
+        {
+            public int NonNullableInt32 { get; set; }
+            public int? NullableInt32 { get; set; }
+            public IntEnum NonNullableInt32Enum { get; set; }
+            public IntEnum? NullableInt32Enum { get; set; }
+        }
+
+        public enum IntEnum : int
+        {
+            Zero = 0, One = 1, Two = 2, Three = 3, Four = 4, Five = 5, Six = 6
+        }
+
+        [Fact]
+        public void Issue_40_AutomaticBoolConversion()
+        {
+            var user = connection.Query<Issue40_User>("select UserId=1,Email='abc',Password='changeme',Active=cast(1 as tinyint)").Single();
+            Assert.True(user.Active);
+            Assert.Equal(1, user.UserID);
+            Assert.Equal("abc", user.Email);
+            Assert.Equal("changeme", user.Password);
+        }
+
+        public class Issue40_User
+        {
+            public Issue40_User()
+            {
+                Email = Password = string.Empty;
+            }
+
+            public int UserID { get; set; }
+            public string Email { get; set; }
+            public string Password { get; set; }
+            public bool Active { get; set; }
+        }
+
+        [Fact]
+        public void ExecuteFromClosed()
+        {
+            using var conn = GetClosedConnection();
+            conn.Execute("-- nop");
+            Assert.Equal(ConnectionState.Closed, conn.State);
+        }
+
+        [Fact]
+        public void ExecuteInvalidFromClosed()
+        {
+            using var conn = GetClosedConnection();
+            var ex = Assert.ThrowsAny<Exception>(() => conn.Execute("nop"));
+            Assert.Equal(ConnectionState.Closed, conn.State);
+        }
+
+        [Fact]
+        public void QueryFromClosed()
+        {
+            using var conn = GetClosedConnection();
+            var i = conn.Query<int>("select 1").Single();
+            Assert.Equal(ConnectionState.Closed, conn.State);
+            Assert.Equal(1, i);
+        }
+
+        [Fact]
+        public void QueryInvalidFromClosed()
+        {
+            using var conn = GetClosedConnection();
+            Assert.ThrowsAny<Exception>(() => conn.Query<int>("select gibberish").Single());
+            Assert.Equal(ConnectionState.Closed, conn.State);
+        }
+
+        [Fact]
+        public void TestDynamicMutation()
+        {
+            var obj = connection.Query("select 1 as [a], 2 as [b], 3 as [c]").Single();
+            Assert.Equal(1, (int)obj.a);
+            IDictionary<string, object> dict = obj;
+            Assert.Equal(3, dict.Count);
+            Assert.True(dict.Remove("a"));
+            Assert.False(dict.Remove("d"));
+            Assert.Equal(2, dict.Count);
+            dict.Add("d", 4);
+            Assert.Equal(3, dict.Count);
+            Assert.Equal("b,c,d", string.Join(",", dict.Keys.OrderBy(x => x)));
+            Assert.Equal("2,3,4", string.Join(",", dict.OrderBy(x => x.Key).Select(x => x.Value)));
+
+            Assert.Equal(2, (int)obj.b);
+            Assert.Equal(3, (int)obj.c);
+            Assert.Equal(4, (int)obj.d);
+            try
+            {
+                Assert.Equal(1, (int)obj.a);
+                throw new InvalidOperationException("should have thrown");
+            }
+            catch (RuntimeBinderException)
+            {
+                // pass
+            }
+        }
+
+        [Fact]
+        public void TestIssue131()
+        {
+            var results = connection.Query<dynamic, int, dynamic>(
+                "SELECT 1 Id, 'Mr' Title, 'John' Surname, 4 AddressCount",
+                (person, addressCount) => person,
+                splitOn: "AddressCount"
+            ).First();
+
+            var asDict = (IDictionary<string, object>)results;
+
+            Assert.True(asDict.ContainsKey("Id"));
+            Assert.True(asDict.ContainsKey("Title"));
+            Assert.True(asDict.ContainsKey("Surname"));
+            Assert.False(asDict.ContainsKey("AddressCount"));
+        }
+
+        // see https://stackoverflow.com/questions/13127886/dapper-returns-null-for-singleordefaultdatediff
+        [Fact]
+        public void TestNullFromInt_NoRows()
+        {
+            var result = connection.Query<int>( // case with rows
+             "select DATEDIFF(day, GETUTCDATE(), @date)", new { date = DateTime.UtcNow.AddDays(20) })
+             .SingleOrDefault();
+            Assert.Equal(20, result);
+
+            result = connection.Query<int>( // case without rows
+                "select DATEDIFF(day, GETUTCDATE(), @date) where 1 = 0", new { date = DateTime.UtcNow.AddDays(20) })
+                .SingleOrDefault();
+            Assert.Equal(0, result); // zero rows; default of int over zero rows is zero
+        }
+
+        [Fact]
+        public void TestDapperTableMetadataRetrieval()
+        {
+            // Test for a bug found in CS 51509960 where the following sequence would result in an InvalidOperationException being
+            // thrown due to an attempt to access a disposed of DataReader:
+            //
+            // - Perform a dynamic query that yields no results
+            // - Add data to the source of that query
+            // - Perform a the same query again
+            connection.Execute("CREATE TABLE #sut (value varchar(10) NOT NULL PRIMARY KEY)");
+            Assert.Equal(Enumerable.Empty<dynamic>(), connection.Query("SELECT value FROM #sut"));
+
+            Assert.Equal(1, connection.Execute("INSERT INTO #sut (value) VALUES ('test')"));
+            var result = connection.Query("SELECT value FROM #sut");
+
+            var first = result.First();
+            Assert.Equal("test", (string)first.value);
+        }
+
+        [Fact]
+        public void DbStringAnsi()
+        {
+            var a = connection.Query<int>("select datalength(@x)",
+                new { x = new DbString { Value = "abc", IsAnsi = true } }).Single();
+            var b = connection.Query<int>("select datalength(@x)",
+                new { x = new DbString { Value = "abc", IsAnsi = false } }).Single();
+            Assert.Equal(3, a);
+            Assert.Equal(6, b);
+        }
+
+        private class HasInt32
+        {
+            public int Value { get; set; }
+        }
+
+        // https://stackoverflow.com/q/23696254/23354
+        [Fact]
+        public void DownwardIntegerConversion()
+        {
+            const string sql = "select cast(42 as bigint) as Value";
+            int i = connection.Query<HasInt32>(sql).Single().Value;
+            Assert.Equal(42, i);
+
+            i = connection.Query<int>(sql).Single();
+            Assert.Equal(42, i);
+        }
+
+        [Fact]
+        public void TypeBasedViaDynamic()
+        {
+            Type type = Common.GetSomeType();
+
+            dynamic template = Activator.CreateInstance(type)!;
+            dynamic actual = CheetViaDynamic(template, "select @A as [A], @B as [B]", new { A = 123, B = "abc" });
+            Assert.Equal(((object)actual).GetType(), type);
+            int a = actual.A;
+            string b = actual.B;
+            Assert.Equal(123, a);
+            Assert.Equal("abc", b);
+        }
+
+        [Fact]
+        public void TypeBasedViaType()
+        {
+            Type type = Common.GetSomeType();
+
+            dynamic actual = connection.Query(type, "select @A as [A], @B as [B]", new { A = 123, B = "abc" }).First();
+            Assert.Equal(((object)actual).GetType(), type);
+            int a = actual.A;
+            string b = actual.B;
+            Assert.Equal(123, a);
+            Assert.Equal("abc", b);
+        }
+
+        private T CheetViaDynamic<T>(T template, string query, object args)
+        {
+            return connection.Query<T>(query, args).Single();
+        }
+
+        [Fact]
+        public void Issue22_ExecuteScalar()
+        {
+            int i = connection.ExecuteScalar<int>("select 123");
+            Assert.Equal(123, i);
+
+            i = connection.ExecuteScalar<int>("select cast(123 as bigint)");
+            Assert.Equal(123, i);
+
+            long j = connection.ExecuteScalar<long>("select 123");
+            Assert.Equal(123L, j);
+
+            j = connection.ExecuteScalar<long>("select cast(123 as bigint)");
+            Assert.Equal(123L, j);
+
+            int? k = connection.ExecuteScalar<int?>("select @i", new { i = default(int?) });
+            Assert.Null(k);
+        }
+
+        [Fact]
+        public void Issue142_FailsNamedStatus()
+        {
+            var row1 = connection.Query<Issue142_Status>("select @Status as [Status]", new { Status = StatusType.Started }).Single();
+            Assert.Equal(StatusType.Started, row1.Status);
+
+            var row2 = connection.Query<Issue142_StatusType>("select @Status as [Status]", new { Status = Status.Started }).Single();
+            Assert.Equal(Status.Started, row2.Status);
+        }
+
+        public class Issue142_Status
+        {
+            public StatusType Status { get; set; }
+        }
+
+        public class Issue142_StatusType
+        {
+            public Status Status { get; set; }
+        }
+
+        public enum StatusType : byte
+        {
+            NotStarted = 1, Started = 2, Finished = 3
+        }
+
+        public enum Status : byte
+        {
+            NotStarted = 1, Started = 2, Finished = 3
+        }
+
+        [Fact]
+        public void Issue178_SqlServer()
+        {
+            const string sql = "select count(*) from Issue178";
+            try { connection.Execute("drop table Issue178"); }
+            catch { }
+            try { connection.Execute("create table Issue178(id int not null)"); }
+            catch { }
+            // raw ADO.net
+            using (var sqlCmd = connection.CreateCommand())
+            {
+                sqlCmd.CommandText = sql;
+                using IDataReader reader1 = sqlCmd.ExecuteReader();
+                Assert.True(reader1.Read());
+                Assert.Equal(0, reader1.GetInt32(0));
+                Assert.False(reader1.Read());
+                Assert.False(reader1.NextResult());
+            }
+
+            // dapper
+            using (var reader2 = connection.ExecuteReader(sql))
+            {
+                Assert.True(reader2.Read());
+                Assert.Equal(0, reader2.GetInt32(0));
+                Assert.False(reader2.Read());
+                Assert.False(reader2.NextResult());
+            }
+        }
+
+        [Fact]
+        public void QueryBasicWithoutQuery()
+        {
+            int? i = connection.Query<int?>("print 'not a query'").FirstOrDefault();
+            Assert.Null(i);
+        }
+
+        [Fact]
+        public void QueryComplexWithoutQuery()
+        {
+            var obj = connection.Query<Foo1>("print 'not a query'").FirstOrDefault();
+            Assert.Null(obj);
+        }
+
+        [FactLongRunning]
+        public void Issue263_Timeout()
+        {
+            var watch = Stopwatch.StartNew();
+            var i = connection.Query<int>("waitfor delay '00:01:00'; select 42;", commandTimeout: 300, buffered: false).Single();
+            watch.Stop();
+            Assert.Equal(42, i);
+            var minutes = watch.ElapsedMilliseconds / 1000 / 60;
+            Assert.True(minutes >= 0.95 && minutes <= 1.05);
+        }
+
+        [Fact]
+        public void SO30435185_InvalidTypeOwner()
+        {
+            var ex = Assert.Throws<InvalidOperationException>(() =>
+            {
+                const string sql = @" INSERT INTO #XXX
+                        (XXXId, AnotherId, ThirdId, Value, Comment)
+                        VALUES
+                        (@XXXId, @AnotherId, @ThirdId, @Value, @Comment); select @@rowcount as [Foo]";
+
+                var command = new
+                {
+                    MyModels = new[]
+                    {
+                        new {XXXId = 1, AnotherId = 2, ThirdId = 3, Value = "abc", Comment = "def" }
+                    }
+                };
+                var parameters = command
+                    .MyModels
+                    .Select(model => new
+                    {
+                        XXXId = model.XXXId,
+                        AnotherId = model.AnotherId,
+                        ThirdId = model.ThirdId,
+                        Value = model.Value,
+                        Comment = model.Comment
+                    })
+                    .ToArray();
+
+                var rowcount = (int)connection.Query(sql, parameters).Single().Foo;
+                Assert.Equal(1, rowcount);
+            });
+            Assert.Equal("An enumerable sequence of parameters (arrays, lists, etc) is not allowed in this context", ex.Message);
+        }
+
+        [Fact]
+        public async void SO35470588_WrongValuePidValue()
+        {
+            // nuke, rebuild, and populate the table
+            try { connection.Execute("drop table TPTable"); } catch { }
+            connection.Execute(@"
+create table TPTable (Pid int not null primary key identity(1,1), Value int not null);
+insert TPTable (Value) values (2), (568)");
+
+            // fetch the data using the query in the question, then force to a dictionary
+            var rows = (await connection.QueryAsync<TPTable>("select * from TPTable").ConfigureAwait(false))
+                .ToDictionary(x => x.Pid);
+
+            // check the number of rows
+            Assert.Equal(2, rows.Count);
+
+            // check row 1
+            var row = rows[1];
+            Assert.Equal(1, row.Pid);
+            Assert.Equal(2, row.Value);
+
+            // check row 2
+            row = rows[2];
+            Assert.Equal(2, row.Pid);
+            Assert.Equal(568, row.Value);
+        }
+
+        public class TPTable
+        {
+            public int Pid { get; set; }
+            public int Value { get; set; }
+        }
+
+        [Fact]
+        public void GetOnlyProperties()
+        {
+            var obj = connection.QuerySingle<HazGetOnly>("select 42 as [Id], 'def' as [Name];");
+            Assert.Equal(42, obj.Id);
+            Assert.Equal("def", obj.Name);
+        }
+
+        private class HazGetOnly
+        {
+            public int Id { get; }
+            public string Name { get; } = "abc";
+        }
+
+        [Fact]
+        public void TestConstructorParametersWithUnderscoredColumns()
+        {
+            DefaultTypeMap.MatchNamesWithUnderscores = true;
+            var obj = connection.QuerySingle<HazGetOnlyAndCtor>("select 42 as [id_property], 'def' as [name_property];");
+            Assert.Equal(42, obj.IdProperty);
+            Assert.Equal("def", obj.NameProperty);
+        }
+
+        private class HazGetOnlyAndCtor
+        {
+            public int IdProperty { get; }
+            public string NameProperty { get; }
+
+            public HazGetOnlyAndCtor(int idProperty, string nameProperty)
+            {
+                IdProperty = idProperty;
+                NameProperty = nameProperty;
+            }
+        }
+
+        [Fact]
+        public void Issue1164_OverflowExceptionForByte()
+        {
+            const string sql = "select cast(200 as smallint) as [value]"; // 200 more than sbyte.MaxValue but less than byte.MaxValue 
+            Issue1164Object<byte> obj = connection.QuerySingle<Issue1164Object<byte>>(sql);
+            Assert.StrictEqual(200, obj.Value);
+        }
+
+        [Fact]
+        public void Issue1164_OverflowExceptionForUInt16()
+        {
+            const string sql = "select cast(40000 as bigint) as [value]"; // 40000 more than short.MaxValue but less than ushort.MaxValue 
+            Issue1164Object<ushort> obj = connection.QuerySingle<Issue1164Object<ushort>>(sql);
+            Assert.StrictEqual(40000, obj.Value);
+        }
+
+        [Fact]
+        public void Issue1164_OverflowExceptionForUInt32()
+        {
+            const string sql = "select cast(4000000000 as bigint) as [value]"; // 4000000000 more than int.MaxValue but less than uint.MaxValue 
+            Issue1164Object<uint> obj = connection.QuerySingle<Issue1164Object<uint>>(sql);
+            Assert.StrictEqual(4000000000, obj.Value);
+        }
+
+        [Fact]
+        public void Issue1164_OverflowExceptionForUInt64()
+        {
+            const string sql = "select cast(10000000000000000000.0 as float) as [value]"; // 10000000000000000000 more than long.MaxValue but less than ulong.MaxValue 
+            Issue1164Object<ulong> obj = connection.QuerySingle<Issue1164Object<ulong>>(sql);
+            Assert.StrictEqual(10000000000000000000, obj.Value);
+        }
+
+        private class Issue1164Object<T>
+        {
+            public T Value = default!;
+        }
+
+        internal record struct One(int OID);
+        internal record struct Two(int OID, string Name);
+
+        [Fact]
+        public async Task QuerySplitStruct() // https://github.com/DapperLib/Dapper/issues/2005
+        {
+            var results = await connection.QueryAsync<One, Two, (One,Two)>(@"SELECT 1 AS OID, 2 AS OID, 'Name' AS Name", (x,y) => (x,y), splitOn: "OID");
+
+            Assert.Single(results);
+        }
+
+        [Fact]
+        public void SetDynamicProperty_WithReferenceType_Succeeds()
+        {
+            var obj = connection.QueryFirst("select 1 as ExistingProperty");
+
+            obj.ExistingProperty = "foo";
+            Assert.Equal("foo", (string)obj.ExistingProperty);
+
+            obj.NewProperty = new Uri("http://example.net/");
+            Assert.Equal(new Uri("http://example.net/"), (Uri)obj.NewProperty);
+        }
+
+        [Fact]
+        public void SetDynamicProperty_WithBoxedValueType_Succeeds()
+        {
+            var obj = connection.QueryFirst("select 'foo' as ExistingProperty");
+
+            obj.ExistingProperty = (object)1;
+            Assert.Equal(1, (int)obj.ExistingProperty);
+
+            obj.NewProperty = (object)true;
+            Assert.True(obj.NewProperty);
+        }
+
+        [Fact]
+        public void SetDynamicProperty_WithValueType_Succeeds()
+        {
+            var obj = connection.QueryFirst("select 'foo' as ExistingProperty");
+
+            obj.ExistingProperty = 1;
+            Assert.Equal(1, (int)obj.ExistingProperty);
+
+            obj.NewProperty = true;
+            Assert.True(obj.NewProperty);
+        }
+        */
+    }
+}
+
+#endif
