tirsdag den 12. november 2013

A PublishQueue for publishing several different items in the background

From time to time, we all happen to have to create some import task in Sitecore, that imports alot of content.

The problem with this content is often, that you have to publish it once it has been imported, which can take quite some time of you are importing several hundred items (and maybe deleting some old items too).

That is why we have created a PublishQueue (not to be mistaken for the normal publish queue in Sitecore), that you can create, and fill with items - and then finally publish in one go.

Enough talking - here we go :-)

This solution consists of three classes: the PublishQueue class, which implements the queue, the QueueProcessor, which does the publishinh, and finally PublishingHelper, which has two helper functions the developer calls once he is ready to publish.

First, we have the PublishQueue class:

using System;
using System.Collections.Generic;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Publishing;

public sealed class PublishQueue
{
    private readonly List<PublishOptions> publishQueue = new List<PublishOptions>();
    private readonly Database targetDatabase;

    public PublishQueue(Database targetDatabase)
    {
        this.targetDatabase = targetDatabase;
    }

    internal int Count
    {
        get
        {
            return publishQueue.Count;
        }
    }

    internal PublishOptions[] QueueContent
    {
        get
        {
            return publishQueue.ToArray();
        }
    }

    public void Clear()
    {
        publishQueue.Clear();
    }

    public void Enqueue(Item item, bool includeChildren, bool allLanguages, PublishMode publishMode)
    {
        if (item == null)
        {
            throw new ArgumentNullException("item", "item must not be null");
        }

        switch (publishMode)
        {
            case PublishMode.Full:
            case PublishMode.Smart:
                break;
            case PublishMode.SingleItem:
            case PublishMode.Unknown:
            case PublishMode.Incremental:
                throw new ArgumentException("The passed PublishMode is invalid", "publishMode");
            default:
                throw new ArgumentOutOfRangeException("publishMode");
        }

        PublishOptions options = new PublishOptions(item.Database, targetDatabase, publishMode, item.Language, DateTime.Now)
        {
            Deep = includeChildren,
            RepublishAll = allLanguages,
            RootItem = item
        };

        publishQueue.Add(options);
    }
}

The next class we need, is the QueueProcessor class:
using System;

using Sitecore.Data;
using Sitecore.Globalization;
using Sitecore.Publishing;

private class QueueProcessor
{
    private readonly PublishQueue queue;

    public QueueProcessor(PublishQueue queue)
    {
        if (queue == null)
        {
            throw new ArgumentNullException("queue", "queue must not be null");
        }

        this.queue = queue;
    }

    public void ProcessQueue()
    {
        foreach (PublishOptions options in queue.QueueContent)
        {
            Language[] languages = options.RepublishAll ? options.RootItem.Languages : new[] { options.RootItem.Language };
            Database[] databases = new[] { options.TargetDatabase };

            PublishManager.PublishItem(options.RootItem, databases, languages, options.Deep, options.Mode == PublishMode.Smart);
        }
    }
}

Finally we need these the helper class, called PublishingHelper:
using System;

using Sitecore;
using Sitecore.Jobs;

public static class PublishingHelper
{
    public static void ProcessPublishQueue(PublishQueue queue)
    {
        QueueProcessor processor = new QueueProcessor(queue);

        processor.ProcessQueue();
    }

    public static Job ProcessPublishQueueAsync(PublishQueue queue)
    {
        string jobName = GetJobName(queue);

        JobOptions jobOptions = new JobOptions(jobName, "publish", "publisher", new QueueProcessor(queue), "ProcessQueue")
        {
            ContextUser = Context.User,
            AfterLife = TimeSpan.FromMinutes(1.0),
            AtomicExecution = true
        };

        if (Context.Job != null)
        {
            jobOptions.ClientLanguage = Context.Job.Options.ClientLanguage;
        }

        return JobManager.Start(jobOptions);
    }

    private static string GetJobName(PublishQueue queue)
    {
        return string.Format("Publish queue containing {0} items.", queue.Count);
    }
}

Then we are done - the only thing needed is to use it - here is an example of how it can be used:
public static void ImportItems(IEnumerable<XElement> dataFromExternalSystem)
{
    PublishQueue queue = new PublishQueue(Factory.GetDatabase("web"));

    IEnumerable<Item> oldItems = GetOldItemsToBeRemoved();

    using (new SecurityDisabler())
    {
        foreach (Item oldItem in oldItems)
        {
            oldItem.Delete();
            queue.Enqueue(oldItem, false, true, PublishMode.Full);
        }
    }

    foreach (XElement element in dataFromExternalSystem)
    {
        Item item = CreateItemFromData(element);
        queue.Enqueue(item, false, true, PublishMode.Full);
    }

    PublishingHelper.ProcessPublishQueueAsync(queue);
}

First we create our queue, defining that the items in it, should be published to the web database.

Then we gets all the old items that we want to remove, and both remove them and add them to the queue, so they get removed from the web database too.
And then we create all the new items, which also gets put into the queue.

Finally we starts an async publish of the entire queue in the background - we don't save the return value, since we don't care about when it gets done, it should just run in the background untill it is done.

Thats really all there is to it - we have been using this for a while now, and it really both simplifies and increase the stability of our import tasks, so thats a win-win :-)

Ingen kommentarer:

Send en kommentar