1

I am attempting to get the information of user whenever user logged in to the website, it success when I used a DataSet, but if I want to use the SqlDataReader, the error says: Invalid attempt to read when reader is closed. I have search why is it like that and I have found an article says that

SqlDataReader requires connection remains open in order to get the data from the server, while DataSet does not need requires connection remains open.

My question is: I want to know how can I use SqlDataReader as well? So that I don't have to depends on DataSet all the times when I want to get the data from the database.

My problem is occurs when I am trying to change the structure of reading the data function using SqlDataReader, so that it can be re-usable anytime.

Here is the code:

DatabaseManager class:

public SqlDataReader GetInformationDataReader(string procName, SqlParameter[] parameters)
        {
            SqlDataReader reader = null;
            using (SqlConnection conn = new SqlConnection(connectionString))
            {
                conn.Open();
                using (SqlCommand cmd = new SqlCommand(procName, conn))
                {
                    cmd.CommandType = CommandType.StoredProcedure;
                    if (parameters != null)
                    {
                        foreach(SqlParameter parameter in parameters)
                        {
                            cmd.Parameters.Add(parameter);
                        }
                    }
                    reader = cmd.ExecuteReader();
                }
            }
            return reader;
        }

Web Manager class:

public ModelContexts.InformationContext GetInformation(string username)
        {
            SqlDataReader reader = null;
            ModelContexts.InformationContext context = new ModelContexts.InformationContext();
            SqlParameter[] parameters =
            {
                new SqlParameter("@Username", SqlDbType.NVarChar, 50)
            };
            parameters[0].Value = username;
            try
            {
                reader = DatabaseManager.Instance.GetInformationDataReader("GetInformation", parameters);
                while(reader.Read())
                {
                    context.FirstName = reader["FirstName"].ToString();
                    context.LastName = reader["LastName"].ToString();
                    context.Email = reader["Email"].ToString();
                }
            }
            catch(Exception ex)
            {
                throw new ArgumentException(ex.Message);
            }
            return context;
        }

Controller:

public ActionResult MainMenu(ModelContexts.InformationContext context, string firstName, string lastName, string username, string email)
        {
            context = WebManager.Instance.GetInformation(User.Identity.Name);
            firstName = context.FirstName;
            lastName = context.LastName;
            username = User.Identity.Name;
            email = context.Email;
            return View(context);
        }

Model contains string return value with getter and setter (FirstName, LastName and Email).

View contains the html label and encode for FirstName, LastName and Email from the Model.

Appreciate your answer.

Thanks.

Stainn
  • 119
  • 1
  • 1
  • 13

2 Answers2

2

Here is an approach you can use to keep the code pretty clean that allows you to read from the SqlDataReader while the connection is still open. It takes advantage of passing delegates. Hopefully the code is understandable. You can adjust it to fit your specific needs, but hopefully it illustrates another option at your disposal.

public void GetInformationDataReader(string procName, SqlParameter[] parameters, Action<SqlDataReader> processRow)
{
    SqlDataReader reader = null;
    using (SqlConnection conn = new SqlConnection(connectionString))
    {
        conn.Open();
        using (SqlCommand cmd = new SqlCommand(procName, conn))
        {
            cmd.CommandType = CommandType.StoredProcedure;
            if (parameters != null)
            {
                foreach(SqlParameter parameter in parameters)
                {
                    cmd.Parameters.Add(parameter);
                }
            }
            using (SqlDataReader dataReader = cmd.ExecuteReader())
            {
                while (dataReader.Read())
                {
                    // call delegate here.
                    processRow(dataReader);
                }
            }
        }
    }
    return reader;
}

public ModelContexts.InformationContext GetInformation(string username)
{
    SqlDataReader reader = null;
    ModelContexts.InformationContext context = new ModelContexts.InformationContext();
    SqlParameter[] parameters =
    {
        new SqlParameter("@Username", SqlDbType.NVarChar, 50)
    };
    parameters[0].Value = username;
    try
    {
        // Instead of returning a reader, pass in a delegate that will perform the work
        // on the data reader at the right time, and while the connection is still open.
        DatabaseManager.Instance.GetInformationDataReader(
            "GetInformation",
            parameters,
            reader => {
                context.FirstName = reader["FirstName"].ToString();
                context.LastName = reader["LastName"].ToString();
                context.Email = reader["Email"].ToString();
            });
    }
    catch(Exception ex)
    {
        throw new ArgumentException(ex.Message);
    }
    return context;
}

