ASP.NET Asynchronous Image Processing

Asynchronous programming is an excellent way to build more scalable applications by using the ASP.NET thread pool efficiently. You rarely see ASP.NET developers use asynchronous programming models, partly because they simply don't know how.

A scalable ASP.NET Web site makes optimum use of the thread pool. That means making sure request-processing threads are executing code instead of waiting for I/O to complete. If the thread pool becomes full due to all the threads grinding away on the CPU, there's little you can do but add servers.

Let's look at a very simple asynchronous image handler to see the basics of this scaling technique. We'll create a GridView that draws members of the Northwind Employees table, and the image column will use an Async Image ASHX handler to request each image.

First, here's the markup for the single page containing the Gridview:

<%@ Page Language="C#"  Inherits="AsyncGridView" Title="Asynchronous ASP.NET" Codebehind="AsyncGridView.aspx.cs" %>

<Form runat="server">
<asp:SqlDataSource ID="SqlDataSource1" Runat="server" SelectCommand="SELECT [EmployeeID], [LastName], [FirstName], [Photo] FROM [Employees] ORDER BY EmployeeID"
ConnectionString="<%$ ConnectionStrings:Northwind %>">
    <div align="center">
    <asp:GridView ID="GridView1" Runat="server" DataSourceID="SqlDataSource1" DataKeyNames="EmployeeID"
AutoGenerateColumns="False" BorderWidth="1px" BackColor="White" GridLines="Horizontal"
CellPadding="3" BorderStyle="None" BorderColor="#E7E7FF" Width="50%">
        <FooterStyle ForeColor="#4A3C8C" BackColor="#B5C7DE"></FooterStyle>
        <PagerStyle ForeColor="#4A3C8C" HorizontalAlign="Right" BackColor="#E7E7FF"></PagerStyle>
        <HeaderStyle ForeColor="#F7F7F7" Font-Bold="True" BackColor="#4A3C8C"></HeaderStyle>
        <AlternatingRowStyle BackColor="#F7F7F7" />
            <asp:TemplateField HeaderText="Photo">
                 <ItemStyle Width="1px"></ItemStyle>
                    <img src='<%# "AsyncImageServer.ashx?ID=" + Eval ("EmployeeID")  %>' />
             <asp:BoundField HeaderText="Last Name" DataField="LastName" SortExpression="LastName"></asp:BoundField>
            <asp:BoundField HeaderText="First Name" DataField="FirstName" SortExpression="FirstName"></asp:BoundField>
        <SelectedRowStyle ForeColor="#F7F7F7" Font-Bold="True" BackColor="#738A9C"></SelectedRowStyle>
        <RowStyle ForeColor="#4A3C8C" BackColor="#E7E7FF"></RowStyle>
        <SortedAscendingCellStyle BackColor="#F4F4FD" />
        <SortedAscendingHeaderStyle BackColor="#5A4C9D" />
        <SortedDescendingCellStyle BackColor="#D8D8F0" />
        <SortedDescendingHeaderStyle BackColor="#3E3277" />

You can see that the image column has a regular HTML img tag whose src property points to the ASHX handler, passing the EmployeeId on the querystring.

Now here is the code for the ASHX handler:

<%@ WebHandler Language="C#" Class="AsyncImageServer" %>

using System;
using System.Web;
using System.Drawing;
using System.Drawing.Imaging;
using System.Data;
using System.Data.SqlClient;
using System.Web.Caching;
using System.Configuration;
using System.Web.Configuration;
using System.IO;    

public class AsyncImageServer : IHttpAsyncHandler
   // private SqlConnection _connection;  this is corrected code for thread safety
   // private SqlCommand _command;
    private HttpContext _context;

    public void ProcessRequest (HttpContext context)
         // We don't use this at all - see BeginProcessRequest
    public bool IsReusable
        get { return true; }
    public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object state)
        // Get the employee ID from the query string
        string _id = context.Request["ID"];
        if (String.IsNullOrEmpty(_id))
            return null;
        int id = 0;
        bool ok = int.TryParse(_id, out id);
        if (!ok) return null;
        _context = context;
        string conn = WebConfigurationManager.ConnectionStrings["AsyncNorthwind"].ConnectionString;
        // Select the image from the database
      SqlConnection  _connection = new SqlConnection (conn);
        _connection.Open ();
       SqlCommand _command = new SqlCommand ("SELECT Photo FROM Employees WHERE EmployeeID=@id", _connection);
        _command.Parameters.AddWithValue("@id",  id);
        return _command.BeginExecuteReader (cb, _command);

    public void EndProcessRequest(IAsyncResult ar)
        SqlCommand _command = (SqlCommand)ar.AsyncState;
            SqlDataReader reader = _command.EndExecuteReader(ar);
            if (reader != null && reader.HasRows)
                // Get the image returned in the query
                    byte[] image = (byte[])reader[0];
                    // WRite the image into the HTTP response output stream
                    _context.Response.ContentType = "image/jpeg";
                    // strip off the 78 byte Ole header (a relic from old MS Access databases)
                    _context.Response.OutputStream.Write(image, 78, image.Length - 78);
            if (_command.Connection != null)

Asynchronous processing begins when ASP.NET calls the handler's BeginProcessRequest method. BeginProcessRequest makes an async call to the Northwind database to get the image. The thread assigned to the request then goes immediately back into the thread pool. When the async call completes, another thread is borrowed from the thread pool to execute the EndProcessRequest callback. This is efficient coding technique that maximizes the number of available threads to process requests.

AsyncImageServer is more scalable than its synchronous counterpart since it consumes ASP.NET threads for only a fraction of the total time required to process a request. The vast majority of the time, it's simply waiting for an asynchronous call to complete.
AsyncImageServer can also outperform it's synchronous counterpart because, rather than making repeated calls to the database in series, it  can make multiple calls in parallel using ThreadPool threads. However, only two outbound calls targeting a given IP address can be pending at a time unless you increase the runtime's default maxconnection setting:

    <add address="*" maxconnection="20" />

You can download the working Visual Studio 2010 Solution here. You'll need to have SQL Server with the Northwind database installed. If you are using SQLEXPRESS, you'll need to modify the connection strings.

NOTE: It was suggested by a user that the original code posted may not be threadsafe, so I have revised the code to declare the SqlCommand inside the method body and pass it to the callback as the State object. This allows one to keep IsReusable = true;

Async is your friend if you want to write scalable ASP.NET Web applications. You can write async HttpModules and you can do async Page methods. All these approaches have the same end goal: to keep more threads available to handle multiple simultaneous requests and be able to process heavier user loads without HTTP errors.

By Peter Bromberg   Popularity  (7765 Views)