From bb2bc4705c1a48b309c19312c9a5f735b510f0fb Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 10 Oct 2023 14:38:05 +0100 Subject: [PATCH 1/3] 1. auto-detect stored procedures as anything without whitespace 2. we have public fields? (hangs head in shame) --- Dapper/CommandDefinition.cs | 19 +++++++-- Dapper/DynamicParameters.cs | 2 +- Dapper/PublicAPI.Shipped.txt | 5 +++ Dapper/SqlMapper.Async.cs | 16 +++---- Dapper/SqlMapper.Identity.cs | 62 +++++++++++++++++++++++++++- Dapper/SqlMapper.cs | 48 ++++++++++----------- tests/Dapper.Tests/ProcedureTests.cs | 20 +++++++++ 7 files changed, 135 insertions(+), 37 deletions(-) diff --git a/Dapper/CommandDefinition.cs b/Dapper/CommandDefinition.cs index 6112353e..b6bec7e3 100644 --- a/Dapper/CommandDefinition.cs +++ b/Dapper/CommandDefinition.cs @@ -2,6 +2,7 @@ using System.Data; using System.Reflection; using System.Reflection.Emit; +using System.Text.RegularExpressions; using System.Threading; namespace Dapper @@ -48,10 +49,15 @@ internal void OnCompleted() /// public int? CommandTimeout { get; } + internal readonly CommandType CommandTypeDirect; + /// /// The type of command that the command-text represents /// - public CommandType? CommandType { get; } +#if DEBUG // prevent use in our own code + [Obsolete("Prefer " + nameof(CommandTypeDirect), true)] +#endif + public CommandType? CommandType => CommandTypeDirect; /// /// Should data be buffered before returning? @@ -92,11 +98,17 @@ public CommandDefinition(string commandText, object? parameters = null, IDbTrans Parameters = parameters; Transaction = transaction; CommandTimeout = commandTimeout; - CommandType = commandType; + + // if the sql contains any whitespace character (mostly space, tab, cr/lf): interpret as ad-hoc; but "SomeName" is a stored-proc + // (note TableDirect would need to be specified explicitly, but in reality providers don't usually support TableDirect anyway) + CommandTypeDirect = commandType ?? ((commandText is null || AnyWhitespace.IsMatch(commandText)) ? System.Data.CommandType.Text : System.Data.CommandType.StoredProcedure); + Flags = flags; CancellationToken = cancellationToken; } + private static readonly Regex AnyWhitespace = new Regex(@"\s", RegexOptions.Compiled | RegexOptions.CultureInvariant); + private CommandDefinition(object? parameters) : this() { Parameters = parameters; @@ -124,8 +136,7 @@ internal IDbCommand SetupCommand(IDbConnection cnn, Action? { cmd.CommandTimeout = SqlMapper.Settings.CommandTimeout.Value; } - if (CommandType.HasValue) - cmd.CommandType = CommandType.Value; + cmd.CommandType = CommandTypeDirect; paramReader?.Invoke(cmd, Parameters); return cmd; } diff --git a/Dapper/DynamicParameters.cs b/Dapper/DynamicParameters.cs index d4759a37..f6708b5c 100644 --- a/Dapper/DynamicParameters.cs +++ b/Dapper/DynamicParameters.cs @@ -168,7 +168,7 @@ internal static bool ShouldSetDbType(DbType dbType) /// Information about the query protected void AddParameters(IDbCommand command, SqlMapper.Identity identity) { - var literals = SqlMapper.GetLiteralTokens(identity.sql); + var literals = SqlMapper.GetLiteralTokens(identity.Sql); if (templates is not null) { diff --git a/Dapper/PublicAPI.Shipped.txt b/Dapper/PublicAPI.Shipped.txt index 36cdc1c7..39a8881e 100644 --- a/Dapper/PublicAPI.Shipped.txt +++ b/Dapper/PublicAPI.Shipped.txt @@ -152,12 +152,17 @@ override Dapper.SqlMapper.Identity.ToString() -> string! override Dapper.SqlMapper.StringTypeHandler.Parse(object! value) -> T override Dapper.SqlMapper.StringTypeHandler.SetValue(System.Data.IDbDataParameter! parameter, T? value) -> void readonly Dapper.SqlMapper.Identity.commandType -> System.Data.CommandType? +Dapper.SqlMapper.Identity.CommandType.get -> System.Data.CommandType? readonly Dapper.SqlMapper.Identity.connectionString -> string! readonly Dapper.SqlMapper.Identity.gridIndex -> int +Dapper.SqlMapper.Identity.GridIndex.get -> int readonly Dapper.SqlMapper.Identity.hashCode -> int readonly Dapper.SqlMapper.Identity.parametersType -> System.Type? +Dapper.SqlMapper.Identity.ParametersType.get -> System.Type? readonly Dapper.SqlMapper.Identity.sql -> string! +Dapper.SqlMapper.Identity.Sql.get -> string! readonly Dapper.SqlMapper.Identity.type -> System.Type? +Dapper.SqlMapper.Identity.Type.get -> System.Type? static Dapper.DbString.IsAnsiDefault.get -> bool static Dapper.DbString.IsAnsiDefault.set -> void static Dapper.DefaultTypeMap.MatchNamesWithUnderscores.get -> bool diff --git a/Dapper/SqlMapper.Async.cs b/Dapper/SqlMapper.Async.cs index 07f073ee..6fb8ca4c 100644 --- a/Dapper/SqlMapper.Async.cs +++ b/Dapper/SqlMapper.Async.cs @@ -422,7 +422,7 @@ private static DbCommand TrySetupAsyncCommand(this CommandDefinition command, ID private static async Task> QueryAsync(this IDbConnection cnn, Type effectiveType, CommandDefinition command) { object? param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, effectiveType, param?.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, effectiveType, param?.GetType()); var info = GetCacheInfo(identity, param, command.AddToCache); bool wasClosed = cnn.State == ConnectionState.Closed; var cancel = command.CancellationToken; @@ -477,7 +477,7 @@ private static async Task> QueryAsync(this IDbConnection cnn, private static async Task QueryRowAsync(this IDbConnection cnn, Row row, Type effectiveType, CommandDefinition command) { object? param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, effectiveType, param?.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, effectiveType, param?.GetType()); var info = GetCacheInfo(identity, param, command.AddToCache); bool wasClosed = cnn.State == ConnectionState.Closed; var cancel = command.CancellationToken; @@ -652,7 +652,7 @@ private static async Task ExecuteMultiImplAsync(IDbConnection cnn, CommandD private static async Task ExecuteImplAsync(IDbConnection cnn, CommandDefinition command, object? param) { - var identity = new Identity(command.CommandText, command.CommandType, cnn, null, param?.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, null, param?.GetType()); var info = GetCacheInfo(identity, param, command.AddToCache); bool wasClosed = cnn.State == ConnectionState.Closed; using var cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader); @@ -930,7 +930,7 @@ public static Task> QueryAsync> MultiMapAsync(this IDbConnection cnn, CommandDefinition command, Delegate map, string splitOn) { object? param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, typeof(TFirst), param?.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, typeof(TFirst), param?.GetType()); var info = GetCacheInfo(identity, param, command.AddToCache); bool wasClosed = cnn.State == ConnectionState.Closed; try @@ -979,7 +979,7 @@ private static async Task> MultiMapAsync(this IDbC } object? param = command.Parameters; - var identity = new IdentityWithTypes(command.CommandText, command.CommandType, cnn, types[0], param?.GetType(), types); + var identity = new IdentityWithTypes(command.CommandText, command.CommandTypeDirect, cnn, types[0], param?.GetType(), types); var info = GetCacheInfo(identity, param, command.AddToCache); bool wasClosed = cnn.State == ConnectionState.Closed; try @@ -1029,7 +1029,7 @@ public static Task QueryMultipleAsync(this IDbConnection cnn, string public static async Task QueryMultipleAsync(this IDbConnection cnn, CommandDefinition command) { object? param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, typeof(GridReader), param?.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, typeof(GridReader), param?.GetType()); CacheInfo info = GetCacheInfo(identity, param, command.AddToCache); DbCommand? cmd = null; @@ -1227,7 +1227,7 @@ private static async Task ExecuteWrappedReaderImplAsync(IDbConnect object? param = command.Parameters; if (param is not null) { - var identity = new Identity(command.CommandText, command.CommandType, cnn, null, param.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, null, param.GetType()); paramReader = GetCacheInfo(identity, command.Parameters, command.AddToCache).ParamReader; } @@ -1296,7 +1296,7 @@ static async IAsyncEnumerable Impl(IDbConnection cnn, Type effectiveType, Com [EnumeratorCancellation] CancellationToken cancel) { object? param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, effectiveType, param?.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, effectiveType, param?.GetType()); var info = GetCacheInfo(identity, param, command.AddToCache); bool wasClosed = cnn.State == ConnectionState.Closed; using var cmd = command.TrySetupAsyncCommand(cnn, info.ParamReader); diff --git a/Dapper/SqlMapper.Identity.cs b/Dapper/SqlMapper.Identity.cs index cfaab8e5..4871d9c0 100644 --- a/Dapper/SqlMapper.Identity.cs +++ b/Dapper/SqlMapper.Identity.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Data; using System.Runtime.CompilerServices; @@ -92,6 +93,7 @@ public class Identity : IEquatable internal virtual Type GetType(int index) => throw new IndexOutOfRangeException(nameof(index)); +#pragma warning disable CS0618 // Type or member is obsolete internal Identity ForGrid(Type primaryType, int gridIndex) => new Identity(sql, commandType, connectionString, primaryType, parametersType, gridIndex); @@ -110,12 +112,14 @@ internal Identity ForGrid(Type primaryType, Type[] otherTypes, int gridIndex) => /// public Identity ForDynamicParameters(Type type) => new Identity(sql, commandType, connectionString, this.type, type, 0, -1); +#pragma warning restore CS0618 // Type or member is obsolete internal Identity(string sql, CommandType? commandType, IDbConnection connection, Type? type, Type? parametersType) : this(sql, commandType, connection.ConnectionString, type, parametersType, 0, 0) { /* base call */ } private protected Identity(string sql, CommandType? commandType, string connectionString, Type? type, Type? parametersType, int otherTypesHash, int gridIndex) { +#pragma warning disable CS0618 // Type or member is obsolete this.sql = sql; this.commandType = commandType; this.connectionString = connectionString; @@ -133,6 +137,7 @@ private protected Identity(string sql, CommandType? commandType, string connecti hashCode = (hashCode * 23) + (connectionString is null ? 0 : connectionStringComparer.GetHashCode(connectionString)); hashCode = (hashCode * 23) + (parametersType?.GetHashCode() ?? 0); } +#pragma warning restore CS0618 // Type or member is obsolete } /// @@ -144,48 +149,101 @@ private protected Identity(string sql, CommandType? commandType, string connecti /// /// The raw SQL command. /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Please use " + nameof(Sql) + ". This API may be removed at a later date.")] public readonly string sql; + /// + /// The raw SQL command. + /// +#pragma warning disable CS0618 // Type or member is obsolete + public string Sql => sql; +#pragma warning restore CS0618 // Type or member is obsolete + /// /// The SQL command type. /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Please use " + nameof(CommandType) + ". This API may be removed at a later date.")] public readonly CommandType? commandType; + /// + /// The SQL command type. + /// +#pragma warning disable CS0618 // Type or member is obsolete + public CommandType? CommandType => commandType; +#pragma warning restore CS0618 // Type or member is obsolete + /// /// The hash code of this Identity. /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Please use " + nameof(GetHashCode) + ". This API may be removed at a later date.")] public readonly int hashCode; /// /// The grid index (position in the reader) of this Identity. /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Please use " + nameof(GridIndex) + ". This API may be removed at a later date.")] public readonly int gridIndex; /// - /// This of this Identity. + /// The grid index (position in the reader) of this Identity. /// +#pragma warning disable CS0618 // Type or member is obsolete + public int GridIndex => gridIndex; +#pragma warning restore CS0618 // Type or member is obsolete + + /// + /// The of this Identity. + /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Please use " + nameof(Type) + ". This API may be removed at a later date.")] public readonly Type? type; + /// + /// The of this Identity. + /// +#pragma warning disable CS0618 // Type or member is obsolete + public Type? Type => type; +#pragma warning restore CS0618 // Type or member is obsolete + /// /// The connection string for this Identity. /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("This API may be removed at a later date.")] public readonly string connectionString; /// /// The type of the parameters object for this Identity. /// + [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] + [Obsolete("Please use " + nameof(ParametersType) + ". This API may be removed at a later date.")] public readonly Type? parametersType; + /// + /// The type of the parameters object for this Identity. + /// +#pragma warning disable CS0618 // Type or member is obsolete + public Type? ParametersType => parametersType; +#pragma warning restore CS0618 // Type or member is obsolete + /// /// Gets the hash code for this identity. /// /// +#pragma warning disable CS0618 // Type or member is obsolete public override int GetHashCode() => hashCode; +#pragma warning restore CS0618 // Type or member is obsolete /// /// See object.ToString() /// +#pragma warning disable CS0618 // Type or member is obsolete public override string ToString() => sql; +#pragma warning restore CS0618 // Type or member is obsolete /// /// Compare 2 Identity objects @@ -198,6 +256,7 @@ public bool Equals(Identity? other) if (other is null) return false; int typeCount; +#pragma warning disable CS0618 // Type or member is obsolete return gridIndex == other.gridIndex && type == other.type && sql == other.sql @@ -206,6 +265,7 @@ public bool Equals(Identity? other) && parametersType == other.parametersType && (typeCount = TypeCount) == other.TypeCount && (typeCount == 0 || TypesEqual(this, other, typeCount)); +#pragma warning restore CS0618 // Type or member is obsolete } [MethodImpl(MethodImplOptions.NoInlining)] diff --git a/Dapper/SqlMapper.cs b/Dapper/SqlMapper.cs index 5f5974f7..b9f8b09d 100644 --- a/Dapper/SqlMapper.cs +++ b/Dapper/SqlMapper.cs @@ -115,7 +115,7 @@ private static void PurgeQueryCacheByType(Type type) { foreach (var entry in _queryCache) { - if (entry.Key.type == type) + if (entry.Key.Type == type) _queryCache.TryRemove(entry.Key, out _); } TypeDeserializerCache.Purge(type); @@ -137,7 +137,9 @@ public static int GetCachedSQLCount() /// public static IEnumerable> GetCachedSQL(int ignoreHitCountAbove = int.MaxValue) { - var data = _queryCache.Select(pair => Tuple.Create(pair.Key.connectionString, pair.Key.sql, pair.Value.GetHitCount())); +#pragma warning disable CS0618 // Type or member is obsolete + var data = _queryCache.Select(pair => Tuple.Create(pair.Key.connectionString, pair.Key.Sql, pair.Value.GetHitCount())); +#pragma warning restore CS0618 // Type or member is obsolete return (ignoreHitCountAbove < int.MaxValue) ? data.Where(tuple => tuple.Item3 <= ignoreHitCountAbove) : data; @@ -146,19 +148,19 @@ public static IEnumerable> GetCachedSQL(int ignoreHit /// /// Deep diagnostics only: find any hash collisions in the cache /// - /// - public static IEnumerable> GetHashCollissions() + public static IEnumerable> GetHashCollissions() // legacy incorrect spelling, oops { var counts = new Dictionary(); foreach (var key in _queryCache.Keys) { - if (!counts.TryGetValue(key.hashCode, out int count)) + var hash = key.GetHashCode(); + if (!counts.TryGetValue(hash, out int count)) { - counts.Add(key.hashCode, 1); + counts.Add(hash, 1); } else { - counts[key.hashCode] = count + 1; + counts[hash] = count + 1; } } return from pair in counts @@ -648,7 +650,7 @@ private static int ExecuteImpl(this IDbConnection cnn, ref CommandDefinition com // nice and simple if (param is not null) { - identity = new Identity(command.CommandText, command.CommandType, cnn, null, param.GetType()); + 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); @@ -1108,7 +1110,7 @@ public static GridReader QueryMultiple(this IDbConnection cnn, CommandDefinition private static GridReader QueryMultipleImpl(this IDbConnection cnn, ref CommandDefinition command) { object? param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, typeof(GridReader), param?.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, typeof(GridReader), param?.GetType()); CacheInfo info = GetCacheInfo(identity, param, command.AddToCache); IDbCommand? cmd = null; @@ -1167,7 +1169,7 @@ private static DbDataReader ExecuteReaderWithFlagsFallback(IDbCommand cmd, bool private static IEnumerable QueryImpl(this IDbConnection cnn, CommandDefinition command, Type effectiveType) { object? param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, effectiveType, param?.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, effectiveType, param?.GetType()); var info = GetCacheInfo(identity, param, command.AddToCache); IDbCommand? cmd = null; @@ -1260,7 +1262,7 @@ private static void ThrowZeroRows(Row row) private static T QueryRowImpl(IDbConnection cnn, Row row, ref CommandDefinition command, Type effectiveType) { object? param = command.Parameters; - var identity = new Identity(command.CommandText, command.CommandType, cnn, effectiveType, param?.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, effectiveType, param?.GetType()); var info = GetCacheInfo(identity, param, command.AddToCache); IDbCommand? cmd = null; @@ -1549,7 +1551,7 @@ private static IEnumerable MultiMap MultiMapImpl(this IDbConnection? cnn, CommandDefinition command, Delegate map, string splitOn, DbDataReader? reader, Identity? identity, bool finalize) { object? param = command.Parameters; - identity ??= new Identity(command.CommandText, command.CommandType, cnn!, typeof(TFirst), param?.GetType()); + identity ??= new Identity(command.CommandText, command.CommandTypeDirect, cnn!, typeof(TFirst), param?.GetType()); CacheInfo cinfo = GetCacheInfo(identity, param, command.AddToCache); IDbCommand? ownedCommand = null; @@ -1620,7 +1622,7 @@ private static IEnumerable MultiMapImpl(this IDbConnection? cn } object? param = command.Parameters; - identity ??= new IdentityWithTypes(command.CommandText, command.CommandType, cnn!, types[0], param?.GetType(), types); + identity ??= new IdentityWithTypes(command.CommandText, command.CommandTypeDirect, cnn!, types[0], param?.GetType(), types); CacheInfo cinfo = GetCacheInfo(identity, param, command.AddToCache); IDbCommand? ownedCommand = null; @@ -1825,7 +1827,7 @@ private static CacheInfo GetCacheInfo(Identity identity, object? exampleParamete throw new InvalidOperationException("An enumerable sequence of parameters (arrays, lists, etc) is not allowed in this context"); } info = new CacheInfo(); - if (identity.parametersType is not null) + if (identity.ParametersType is not null) { Action reader; if (exampleParameters is IDynamicParameters) @@ -1842,10 +1844,10 @@ private static CacheInfo GetCacheInfo(Identity identity, object? exampleParamete } else { - var literals = GetLiteralTokens(identity.sql); + var literals = GetLiteralTokens(identity.Sql); reader = CreateParamInfoGenerator(identity, false, true, literals); } - if ((identity.commandType is null || identity.commandType == CommandType.Text) && ShouldPassByPosition(identity.sql)) + if ((identity.CommandType is null || identity.CommandType == CommandType.Text) && ShouldPassByPosition(identity.Sql)) { var tail = reader; reader = (cmd, obj) => @@ -2517,7 +2519,7 @@ internal static IList GetLiteralTokens(string sql) /// Whether to check for duplicates. /// Whether to remove unused parameters. public static Action CreateParamInfoGenerator(Identity identity, bool checkForDuplicates, bool removeUnused) => - CreateParamInfoGenerator(identity, checkForDuplicates, removeUnused, GetLiteralTokens(identity.sql)); + CreateParamInfoGenerator(identity, checkForDuplicates, removeUnused, GetLiteralTokens(identity.Sql)); private static bool IsValueTuple(Type? type) => (type?.IsValueType == true && type.FullName?.StartsWith("System.ValueTuple`", StringComparison.Ordinal) == true) @@ -2525,7 +2527,7 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true internal static Action CreateParamInfoGenerator(Identity identity, bool checkForDuplicates, bool removeUnused, IList literals) { - Type type = identity.parametersType!; + Type type = identity.ParametersType!; if (IsValueTuple(type)) { @@ -2533,9 +2535,9 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true } bool filterParams = false; - if (removeUnused && identity.commandType.GetValueOrDefault(CommandType.Text) == CommandType.Text) + if (removeUnused && identity.CommandType.GetValueOrDefault(CommandType.Text) == CommandType.Text) { - filterParams = !smellsLikeOleDb.IsMatch(identity.sql); + filterParams = !smellsLikeOleDb.IsMatch(identity.Sql); } var dm = new DynamicMethod("ParamInfo" + Guid.NewGuid().ToString(), null, new[] { typeof(IDbCommand), typeof(object) }, type, true); @@ -2628,7 +2630,7 @@ private static bool IsValueTuple(Type? type) => (type?.IsValueType == true } if (filterParams) { - props = FilterParameters(props, identity.sql); + props = FilterParameters(props, identity.Sql); } var callOpCode = isStruct ? OpCodes.Call : OpCodes.Callvirt; @@ -2971,7 +2973,7 @@ private static int ExecuteCommand(IDbConnection cnn, ref CommandDefinition comma object? param = command.Parameters; if (param is not null) { - var identity = new Identity(command.CommandText, command.CommandType, cnn, null, param.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, null, param.GetType()); paramReader = GetCacheInfo(identity, command.Parameters, command.AddToCache).ParamReader; } @@ -3033,7 +3035,7 @@ private static DbDataReader ExecuteReaderImpl(IDbConnection cnn, ref CommandDefi // nice and simple if (param is not null) { - var identity = new Identity(command.CommandText, command.CommandType, cnn, null, param.GetType()); + var identity = new Identity(command.CommandText, command.CommandTypeDirect, cnn, null, param.GetType()); info = GetCacheInfo(identity, param, command.AddToCache); } var paramReader = info?.ParamReader; diff --git a/tests/Dapper.Tests/ProcedureTests.cs b/tests/Dapper.Tests/ProcedureTests.cs index 1bd56304..9d76f44b 100644 --- a/tests/Dapper.Tests/ProcedureTests.cs +++ b/tests/Dapper.Tests/ProcedureTests.cs @@ -92,6 +92,26 @@ @ErrorDescription varchar(255) OUTPUT Assert.Equal("Completed successfully", p.Get("ErrorDescription")); } + [Theory] + [InlineData(CommandType.StoredProcedure)] + [InlineData(null)] // auto + public void InferProcedure(CommandType? commandType) + { + connection.Execute("CREATE PROCEDURE #InferProcedure @id int AS BEGIN SELECT -@id END"); + var result = connection.QuerySingle("#InferProcedure", new { id = 42 }, commandType: commandType); + Assert.Equal(-42, result); + } + + [Theory] + [InlineData(CommandType.Text)] + [InlineData(null)] // auto + public void InferNotProcedure(CommandType? commandType) + { + connection.Execute("CREATE PROCEDURE #InferNotProcedure @id int AS BEGIN SELECT -@id END"); + var result = connection.QuerySingle("EXEC #InferNotProcedure @id", new { id = 42 }, commandType: commandType); + Assert.Equal(-42, result); + } + [Fact] public void SO24605346_ProcsAndStrings() { From de731abe02c514b857750cb43e21e18db4bf636c Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 10 Oct 2023 14:40:42 +0100 Subject: [PATCH 2/3] release notes --- docs/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/index.md b/docs/index.md index 9ebb9efb..25a22caf 100644 --- a/docs/index.md +++ b/docs/index.md @@ -24,6 +24,8 @@ Note: to get the latest pre-release build, add ` -Pre` to the end of the command (note: new PRs will not be merged until they add release note wording here) +- infer command text without any whitespace as stored-procedure (#1975 via @mgravell) + ### 2.1.4 - add untyped `GridReader.ReadUnbufferedAsync` API (#1958 via @mgravell) From 78e1fccd7cf9c6550513f5e919a844b082995eb6 Mon Sep 17 00:00:00 2001 From: Marc Gravell Date: Tue, 10 Oct 2023 14:47:41 +0100 Subject: [PATCH 3/3] no need to use regex --- Dapper/CommandDefinition.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Dapper/CommandDefinition.cs b/Dapper/CommandDefinition.cs index b6bec7e3..1820d86f 100644 --- a/Dapper/CommandDefinition.cs +++ b/Dapper/CommandDefinition.cs @@ -2,7 +2,6 @@ using System.Data; using System.Reflection; using System.Reflection.Emit; -using System.Text.RegularExpressions; using System.Threading; namespace Dapper @@ -98,16 +97,20 @@ public CommandDefinition(string commandText, object? parameters = null, IDbTrans Parameters = parameters; Transaction = transaction; CommandTimeout = commandTimeout; - - // if the sql contains any whitespace character (mostly space, tab, cr/lf): interpret as ad-hoc; but "SomeName" is a stored-proc - // (note TableDirect would need to be specified explicitly, but in reality providers don't usually support TableDirect anyway) - CommandTypeDirect = commandType ?? ((commandText is null || AnyWhitespace.IsMatch(commandText)) ? System.Data.CommandType.Text : System.Data.CommandType.StoredProcedure); - + CommandTypeDirect = commandType ?? InferCommandType(commandText); Flags = flags; CancellationToken = cancellationToken; + + static CommandType InferCommandType(string sql) + { + if (sql is null || sql.IndexOfAny(WhitespaceChars) >= 0) return System.Data.CommandType.Text; + return System.Data.CommandType.StoredProcedure; + } } - private static readonly Regex AnyWhitespace = new Regex(@"\s", RegexOptions.Compiled | RegexOptions.CultureInvariant); + // if the sql contains any whitespace character (space/tab/cr/lf): interpret as ad-hoc; but "SomeName" should be treated as a stored-proc + // (note TableDirect would need to be specified explicitly, but in reality providers don't usually support TableDirect anyway) + private static readonly char[] WhitespaceChars = new char[] { ' ', '\t', '\r', '\n' }; private CommandDefinition(object? parameters) : this() {