Importance of closing SQLConnection objects when using the SQLDataReader object - vb.net

My present contract engagement is at a large E-Commerce company. Their code base which has origins going back to .Net 1.0 has caught me by surprise to contain many issues that raise the level of smell beyond the last crap I took.
That notwithstanding and trying to diffuse my level of distraction from it, I go along merrily trying to add in features to either fix other problems or extend more crap. Where I touch the DAL/BLL the time it will take to fix the aforementioned will be done. However I wanted to get a vote of confidence from the experts to get some assurance of not wasting the clients time or worse having my credibility voted down by touching "stuff that works". Of course unit testing would solve or at least soften this worry. Perhaps this should also be added to the wtf.com?
Public Function GetSizeInfoBySite(ByVal siteID As String) As IList
Dim strSQL As String = "YES INLINE SQL!! :)"
Dim ci As CrapInfo
Dim alAnArrayList As ArrayList
Dim cn As New SqlConnection(ConfigurationSettings.AppSettings("ConnectionString"))
Dim cmd As New SqlCommand(strSQL, cn)
cmd.Parameters.Add(New SqlParameter("#MySiteID", SqlDbType.NVarChar, 2)).Value = siteID
cn.Open()
Dim rs As SqlDataReader = cmd.ExecuteReader(CommandBehavior.CloseConnection)
While rs.Read()
ci = New CategoryInfo(rs("someID"), rs("someName"))
If IsNothing(alAnArrayList) Then
alAnArrayList = New ArrayList
End If
alAnArrayList.Add(ci)
End While
rs.Close()
Return CType(alAnArrayList, IList)
End Function
Does anyone see problems with this aside from the inline SQL which makes my gut churn? At the least wouldn't you ordinarily wrap the above in a try/catch/finally which most of us knows has been around since .Net v1.0? Even better would'nt it be wise to fix with Using statements? Does the SQLDataReader close really encapsulate the connection close automagically?

Nothing wrong with inline sql if the user input is properly parameterized, and this looks like it is.
Other than that, yes you do need to close the connections. On a busy web site you could hit your limit and that would cause all kinds of weirdness.
I also noticed it's still using an arraylist. Since they've moved on from .Net 1.0 it's time to update those to generic List<T>'s (and avoid the call to CType- you should be able to DirectCast() that instead).

Definitely get some using statements around the Connection and Reader objects. If there is an exception, they won't be closed until the Garbage Collector gets around to them.
I tend not to call .Close() when there are using statements. Even if the SqlDataReader closes the connection on dispose (check the doco), putting a using around the Connection can't hurt and sticks to the pattern .
If you do that the try/finally is only needed if you need to do exception handling right there. I tend to leave exception handling at the higher levels (wrap each UI entry point, Library entry points, extra info in exception) as the stacktrace is usually enough to debug the errors.
Not that it matters much, but if you are re-factoring, move the collection initialization outside the loop. On second thoughts the code returns null if there are no records.
At least SqlParameters are used! Get rid of anything that concatenates user input with SQL if you find it (SQL Injection attack) no matter how well "Cleaned" it is.

The connection will be closed when the reader is closed because it's using the CloseConnection command behavior.
Dim rs As SqlDataReader = cmd.ExecuteReader(CommandBehavior.CloseConnection)
According to MSDN (http://msdn.microsoft.com/en-us/library/aa326246(VS.71).aspx)
If the SqlDataReader is created with CommandBehavior set to CloseConnection, closing the SqlDataReader closes the connection automatically.

In reply to some of the great points indicated by Joel and Robert I refactored the method as follows which ran flawless.
Public Function GetSomeInfoByBusObject(ByVal SomeID As String) As IList
Dim strSQL As String = "InLine SQL"
Dim ci As BusObject
Dim list As New GenList(Of BusObject)
Dim cn As New SqlConnection(
ConfigurationSettings.AppSettings("ConnectionString"))
Using cn
Dim cmd As New SqlCommand(strSQL, cn)
Using cmd
cmd.Parameters.Add(New SqlParameter
("#SomeID", SqlDbType.NVarChar, 2)).Value = strSiteID
cn.Open()
Dim result As SqlDataReader = cmd.ExecuteReader(CommandBehavior.CloseConnection)
While result.Read()
ci = New BusObject(rs("id), result("description"))
list.Add(DirectCast(ci, BusObject))
End While
result.Close()
End Using
Return list
End Using
End Function
Created a nice little helper class to wrap the generic details up
Public Class GenList(Of T)
Inherits CollectionBase
Public Function Add(ByVal value As T) As Integer
Return List.Add(value)
End Function
Public Sub Remove(ByVal value As T)
List.Remove(value)
End Sub
Public ReadOnly Property Item(ByVal index As Integer) As T
Get
Return CType(List.Item(index), T)
End Get
End Property
End Class

If you were using c# I would wrap the datareader creation in a using statement but I don't think vb has those?

Public Function GetSizeInfoBySite(ByVal siteID As String) As IList(Of CategoryInfo)
Dim strSQL As String = "YES INLINE SQL!! :)"
'reference the 2.0 System.Configuration, and add a connection string section to web.config
' <connectionStrings>
' <add name="somename" connectionString="someconnectionstring" />
' </connectionStrings >
Using cn As New SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings("somename").ConnectionString
Using cmd As New SqlCommand(strSQL, cn)
cmd.Parameters.Add(New SqlParameter("#MySiteID", SqlDbType.NVarChar, 2)).Value = siteID
cn.Open()
Using reader As IDataReader = cmd.ExecuteReader()
Dim records As IList(Of CategoryInfo) = New List(Of CategoryInfo)
'get ordinal col indexes
Dim ordinal_SomeId As Integer = reader.GetOrdinal("someID")
Dim ordinal_SomeName As Integer = reader.GetOrdinal("someName")
While reader.Read()
Dim ci As CategoryInfo = New CategoryInfo(reader.GetInt32(ordinal_SomeId), reader.GetString(ordinal_SomeName))
records.Add(ci)
End While
Return records
End Using
End Using
End Using
End Function
You could try something like the above, the using statements will handle connection closing and object disposal. This is available when the class implements IDisposable. Also build and return your IList of CategoryInfo.

Related

VB.net connection string works, then doesn't, then does - why?

Using VB.NET, VS 19 v16.10.4
I have a small application which asks a user to provide database server, database name and then builds a connection string. Then, using a data access layer DLL I've written it runs a check to see if a connection can be made to the database.
The problem is unusual:
If I write the connection string direct to the data access layer into a variable it connects;
If I use a string builder to build the string then pass it to the data access layer it fails;
If I write the connection string into a variable then pass it to the data access layer it fails;
If I use the first step again it works.
With DAL
MessageBox.Show("Using hard-coded string")
.ConnectionString = "data source=THEWINELIBRARY\MSSQLSERVER01;initial catalog=TrialDatabase;trusted_connection=true"
.DoConnectionCheck() 'THIS WORKS
MessageBox.Show("Using string builder string")
.ConnectionString = SBConnString.ToString.Trim
.DoConnectionCheck() 'THIS FAILS
MessageBox.Show("Using CS string")
.ConnectionString = CS.Trim
.DoConnectionCheck() 'THIS FAILS
MessageBox.Show("Using hard-coded string")
.ConnectionString = "data source=THEWINELIBRARY\MSSQLSERVER01;initial catalog=TrialDatabase;trusted_connection=true"
.DoConnectionCheck() 'THIS WORKS
End With
The data access layer does the following in the DoConnectionCheck method:
Public Sub DoConnectionCheck()
OpenConnection()
With AppConnection
If (.State = ConnectionState.Open) Then
RaiseEvent ConnectionOpened()
CloseConnection()
End If
End With
End Sub
and the OpenConnection method:
Private Sub OpenConnection()
With AppConnection
'Is the conenction currently closed?
If (.State = ConnectionState.Closed) Then
Try
'Set connection string to POS
.ConnectionString = ConnectionString
'Try opening the connection
.Open()
'Otherwise raise an event.
Catch E As Exception
RaiseEvent ConnectionFailed()
End Try
End If
End With
End Sub
And the connection string is a simple property:
Public ConnectionString As String
So, what I don't understand is why the connection fails with the string builder string and the variable string, but not with the hard-coded string?
If there is any other code you need please let me know, I posted what i think is enough to explain the problem without putting in too much that makes the question unreadable.
here is the code which constructs the connection string from entries in a form:
Dim CS As String = "datasource=" & .TextServer.Text.Trim & ";initial catalog=" & .TextDatabase.Text.Trim & ";trusted_connection=true"
SBConnString = New StringBuilder
With SBConnString
.Clear()
.Append("datasource=")
.Append(Me.TextServer.Text.Trim)
.Append(";initial catalog=")
.Append(Me.TextDatabase.Text.Trim)
.Append(";trusted_connection=true")
End With
When these strings are compared to the hard coded string
DAL.ConnectionString = "data source=THEWINELIBRARY\MSSQLSERVER01;initial catalog=TrialDatabase;trusted_connection=true"
the function returns a value 1, "The first substring follows the second substring in the sort order." according to the documentation.
As far as I can see, these strings are identical but somewhere an additional character must be getting inserted in the form textbox.
Again, any suggestions gratefully received.
All the best,
Dermot
Mea culpa - I had a very simple mistake which I could not see - in the two strings causing the problem I had datasource rather than date source...

VB SQL CommandText property has not been initialized

I'm new to working with background workers, and I'm trying to run the following code. But I recieve a run-time error on the m._Value_CreatedDate = m._MyCMD.ExecuteScalar() line. The error is:
Additional information: ExecuteScalar: CommandText property has not
been initialized
Try
Dim m As MyParameters = DirectCast(e.Argument, MyParameters)
m._Con.Open()
m._QueryStr = "SELECT TOP 1 CONVERT(varchar(10),aCreated,103) FROM Account WHERE aMember_ID = " & m._Selected_MemberID & ""
m._MyCMD.CommandType = CommandType.Text
m._Value_CreatedDate = m._MyCMD.ExecuteScalar()
Catch ex As Exception
m._Value_CreatedDate = "N/A"
End Try
Here's the Parameter's I'm using:
Class MyParameters
Friend _QueryStr As String
Friend _Value_CreatedDate As Object
Friend _AccountID As Object
Friend _Selected_MemberID As String = Committee_Database.GridView1.GetFocusedRowCellValue("Unique ID").ToString
Friend _Con As New SqlConnection('Connection string ommitted)
Friend _MyCMD As New SqlCommand(_QueryStr, _Con)
End Class
Please forgive me if I'm doing something extremely wrong, I'm self taught and experimenting with the backgroundworker. It's worth noting that _QueryStr will change multiple times as the background worker runs multiple queries against the same database and (as I understand it) stores each of the returned values from the queries into variables - _Value_CreatedDate is the variable I am using in this code. I've included an example of how I'm recycling the _QueryStr variable below and storing the returned value into a diffent Variable each time.
Try
m._QueryStr = "SELECT TOP 1 aAccount_ID FROM Account WHERE aUser_Name='" & _Selected_AccountID & "'"
m._MyCMD.CommandType = CommandType.Text
m._AccountID = m._MyCMD.ExecuteScalar()
Catch ex As Exception
End Try
Am I doing something wrong?
In the implementation of your class MyParameters you initialize the SqlCommand directly with the declaration using the value of the variable _QueryStr. At that point in time the variable _QueryStr is not yet initialized, so it is an empty string.
After the initialization of an instance of MyParameters, you change the value of _QueryStr (many times according to you) but these changes are not automatically passed to the CommandText of your SqlCommand. It still contains the empty initial value.
You could fix this problem building appropriate getter and setter for a new QueryStr property. When someone tries to set the property the code below change the value of the internal field _QueryStr (now private) and reinitializes the CommandText property of your SqlCommand.
Class MyParameters
Private _QueryStr As String
Public Property QueryStr() As String
Get
Return _QueryStr
End Get
Set(ByVal value As String)
_QueryStr = value
_MyCMD.CommandText = _QueryStr
End Set
End Property
..... other properties
Friend _MyCMD As New SqlCommand(_QueryStr, _Con)
End Class
Now when you write
Try
m.QueryStr = "SELECT ...."
....
the new command text is correctly assigned to your command.
As a side note: I suggest to use plain ADO.NET objects (or learn how to use an ORM tool). Do not try to encapsulate them in custom classes unless you have a very deep understanding on how these ADO.NET objects works. You gain nothing and expose your code to many problems. For example your code is very easy to exploit with Sql Injection because the MyParameters class has no provision to use a parameterized query. Your code has no method to close and dispose the SqlConnection embedded in your class thus leading to resource leaks.

How do I store a set of SqlCommands and apply or discard them on form close in vb.net?

I have a form with information pulled from SQL that can be edited, once the form is closed I want to either write any changes back to the database or discard them. I don't want to store two sets of the data for comparison and I don't want to re-query SQL for the same.
I came up with the idea of creating a List of SQLCommand items and processing them if changes are to be saved however I get the following error when trying to add a SQLCommand to the list:
An unhandled exception of type 'System.NullReferenceException' occurred in Application1.exe
Additional information: Object reference not set to an instance of an object.
I've probably missed something really simple but I can't see what myself.
My code is below:
Public Class frm1
Public Property ContID As String
Dim PendingSQLChanges As List(Of SqlCommand)
Private Sub btnAddTel_Click(sender As Object, e As EventArgs) Handles btnAddTel.Click
Using x As New SqlCommand("INSERT into ContactTels (Tel, CoID) Values (#NewValue, #ContID) ;--", cnn)
x.Parameters.Add("#NewValue", SqlDbType.NVarChar, 100).Value = strNewvalue
x.Parameters.Add("#ContID", SqlDbType.Int).Value = ContID
PendingSQLChanges.Add(x) <This is the line I get the error on.
btnSave.Enabled = True
End Using
End Sub
As you can see I am adding parameters to the SQLCommand otherwise I would have used a List of strings containing the SQLCommand.Commandtext and used that later.
If I can get the list working the code I will use to process the saved commands is:
For x = 0 To PendingSQLChanges.Count - 1
PendingSQLChanges(x).ExecuteNonQuery()
Next
Is that correct as well?
Try using New in your List(of SqlCommand) declaration:
Dim PendingSQLChanges As New List(Of SqlCommand)
It was not instantiated thus the Null Reference.

Thread safe variable in VB.NET

Is this the right way of declaration dbreaders when mulitple users access the same page?
public dbReader as system.Data.IDataReader at class level or
Dim dbReader as System.Data.IDataReader in each function inside a class.
What would be the best practice to make the dbReader thread safe in VB.Net?
Does declaring them as static makes it thread safe?
Thanks in Advance,
If you'd like each thread to modify the variable without 'fear' another thread will change it somewhere along the line, it is best you adorn the variable with the ThreadStatic attribute.
The ThreadStatic attribute creates a different instance of the variable for each thread that's created so you're confident there won't be any race conditions.
Example (from MSDN)
Imports System
<ThreadStatic> Shared value As Integer
I would recommend you using reentrant functions when possible which are thread safe by definition instead of using class fields:
Function GetIds() As IEnumerable(Of Integer)
Dim result = New List(Of Integer)()
Using conn = New SqlConnection("SomeConnectionString")
Using cmd = conn.CreateCommand()
conn.Open()
cmd.CommandText = "SELECT id FROM foo"
Using reader = cmd.ExecuteReader()
While reader.Read()
result.Add(reader.GetInt32(0))
End While
End Using
End Using
End Using
Return result
End Function
If you are Diming the variable in a function, no other thread can access that variable making it thread-safe by definition.
However, if you are declaring it at a class level, you might want to use SyncLock which will prevent other threads from accessing it if it is currently being used by an other one.
Example:
Public Sub AccessVariable()
SyncLock Me.dbReader
'Work With dbReader
End SyncLock
End Sub

Disposing of custom Data Access Layer references

Our application uses a custom DataAccessLayer class almost exclusively, and within that we do use Data Access Application Block (currently version 2). We are getting the infamous "GetOrdinal" error sporadically. We are not using out-of-method connections. We are using DAAB version 2. Below is a typical example of our DAL methods:
Public Function MyDALMethod(ByVal Param1 As Integer, ByVal Param2 As Integer) As System.Data.IDataReader
Dim db As Database = DatabaseFactory.CreateDatabase()
Dim sqlCommand As String = "usp_MyProcedure"
Dim dbCommand As DbCommand = db.GetStoredProcCommand(sqlCommand)
db.AddInParameter(dbCommand, "Param1", DbType.Int32, MyParam1)
db.AddInParameter(dbCommand, "Param2", DbType.Int32, MyParam2)
Return db.ExecuteReader(dbCommand)
End Function
In our code we just instantiate a DAL var and call the desired method. After using the DataReader, the referencing code will close, dispose, and set the reader to nothing. However, nothing is done with the reference to the DAL. I've wondered if this is part of our problem. A typical method would use our DAL like this:
Dim DAL as New DataAccessLayer
Dim dbReader as system.data.idatareader
dbreader = DAL.MyDALMethod(parm1, parm2)
While dbreader.read
i = dbreader.items("column1")
j = dbreader.items("column2")
End While
dbreader.close()
dbreader.dispose()
dbreader = nothing
My main question is should these DAL references be disposed somehow? It's a custom class written in VB.NET, so it doesn't implement IDisposable so I'm not sure if there's anything to be done or not, but we do have errors and issues (like the GetOrdinal problem) which seem to be load-related, and I'm wondering if this is part of the problem.
If the DAL holds at least one member variable that is Disposable (implements IDisposable), then it too should implement IDisposable. This would then indicate to DAL's client that Dispose() should be called. The DAL's Dispose() would then call the member variable(s) Dispose().
That is the general guidance for implementing IDisposable.
BTW - there is no need to dbreader = nothing - it achieves nothing. See Eric Lippert's post