From a7ea83b665d7f148a1268dba302c5a109d268e76 Mon Sep 17 00:00:00 2001 From: electricessence <5899455+electricessence@users.noreply.github.com> Date: Tue, 10 Nov 2026 15:47:19 -0800 Subject: [PATCH 1/3] ExpressiveDbCommand.RetrieveAsync takes a CancellationToken; misnamed parameters renamed (TConn, commandText, procedureName) The three RetrieveAsync overloads that can take a trailing token do; the two params overloads are unchanged. CreateTextCommand(SqlTransaction) in both SQL packages takes commandText, CreateStoredProcedureCommand(IDbConnection) takes procedureName, and the Using/UsingAsync type parameter TConn is TConnection. Co-Authored-By: Claude Sonnet 5.5 --- Source/Core/ExpressiveDbCommandBase.cs | 64 +++++++++++-- .../Extensions/Connection.CreateCommand.cs | 10 +- .../Extensions/ConnectionProvider.Using.cs | 96 +++++++++---------- Source/Core/PublicAPI.Unshipped.txt | 32 +++++++ .../Extensions/Transaction.CreateCommand.cs | 10 +- .../Extensions/Transaction.CreateCommand.cs | 10 +- 6 files changed, 151 insertions(+), 71 deletions(-) diff --git a/Source/Core/ExpressiveDbCommandBase.cs b/Source/Core/ExpressiveDbCommandBase.cs index 74c09f0b..3719cda9 100644 --- a/Source/Core/ExpressiveDbCommandBase.cs +++ b/Source/Core/ExpressiveDbCommandBase.cs @@ -274,19 +274,31 @@ public ValueTask> FirstOrdinalResultsAsync() /// /// Asynchronously iterates all records within the current result set using an IDataReader and returns the desired results. /// + /// + /// A parameter must come last, so this overload takes no token: + /// to cancel, use the overload that takes the and a token. + /// /// The first ordinal to include in the request to the reader for each record. /// The remaining ordinals to request from the reader for each record. /// The QueryResult that contains all the results and the column mappings. - public ValueTask> RetrieveAsync(int n, params IEnumerable others) + public ValueTask> RetrieveAsync( + int n, + params IEnumerable others) => RetrieveAsync(others.Prepend(n)); /// /// Iterates all records within the current result set using an IDataReader and returns the desired results. /// + /// + /// A parameter must come last, so this overload takes no token: + /// to cancel, use the overload that takes the and a token. + /// /// The first column name to include in the request to the reader for each record. /// The remaining column names to request from the reader for each record. /// The QueryResult that contains all the results and the column mappings. - public ValueTask> RetrieveAsync(string c, params IEnumerable others) + public ValueTask> RetrieveAsync( + string c, + params IEnumerable others) => RetrieveAsync(others.Prepend(c)); /// @@ -341,24 +353,60 @@ public ValueTask> ResultsAsync() /// /// Asynchronously iterates all records within the first result set using an IDataReader and returns the results. /// + /// + /// A token that can cancel the call. + /// It is used together with the command's own token; either one cancels. + /// /// The QueryResult that contains all the results and the column mappings. - public ValueTask> RetrieveAsync() - => ExecuteReaderAsync(reader => reader.RetrieveAsync(useReadAsync: UseAsyncRead), CommandBehavior.SingleResult); + public ValueTask> RetrieveAsync( + CancellationToken cancellationToken = default) + => ExecuteReaderAsync( + reader => reader.RetrieveAsync( + UseAsyncRead, + cancellationToken), + CommandBehavior.SingleResult, + cancellationToken); /// /// Asynchronously iterates all records within the current result set using an IDataReader and returns the desired results. /// /// The ordinals to request from the reader for each record. + /// + /// A token that can cancel the call. + /// It is used together with the command's own token; either one cancels. + /// /// The QueryResult that contains all the results and the column mappings. - public ValueTask> RetrieveAsync(IEnumerable ordinals) - => ExecuteReaderAsync(reader => reader.RetrieveAsync(ordinals, useReadAsync: UseAsyncRead), CommandBehavior.SingleResult); + public ValueTask> RetrieveAsync( + IEnumerable ordinals, + CancellationToken cancellationToken = default) + => ExecuteReaderAsync( + reader => reader.RetrieveAsync( + ordinals, + UseAsyncRead, + cancellationToken), + CommandBehavior.SingleResult, + cancellationToken); /// /// Iterates all records within the first result set using an IDataReader and returns the desired results as a list of Dictionaries containing only the specified column values. /// /// The column names to select. /// Orders the results arrays by ordinal. + /// + /// A token that can cancel the call. + /// It is used together with the command's own token; either one cancels. + /// /// The QueryResult that contains all the results and the column mappings. - public ValueTask> RetrieveAsync(IEnumerable columnNames, bool normalizeColumnOrder = false) - => ExecuteReaderAsync(reader => reader.RetrieveAsync(columnNames, normalizeColumnOrder, useReadAsync: UseAsyncRead)); + public ValueTask> RetrieveAsync( + IEnumerable columnNames, + bool normalizeColumnOrder = false, + CancellationToken cancellationToken = default) + => ExecuteReaderAsync( + reader => reader.RetrieveAsync( + columnNames, + normalizeColumnOrder, + UseAsyncRead, + cancellationToken), + CommandBehavior.Default, + cancellationToken); } diff --git a/Source/Core/Extensions/Connection.CreateCommand.cs b/Source/Core/Extensions/Connection.CreateCommand.cs index 19bd7211..ebc7f5ce 100644 --- a/Source/Core/Extensions/Connection.CreateCommand.cs +++ b/Source/Core/Extensions/Connection.CreateCommand.cs @@ -63,17 +63,17 @@ public static IDbCommand CreateTextCommand(this IDbConnection connection, /// Creates a stored procedure from a connection. /// /// The connection to create the command from. - /// The stored procedure name to use. + /// The stored procedure name to use. /// The number of seconds to wait before the command times out. The default is 60 seconds. /// A new that the caller owns and disposes. It is not associated with a transaction. /// /// The connection is not opened by this method; open it (see ) before executing the command. /// - /// or is . - /// is empty or white space. + /// or is . + /// is empty or white space. public static IDbCommand CreateStoredProcedureCommand(this IDbConnection connection, - string commandText, int secondsTimeout = CommandTimeout.DEFAULT_SECONDS) - => connection.CreateCommand(CommandType.StoredProcedure, commandText, secondsTimeout); + string procedureName, int secondsTimeout = CommandTimeout.DEFAULT_SECONDS) + => connection.CreateCommand(CommandType.StoredProcedure, procedureName, secondsTimeout); /// /// Creates a from a with the command type, text, and timeout already set. diff --git a/Source/Core/Extensions/ConnectionProvider.Using.cs b/Source/Core/Extensions/ConnectionProvider.Using.cs index 0263f738..1263a9f0 100644 --- a/Source/Core/Extensions/ConnectionProvider.Using.cs +++ b/Source/Core/Extensions/ConnectionProvider.Using.cs @@ -45,7 +45,7 @@ public static void Using(this IDbConnectionFactory connectionFactory, Action /// Creates a connection from the factory, invokes the action, and then disposes the connection. The connection is not opened by this method. /// - /// The connection type. + /// The connection type. /// The type returned from the action. /// The connection factory to create the connection from. /// The action to execute. It receives the connection. @@ -54,35 +54,35 @@ public static void Using(this IDbConnectionFactory connectionFactory, Action /// Useful for single-line operations. Exceptions thrown by the action propagate after the connection has been disposed. /// - public static T Using(this IDbConnectionFactory connectionFactory, Func action) - where TConn : IDbConnection + public static T Using(this IDbConnectionFactory connectionFactory, Func action) + where TConnection : IDbConnection { if (connectionFactory is null) throw new ArgumentNullException(nameof(connectionFactory)); if (action is null) throw new ArgumentNullException(nameof(action)); Contract.EndContractBlock(); - using TConn conn = connectionFactory.Create(); + using TConnection conn = connectionFactory.Create(); return action(conn); } /// /// Creates a connection from the factory, invokes the action, and then disposes the connection. The connection is not opened by this method. /// - /// The connection type. + /// The connection type. /// The connection factory to create the connection from. /// The action to execute. It receives the connection. /// or is . /// /// Useful for single-line operations. Exceptions thrown by the action propagate after the connection has been disposed. /// - public static void Using(this IDbConnectionFactory connectionFactory, Action action) - where TConn : IDbConnection + public static void Using(this IDbConnectionFactory connectionFactory, Action action) + where TConnection : IDbConnection { if (connectionFactory is null) throw new ArgumentNullException(nameof(connectionFactory)); if (action is null) throw new ArgumentNullException(nameof(action)); Contract.EndContractBlock(); - using TConn conn = connectionFactory.Create(); + using TConnection conn = connectionFactory.Create(); action(conn); } @@ -143,7 +143,7 @@ public static void Using(this IDbConnectionPool connectionPool, Action /// Takes a connection from the pool, invokes the action, and then gives the connection back to the pool. The connection is not opened by this method. /// - /// The connection type. + /// The connection type. /// The type returned from the action. /// The connection pool to take the connection from. /// The action to execute. It receives the connection. @@ -152,14 +152,14 @@ public static void Using(this IDbConnectionPool connectionPool, Action /// Useful for single-line operations. Exceptions thrown by the action propagate after the connection has been given back to the pool. /// - public static T Using(this IDbConnectionPool connectionPool, Func action) - where TConn : IDbConnection + public static T Using(this IDbConnectionPool connectionPool, Func action) + where TConnection : IDbConnection { if (connectionPool is null) throw new ArgumentNullException(nameof(connectionPool)); if (action is null) throw new ArgumentNullException(nameof(action)); Contract.EndContractBlock(); - TConn conn = connectionPool.Take(); + TConnection conn = connectionPool.Take(); try { return action(conn); @@ -173,21 +173,21 @@ public static T Using(this IDbConnectionPool connectionPool, Fu /// /// Takes a connection from the pool, invokes the action, and then gives the connection back to the pool. The connection is not opened by this method. /// - /// The connection type. + /// The connection type. /// The connection pool to take the connection from. /// The action to execute. It receives the connection. /// or is . /// /// Useful for single-line operations. Exceptions thrown by the action propagate after the connection has been given back to the pool. /// - public static void Using(this IDbConnectionPool connectionPool, Action action) - where TConn : IDbConnection + public static void Using(this IDbConnectionPool connectionPool, Action action) + where TConnection : IDbConnection { if (connectionPool is null) throw new ArgumentNullException(nameof(connectionPool)); if (action is null) throw new ArgumentNullException(nameof(action)); Contract.EndContractBlock(); - TConn conn = connectionPool.Take(); + TConnection conn = connectionPool.Take(); try { action(conn); @@ -201,7 +201,7 @@ public static void Using(this IDbConnectionPool connectionPool, Ac /// /// Invokes the function to create a connection, invokes the action, and then disposes the connection. The connection is not opened by this method. /// - /// The connection type. + /// The connection type. /// The type returned from the action. /// The function that creates the connection. /// The action to execute. It receives the connection. @@ -210,35 +210,35 @@ public static void Using(this IDbConnectionPool connectionPool, Ac /// /// Useful for single-line operations. Exceptions thrown by the action propagate after the connection has been disposed. /// - public static T Using(this Func connectionFactory, Func action) - where TConn : IDbConnection + public static T Using(this Func connectionFactory, Func action) + where TConnection : IDbConnection { if (connectionFactory is null) throw new ArgumentNullException(nameof(connectionFactory)); if (action is null) throw new ArgumentNullException(nameof(action)); Contract.EndContractBlock(); - using TConn conn = connectionFactory(); + using TConnection conn = connectionFactory(); return action(conn); } /// /// Invokes the function to create a connection, invokes the action, and then disposes the connection. The connection is not opened by this method. /// - /// The connection type. + /// The connection type. /// The function that creates the connection. /// The action to execute. It receives the connection. /// or is . /// /// Useful for single-line operations. Exceptions thrown by the action propagate after the connection has been disposed. /// - public static void Using(this Func connectionFactory, Action action) - where TConn : IDbConnection + public static void Using(this Func connectionFactory, Action action) + where TConnection : IDbConnection { if (connectionFactory is null) throw new ArgumentNullException(nameof(connectionFactory)); if (action is null) throw new ArgumentNullException(nameof(action)); Contract.EndContractBlock(); - using TConn conn = connectionFactory(); + using TConnection conn = connectionFactory(); action(conn); } @@ -286,7 +286,7 @@ public static async ValueTask UsingAsync(this IDbConnectionFactory connectionFac /// /// Creates a connection from the factory, invokes the asynchronous action, and then disposes the connection. The connection is not opened by this method. /// - /// The connection type. + /// The connection type. /// The type returned from the action. /// The connection factory to create the connection from. /// The asynchronous action to execute. It receives the connection. @@ -295,21 +295,21 @@ public static async ValueTask UsingAsync(this IDbConnectionFactory connectionFac /// /// Useful for single-line operations. Exceptions thrown by the action propagate after the connection has been disposed. /// - public static async ValueTask UsingAsync(this IDbConnectionFactory connectionFactory, Func> action) - where TConn : IDbConnection + public static async ValueTask UsingAsync(this IDbConnectionFactory connectionFactory, Func> action) + where TConnection : IDbConnection { if (connectionFactory is null) throw new ArgumentNullException(nameof(connectionFactory)); if (action is null) throw new ArgumentNullException(nameof(action)); Contract.EndContractBlock(); - using TConn conn = connectionFactory.Create(); + using TConnection conn = connectionFactory.Create(); return await action(conn).ConfigureAwait(false); } /// /// Creates a connection from the factory, invokes the asynchronous action, and then disposes the connection. The connection is not opened by this method. /// - /// The connection type. + /// The connection type. /// The connection factory to create the connection from. /// The asynchronous action to execute. It receives the connection. /// A task that completes after the action has completed and the connection has been released. @@ -317,14 +317,14 @@ public static async ValueTask UsingAsync(this IDbConnectionFactory< /// /// Useful for single-line operations. Exceptions thrown by the action propagate after the connection has been disposed. /// - public static async ValueTask UsingAsync(this IDbConnectionFactory connectionFactory, Func action) - where TConn : IDbConnection + public static async ValueTask UsingAsync(this IDbConnectionFactory connectionFactory, Func action) + where TConnection : IDbConnection { if (connectionFactory is null) throw new ArgumentNullException(nameof(connectionFactory)); if (action is null) throw new ArgumentNullException(nameof(action)); Contract.EndContractBlock(); - using TConn conn = connectionFactory.Create(); + using TConnection conn = connectionFactory.Create(); await action(conn).ConfigureAwait(false); } @@ -386,7 +386,7 @@ public static async ValueTask UsingAsync(this IDbConnectionPool connectionPool, /// /// Asynchronously takes a connection from the pool, invokes the action, and then gives the connection back to the pool. The connection is not opened by this method. /// - /// The connection type. + /// The connection type. /// The type returned from the action. /// The connection pool to take the connection from. /// The asynchronous action to execute. It receives the connection. @@ -395,14 +395,14 @@ public static async ValueTask UsingAsync(this IDbConnectionPool connectionPool, /// /// Useful for single-line operations. Exceptions thrown by the action propagate after the connection has been given back to the pool. /// - public static async ValueTask UsingAsync(this IDbConnectionPool connectionPool, Func> action) - where TConn : IDbConnection + public static async ValueTask UsingAsync(this IDbConnectionPool connectionPool, Func> action) + where TConnection : IDbConnection { if (connectionPool is null) throw new ArgumentNullException(nameof(connectionPool)); if (action is null) throw new ArgumentNullException(nameof(action)); Contract.EndContractBlock(); - TConn conn = connectionPool.Take(); + TConnection conn = connectionPool.Take(); try { return await action(conn).ConfigureAwait(false); @@ -416,7 +416,7 @@ public static async ValueTask UsingAsync(this IDbConnectionPool /// Asynchronously takes a connection from the pool, invokes the action, and then gives the connection back to the pool. The connection is not opened by this method. /// - /// The connection type. + /// The connection type. /// The connection pool to take the connection from. /// The asynchronous action to execute. It receives the connection. /// A task that completes after the action has completed and the connection has been released. @@ -424,14 +424,14 @@ public static async ValueTask UsingAsync(this IDbConnectionPool /// Useful for single-line operations. Exceptions thrown by the action propagate after the connection has been given back to the pool. /// - public static async ValueTask UsingAsync(this IDbConnectionPool connectionPool, Func action) - where TConn : IDbConnection + public static async ValueTask UsingAsync(this IDbConnectionPool connectionPool, Func action) + where TConnection : IDbConnection { if (connectionPool is null) throw new ArgumentNullException(nameof(connectionPool)); if (action is null) throw new ArgumentNullException(nameof(action)); Contract.EndContractBlock(); - TConn conn = connectionPool.Take(); + TConnection conn = connectionPool.Take(); try { await action(conn).ConfigureAwait(false); @@ -445,7 +445,7 @@ public static async ValueTask UsingAsync(this IDbConnectionPool co /// /// Invokes the function to create a connection, invokes the asynchronous action, and then disposes the connection. The connection is not opened by this method. /// - /// The connection type. + /// The connection type. /// The type returned from the action. /// The function that creates the connection. /// The asynchronous action to execute. It receives the connection. @@ -454,21 +454,21 @@ public static async ValueTask UsingAsync(this IDbConnectionPool co /// /// Useful for single-line operations. Exceptions thrown by the action propagate after the connection has been disposed. /// - public static async ValueTask UsingAsync(this Func connectionFactory, Func> action) - where TConn : IDbConnection + public static async ValueTask UsingAsync(this Func connectionFactory, Func> action) + where TConnection : IDbConnection { if (connectionFactory is null) throw new ArgumentNullException(nameof(connectionFactory)); if (action is null) throw new ArgumentNullException(nameof(action)); Contract.EndContractBlock(); - using TConn conn = connectionFactory(); + using TConnection conn = connectionFactory(); return await action(conn).ConfigureAwait(false); } /// /// Invokes the function to create a connection, invokes the asynchronous action, and then disposes the connection. The connection is not opened by this method. /// - /// The connection type. + /// The connection type. /// The function that creates the connection. /// The asynchronous action to execute. It receives the connection. /// A task that completes after the action has completed and the connection has been released. @@ -476,14 +476,14 @@ public static async ValueTask UsingAsync(this Func connectio /// /// Useful for single-line operations. Exceptions thrown by the action propagate after the connection has been disposed. /// - public static async ValueTask UsingAsync(this Func connectionFactory, Func action) - where TConn : IDbConnection + public static async ValueTask UsingAsync(this Func connectionFactory, Func action) + where TConnection : IDbConnection { if (connectionFactory is null) throw new ArgumentNullException(nameof(connectionFactory)); if (action is null) throw new ArgumentNullException(nameof(action)); Contract.EndContractBlock(); - using TConn conn = connectionFactory(); + using TConnection conn = connectionFactory(); await action(conn).ConfigureAwait(false); } } diff --git a/Source/Core/PublicAPI.Unshipped.txt b/Source/Core/PublicAPI.Unshipped.txt index 159b5a43..2eef38d4 100644 --- a/Source/Core/PublicAPI.Unshipped.txt +++ b/Source/Core/PublicAPI.Unshipped.txt @@ -65,3 +65,35 @@ static Open.Database.Extensions.Core.QueryResultExtensions.DequeueAs(this Sys static Open.Database.Extensions.Core.QueryResultExtensions.DequeueAsMappedDictionaries(this Open.Database.Extensions.Core.QueryResult!>! source) -> System.Collections.Generic.IEnumerable!>! static Open.Database.Extensions.Core.QueryResultExtensions.DequeueAsMappedDictionaries(this System.Threading.Tasks.Task!>!>! source) -> System.Threading.Tasks.ValueTask!>!> static Open.Database.Extensions.Core.QueryResultExtensions.DequeueAsMappedDictionaries(this System.Threading.Tasks.ValueTask!>!> source) -> System.Threading.Tasks.ValueTask!>!> +*REMOVED*Open.Database.Extensions.ExpressiveDbCommandBase.RetrieveAsync() -> System.Threading.Tasks.ValueTask!> +*REMOVED*Open.Database.Extensions.ExpressiveDbCommandBase.RetrieveAsync(System.Collections.Generic.IEnumerable! ordinals) -> System.Threading.Tasks.ValueTask!> +*REMOVED*Open.Database.Extensions.ExpressiveDbCommandBase.RetrieveAsync(System.Collections.Generic.IEnumerable! columnNames, bool normalizeColumnOrder = false) -> System.Threading.Tasks.ValueTask!> +*REMOVED*static Open.Database.Extensions.ConnectionExtensions.CreateStoredProcedureCommand(this System.Data.IDbConnection! connection, string! commandText, int secondsTimeout = 60) -> System.Data.IDbCommand! +*REMOVED*static Open.Database.Extensions.ConnectionExtensions.Using(this Open.Database.Extensions.IDbConnectionFactory! connectionFactory, System.Func! action) -> T +*REMOVED*static Open.Database.Extensions.ConnectionExtensions.Using(this Open.Database.Extensions.IDbConnectionPool! connectionPool, System.Func! action) -> T +*REMOVED*static Open.Database.Extensions.ConnectionExtensions.Using(this System.Func! connectionFactory, System.Func! action) -> T +*REMOVED*static Open.Database.Extensions.ConnectionExtensions.Using(this Open.Database.Extensions.IDbConnectionFactory! connectionFactory, System.Action! action) -> void +*REMOVED*static Open.Database.Extensions.ConnectionExtensions.Using(this Open.Database.Extensions.IDbConnectionPool! connectionPool, System.Action! action) -> void +*REMOVED*static Open.Database.Extensions.ConnectionExtensions.Using(this System.Func! connectionFactory, System.Action! action) -> void +*REMOVED*static Open.Database.Extensions.ConnectionExtensions.UsingAsync(this Open.Database.Extensions.IDbConnectionFactory! connectionFactory, System.Func>! action) -> System.Threading.Tasks.ValueTask +*REMOVED*static Open.Database.Extensions.ConnectionExtensions.UsingAsync(this Open.Database.Extensions.IDbConnectionPool! connectionPool, System.Func>! action) -> System.Threading.Tasks.ValueTask +*REMOVED*static Open.Database.Extensions.ConnectionExtensions.UsingAsync(this System.Func! connectionFactory, System.Func>! action) -> System.Threading.Tasks.ValueTask +*REMOVED*static Open.Database.Extensions.ConnectionExtensions.UsingAsync(this Open.Database.Extensions.IDbConnectionFactory! connectionFactory, System.Func! action) -> System.Threading.Tasks.ValueTask +*REMOVED*static Open.Database.Extensions.ConnectionExtensions.UsingAsync(this Open.Database.Extensions.IDbConnectionPool! connectionPool, System.Func! action) -> System.Threading.Tasks.ValueTask +*REMOVED*static Open.Database.Extensions.ConnectionExtensions.UsingAsync(this System.Func! connectionFactory, System.Func! action) -> System.Threading.Tasks.ValueTask +static Open.Database.Extensions.ConnectionExtensions.CreateStoredProcedureCommand(this System.Data.IDbConnection! connection, string! procedureName, int secondsTimeout = 60) -> System.Data.IDbCommand! +static Open.Database.Extensions.ConnectionExtensions.Using(this Open.Database.Extensions.IDbConnectionFactory! connectionFactory, System.Func! action) -> T +static Open.Database.Extensions.ConnectionExtensions.Using(this Open.Database.Extensions.IDbConnectionPool! connectionPool, System.Func! action) -> T +static Open.Database.Extensions.ConnectionExtensions.Using(this System.Func! connectionFactory, System.Func! action) -> T +static Open.Database.Extensions.ConnectionExtensions.Using(this Open.Database.Extensions.IDbConnectionFactory! connectionFactory, System.Action! action) -> void +static Open.Database.Extensions.ConnectionExtensions.Using(this Open.Database.Extensions.IDbConnectionPool! connectionPool, System.Action! action) -> void +static Open.Database.Extensions.ConnectionExtensions.Using(this System.Func! connectionFactory, System.Action! action) -> void +static Open.Database.Extensions.ConnectionExtensions.UsingAsync(this Open.Database.Extensions.IDbConnectionFactory! connectionFactory, System.Func>! action) -> System.Threading.Tasks.ValueTask +static Open.Database.Extensions.ConnectionExtensions.UsingAsync(this Open.Database.Extensions.IDbConnectionPool! connectionPool, System.Func>! action) -> System.Threading.Tasks.ValueTask +static Open.Database.Extensions.ConnectionExtensions.UsingAsync(this System.Func! connectionFactory, System.Func>! action) -> System.Threading.Tasks.ValueTask +static Open.Database.Extensions.ConnectionExtensions.UsingAsync(this Open.Database.Extensions.IDbConnectionFactory! connectionFactory, System.Func! action) -> System.Threading.Tasks.ValueTask +static Open.Database.Extensions.ConnectionExtensions.UsingAsync(this Open.Database.Extensions.IDbConnectionPool! connectionPool, System.Func! action) -> System.Threading.Tasks.ValueTask +static Open.Database.Extensions.ConnectionExtensions.UsingAsync(this System.Func! connectionFactory, System.Func! action) -> System.Threading.Tasks.ValueTask +Open.Database.Extensions.ExpressiveDbCommandBase.RetrieveAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +Open.Database.Extensions.ExpressiveDbCommandBase.RetrieveAsync(System.Collections.Generic.IEnumerable! ordinals, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> +Open.Database.Extensions.ExpressiveDbCommandBase.RetrieveAsync(System.Collections.Generic.IEnumerable! columnNames, bool normalizeColumnOrder = false, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask!> diff --git a/Source/MSSqlClient/Extensions/Transaction.CreateCommand.cs b/Source/MSSqlClient/Extensions/Transaction.CreateCommand.cs index c8152790..079ee4fa 100644 --- a/Source/MSSqlClient/Extensions/Transaction.CreateCommand.cs +++ b/Source/MSSqlClient/Extensions/Transaction.CreateCommand.cs @@ -32,14 +32,14 @@ public static SqlCommand CreateCommand(this SqlTransaction transaction, /// Creates a text enlisted in a transaction. /// /// The transaction to create the command from. - /// The command text to use. + /// The command text to use. /// The number of seconds to wait before the command times out. The default is 60 seconds. /// A new that the caller owns and disposes. - /// or is . - /// is empty or white space. + /// or is . + /// is empty or white space. public static SqlCommand CreateTextCommand(this SqlTransaction transaction, - string procedureName, int secondsTimeout = CommandTimeout.DEFAULT_SECONDS) - => transaction.CreateCommand(CommandType.Text, procedureName, secondsTimeout); + string commandText, int secondsTimeout = CommandTimeout.DEFAULT_SECONDS) + => transaction.CreateCommand(CommandType.Text, commandText, secondsTimeout); /// /// Creates a stored procedure enlisted in a transaction. diff --git a/Source/SqlClient/Extensions/Transaction.CreateCommand.cs b/Source/SqlClient/Extensions/Transaction.CreateCommand.cs index c8152790..079ee4fa 100644 --- a/Source/SqlClient/Extensions/Transaction.CreateCommand.cs +++ b/Source/SqlClient/Extensions/Transaction.CreateCommand.cs @@ -32,14 +32,14 @@ public static SqlCommand CreateCommand(this SqlTransaction transaction, /// Creates a text enlisted in a transaction. /// /// The transaction to create the command from. - /// The command text to use. + /// The command text to use. /// The number of seconds to wait before the command times out. The default is 60 seconds. /// A new that the caller owns and disposes. - /// or is . - /// is empty or white space. + /// or is . + /// is empty or white space. public static SqlCommand CreateTextCommand(this SqlTransaction transaction, - string procedureName, int secondsTimeout = CommandTimeout.DEFAULT_SECONDS) - => transaction.CreateCommand(CommandType.Text, procedureName, secondsTimeout); + string commandText, int secondsTimeout = CommandTimeout.DEFAULT_SECONDS) + => transaction.CreateCommand(CommandType.Text, commandText, secondsTimeout); /// /// Creates a stored procedure enlisted in a transaction. From d8e352451bf7929b8b0c0ee5f947e1ea5dfa8601 Mon Sep 17 00:00:00 2001 From: electricessence <5899455+electricessence@users.noreply.github.com> Date: Tue, 10 Nov 2026 15:47:19 -0800 Subject: [PATCH 2/3] Binding and cancellation tests for ExpressiveDbCommand.RetrieveAsync Co-Authored-By: Claude Sonnet 5.5 --- .../ExpressiveRetrieveAsyncTokenTests.cs | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 Tests/Open.Database.Extensions.Core.Tests/ExpressiveRetrieveAsyncTokenTests.cs diff --git a/Tests/Open.Database.Extensions.Core.Tests/ExpressiveRetrieveAsyncTokenTests.cs b/Tests/Open.Database.Extensions.Core.Tests/ExpressiveRetrieveAsyncTokenTests.cs new file mode 100644 index 00000000..546ff405 --- /dev/null +++ b/Tests/Open.Database.Extensions.Core.Tests/ExpressiveRetrieveAsyncTokenTests.cs @@ -0,0 +1,132 @@ +using System.Data.Common; +using Open.Database.Extensions.Core; +using static Open.Database.Extensions.Tests.ReaderFixtures; + +#nullable enable + +namespace Open.Database.Extensions.Tests; + +// ExpressiveDbCommand.RetrieveAsync takes a trailing CancellationToken on the three overloads that can. +// The method-group conversions fail to compile if a signature moves; +// the call forms fail to compile (or bind elsewhere) if an existing call stops binding to the same member; +// the behavior tests show each form runs the same query and honors the token. +[ExcludeFromCodeCoverage] +public static class ExpressiveRetrieveAsyncTokenTests +{ + static (TrackedConnection Connection, ExpressiveDbCommand Command) Make() + { + var tally = new DbTally(new DbScript { Columns = Columns, Rows = TwoRows }); + TrackedConnection connection = tally.NewConnection(); + return (connection, new ExpressiveDbCommand(connection, CommandType.Text, "select 1")); + } + + [Fact] + public static void EveryOverload_HasTheExpectedSignature() + { + ExpressiveDbCommand command = Make().Command; + + // Compile-time pins: each conversion names exactly one parameter list. + Func>> a = command.RetrieveAsync; + Func, CancellationToken, ValueTask>> b = command.RetrieveAsync; + Func, bool, CancellationToken, ValueTask>> c = command.RetrieveAsync; + Func, ValueTask>> d = command.RetrieveAsync; + Func, ValueTask>> e = command.RetrieveAsync; + + Assert.All([a, b, c, d, e], x => Assert.Same(command, x.Target)); + Assert.Equal(5, typeof(ExpressiveDbCommand).GetMethods().Count(m => m.Name == nameof(ExpressiveDbCommand.RetrieveAsync))); + } + + [Fact] + public static void TheTrailingToken_IsOptional_OnlyOnTheThreeOverloads() + { + var withToken = typeof(ExpressiveDbCommand).GetMethods() + .Where(m => m.Name == nameof(ExpressiveDbCommand.RetrieveAsync)) + .Select(m => m.GetParameters()) + .Where(p => p.Length != 0 && p[^1].ParameterType == typeof(CancellationToken)) + .ToArray(); + + Assert.Equal(3, withToken.Length); + Assert.All(withToken, p => Assert.True(p[^1].IsOptional)); + } + +#pragma warning disable xUnit1051 // The point is the forms that pass no token. + [Fact] + public static async Task EveryExistingCallForm_StillBinds_AndReturnsTheSameRows() + { + (TrackedConnection connection, ExpressiveDbCommand command) = Make(); + IEnumerable ordinals = [2, 0]; + IEnumerable names = ["Note", "Id"]; + + // These compile only while each form still binds to a member of the shape it had. + QueryResultQueue all = await command.RetrieveAsync(); + QueryResultQueue byOrdinals = await command.RetrieveAsync(ordinals); + QueryResultQueue byNames = await command.RetrieveAsync(names); + QueryResultQueue byNamesNormalized = await command.RetrieveAsync(names, true); + QueryResultQueue firstAndOthers = await command.RetrieveAsync(2, 0); + QueryResultQueue firstOnly = await command.RetrieveAsync(1); + QueryResultQueue namedAndOthers = await command.RetrieveAsync("Note", "Id"); + QueryResultQueue nameOnly = await command.RetrieveAsync("Id"); + QueryResultQueue defaultToken = await command.RetrieveAsync(default(CancellationToken)); + + Assert.Equal("Id,Name,Note", string.Join(",", all.Names)); + Assert.Equal("Id,Name,Note", string.Join(",", defaultToken.Names)); // the token overload; a bare `default` literal is ambiguous between the overloads + Assert.Equal("Note,Id", string.Join(",", byOrdinals.Names)); + Assert.Equal("Note,Id", string.Join(",", byNames.Names)); + Assert.All( + new[] { all, byOrdinals, byNames, byNamesNormalized, firstAndOthers, firstOnly, namedAndOthers, nameOnly, defaultToken }, + r => Assert.Equal(2, r.Result.Count)); + Assert.Equal(ConnectionState.Closed, connection.State); + } +#pragma warning restore xUnit1051 + + [Theory] + [InlineData("token")] + [InlineData("ordinals, token")] + [InlineData("names, token")] + [InlineData("names, normalize, token")] + public static async Task EveryTokenForm_Runs_AndALiveTokenChangesNothing(string variant) + { + (TrackedConnection connection, ExpressiveDbCommand command) = Make(); + + QueryResultQueue result = await RetrieveVia(command, variant, TestContext.Current.CancellationToken); + + Assert.Equal(2, result.Result.Count); + Assert.Equal(ConnectionState.Closed, connection.State); + Assert.Equal(connection.Opens, connection.Closes); + } + + [Theory] + [InlineData("token")] + [InlineData("ordinals, token")] + [InlineData("names, token")] + [InlineData("names, normalize, token")] + public static async Task EveryTokenForm_ACancelledToken_Throws_AndLeavesNothingOpen(string variant) + { + (TrackedConnection connection, ExpressiveDbCommand command) = Make(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAnyAsync( + async () => await RetrieveVia(command, variant, cts.Token)); + + Assert.Equal(ConnectionState.Closed, connection.State); + Assert.Equal(connection.Opens, connection.Closes); + Assert.Equal(0, connection.Disposes); + } + + static ValueTask> RetrieveVia( + ExpressiveDbCommand command, + string variant, + CancellationToken token) + { + IEnumerable ordinals = [2, 0]; + IEnumerable names = ["Note", "Id"]; + return variant switch + { + "token" => command.RetrieveAsync(token), + "ordinals, token" => command.RetrieveAsync(ordinals, token), + "names, token" => command.RetrieveAsync(names, cancellationToken: token), + _ => command.RetrieveAsync(names, true, token), // "names, normalize, token" + }; + } +} From 9b5ba633fc3a51d479b88e43bc0ed3fa2cd9f1cc Mon Sep 17 00:00:00 2001 From: electricessence <5899455+electricessence@users.noreply.github.com> Date: Tue, 10 Nov 2026 15:47:19 -0800 Subject: [PATCH 3/3] whats-new-11: breaking in rc.4 for the RetrieveAsync token and the renames Co-Authored-By: Claude Sonnet 5.5 --- docsite/articles/whats-new-11.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docsite/articles/whats-new-11.md b/docsite/articles/whats-new-11.md index 94374ce6..6a63f594 100644 --- a/docsite/articles/whats-new-11.md +++ b/docsite/articles/whats-new-11.md @@ -5,6 +5,13 @@ Version 11 is a performance release and a tidy-up release. Result mapping is rewritten around a cached, typed row reader, column mapping is declared once, NULL and error handling follow simple rules, and stored-procedure parameters can be named from the arguments you already have. See [Performance](performance.md) for what the speed-up does and does not buy you. Coming from 10.x? Read "Upgrading from 10.x" below first. +## Breaking in rc.4 + +Rebuild consumers of rc.3. Both changes are source-compatible for every call except the ones noted. + +- `ExpressiveDbCommand.RetrieveAsync` takes a trailing `CancellationToken cancellationToken = default` on the three overloads that can: `RetrieveAsync(CancellationToken)`, `RetrieveAsync(IEnumerable, CancellationToken)` and `RetrieveAsync(IEnumerable, bool, CancellationToken)`. The token is used together with the command's own token. A `params` list must come last, so the two `params` overloads (`RetrieveAsync(int, params IEnumerable)` and `RetrieveAsync(string, params IEnumerable)`) are unchanged and take no token: pass the ordinals or names as a collection to the `IEnumerable` overload to cancel. A call that passed no token binds to the same overload as before, but the signatures are new, so rebuild. +- Three parameters are renamed to say what they hold. This breaks only a caller that passes the argument by name: `CreateTextCommand(SqlTransaction, ...)` in `SqlClient` and `MSSqlClient` takes `commandText` (it was `procedureName`), `CreateStoredProcedureCommand(IDbConnection, ...)` in Core takes `procedureName` (it was `commandText`), and the type parameter `TConn` of the `Using` and `UsingAsync` connection helpers is now `TConnection`. The type-parameter rename breaks no caller. + ## Changes since rc.2 - A `Map` terminal now reads through the later result sets after the first, so a failure in a later statement is thrown, where rc.2 returned the rows and lost it. This is a behavior change from rc.2. `Map` still returns only the first set's rows. `.SingleResult()` after `Map` stops after the first set and does not see a later failure. A later failure can follow a write that already committed, so a caller that retries can repeat it. The older command methods, Channel and Dataflow do not read through yet.