diff --git a/CodeOnlyStoredProcedure-NET40/CodeOnlyStoredProcedure-NET40.csproj b/CodeOnlyStoredProcedure-NET40/CodeOnlyStoredProcedure-NET40.csproj index 2d9427d..015f055 100644 --- a/CodeOnlyStoredProcedure-NET40/CodeOnlyStoredProcedure-NET40.csproj +++ b/CodeOnlyStoredProcedure-NET40/CodeOnlyStoredProcedure-NET40.csproj @@ -129,6 +129,7 @@ + diff --git a/CodeOnlyStoredProcedure/CodeOnlyStoredProcedure.csproj b/CodeOnlyStoredProcedure/CodeOnlyStoredProcedure.csproj index 4d85823..99a129c 100644 --- a/CodeOnlyStoredProcedure/CodeOnlyStoredProcedure.csproj +++ b/CodeOnlyStoredProcedure/CodeOnlyStoredProcedure.csproj @@ -119,6 +119,7 @@ + diff --git a/CodeOnlyStoredProcedure/Dynamic/DynamicStoredProcedure.cs b/CodeOnlyStoredProcedure/Dynamic/DynamicStoredProcedure.cs index 4f9c139..a28520f 100644 --- a/CodeOnlyStoredProcedure/Dynamic/DynamicStoredProcedure.cs +++ b/CodeOnlyStoredProcedure/Dynamic/DynamicStoredProcedure.cs @@ -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) diff --git a/CodeOnlyStoredProcedure/StoredProcedureExtensions.WithTableValuedParameter.cs b/CodeOnlyStoredProcedure/StoredProcedureExtensions.WithTableValuedParameter.cs index 141b745..282bc65 100644 --- a/CodeOnlyStoredProcedure/StoredProcedureExtensions.WithTableValuedParameter.cs +++ b/CodeOnlyStoredProcedure/StoredProcedureExtensions.WithTableValuedParameter.cs @@ -88,5 +88,87 @@ public static TSP WithTableValuedParameter(this TSP sp, return (TSP)sp.CloneWith(new TableValuedParameter(name, table, typeof(TRow), tableTypeName, tableTypeSchema)); } + + /// + /// Clones the given , and associates the items + /// as a Table Valued Parameter. + /// + /// The type of to associate the Table Valued Parameter with. + /// The to clone. + /// The name of the Table Valued Parameter in the stored procedure. + /// The to pass in the Table Valued Parameter. Make sure the is set, + /// as that will be used to identify the TVP's type in the database. + /// A copy of the that has the Table Valued Parameter set. + /// StoredProcedures are immutable, so all the Fluent API methods return copies. + public static TSP WithTableValuedParameter(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() != null); + + return (TSP)sp.CloneWith(new TableValuedParameter(name, table)); + } + + /// + /// Clones the given , and associates the items + /// as a Table Valued Parameter. + /// + /// The type of to associate the Table Valued Parameter with. + /// The to clone. + /// The name of the Table Valued Parameter in the stored procedure. + /// The to pass in the Table Valued Parameter. + /// The name of the table that the database's stored procedure expects + /// in its Table Valued Parameter. + /// A copy of the that has the Table Valued Parameter set. + /// StoredProcedures are immutable, so all the Fluent API methods return copies. + public static TSP WithTableValuedParameter(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() != null); + + return (TSP)sp.CloneWith(new TableValuedParameter(name, table, tableTypeName)); + } + + /// + /// Clones the given , and associates the items + /// as a Table Valued Parameter. + /// + /// The type of to associate the Table Valued Parameter with. + /// The to clone. + /// The name of the Table Valued Parameter in the stored procedure. + /// The to pass in the Table Valued Parameter. + /// The schema of the table that the database's stored procedure expects + /// in its Table Valued Parameter. + /// The name of the table that the database's stored procedure expects + /// in its Table Valued Parameter. + /// A copy of the that has the Table Valued Parameter set. + /// StoredProcedures are immutable, so all the Fluent API methods return copies. + public static TSP WithTableValuedParameter(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() != null); + + return (TSP)sp.CloneWith(new TableValuedParameter(name, table, tableTypeName, tableTypeSchema)); + } } } diff --git a/CodeOnlyStoredProcedure/StoredProcedureParameters/TableValuedParameter.cs b/CodeOnlyStoredProcedure/StoredProcedureParameters/TableValuedParameter.cs index 6324b5b..a767b8b 100644 --- a/CodeOnlyStoredProcedure/StoredProcedureParameters/TableValuedParameter.cs +++ b/CodeOnlyStoredProcedure/StoredProcedureParameters/TableValuedParameter.cs @@ -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)); @@ -43,7 +70,9 @@ public IDbDataParameter CreateDbDataParameter(IDbCommand command) parm.SqlDbType = SqlDbType.Structured; parm.TypeName = TypeName; - if (values.Cast().Any()) + if (data != null) + parm.Value = data; + else if (values.Cast().Any()) parm.Value = CrateValuedParameter(values, valueType); return parm; @@ -51,11 +80,17 @@ public IDbDataParameter CreateDbDataParameter(IDbCommand command) 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; @@ -86,9 +121,10 @@ private static IEnumerable 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 diff --git a/CodeOnlyStoredProcedures.nuspec b/CodeOnlyStoredProcedures.nuspec index ce53b43..e0587b5 100644 --- a/CodeOnlyStoredProcedures.nuspec +++ b/CodeOnlyStoredProcedures.nuspec @@ -12,6 +12,7 @@ Code Only Stored Procedures will not create any Stored Procedures on your databa 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. diff --git a/CodeOnlyTests/Dynamic/DynamicStoredProcedureTests.cs b/CodeOnlyTests/Dynamic/DynamicStoredProcedureTests.cs index f0a6952..d940cbe 100644 --- a/CodeOnlyTests/Dynamic/DynamicStoredProcedureTests.cs +++ b/CodeOnlyTests/Dynamic/DynamicStoredProcedureTests.cs @@ -357,6 +357,39 @@ public void CanNotPassStringsForTableValueParameter() "because the message should be helpful"); } + [TestMethod] + public void CanPassDataTable_AsTableValuedParameter() + { + var reader = new Mock(); + reader.SetupGet(r => r.FieldCount).Returns(0); + reader.Setup(r => r.Read()).Returns(false); + + var parms = new DataParameterCollection(); + var cmd = new Mock(); + 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(); + 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().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() { diff --git a/CodeOnlyTests/StoredProcedureExtensionsTests.WithTableValuedParameter.cs b/CodeOnlyTests/StoredProcedureExtensionsTests.WithTableValuedParameter.cs index 063cdac..70d8b17 100644 --- a/CodeOnlyTests/StoredProcedureExtensionsTests.WithTableValuedParameter.cs +++ b/CodeOnlyTests/StoredProcedureExtensionsTests.WithTableValuedParameter.cs @@ -1,4 +1,5 @@ -using CodeOnlyStoredProcedure; +using System.Data; +using CodeOnlyStoredProcedure; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -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().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() { @@ -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().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"); + } } } diff --git a/CodeOnlyTests/StoredProcedureParameters/TableValuedParameterTests.cs b/CodeOnlyTests/StoredProcedureParameters/TableValuedParameterTests.cs index fba4ad7..e24be86 100644 --- a/CodeOnlyTests/StoredProcedureParameters/TableValuedParameterTests.cs +++ b/CodeOnlyTests/StoredProcedureParameters/TableValuedParameterTests.cs @@ -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(); + 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().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() { @@ -83,6 +107,17 @@ 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() { @@ -90,6 +125,17 @@ public void ToStringDoesNotDisplayExtraAts() .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() {