Improving Website Performance With The List<T>

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

Website Performance with List<T>

In my previous article we discussed ways to efficiently get data out of our database and onto our web pages. In this article we will take a look at a few of the performance benefits of using the Generic List, (List<T>), as opposed to the Ado.Net objects like the DataSet, DataTable, etc.

If you haven't run my Sample Data script in your database yet you can download the script here. Let's get started.

First because we are dealing with a large amounts of data, we will be caching our data into the Asp.Net cache and creating a standard DataSet and also a Generic List that holds identical data. We will make 2 cache objects.

Use the code below and turn the trace on on the Aspx page and if everything went well, we should be restoring our DataSet and Generic List extremely quickly on subsequent page loads because we are getting the data from the Cache and not the Database each time.

Run the page a few times and look at the performance metrics. This should be giving you ideas already!

Basic Page to Cache Data

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_set5 : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        // Cache our data
        List<Customer> l = GetCustomers();
        DataSet ds = DSCustomers();
    }

    private DataSet DSCustomers()
    {
        Trace.Write("DSCustomers()", "Starting");
        DataSet ds = new DataSet();
        if (Cache["DSCustomers"] == null)
        {
            SqlConnection objConn = new SqlConnection(ConfigurationManager.ConnectionStrings["DBLocal"].ConnectionString);
            SqlCommand objCmd = new SqlCommand("dbo.GetMyCustomerList", objConn);
            objCmd.CommandType = CommandType.StoredProcedure;
            SqlDataAdapter objAdpt = new SqlDataAdapter();
            objAdpt.SelectCommand = objCmd;
            objAdpt.Fill(ds);
            Cache.Insert("DSCustomers", ds, null, DateTime.Now.AddMinutes(60), TimeSpan.Zero);
            Trace.Warn("DSCustomers()", "Returning from dBAse");
        }
        else
        {
            ds = (DataSet)Cache["DSCustomers"];
            Trace.Write("DSCustomers()", "Returning from Cache");
        }
        return ds;
    }

    private List<Customer> GetCustomers()
    {
        Trace.Write("GetCustomers()", "Starting");
        // Create a new Generic List of Articles
        List<Customer> l = new List<Customer>();
        Customer c;

        if (Cache["LCustomers"] == null)
        {
            // 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;
            // 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();
            Cache.Insert("LCustomers", l, null, DateTime.Now.AddMinutes(60), TimeSpan.Zero);
            Trace.Warn("GetCustomers()", "Returning from dBAse");
        }
        else
        {
            l = (List<Customer>)Cache["LCustomers"];
            Trace.Write("GetCustomers()", "Returning from Cache");
        }
        return l;
    }
}

Manipulate Data with ADO

Ok let's first take our dataset and select only those records that have an ID greater than 36985. Because we have 50k records we should be left with 13015 records. And lets just go ahead and get the value of the GUID field at index position 8964 after we are done. Let's also time this to see how fast our DataTable.Select method really performs.

DataSet Routines

    protected void Page_Load(object sender, EventArgs e)
    {
        // Cache our data
        List<Customer> l = GetCustomers();
        DataSet ds = DSCustomers();

        // DataSet Stuff
        Response.Write("<font color=\"green\">DataSet Stuff</font>");
        Response.Write("<br />");
        Response.Write(ds.Tables[0].Rows.Count.ToString());
        Response.Write("<br />");
        
        // Get an Array of Rows
        DataRow[] foundRows;
        
        // Perform our Select Statment
        Trace.Warn("Time DS", "Start");
        foundRows = ds.Tables[0].Select("ID > 36985");
        Trace.Warn("Time DS", "End");
        
        // Count the remaining Rows
        Response.Write(foundRows.GetUpperBound(0) + 1);
        Response.Write("<br />");
        
        // Get our value at Index position 8964
        Response.Write(foundRows[8964]["Guid"].ToString());
        Response.Write("<br />");
        
        Response.Write("________________________________________________________________");
        Response.Write("<br />");
    }

DataSet Trace Info

Trace Information
Category Message From First(s) From Last(s)
aspx.page Begin Load 0.000200214279001284 0.000022
GetCustomers() Starting 0.00022563954763322 0.000025
GetCustomers() Returning from Cache 0.000258673600451114 0.000033
DSCustomers() Starting 0.000281023777450581 0.000022
DSCustomers() Returning from Cache 0.000315626703424078 0.000035
Time DS Start 0.000341934149932646 0.000026
Time DS End 0.135652898092165 0.135311
aspx.page End Load 0.135731015945616 0.000078

Awesome! We were able to use a Select ID > 36985, just like in T-SQL and get our result in only 135 milliseconds. Very simple, easy and straight forward right? Don't answer yet! Now let's do the same thing using our List<T> and see what happens.

Manipulate Data with List<T>

In order to find data in our generic list we need to write a method using a delegate, in this case our Customer Class that we created previously. Of course with speed comes a little over head in the way of typing code but I think you will find it not too difficult and fairy easy to get your mind around once you've done it once or twice.

Ok now let's get all records with an ID greater than 36985, leaving us 13015 records in our sub set of data and then we will grab the value of the GUID field at index position 8965 just like we did with our DataSet. Then we will time the results and look at the difference. To retrieve this sub-set of data will will use the Generic List's FindAll() method.

List<T> Routines

    protected void Page_Load(object sender, EventArgs e)
    {
        // Cache our data
        List<Customer> l = GetCustomers();
        DataSet ds = DSCustomers();

        // List Stuff
        Response.Write("<font color=\"green\">List Stuff</font>");
        Response.Write("<br />");
        Response.Write(l.Count.ToString());
        Response.Write("<br />");
        
        // Get Our Sub Set of data
        Trace.Warn("Time List", "Start");
        List<Customer> l2 = FindGreaterThanByID(l, 36985);
        Trace.Warn("Time List", "End");

        // Count the remaining Rows
        Response.Write(l2.Count.ToString());
        Response.Write("<br />");

        // Get our value at Index position 8964
        Response.Write(l2[8964].Guid.ToString());
        Response.Write("<br />");
        Response.Write("________________________________________________________________");
        Response.Write("<br />");
    }

    // Returns all rows greater the the ID value
    private List<Customer> FindGreaterThanByID(List<Customer> l, Int64 i)
    {
        return l.FindAll(delegate(Customer c) { return c.ID > i; });
    }

List<T> Trace Info

Trace Information
Category Message From First(s) From Last(s)
aspx.page Begin Load 0.000192675668055512 0.000022
GetCustomers() Starting 0.000217166128880674 0.000024
GetCustomers() Returning from Cache 0.000244889571128724 0.000028
DSCustomers() Starting 0.000267575577206228 0.000023
DSCustomers() Returning from Cache 0.000296256382945396 0.000029
Time List Start 0.000322002443532471 0.000026
Time List End 0.0040860975533348 0.003764
aspx.page End Load 0.00415716048995959 0.000071

As you can see we see a slight increase in performance. (hehe!)

And while there is not enough room in this article to display all of the things possible using this approach, I hope you get the idea that using a Generic List, List<T> far surpasses the performance of the standard Ado.Net objects.

This approach used properly with Asp.Net's caching facilities may just be the difference between your visitors coming back to you website time after time or sitting and waiting for results. Well maybe not, but it certainly will take a load off of your servers and render your pages much more quickly and efficiently than before.

In the closing code I have left you with a page that demonstrates all of the above and a few other things and even included a DataView routine. Try it out for yourself!

Happy coding!

Aspx

<%@ Page Language="C#" AutoEventWireup="true" trace="true" CodeFile="data-set3.aspx.cs" Inherits="x_test_data_set3" %>
<!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_set3 : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        // Cache our data
        List<Customer> l = GetCustomers();
        DataSet ds = DSCustomers();

        // List Stuff
        Response.Write("<font color=\"green\">List Stuff</font>");
        Response.Write("<br />");
        Response.Write(l.Count.ToString());
        Response.Write("<br />");
        Trace.Warn("Time List", "Start");
        List<Customer> l2 = FindGreaterThanByID(l, 36985);
        Trace.Warn("Time List", "End");
        Response.Write(l2.Count.ToString());
        Response.Write("<br />");
        Response.Write(l2[8964].Guid.ToString());
        Response.Write("<br />");
        Trace.Warn("Time List Between", "Start");
        List<Customer> l3 = FindBetweenByID(l, 36985, 42152);
        Trace.Warn("Time List Between", "End");
        Response.Write(l3.Count.ToString());
        Response.Write("<br />");
        Response.Write(l3[4119].Guid.ToString());
        Response.Write("<br />");
        Response.Write("________________________________________________________________");
        Response.Write("<br />");

        // DataSet Stuff
        Response.Write("<font color=\"green\">DataSet Stuff</font>");
        Response.Write("<br />");
        Response.Write(ds.Tables[0].Rows.Count.ToString());
        Response.Write("<br />");
        DataRow[] foundRows;
        Trace.Warn("Time DS", "Start");
        foundRows = ds.Tables[0].Select("ID > 36985");
        Trace.Warn("Time DS", "End");
        Response.Write(foundRows.GetUpperBound(0)+1);
        Response.Write("<br />");
        Response.Write(foundRows[8964]["Guid"].ToString());
        Response.Write("<br />");
        DataRow[] foundRows2;
        Trace.Warn("Time DS Between", "Start");
        foundRows2 = ds.Tables[0].Select("ID > 36985 And ID < 42152");
        Trace.Warn("Time DS Between", "End");
        Response.Write(foundRows2.GetUpperBound(0) + 1);
        Response.Write("<br />");
        Response.Write(foundRows2[4119]["Guid"].ToString());
        Response.Write("<br />");
        Response.Write("________________________________________________________________");
        Response.Write("<br />");

        // DataView Stuff
        Response.Write("<font color=\"green\">DataView Stuff</font>");
        Response.Write("<br />");
        Response.Write(ds.Tables[0].Rows.Count.ToString());
        Response.Write("<br />");
        Trace.Warn("Time Dv", "Start");
        DataView dv = new DataView(ds.Tables[0], "ID > 36985", "ID", DataViewRowState.CurrentRows);
        Trace.Warn("Time Dv", "Start");
        Response.Write(dv.Count.ToString());
        Response.Write("<br />");
        Response.Write(dv[8964]["Guid"].ToString());
        Response.Write("<br />");
        Trace.Warn("Time Dv Between", "Start");
        DataView dv2 = new DataView(ds.Tables[0], "ID > 36985 And ID < 42152", "ID", DataViewRowState.CurrentRows);
        Trace.Warn("Time Dv Between", "Start");
        Response.Write(dv2.Count.ToString());
        Response.Write("<br />");
        Response.Write(dv2[4119]["Guid"].ToString());
        Response.Write("<br />");
        Response.Write("________________________________________________________________");
        Response.Write("<br />");
    }

    private List<Customer> FindGreaterThanByID(List<Customer> l, Int64 i)
    {

    return l.FindAll(delegate(Customer c) { return c.ID > i; });
    }

    private List<Customer> FindBetweenByID(List<Customer> l, Int64 i, Int64 i2)
    {

    return l.FindAll(delegate(Customer c) { return c.ID > i && c.ID < i2; });
    }

    private DataSet DSCustomers()
    {
        Trace.Write("DSCustomers()", "Starting");
        DataSet ds = new DataSet();
        if (Cache["DSCustomers"] == null)
        {
            SqlConnection objConn = new SqlConnection(ConfigurationManager.ConnectionStrings["DBLocal"].ConnectionString);
            SqlCommand objCmd = new SqlCommand("dbo.GetMyCustomerList", objConn);
            objCmd.CommandType = CommandType.StoredProcedure;
            SqlDataAdapter objAdpt = new SqlDataAdapter();
            objAdpt.SelectCommand = objCmd;
            objAdpt.Fill(ds);
            Cache.Insert("DSCustomers", ds, null, DateTime.Now.AddMinutes(60), TimeSpan.Zero);
            Trace.Warn("DSCustomers()", "Returning from dBAse");
        
        }
        else
        {
            ds = (DataSet)Cache["DSCustomers"];
            Trace.Write("DSCustomers()", "Returning from Cache");
        }
       return ds;
    }

    private List<Customer> GetCustomers()
    {
        Trace.Write("GetCustomers()", "Starting");

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

        if (Cache["LCustomers"] == null)
        {
            // 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;
            // 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();
            Cache.Insert("LCustomers", l, null, DateTime.Now.AddMinutes(60), TimeSpan.Zero);
            Trace.Warn("GetCustomers()", "Returning from dBAse");
        }
        else
        {
            l = (List<Customer>)Cache["LCustomers"];
            Trace.Write("GetCustomers()", "Returning from Cache");
        }
        return l;
    }
}