WebKMS Toronto SharePoint Consulting

Wednesday, January 20, 2010

Creating custom SharePoint Timer Job

Background:

In one of my recent tasks I had to synchronize the data between two lists that resided on different site collections. The synchronization must occur on an ongoing basis (every 30 minutes interval). There are two ways to implement this; either as a console application or as a SharePoint Timer Job. The second option is more ideal as it doesn’t involve user action (though a console application can be scheduled, it would be more intuitive to run it as a SharePoint Timer Job on a SharePoint Web Application).

Introduction:

SharePoint Timer Job is a scheduled task that should be installed on the Web Application. There could be many ways to install the job (create a custom stsadm command, console application, via a SharePoint feature, etc...). The most ideal candidate to install SharePoint Timer Job is through a SharePoint feature. This feature will be hidden as no one can activate it through SharePoint Interface due to security issues. This will be activated by the administrator using the appropriate stsadm command.

Note: You can neither edit, nor reschedule the existing job (once you’ve installed it) through SharePoint Interface, but you can download some utilities like zevenseas Timerjob Overview” or develop your own that can do it.

Development Process:

1. Custom SharePoint Job.

The custom job must be inherited from SPJobDefinition class that implements the virtual Execute method which Windows SharePoint Services Timer calls when the job is executed. In addition the lock type of the Job must be considered from the following options:

  • SPJobLockType.ContentDatabase - Locks the content database. A timer job runs one time for each content database that is associated with the Web Application; therefore your job will run as many times for each content database associated with the Web Application that exists.
  • SPJobLockType.Job - Locks the timer job so that it runs on only one machine in the Farm.
  • SPJobLockType.None - No locks. The timer job runs on every machine on which the parent service is provisioned.

Please refer to ZevenSeas Post in order to get a clear picture of the subject.

public class PeopleSynchronizerJob : SPJobDefinition

{

public PeopleSynchronizerJob(): base(){ }

public PeopleSynchronizerJob(string jobName, SPService service, SPServer server, SPJobLockType targetType)

: base(jobName, service, server, targetType)

{

}

public PeopleSynchronizerJob(string jobName, SPWebApplication webApplication)

: base(jobName, webApplication, null, SPJobLockType.ContentDatabase)

{

this.Title = " People Synchronizer";

}

public override void Execute(Guid contentDbId)

{

// get a reference to the current site collection's content database

SPWebApplication webApplication = this.Parent as SPWebApplication;

SPContentDatabase contentDb = webApplication.ContentDatabases[contentDbId];

// Do the execute stuff

}

2. SharePoint Feature for the job activation:

