Skip to content

Commit 2554bc7

Browse files
committed
Support null/empty TVP parameters
1 parent b8ccc2a commit 2554bc7

File tree

3 files changed

+106
-32
lines changed

3 files changed

+106
-32
lines changed

src/FluentCommand.SqlServer/SqlCommandExtensions.cs

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ public static class SqlCommandExtensions
1919
/// <typeparam name="TEntity">The type of the data entities.</typeparam>
2020
/// <param name="dataCommand">The <see cref="IDataCommand"/> to extend.</param>
2121
/// <param name="name">The name of the parameter.</param>
22-
/// <param name="data">The enumerable data to be added as a table-valued parameter.</param>
22+
/// <param name="data">The enumerable data to be added as a table-valued parameter. Can be <c>null</c> or empty.</param>
2323
/// <returns>
2424
/// The same <see cref="IDataCommand"/> instance for fluent chaining.
2525
/// </returns>
26-
public static IDataCommand ParameterStructured<TEntity>(this IDataCommand dataCommand, string name, IEnumerable<TEntity> data)
26+
public static IDataCommand ParameterStructured<TEntity>(this IDataCommand dataCommand, string name, IEnumerable<TEntity>? data)
2727
where TEntity : class
2828
{
2929
if (dataCommand is null)
@@ -32,10 +32,7 @@ public static IDataCommand ParameterStructured<TEntity>(this IDataCommand dataCo
3232
if (string.IsNullOrEmpty(name))
3333
throw new ArgumentException($"'{nameof(name)}' cannot be null or empty.", nameof(name));
3434

35-
if (data is null)
36-
throw new ArgumentNullException(nameof(data));
37-
38-
var records = new SqlDataRecordAdapter<TEntity>(data);
35+
var records = data is null ? null : new SqlDataRecordAdapter<TEntity>(data);
3936
return ParameterStructured(dataCommand, name, records);
4037
}
4138

@@ -45,30 +42,29 @@ public static IDataCommand ParameterStructured<TEntity>(this IDataCommand dataCo
4542
/// </summary>
4643
/// <param name="dataCommand">The <see cref="IDataCommand"/> to extend.</param>
4744
/// <param name="name">The name of the parameter.</param>
48-
/// <param name="records">The <see cref="IEnumerable{SqlDataRecord}"/> to be added as a table-valued parameter.</param>
45+
/// <param name="records">The <see cref="IEnumerable{SqlDataRecord}"/> to be added as a table-valued parameter. Can be <c>null</c> or empty.</param>
4946
/// <returns>
5047
/// The same <see cref="IDataCommand"/> instance for fluent chaining.
5148
/// </returns>
5249
/// <exception cref="InvalidOperationException">
5350
/// Thrown if the underlying command is not a SQL Server command.
5451
/// </exception>
55-
public static IDataCommand ParameterStructured(this IDataCommand dataCommand, string name, IEnumerable<SqlDataRecord> records)
52+
public static IDataCommand ParameterStructured(this IDataCommand dataCommand, string name, IEnumerable<SqlDataRecord>? records)
5653
{
5754
if (dataCommand is null)
5855
throw new ArgumentNullException(nameof(dataCommand));
5956

6057
if (string.IsNullOrEmpty(name))
6158
throw new ArgumentException($"'{nameof(name)}' cannot be null or empty.", nameof(name));
6259

63-
if (records is null)
64-
throw new ArgumentNullException(nameof(records));
65-
6660
var sqlParameter = CreateSqlParameter(dataCommand);
6761

6862
sqlParameter.ParameterName = name;
69-
sqlParameter.Value = records;
70-
sqlParameter.Direction = ParameterDirection.Input;
7163
sqlParameter.SqlDbType = SqlDbType.Structured;
64+
sqlParameter.Direction = ParameterDirection.Input;
65+
66+
// SQL Server requires null (not an empty enumerable) when a TVP has no rows
67+
sqlParameter.Value = records?.Any() == true ? records : null;
7268

7369
return dataCommand.Parameter(sqlParameter);
7470
}
@@ -79,30 +75,29 @@ public static IDataCommand ParameterStructured(this IDataCommand dataCommand, st
7975
/// </summary>
8076
/// <param name="dataCommand">The <see cref="IDataCommand"/> to extend.</param>
8177
/// <param name="name">The name of the parameter.</param>
82-
/// <param name="dataReader">The <see cref="DbDataReader"/> to be added as a table-valued parameter.</param>
78+
/// <param name="dataReader">The <see cref="DbDataReader"/> to be added as a table-valued parameter. Can be <c>null</c>.</param>
8379
/// <returns>
8480
/// The same <see cref="IDataCommand"/> instance for fluent chaining.
8581
/// </returns>
8682
/// <exception cref="InvalidOperationException">
8783
/// Thrown if the underlying command is not a SQL Server command.
8884
/// </exception>
89-
public static IDataCommand ParameterStructured(this IDataCommand dataCommand, string name, DbDataReader dataReader)
85+
public static IDataCommand ParameterStructured(this IDataCommand dataCommand, string name, DbDataReader? dataReader)
9086
{
9187
if (dataCommand is null)
9288
throw new ArgumentNullException(nameof(dataCommand));
9389

9490
if (string.IsNullOrEmpty(name))
9591
throw new ArgumentException($"'{nameof(name)}' cannot be null or empty.", nameof(name));
9692

97-
if (dataReader is null)
98-
throw new ArgumentNullException(nameof(dataReader));
99-
10093
var sqlParameter = CreateSqlParameter(dataCommand);
10194

10295
sqlParameter.ParameterName = name;
103-
sqlParameter.Value = dataReader;
104-
sqlParameter.Direction = ParameterDirection.Input;
10596
sqlParameter.SqlDbType = SqlDbType.Structured;
97+
sqlParameter.Direction = ParameterDirection.Input;
98+
99+
// SQL Server requires null (not an empty reader) when a TVP has no rows
100+
sqlParameter.Value = dataReader?.HasRows == true ? dataReader : null;
106101

107102
return dataCommand.Parameter(sqlParameter);
108103
}
@@ -112,30 +107,29 @@ public static IDataCommand ParameterStructured(this IDataCommand dataCommand, st
112107
/// </summary>
113108
/// <param name="dataCommand">The <see cref="IDataCommand"/> to extend.</param>
114109
/// <param name="name">The name of the parameter.</param>
115-
/// <param name="dataTable">The <see cref="DataTable"/> to be added as a table-valued parameter.</param>
110+
/// <param name="dataTable">The <see cref="DataTable"/> to be added as a table-valued parameter. Can be <c>null</c> or empty.</param>
116111
/// <returns>
117112
/// The same <see cref="IDataCommand"/> instance for fluent chaining.
118113
/// </returns>
119114
/// <exception cref="InvalidOperationException">
120115
/// Thrown if the underlying command is not a SQL Server command.
121116
/// </exception>
122-
public static IDataCommand ParameterStructured(this IDataCommand dataCommand, string name, DataTable dataTable)
117+
public static IDataCommand ParameterStructured(this IDataCommand dataCommand, string name, DataTable? dataTable)
123118
{
124119
if (dataCommand is null)
125120
throw new ArgumentNullException(nameof(dataCommand));
126121

127122
if (string.IsNullOrEmpty(name))
128123
throw new ArgumentException($"'{nameof(name)}' cannot be null or empty.", nameof(name));
129124

130-
if (dataTable is null)
131-
throw new ArgumentNullException(nameof(dataTable));
132-
133125
var sqlParameter = CreateSqlParameter(dataCommand);
134126

135127
sqlParameter.ParameterName = name;
136-
sqlParameter.Value = dataTable;
137-
sqlParameter.Direction = ParameterDirection.Input;
138128
sqlParameter.SqlDbType = SqlDbType.Structured;
129+
sqlParameter.Direction = ParameterDirection.Input;
130+
131+
// SQL Server requires null (not an empty DataTable) when a TVP has no rows
132+
sqlParameter.Value = dataTable?.Rows.Count > 0 ? dataTable : null;
139133

140134
return dataCommand.Parameter(sqlParameter);
141135
}

src/FluentCommand.SqlServer/SqlDataRecordAdapter.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,23 @@ public class SqlDataRecordAdapter<T> : IEnumerable<SqlDataRecord> where T : clas
2020
// ReSharper disable once StaticMemberInGenericType
2121
private static readonly ConcurrentDictionary<Type, (SqlMetaData[] MetaData, IMemberAccessor[] Columns)> _metaDataCache = new();
2222

23-
private readonly IEnumerable<T> _source;
23+
private readonly IEnumerable<T>? _source;
2424

2525
/// <summary>
2626
/// Initializes a new instance of the <see cref="SqlDataRecordAdapter{T}"/> class.
2727
/// </summary>
28-
/// <param name="source">The source collection to adapt.</param>
29-
/// <exception cref="ArgumentNullException">Thrown if <paramref name="source"/> is null.</exception>
30-
public SqlDataRecordAdapter(IEnumerable<T> source)
28+
/// <param name="source">The source collection to adapt. Can be <c>null</c> or empty.</param>
29+
public SqlDataRecordAdapter(IEnumerable<T>? source)
3130
{
32-
_source = source ?? throw new ArgumentNullException(nameof(source));
31+
_source = source;
3332
}
3433

3534
/// <inheritdoc/>
3635
public IEnumerator<SqlDataRecord> GetEnumerator()
3736
{
37+
if (_source is null)
38+
yield break;
39+
3840
var (metaData, columns) = GetCachedMetaData();
3941
var record = new SqlDataRecord(metaData);
4042

test/FluentCommand.SqlServer.Tests/ParameterStructuredTests.cs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,82 @@ public void ParameterStructuredWithEmptyDataTableReturnsNoResults()
145145
users.Should().BeEmpty();
146146
total.Should().Be(0);
147147
}
148+
149+
[Fact]
150+
public void ParameterStructuredWithNullDataTableReturnsNoResults()
151+
{
152+
DataTable? dataTable = null;
153+
154+
long total = -1;
155+
156+
using var session = Services.GetRequiredService<IDataSession>();
157+
session.Should().NotBeNull();
158+
159+
var users = session
160+
.StoredProcedure("[dbo].[UserListByIds]")
161+
.ParameterStructured("@IdList", dataTable)
162+
.Parameter<long>(p => p
163+
.Name("@Total")
164+
.Type(DbType.Int64)
165+
.Output(v => total = v)
166+
.Direction(ParameterDirection.Output)
167+
)
168+
.Query<User>()
169+
.ToList();
170+
171+
users.Should().BeEmpty();
172+
total.Should().Be(0);
173+
}
174+
175+
[Fact]
176+
public void ParameterStructuredWithEmptyEntityListReturnsNoResults()
177+
{
178+
var ids = new List<IdItem>();
179+
180+
long total = -1;
181+
182+
using var session = Services.GetRequiredService<IDataSession>();
183+
session.Should().NotBeNull();
184+
185+
var users = session
186+
.StoredProcedure("[dbo].[UserListByIds]")
187+
.ParameterStructured("@IdList", ids)
188+
.Parameter<long>(p => p
189+
.Name("@Total")
190+
.Type(DbType.Int64)
191+
.Output(v => total = v)
192+
.Direction(ParameterDirection.Output)
193+
)
194+
.Query<User>()
195+
.ToList();
196+
197+
users.Should().BeEmpty();
198+
total.Should().Be(0);
199+
}
200+
201+
[Fact]
202+
public void ParameterStructuredWithNullEntityListReturnsNoResults()
203+
{
204+
List<IdItem>? ids = null;
205+
206+
long total = -1;
207+
208+
using var session = Services.GetRequiredService<IDataSession>();
209+
session.Should().NotBeNull();
210+
211+
var users = session
212+
.StoredProcedure("[dbo].[UserListByIds]")
213+
.ParameterStructured("@IdList", ids)
214+
.Parameter<long>(p => p
215+
.Name("@Total")
216+
.Type(DbType.Int64)
217+
.Output(v => total = v)
218+
.Direction(ParameterDirection.Output)
219+
)
220+
.Query<User>()
221+
.ToList();
222+
223+
users.Should().BeEmpty();
224+
total.Should().Be(0);
225+
}
148226
}

0 commit comments

Comments
 (0)