logo

ASP.NET Request Logging with Asynchronous Fire And Forget Pattern

By Peter Bromberg
Printer Friendly Version
View My Articles
923 Views
    

You too could earn $100 for writing an article for EggHeadCafe.

Shows how to perform high-speed ASP.NET Request logging to a database using the asynchronous Fire and Forget delegate wrapper patterns.



"My analyst told me (what) that I was right out of my head the way he described it (how) he said I'd be better dead than live I didn't listen to his jive I knew all along he was all wrong and I knew that he thought (what) I was crazy but I'm not oh no oh no oh no" -- Lambert, Hendricks and Ross

In wiring up a recent article about Request Logging to feature a couple of tricks I've learned in my short happy career as a .NET programmer, I revisited the Fire And Forget pattern and decided to add this into my code to make it even faster.

Fire And Forget using a "self completing delegate wrapper" is something I first came across on Mike Woodring's page, and I've written about it several times here.  Jon Skeet also has an implementation, which I think is even "cleaner", and in my last article I used Jon's code, so I continue with that.

But I also like to "package up" commonly used utilities so that everything I need is in one class library. In this particular case, I have found that the most common usage of the Fire and Forget pattern, at least for me, is to execute insert or update stored procs in SQL Server.

The idea is that anytime you have an operation for which no explicit return value is required, especially if that operation is time consuming, you want to turn it from a blocking call, where you are waiting for it to complete before your code logic can continue, into a non-blocking "fire and forget" call that gets handled immediately  on a background thread, thus freeing your code to sail right on so there aren't any delays at all.

In a high-volume web application, you can probably think of several candidates for this pattern.

In order to do this "Packaging" I went back to the old standby, the v2 SqlHelper class.  I use this for two reasons: first, it has overloads that accept a connection string, a stored procedure name and a simple object array of the stored proc's parameter values, making it a great candidate for a fixed delegate signature.  Second, the SqlHelper class caches SqlParameters statically when you use these overloads, making it another order of magnitude faster for quick database access. In sum, the SqlHelper class remains one of those little gems of best - practices code that cannot be beat.

I don't know about you, but 95% of my data access is with SQL Server on applications where that's very unlikely to change, so you are going to have a real fight on your hands trying to convince me to get all RDBMS agnostic and go for the new "provider model" approaches that everyone is evangelizing, at least as far as database work is concerned. Not that I am against it, I already have used Enterprise Library 3.1 in production code. It's just that I believe in keeping things simple when possible.

The debate raged again recently on some blogs with Frans Bouma bellowing how "stored procs are evil". No, they aren't evil. And you also have triggers, and user-defined functions, and now CLR-hosting of .NET code with stored procs. There's absolutely no way you are going to get this kind of useful, RDBMS-specific functionality in some generic "Sql in the mapping class" approach. Sorry!

To make this work, all I had to add to Jon's excellent ThreadUtil class was the following:

// This is the "genericized" insert or update pattern delegate we use:
        // Our target method only needs a connection string, a stored proc name, and an object array of the parameter values.
        public delegate void InsertOrUpdateDelegate(string connectionString, string storedProcName, object[] parms);

        // This is the target method of our delegate. It simply calls SqlHelper.ExecuteNonQuery.
        // Since the whole idea here is "fire and forget", we do not need or want any return value.
        public static void PerformInsertOrUpdate(string connectionString, string storedProcName, object[] parms)
        {
            SqlHelper.ExecuteNonQuery(connectionString, storedProcName, parms);
        }

As can be seen, I have an InsertOrUpdateDelegate delegate whose signature exactly matches the following PerformInsertOrUpdate method, which simply makes the Sqlhelper call.

This does not have to be database access - you can use the identical pattern to do HttpWebRequest calls, fileSystem writes, and so on. The key determinant is that you do not need a return value; you want to Fire and forget in a non-blocking "handoff" method call that gets handled on a background thread.

In order to use this in the sample Request Logger, all I need to do is this:

In Global.asax:

protected void Application_PreRequestHandlerExecute (object sender, EventArgs e)
        {
            RequestLogger.Logger.LogRequest(sender as HttpApplication);
        }

