Using Sitecore Item Web API to rename items

The Issue

Sitecore's Item Web API provides a limited set of operations to manipulate content. Aside from the basic CRUD operations on one or more items, there's not much more you can do out of the box. In particular, there's no simple way to rename an item by just calling an API resource.

Luckily, as is the case with most Sitecore functionality, the Item Web API performs its functions by running pipelines, for which we can write additional processors.

An Approach

If we open the Item Web API's configuration file, we find that there are four basic pipelines we may customize:

  • itemWebApiRead, which reads data from the database for the items in scope;
  • itemWebApiCreate, for creating a new item and optionally setting field values;
  • itemWebApiUpdate, to update existing items' fields; and
  • itemWebApiDelete, which deletes items from the database.

Of these pipelines, it seems that a valid approach to solve our problem would be to extend the itemWebApiUpdate pipeline in some way, perhaps by adding a processor such that it reads an extra querystring parameter to rename the items in scope. However, there is a better way, such that we keep our renaming logic separate from the API's functionality.

Of the other pipelines in the configuration, there is one that suits our needs perfectly: the itemWebApiRequest pipeline, and the ResolveAction pipeline processor in particular. The itemWebApiRequest pipeline acts like an orchestrator for the lifecycle of an Item Web API request, starting the other four core pipelines as needed. By replacing the stock ResolveAction pipeline processor with one of our own, we can specify what happens when a specific HTTP method is received, and (more importantly) handle other methods than the four default ones (GET, PUT, POST, DELETE).

A PATCH pipeline

PATCH, as an HTTP method, is defined in RFC5789 as a "feature to do partial modification". Since we only wish to modify the item's name and nothing more, it seems that our intentions fall within PATCH's area of application. (On a side note, perhaps the itemWebApiUpdate pipeline should be responding to the PATCH verb as well, instead of PUT, since it allows for partial modification too).

So let's get on with writing a replacement for the ResolveAction pipeline processor, that dispatches to a custom pipeline when it receives the PATCH verb.

public class ResolveAction : Sitecore.ItemWebApi.Pipelines.Request.ResolveAction
{
    public override void Process(RequestArgs requestArgs)
    {
        Assert.ArgumentNotNull(requestArgs, "requestArgs");
        string method = GetMethod(requestArgs.Context);
        AssertOperation(requestArgs, method);
        
        switch (method)
        {
            case 'post':
                ExecuteCreateRequest(requestArgs);
                return;
            case 'get':
                ExecuteReadRequest(requestArgs);
                return;
            case 'put':
                ExecuteUpdateRequest(requestArgs);
                return;
            case 'delete':
                ExecuteDeleteRequest(requestArgs);
                return;
            case 'patch':
                ExecutePatchRequest(requestArgs);
                return;
        }
    }

    private static void AssertOperation(RequestArgs requestArgs, string method)
    {
        Assert.ArgumentNotNull(requestArgs, "requestArgs");
        if (requestArgs.Context.Settings.Access == AccessType.ReadOnly && method != "get")
        {
            throw new AccessDeniedException("The operation is not allowed.");
        }
    }

    private string GetMethod(Sitecore.ItemWebApi.Context context)
    {
        Assert.ArgumentNotNull(context, "context");
        return context.HttpContext.Request.HttpMethod.ToLower();
    }

    /* More code follows */
}

We're missing the ExecutePatchRequest method(). Before we move on to show the implementation, let's patch this processor into the Item Web API pipeline system using a configuration file:

<pipelines>
  <itemwebapirequest>
    <processor patch:instead="*[@type='Sitecore.ItemWebApi.Pipelines.Request.ResolveAction, Sitecore.ItemWebApi']" type="LD.Pipelines.ItemWebApi.ResolveAction, LD.Extensions"></processor>
  </itemwebapirequest>

  <itemwebapipatch>
    <processor type="LD.Pipelines.ItemWebApi.RenameScope, LD.Extensions"></processor>
  </itemwebapipatch>
</pipelines>

As shown in the configuration file, we have also created a custom pipeline that runs whenever a PATCH request arrives at the API. To implement the custom pipeline, we need to create the basic infrastructure, i.e. a class for the pipeline's arguments, as well as an abstract base class for all pipeline processors in this pipeline (not technically required, but always a good idea). The arguments class must derive from the OperationArgs class, in order for us to have access to the parsed scope:

// Pipeline arguments
public class PatchArgs : OperationArgs
{
    public PatchArgs(Item[] scope)
        : base(scope)
    {
    }
}

// Pipeline processor base class
public abstract class PatchProcessor : OperationProcessor<PatchArgs>
{
    protected PatchProcessor()
    {
    }
}

Returning to our ResolveAction implementation, we can now fill in the missing ExecutePatchRequest() method, which simply starts the itemWebApiPatch pipeline:

protected virtual void ExecutePatchRequest(RequestArgs requestArgs)
{
    Assert.ArgumentNotNull(requestArgs, "requestArgs");
    PatchArgs patchArgs = new PatchArgs(requestArgs.Scope);
    CorePipeline.Run("itemWebApiPatch", patchArgs);
    requestArgs.Result = patchArgs.Result;
}

A PATCH pipeline processor

We have now reached the final piece of the puzzle: actually populating the itemWebApiPatch pipeline with a pipeline processor that takes care of renaming the items in scope. Let's look at the code:

public class RenameScope : PatchProcessor
{
    public override void Process(PatchArgs arguments)
    {
        Assert.ArgumentNotNull(arguments, "arguments");
        Assert.IsNotNull(arguments.Scope, "The scope is null");
        RenameItems(arguments.Scope);
        arguments.Result = GetResult(arguments.Scope);
    }

    private void RenameItems(IEnumerable<Item> items)
    {
        Assert.IsNotNullOrEmpty(WebUtil.GetQueryString("ld_name"), "no new name specified");
        foreach (var item in items)
        {
            var name = ItemUtil.GetUniqueName(item.Parent, WebUtil.GetQueryString("ld_name"));
            if (Sitecore.Context.Site.Name != "shell" && item.Access.CanRename())
            {
                item.Editing.BeginEdit();
                item.Name = name;
                item.Editing.EndEdit();
            }
        }
    }

    private static dynamic GetResult(IEnumerable<Item> scope)
    {
        Assert.ArgumentNotNull(scope, "scope");
        Dynamic result = new Dynamic();
        result["statusCode"] = 200;
        result["result"] = GetInnerResult(scope);
        return result;
    }

    private static dynamic GetInnerResult(IEnumerable<Item> scope)
    {
        Assert.ArgumentNotNull(scope, "scope");
        Dynamic innerResult = new Dynamic();
        innerResult["count"] = scope.Count();
        innerResult["itemIds"] = scope.Select(item => item.ID.ToString());
        return innerResult;
    }
}

Let's look at the basic steps the Process() method takes:

  1. It asserts that the pipeline has been called with non-null arguments;
  2. it asserts that a valid scope has been provided;
  3. it performs the renaming for each item in scope;
  4. it populates the result.

All items in scope are renamed as specified by the ?ld_name querystring parameter. It is first verified that the user has permission to write the item. Finally, the GetResult() and GetInnerResult() methods are used to populate the response.

That's all. We now have a working Item Web API resource which can respond to calls such as:

PATCH /-/item/v1/sitecore/content/Home/old-item-name&ld_name=new-name

As always, have fun!