A RESTful, lightweight (Service) interface for Sitecore

So, it seems to me that a Sitecore website is a slightly jealous thing. There, I’ve said it. It only wants access via itself, not direct to the databases, despite that being where the content actually resides. I suppose that is the point of an API, but it does mean that the first thing that a Sitecore project needs is likely to be a set of services to get data into the site.

So what kind of services do we build? Well this is a website so what we need is a service with the lowest barrier to entry possible: how about a RESTful web service based on HTTP and JSON? Ideally this web service should be almost pre-fabricated, allowing the developer to pretty much drop it into any Sitecore website.

For this web service we need a number of things: a Service interface, a Service implementation and some generic classes to hold a representation of Sitecore items and Fields to be consumed by our (.NET) Client code which doesn’t “know” anything about Sitecore. (Clearly, a Client which is not .NET simply receives a JSON representation of a simple class containing fields and can go from there.)

RIO (RESTful I/O for Sitecore)

First we need the generic class to represent the Sitecore Items and Fields (CmsItem and CmsField):

[DataContract]
public class CmsItem
{
    [DataMember]
    public string Name { get; set; }
    [DataMember]
    public string Id { get; set; }
    [DataMember]
    public string Title { get; set; }
    [DataMember]
    public string Path { get; set; }
    [DataMember]
    public string TemplateName { get; set; }
    [DataMember]
    public string TemplateId { get; set; }
    [DataMember]
    public string ParentId { get; set; }

    [DataMember]
    public List<CmsField> Fields = new List<CmsField>();

    public CmsItem()
    {

    }

    public CmsItem(Item item, bool includeStandard)
    {
        Update(item);
        item.Fields.ReadAll();
        foreach (Field field in item.Fields)
        {
            if (!string.IsNullOrEmpty(field.Name))
            {
                if (!field.Name.StartsWith("__") || includeStandard)
                    Fields.Add(new CmsField(field));
            }
        }
    }

    public void Update(Item item)
    {
        Name = item.Name;
        Id = item.ID.ToString();
        Title = item.DisplayName;
        Path = item.Paths.FullPath;
        TemplateName = item.TemplateName;
        TemplateId = item.TemplateID.ToString();
        ParentId = item.ParentID.ToString();
    }

}

[DataContract]
public class CmsField
{
    public CmsField()
    {
    }

    [DataMember]
    public string Name { get; set; }
    [DataMember]
    public string Type { get; set; }
    [DataMember]
    public string Data { get; set; }

    public CmsField(Field field)
    {
        Name = field.Name;
        Type = !string.IsNullOrEmpty(field.Type) ? field.Type : "Unknown Type";
        Data = field.GetValue(true);
    }
}

The classes are marked up as DataContract and DataMember so that the JSON serialization is easy. I decided to have a few key attributes in the class itself, namely Name, Id, Title and so on, of the Item, although much of this is also available as a Field.

With the Fields you can either request Standard Value Fields (i.e. ones that begin with a double underscore) or just the user defined ones. The Fields are accessed via a generic List of type Field, with all properties ultimately being strings.

Next we need the interface definition. This (in this implementation) is a minimum, a couple of methods to get items, an add Item and an update Item method:

[ServiceContract]
public interface IRio
{
    [OperationContract]
    [WebInvoke(Method = "GET", ResponseFormat = WebMessageFormat.Json,
    RequestFormat = WebMessageFormat.Json)]
    List<CmsItem> GetChildItemsByPath(string url, bool allFields);

    [WebInvoke(Method = "POST", ResponseFormat = WebMessageFormat.Json,
    RequestFormat = WebMessageFormat.Json)]
    string AddItem(CmsItem itemData);

    [WebInvoke(Method = "GET", ResponseFormat = WebMessageFormat.Json,
    RequestFormat = WebMessageFormat.Json)]
    CmsItem GetItemById(string id, bool allFields);

    [WebInvoke(Method = "GET", ResponseFormat = WebMessageFormat.Json,
    RequestFormat = WebMessageFormat.Json)]
    string GetItemIdByPath(string path);

    [WebInvoke(Method = "POST", ResponseFormat = WebMessageFormat.Json,
    RequestFormat = WebMessageFormat.Json)]
    string UpdateItem(CmsItem cmsItem);

    [WebInvoke(Method = "GET", ResponseFormat = WebMessageFormat.Json,
    RequestFormat = WebMessageFormat.Json)]
    string GetFieldByItemId(string id, string fieldName);
}

The wonderful thing about using the ResponseFormat and RequestFormat attributes is that all of the serialization is done for you, transparently (well, you have to deserialize the data in the Client, but, hey fair enough).

Finally, we need an implementation if the interface. This, actually, could be the same from one instance to the next, and only really needs to be part of the website so that so that the service is part of the site (the interface and CmsItem can reside in a class library and be added as a reference to the project). Assuming a website called Website (original eh,) the following implementation works:

using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using Rio;
using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;

namespace Website
{
    [AspNetCompatibilityRequirements(RequirementsMode =
     AspNetCompatibilityRequirementsMode.Allowed)]
    public class Rio : IRio
    {
        private readonly Database _database =
        Sitecore.Configuration.Factory.GetDatabase("master");

        public List<CmsItem> GetChildItemsByPath(string url, bool allFields)
        {
            var childlist = new List<CmsItem>();
            var item = _database.GetItem(url);
            if (item != null)
            {
                foreach (Item childItem in item.GetChildren())
                {
                    var cmsItem = new CmsItem(childItem, allFields);
                    childlist.Add(cmsItem);
                }

                if (childlist.Count > 0)
                    return childlist;
            }
            return null;
        }

        public string AddItem(CmsItem itemData)
        {
            if (itemData != null)
            {
                Item item = null;
                if (IsInPermittedHostList())
                {
                    Item parentItem = _database.GetItem(new ID(itemData.ParentId));
                    TemplateItem template = _database.GetTemplate(itemData.TemplateName);

                    if (parentItem != null && template != null)
                    {
                        using (new Sitecore.SecurityModel.SecurityDisabler())
                        {
                            parentItem.Editing.BeginEdit();
                            try
                            {
                                item = parentItem.Add(itemData.Name, template);
                            }
                            finally
                            {
                                parentItem.Editing.EndEdit();
                            }
                            UpdateFields(ref item, itemData);
                        }
                    }
                }
                return item != null ? item.ID.ToString() : Guid.Empty.ToString();
            }
            return Guid.Empty.ToString();
        }

        public CmsItem GetItemById(string id, bool allFields)
        {
            var item = _database.GetItem(new ID(id));
            if (item != null)
            {
                var cmsItem = new CmsItem(item, allFields);
                return cmsItem;
            }

            return null;
        }

        public string GetItemIdByPath(string path)
        {
            var item = _database.GetItem(path);
            return item != null ? item.ID.ToString() : Guid.Empty.ToString();
        }

        public string UpdateItem(CmsItem cmsItem)
        {

            var item = _database.GetItem(new ID(cmsItem.Id));
            if (item != null)
            {
                UpdateFields(ref item, cmsItem);
            }

            return item != null ? item.ID.ToString() : Guid.Empty.ToString();
        }

        public string GetFieldByItemId(string id, string fieldName)
        {
            Item item = null;
            if (IsInPermittedHostList())
            {
                item = _database.GetItem(new ID(id));
            }
            if (item != null)
            {
                var fieldData = item[fieldName];
                return fieldData;
            }
            return "";
        }

        private static bool IsInPermittedHostList()
        {
            bool statusCode = false;

            if (System.Web.Configuration.WebConfigurationManager.AppSettings.Count > 0)
            {
                string hosts = System.Web.Configuration.WebConfigurationManager
                .AppSettings.Get("RioPermittedHosts");
                var hostlist = hosts.Split(new[] { ';' });
                if (hostlist.Length > 0)
                {
                    statusCode =
                    hostlist.Any(host =>
                    OperationContext.Current.IncomingMessageProperties.Via.Host == host);
                }
            }
            if (statusCode == false)
            {
                WebOperationContext ctx = WebOperationContext.Current;
                if (ctx != null)
                    ctx.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.NotFound;
            }
            return statusCode;
        }

        private static void UpdateFields(ref Item item, CmsItem cmsItem)
        {
            foreach (var field in cmsItem.Fields)
            {
                AddFieldData(ref item, field);
            }
        }

        private static void AddFieldData(ref Item item, CmsField field)
        {
            if (item.Fields[field.Name] != null)
            {
                item.Editing.BeginEdit();

                if (item.Fields[field.Name].Type != null
                    && item.Fields[field.Name].Type == "General Link")
                {
                    LinkField linkfield = item.Fields[field.Name];
                    linkfield.Url = field.Data;
                }
                else
                {
                    item.Fields[field.Name].Value = field.Data;
                }

                item.Editing.EndEdit();
            }
        }
    }
}

The CmsItem class and the interface definition are available via the Rio class library, and all that is left is the .svc file to define the service:

<%@ ServiceHost Language="C#" Debug="true" Service="Website.Rio" CodeBehind="Rio.svc.cs"
Factory="System.ServiceModel.Activation.WebServiceHostFactory" %>

This, then will expose the RESTful JSON interface on a URL like so:

http://localhost:7638/Rio.svc/GetItemById?id=110D559F-DEA5-42EA-9C1C-8A5DF7E70EF9&allFields=true

And, as this solution has a minimal footprint, no endpoint needs to be defined (and clutter up) the web.config.

This solution can be used in development to load data into a website, or even in production if the IsInPermittedHostList() function is called on every method to secure the service.