Extending Sitecore.Services.Client results

Last year, David Peterson posted an article about Extending the Sitecore Services Client. Though that is the most elegant solution, Sitecore (-Services Client) doesn’t come with dependency injection out of the box. I tried to go the other way around, and modified the results by adding actionfilters. I’d rather see Sitecore modifying their code to easily override the modelfactory used by the default HandlerProvider somehow, but this solution will work for now.

First, we will create a config include file that adds a step to the initialize pipeline:



  
    
      
        
      
    
  

Now, let’s create this very simple class:

using System.Web.Http;
using Sitecore.Pipelines;

namespace BoC.Sitecore.ApiExtensions.Pipelines
{
    public class RegisterSitecoreClientServicesEnrichments
    {
        public void Process(PipelineArgs args)
        {
            GlobalConfiguration.Configuration.Filters.Add(new ItemModelActionFilter());
        }
    }
}

So, all this does is register a new ActionFilter to the list of global WebAPI filters.

Now, for the real work, we add the actionfilter class:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http.Filters;
using Sitecore;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Data.Managers;
using Sitecore.Links;
using Sitecore.Pipelines;
using Sitecore.Pipelines.RenderField;
using Sitecore.Services.Core.Model;
using ActionFilterAttribute = System.Web.Http.Filters.ActionFilterAttribute;

namespace BoC.Sitecore.ApiExtensions
{
    public class ItemModelActionFilter : ActionFilterAttribute
    {
        public override Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)
        {
            var objectContent = actionExecutedContext.Response.Content as ObjectContent;
            if (objectContent?.Value == null)
                return base.OnActionExecutedAsync(actionExecutedContext, cancellationToken);
            var currentLanguage = Context.Site.Language;
            if (typeof(ItemModel).IsAssignableFrom(objectContent.ObjectType))
            {
                EnrichItemModel(objectContent.Value as ItemModel, currentLanguage, global::Sitecore.Context.PageMode.IsExperienceEditor);
            }
            else if ((typeof(IEnumerable<>).IsAssignableFrom(objectContent.ObjectType) && typeof(ItemModel).IsAssignableFrom(objectContent.ObjectType.GetGenericArguments()[0]))
                     || (objectContent.ObjectType.IsArray && typeof(ItemModel).IsAssignableFrom(objectContent.ObjectType.GetElementType())))
            {
                foreach (ItemModel model in (IEnumerable) objectContent.Value)
                {
                    EnrichItemModel(model, currentLanguage, global::Sitecore.Context.PageMode.IsExperienceEditor);
                }
            }
            return base.OnActionExecutedAsync(actionExecutedContext, cancellationToken);
        }

        public static void EnrichItemModel(ItemModel model, string defaultLanguage, bool includeEditMode = false)
        {
            if (model == null)
                return;
            if (model.ContainsKey("TemplateID") && !model.ContainsKey("TemplateIDs"))
            {
                var templateId = new ID((Guid) model["TemplateID"]);
                var template = TemplateManager.GetTemplate(templateId, global::Sitecore.Context.Database);
                if (template != null)
                    model.Add("TemplateIDs", template.GetBaseTemplates().Select(t => t.ID.Guid).Concat(new[] {templateId.Guid}));
            }

            var scItem = global::Sitecore.Context.Database.GetItem(new ID((Guid)model["ItemID"]));
            if (scItem == null)
                return;
            if (!model.ContainsKey("ParentIds"))
            {
                model.Add("ParentIds", scItem?.Paths?.LongID?.Split(new[] {'/'}, StringSplitOptions.RemoveEmptyEntries).Select(id => new Guid(id)));
            }
            if (!model.ContainsKey("ItemUrlExpanded"))
            {
                var urlOptions = LinkManager.GetDefaultUrlOptions();
                urlOptions.AlwaysIncludeServerUrl = false;
                urlOptions.SiteResolving = true;
                urlOptions.LanguageEmbedding = scItem.Language.Name == defaultLanguage ? LanguageEmbedding.Never : LanguageEmbedding.Always;
                urlOptions.LowercaseUrls = true;
                model.Add("ItemUrlExpanded", LinkManager.GetItemUrl(scItem, urlOptions));
            }

            if (includeEditMode)
            {
                var editor = new Dictionary();
                foreach (var field in model.Keys)
                {
                    var editfield = EditField(field, scItem);
                    if (!string.IsNullOrEmpty(editfield))
                        editor.Add(field, editfield);
                }
                model.Add("FieldEditors", editor);
            }
        }

        public static string EditField(string fieldName, Item item)
        {
            using (new ContextItemSwitcher(item))
            {
                RenderFieldArgs renderFieldArgs = new RenderFieldArgs();
                renderFieldArgs.Item = item;
                renderFieldArgs.FieldName = fieldName;
                renderFieldArgs.DisableWebEdit = false;

                CorePipeline.Run("renderField", (PipelineArgs)renderFieldArgs);

                return renderFieldArgs.Result.ToString();

            }
        }

    }
}

So, when any WebApi controller action is executed, this class’ OnActionExecutedAsync method will get invoked. This method will check if the controller action has returned an instance of ItemModel or an IEnumerable of ItemModel.
If so, it calls EnrichItemModel. As you can see, EnrichItemModel extends the returned ItemModels with extra fields. In this case an array of all TemplateIds, an array of all ParentIds and a full url to the item. If the user is in Experience Editor mode, all field-editors are returned as HTML as well.

That is all there is to it! The code is part of a project hosted at GitHub. A project that is a nice R&D project I will blog about in the near future.