dotnet / dotnet/SqlClient

Limited SQL Computed column support when using SqlParameter.Structured

Open
#3,242 7 comments 7 reactions 0 assignees View on GitHub
Dominant language
C#
Stars
989
Forks
340
Avg merge
4d 19h
Merged PRs (30d)
72

Description

### Describe the bug
When using udtt's with SqlParameter.Structured the passing of values into the underlying udtt works only if the udtt lacks any computed columns.
Like so;

![Image](https://github.com/user-attachments/assets/d18db6eb-a903-4647-842a-88f0e5cd8c73)

But if we add any computed column into the udtt the passing of values into the underlying udtt fails on first computed column and library does not provide any means to skip computed columns or mitigate this issues atleast none that I know of (Tried multiple approaches). One of them listed below like so;

![Image](https://github.com/user-attachments/assets/e4664974-0323-4954-b0bb-a25a8712c666)

The issue came up when we tried to create an udtt that would calculate hash value based on the value fields in the udtt and store it in the udtt as varbinary field, thus would allow us to use the calculated hash value to determine if some data has changed when compared to the data in the db table.

### To reproduce
Program.cs

```c#
internal class Program
{
static async Task Main(string[] args)
{
var x = new TestAppSimple();
await x.Test();
}
}
```
TestAppSimple.cs
```c#
internal class TestAppSimple
{
private SqlConnection Connection { get; set; }

public TestAppSimple()
{
var connectionString = "Server=localhost;...";

Connection = new SqlConnection(connectionString);

}

public async Task Test()
{
try
{
Connection.Open();

//As Datatable
await TryToPassUdttWithoutComputedAsStructuredDataTableParamSimple(); //Works!

await TryToPassUdttWithComputedAsStructuredDataTableParamSimple1(); //Fails!
await TryToPassUdttWithComputedAsStructuredDataTableParamSimple2(); //Fails!
await TryToPassUdttWithComputedAsStructuredDataTableParamSimple3(); //Fails!
await TryToPassUdttWithComputedAsStructuredDataTableParamSimple4(); //Fails!
await TryToPassUdttWithComputedAsStructuredDataTableParamSimple5(); //Fails!
await TryToPassUdttWithComputedAsStructuredDataTableParamSimple6(); //Fails!

//As SqlDataRecord
await TryToPassUdttWithoutComputedAsStructuredDataRecordParamSimple1(); //Works!

await TryToPassUdttWithComputedAsStructuredDataRecordParamSimple1(); //Fails!
await TryToPassUdttWithComputedAsStructuredDataRecordParamSimple2(); //Fails!
await TryToPassUdttWithComputedAsStructuredDataRecordParamSimple3(); //Fails!
await TryToPassUdttWithComputedAsStructuredDataRecordParamSimple4(); //Fails!
await TryToPassUdttWithComputedAsStructuredDataRecordParamSimple5(); //Fails!
//await TryToPassUdttWithComputedAsStructuredDataRecordParamSimple6(); //Fails!
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
finally
{
Connection?.Close();
Connection?.Dispose();
}
}

#region Sql tests

#region As DataTable
private async Task TryToPassUdttWithoutComputedAsStructuredDataTableParamSimple()
{
try
{
var command = Connection.CreateCommand();
command.CommandText = "[dbo].[spTestUdttWithoutComputed]";
command.CommandTimeout = 30;
command.CommandType = CommandType.StoredProcedure;

//init param data
var nums = new List<(int Num1, int Num2)>() { (1, 1), (2, 2), (3, 3) };

//populate dt parameter
var dtNums = new DataTable();
dtNums.Columns.Add(new DataColumn("Num1", typeof(int)));
dtNums.Columns.Add(new DataColumn("Num2", typeof(int)));
foreach (var num in nums)
{
var dataRow = dtNums.NewRow();
dataRow["Num1"] = num.Num1;
dataRow["Num2"] = num.Num2;
dtNums.Rows.Add(dataRow);
}

//pass dt parameter to procedure
command.Parameters.Add(new SqlParameter
{
ParameterName = "@numsWithoutComputed",
SqlDbType = SqlDbType.Structured,
TypeName = "[dbo].[udttNumsWithoutCompute]",
Value = dtNums
});

using (var reader = await command.ExecuteReaderAsync())
{
//if passes here, then it worked.
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}

}

private async Task TryToPassUdttWithComputedAsStructuredDataTableParamSimple1()
{
try
{
var command = Connection.CreateCommand();
command.CommandText = "[dbo].[spTestUdttWithComputed]";
command.CommandTimeout = 30;
command.CommandType = CommandType.StoredProcedure;

//init param data
var nums = new List<(int Num1, int Num2)>() { (1, 1), (2, 2), (3, 3) };

//populate dt parameter
var dtNums = new DataTable();
dtNums.Columns.Add(new DataColumn("Num1", typeof(int)));
dtNums.Columns.Add(new DataColumn("Num2", typeof(int)));

//Total column omitted from datatable, because is computed, we cannot pass the value to it.

foreach (var num in nums)
{
var dataRow = dtNums.NewRow();
dataRow["Num1"] = num.Num1;
dataRow["Num2"] = num.Num2;
dtNums.Rows.Add(dataRow);
}

//pass dt parameter to procedure
command.Parameters.Add(new SqlParameter
{
ParameterName = "@numsWithComputed",
SqlDbType = SqlDbType.Structured,
TypeName = "[dbo].[udttNumsWithCompute]",
Value = dtNums
});

using (var reader = await command.ExecuteReaderAsync())
{
//if passes here, then it worked.
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}

}

private async Task TryToPassUdttWithComputedAsStructuredDataTableParamSimple2()
{
try
{
var command = Connection.CreateCommand();
command.CommandText = "[dbo].[spTestUdttWithComputed]";
command.CommandTimeout = 30;
command.CommandType = CommandType.StoredProcedure;

//init param data
var nums = new List<(int Num1, int Num2)>() { (1, 1), (2, 2), (3, 3) };

//populate dt parameter
var dtNums = new DataTable();
dtNums.Columns.Add(new DataColumn("Num1", typeof(int)));
dtNums.Columns.Add(new DataColumn("Num2", typeof(int)));

//Total column defined in datatable, but not populated on rows because is computed, we cannot pass the value to it.
dtNums.Columns.Add(new DataColumn("Total", typeof(int)));

foreach (var num in nums)
{
var dataRow = dtNums.NewRow();
dataRow["Num1"] = num.Num1;
dataRow["Num2"] = num.Num2;
dtNums.Rows.Add(dataRow);
}

//pass dt parameter to procedure
command.Parameters.Add(new SqlParameter
{
ParameterName = "@numsWithComputed",
SqlDbType = SqlDbType.Structured,
TypeName = "[dbo].[udttNumsWithCompute]",
Value = dtNums
});

using (var reader = await command.ExecuteReaderAsync())
{
//if passes here, then it worked.
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}

}

private async Task TryToPassUdttWithComputedAsStructuredDataTableParamSimple3()
{
try
{
var command = Connection.CreateCommand();
command.CommandText = "[dbo].[spTestUdttWithComputed]";
command.CommandTimeout = 30;
command.CommandType = CommandType.StoredProcedure;

//init param data
var nums = new List<(int Num1, int Num2)>() { (1, 1), (2, 2), (3, 3) };

//populate dt parameter
var dtNums = new DataTable();
dtNums.Columns.Add(new DataColumn("Num1", typeof(int)));
dtNums.Columns.Add(new DataColumn("Num2", typeof(int)));

//Total column defined in datatable and dummy value assigned on rows
dtNums.Columns.Add(new DataColumn("Total", typeof(int)));

foreach (var num in nums)
{
var dataRow = dtNums.NewRow();
dataRow["Num1"] = num.Num1;
dataRow["Num2"] = num.Num2;
dataRow["Total"] = 0; //Dummy value of total!
dtNums.Rows.Add(dataRow);
}

//pass dt parameter to procedure
command.Parameters.Add(new SqlParameter
{
ParameterName = "@numsWithComputed",
SqlDbType = SqlDbType.Structured,
TypeName = "[dbo].[udttNumsWithCompute]",
Value = dtNums
});

using (var reader = await command.ExecuteReaderAsync())
{
//if passes here, then it worked.
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}

}

private async Task TryToPassUdttWithComputedAsStructuredDataTableParamSimple4()
{
try
{
var command = Connection.CreateCommand();
command.CommandText = "[dbo].[spTestUdttWithComputed]";
command.CommandTimeout = 30;
command.CommandType = CommandType.StoredProcedure;

//init param data
var nums = new List<(int Num1, int Num2)>() { (1, 1), (2, 2), (3, 3) };

//populate dt parameter
var dtNums = new DataTable();
dtNums.Columns.Add(new DataColumn("Num1", typeof(int)));
dtNums.Columns.Add(new DataColumn("Num2", typeof(int)));

//Total column defined in datatable as readonly, but not populated on rows because is computed, we cannot pass the value to it.
var totalColumn = new DataColumn("Total", typeof(int));
totalColumn.ReadOnly = true;

dtNums.Columns.Add(totalColumn);

foreach (var num in nums)
{
var dataRow = dtNums.NewRow();
dataRow["Num1"] = num.Num1;
dataRow["Num2"] = num.Num2;
dtNums.Rows.Add(dataRow);
}

//pass dt parameter to procedure
command.Parameters.Add(new SqlParameter
{
ParameterName = "@numsWithComputed",
SqlDbType = SqlDbType.Structured,
TypeName = "[dbo].[udttNumsWithCompute]",
Value = dtNums
});

using (var reader = await command.ExecuteReaderAsync())
{
//if passes here, then it worked.
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}

}

private async Task TryToPassUdttWithComputedAsStructuredDataTableParamSimple5()
{
try
{
var command = Connection.CreateCommand();
command.CommandText = "[dbo].[spTestUdttWithComputed]";
command.CommandTimeout = 30;
command.CommandType = CommandType.StoredProcedure;

//init param data
var nums = new List<(int Num1, int Num2)>() { (1, 1), (2, 2), (3, 3) };

//populate dt parameter
var dtNums = new DataTable();
dtNums.Columns.Add(new DataColumn("Num1", typeof(int)));
dtNums.Columns.Add(new DataColumn("Num2", typeof(int)));

//Total column defined in datatable as readonly and populated on rows
var totalColumn = new DataColumn("Total", typeof(int));
totalColumn.ReadOnly = true;

dtNums.Columns.Add(totalColumn);

foreach (var num in nums)
{
var dataRow = dtNums.NewRow();
dataRow["Num1"] = num.Num1;
dataRow["Num2"] = num.Num2;
dataRow["Total"] = 0; //Dummy value of total!
dtNums.Rows.Add(dataRow);
}

//pass dt parameter to procedure
command.Parameters.Add(new SqlParameter
{
ParameterName = "@numsWithComputed",
SqlDbType = SqlDbType.Structured,
TypeName = "[dbo].[udttNumsWithCompute]",
Value = dtNums
});

using (var reader = await command.ExecuteReaderAsync())
{
//if passes here, then it worked.
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}

}

private async Task TryToPassUdttWithComputedAsStructuredDataTableParamSimple6()
{
try
{
var command = Connection.CreateCommand();
command.CommandText = "[dbo].[spTestUdttWithComputed]";
command.CommandTimeout = 30;
command.CommandType = CommandType.StoredProcedure;

//init param data
var nums = new List<(int Num1, int Num2)>() { (1, 1), (2, 2), (3, 3) };

//populate dt parameter
var dtNums = new DataTable();
dtNums.Columns.Add(new DataColumn("Num1", typeof(int)));
dtNums.Columns.Add(new DataColumn("Num2", typeof(int)));

//Total column defined in datatable as readonly and populated on rows with default of (int)
var totalColumn = new DataColumn("Total", typeof(int));
totalColumn.ReadOnly = true;

dtNums.Columns.Add(totalColumn);

foreach (var num in nums)
{
var dataRow = dtNums.NewRow();
dataRow["Num1"] = num.Num1;
dataRow["Num2"] = num.Num2;
dataRow["Total"] = default(int);
dtNums.Rows.Add(dataRow);
}

//pass dt parameter to procedure
command.Parameters.Add(new SqlParameter
{
ParameterName = "@numsWithComputed",
SqlDbType = SqlDbType.Structured,
TypeName = "[dbo].[udttNumsWithCompute]",
Value = dtNums
});

using (var reader = await command.ExecuteReaderAsync())
{
//if passes here, then it worked.
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}

}
#endregion

#region As SqlDataRecord
private async Task TryToPassUdttWithoutComputedAsStructuredDataRecordParamSimple1()
{
try
{
var command = Connection.CreateCommand();
command.CommandText = "[dbo].[spTestUdttWithoutComputed]";
command.CommandTimeout = 30;
command.CommandType = CommandType.StoredProcedure;

//init param data
var nums = new List<(int Num1, int Num2)>() { (1, 1), (2, 2), (3, 3) };

//populate records parameter
SqlMetaData[] metaData = new SqlMetaData[2];
metaData[0] = new SqlMetaData("Num1", SqlDbType.Int);
metaData[1] = new SqlMetaData("Num2", SqlDbType.Int);

List records = new List();

foreach (var num in nums)
{
SqlDataRecord record = new SqlDataRecord(metaData);
record.SetInt32(0, num.Num1);
record.SetInt32(1, num.Num2);
records.Add(record);
}

//pass dt parameter to procedure
command.Parameters.Add(new SqlParameter
{
ParameterName = "@numsWithoutComputed",
SqlDbType = SqlDbType.Structured,
TypeName = "[dbo].[udttNumsWithoutCompute]",
Value = records
});

using (var reader = await command.ExecuteReaderAsync())
{
//if passes here, then it worked.
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}

}

private async Task TryToPassUdttWithComputedAsStructuredDataRecordParamSimple1()
{
try
{
var command = Connection.CreateCommand();
command.CommandText = "[dbo].[spTestUdttWithComputed]";
command.CommandTimeout = 30;
command.CommandType = CommandType.StoredProcedure;

//init param data
var nums = new List<(int Num1, int Num2)>() { (1, 1), (2, 2), (3, 3) };

//populate records parameter
SqlMetaData[] metaData = new SqlMetaData[2];
metaData[0] = new SqlMetaData("Num1", SqlDbType.Int);
metaData[1] = new SqlMetaData("Num2", SqlDbType.Int);

//Total column omitted from metadata, because is computed, we cannot pass the value to it.

List records = new List();

foreach (var num in nums)
{
SqlDataRecord record = new SqlDataRecord(metaData);
record.SetInt32(0, num.Num1);
record.SetInt32(1, num.Num2);
records.Add(record);
}

//pass dt parameter to procedure
command.Parameters.Add(new SqlParameter
{
ParameterName = "@numsWithComputed",
SqlDbType = SqlDbType.Structured,
TypeName = "[dbo].[udttNumsWithCompute]",
Value = records
});

using (var reader = await command.ExecuteReaderAsync())
{
//if passes here, then it worked.
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}

}

private async Task TryToPassUdttWithComputedAsStructuredDataRecordParamSimple2()
{
try
{
var command = Connection.CreateCommand();
command.CommandText = "[dbo].[spTestUdttWithComputed]";
command.CommandTimeout = 30;
command.CommandType = CommandType.StoredProcedure;

//init param data
var nums = new List<(int Num1, int Num2)>() { (1, 1), (2, 2), (3, 3) };

//populate records parameter
SqlMetaData[] metaData = new SqlMetaData[3];
metaData[0] = new SqlMetaData("Num1", SqlDbType.Int);
metaData[1] = new SqlMetaData("Num2", SqlDbType.Int);

//Total column defined in metadata, but not populated on records because is computed, we cannot pass the value to it.
metaData[2] = new SqlMetaData("Total", SqlDbType.Int);

List records = new List();

foreach (var num in nums)
{
SqlDataRecord record = new SqlDataRecord(metaData);
record.SetInt32(0, num.Num1);
record.SetInt32(1, num.Num2);
records.Add(record);
}

//pass dt parameter to procedure
command.Parameters.Add(new SqlParameter
{
ParameterName = "@numsWithComputed",
SqlDbType = SqlDbType.Structured,
TypeName = "[dbo].[udttNumsWithCompute]",
Value = records
});

using (var reader = await command.ExecuteReaderAsync())
{
//if passes here, then it worked.
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}

}

private async Task TryToPassUdttWithComputedAsStructuredDataRecordParamSimple3()
{
try
{
var command = Connection.CreateCommand();
command.CommandText = "[dbo].[spTestUdttWithComputed]";
command.CommandTimeout = 30;
command.CommandType = CommandType.StoredProcedure;

//init param data
var nums = new List<(int Num1, int Num2)>() { (1, 1), (2, 2), (3, 3) };

//populate records parameter
SqlMetaData[] metaData = new SqlMetaData[3];
metaData[0] = new SqlMetaData("Num1", SqlDbType.Int);
metaData[1] = new SqlMetaData("Num2", SqlDbType.Int);

//Total column defined in metadata and dummy value assigned on records
metaData[2] = new SqlMetaData("Total", SqlDbType.Int);

List records = new List();

foreach (var num in nums)
{
SqlDataRecord record = new SqlDataRecord(metaData);
record.SetInt32(0, num.Num1);
record.SetInt32(1, num.Num2);
record.SetInt32(2, 0); //Dummy value of total!
records.Add(record);
}

//pass dt parameter to procedure
command.Parameters.Add(new SqlParameter
{
ParameterName = "@numsWithComputed",
SqlDbType = SqlDbType.Structured,
TypeName = "[dbo].[udttNumsWithCompute]",
Value = records
});

using (var reader = await command.ExecuteReaderAsync())
{
//if passes here, then it worked.
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}

}

private async Task TryToPassUdttWithComputedAsStructuredDataRecordParamSimple4()
{
try
{
var command = Connection.CreateCommand();
command.CommandText = "[dbo].[spTestUdttWithComputed]";
command.CommandTimeout = 30;
command.CommandType = CommandType.StoredProcedure;

//init param data
var nums = new List<(int Num1, int Num2)>() { (1, 1), (2, 2), (3, 3) };

//populate records parameter
SqlMetaData[] metaData = new SqlMetaData[3];
metaData[0] = new SqlMetaData("Num1", SqlDbType.Int);
metaData[1] = new SqlMetaData("Num2", SqlDbType.Int);

//Total column defined in metadata as use server value, but not populated on records because is computed, we cannot pass the value to it. !Hox. Did not find a way to set useServerDefault to true without setting isUniqueKey and sorting!
metaData[2] = new SqlMetaData("Total", SqlDbType.Int, useServerDefault:true, isUniqueKey:false, columnSortOrder: SortOrder.Ascending, sortOrdinal:0);

List records = new List();

foreach (var num in nums)
{
SqlDataRecord record = new SqlDataRecord(metaData);
record.SetInt32(0, num.Num1);
record.SetInt32(1, num.Num2);
records.Add(record);
}

//pass dt parameter to procedure
command.Parameters.Add(new SqlParameter
{
ParameterName = "@numsWithComputed",
SqlDbType = SqlDbType.Structured,
TypeName = "[dbo].[udttNumsWithCompute]",
Value = records
});

using (var reader = await command.ExecuteReaderAsync())
{
//if passes here, then it worked.
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}

}

private async Task TryToPassUdttWithComputedAsStructuredDataRecordParamSimple5()
{
try
{
var command = Connection.CreateCommand();
command.CommandText = "[dbo].[spTestUdttWithComputed]";
command.CommandTimeout = 30;
command.CommandType = CommandType.StoredProcedure;

//init param data
var nums = new List<(int Num1, int Num2)>() { (1, 1), (2, 2), (3, 3) };

//populate records parameter
SqlMetaData[] metaData = new SqlMetaData[3];
metaData[0] = new SqlMetaData("Num1", SqlDbType.Int);
metaData[1] = new SqlMetaData("Num2", SqlDbType.Int);

//Total column defined in metadata as use server value, but not populated on records because is computed, we cannot pass the value to it. !Hox. Did not find a way to set useServerDefault to true without setting isUniqueKey and sorting!
metaData[2] = new SqlMetaData("Total", SqlDbType.Int, useServerDefault: true, isUniqueKey: false, columnSortOrder: SortOrder.Ascending, sortOrdinal: 0);

List records = new List();

foreach (var num in nums)
{
SqlDataRecord record = new SqlDataRecord(metaData);
record.SetInt32(0, num.Num1);
record.SetInt32(1, num.Num2);
record.SetInt32(2, 0); //Dummy value of total!
records.Add(record);
}

//pass dt parameter to procedure
command.Parameters.Add(new SqlParameter
{
ParameterName = "@numsWithComputed",
SqlDbType = SqlDbType.Structured,
TypeName = "[dbo].[udttNumsWithCompute]",
Value = records
});

using (var reader = await command.ExecuteReaderAsync())
{
//if passes here, then it worked.
}
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}

}
#endregion

#endregion

}
```

sql - udttNumsWithCompute
```
CREATE TYPE [dbo].[udttNumsWithCompute] AS TABLE(
[Num1] INT NULL,
[Num2] INT NULL,
[Total] AS (ISNULL([Num1], 0) + ISNULL([Num2], 0)) --COMPUTE COLUMN
)
GO
```

sql - udttNumsWithoutCompute
```
CREATE TYPE [dbo].[udttNumsWithoutCompute] AS TABLE(
[Num1] INT NULL,
[Num2] INT NULL
)
GO
```

sql - spTestUdttWithComputed
```
/******************************************************************
*
* PROCEDURE: [spTestUdttWithComputed]
* This will try to pass udtt with computed column into a procedure as parameter.
******************************************************************/
CREATE OR ALTER PROC [dbo].[spTestUdttWithComputed]
(
@numsWithComputed [dbo].[udttNumsWithCompute] READONLY
)
AS
SET NOCOUNT ON;

DECLARE @context NVARCHAR(255) = '[spTestUdttWithComputed]'

SELECT * FROM @numsWithComputed;

GO

```

sql - spTestUdttWithoutComputed
```
/******************************************************************
*
* PROCEDURE: [spTestUdttWithoutComputed]
* This will try to pass udtt without computed column into a procedure as parameter.
******************************************************************/
CREATE OR ALTER PROC [dbo].[spTestUdttWithoutComputed]
(
@numsWithoutComputed [dbo].[udttNumsWithoutCompute] READONLY
)
AS
SET NOCOUNT ON;

DECLARE @context NVARCHAR(255) = '[spTestUdttWithoutComputed]'

SELECT * FROM @numsWithoutComputed;

GO
```

### Expected behavior
The library should provide a way to configure the functionality of SqlParameter.Structured so that it would skip computed columns and let the server resolve the value in the udtt, once the rest of the fields are populated that the computed column depends upon.

### Further technical details
Microsoft.Data.SqlClient version: 6.0.1
.NET target: .NET 8, .NET 9, .NET 10
SQL Server version: SQL Server 2022 (16.0.1135.2)
Operating system: Microsoft Windows 11 Business (10.0.22631)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.