Introduction
Storing and retrieving BLOB images from a database column has been implemented since the first days of ASP in several ways. Scalability (more users using the same resources) and performance (faster execution times) are important factors in today's high demanding websites. There are 1001 ways to write code to do a predefined task (and they might all even work), yet getting it right requires paying attention to the details. With the introduction of HTTP handlers in ASP.NET, this can be done better than using a standard ASP.NET (.aspx) page; with async DB operations in ADO.NET v2 combined with async HTTP handlers, this gets even better!
Background
- Why use HTTP handlers?
The difference between an HTTP handler and a normal ASP.NET page is mainly performance, since an HTTP handler doesn't go through the full page events, browser compatibility tests, and formatting that a standard ASP.NET page does. Thereafter, if your page has no output (such as for audit logging) or has non-HTML output (such as XML, IMAGE, or anything else), you'll gain more scalability by implementing it as an HTTP handler.
- Why use Asynchronous HTTP handlers?
Asynchronous pages are a lot easier to implement in ASP.NET v2, and allow for greater scalability (more users on same hardware) on pages that require extensive I/O (thread delays while waiting for a response). When ADO.NET executes a database operation, the thread executing that will "hang" while waiting for a reply from the DB; if the latter is, for example, down and the timeout is set to 5 minutes (incredible but true), that thread will be wasted for 5 minutes. If your thread limit is set to 25, that number of users will already cause thread starvation. With asynchronous operations, the thread is released back to the thread pool while waiting for the response, and as such allows to answer to other pages on the web (that might or might not hit the DB again). In any case, it's primarily a scalability consideration that, along the run, improves performance as well (better resources usage).
Using the code
The solution source code includes the following multiple demonstrations:
- syncFlavors/ folder (each is a full standalone example) includes the "old" synchronous way of doing things:
- AddImage.aspx - was initially used to add an image to the included SQLExpress database
- ShowImageScalar.aspx - ASP.NET page that uses the ADO.NET
ExecuteScalar
method to render an image to the browser
- ShowImageReader.aspx - ASP.NET page that uses the ADO.NET
ExecuteReader
method to render an image to the browser
- ShowImageAsyncReader.aspx - ASP.NET page that uses the new ADO.NET v2
BeginExecuteReader
method to render an image to the browser
- Asynchronous HTTP Handler:
- App_Code/ImageAsyncHandler.cs - the class for the HTTP handler
- web.config - requires changes to register our HTTP handler
- ShowImage.aspx - includes an image tag to retrieve BLOB using our HTTP handler
- Asynchronous WebHandler (alternative implementation)
- ImageAsync.ashx - our WebHandler (same as HTTP handler, but requires no web.config changes!)
- ShowImage.aspx - includes an image tag to retrieve BLOB using our WebHandler
The only difference between the implementations is the requirement for HTTP handlers to be registered in the web.config file, which gives us more flexibility in the URL, but limits us, for example, when deploying to ISPs (since they don't always allow changes to the web.config or IIS mappings). That's why WebHandlers where created to workaround those changes - it's in fact exactly the same, without the need to register any handlers or IIS mappings (to map to our new file type, since .ASHX files are mapped by default when installing ASP.NET). Personally, I find the WebHandler implementation "cleaner" and easier to maintain, at the cost of avoiding interesting URLs.
Points of interest
- web.config
ConnectionStrings
property
This new web.config section allows us to centrally store our connection strings, identified by name.
<connectionStrings>
<add name="LocalBLOB"
connectionString="Data Source=.\SQLEXPRESS;
AttachDbFilename=|DataDirectory|BLOBTest.mdf;
Integrated Security=True;User Instance=True;
Asynchronous Processing=true"/>
</connectionStrings>
Note the "|DataDirectory|" special keyword which will be replaced on runtime by ASP.NET with our App_Data folder to avoid hard-coding paths. In code, we can then retrieve this, by using:
ConfigurationManager.ConnectionStrings["LocalBLOB"].ConnectionString
- Asynchronous HTTP handler with asynchronous DB read
An asynchronous HTTP handler uses an IAsyncResult
object to notify when the operation has been completed, freeing our thread from waiting for a response. Combined with an asynchronous DB operation (which also returns an IAsyncResult
object!), ASP.NET will continue the execution of the handler (and re-assign the thread) once the DB operation is finished.
using (SqlDataReader reader = cmd.EndExecuteReader(result))
{
this.renderImage(context, reader);
}
- Asynchronous HTTP handler vs. WebHandler (.ashx)
An HTTP handler requires a change to the web.config file to make it visible to ASP.NET. The "path
" property allows us to specify almost any URL we can think of, and the "type
" property points to our class.
<httpHandlers>
<add verb="*" path="img/*" type="ImageAsyncHandler"/>
</httpHandlers>
Our HTTP handler uses URL segments to figure out the "*" in the URL; to retrieve an image served by our HTTP handler, we can specify an image tag like the following:
<img src="img/960a3be7-80d1-4528-bfaa-975cf9d53800" />
WebHandlers overcome this requirement by making the .ASHX extension already known to ASP.NET (pre-configured IIS mapping), and as such do not require any web.config changes. Of course, this also means that we must use a different URL, and as such to retrieve an image from our WebHandler (which now uses the query string), the following image tag is required:
<img src="ImageAsync.ashx?960a3be7-80d1-4528-bfaa-975cf9d53800" />
- ADO.NET v2 Asynchronous
DataReader
The new BeginExecuteReader
method requires a slightly different code design since now the execution of the query and actually using the results are in two different methods. This also allows better thread reuse since our main thread does not wait for the DB operation to finish.
- Buffering BLOB fields
When using the DataReader
, each row in the database is fully loaded into memory before it gets to our code; obviously, if our BLOB column includes, let's say, 1GB of data, this would exhaust our memory resources. If we instead specify a different CommandBehavior
we can then better buffer it from the DB, saving us memory.
return cmd.BeginExecuteReader(cb, context,
CommandBehavior.SequentialAccess |
CommandBehavior.SingleRow |
CommandBehavior.CloseConnection);
- Rendering an error message as an image
An HTTP handler that returns an image makes it difficult to show any exception details, thus hindering the debugging and administration of a live site. In my case, I preferred showing the real exception to the user by rendering during runtime an image that includes the error text.
context.Response.ContentType = "image/jpeg";
Bitmap bitmap = new Bitmap(7 * msg.Length, 30);
Graphics g = Graphics.FromImage(bitmap);
g.FillRectangle(new SolidBrush(Color.DarkRed), 0, 0,
bitmap.Width, bitmap.Height);
g.DrawString(msg, new Font("Tahoma", 10, FontStyle.Bold),
new SolidBrush(Color.White), new PointF(5, 5));
bitmap.Save(context.Response.OutputStream,
System.Drawing.Imaging.ImageFormat.Jpeg);
History
- First release - feel free to use and let me know your feedback!