  1. Declare the SharePoint Feature as following: Do not forget to set Hidden to be True

xml version="1.0" encoding="utf-8"?>

<Feature Id="4FAAC725-FF19-4084-8248-F72E1F628E39"

Title="People Data Synchronization"

Description="PeopleSynchronizerJob activation"

Version="1.0.0.0"

Scope="Site"

Hidden="True"

DefaultResourceFile="core"

ReceiverAssembly="’Your assembly Name’, Version=1.0.0.0, Culture=neutral, PublicKeyToken=’Your assembly public key’"

ReceiverClass="’Your assembly Name’.PeopleSynchronization.EventReceiver"

xmlns="http://schemas.microsoft.com/sharepoint/">

<ElementManifests/>

Feature>

2. Create another class that inherits from the Microsoft.SharePoint.SPFeatureReceiver class and implement the FeatureActivated & FeatureDeactivated event handlers like so:

public class EventReceiver : SPFeatureReceiver

{

const string PEOPLE_SYNCHRONIZER_JOB_NAME = "People Synchronizer";

public override void FeatureActivated(SPFeatureReceiverProperties properties)

{

string msg = "";

try

{

SPSite site = properties.Feature.Parent as SPSite;

msg = "People Data Synchronization Feature Activation on site " + site.Url + Environment.NewLine;

if (UnregisterJob(site))

msg += string.Format("The job {0} was successfully unregistered {1}", Consts.PEOPLE_SYNCHRONIZER_JOB_NAME, Environment.NewLine);

PeopleSynchronizerJob job = new PeopleSynchronizerJob(Consts.PEOPLE_SYNCHRONIZER_JOB_NAME, site.WebApplication);

SPMinuteSchedule schedule = new SPMinuteSchedule();

schedule.BeginSecond = 0;

schedule.EndSecond = 59;

schedule.Interval = 30;

job.Schedule = schedule;

job.Update();

msg += string.Format("The job {0} was registered successfully {1}", Consts.PEOPLE_SYNCHRONIZER_JOB_NAME, Environment.NewLine);

Utils.LogMessage(msg + "Activation performed successfully.", EventLogEntryType.Information);

}

catch (Exception e)

{

msg += string.Format("Failed to register {0} job {1}", Consts.PEOPLE_SYNCHRONIZER_JOB_NAME, Environment.NewLine + e.ToString());

Utils.LogMessage(msg, EventLogEntryType.Error);

}

}

public override void FeatureDeactivating(SPFeatureReceiverProperties properties)

{

string msg = "";

try

{

SPSite site = properties.Feature.Parent as SPSite;

msg = "People Data Synchronization Feature Deactivation on site " + site.Url + Environment.NewLine;

if (UnregisterJob(site))

msg += string.Format("The job {0} was successfully unregistered", Consts.PEOPLE_SYNCHRONIZER_JOB_NAME + Environment.NewLine);

Utils.LogMessage(msg, EventLogEntryType.Information);

}

catch (Exception e)

{

msg += string.Format("Failed to unregister {0} job {1}", Consts.PEOPLE_SYNCHRONIZER_JOB_NAME, Environment.NewLine + e.ToString());

Utils.LogMessage(msg, EventLogEntryType.Error);

}

}

public override void FeatureInstalled(SPFeatureReceiverProperties properties)

{

}

public override void FeatureUninstalling(SPFeatureReceiverProperties properties)

{

}

private bool UnregisterJob(SPSite site)

{

SPJobDefinitionCollection jobs = site.WebApplication.JobDefinitions;

foreach (SPJobDefinition job in jobs)

if (job.Name == Consts.PEOPLE_SYNCHRONIZER_JOB_NAME)

{

job.Delete();

return true;

}

return false;

}

}

Deployment process:

In order to deploy this job, all you need to do is:

1. Install the strongly named assembly of the custom job (PeopleSynchronizerJob.dll)on the GAC.

2. Do an iisreset /noforce.

3. Install and activate the feature (through stsadm command)

Once you activate the feature, it should show up on the Timer Job Definitions page in Central Administration / Operations. It will appear in the Timer Job Status page after the first time it is executed.

Debugging SharePoint Timer Job (Visual Studio):

SharePoint Jobs do not have a current context, they are executed by another windows process called “Windows SharePoint Services Timer”. The executable name of this process is “OWSTIMER.EXE”. This is the process that should be attached to the code within Execute() method to be able to debug the job. Yes, I know - it can be tedious to wait for all 30 minutes until the break point color turns into yellow… But the job can be executed job instantly, by creating your small windows application utility that will pass through all jobs in the environment that populates a list of those jobs; pick the custom job from this job to run the same instantly. But then attach this running application to start debugging the job.

private void PopulateJobNames()

{

msgStatus = txtStatus.Text = "";

ddlJobs.Items.Clear();

using (SPSite site = new SPSite(txtURL.Text))

{

SPJobDefinitionCollection jobs = site.WebApplication.JobDefinitions;

SPContentDatabase cntDB = site.ContentDatabase;

foreach (SPJobDefinition job in jobs)

ddlJobs.Items.Add(job.Name);

}

}

private void ExecuteJob()

{

msgStatus = Environment.NewLine + DateTime.Now.ToString();

using (SPSite site = new SPSite(txtURL.Text))

{

SPJobDefinitionCollection jobs = site.WebApplication.JobDefinitions;

SPContentDatabase cntDB = site.ContentDatabase;

foreach (SPJobDefinition job in jobs)

if (job.Name == ddlJobs.Text)

{

job.Execute(cntDB.Id);

msgStatus += string.Format("{0}The Job {1}, was executed.", Environment.NewLine, job.Name);

break;

}

} }

Note:

1. This debug process may not cover all cases.

2. SharePoint Timer job is executed under the account that runs “Windows SharePoint Services Timer”; therefore one should be aware of security issues in production. Refer to the StackOverFlow Post regarding this subject.

Followers