Processing items with Work Item Timer Jobs in SharePoint 2010


Once in a while you find yourself in a situation where you have to process some items in SharePoint. Using Timer Jobs in only a half of the answer. Find out how to process items using Work Item Timer Jobs in SharePoint 2010.

Processing items in SharePoint 2010

One of the things we have probably all done at least once during our SharePoint developer career was to create a solution for processing a number of items, such as ratings, subscriptions, some kind of requests, etc. We have all learned that such processes can take quite a while to complete and it is therefore a good practice to use Timer Jobs for implementing the logic. Because Timer Jobs are being executed outside the w3wp.exe process, they are not only less prone to failures due to process termination but also allow us to move the load away from the Application Pool serving our solution.

When dealing with items you have to have some kind of queue which describes what should be processed. Most frequently such queue contains some input information for the task (rating, username, etc.) but it may also contain ID’s of sites and items to which the task refers.

While implementing queues for Timer Jobs many people choose Lists for storage. And although there is nothing wrong with this approach, it requires some additional work in creating and maintaining the queue List’s schema and all the plumbing for adding and cleaning queued items not to mention supporting upgrades should anything change in the future! There is however an easier way to work with item queues in SharePoint 2010.

Presenting Work Item Timer Jobs

Work Item Timer Jobs (SPWorkItemJobDefinition) are specialized types of Timer Jobs designed for dealing with item queues. And although they have been around for quite some time (available as a part of Windows SharePoint Services v3) they haven’t been that well documented yet and there are not many samples to find of how they can be used.

Creating Work Items

At the base of every Work Item Timer Job is the Work Item: a unit of work that is picked up and processed by the Job when it executes. A work item can be added using the SPSite.AddWorkItem method, eg.:

Guid siteId = SPContext.Current.Site.ID;
Guid webId = SPContext.Current.Web.ID;
Guid listId = SPContext.Current.ListId;
int itemId = SPContext.Current.ItemId;
Guid itemUniqueId = SPContext.Current.ListItem.UniqueId;
int currentUserId = SPContext.Current.Web.CurrentUser.ID;
SPSecurity.RunWithElevatedPrivileges(() => {
    using (SPSite site = new SPSite(siteId)) {
        site.AddWorkItem(Guid.NewGuid(),
            DateTime.Now.ToUniversalTime(),
            MyJobDefinition.WorkItemTypeId,
            webId,
            listId,
            itemId,
            true,
            itemUniqueId,
            itemUniqueId,
            currentUserId,
            null,
            "Hello World",
            Guid.Empty);
    }
});

Although the number of parameters required by the AddWorkItem method might seem scary it is all way easier than it looks.

First there is the gWorkItemId parameter which uniquely identifies the work item.

The second parameter (schdDateTime) determines when the item should be processed. One important thing to notice here is that the time should be stored in the Universal Time format. Without this you might find yourself creating work items in the past what would prevent the Timer Job from processing them.

The next parameter (gWorkItemType) contains the ID of the work item type. This value is very important as it’s used by the Work Item Timer Job to pick up its work items. The identifier set while creating a Work Item should match the value returned by the SPWorkItemJobDefinition.WorkItemType method.

Next there are a few parameters that can be used for retrieving the List Item to which the Work Item refers to. For example when working with ratings you would have the item that has been rated (List Item) and the rating Work Item which contains the rating value. In order to update the average rating on the List Item you would need a reference to the Site and List where the particular item is stored but also the ID of the item itself so that you can retrieve and update it.

Then there is the nUserId parameter which contains the ID of the user who requested the Work Item. This can be useful for tracking purposes.

Next, there are two parameters which can be used for storing input values for the Timer Job and which replace the need for a whole separate List. Those parameters are rgbBinaryPayload and strTextPayload and can be used to store respectively binary and text payloads. When working with ratings you could for example store the rating value (eg. 4) as the text payload. In other scenarios you could choose to store more complex objects: either serialized as JSON/XML as the text payload or as a binary payload. Which one you use depends mostly on the type of Timer Jobs that you are building.

