Skip to content

Commit

Permalink
Merge pull request #99 from abe545/add-datatable-tvp
Browse files Browse the repository at this point in the history
Add datatable tvp
  • Loading branch information
abe545 authored Apr 27, 2017
2 parents bb06c3b + ab0b022 commit eb6ad23
Show file tree
Hide file tree
Showing 9 changed files with 251 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
<Reference Include="System.Core" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.XML" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\CodeOnlyStoredProcedure\DataTransformation\ConvertNumericAttribute.cs">
Expand Down
1 change: 1 addition & 0 deletions CodeOnlyStoredProcedure/CodeOnlyStoredProcedure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
<Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="DataTransformation\ConvertNumericAttribute.cs" />
Expand Down
2 changes: 2 additions & 0 deletions CodeOnlyStoredProcedure/Dynamic/DynamicStoredProcedure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, o
attr.Schema));
}
}
else if (argType == typeof(DataTable))
parameters.Add(new TableValuedParameter(parmName, (DataTable)arg));
else if (dbType == DbType.Object)
parameters.AddRange(argType.GetParameters(arg));
else if (direction == ParameterDirection.Output)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,87 @@ public static TSP WithTableValuedParameter<TSP, TRow>(this TSP sp,

return (TSP)sp.CloneWith(new TableValuedParameter(name, table, typeof(TRow), tableTypeName, tableTypeSchema));
}

/// <summary>
/// Clones the given <see cref="StoredProcedure"/>, and associates the <see cref="DataTable"/> items
/// as a Table Valued Parameter.
/// </summary>
/// <typeparam name="TSP">The type of <see cref="StoredProcedure"/> to associate the Table Valued Parameter with.</typeparam>
/// <param name="sp">The <see cref="StoredProcedure"/> to clone.</param>
/// <param name="name">The name of the Table Valued Parameter in the stored procedure.</param>
/// <param name="table">The <see cref="DataTable"/> to pass in the Table Valued Parameter. Make sure the <see cref="DataTable.TableName"/> is set,
/// as that will be used to identify the TVP's type in the database.</param>
/// <returns>A copy of the <see cref="StoredProcedure"/> that has the Table Valued Parameter set.</returns>
/// <remarks>StoredProcedures are immutable, so all the Fluent API methods return copies.</remarks>
public static TSP WithTableValuedParameter<TSP>(this TSP sp,
string name,
DataTable table)
where TSP : StoredProcedure
{
Contract.Requires(sp != null);
Contract.Requires(!string.IsNullOrWhiteSpace(name));
Contract.Requires(table != null);
Contract.Ensures(Contract.Result<TSP>() != null);

return (TSP)sp.CloneWith(new TableValuedParameter(name, table));
}

/// <summary>
/// Clones the given <see cref="StoredProcedure"/>, and associates the <see cref="DataTable"/> items
/// as a Table Valued Parameter.
/// </summary>
/// <typeparam name="TSP">The type of <see cref="StoredProcedure"/> to associate the Table Valued Parameter with.</typeparam>
/// <param name="sp">The <see cref="StoredProcedure"/> to clone.</param>
/// <param name="name">The name of the Table Valued Parameter in the stored procedure.</param>
/// <param name="table">The <see cref="DataTable"/> to pass in the Table Valued Parameter.</param>
/// <param name="tableTypeName">The name of the table that the database's stored procedure expects
/// in its Table Valued Parameter.</param>
/// <returns>A copy of the <see cref="StoredProcedure"/> that has the Table Valued Parameter set.</returns>
/// <remarks>StoredProcedures are immutable, so all the Fluent API methods return copies.</remarks>
public static TSP WithTableValuedParameter<TSP>(this TSP sp,
string name,
DataTable table,
string tableTypeName)
where TSP : StoredProcedure
{
Contract.Requires(sp != null);
Contract.Requires(!string.IsNullOrWhiteSpace(name));
Contract.Requires(table != null);
Contract.Requires(!string.IsNullOrWhiteSpace(tableTypeName));
Contract.Ensures(Contract.Result<TSP>() != null);

return (TSP)sp.CloneWith(new TableValuedParameter(name, table, tableTypeName));
}

/// <summary>
/// Clones the given <see cref="StoredProcedure"/>, and associates the <see cref="DataTable"/> items
/// as a Table Valued Parameter.
/// </summary>
/// <typeparam name="TSP">The type of <see cref="StoredProcedure"/> to associate the Table Valued Parameter with.</typeparam>
/// <param name="sp">The <see cref="StoredProcedure"/> to clone.</param>
/// <param name="name">The name of the Table Valued Parameter in the stored procedure.</param>
/// <param name="table">The <see cref="DataTable"/> to pass in the Table Valued Parameter.</param>
/// <param name="tableTypeSchema">The schema of the table that the database's stored procedure expects
/// in its Table Valued Parameter.</param>
/// <param name="tableTypeName">The name of the table that the database's stored procedure expects
/// in its Table Valued Parameter.</param>
/// <returns>A copy of the <see cref="StoredProcedure"/> that has the Table Valued Parameter set.</returns>
/// <remarks>StoredProcedures are immutable, so all the Fluent API methods return copies.</remarks>
public static TSP WithTableValuedParameter<TSP>(this TSP sp,
string name,
DataTable table,
string tableTypeSchema,
string tableTypeName)
where TSP : StoredProcedure
{
Contract.Requires(sp != null);
Contract.Requires(!string.IsNullOrWhiteSpace(name));
Contract.Requires(table != null);
Contract.Requires(!string.IsNullOrWhiteSpace(tableTypeSchema));
Contract.Requires(!string.IsNullOrWhiteSpace(tableTypeName));
Contract.Ensures(Contract.Result<TSP>() != null);

return (TSP)sp.CloneWith(new TableValuedParameter(name, table, tableTypeName, tableTypeSchema));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,38 @@ internal class TableValuedParameter : IInputStoredProcedureParameter
{
private readonly IEnumerable values;
private readonly Type valueType;
private readonly DataTable data;

public string ParameterName { get; }
public object Value { get { return values; } }
public object Value { get { return data as object ?? values; } }

internal string TypeName { get; }

public TableValuedParameter(string name, DataTable data)
{
Contract.Requires(!string.IsNullOrWhiteSpace(name));
Contract.Requires(data != null);

if (string.IsNullOrWhiteSpace(data.TableName))
throw new NotSupportedException("When passing a DataTable, either set its TypeName to the TVP's type, or pass it in as one of the parameters.");

ParameterName = name;
this.data = data;
this.TypeName = data.TableName;
}

public TableValuedParameter(string name, DataTable data, string tableTypeName, string tableTypeSchema = "dbo")
{
Contract.Requires(!string.IsNullOrWhiteSpace(name));
Contract.Requires(data != null);
Contract.Requires(!string.IsNullOrWhiteSpace(tableTypeName));
Contract.Requires(!string.IsNullOrWhiteSpace(tableTypeSchema));

ParameterName = name;
this.data = data;
this.TypeName = $"[{tableTypeSchema}].[{tableTypeName}]";
}

public TableValuedParameter(string name, IEnumerable values, Type valueType, string tableTypeName, string tableTypeSchema = "dbo")
{
Contract.Requires(!string.IsNullOrWhiteSpace(name));
Expand All @@ -43,19 +70,27 @@ public IDbDataParameter CreateDbDataParameter(IDbCommand command)
parm.SqlDbType = SqlDbType.Structured;
parm.TypeName = TypeName;

if (values.Cast<object>().Any())
if (data != null)
parm.Value = data;
else if (values.Cast<object>().Any())
parm.Value = CrateValuedParameter(values, valueType);

return parm;
}

public override string ToString()
{
if (data != null)
return $"@{ParameterName.FormatParameterName()} = DataTable ({data.Rows.Count} items)";

return $"@{ParameterName.FormatParameterName()} = IEnumerable<{valueType}> ({GetValueCount()} items)";
}

private int GetValueCount()
{
if (data != null)
return data.Rows.Count;

int i = 0;
foreach (var o in values)
++i;
Expand Down Expand Up @@ -86,9 +121,10 @@ private static IEnumerable<SqlDataRecord> CrateValuedParameter(IEnumerable table
}

// copy the input list into a list of SqlDataRecords
var columns = columnList.ToArray();
foreach (var row in table)
{
var record = new SqlDataRecord(columnList.ToArray());
var record = new SqlDataRecord(columns);
for (int i = 0; i < columnList.Count; i++)
{
// locate the value of the matching property
Expand Down
1 change: 1 addition & 0 deletions CodeOnlyStoredProcedures.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Code Only Stored Procedures will not create any Stored Procedures on your databa
<releaseNotes>2.4.0
Added support for binary blobs for both results and parameters by using a byte array.
Fixes bug where output parameters can not be used if the stored procedure returns results.
Added support for using DataTable to pass a Table-Valued Parameter

2.3.0
Can now opt in to not clone the database connection before executing a StoredProcedure.
Expand Down
33 changes: 33 additions & 0 deletions CodeOnlyTests/Dynamic/DynamicStoredProcedureTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,39 @@ public void CanNotPassStringsForTableValueParameter()
"because the message should be helpful");
}

[TestMethod]
public void CanPassDataTable_AsTableValuedParameter()
{
var reader = new Mock<IDataReader>();
reader.SetupGet(r => r.FieldCount).Returns(0);
reader.Setup(r => r.Read()).Returns(false);

var parms = new DataParameterCollection();
var cmd = new Mock<IDbCommand>();
cmd.SetupAllProperties();
cmd.Setup(c => c.ExecuteReader()).Returns(reader.Object);
cmd.SetupGet(c => c.Parameters).Returns(parms);
cmd.Setup(c => c.CreateParameter()).Returns(new SqlParameter());

var ctx = new Mock<IDbConnection>();
ctx.Setup(c => c.CreateCommand()).Returns(cmd.Object);

var dt = new DataTable();
dt.TableName = "[dbo].[Person]";
dt.Columns.Add("FirstName");
dt.Rows.Add("Foo");
dt.Rows.Add("Bar");

dynamic toTest = new DynamicStoredProcedure(ctx.Object, transformers, CancellationToken.None, TEST_TIMEOUT, DynamicExecutionMode.Synchronous, true);

toTest.usp_AddPeople(people: dt);

var p = parms.OfType<SqlParameter>().Single();
p.ParameterName.Should().Be("people", "because that was the argument name");
p.SqlDbType.Should().Be(SqlDbType.Structured, "because it is a table-valued parameter");
p.TypeName.Should().Be("[dbo].[Person]", "because that is the name of the TableName set on the DataTable being passed as a TVP");
}

[TestMethod]
public void CanPassNullParameter()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using CodeOnlyStoredProcedure;
using System.Data;
using CodeOnlyStoredProcedure;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

Expand Down Expand Up @@ -32,6 +33,28 @@ public void TestWithTableValuedParameterAddsParameter()
param.TypeName.Should().Be("[dbo].[TVP]", "because it was passed to WithInputParameter");
}

[TestMethod]
public void TestWithTableValuedParameterAddsParameter_DataTable()
{
var orig = new StoredProcedure("Test");
var tvp = new DataTable();
tvp.Columns.Add("Name", typeof(string));
tvp.Columns.Add("Foo", typeof(int));
tvp.Columns.Add("Bar", typeof(decimal));
tvp.Rows.Add("Hello", 0, 100M);
tvp.Rows.Add("World", 3, 331M);

var toTest = orig.WithTableValuedParameter("Foo", tvp, "TVP");

toTest.Should().NotBeSameAs(orig, "because StoredProcedures should be immutable");
orig.Parameters.Should().BeEmpty("because StoredProcedures should be immutable");

var param = toTest.Parameters.Should().ContainSingle(p => p.ParameterName == "Foo", "because we added one Parameter")
.Which.Should().BeOfType<TableValuedParameter>().Which;
param.Value.Should().Be(tvp, "because it was passed to WithInputParameter");
param.TypeName.Should().Be("[dbo].[TVP]", "because it was passed to WithInputParameter");
}

[TestMethod]
public void TestWithTableValuedParameterWithSchemaAddsParameter()
{
Expand All @@ -53,5 +76,27 @@ public void TestWithTableValuedParameterWithSchemaAddsParameter()
param.Value.Should().Be(tvp, "because it was passed to WithInputParameter");
param.TypeName.Should().Be("[TVP].[Table Type]", "because it was passed to WithInputParameter");
}

[TestMethod]
public void TestWithTableValuedParameterWithSchemaAddsParameter_DataTable()
{
var orig = new StoredProcedure("Test");
var tvp = new DataTable();
tvp.Columns.Add("Name", typeof(string));
tvp.Columns.Add("Foo", typeof(int));
tvp.Columns.Add("Bar", typeof(decimal));
tvp.Rows.Add("Hello", 0, 100M);
tvp.Rows.Add("World", 3, 331M);

var toTest = orig.WithTableValuedParameter("Foo", tvp, "TVP", "Table Type");

toTest.Should().NotBeSameAs(orig, "because StoredProcedures should be immutable");
orig.Parameters.Should().BeEmpty("because StoredProcedures should be immutable");

var param = toTest.Parameters.Should().ContainSingle(p => p.ParameterName == "Foo", "because we added one Parameter")
.Which.Should().BeOfType<TableValuedParameter>().Which;
param.Value.Should().Be(tvp, "because it was passed to WithInputParameter");
param.TypeName.Should().Be("[TVP].[Table Type]", "because it was passed to WithInputParameter");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,30 @@ public void SetsConstructorValuesOnParameter()
meta.GetInt32(0).Should().Be(42, "it was passed in the row");
}

[TestMethod]
public void SetsConstructorDataTableOnParameter()
{
var cmd = new Mock<IDbCommand>();
cmd.Setup(c => c.CreateParameter()).Returns(new SqlParameter());
var dt = new DataTable();
dt.Columns.Add("Int", typeof(int));
dt.Rows.Add(42);

var toTest = new TableValuedParameter("Foo", dt, "CustomInt", "Schema");

var res = toTest.CreateDbDataParameter(cmd.Object);

res.DbType.Should().Be(DbType.Object, "table valued parameters pass DbType.Object");
res.ParameterName.Should().Be("Foo", "it was passed in the constructor");
res.Direction.Should().Be(ParameterDirection.Input, "it is an input parameter");

var typed = res.Should().BeOfType<SqlParameter>().Which;

typed.SqlDbType.Should().Be(SqlDbType.Structured, "table valued parameters are Structured");
typed.TypeName.Should().Be("[Schema].[CustomInt]", "it was passed in the constructor");
typed.Value.Should().Be(dt);
}

[TestMethod]
public void IgnoresSetOnlyProperties()
{
Expand Down Expand Up @@ -83,13 +107,35 @@ public void ToStringRepresentsTheParameter()
.Should().Be(string.Format("@Foo = IEnumerable<{0}> (1 items)", typeof(TVP)));
}

[TestMethod]
public void ToStringRepresentsTheDataTableParameter()
{
var dt = new DataTable();
dt.Columns.Add("Int", typeof(int));
dt.Rows.Add(42);

new TableValuedParameter("Foo", dt, "CustomInt", "Schema").ToString()
.Should().Be("@Foo = DataTable (1 items)");
}

[TestMethod]
public void ToStringDoesNotDisplayExtraAts()
{
new TableValuedParameter("@Foo", new[] { new TVP(42) }, typeof(TVP), "CustomInt", "Schema").ToString()
.Should().Be(string.Format("@Foo = IEnumerable<{0}> (1 items)", typeof(TVP)));
}

[TestMethod]
public void ToStringDoesNotDisplayExtraAtsDataTable()
{
var dt = new DataTable();
dt.Columns.Add("Int", typeof(int));
dt.Rows.Add(42);

new TableValuedParameter("@Foo", dt, "CustomInt", "Schema").ToString()
.Should().Be("@Foo = DataTable (1 items)");
}

[TestMethod]
public void SetsNullValueWhenEnumerableIsEmpty()
{
Expand Down

0 comments on commit eb6ad23

Please sign in to comment.