diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlBatch.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlBatch.xml index 40f34cba48..d2fdff51d2 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlBatch.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlBatch.xml @@ -1,4 +1,4 @@ - + @@ -179,7 +179,7 @@ The list of contained in the batch in a . - + Sends the to the and builds a . @@ -232,7 +232,85 @@ - + + + An instance of , specifying options for batch execution and data retrieval. + Only and are supported at the batch level. + + + Sends the to the and builds a . + + + + The following example creates a and a , then adds multiple objects to the batch. It then executes the batch, creating a . The example reads through the results of the batch commands, writing them to the console. Finally, the example closes the and then the as the using blocks fall out of scope. + + + using Microsoft.Data.SqlClient; + + class Program + { + static void Main() + { + string str = "Data Source=(local);Initial Catalog=Northwind;" + + "Integrated Security=SSPI;Encrypt=False"; + RunBatch(str); + } + + static void RunBatch(string connString) + { + using var connection = new SqlConnection(connString); + connection.Open(); + + using var batch = new SqlBatch(connection); + + const int count = 10; + const string parameterName = "parameter"; + for (int i = 0; i < count; i++) + { + var batchCommand = new SqlBatchCommand($"SELECT @{parameterName} as value"); + batchCommand.Parameters.Add(new SqlParameter(parameterName, i)); + batch.BatchCommands.Add(batchCommand); + } + + var results = new List<int>(count); + using (SqlDataReader reader = batch.ExecuteReader(CommandBehavior.CloseConnection)) + { + do + { + while (reader.Read()) + { + results.Add(reader.GetFieldValue<int>(0)); + } + } while (reader.NextResult()); + } + Console.WriteLine(string.Join(", ", results)); + } + } + + + + + A token to cancel the asynchronous operation. + + An asynchronous version of , which sends the to the and builds a . + Exceptions will be reported via the returned Task object. + + A task representing the asynchronous operation. + + An error occurred while executing the batch. + + + The value is invalid. + + + The cancellation token was canceled. This exception is stored into the returned task. + + + + + An instance of , specifying options for batch execution and data retrieval. + Only and are supported at the batch level. + A token to cancel the asynchronous operation. An asynchronous version of , which sends the to the and builds a . @@ -303,6 +381,7 @@ An instance of , specifying options for batch execution and data retrieval. + Only and are supported at the batch level. Executes the batch against its connection, returning a which can be used to access the results. @@ -326,7 +405,10 @@ When the batch returns multiple result sets from different commands, - One of the enumeration values that specifies options for batch execution and data retrieval. + + An instance of , specifying options for batch execution and data retrieval. + Only and are supported at the batch level. + A token to cancel the asynchronous operation. This implementation invokes the method and returns a completed task. The default implementation will return a cancelled task if passed an already cancelled cancellation token. This method accepts a cancellation token that can be used to request the operation to be cancelled early. diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlBatchCommand.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlBatchCommand.xml index 8f0be84d25..5145571840 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlBatchCommand.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlBatchCommand.xml @@ -1,4 +1,4 @@ - + @@ -87,7 +87,8 @@ - One of the values, indicating options for statement execution and data retrieval. + An instance of , specifying options for statement execution and data retrieval. + Only and are supported at the statement level. diff --git a/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.cs index e52cbdc8d3..313fffec5d 100644 --- a/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.cs @@ -144,14 +144,26 @@ public class SqlBatch : System.IDisposable, System.IAsyncDisposable #else public System.Threading.Tasks.Task ExecuteNonQueryAsync(System.Threading.CancellationToken cancellationToken = default) => throw null; #endif - /// + /// public Microsoft.Data.SqlClient.SqlDataReader ExecuteReader() => throw null; - /// + /// + #if NET + public new Microsoft.Data.SqlClient.SqlDataReader ExecuteReader(System.Data.CommandBehavior behavior) => throw null; + #else + public Microsoft.Data.SqlClient.SqlDataReader ExecuteReader(System.Data.CommandBehavior behavior) => throw null; + #endif + /// #if NET public new System.Threading.Tasks.Task ExecuteReaderAsync(System.Threading.CancellationToken cancellationToken = default) => throw null; #else public System.Threading.Tasks.Task ExecuteReaderAsync(System.Threading.CancellationToken cancellationToken = default) => throw null; #endif + /// + #if NET + public new System.Threading.Tasks.Task ExecuteReaderAsync(System.Data.CommandBehavior behavior, System.Threading.CancellationToken cancellationToken = default) => throw null; + #else + public System.Threading.Tasks.Task ExecuteReaderAsync(System.Data.CommandBehavior behavior, System.Threading.CancellationToken cancellationToken = default) => throw null; + #endif /// #if NET public override object ExecuteScalar() => throw null; diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBatch.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBatch.cs index a1f79f6f15..8412d3c7af 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBatch.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBatch.cs @@ -266,24 +266,54 @@ SqlTransaction Transaction } } - /// - public SqlDataReader ExecuteReader() + /// + public SqlDataReader ExecuteReader() => ExecuteReader(CommandBehavior.Default); + + /// + public + #if NET + new + #endif + SqlDataReader ExecuteReader(CommandBehavior behavior) { + ValidateExecuteCommandBehavior(nameof(ExecuteReader), behavior); + CheckDisposed(); SetupBatchCommandExecute(); - return _batchCommand.ExecuteReader(); + return _batchCommand.ExecuteReader(behavior); } - /// + /// public #if NET new #endif - Task ExecuteReaderAsync(CancellationToken cancellationToken = default) + Task ExecuteReaderAsync(CancellationToken cancellationToken = default) => ExecuteReaderAsync(CommandBehavior.Default, cancellationToken); + + /// + public + #if NET + new + #endif + Task ExecuteReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken = default) { + ValidateExecuteCommandBehavior(nameof(ExecuteReaderAsync), behavior); + CheckDisposed(); SetupBatchCommandExecute(); - return _batchCommand.ExecuteReaderAsync(cancellationToken); + return _batchCommand.ExecuteReaderAsync(behavior, cancellationToken) + .ContinueWith((result) => + { + if (result.IsFaulted) + { + throw result.Exception.InnerException; + } + return result.Result; + }, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.NotOnCanceled, + TaskScheduler.Default + ); } /// @@ -293,7 +323,7 @@ Task ExecuteReaderAsync(CancellationToken cancellationToken = def #else virtual #endif - DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => ExecuteReader(); + DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => ExecuteReader(behavior); /// protected @@ -302,24 +332,7 @@ Task ExecuteReaderAsync(CancellationToken cancellationToken = def #else virtual #endif - Task ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) - { - CheckDisposed(); - SetupBatchCommandExecute(); - return _batchCommand.ExecuteReaderAsync(cancellationToken) - .ContinueWith((result) => - { - if (result.IsFaulted) - { - throw result.Exception.InnerException; - } - return result.Result; - }, - CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.NotOnCanceled, - TaskScheduler.Default - ); - } + async Task ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) => await ExecuteReaderAsync(behavior, cancellationToken); private void CheckDisposed() { @@ -350,5 +363,30 @@ private void SetupBatchCommandExecute() } _batchCommand.SetBatchRPCModeReadyToExecute(); } + + /// + /// Validates that the provided is compatible with . + /// + /// The name of the calling method for error reporting. + /// The behavior flags to validate. + /// + /// + /// Only and + /// are supported at the batch level. + /// + /// + /// To apply other behaviors (such as or ), + /// they must be set on individual instances within the batch. + /// + /// + /// Thrown when unsupported behavior flags are detected. + internal static void ValidateExecuteCommandBehavior(string method, CommandBehavior behavior) + { + if (0 != (behavior & ~(CommandBehavior.SequentialAccess | CommandBehavior.CloseConnection))) + { + ADP.ValidateCommandBehavior(behavior); + throw ADP.NotSupportedCommandBehavior(behavior & ~(CommandBehavior.SequentialAccess | CommandBehavior.CloseConnection), method); + } + } } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Batch.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Batch.cs index 7b6d16a420..da7d1e2ed7 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Batch.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Batch.cs @@ -37,7 +37,7 @@ internal void AddBatchCommand(SqlBatchCommand batchCommand) { // All batch sql statements must be executed inside sp_executesql, including those // without parameters - BuildExecuteSql(CommandBehavior.Default, commandText, batchCommand.Parameters, ref rpc); + BuildExecuteSql(batchCommand.CommandBehavior, batchCommand.Parameters, ref rpc); } _RPCList.Add(rpc); diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs index d9f6c33233..91ccb684bc 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommand.Reader.cs @@ -442,7 +442,6 @@ private _SqlRPC BuildExecute(bool inSchema) // @TODO: Can we return the RPC here like BuildExecute does? private void BuildExecuteSql( CommandBehavior behavior, - string commandText, SqlParameterCollection parameters, ref _SqlRPC rpc) { @@ -463,13 +462,13 @@ private void BuildExecuteSql( SqlParameter sqlParam; // @batch_text - commandText ??= GetCommandText(behavior); + string text = GetCommandText(behavior); sqlParam = rpc.systemParams[0]; - sqlParam.SqlDbType = (commandText.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT + sqlParam.SqlDbType = (text.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT ? SqlDbType.NVarChar : SqlDbType.NText; - sqlParam.Size = commandText.Length; - sqlParam.Value = commandText; + sqlParam.Size = text.Length; + sqlParam.Value = text; sqlParam.Direction = ParameterDirection.Input; // @batch_params @@ -1455,7 +1454,7 @@ private SqlDataReader RunExecuteReaderTds( else { Debug.Assert(_execType is EXECTYPE.UNPREPARED, "Invalid execType!"); - BuildExecuteSql(cmdBehavior, commandText: null, _parameters, ref rpc); + BuildExecuteSql(cmdBehavior, _parameters, ref rpc); } rpc.options = TdsEnums.RPC_NOMETADATA; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Batch/BatchTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Batch/BatchTests.cs index 991d55cdeb..e92f8470bd 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Batch/BatchTests.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Batch/BatchTests.cs @@ -649,6 +649,97 @@ public static async Task ExecuteReaderAsyncMultiple() Assert.Equal(10, resultRowCount); } + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void ExecuteReaderCommandCommandBehaviorSchemaOnlyKeyInfo() + { + System.Collections.ObjectModel.ReadOnlyCollection schema; + + using (SqlConnection conn = new SqlConnection(DataTestUtility.TCPConnectionString)) + using (SqlBatch batch = new SqlBatch(conn)) + { + conn.Open(); + + var cmd = new SqlBatchCommand("SELECT * FROM Categories"); + cmd.CommandBehavior = CommandBehavior.SchemaOnly | CommandBehavior.KeyInfo; + batch.BatchCommands.Add(cmd); + + using var reader = batch.ExecuteReader(); + + Assert.False(reader.Read()); + + schema = reader.GetColumnSchema(); + } + + Assert.Equal(4, schema.Count); + Assert.True(schema[0].IsKey); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void ExecuteReaderCommandBehaviorCloseConnection() + { + int resultSetCount = 0; + int resultRowCount = 0; + + using (SqlConnection conn = new SqlConnection(DataTestUtility.TCPConnectionString)) + using (SqlBatch batch = new SqlBatch(conn)) + { + conn.Open(); + + batch.BatchCommands.Add(new SqlBatchCommand("SELECT 1")); + batch.BatchCommands.Add(new SqlBatchCommand("SELECT 2")); + + using (var reader = batch.ExecuteReader(CommandBehavior.CloseConnection)) + { + do + { + resultSetCount += 1; + while (reader.Read()) + { + resultRowCount += 1; + } + } while (reader.NextResult()); + } + + Assert.Equal(ConnectionState.Closed, conn.State); + } + + Assert.Equal(2, resultSetCount); + Assert.Equal(2, resultRowCount); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static async Task ExecuteReaderAsyncCommandBehaviorCloseConnection() + { + int resultSetCount = 0; + int resultRowCount = 0; + + using (SqlConnection conn = new SqlConnection(DataTestUtility.TCPConnectionString)) + await using (SqlBatch batch = new SqlBatch(conn)) + { + await conn.OpenAsync(); + + batch.BatchCommands.Add(new SqlBatchCommand("SELECT 1")); + batch.BatchCommands.Add(new SqlBatchCommand("SELECT 2")); + + using (var reader = await batch.ExecuteReaderAsync(CommandBehavior.CloseConnection)) + { + do + { + resultSetCount += 1; + while (await reader.ReadAsync()) + { + resultRowCount += 1; + } + } while (await reader.NextResultAsync()); + } + + Assert.Equal(ConnectionState.Closed, conn.State); + } + + Assert.Equal(2, resultSetCount); + Assert.Equal(2, resultRowCount); + } + private static SqlParameter CreateParameter(string name, SqlDbType type, T value, ParameterDirection direction = ParameterDirection.Input) { var parameter = new SqlParameter(name, type); diff --git a/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlBatchTest.cs b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlBatchTest.cs new file mode 100644 index 0000000000..ebed579cd2 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlBatchTest.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Data; +using Xunit; + +namespace Microsoft.Data.SqlClient.UnitTests; + +/// +/// Provides unit tests for verifying the behavior of the SqlBatch class. +/// +public class SqlBatchTest +{ + /// + /// Verifies that SqlBatch.ValidateExecuteCommandBehavior throws an ArgumentOutOfRangeException when an invalid CommandBehavior is specified. + /// + [Fact] + public void InvalidCommandBehaviorValidateExecuteCommandBehavior_Throws() + { + ArgumentOutOfRangeException ex = Assert.Throws(() => SqlBatch.ValidateExecuteCommandBehavior("ExecuteNonQuery", (CommandBehavior)64)); + Assert.Contains("CommandBehavior", ex.Message, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Verifies that SqlBatch.ValidateExecuteCommandBehavior throws an ArgumentOutOfRangeException when a valid but unsupported CommandBehavior is specified. + /// + [Fact] + public void NotSupportedCommandBehaviorValidateExecuteCommandBehavior_Throws() + { + ArgumentOutOfRangeException ex = Assert.Throws(() => SqlBatch.ValidateExecuteCommandBehavior("ExecuteNonQuery", CommandBehavior.KeyInfo)); + Assert.Contains("not supported", ex.Message, StringComparison.OrdinalIgnoreCase); + } +}