List<T> Grabbing Data for Speed

By John Belthoff / In Web / Posted Oct 17, 2009

Speeding up your Data Gathering

In my previous article I began writing about the virtues of the Generic List - List<T>. In this article I will demonstrate the efficiency of using the Generic List. Keep in mind that this is a precursor to the more elegant features of the List<T> but mandatory in understanding its elegance.

Let's take a look at a classic example of populating DataSet and then compare it to populating data into a Generic List. I will warn you, at first it might look like we have to write additional code and we will but only in the beginning. Ok let's create some data, make a stored procedure and then populate out DataSet.

Create a Table and stuff some data into it, then create a stored proc for retrieval

-- Create Our Table
CREATE TABLE [dbo].[MyCustomers](
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [Guid] [uniqueidentifier] NOT NULL CONSTRAINT [DF_MyCustomers_Guid]  DEFAULT (newid()),
    [FName] [varchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
    [LName] [varchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
 CONSTRAINT [PK_MyCustomers] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX  = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

-- Stuff our Table with some data
Declare @Count int
Set @Count = 0
While @Count < 50000
    Begin

        Insert Into dbo.MyCustomers (FName, LName)
        Values (Cast(NewID() as varchar(50)), Cast(NewID() as varchar(50)))

        Set @Count = @Count + 1

    End
GO

-- Create a Stroed Proc to retrive our data
Create Procedure dbo.GetMyCustomerList
As
Set NoCount On
Select
        ID
        ,Guid
        ,FName
        ,LName
From dbo.MyCustomers
Set NoCount Off
GO

-- Quick Test to see if everything went right
Select Top 100 *
From dbo.MyCustomers
Order By ID desc

Now if everything went right you should be seeing the bottom 100 rows of our data starting at row fifty thousand and counting down. I will assume you are ok.

The SqlDataAdapter Approach

In order to get our data onto a web page we need to crate a dataset and then fill it with data from our database and then manipulate it using the Asp.Net server side tools like DataSet, DataTable, DataRow etc... We do this by using the System.SqlClient, and System.Data namespaces. After populating our data we can begin to manipulate it. However for our purposes we are only going to grab the data and count how many rows we have. We will do this again populating a list to see if there is any performance gain.

Populate our Dataset and Count the Rows

using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
public partial class x_test_data_set1 : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        Trace.Warn("Fill", "Started");
        DataSet ds = Customers();
        Trace.Warn("Fill", "Ended");
        Response.Write(ds.Tables[0].Rows.Count.ToString());
    }

    private DataSet Customers()
    {
        SqlConnection objConn = new SqlConnection(ConfigurationManager.ConnectionStrings["DBLocal"].ConnectionString);
        SqlCommand objCmd = new SqlCommand("dbo.GetMyCustomerList", objConn);
        objCmd.CommandType = CommandType.StoredProcedure;
        DataSet ds = new DataSet();
        SqlDataAdapter objAdpt = new SqlDataAdapter();
        objAdpt.SelectCommand = objCmd;
        objAdpt.Fill(ds);
        return ds;
    }
}

Turn the trace on for the .aspx page and running the code above produces our timing of execution. Mine looks like the following. 810 milliseconds to perform that database retrieval. (Remember, we are retrieving 50k rows).

Asp Trace from DataAdapter.Fill

Trace Information
Category Message From First(s) From Last(s)
aspx.page Begin PreInit    
aspx.page End PreInit 3.95025218508192E-05 0.000040
aspx.page Begin Init 7.93333542182263E-05 0.000040
aspx.page End Init 0.000124179067071834 0.000045
aspx.page Begin InitComplete 0.00016021051972056 0.000036
aspx.page End InitComplete 0.000191851132483318 0.000032
aspx.page Begin PreLoad 0.000225150841139062 0.000033
aspx.page End PreLoad 0.000259768804235456 0.000035
aspx.page Begin Load 0.000291965790545409 0.000032
Fill Started 0.000330014723849503 0.000038
Fill Ended 0.810523900880298 0.810194
aspx.page End Load 0.810590730866828 0.000067
aspx.page Begin LoadComplete 0.810614830362457 0.000024
aspx.page End LoadComplete 0.810638305817487 0.000023
aspx.page Begin PreRender 0.810662269979011 0.000024
aspx.page End PreRender 0.810696173678769 0.000034
aspx.page Begin PreRenderComplete 0.810719766924595 0.000024
aspx.page End PreRenderComplete 0.810743292503368 0.000024
aspx.page Begin SaveState 0.811033807211553 0.000291
aspx.page End SaveState 0.811170680116538 0.000137
aspx.page Begin SaveStateComplete 0.811195408665142 0.000025
aspx.page End SaveStateComplete 0.811218021991792 0.000023
aspx.page Begin Render 0.811241998684252 0.000024
aspx.page End Render 0.811494943140879 0.000253

Ok, now let's perform the same thing but use a Generic List to hold our data as opposed to a DataSet, In order to do this we first need to create a class that will hold our data. This will give us a strongly typed data wrapper which makes things a whole lot easier on the other side as you will soon see. After we create our class, we use and SqlDataReader to get our data, create an instance of our class and then add it to our Generic List. Let's get started. We create our class inside the App_Code foler.

Create a Data Wrapper Class

using System;
/// <summary>
/// Holds Data Related to a Customer Object
/// </summary>
public class Customer
{
    private Int64 _id;
    /// <summary>
    /// ID (System.Int64)
    /// </summary>
    public Int64 ID
    {
        get { return this._id; }
        set { this._id = value; }
    }

    private Guid _guid;
    /// <summary>
    /// ID (System.Guid)
    /// </summary>
    public Guid Guid
    {
        get { return this._guid; }
        set { this._guid = value; }
    }

    private String _fname;
    /// <summary>
    /// FName (System.String)
    /// </summary>
    public String FName
    {
        get { return this._fname; }
        set { this._fname = value; }
    }

    private String _lname;
    /// <summary>
    /// LName (System.String)
    /// </summary>
    public String LName
    {
        get { return this._lname; }
        set { this._lname = value; }
    }
}

OK We have our base class to hold our data, let's populate the same data into a Generic List and take a look at the performance results.

Generic List Code Behind

using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Collections;
using System.Collections.Generic;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
public partial class x_test_data_set2 : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        Trace.Warn("Reader", "Started");
        List<Customer> l = GetCustomers();
        Trace.Warn("Reader", "Ended");
        Response.Write(l.Count.ToString());
    }

    private List<Customer> GetCustomers()
    {
        
        SqlConnection objConn = new SqlConnection(ConfigurationManager.ConnectionStrings["DBLocal"].ConnectionString);
        SqlCommand objCmd = new SqlCommand("dbo.GetMyCustomerList", objConn);
        objCmd.CommandType = CommandType.StoredProcedure;
        SqlDataReader objRdr;
        List<Customer> l = new List<Customer>();
        Customer c;
        
        objConn.Open();
        objRdr = objCmd.ExecuteReader();
        while (objRdr.Read())
        {
            c = new Customer();
            c.ID = Convert.ToInt64(objRdr["ID"]);
            c.Guid =  (Guid)objRdr["Guid"];
            c.FName = objRdr["FName"].ToString();
            c.LName = objRdr["LName"].ToString();
            l.Add(c);
        }
        objRdr.Close();
        objConn.Close();

        return l;

    }
}

Now we run our page and look at the results. Still returning the same 50k rows, but look at the time it took to do this! We went from 810 milliseconds down to 330 milliseconds! And that's not all, read on...

Asp Trace from Generic List Approach

Trace Information
Category Message From First(s) From Last(s)
aspx.page Begin PreInit    
aspx.page End PreInit 2.75079101531907E-05 0.000028
aspx.page Begin Init 5.23968547351273E-05 0.000025
aspx.page End Init 8.00275680586448E-05 0.000028
aspx.page Begin InitComplete 0.000102370226496664 0.000022
aspx.page End InitComplete 0.00012546724726669 0.000023
aspx.page Begin PreLoad 0.000146842517464992 0.000021
aspx.page End PreLoad 0.000171558535133611 0.000025
aspx.page Begin Load 0.000193981391560415 0.000022
Reader Started 0.000218542025625764 0.000025
Reader Ended 0.327289906957802 0.327071
aspx.page End Load 0.327363586353811 0.000074
aspx.page Begin LoadComplete 0.327409675135491 0.000046
aspx.page End LoadComplete 0.327446180257511 0.000037
aspx.page Begin PreRender 0.327479119075217 0.000033
aspx.page End PreRender 0.327517659221202 0.000039
aspx.page Begin PreRenderComplete 0.327542335139877 0.000025
aspx.page End PreRenderComplete 0.327566920835813 0.000025
aspx.page Begin SaveState 0.327857360358385 0.000290
aspx.page End SaveState 0.32799586729739 0.000139
aspx.page Begin SaveStateComplete 0.328020593339808 0.000025
aspx.page End SaveStateComplete 0.328042966072491 0.000022
aspx.page Begin Render 0.32806449923248 0.000022
aspx.page End Render 0.328313473888663 0.000249

OK, we have over twice the computing performance and if you haven't realized yet working with a DataSet and trying to find information inside of it, at least for me, is a time consuming task. When working with the Generic List and because we created our class Intellisense does our work for us. Instead of remembering column names and positions and everything else that you often have to look up references for the syntax you now have everything at you finger tips.

Consider the following scenarios. Lets find the value of the field "Guid" at index position 48709 and display that on a web page. If you are using a dataset, you would type something like this:
ds.Tables[0].Rows[48709]["Guid"].ToString()

And even that requires you to remember the Database field name "Guid"! However, with the Generic List we can type the following:
l[48709].Guid.ToString()

The beauty of this is that after you type the closing bracket and put a period, you are now offered a list of properties to choose from. Now that is very convenient indeed!

Anyway, you should now have firm grasp on my conceptual thinking. In the next article we will be looking at finding, manipulating and playing with our server side disconnected record sets.

Happy Coding!

Aspx:

<%@ Page Language="C#" AutoEventWireup="true" trace="true" CodeFile="data-set.aspx.cs" Inherits="x_test_data_set" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
  <title>Untitled Page</title>
</head>
<body>
  <form id="form1" runat="server">
    <div>
    
    </div>
  </form>
</body>
</html>

C#:

using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Collections;
using System.Collections.Generic;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
public partial class x_test_data_set : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        // Get Our Data Using a DataSet
        Trace.Warn("SqlDataAdapter", "Started");
        DataSet ds = Customers();
        Trace.Warn("SqlDataAdapter", "Ended");
        Response.Write("DataSet Total Rows: " + ds.Tables[0].Rows.Count.ToString());
        Response.Write("<br />");
        Response.Write("Value at Index Position 48709: " + ds.Tables[0].Rows[48709]["Guid"].ToString());
        
        Response.Write("<br />");
        Response.Write("<br />");

        // Get Our Data Using a Generic List
        Trace.Warn("SqlDataReader", "Started");
        List<Customer> l = GetCustomers();
        Trace.Warn("SqlDataReader", "Ended");
        Response.Write("Generic List Total Rows: " + l.Count.ToString());
        Response.Write("<br />");
        Response.Write("Value at Index Position 48709: " + l[48709].Guid.ToString());
    }

    private DataSet Customers()
    {
        SqlConnection objConn = new SqlConnection(ConfigurationManager.ConnectionStrings["DBLocal"].ConnectionString);
        SqlCommand objCmd = new SqlCommand("dbo.GetMyCustomerList", objConn);

        // Mark the Command as a SPROC
        objCmd.CommandType = CommandType.StoredProcedure;

        // Create a new Dataset
        DataSet ds = new DataSet();

        // Create a SqlDataAdapter and fill the DataSet
        SqlDataAdapter objAdpt = new SqlDataAdapter();
        objAdpt.SelectCommand = objCmd;
        objAdpt.Fill(ds);
        objAdpt.Dispose();
        objConn.Close();

        return ds;

    }

    private List<Customer> GetCustomers()
    {
        // Create Instance of Connection and Command Object
        SqlConnection objConn = new SqlConnection(ConfigurationManager.ConnectionStrings["DBLocal"].ConnectionString);
        SqlCommand objCmd = new SqlCommand("dbo.GetMyCustomerList", objConn);

        // Mark the Command as a SPROC
        objCmd.CommandType = CommandType.StoredProcedure;

        // Create a SqlDataReader
        SqlDataReader objRdr;

        // Create a new Generic List of Articles
        List<Customer> l = new List<Customer>();
        Customer c;

        // Fill our Generic List
        objConn.Open();
        objRdr = objCmd.ExecuteReader();
        while (objRdr.Read())
        {
            c = new Customer();
            c.ID = Convert.ToInt64(objRdr["ID"]);
            c.Guid = (Guid)objRdr["Guid"];
            c.FName = objRdr["FName"].ToString();
            c.LName = objRdr["LName"].ToString();
            l.Add(c);
        }
        objRdr.Close();
        objConn.Close();

        return l;

    }
}