In the RequestLogger class:

public static void LogRequest(HttpApplication app)
        {
            HttpRequest request = app.Request;
            EnsureSwitches(app);
            if (!_loggingOn) return;
            bool isCrawler = IsCrawler(request);
            string userAgent = request.UserAgent;
            string requestPath = request.Url.AbsolutePath;
            string referer = request.UrlReferrer != null ? request.UrlReferrer.AbsolutePath : "";
            string userIp = request.UserHostAddress;
            string isCrawlerStr = isCrawler.ToString();
            object[] parms = new object[] {userAgent, requestPath,referer, userIp, isCrawlerStr};
             try
             {
               ThreadUtil.FireAndForget(
                      new ThreadUtil.InsertOrUpdateDelegate(ThreadUtil.PerformInsertOrUpdate),
                        _connectionString, "dbo.insertRequest", parms);
            }
            catch (Exception ex)
            {
                // this is just for quick debugging, can be commented out:
                app.Response.Write(ex.Message);
            }
          
            if (isCrawler && _denyBots)
                DenyAccess(app);
        }

As you can see in the ThreadUtil.FireAndForget(... call above, because of the nice "packaging", the FireAndForget call could  be made with a single line of code, assuming you wanted to define your object[] parameter array inline. For a high-volume web application where you want to log information about every request, this is the way to absolutely speed up the operation! On an "average" machine, this arrangement will reliably queue up 100,000 such inserts in as little as 2.6 seconds total. Assuming those 100,000 operations were queued up all at once, they might take another full minute to complete and get into the database, but that's not your problem because your code is already free to go on and finish its business without delay. The key thing to understand is that your FireAndForget utility is handling the EndInvoke call for you and closing the WaitHandle to prevent leaks.

You can download the sample project which includes a SQL Script to generate the database table and sproc, along with all related code. As would be expected, you'll need to create a database or using an existing one and run the SQL Script, then adjust your connection string in web.config to match your environment.


Biography - Peter Bromberg
Peter Bromberg is a C# MVP, MCP, and .NET expert who has worked in banking, financial and telephony for over 20 years. Pete focuses exclusively on the .NET Platform, and currently develops SOA and other .NET applications for a Fortune 500 clientele. Peter enjoys producing digital photo collage with Maya,playing jazz flute, the beach, and fine wines. You can view Peter's UnBlog and IttyUrl sites. Pete Tweets at peterbromberg


Didn't Find The Answer You Were Looking For?

EggHeadCafe has experts online right now that may know the answer to your question.  We pay them a bonus for answering as many questions as they can.  So, why not help them and yourself by becoming a member (free) and ask them your question right now?
Ask Question In Live Forum

Article Discussion: ASP.NET Request Logging with Asynchronous Fire And Forget Pattern
Peter Bromberg posted at Thursday, June 21, 2007 3:22 PM
Original Article
 

Download Material
Joe Reynolds replied to Peter Bromberg at Sunday, July 01, 2007 12:25 PM
I downloaded the code but see no script to create the sql table and stored procedures as mentioned in the article.
 

Sorry- its in the zip file now.
Peter Bromberg replied to Joe Reynolds at Sunday, July 01, 2007 12:53 PM
"RequestLogger.sql" is in the root of the zip.
 

Logging Issues
Joe Reynolds replied to Peter Bromberg at Sunday, July 01, 2007 1:43 PM

Sorry for thre prior message. I did find the sql script. Now I have the project running and compiled. To do so I have to all the reference to RequestLogger.

Default aspx runs fine and items are recorded in my WebLogs database. However, when looking at the database table Requests, I'm seeing the UserAgent in the RemoteIP column and the UserAgent column is is a dupe of the RequestPath column -- both show "/default.aspx."

I'm assuming the code that is doing all this is in RequestLogger.dll. However, I see no way to edit or change this class.

Any assistance would be appreciated. Obviously I'm missing something. Thanks.

 

Ohboy-
Peter Bromberg replied to Joe Reynolds at Sunday, July 01, 2007 7:30 PM
I apologize, this was because of my experimentation. Download the solution zip again, it has everything in it, and the stored proc parameter values in the insert method are adjusted to match the table. I promise- I built and tested it!
 

Working OK
Joe Reynolds replied to Peter Bromberg at Wednesday, July 04, 2007 3:16 PM

Peter, Thanks for the corrected code. All is now working ok with the sample project and I have all the necessary files. Did have to change the VS ver 9 in your file to ver 8.

Do you have any thoughts as to how this compares in terms of speed, server load, whatever to using some sort of standard IIS sql logging techique or the normal pure text IIS logging?

 

Wondering how to get full referer path?
Tim Meers replied to Peter Bromberg at Saturday, July 14, 2007 6:16 PM

In case any one else wants to know you can modify lines 72 and 73 in logger.cs to this:
string requestPath = request.Url.AbsoluteUri;
string referer = (request.UrlReferrer != null) ? request.UrlReferrer.AbsoluteUri : "";

Thus getting the full path on the requester and the requested page. This might be a little more helpful than just the file name, even moreso in my case where I'm calling rows from a DB

Oh one more quick fix, I'm always annoyed with the webresource.axd calls in my logs so I added a delete statement to the end of the insert SP to clear those out for me.

Delete from Requests
Where RequestPath like "/webresource.axd"

But again great code, very helpful

Tim Meers

 

How to use
Apoorva Dixit replied to Peter Bromberg at Tuesday, July 17, 2007 12:55 PM
Hi
I have got an instance where on a submit of a web page I need to make a big database operation call. I dont need to wait for the results, so this is the perfect solution to my problem.
I have downloaded the sample and installed it but I am wondering as to how I can make calls to the FireandForget methods from my web pages?

Regards
Apoorva
 

Apoorava, Did you get this working
prat Rani replied to Apoorva Dixit at Thursday, June 12, 2008 5:01 PM

Apoorva,

I have got an instance where on a submit of a web page I need to make a big database operation call. I dont need to wait for the results, so this is the perfect solution to my problem.
I have downloaded the sample and installed it but I am wondering as to how I can make calls to the FireandForget methods from my web pages?

I have similar requirements. Did you implement it and is it working. If yes, can you please share the steps and code you used.

 

Thanks,

PK

 

 

Source code for RequestLogger.dll
Ali Faradj replied to prat Rani at Monday, January 19, 2009 9:00 PM

Hello,

I have downloaded the source for the article, but I do not see the source for the RequestLogger.dll. Am I missing something here.

Your assistance will gratly be appreciated.

 

Ali,

 

Source for RequestLogger Class
Ali Faradj replied to Ali Faradj at Tuesday, January 20, 2009 9:34 AM

Hello Peter,

 

Would you please assist me in getting the source for the RequestLogger? I cannot find it in the download file.

Without having a source for the RequestLogger, there will be no way to modify or use this class. Your assistance would greatly be appreciated.

 

Ali,

 

 

Missing source RequestLogger
Ali Faradj replied to Peter Bromberg at Saturday, January 24, 2009 11:52 AM

Would you please assist me in getting the source for the RequestLogger? I cannot find it in the download file.

Without having a source for the RequestLogger, there will be no way to modify or use this class. Your assistance would greatly be appreciated.

 

Ali,

 

The Logger class is right there in the downloadable source
Peter Bromberg replied to Ali Faradj at Saturday, January 24, 2009 12:02 PM
I just checked it. Unzip the source, load the solution, and make sure that both projects are loaded in Visual Studio.
 

Time-consuming sql queries
Tom . replied to Peter Bromberg at Saturday, March 14, 2009 6:30 AM
Hi, I've tried this method to call an sql stored procedure that takes ~10 minutes to run (which is over the regular timeout time). The sql command still times out (well it makes sense). Could this pattern be upgraded to include such eventualities too? Because i don't need to know the return value of the query either, so it's similar in the nature of fire-and-forget. Cheers.
 




  $1000    Adam Houldsworth - $210  |  Jonathan VH - $142  |  Kirtan Patel - $140  |  F Cali - $117  |  Huggy Bear - $67  |  more Neado  |  Free Icons  |  Privacy  |   (c) 2010