XAF - Quartz JobScheduler module
In the past few weeks I have been working on creating a XAF wrapper module for Quartz.Net v2 which is is a full-featured, open source job scheduling system that can be used from smallest apps to large scale enterprise systems.
For those who are not familiar with concept of job scheduling, here is some background information. Also, there great tutorial for Quartz, which will give you a good and necessary introduction
Quartz default implementation
Quartz supports several datastores (sqlserver,ce,mysql,imemory etc) that you can utilize by providing an appropriate connection string, same as XPO.
It has a decoupled architecture with just 2 basic entities (JobDetails and Triggers) which are related by Group property. For instance you can create a number of JobDetails, set its Group to “LongRunning”, than create a Trigger with the same Group property. When the trigger is executed, all jobs for that group will run.
There is also a JobDataMap which represents a dictionary that holds serialized values and is used for jobs-triggers communication. You fill it with data when you create a trigger or a job and when they are executed you get that values back for further use.
JobDetails holds a reference of the Type that implements the Quartz.IJob interface where you put the code specific to your job. Quartz instantiates that type using reflection and runs its Execute method using the context parameter that passes all available information (Scheduler instance, JobDataMaps, etc.)
namespace Quartz {
public interface IJob {
void Execute(IJobExecutionContext context);
}
}
That was a description of how Quartz works by default. Now we would want to put a nice-looking and functional UI on top, and we have XAF (+Xpand) to come for the rescue. To provide as much decoupling as possible from the original Quartz implementation I resorted to creating persistent classes with the same set of properties as Quartz ones. I also needed to have a set of controllers to call Quartz API when the Xpand classes are changed. This allows me to be able to design any XAF domain and use the API to interface with Quartz datastore.
Xpand JobScheduler Domain description
First of all we have the same basic entities XpandJobDetail, XpandJobTrigger. A JobSchedulerGroup was introduced to achieve the default quartz relationship as shown below:
Additionally a many to many relationship between XpandJobDetail and XpandJobTrigger has been added allowing you to associate triggers, and jobs (even if they not belong to any group).
A new type of trigger the JobListenerTrigger was introduced in order to be able to trigger a job after an event of another job was executed
Now our XpandJobDetail stores a reference to the Quartz.IJob interface using the Persistent XpandJob object (the same way Quarz does). However, one would have to decorate this class with JobDetailDataMapType or JobDataMapType attribute in order to tell XAF which XpandJobDataMap to associate with the XpandJobDetail or XpandJob And, as you already guessed, JobDataMap, that is dictionary instance for Quartz, is populated from XAF persistent classes.
Bellow you see the sample ThresholdCalculationJob provided by Xpand.ExpressApp.JobScheduler.Jobs assembly:
[System.ComponentModel.DisplayName("ThresholdJob")]
[JobDetailDataMapType(typeof(ThresholdJobDetailDataMap))]
public class ThresholdCalculationJob : IJob {
public const string ThresholdCalcCount = "ThresholdCalcCount";
readonly ILog log = LogManager.GetLogger(typeof(ThresholdCalculationJob));
public void Execute(IJobExecutionContext context) {
log.Info("EXECUTING:ThresholdCalculationJob");
var application = ((IXpandScheduler)context.Scheduler).Application;
var objectSpaceProvider = application.ObjectSpaceProvider;
var jobDataMap = context.JobDetail.JobDataMap;
var typeInfo = objectSpaceProvider.TypesInfo.FindTypeInfo((Type)jobDataMap.Get<ThresholdJobDetailDataMap>(map => map.DataType));
object count;
using (var unitOfWork = new UnitOfWork(((ObjectSpaceProvider)objectSpaceProvider).WorkingDataLayer)) {
count = unitOfWork.GetCount(typeInfo.Type, CriteriaOperator.Parse(jobDataMap.GetString<ThresholdJobDetailDataMap>(map => map.Criteria)));
}
jobDataMap.Put<ThresholdJobDetailDataMap>(ThresholdCalcCount, count);
}
}
Viola! We get a XafApplication instance and off we go using our usual XAF routines!
Apart from the XafApplication instance, the above code has some other interesting details, e.g.
var typeInfo = objectSpaceProvider.TypesInfo.FindTypeInfo((Type)jobDataMap.Get<ThresholdJobDetailDataMap>(map => map.DataType));
Here we are requesting the DataType property value from ThresholdJobDetailDataMap object (didn’t I already tell you that our XpandJobDetails have JobDataMaps are simply XAF persistent objects?).
The server
Xpand provides a very easy way to configure Xpand.Quartz.Server assembly. Server is built with the TopShelf framework, that means you can either run it as a console app or as a windows service (using –install as parameter) . Server is responsible of executig all jobs (classes that implement that Quartz.IJob interface).
One thing you have to remember is that, since Server instantiates XafApplication, the configuration file on the server has to be synchronized with the one on the client.
In order for the server to know which XafAppliation to create the following change is needed:
The Demo
Xpand FeatureCenter has a special assembly (Xpand.ExpressApp.JobScheduler.Jobs) that provides 2 sample job, the ThresholdCalculationJob that you saw already and SendThresholdCalculationEmailJob. Also there is a new EmailTemplateEngine that has been build in order to support the SendThresholdCalculationEmailJob which is naturally based on persistent objects and that can be configured runtime.
The scripts that create a Quartz database can be found in eXpand sources under Resource/Quartz folder. Quartz database has to be created in order to run the demo from FeatureCenter.
To give you a better idea about above mentioned functionality and to show what the sample application (Feature Center in this case) that uses JobScheduler module may look like, here is a short screen cast:

