Per la serie “Non si finisce mai di imparare“, un bel giorno ci siamo accorti di un baco all’interno di ADO.NET (versione 1.1) che si manifesta in concomitanza di una serie precisa di eventi: apriamo la connessione al database (SqlConnection), iniziamo una transazione (SqlTransaction), tramite un SqlCommand richiamiamo la stored procedure passandogli la connessione, la transazione ed i suoi parametri correttamente valorizzati e… per qualche motivo l’unico risultato che si ottiene è un errore che ci informa che la stored procedure deve essere eseguita all’interno della transazione che risulta aperta sulla connessione. Ma come? Io ho passato la transazione all’SqlCommand!
Per venirne a capo abbiamo dovuto fare ricorso, oltre che alle tecniche di debug classiche, anche al buon vecchio Reflector: nel codice, ad un certo punto veniva invocato un SqlCommandBuilder.DeriveParameters: questo metodo, che è definito come Shared (credo si traduca in static per gli amanti di C#), è richiamabile dalla classe senza bisogno di instanziare un oggetto.
Nello screenshot sottostante vi mostro parte del codice incriminato (le righe evidenziate sono quelle che riportano effettivamente l’errore):

La soluzione è apparsa subito semplice: riscriviamoci la DeriveParameters (tanto più che è Shared, quindi in teoria non necessita di proprietà particolari della class SqlCommandBuilder), aggiungendo la valorizzazione della transazione nelle due righe incriminate e siamo a posto. Più o meno…
Perché, tanto per cominciare nelle prime righe righe del codice si fa riferimento ad una serie di eccezioni definite nella classe ADP, definita come Friend nella namespace System.Data.
Perciò abbiamo cominciato a riscrivere il codice di DeriveParameters:
Private Function DeriveParameters(ByRef cmd As SqlCommand) As Boolean
Dim result As Boolean
Dim cmdText As String
Dim strArray As String()
Dim command As SqlCommand
Dim list As ArrayList
result = (cmd.CommandType = CommandType.StoredProcedure)
If result Then
strArray = ParseProcedureName(cmd.CommandText)
If (Not strArray(1) Is Nothing) Then
cmdText = ("[" & strArray(1) & "]..sp_procedure_params_rowset")
If (Not strArray(0) Is Nothing) Then
cmdText = (strArray(0) & "." & cmdText)
End If
Else
cmdText = "sp_procedure_params_rowset"
End If
command = New SqlCommand(cmdText, cmd.Connection, cmd.Transaction)
command.CommandType = CommandType.StoredProcedure
command.Parameters.Add(New SqlParameter("@procedure_name", SqlDbType.NVarChar, &HFF))
command.Parameters.Item(0).Value = strArray(3)
list = New ArrayList
Try
Dim ds As New DataSet
Dim dataAdapter As New SqlDataAdapter(command)
dataAdapter.Fill(ds)
Dim dtParms As DataTable = ds.Tables(0)
Dim parameter As SqlParameter
For Each dr As DataRow In dtParms.Rows
parameter = New SqlParameter
parameter.ParameterName = CStr(dr("PARAMETER_NAME"))
parameter.SqlDbType = GetSqlDbTypeFromOleDbType(CShort(dr("DATA_TYPE")), CStr(dr("TYPE_NAME")))
Dim obj2 As Object = dr("CHARACTER_MAXIMUM_LENGTH")
If TypeOf obj2 Is Integer Then
parameter.Size = CInt(obj2)
End If
parameter.Direction = ParameterDirectionFromOleDbDirection(CShort(dr("PARAMETER_TYPE")))
If (parameter.SqlDbType = SqlDbType.Decimal) Then
parameter.Scale = CByte((CShort(dr("NUMERIC_SCALE")) And &HFF))
parameter.Precision = CByte((CShort(dr("NUMERIC_PRECISION")) And &HFF))
End If
list.Add(parameter)
Next
Catch ex As Exception
result = False
End Try
If result _
AndAlso (list.Count = 0) Then
result = False
End If
If result Then
cmd.Parameters.Clear()
Dim param As Object
For Each param In list
cmd.Parameters.Add(param)
Next
End If
End If ' result = (cmd.CommandType = CommandType.StoredProcedure)
Return result
End Function
Nel fare ciò, è stato necessario riscrivere anche alcune procedure che erano richiamate dalla DeriveParameters originale:
Private Shared Function ParseProcedureName(ByVal procedure As String) As String()
Dim strArray As String() = New String(3) {}
Dim temp As String()
If (Not procedure Is Nothing) _
AndAlso (procedure.Length > 0) Then
temp = procedure.Split("."c)
Select Case temp.Length
Case 1
strArray(3) = procedure
strArray(2) = Nothing
strArray(1) = Nothing
strArray(0) = Nothing
Case 2
strArray(3) = temp(1)
strArray(2) = temp(0)
strArray(1) = Nothing
strArray(0) = Nothing
Case 3
strArray(3) = temp(2)
strArray(2) = temp(1)
strArray(1) = temp(0)
strArray(0) = Nothing
Case 4
strArray(3) = temp(3)
strArray(2) = temp(2)
strArray(1) = temp(1)
strArray(0) = temp(0)
End Select
End If
Return strArray
End Function
Private Function ParameterDirectionFromOleDbDirection(ByVal oledbDirection As Short) As ParameterDirection
Dim result As ParameterDirection
Select Case oledbDirection
Case 2
result = ParameterDirection.InputOutput
Case 3
result = ParameterDirection.Output
Case 4
result = ParameterDirection.ReturnValue
Case Else
result = ParameterDirection.Input
End Select
Return result
End Function
Friend Shared Function GetSqlDbTypeFromOleDbType(ByVal dbType As Short, ByVal typeName As String) As SqlDbType
Dim result As SqlDbType
Dim oleType As OleDbType = CType(dbType, OleDbType)
result = SqlDbType.Variant
Select Case oleType
Case OleDbType.SmallInt, OleDbType.UnsignedSmallInt
result = SqlDbType.SmallInt
Case OleDbType.Integer
result = SqlDbType.Int
Case OleDbType.Single
result = SqlDbType.Real
Case OleDbType.Double
result = SqlDbType.Float
Case OleDbType.Currency
If (typeName = "money") Then
result = SqlDbType.Money
Else
result = SqlDbType.SmallMoney
End If
Case OleDbType.Date, OleDbType.Filetime
If (typeName = "datetime") Then
result = SqlDbType.DateTime
Else
result = SqlDbType.SmallDateTime
End If
Case OleDbType.BSTR
If (typeName = "nchar") Then
result = SqlDbType.NChar
Else
result = SqlDbType.NVarChar
End If
Case OleDbType.IDispatch, OleDbType.Error, OleDbType.IUnknown, CType(15, OleDbType), OleDbType.UnsignedInt
result = SqlDbType.Variant
Case OleDbType.Boolean
result = SqlDbType.Bit
Case OleDbType.Variant
result = SqlDbType.Variant
Case OleDbType.Decimal
result = SqlDbType.Decimal
Case OleDbType.TinyInt, OleDbType.UnsignedTinyInt
result = SqlDbType.TinyInt
Case OleDbType.BigInt
result = SqlDbType.BigInt
Case OleDbType.Binary, OleDbType.VarBinary
If (typeName = "binary") Then
result = SqlDbType.Binary
Else
result = SqlDbType.VarBinary
End If
Case OleDbType.Char, OleDbType.VarChar
If (typeName = "char") Then
result = SqlDbType.Char
Else
result = SqlDbType.VarChar
End If
Case OleDbType.WChar, OleDbType.VarWChar
If (typeName = "nchar") Then
result = SqlDbType.NChar
Else
result = SqlDbType.NVarChar
End If
Case OleDbType.Numeric
result = SqlDbType.Decimal
Case (OleDbType.Binary Or OleDbType.Single)
result = result
Case OleDbType.DBDate, OleDbType.DBTime, OleDbType.DBTimeStamp
If (typeName = "datetime") Then
result = SqlDbType.DateTime
Else
result = SqlDbType.SmallDateTime
End If
Case OleDbType.Guid
result = SqlDbType.UniqueIdentifier
Case OleDbType.LongVarChar
result = SqlDbType.Text
Case OleDbType.LongVarWChar
result = SqlDbType.NText
Case OleDbType.LongVarBinary
result = SqlDbType.Image
End Select
Return result
End Function