One of the greatest additions to DotNetNuke was the inclusion of a threaded scheduler in DotNetNuke 2.1.1. The scheduler has provided a robust framework for handling many background tasks in DotNetNuke. Even though the scheduler was added to the framework two years ago, it has not seen widespread adoption by the module developer community due to a couple of major shortcomings.
The scheduler executes tasks in a separate thread pool which allows these tasks to run independent of any single web request. This leads to a huge problem: the threads in the thread pool do not have access to an HttpContext. Several of the DotNetNuke framework APIs use the current context for storing data. This data is necessary for certain parts of the framework to function correctly. When these APIs are used in a scheduled task, an “object reference not set” exception occurs.
The lack of HttpContext has been a major stumbling block for the DotNetNuke development community, but it is not the only problem that exists for the scheduler. The scheduler also does not operate on a given page or within a given portal. The majority of the DotNetNuke APIs require either a PortalID or a TabID to function. Until now a scheduled task was forced to either hardcode these values or iterate over all portals and/or tabs and use some business logic to run against the data for these portals or tabs. This is not an optimal solution.
So how do we overcome these problems and allow the scheduler to achieve it’s true potential? Let’s first look at the HttpContext problem.
In June of 2005, Phil Haack provided the first nugget of information needed to overcome this barrier. In his blog, Phil describes building a simulated HttpRequest which can then be used to construct an HttpContext object. Because HttpContext.Current is a read and write property we can create our own Context object and set HttpContext.Current to our new object.
Unfortunately, Phil’s approach is not complete for our purposes. Phil is building his context from within a non-web appdomain. If you try to run Phil’s code inside of DNN you will get an error in the constructor of the SimpleWorkerRequest that Phil subclasses. In a web environment you cannot use the long constructor for SimpleWorkerRequest:
Public Sub New(ByVal appVirtualDir As String, ByVal appPhysicalDir As String, ByVal page As String, ByVal query As String, ByVal output As TextWriter)
Using this constructor in a web app will result in an HttpException telling you that this constructor cannot be used in the current application context. The error message indicates that you cannot override the application directory (as set by appPhysicalDir). Well, what if we used the shorter constructor:
Public Sub New(ByVal page As String, ByVal query As String, ByVal output As TextWriter)
Since this doesn’t change the physical directory is should run ok. Unfortunately, this constructor throws and internal “object reference not set” error.
So why does this work in a normal application but not in a web application, and can we get around the problem? After doing some testing on the shorter constructor and using our trusty Reflector, I was able to determine that there are three lines of code inside the constructor which are relevant:
Me._appPhysPath = Thread.GetDomain.GetData(".appPath").ToString
Me._appVirtPath = Thread.GetDomain.GetData(".hostingVirtualPath").ToString
Me._installDir = Thread.GetDomain.GetData(".hostingInstallDir").ToString
These three lines essentially get some information from the AppDomain internal cache. The error occurs because the “.hostingVirtualPath” is not set.
It seems that the longer constructor would allow me to set these values. Maybe it is possible to call the longer constructor. Sure enough, the long constructor uses the data passed in the constructor to set these values rather than relying on the AppDomain cache. However there is one little problem. There is a guarding test at the beginning of the constructor that will prevent this code from running in a web-context:
In looking at the MSDN documentation, I found that there is a matching SetData method on the AppDomain object. By calling Thread.GetDomain.SetData(“.appPath”, nothing) before we call the constructor we should be able to fool the system into allowing us to call this constructor. This in fact appears to work. I further simplified Phil’s code because in my case I didn’t need a custom WorkerRequest. So here is what my code looks like:
Imports System.IO
Imports System.Web
Imports System.Web.Hosting
Imports System.Threading
Namespace DotNetNuke.Catalook.SHD.ExportOrders
Public Class SchedulerHttpContext
Private Shared appPhysicalDir As String
Public Shared Sub SetHttpContextWithSimulatedRequest()
Dim appVirtualDir As String = "/"
' We have to save the Physical Directory. Then reset the appdomain data.
' This is necessary so that we can reset path information in our new requestobject.
' We want to save this info because it won't be available the next time this code runs.
If appPhysicalDir Is Nothing OrElse appPhysicalDir.Length = 0 Then
If Not Thread.GetDomain.GetData(".appPath") Is Nothing Then
appPhysicalDir = Thread.GetDomain.GetData(".appPath").ToString
Else
Throw New System.ApplicationException("Unable to determine the application directory.")
End If
End If
Thread.GetDomain.SetData(".appPath", Nothing)
Dim page As String = (HttpRuntime.AppDomainAppVirtualPath + "/default.aspx").TrimStart("/")
Dim query As String = ""
Dim output As StringWriter = New StringWriter
Dim workerRequest As SimpleWorkerRequest = New SimpleWorkerRequest(appVirtualDir, appPhysicalDir, page, query, output)
HttpContext.Current = New HttpContext(workerRequest)
End Sub
End Class
End Namespace
I do save the value of the .appPath before we delete it and then pass this value back into the SimpleWorkerRequest. This should help ensure that we don’t run into other unforseen problems.
Now, before I perform an work in my SchedulerClient I just make a call to SetHttpContextWithSimulatedRequest:
SchedulerHttpContext.SetHttpContextWithSimulatedRequest()
The need that led me to this solution was an error that was called by the User API. Buried in the User controller code is a reference to Common.Globals.SetApplicationName. As you probably already know, or could guess, SetApplicationName/GetApplicationName saves or retrieves the application name from HttpContext.Current.Items collection. This value is used by the membership and profile providers and has been one of the major sticking points in our framework for the last two years. I have not tested this code with other troublesome APIs so use it at your own risk. There is also the possibility that there could be some other unseen side-effects with changing the appdomain cache, but for now everything appears to be working on my system. Also, this code has not been tested in a medium trust environment. I suspect that it will not work. The reason for this is that both constructors for the SimpleWorkerRequest performs a security demand that requires UnmanagedCode permission. This permission is not normally granted in any partial trust environment. What? You thought the Holy Grail would be without blemishes?
So that solves (partially) the context problem but what about other data that we may need to provide to the scheduler like PortalId and TabId? Well this is a much easier problem to solve.
When Dan Caron first built the scheduler, he envisioned that it would be useful to be able to pass some parameters to the scheduled task. To support this feature, he added a ScheduleItemSettings table. This table holds the ScheduleId, a name and a value. When your SchedulerClient is instantiated it is passed a reference to a ScheduleHistoryItem object. This class inherits ScheduleItem which provides the GetSettings and GetSetting methods for reading values from the ScheduleItemSettings table. Unfortunately, Dan did not have time to finish the API and so a method was never created to allow code to store data to the ScheduleItemSettings table. This is relatively simple to correct, and in fact I have checked in code to add an AddScheduleItemSetting method to the API. This method will be in the upcoming DNN release (assuming I didn’t mess up the code which would result in Shaun ripping it back out ? ).
With this API addition, it is trivial now for a module admin screen to save values to the ScheduleItemSettings table for use by the associated schedule task.
Hopefully, these two techniques will resolve the major problems with the scheduler and allow it to be used for a much richer DNN environment.