From 29a6c83efe99be0700d88ba570932746c18af837 Mon Sep 17 00:00:00 2001 From: Marc Gravell 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 @@ Dapper (Strong Named) A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc. Major Sponsor: Dapper Plus from ZZZ Projects. Sam Saffron;Marc Gravell;Nick Craver - net461;netstandard2.0;net5.0;net7.0 + net461;netstandard2.0;net6.0;net8.0 true true enable 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; + } + /// /// For asynchronous operations, the cancellation-token /// 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 @@ orm;sql;micro-orm A high performance Micro-ORM supporting SQL Server, MySQL, Sqlite, SqlCE, Firebird etc. Major Sponsor: Dapper Plus from ZZZ Projects. Sam Saffron;Marc Gravell;Nick Craver - net461;netstandard2.0;net5.0;net6.0;net7.0 + net461;netstandard2.0;net6.0;net8.0;net9.0 enable + 13 true 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 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(this System.Data.IDbConnection! connection, ref Dapper.SqlMapper.SqlBuilder sql, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null) -> T? +static Dapper.SqlMapper.Query(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! +static Dapper.SqlMapper.QueryBuffered(this System.Data.IDbConnection! connection, ref Dapper.SqlMapper.SqlBuilder sql, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null) -> System.Collections.Generic.List! +static Dapper.SqlMapper.QueryUnbuffered(this System.Data.IDbConnection! connection, ref Dapper.SqlMapper.SqlBuilder sql, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null) -> System.Collections.Generic.IEnumerable! \ 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 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(this System.Data.IDbConnection! connection, ref Dapper.SqlMapper.SqlBuilder sql, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null) -> T? +static Dapper.SqlMapper.Query(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! +static Dapper.SqlMapper.QueryBuffered(this System.Data.IDbConnection! connection, ref Dapper.SqlMapper.SqlBuilder sql, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null) -> System.Collections.Generic.List! +static Dapper.SqlMapper.QueryUnbuffered(this System.Data.IDbConnection! connection, ref Dapper.SqlMapper.SqlBuilder sql, System.Data.IDbTransaction? transaction = null, int? commandTimeout = null) -> System.Collections.Generic.IEnumerable! 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 QueryRowAsync(this IDbConnection cnn, Row row, T T result = default!; if (await reader.ReadAsync(cancel).ConfigureAwait(false) && reader.FieldCount != 0) { - result = ReadRow(info, identity, ref command, effectiveType, reader); + result = ReadRow(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 ExecuteReaderAsync(this DbConnection cnn, Comma private static async Task ExecuteWrappedReaderImplAsync(IDbConnection cnn, CommandDefinition command, CommandBehavior commandBehavior) { - Action? paramReader = GetParameterReader(cnn, ref command); + Action? 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 +{ + /// + [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); + + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous")] + public static IEnumerable QueryUnbuffered( + this IDbConnection connection, + [InterpolatedStringHandlerArgument("connection")] ref SqlBuilder sql, + IDbTransaction? transaction = null, + int? commandTimeout = null) + { + var cmd = sql.GetCommandAndReset(transaction, commandTimeout); + return QueryImpl(connection, new(cmd), typeof(T), cmd); + } + + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous")] + public static IEnumerable Query( + 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(connection, new(cmd, buffered ? CommandFlags.Buffered : CommandFlags.None), typeof(T), cmd); + return buffered ? result.ToList() : result; + } + + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous")] + public static List QueryBuffered( + this IDbConnection connection, + [InterpolatedStringHandlerArgument("connection")] ref SqlBuilder sql, + IDbTransaction? transaction = null, + int? commandTimeout = null) + { + var cmd = sql.GetCommandAndReset(transaction, commandTimeout); + return QueryImpl(connection, new(cmd, CommandFlags.Buffered), typeof(T), cmd).ToList(); + } + + /// + [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(connection, new(cmd), cmd); + } + + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous")] + public static T? ExecuteScalar( + this IDbConnection connection, + [InterpolatedStringHandlerArgument("connection")] ref SqlBuilder sql, + IDbTransaction? transaction = null, + int? commandTimeout = null) + { + var cmd = sql.GetCommandAndReset(transaction, commandTimeout); + return ExecuteScalarImpl(connection, new(cmd), cmd); + } + + /// + /// Allows efficient construction of SQL, intended for use with interpolated string literals. + /// + [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 + + /// + /// Create a new builder instance. + /// + 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; + } + + /// + /// Returns the for this operation with the set to the final SQL. + /// + 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>(); + 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; + } + + /// + 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 _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 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; + } + + /// + [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 + } + } + + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Not ambiguous")] + public void AppendFormatted(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 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; + } + + /// + 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); } /// @@ -562,7 +562,7 @@ public static int Execute(this IDbConnection cnn, string sql, object? param = nu /// The connection to execute on. /// The command to execute on this connection. /// The number of rows affected. - 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); /// /// 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(cnn, ref command); + return ExecuteScalarImpl(cnn, in command); } /// @@ -594,7 +594,7 @@ public static int Execute(this IDbConnection cnn, string sql, object? param = nu public static T? 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(cnn, ref command); + return ExecuteScalarImpl(cnn, in command); } /// @@ -604,7 +604,7 @@ public static int Execute(this IDbConnection cnn, string sql, object? param = nu /// The command to execute. /// The first cell selected as . public static object? ExecuteScalar(this IDbConnection cnn, CommandDefinition command) => - ExecuteScalarImpl(cnn, ref command); + ExecuteScalarImpl(cnn, in command); /// /// Execute parameterized SQL that selects a single value. @@ -614,7 +614,7 @@ public static int Execute(this IDbConnection cnn, string sql, object? param = nu /// The command to execute. /// The first cell selected as . public static T? ExecuteScalar(this IDbConnection cnn, CommandDefinition command) => - ExecuteScalarImpl(cnn, ref command); + ExecuteScalarImpl(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); } /// @@ -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 /// 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 /// 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 Query(this IDbConnection cnn, string sql, object public static T QueryFirst(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(cnn, Row.First, ref command, typeof(T)); + return QueryRowImpl(cnn, Row.First, in command, typeof(T)); } /// @@ -883,7 +883,7 @@ public static T QueryFirst(this IDbConnection cnn, string sql, object? param public static T? QueryFirstOrDefault(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(cnn, Row.FirstOrDefault, ref command, typeof(T)); + return QueryRowImpl(cnn, Row.FirstOrDefault, in command, typeof(T)); } /// @@ -904,7 +904,7 @@ public static T QueryFirst(this IDbConnection cnn, string sql, object? param public static T QuerySingle(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(cnn, Row.Single, ref command, typeof(T)); + return QueryRowImpl(cnn, Row.Single, in command, typeof(T)); } /// @@ -925,7 +925,7 @@ public static T QuerySingle(this IDbConnection cnn, string sql, object? param public static T? QuerySingleOrDefault(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(cnn, Row.SingleOrDefault, ref command, typeof(T)); + return QueryRowImpl(cnn, Row.SingleOrDefault, in command, typeof(T)); } /// @@ -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(cnn, Row.First, ref command, type); + return QueryRowImpl(cnn, Row.First, in command, type); } /// @@ -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(cnn, Row.FirstOrDefault, ref command, type); + return QueryRowImpl(cnn, Row.FirstOrDefault, in command, type); } /// @@ -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(cnn, Row.Single, ref command, type); + return QueryRowImpl(cnn, Row.Single, in command, type); } /// @@ -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(cnn, Row.SingleOrDefault, ref command, type); + return QueryRowImpl(cnn, Row.SingleOrDefault, in command, type); } /// @@ -1072,7 +1072,7 @@ public static IEnumerable Query(this IDbConnection cnn, CommandDefinition /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). /// public static T QueryFirst(this IDbConnection cnn, CommandDefinition command) => - QueryRowImpl(cnn, Row.First, ref command, typeof(T)); + QueryRowImpl(cnn, Row.First, in command, typeof(T)); /// /// Executes a query, returning the data typed as . @@ -1085,7 +1085,7 @@ public static T QueryFirst(this IDbConnection cnn, CommandDefinition command) /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). /// public static T? QueryFirstOrDefault(this IDbConnection cnn, CommandDefinition command) => - QueryRowImpl(cnn, Row.FirstOrDefault, ref command, typeof(T)); + QueryRowImpl(cnn, Row.FirstOrDefault, in command, typeof(T)); /// /// Executes a query, returning the data typed as . @@ -1098,7 +1098,7 @@ public static T QueryFirst(this IDbConnection cnn, CommandDefinition command) /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). /// public static T QuerySingle(this IDbConnection cnn, CommandDefinition command) => - QueryRowImpl(cnn, Row.Single, ref command, typeof(T)); + QueryRowImpl(cnn, Row.Single, in command, typeof(T)); /// /// Executes a query, returning the data typed as . @@ -1111,7 +1111,7 @@ public static T QuerySingle(this IDbConnection cnn, CommandDefinition command /// created per row, and a direct column-name===member-name mapping is assumed (case insensitive). /// public static T? QuerySingleOrDefault(this IDbConnection cnn, CommandDefinition command) => - QueryRowImpl(cnn, Row.SingleOrDefault, ref command, typeof(T)); + QueryRowImpl(cnn, Row.SingleOrDefault, in command, typeof(T)); /// /// Execute a command that returns multiple result sets, and access each in turn. @@ -1125,7 +1125,7 @@ public static T QuerySingle(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); } /// @@ -1134,9 +1134,9 @@ public static GridReader QueryMultiple(this IDbConnection cnn, string sql, objec /// The connection to query on. /// The command to execute for this query. 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 QueryImpl(this IDbConnection cnn, CommandDefinition command, Type effectiveType) + private static IEnumerable QueryImpl(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(IDbConnection cnn, Row row, ref CommandDefinition command, Type effectiveType) + private static T QueryRowImpl(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(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(info, identity, ref command, effectiveType, reader); + result = ReadRow(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(IDbConnection cnn, Row row, ref CommandDefiniti /// Shared value deserialization path for QueryRowImpl and QueryRowAsync /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static T ReadRow(CacheInfo info, Identity identity, ref CommandDefinition command, Type effectiveType, DbDataReader reader) + private static T ReadRow(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? paramReader) + private static int ExecuteCommand(IDbConnection cnn, in CommandDefinition command, Action? 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(IDbConnection cnn, ref CommandDefinition command) + private static T? ExecuteScalarImpl(IDbConnection cnn, in CommandDefinition command, IDbCommand? cmd = null) { Action? 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(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? paramReader = GetParameterReader(cnn, ref command); + Action? 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? GetParameterReader(IDbConnection cnn, ref CommandDefinition command) + private static Action? 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 { } +#if MSSQLCLIENT + [Collection("MiscTests")] + public sealed class MicrosoftSqlClientSqlBuilderLiteralTests : SqlBuilderLiteralTests { } +#endif + public abstract class SqlBuilderLiteralTests : TestBase where TProvider : DatabaseProvider + { + [Fact] + public void TestNullableGuidSupport() + { + var guid = connection.Query("select null").First(); + Assert.Null(guid); + + guid = Guid.NewGuid(); + var guid2 = connection.Query($"select @{guid}").First(); + Assert.Equal(guid, guid2); + } + + [Fact] + public void TestNonNullableGuidSupport() + { + var guid = Guid.NewGuid(); + var guid2 = connection.Query($"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($"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("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($""" + SELECT * FROM #users16726709 + WHERE (first_name LIKE CONCAT(@{term}, '%') OR last_name LIKE CONCAT(@{term}, '%')); + """).Count()); + Assert.Equal(3, connection.Query($""" + 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($"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($"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("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("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("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(2) + { + new Student{Age = 1, Name = "sam"}, + new Student{Age = 2, Name = "bob"} + }); + int sum = connection.Query("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("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("select 10 as [Internal]").First()._internal); + } + + [Fact] + public void TestSetPrivate() + { + Assert.Equal(10, connection.Query("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("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 enumerated + en = connection.Query("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("select @foo", new { foo }).Single(); + Assert.Equal(foo, result); + } + + [Fact] + public void TestBigIntMember() + { + const long foo = 12345; + var result = connection.Query(@" +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( + "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("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(() => 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; + Assert.NotNull(row); + Assert.Equal(1, row["A"]); + Assert.Equal("two", row["B"]); + } + + [Fact] + public void TestDapperSetsPrivates() + { + Assert.Equal(1, connection.Query("select 'one' ShadowInDB").First().Shadow); + + Assert.Equal(1, connection.QueryFirstOrDefault("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("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("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("select @c", new { c = test }).Single(); + + Assert.Equal(test, c); + + var obj = connection.Query("select @Value as Value", new WithCharValue { Value = c }).Single(); + + Assert.Equal(test, obj.Value); + } + + [Fact] + public void TestNullableCharInputAndOutputNonNull() + { + char? test = '〠'; + char? c = connection.Query("select @c", new { c = test }).Single(); + + Assert.Equal(c, test); + + var obj = connection.Query("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("select @c", new { c = test }).Single(); + + Assert.Equal(c, test); + + var obj = connection.Query("select @ValueNullable as ValueNullable", new WithCharValue { ValueNullable = c }).Single(); + + Assert.Equal(obj.ValueNullable, test); + } + + [Fact] + public void WorkDespiteHavingWrongStructColumnTypes() + { + var hazInt = connection.Query("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("select cast(42 as smallint)").Single()); + Assert.Equal((short?)42, connection.Query("select cast(42 as smallint)").Single()); + Assert.Equal((short?)null, connection.Query("select cast(null as smallint)").Single()); + + Assert.Equal((ShortEnum)42, connection.Query("select cast(42 as smallint)").Single()); + Assert.Equal((ShortEnum?)42, connection.Query("select cast(42 as smallint)").Single()); + Assert.Equal((ShortEnum?)null, connection.Query("select cast(null as smallint)").Single()); + + var row = + connection.Query( + "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( + "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("select cast(42 as int)").Single()); + Assert.Equal((int?)42, connection.Query("select cast(42 as int)").Single()); + Assert.Equal((int?)null, connection.Query("select cast(null as int)").Single()); + + Assert.Equal((IntEnum)42, connection.Query("select cast(42 as int)").Single()); + Assert.Equal((IntEnum?)42, connection.Query("select cast(42 as int)").Single()); + Assert.Equal((IntEnum?)null, connection.Query("select cast(null as int)").Single()); + + var row = + connection.Query( + "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( + "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("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(() => conn.Execute("nop")); + Assert.Equal(ConnectionState.Closed, conn.State); + } + + [Fact] + public void QueryFromClosed() + { + using var conn = GetClosedConnection(); + var i = conn.Query("select 1").Single(); + Assert.Equal(ConnectionState.Closed, conn.State); + Assert.Equal(1, i); + } + + [Fact] + public void QueryInvalidFromClosed() + { + using var conn = GetClosedConnection(); + Assert.ThrowsAny(() => conn.Query("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 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( + "SELECT 1 Id, 'Mr' Title, 'John' Surname, 4 AddressCount", + (person, addressCount) => person, + splitOn: "AddressCount" + ).First(); + + var asDict = (IDictionary)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( // case with rows + "select DATEDIFF(day, GETUTCDATE(), @date)", new { date = DateTime.UtcNow.AddDays(20) }) + .SingleOrDefault(); + Assert.Equal(20, result); + + result = connection.Query( // 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(), 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("select datalength(@x)", + new { x = new DbString { Value = "abc", IsAnsi = true } }).Single(); + var b = connection.Query("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(sql).Single().Value; + Assert.Equal(42, i); + + i = connection.Query(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 template, string query, object args) + { + return connection.Query(query, args).Single(); + } + + [Fact] + public void Issue22_ExecuteScalar() + { + int i = connection.ExecuteScalar("select 123"); + Assert.Equal(123, i); + + i = connection.ExecuteScalar("select cast(123 as bigint)"); + Assert.Equal(123, i); + + long j = connection.ExecuteScalar("select 123"); + Assert.Equal(123L, j); + + j = connection.ExecuteScalar("select cast(123 as bigint)"); + Assert.Equal(123L, j); + + int? k = connection.ExecuteScalar("select @i", new { i = default(int?) }); + Assert.Null(k); + } + + [Fact] + public void Issue142_FailsNamedStatus() + { + var row1 = connection.Query("select @Status as [Status]", new { Status = StatusType.Started }).Single(); + Assert.Equal(StatusType.Started, row1.Status); + + var row2 = connection.Query("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("print 'not a query'").FirstOrDefault(); + Assert.Null(i); + } + + [Fact] + public void QueryComplexWithoutQuery() + { + var obj = connection.Query("print 'not a query'").FirstOrDefault(); + Assert.Null(obj); + } + + [FactLongRunning] + public void Issue263_Timeout() + { + var watch = Stopwatch.StartNew(); + var i = connection.Query("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(() => + { + 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("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("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("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 obj = connection.QuerySingle>(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 obj = connection.QuerySingle>(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 obj = connection.QuerySingle>(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 obj = connection.QuerySingle>(sql); + Assert.StrictEqual(10000000000000000000, obj.Value); + } + + private class Issue1164Object + { + 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(@"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