Introduction
Ever had an application where the Stored Procedure just takes too long? You've
done all the optimization you can but still takes a few minutes to run. Maybe a
search on a VLDB or building a complex report. Maybe you have a lot of little
processes that you run one after another.
In this article we are going to build a web application that executes an
asynchronous process. This will allow us to return a web page so that the user
is not left there waiting with a blank screen, while our database is still
working in the background.
Section 1: Assumptions
This article is not an introduction to C#, and assumes that you are familiar
with the basic syntax. This article is not an introduction to the ADO.Net data
model, I'm going to assume that you are already familiar with the following
classes:
To keep this article a reasonable length I'm going to look at a query that
doesn't return data, although you will be able to use what you learn here to
write an application that does.
We will use a test stored procedure that just takes a long time, but this
article can be adapted to any process. This is the stored procedure we will
use:
create proc waittest ( @iterations int = 10 ) as begin print 'Doing wait test' print 'This will wait for 1 second ' + cast( @iterations as varchar ) + ' times.' declare @i int set @i = 0 while @i < @iterations begin select @i = @i + 1 waitfor delay '000:00:01' print 'You have reached step ' + cast( @i as varchar ) + '. Here is a load of technical' + ' information about this step. ' + 'blah blah blah blah blah blah' end print 'process complete' end go
Finally we will be building a web application, but this could be easily adapted
to a desktop solution. We will leave out error handleing and the like for now,
just to keep things simple.
Section 2: Getting Started
In order to execute our SqlCommand asynchronously we will build a new class. We
will start with our basic class declaration and constructor:
using System; using System.Data; using System.Data.SqlClient; using System.Threading; //this is our Threading namespace namespace kh.SqlTools { [Serializable] public class AsyncCmd : System.Object , System.IDisposable { //this will be the connection we execute against public System.Data.SqlClient.SqlConnection Connection; //this will be the command we execute public System.Data.SqlClient.SqlCommand Command; public AsyncCmd() {} public AsyncCmd(SqlConnection conn, SqlCommand comm) { this.Connection = conn; this.Command = comm; } //IDisposable requires this and it allows us to destroy our //connection and command objects public void Dispose () { try { if (this.Connection != null) this.Connection.Dispose() ; if (this.Command != null) this.Command.Dispose() ; } catch {} } //bool to show true once our process is complete. public bool IsComplete = false;
Section 3: Delegates and Events
A delegate is a description of what a method will "look like"; what it
should return and what parameters it should take. It allows you to call a
method in a distant class that probably isn't even created yet.
An event is a method in the local class that calls a method in the
distant class, both the local event and the distant method take the same
variables and return the same thing as the delegate.
Both delegates and events are seen as quite arcane, complex and
technical, but in reality if you have used .net then you have experience of
these already. See the cut-out below for an example of where you may have used
them before:
A Brief Intro to Delegates
When you create an .aspx page in Visual Studio it is created with a method
called Page_Load already written for you and a region called "Web Form Designer
generated code". Towards the bottom of this region is a line like this:
this.Load += new System.EventHandler(this.Page_Load);
This line is sometimes described as "subscribing" to an event. I'm going to
break it down into three parts:
- this.Load
is the event fired by the System.Web.UI.Page class when it is about to load.
- new System.EventHandler
is the delegate for the method required to handle this event.
- this.Page_Load is the method in our page that will be executed when the
event fires.
System.EventHandler is a delegate, which describes what the method should look
like, what it should return and what parameters it should expect.
Page_Load is going to handle the event System.Web.UI.Page.Load which is
of delegate type System.EventHandler, so Page_Load has to return the same thing
(void) and take the same parameters (object sender and System.EventArgs e) as
System.EventHandler
We need a delegate and an event so that we can fire
events that our web page can handle:
public delegate void InfoMessage ( object sender, string Message ); public event InfoMessage OnInfo;
We also want to handle events from the objects we are using within this class:
public void InfoHandler(object sender, SqlInfoMessageEventArgs e) { if (OnInfo != null) //check for subscriber OnInfo(sender, "Info: " + e.Message); } public void ChangeHandler(object sender, StateChangeEventArgs e) { if (OnInfo != null) //check for subscriber OnInfo(sender, "SqlConnection Change from " + e.OriginalState.ToString() + " to " + e.CurrentState.ToString()); }
These methods are going to handle events fired from the
System.Data.SqlClient.SqlConnection class and bubble them up to whatever
is calling this class. Now we will add a method that executes the process we
want to be done asynchronously:
public void ExecSql() { if (OnInfo != null) OnInfo(this, "AsyncCmd Starting"); if(this.Connection == null || this.Command == null) throw new System.Exception( //fire error if objects not set "Both Connection and Command values must be set!"); if (OnInfo != null) { //check for subscriber this.Connection.InfoMessage += //bubble prints new SqlInfoMessageEventHandler(InfoHandler); this.Connection.StateChange += //bubble open|close new StateChangeEventHandler(ChangeHandler); } this.Connection.Open(); this.Command.Connection = this.Connection; this.Command.CommandTimeout = 0; if (OnInfo != null) OnInfo(this, "Executing SqlCommand"); this.Command.ExecuteNonQuery(); this.Connection.Close(); if (OnInfo != null) OnInfo(this, "AsyncCmd Complete"); this.IsComplete = true; }
I'm assuming here that you're pretty familiar with opening a connection and
executing a command against it, so I'm mainly going into detail on the
delegates here. When OnInfo is called it fires the event to a method of
the class that is calling this one and looks like the delegate InfoMessage.
Also here we have subscribed to the InfoMessage and StateChange events
of the SqlConnection object.
Section 4: Asynchronous Methods
The first thing we need to add is a thread which is going to be doing the
asynchronous work:
protected System.Threading.Thread _bldThrd;
We are going to create Start(), Stop() and Join() methods. Start() sets the
asynchronous process off, Join() allows us to re-join and wait for it to finish
synchronously and Stop() aborts the process.
//start async process public void Start() { //create new Thread and set ExecSql to the async //method using ThreadStart. _bldThrd = new System.Threading.Thread( new System.Threading.ThreadStart( ExecSql ) ); _bldThrd.Start(); } //join async process, if running public void Join() { if( _bldThrd != null ) //check Thread init if ( _bldThrd.IsAlive ) //check running _bldThrd.Join(); //join it } //stop async process, if running public void Stop() { if( _bldThrd != null ) //check Thread init if ( _bldThrd.IsAlive ) //check running _bldThrd.Abort(); //kill it //it may have stopped, but wait for it this.Join(); } } }
I'm not going too deep into the System.Threading namespace here (no space) but
these three methods are enough to build our application in this article.
Section 5: The Web Page
Now create a new web forms page (.aspx) We need a few basic methods that we are
going to use on the page:
//Adds a log item to the session private void AppendSessionLog(string message) { if (Session["log"] != null) Session["log"] += "|" + message; else Session["log"] = message; } //Writes the contents of the log to the page private void DisplaySessionLog() { if (Session["log"] != null) foreach(string s in Session["Log"].ToString().Split( new char[] {'|'})) Response.Write ( s + "<br>" ); } //client side javascript that refreshes the page after 2 seconds private void ClientSideReload() { Response.Write("<script language=\"javascript\">\n" + " setTimeout(\"submit();\",2*1000);\n" + "</script>Please Wait..."); }
We also need the class we have just written:
public kh.SqlTools.AsyncCmd ac;
If we add a method which looks like our delegate (see InfoMessage
above) we will be able to handle events fired from this class:
//note how this returns void and takes object, string //just like kh.SqlTools.AsyncCmd.InfoMessage private void ac_OnInfo(object sender, string Message) { this.AppendSessionLog( Message ); }
This method clears any old process and starts a new one.
private void StartProc () { if ( ac != null ) //check we have an async cmd ac.Dispose(); //dispose of it if we do Session.RemoveAll(); //clear the session to start again //create an instance of our async class ac = new kh.SqlTools.AsyncCmd(); ac.Connection = new System.Data.SqlClient.SqlConnection( ## Your connection string here! ## ); ac.Command = new System.Data.SqlClient.SqlCommand("waittest"); ac.Command.CommandType = System.Data.CommandType.StoredProcedure ; ac.Command.Parameters.Add("@iterations",50) ; //subscribe to the delegate we created earlier ac.OnInfo += new kh.SqlTools.AsyncCmd.InfoMessage(ac_OnInfo); ac.Start(); //add our class to the session so that we can retrieve it Session["ac"] = (object) ac; }
All that remains is some events fired from the page to pick up our session
objects and write them to the page as appropriate
private void Page_Load(object sender, System.EventArgs e) { // Get the stored Session variable if (Session["ac"] != null) ac = (kh.SqlTools.AsyncCmd) Session["ac"]; } private void Page_PreRender(object sender, EventArgs e) { this.DisplaySessionLog(); if ( ac != null ) { //check we have an async cmd if ( !ac.IsComplete ) //if it's not finished ClientSideReload(); //reload the page else { ac.Dispose(); ac = null; Session.Remove("ac"); } } }
Now when we call the StartProc() (from a button click for instance) this page
will start to execute the Sql Command and keep refreshing every 2 seconds until
it has finished.
Conclusion
So put this all together and we have a page that can access a process that
takes a long time. We could execute two or more of these at the same time,
which give you the ability to balance the additional connections or server
resources used verses how quickly you need the results.
In order to keep this complicated article down to a reasonable length I've cut
out a few things:
- Error Handling
Unless you want lots of strange errors that you just can't find you should
always handle all potential errors in asynchronous calls by firing an event to
the calling class. You also need to make sure that you kill any other
Threads when an error occurs.
- XML comments
This is a fantastic feature of C# that makes your code self documenting (well,
almost) and I've completely ignored it here for sake of brevity.
- SqlClient objects not Thread Safe
In the classes above it is possible to access the SqlConnection and SqlCommand
objects that are being accessed by another thread by calling the Start() method
and then just accessing them. This is a bad idea because they are not Thread
safe, i.e. this behaviour will often crash the application and occasionally the
whole server!
You can do what I have here (which works and is safe but messy) by just not
accessing them. However you should generate static copies of the SqlClient
objects for use in the new thread.
- Intellisense
This is pretty useful for delegates in Visual Studio 2003. When you declare
your event (e.g. "ac.OnInfo += ") hitting TAB generates the rest of the
statement. Hitting TAB again generates the handler (e.g. "ac_OnInfo") method
for you.
Resources
1:
2: