DNN Community Blog

Do you have useful information that you would like to share with the DNN Community in a featured article or blog? If so, please contact community@dnnsoftware.com.

 


The quest for the DotNetNuke Holy Grail

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:

If (Not Thread.GetDomain.GetData(".appPath") Is Nothing) Then
                Throw New HttpException(HttpRuntime.FormatResourceString("Wrong_SimpleWorkerRequest"))
End If

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.

Comments

Comment Form

Only registered users may post comments.

NewsArchives


July 2014 (7)
June 2014 (10)
May 2014 (6)
April 2014 (9)
March 2014 (3)
February 2014 (4)
January 2014 (8)
December 2013 (5)
November 2013 (2)
October 2013 (9)
September 2013 (10)
August 2013 (8)
July 2013 (4)
June 2013 (8)
May 2013 (13)
April 2013 (2)
March 2013 (7)
February 2013 (7)
January 2013 (10)
December 2012 (6)
November 2012 (20)
October 2012 (12)
September 2012 (27)
August 2012 (29)
July 2012 (22)
June 2012 (17)
May 2012 (23)
April 2012 (24)
March 2012 (27)
February 2012 (21)
January 2012 (12)
December 2011 (18)
November 2011 (20)
October 2011 (27)
September 2011 (17)
August 2011 (18)
July 2011 (45)
June 2011 (22)
May 2011 (23)
April 2011 (19)
March 2011 (36)
February 2011 (19)
January 2011 (22)
December 2010 (29)
November 2010 (37)
October 2010 (32)
September 2010 (43)
August 2010 (46)
July 2010 (37)
June 2010 (46)
May 2010 (29)
April 2010 (38)
March 2010 (27)
February 2010 (33)
January 2010 (34)
December 2009 (13)
November 2009 (20)
October 2009 (29)
September 2009 (18)
August 2009 (29)
July 2009 (19)
June 2009 (18)
May 2009 (23)
April 2009 (16)
March 2009 (13)
February 2009 (20)
January 2009 (25)
December 2008 (25)
November 2008 (29)
October 2008 (34)
September 2008 (33)
August 2008 (36)
July 2008 (31)
June 2008 (25)
May 2008 (26)
April 2008 (33)
March 2008 (31)
February 2008 (24)
January 2008 (18)
December 2007 (27)
November 2007 (51)
October 2007 (24)
September 2007 (32)
August 2007 (24)
July 2007 (20)
June 2007 (28)
May 2007 (27)
April 2007 (24)
March 2007 (47)
February 2007 (21)
January 2007 (41)
December 2006 (21)
November 2006 (16)
October 2006 (24)
September 2006 (36)
August 2006 (30)
July 2006 (31)
June 2006 (37)
May 2006 (13)
April 2006 (13)
March 2006 (18)
February 2006 (20)
January 2006 (13)
December 2005 (6)
November 2005 (15)
October 2005 (15)
September 2005 (16)
August 2005 (7)
April 2005 (1)
March 2004 (4)
February 2004 (6)
January 2004 (1)
November 2003 (4)
October 2003 (22)
September 2003 (22)
August 2003 (15)
July 2003 (14)

Copyright 2014 by DNN Corp | Terms of Use | Privacy | Design by Parker Moore Design