0

Please have a look at the code below:

Private objCommand As SQLCommand 

Public Overrides Function ExecuteDataReader(ByVal strCommandType As String, ByVal sqlCommandString As String) As DbDataReader
    Dim objDR As SqlDataReader

    Try
        _objCon = getConnection()
        _objCon.Open()
        Using _objCon
            Using _objCommand
                _objCommand.Connection = _objCon
                _objCommand.CommandText = sqlCommandString
                _objCommand.CommandType = strCommandType
                objDR = _objCommand.ExecuteReader
                ExecuteDataReader = objDR
            End Using
        End Using
    Catch ex As Exception
        Throw
    Finally
        _objCon = Nothing
        _objCommand.Dispose()
        _objCommand = Nothing
        objDR = Nothing
    End Try
End Function

The DataReader is returned closed because it is closed when the connection object is closed. How can the DataReader outlive the connection object?

I have looked for similar questions and I found this one: DataReader not closed when Connection is closed, consequences?. However, it does not answer my specific question.

Community
  • 1
  • 1
w0051977
  • 15,099
  • 32
  • 152
  • 329
  • 1
    Why do you set your objects to `Nothing`? This is not helpful in VB.Net, has never been helpful in VB.Net. In fact, it can in _rare_ cases actually be harmful. This has been poor practice ever since the move away from vb6. Also, you fail to close your connection properly. – Joel Coehoorn Nov 16 '12 at 18:09
  • @Joel Coehoorn ,thanks. Can you describe what you mean by harmful? – w0051977 Nov 16 '12 at 19:45
  • It can cause side effects with the garbage collector, such that the object is moved to higher order generation and hangs around much longer than needed or intended. Again, this is _extremely rare_, to the point of near-nonexistence... but the potential is non-zero. As the benefit really is zero, the practice should be stopped. – Joel Coehoorn Nov 16 '12 at 19:53
  • 1
    ...so I guess the point is that setting the object to nothing will, if anything, have the opposite of the intended effect and purpose for which it was used in vb6. – Joel Coehoorn Nov 16 '12 at 20:01
  • @Joel Coehoorn, thanks for that. I have a memory leak and I have not managed to pin it down. I have experience with WINDBG. Your statement has me wandering. – w0051977 Nov 16 '12 at 20:10
  • Most "memory leaks" in .Net relate to something called the "Large Object Heap"... anything over about 85,000 bytes ends up there. A good example is string processing large documents... every time you make any change to a 42500 character string, you put a new object on the LOH. The LOH is almost never _compacted_. This means the old string can be collected and the physical memory available, but the memory _address space_ is still assigned within your app's process. Change too many big strings or copy too many byte[]'s, and that address space can run out. – Joel Coehoorn Nov 16 '12 at 20:27
  • @Joel Coehoorn, 42500 characters! I think most my Strings are a tenth of that at most (4700) – w0051977 Nov 16 '12 at 20:38
  • I'd check it anyway: you could still be seeing memory fragmentation in other generations. – Joel Coehoorn Nov 16 '12 at 21:56

2 Answers2

2

The reason your connection is closed pre-maturely is that you don't return from the function until after you exit the Using block. The act of leaving the Using block will force your connection to close immediately. Setting the datareader as your returned object is not enough. Even using an explicit Return statement would not be enough... leaving the function would still mean leaving the Using block, and so your connection is still closed before you ever get to use the datareader.


To get around all that, I use a pattern that looks like this:

Public Iterator Function ExecuteDataReader(Of T)(ByVal sql As String, ByVal addParams as Action(Of SqlParameterCollection), ByVal castRow As Funnction(Of IDataRecord, T)) As IEnumerable(Of T)

    Using cn As SqlConnection = getConnection(), _
          cmd As New SqlCommand(sql, cn)

        addParams(cmd.Parameters)
        cn.Open()

        Using rdr As SqlDataReader = cmd.ExecuteReader()
            While rdr.Read()
                Yield castRow(rdr)
            End While
        End Using
    End Using
End Function

I would then call that function like this:

Dim results As IEnumerable(Of Customer) = ExecuteDataReader( _
           "SELECT * FROM Customer WHERE Sales> @MinSales", _
      Sub(p) p.Add("@MinSales", SqlDbType.Double).Value = 10000.0, _
      Function(r) New Customer() With {Name=r("Name"), Address=r("Address"), Sales=r("Sales") })

For Each c As Customer in results
   '...
Next

Let's go over that pattern a bit, because there are some things that can confuse... namely I want to cover the delegate arguments.

First up is the addParameter argument. You need to understand is that your original pattern is horribly broken, because it forces you to create code riddled with Sql injection vulnerabilities, as there is no other way to send parameter information. That is a huge problem. Thankfully, it's easily solved. This is what the addParameter argument is for. It's not the only way to do this — ie, you could do something as simple as pass an array of key/values/type as well — but I like it because it avoids duplicate work going through the array or duplicate memory storing the parameter data twice.

Next is the castRow argument. This is necessary because without it you run into a similar problem you see with your example. Here, the code would still run, but because you keep yielding the same object code elsewhere would end up all working with the final record in the results. This way, you get the correct expected results, and you get them in a strongly-typed manner.

Moving on, hopefully you're already familiar with the Iterator and Yield keywords, but as they're relatively new to VB.Net it's okay if you're not. Just know that they cause the compiler to transform this code into something that will let iterate over your datareader object.

Joel Coehoorn
  • 399,467
  • 113
  • 570
  • 794
  • Thanks. I am using .NET 3.5 and VS 2008. I don't believe I can use Yield? – w0051977 Nov 16 '12 at 21:53
  • No, you can't yet :( You could refactor that out to the same code that the vb compiler would use (add a class that actually implements the required IEnumerable members), but it's a little ugly. – Joel Coehoorn Nov 16 '12 at 21:55
  • Thanks. Is there another solution? I believe that ApplicationBlocks allows you to return a DataReader from a function. – w0051977 Nov 16 '12 at 21:55
  • Are you able to answer my other question here: http://stackoverflow.com/questions/13432292/commandbehavior-closeconnection-or-datatable ? It follows on from this one. Thanks. – w0051977 Nov 17 '12 at 22:36
0

You can use the over load of ExecuteReader and pass CommandBehaviour.Close, then when the reader gets disposed of the rest of it goes but it can get messy and you lose control of the lifetimes.

My recomendation would be to just not do this, I did in one project and While I still like DataReader as a lightweight access, and dislike Datatable, I don't pass raeders about anymore, too much scope for error. It's a constant fight between the code , connection pooling and the Garbarge Collector a,d one you wonlt win.

Use a DataTable, EF, Linq, or just an old fashioned IEnumberable

Tony Hopkinson
  • 20,172
  • 3
  • 31
  • 39