Brief explanation:

You'll notice that the overall structure of the code is very similar to what you already have. The only changes are:

  • Instead of returning a SqlDataReader, the GetInformationDataReader() method accepts an Action<SqlDataReader> delegate.
  • Within the GetInformationDataReader() method, the delegate is invoked at the correct time, while the connection is still open.
  • The call to GetInformationDataReader() is modified to pass in a block of code as a delegate.

This sort of pattern can be useful for exactly these cases. It makes the code reusable, it keeps it pretty clean and separate, and it doesn't prevent you from benefiting from the using construct to avoid resource/connection leaks.

sstan
  • 35,425
  • 6
  • 48
  • 66
  • Nice explanation. Thank you for pointing out using a `delegate`, I am not considering that to use `delegate` and it is not comes in my mind in the first place. And yeah, like @Thomas Stringer mentions, it does not good approach (avoid if possible) to not use `using` keyword while dealing with database. It will have a connection leaks and sql injection I suppose. – Stainn Jul 16 '15 at 03:13
  • 1
    He's right, you should always wrap these resource-hungry objects in `using` blocks. The pattern I just showed you cannot have leaks, because it does use the `using` blocks in a resusable and centralized location. But it is still up to you to determine if you're better off using `DataSet` vs. `SqlDataReader`. If you decide to go with `SqlDataReader`, then the option I provided you is pretty clean, and it definitely is safe. – sstan Jul 16 '15 at 03:16
  • 1
    btw, the use of `using` has nothing to do with sql injection. SQL injection is prevented by performing proper parameter binding instead of concatenating the values directly in the sql string. Based on your sample, you seem to be ok in that regard. – sstan Jul 16 '15 at 03:19
  • Thank you for your time to answer my question sir, but regarding the performance, which one is better? `SqlDataReader` or `DataSet`? If we talk about retrieving not a single data, but thousand data from the database which have a specific or does not have a specific value. Also, in the `GridView`, do I can use `SqlDataReader` and bind the data source with `SqlDataReader`? Since what I knew is data source in `GridView` can only be bind with `DataSet` – Stainn Jul 16 '15 at 05:31
  • There are many factors to consider in choosing DataSet vs SqlDataReader. I think you'll find the different answers in [this discussion](http://stackoverflow.com/questions/1083193/whats-better-dataset-or-datareader) useful. In short, I would say that if you don't mind loading up all your results in memory and accepting that cost, it can be practical and more flexible to use a DataSet. But if you expect a lot or results and can only afford to load up a few results in memory at a time, then SqlDataReader is more appropriate. – sstan Jul 16 '15 at 12:08
1

You have wrapped your SqlConnection object in a using clause, therefore at the end of it SqlConnect.Dispose is called, closing the connection. Whatever caller is consuming your SqlDataReader no longer has the open connection, therefore you're getting your error.

while DataSet does not need requires connection remains open.

That is not entirely correct. DataSet is just an object that is typically filled when called by SqlDataAdapter (the Fill() method of that class). The SqlDataAdapter handles the opening and closing of the SqlConnection, which is most likely why that comment states that. But it's a different class that handles that, not the DataSet itself. Think of the DataSet as just the object that holds the result set of the SqlCommand.

To answer your comment...

So, shouldn't I use using keyword for this matter? In all of the Sql keyword?

I wouldn't take that approach either. You could have a connection leak bug quite easily with that model, and running out of pooled connections could be a not-so-fun thing to troubleshoot.

Typically it's best to consume your data and then close/dispose your connection. There's a saying, "open late, close early". That's typically how you'd want to approach this. I wouldn't try to pass a SqlDataReader object between class methods for this very issue that you're dealing with. The workaround (leaving the connection open) is very error prone.

Another though process, going back to something we mentioned, don't use the SqlDataReader. You have no benefit to cyclically loop through reading each row. Depending on your result set, just fill a DataSet (or usually more appropriate, a DataTable) and return either that Data[Set | Table] or, even better, an object that is more representative of the data it pertains to.

Thomas Stringer
  • 5,682
  • 3
  • 24
  • 40
  • So, shouldn't I use `using` keyword for this matter? In all of the `Sql` keyword? Nice explanation by the way :D – Stainn Jul 16 '15 at 02:59
  • Thank you for your explanation @Thomas Stringer, and yes it does not good approach (avoid if possible) to not use using keyword while dealing with database. It will have a connection leaks and sql injection I suppose. – Stainn Jul 16 '15 at 03:14