This tutorial covers a method for executing custom code upon Application Start that requires full access to all portions of the ASP.NET and DotNetNuke applications. This method avoids the buggy DotNetNuke 5.x Scheduler and expands upon Joe Brinkman's method of using a static initializer to execute code exactly once upon application start.
Skip to Step 4 below for a summary.
There has long been a need to have custom code run when your DotNetNuke application starts. Historically, this has been accomplished through two primary methods. First, the DotNetNuke scheduler facilitates tasks running upon Application_Start. Unfortunately, this method does not work very well and has been known to cause application failure (Example: Setting Scheduler method to "Timer" and having at least one task configured to run upon Application_Start in DNN 5.x). The second method is to use a static initializer. This method is summarized well at Joe Brinkman's site: http://www.dotnetnuke.com/Community/Blogs/tabid/825/EntryId/1103/Simulating-Application_Start-Event.aspx.
The drawbacks to both of these methods is the lack of access to the ASP.NET Application State. For example, some websites need to populate Application Cache when they are fired up. Unfortunately, you do not have access to the Application State from a task, because tasks are run from a separate thread pool. It may be possible to access the Application State using the static initializer method by adding a reference to system.web. It is my understanding that this should work, but this method made me nervous because even code in an HTTPModule constructor is not guaranteed to access the same application state, since an HTTPModule may be invoked more than once within a few seconds on separate threads. I don't think the two are particularly related, but it made me nervous nonetheless.
My solution was to use the static initializer method, but spawn a thread that performs a delayed HTTPWebRequest on one of the DotNetNuke pages that has a custom "maintenance" module dropped onto it. This may sound like a strange solution, but it has a lot of benefits. The primary benefit is that you have access to fully initialized DotNetNuke and ASP.NET application states.
The following will demonstrate how to Cause a specific DotNetNuke page to be run upon application start for the purpose of populating ASP.NET Application Cache.
The purpose of this class is to fire off a thread to access the DotNetNuke page. The execute() function of this class will be called more than once (each time the HTTPModule is instantiated at application start), but the init() function is guaranteed to only be called once. This is key! We don't want the cache to be populated more than once!
Note: We perform a delayed request to a DNN page on a separate thread for a few reasons. First, if we performed the request directly, keep in mind where we are in the Application Start-up Pipeline: The very first part of the intialization. If we perform a synchronous request for a DNN page, we will be deadlocked, because the DNN page will be waiting on initialization to complete, and the intiialization will be waiting on the DNN page to load! Our solution is to offload this "task" to a thread. We'll be content knowing that within the first few seconds of application start, our Maintenance module will have been processed and our Application Cache populated.
Imports System.Web Public Class DNN_App_Start 'This static constructor will be run only once per application start, guaranteed Shared Sub New() Dim t As New System.Threading.Thread(AddressOf GetWebpage) t.Start() End Sub 'This function will be called at least one time (usually more) when the application starts. The first time this function is called, the runtime will fire off the static constructor above Public Shared Sub Execute() Dim x As Integer = 1 End Sub 'This is the function that we spawn our thread on. It performs a 4-second-delayed request for a specific page on the DNN site Private Shared Sub GetWebpage() System.Threading.Thread.Sleep(4000) Dim REQ As System.Net.HttpWebRequest = System.Net.WebRequest.Create("http://test/Maintenance.aspx?Task=PopulateURLCache&Payload=App_Start") REQ.GetResponse() End Sub End Class
Imports System.Web Public Class DNN_App_Start
'This static constructor will be run only once per application start, guaranteed Shared Sub New() Dim t As New System.Threading.Thread(AddressOf GetWebpage) t.Start() End Sub
'This function will be called at least one time (usually more) when the application starts. The first time this function is called, the runtime will fire off the static constructor above Public Shared Sub Execute() Dim x As Integer = 1 End Sub
'This is the function that we spawn our thread on. It performs a 4-second-delayed request for a specific page on the DNN site Private Shared Sub GetWebpage() System.Threading.Thread.Sleep(4000) Dim REQ As System.Net.HttpWebRequest = System.Net.WebRequest.Create("http://test/Maintenance.aspx?Task=PopulateURLCache&Payload=App_Start") REQ.GetResponse() End Sub End Class
Note: I created a class library project with only the above code in it. I then dropped the resulting .dll into the bin folder of the DNN project.
Since I had zero experience working with (or understanding) HttpModules, I chose to break one of my cardinal rules and break into the DNN Source. I downloaded the full source, but opened a single project: DotNetNuke.HttpModules. You can insert the following into the Init event handler of your own HTTPModule if you are competent enough to know how to inject it into the pipeline of your DNN installation.
The following code replaced the Init Function in UrlRewriteModule.vb in the DotNetNuke.HttpModules project
Public Sub Init(ByVal application As HttpApplication) Implements IHttpModule.Init AddHandler application.BeginRequest, AddressOf Me.OnBeginRequest Rubicite.DNN_App_Start.Execute() End Sub
Note: You need to add a reference to your HttpModule project to our .DLL from step 1. I suggest setting copy-local to true, since there will be a .dll of it sitting next to the HttpModule DLL in the DNN Site bin folder.
To apply these changes to my primary DNN instance (which is NOT compiled from my DNN-source), I take the resulting DotNetNuke.HttpModules.dll, pdb, and .xml files and copy them over their counterparts in my DNN site's bin folder.
This step is the most straight forward since most people reading this tutorial will have experience creating DNN Modules.
This module may seem complex at first glance, but it's really quite simple. If "Task=PopulateURLCache" exists in the QueryString, then the module executes a PopulateURLCache function. I set this function up to be able to perform other tasks. It is simply a portal into DotNetNuke from the outside (or from areas immediately surrounding or inside DNN that may be executing before the Application is fully initialized!). Finally, note that the module is IP-restricted, since we don't want people poking around in our maintenance code.
Namespace Rubicite.Modules.Rubicite_Maintenance '--------------------------------------------------------------------------------------------------------------------- ' Purpose: This module is designed to be run from a single DNN page by other bits of code on the site. The original ' reason for developing it was to populate URL Application Cache when the ASP.NET application restarts, since ' we cannot overload the Global.asax code. ' ' Query String Info: ' Task: A string telling the maintenance module which task to perform (required) ' Payload: A string containing additional information for some tasks (optional) '--------------------------------------------------------------------------------------------------------------------- Partial Class ViewRubicite_Maintenance Inherits Entities.Modules.PortalModuleBase Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load 'Get the IP address of the remote host making the request Dim IP As String = Request.ServerVariables("REMOTE_ADDR") 'If it is not an acceptable IP address, GTFO If Not (IP = "127.0.0.1" OrElse IP = redacted) Then Log("Invalid Attempt", "Invalid IP Address: " & IP & ". Terminated.") Return End If 'Determine the task to perform Dim Task As String = Request.QueryString("Task") : If Task Is Nothing Then Task = "" Dim Payload As String = Request.QueryString("Payload") : If Payload Is Nothing Then Payload = "" Select Case Task Case "PopulateURLCache" PopulateURLCache() Case "VerifyURLCache" VerifyURLCache() Case Else Log("No Task Performed", "Requested Task: " & Task & " Payload: " & Payload) Return End Select End Sub '''<summary></summary> Private Sub PopulateURLCache() Try 'Gather URL Mappings '------------------------------------------------ Dim SQL As String = "select * from URLRewrites" Dim CMD As New SqlCommand(SQL) Dim DT As DataTable = ExecuteCMD(CMD).Tables(0) '------------------------------------------------ 'Set the URL Cache Entries. Track count + time Dim CacheCount As Integer = 0 Dim Start As DateTime = DateTime.Now For Each DR As DataRow In DT.Rows If Application(DR("Source")) Is Nothing Then HttpContext.Current.Application(DR("Source").ToString()) = DR("Destination").ToString() End If CacheCount += 1 Next Dim Ender As DateTime = DateTime.Now '-- Log Success ---------------------------------------------------------------------------------------------------------------------------------------- redacted '------------------------------------------------------------------------------------------------------------------------------------------------------- Catch ex As Exception redacted End Try End Sub '''<summary></summary> Private Sub VerifyURLCache() End Sub End Class End Namespace
To cause the Application State to be modified upon Application Start, we do the following:
1) Cause a static constructor to be run upon application start (Steps 1 and 2)
2) Spawn a thread that performs a delayed HTTP Request for one of our DNN pages (Step 2)
3) Our DNN page contains a Maintenance module which populates the application cache. (Step 3)
This solves many of our problems: We caused custom code that needs access to the Application State to be run upon application_start.
If you have any questions or see any errors, please use the contact form and shoot me an email.