Important: there are two things that you should keep in mind when designing your solution based on Work Items. First of all the assembly that contains the code which adds a new Work Item must have Full Trust. Secondly the user who adds the Work Item must be a Site Collection Administrator. In most scenarios you will therefore be adding new Work Items after elevating privileges.

Calling the SPSite.AddWorkItem method results in adding a record to the ScheduledWorkItems table in the Content Database of your Site Collection.

Work Item created in the Content Database

Those Work Items records are cleaned up once the item has been processed by the Timer Job.

Now we have our Work Item created let’s create a job that will process our Work Items.

Creating a Work Item Timer Job

As mentioned before, Work Item Timer Job are specialized types of Timer Job. From the deployment perspective they look the same as regular Timer Jobs, so they also need a Feature to be deployed and a schedule to execute. The big difference is in the execution process. Where you would implement the Execute method in a regular Timer Job, you use the ProcessWorkItem method to execute your logic.

The following code snippet presents a sample Work Item Timer Job.

public class MyJobDefinition : SPWorkItemJobDefinition {
    public static readonly string WorkItemJobDisplayName = "My Work Item Job";
    public static readonly Guid WorkItemTypeId = new Guid("{CEAAFFA4-4391-40D6-868E-19EDEDB78DD4}");

    public override Guid WorkItemType() {
        return WorkItemTypeId;
    }

    public override int BatchFetchLimit {
        get {
            return 50;
        }
    }

    public override string DisplayName {
        get {
            return WorkItemJobDisplayName;
        }
    }

    public MyJobDefinition() {

    }

    public MyJobDefinition(string name, SPWebApplication webApplication)
        : base(name, webApplication) {

    }

    protected override bool ProcessWorkItem(SPContentDatabase contentDatabase, SPWorkItemCollection workItems, SPWorkItem workItem, SPJobState jobState) {
        if (workItem == null || String.IsNullOrEmpty(workItem.TextPayload)) {
            throw new ArgumentNullException("workItem");
        }

        try {
            using (SPSite site = new SPSite(workItem.SiteId)) {
                using (SPWeb web = site.OpenWeb(workItem.WebId)) {

                    try {
                        SPList list = web.Lists[workItem.ParentId];
                        SPListItem listItem = list.GetItemByUniqueId(workItem.ItemGuid);
                        string message = workItem.TextPayload;
                        listItem["Message"] = message;
                        listItem.SystemUpdate(false);
                    }
                    catch (Exception ex) {
                        // exception handling
                    }
                    finally {
                        // SubCollection(): required to set the ParentWeb property on the WorkItemsCollection
                        // Delete(): required to remove the item from the queue
                        workItems.SubCollection(site, web, 0, (uint)workItems.Count).DeleteWorkItem(workItem.Id);
                    }
                }
            }
        }
        catch (Exception ex) {
            // exception handling
        }

        return true;
    }
}

We begin with defining the name for our Timer Job and the Work Item Type. Next, using the BatchFetchLimit property we specify how many items our Timer Job should process in a single run. Depending on the logic of your job this can help you keep your environment from overloading and the job running for too long. Finally, in the ProcessWorkItem method, we implement our logic for processing Work Items. In this example we retrieve the associated List Item and we update the value of its Message field with the message stored as the Text Payload of our Work Item.

As you can see this is all very straightforward and allows you to focus on the real job instead of the plumbing.

Important: One very important thing that you should implement correctly and test thoroughly is cleaning up Work Items after they have been processed. Without this they would be processed every time the Timer Job executes over and over again.

In the code sample above you can see how a processed Work Item is being removed in line 52. Although the ProcessWorkItem method receives the collection of Work Items you cannot just remove the Work Item from it. For the Work Item to be removed the collection has to have a reference to the Parent Web and therefore you have to call the SubCollection method first before calling the DeleteWorkItem method.

Summary

Work Item Timer Jobs are specialized type of Timer Jobs in the SharePoint platform, that have been designed to work with queued items. The mechanics behind the Work Item Timer Jobs contain all the plumbing required for managing queues allowing you to focus on processing items. Work Item Timer Jobs are a great alternative to using custom Lists as they require less maintenance and are a part of the SharePoint platform.

Others found also helpful: