Home » Pipeline

Category Archives: Pipeline

Advertisements

Service Locate or Create Objects Defined in a Fully Qualified Type Name Field in Sitecore

<TL;DR>

This is — without a doubt — the longest blog post I have ever written — and hopefully to ever write as it nearly destroyed me 😉 — so will distill the main points in this TL;DR synopsis.

Most bits in Sitecore Experience Forms use objects/service class instances sourced from the Sitecore IoC container but not all. Things not sourced from the Sitecore IoC container are defined on Items in the following folders:

.

Why?

¯\_(ツ)_/¯

This is most likely due to their fully qualified type names being defined in a type field on Items contained in these folders, and sourcing these from the Sitecore IoC is not a thing OOTB in Sitecore as far as I am aware (reflection is used to create them):

Moreover, this is the same paradigm found in Web Forms for Marketers (WFFM) for some of its parts (Save Actions are an example).

Well, this paradigm bothers me a lot — I strongly feel that virtually everything should be sourced from the Sitecore IoC container as it promotes SOLID principles, a discussion I will leave for another time — so went ahead and built a system of Sitecore pipelines and service classes to:

  1. Get the fully qualified type name string out of a field of an Item.
  2. Resolve the Type from the string from #1.
  3. Try to find the Type in the Sitecore IoC container using Service Locator (before whinging about using Service Locator for this, keep in mind that it would be impossible to inject everything from the IoC container into a class instance’s constructor in order to find it). If found, return to the caller. Otherwise, proceed to #4.
  4. Create an instance of the Type using Reflection. Return the result to the caller.

Most of the code in the solution that follows are classes which serve as custom pipeline processors for 5 custom pipelines. Pipelines in Sitecore — each being an embodiment of the chain-of-responsibility pattern — are extremely flexible and extendable, hence the reason for going with this approach.

I plan on putting this solution up on GitHub in coming days (or weeks depending on timing) so it is more easily digestible than in a blost post. For now, Just have a scan of the code below.

Note: This solution is just a Proof of concept (PoC). I have not rigorously tested this solution; have no idea what its performance is nor the performance impact it may have; and definitely will not be held responsible if something goes wrong if you decided to use this code in any of your solutions. Use at your own risk!

</TL;DR>

Now that we have that out of the way, let’s jump right into it.

I first created the following abstract class to serve as the base for all pipeline processors in this solution:

using Sitecore.Pipelines;

namespace Sandbox.Foundation.ObjectResolution.Pipelines
{
	public abstract class ResolveProcessor<TPipelineArgs> where TPipelineArgs : PipelineArgs
	{
		public void Process(TPipelineArgs args)
		{
			if (!CanProcess(args))
			{
				return;
			}

			Execute(args);
		}

		protected virtual bool CanProcess(TPipelineArgs args) => args != null;

		protected virtual void AbortPipeline(TPipelineArgs args) => args?.AbortPipeline();

		protected virtual void Execute(TPipelineArgs args)
		{
		}
	}
}

The Execute() method on all pipeline processors will only run when the processor’s CanProcess() method returns true. Also, pipeline processors have the ability to abort the pipeline where they are called.

I then created the following abstract class for all service classes which call a pipeline to “resolve” a particular thing:

using Sitecore.Abstractions;
using Sitecore.Pipelines;

namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers
{
	public abstract class PipelineObjectResolver<TArguments, TPipelineArguemnts, TResult> where TPipelineArguemnts : PipelineArgs
	{
		private readonly BaseCorePipelineManager _corePipelineManager;

		protected PipelineObjectResolver(BaseCorePipelineManager corePipelineManager)
		{
			_corePipelineManager = corePipelineManager;
		}

		public TResult Resolve(TArguments arguments)
		{
			TPipelineArguemnts args = CreatePipelineArgs(arguments);
			RunPipeline(GetPipelineName(), args);
			return GetObject(args);
		}

		protected abstract TResult GetObject(TPipelineArguemnts args);

		protected abstract TPipelineArguemnts CreatePipelineArgs(TArguments arguments);

		protected abstract string GetPipelineName();

		protected virtual void RunPipeline(string pipelineName, PipelineArgs args) => _corePipelineManager.Run(pipelineName, args);
	}
}

Each service class will “resolve” a particular thing with arguments passed to their Resolve() method — these service class’ Resolve() method will take in a TArguments type which serves as the input arguments for it. They will then delegate to a pipeline via the RunPipeline() method to do the resolving. Each will also parse the results returned by the pipeline via the GetObject() method.

Moving forward in this post, I will group each resolving pipeline with their service classes under a <pipeline name /> section.

<resolveItem />

I then moved on to creating a custom pipeline to “resolve” a Sitecore Item. The following class serves as its arguments data transfer object (DTO):

using System.Collections.Generic;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;
using Sitecore.Pipelines;

using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ItemResolvers;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem
{
	public class ResolveItemArgs : PipelineArgs
	{
		public Database Database { get; set; }

		public string ItemPath { get; set; }

		public Language Language { get; set; }

		public IList<IItemResolver> ItemResolvers { get; set; } = new List<IItemResolver>();

		public Item Item { get; set; }
	}
}

The resolution of an Item will be done by a collection of IItemResolver instances — these are defined further down in this post — which ultimately do the resolution of the Item.

Next, I created the following arguments class for IItemResolver instances:

using Sitecore.Data;
using Sitecore.Globalization;

namespace Sandbox.Foundation.ObjectResolution.Models.Resolvers.ItemResolvers
{
	public class ItemResolverArguments
	{
		public Database Database { get; set; }

		public Language Language { get; set; }

		public string ItemPath { get; set; }
	}
}

Since I hate calling the “new” keyword directly on classes, I created the following factory interface which will construct the argument objects for both the pipeline and service classes for resolving an Item:

using Sitecore.Data;
using Sitecore.Globalization;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ItemResolvers;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType;

namespace Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ItemResolvers
{
	public interface IItemResolverArgumentsFactory
	{
		ItemResolverArguments CreateItemResolverArguments(ResolveTypeArgs args);

		ItemResolverArguments CreateItemResolverArguments(ResolveItemArgs args);

		ItemResolverArguments CreateItemResolverArguments(Database database = null, Language language = null, string itemPath = null);

		ResolveItemArgs CreateResolveItemArgs(ItemResolverArguments arguments);

		ResolveItemArgs CreateResolveItemArgs(Database database = null, Language language = null, string itemPath = null);
	}
}

Here is the class that implements the interface above:

using Sitecore.Data;
using Sitecore.Globalization;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ItemResolvers;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType;

namespace Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ItemResolvers
{
	public class ItemResolverArgumentsFactory : IItemResolverArgumentsFactory
	{
		public ItemResolverArguments CreateItemResolverArguments(ResolveTypeArgs args)
		{
			if (args == null)
			{
				return null;
			}

			return CreateItemResolverArguments(args.Database, args.Language, args.ItemPath);
		}

		public ItemResolverArguments CreateItemResolverArguments(ResolveItemArgs args)
		{
			if (args == null)
			{
				return null;
			}

			return CreateItemResolverArguments(args.Database, args.Language, args.ItemPath);
		}

		public ItemResolverArguments CreateItemResolverArguments(Database database = null, Language language = null, string itemPath = null)
		{
			return new ItemResolverArguments
			{
				Database = database,
				Language = language,
				ItemPath = itemPath
			};
		}

		public ResolveItemArgs CreateResolveItemArgs(ItemResolverArguments arguments)
		{
			if (arguments == null)
			{
				return null;
			}

			return CreateResolveItemArgs(arguments.Database, arguments.Language, arguments.ItemPath);
		}

		public ResolveItemArgs CreateResolveItemArgs(Database database = null, Language language = null, string itemPath = null)
		{
			return new ResolveItemArgs
			{
				Database = database,
				Language = language,
				ItemPath = itemPath
			};
		}
	}
}

It just creates argument types for the pipeline and service classes.

The following interface is for classes that “resolve” Items based on arguments set on an ItemResolverArguments instance:

using Sitecore.Data.Items;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ItemResolvers;

namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ItemResolvers
{
	public interface IItemResolver
	{
		Item Resolve(ItemResolverArguments arguments);
	}
}

I created a another interface for an IItemResolver which resolves an Item from a Sitecore Database:

namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ItemResolvers
{
	public interface IDatabaseItemResolver : IItemResolver
	{
	}
}

The purpose of this interface is so I can register it and the following class which implements it in the Sitecore IoC container:

using Sitecore.Data.Items;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ItemResolvers;

namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ItemResolvers
{
	public class DatabaseItemResolver : IDatabaseItemResolver
	{
		public Item Resolve(ItemResolverArguments arguments)
		{
			if (!CanResolveItem(arguments))
			{
				return null;
			}

			if(arguments.Language == null)
			{
				return arguments.Database.GetItem(arguments.ItemPath);
			}

			return arguments.Database.GetItem(arguments.ItemPath, arguments.Language);
		}

		protected virtual bool CanResolveItem(ItemResolverArguments arguments) => arguments != null && arguments.Database != null && !string.IsNullOrWhiteSpace(arguments.ItemPath);
	}
}

The instance of the class above will return a Sitecore Item if a Database and Item path (this can be an Item ID) are supplied via the ItemResolverArguments instance passed to its Reolve() method.

Now, let’s start constructing the processors for the pipeline:

First, I created an interface and class for adding a “default” IItemResolver to a collection of IItemResolver defined on the pipeline’s arguments object:

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem.AddDefaultItemResolverProcessor
{
	public interface IAddDefaultItemResolver
	{
		void Process(ResolveItemArgs args);
	}
}
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ItemResolvers;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem.AddDefaultItemResolverProcessor
{
	public class AddDefaultItemResolver : ResolveProcessor<ResolveItemArgs>, IAddDefaultItemResolver
	{
		private readonly IDatabaseItemResolver _databaseItemResolver;

		public AddDefaultItemResolver(IDatabaseItemResolver databaseItemResolver)
		{
			_databaseItemResolver = databaseItemResolver;
		}

		protected override bool CanProcess(ResolveItemArgs args) => base.CanProcess(args) && args.ItemResolvers != null;

		protected override void Execute(ResolveItemArgs args) => args.ItemResolvers.Add(GetTypeResolver());

		protected virtual IItemResolver GetTypeResolver() => _databaseItemResolver;
	}
}

In the above class, I’m injecting the IDatabaseItemResolver instance — this was shown further up in this post — into the constructor of this class, and then adding it to the collection of resolvers.

I then created the following interface and implementation class to doing the “resolving” of the Item:

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem.ResolveItemProcessor
{
	public interface IResolveItem
	{
		void Process(ResolveItemArgs args);
	}
}
using System.Linq;

using Sitecore.Data.Items;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ItemResolvers;
using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ItemResolvers;
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ItemResolvers;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem.ResolveItemProcessor
{
	public class ResolveItem : ResolveProcessor<ResolveItemArgs>, IResolveItem
	{
		private readonly IItemResolverArgumentsFactory _itemResolverArgumentsFactory;
		

		public ResolveItem(IItemResolverArgumentsFactory itemResolverArgumentsFactory)
		{
			_itemResolverArgumentsFactory = itemResolverArgumentsFactory;
		}

		protected override bool CanProcess(ResolveItemArgs args) => base.CanProcess(args) && args.Database != null && !string.IsNullOrWhiteSpace(args.ItemPath) && args.ItemResolvers.Any();

		protected override void Execute(ResolveItemArgs args) => args.Item = GetItem(args);

		protected virtual Item GetItem(ResolveItemArgs args)
		{
			ItemResolverArguments arguments = CreateItemResolverArguments(args);
			if (arguments == null)
			{
				return null;
			}

			foreach (IItemResolver resolver in args.ItemResolvers)
			{
				Item item = resolver.Resolve(arguments);
				if (item != null)
				{
					return item;
				}
			}

			return null;
		}

		protected virtual ItemResolverArguments CreateItemResolverArguments(ResolveItemArgs args) => _itemResolverArgumentsFactory.CreateItemResolverArguments(args);
	}
}

The class above just iterates over all IItemResolver instances on the PipelineArgs instance; passes an ItemResolverArguments instance the Resolve() method on each — the ItemResolverArguments instance is created from a factory — and returns the first Item found by one of the IItemResolver instances. If none were found, null is returned.

Now, we need to create a service class that calls the custom pipeline. I created the following class to act as a settings class for the service.

namespace Sandbox.Foundation.ObjectResolution.Models.Resolvers.ItemResolvers
{
	public class ItemResolverServiceSettings
	{
		public string ResolveItemPipelineName { get; set; }
	}
}

An instance of this class will be injected into the service — the instance is created by the Sitecore Configuration Factory — and its ResolveItemPipelineName property will contain a value from Sitecore Configuration (see the Sitecore patch configuration file towards the bottom of this blog post).

I then created the following interface for the service — it’s just another IItemResolver — so I can register it in the Sitecore IoC container:

namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ItemResolvers
{
	public interface IItemResolverService : IItemResolver
	{
	}
}

The following class implements the interface above:

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ItemResolvers;
using Sitecore.Abstractions;
using Sitecore.Data.Items;

using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ItemResolvers;


using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem;

namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ItemResolvers
{
	public class ItemResolverService : PipelineObjectResolver<ItemResolverArguments, ResolveItemArgs, Item>, IItemResolverService
	{
		private readonly ItemResolverServiceSettings _settings;
		private readonly IItemResolverArgumentsFactory _itemResolverArgumentsFactory;

		public ItemResolverService(ItemResolverServiceSettings settings, IItemResolverArgumentsFactory itemResolverArgumentsFactory, BaseCorePipelineManager corePipelineManager)
			: base(corePipelineManager)
		{
			_settings = settings;
			_itemResolverArgumentsFactory = itemResolverArgumentsFactory;
		}

		protected override Item GetObject(ResolveItemArgs args)
		{
			return args.Item;
		}

		protected override ResolveItemArgs CreatePipelineArgs(ItemResolverArguments arguments) => _itemResolverArgumentsFactory.CreateResolveItemArgs(arguments);

		protected override string GetPipelineName() => _settings.ResolveItemPipelineName;
	}
}

The above class subclasses the abstract PipelineObjectResolver class I had shown further above in this post. Most of the magic happens in that base class — for those interested in design patterns, this is an example of the Template Method pattern if you did not know — and all subsequent custom pipeline wrapping service classes will follow this same pattern.

I’m not going to go much into detail on the above class as it should be self-evident on what’s happening after looking at the PipelineObjectResolver further up in this post.

<resolveType />

I then started code for the next pipeline — a pipeline to resolve Types.

I created the following PipelineArgs subclass class whose instances will serve as arguments to this new pipeline:

using System;
using System.Collections.Generic;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;
using Sitecore.Pipelines;

using Sandbox.Foundation.ObjectResolution.Services.Cachers;
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ItemResolvers;
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType
{
	public class ResolveTypeArgs : PipelineArgs
	{
		public Database Database { get; set; }

		public string ItemPath { get; set; }

		public Language Language { get; set; }

		public IItemResolver ItemResolver { get; set; }

		public Item Item { get; set; }

		public string TypeFieldName { get; set; }

		public string TypeName { get; set; }

		public IList<ITypeResolver> TypeResolvers { get; set; } = new List<ITypeResolver>();

		public ITypeCacher TypeCacher { get; set; }

		public Type Type { get; set; }

		public bool UseTypeCache { get; set; }
	}
}

I then created the following class to serve as an arguments object for services that will resolve types:

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;

namespace Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers
{
	public class TypeResolverArguments
	{
		public Database Database { get; set; }

		public Language Language { get; set; }

		public string ItemPath { get; set; }

		public Item Item { get; set; }

		public string TypeFieldName { get; set; }

		public string TypeName { get; set; }

		public bool UseTypeCache { get; set; }
	}
}

As I had done for the previous resolver, I created a factory to create arguments for both the PipelineArgs and arguments used by the service classes. Here is the interface for that factory class:

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers;
using Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject;
using Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType;

namespace Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.TypeResolvers
{
	public interface ITypeResolverArgumentsFactory
	{
		TypeResolverArguments CreateTypeResolverArguments(ResolveObjectArgs args);

		TypeResolverArguments CreateTypeResolverArguments(LocateObjectArgs args);

		TypeResolverArguments CreateTypeResolverArguments(CreateObjectArgs args);

		TypeResolverArguments CreateTypeResolverArguments(ResolveTypeArgs args);

		TypeResolverArguments CreateTypeResolverArguments(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, bool useTypeCache = false);

		ResolveTypeArgs CreateResolveTypeArgs(TypeResolverArguments arguments);

		ResolveTypeArgs CreateResolveTypeArgs(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, bool useTypeCache = false);
	}
}

The following class implements the interface above:

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers;
using Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject;
using Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType;

namespace Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.TypeResolvers
{
	public class TypeResolverArgumentsFactory : ITypeResolverArgumentsFactory
	{
		public TypeResolverArguments CreateTypeResolverArguments(ResolveObjectArgs args)
		{
			if (args == null)
			{
				return null;
			}

			return CreateTypeResolverArguments(args.Database, args.Language, args.ItemPath, args.Item, args.TypeFieldName, args.TypeName, args.UseTypeCache);
		}
		
		public TypeResolverArguments CreateTypeResolverArguments(LocateObjectArgs args)
		{
			if (args == null)
			{
				return null;
			}

			return CreateTypeResolverArguments(args.Database, args.Language, args.ItemPath, args.Item, args.TypeFieldName, args.TypeName, args.UseTypeCache);
		}

		public TypeResolverArguments CreateTypeResolverArguments(CreateObjectArgs args)
		{
			if (args == null)
			{
				return null;
			}

			return CreateTypeResolverArguments(args.Database, args.Language, args.ItemPath, args.Item, args.TypeFieldName, args.TypeName, args.UseTypeCache);
		}

		public TypeResolverArguments CreateTypeResolverArguments(ResolveTypeArgs args)
		{
			return CreateTypeResolverArguments(args.Database, args.Language, args.ItemPath, args.Item, args.TypeFieldName, args.TypeName, args.UseTypeCache);
		}

		public TypeResolverArguments CreateTypeResolverArguments(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, bool useTypeCache = false)
		{
			return new TypeResolverArguments
			{
				Database = database,
				Language = language,
				ItemPath = itemPath,
				Item = item,
				TypeFieldName = typeFieldName,
				TypeName = typeName,
				UseTypeCache = useTypeCache
			};
		}

		public ResolveTypeArgs CreateResolveTypeArgs(TypeResolverArguments arguments)
		{
			if (arguments == null)
			{
				return null;
			}

			return CreateResolveTypeArgs(arguments.Database, arguments.Language, arguments.ItemPath, arguments.Item, arguments.TypeFieldName, arguments.TypeName, arguments.UseTypeCache);
		}

		public ResolveTypeArgs CreateResolveTypeArgs(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, bool useTypeCache = false)
		{
			return new ResolveTypeArgs
			{
				Database = database,
				Language = language,
				ItemPath = itemPath,
				Item = item,
				TypeFieldName = typeFieldName,
				TypeName = typeName,
				UseTypeCache = useTypeCache
			};
		}
	}
}

I’m not going to discuss much on the class above — it just creates instances of TypeResolverArguments and ResolveTypeArgs based on a variety of things provided to each method.

I then created the following interface for a pipeline processor to resolve an Item and set it on the passed PipelineArgs instance if one wasn’t provided by the caller or set by another processor:

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.SetItemResolverProcessor
{
	public interface ISetItemResolver
	{
		void Process(ResolveTypeArgs args);
	}
}

The following class implements the interface above:

using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ItemResolvers;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.SetItemResolverProcessor
{
	public class SetItemResolver : ResolveProcessor<ResolveTypeArgs>, ISetItemResolver
	{
		private readonly IItemResolverService _itemResolverService;

		public SetItemResolver(IItemResolverService itemResolverService)
		{
			_itemResolverService = itemResolverService;
		}

		protected override bool CanProcess(ResolveTypeArgs args) => base.CanProcess(args) && args.Database != null && !string.IsNullOrWhiteSpace(args.ItemPath);

		protected override void Execute(ResolveTypeArgs args) => args.ItemResolver = GetItemResolver();

		protected virtual IItemResolver GetItemResolver() => _itemResolverService;
	}
}

In the class above, I’m injecting an instance of a IItemResolverService into its constructor, and setting it on the ItemResolver property of the ResolveTypeArgs instance.

Does this IItemResolverService interface look familiar? It should as it’s the IItemResolverService defined further up in this post which calls the <resolveItem /> pipeline.

Now we need a processor to resolve the Item. The following interface and class do this:

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.ResolveTypeProcessor
{
	public interface IResolveItem
	{
		void Process(ResolveTypeArgs args);
	}
}
using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ItemResolvers;
using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ItemResolvers;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.ResolveTypeProcessor
{
	public class ResolveItem : ResolveProcessor<ResolveTypeArgs>, IResolveItem
	{
		private readonly IItemResolverArgumentsFactory _itemResolverArgumentsFactory;

		public ResolveItem(IItemResolverArgumentsFactory itemResolverArgumentsFactory)
		{
			_itemResolverArgumentsFactory = itemResolverArgumentsFactory;
		}

		protected override bool CanProcess(ResolveTypeArgs args) => base.CanProcess(args) && args.Database != null && args.ItemResolver != null;

		protected override void Execute(ResolveTypeArgs args) => args.Item = args.ItemResolver.Resolve(CreateTypeResolverArguments(args));

		protected virtual ItemResolverArguments CreateTypeResolverArguments(ResolveTypeArgs args) => _itemResolverArgumentsFactory.CreateItemResolverArguments(args);
	}
}

The class above just delegates to the IItemResolver instance on the ResolveTypeArgs instance to resolve the Item.

Next, we need a processor to get the fully qualified type name from the Item. The following interface and class are for a processor that does just that:

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.SetTypeNameProcessor
{
	public interface ISetTypeName
	{
		void Process(ResolveTypeArgs args);
	}
}
namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.SetTypeNameProcessor
{
	public class SetTypeName : ResolveProcessor<ResolveTypeArgs>, ISetTypeName
	{
		protected override bool CanProcess(ResolveTypeArgs args) => base.CanProcess(args) && args.Item != null && !string.IsNullOrWhiteSpace(args.TypeFieldName);

		protected override void Execute(ResolveTypeArgs args) => args.TypeName = args.Item[args.TypeFieldName];
	}
}

The class above just gets the value from the field where the fully qualified type is defined — the name of the field where the fully qualified type name is defined should be set by the caller of this pipeline.
I then defined the following interface and class which will sort out what the Type object is based on a fully qualified type name passed to it:

using System;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers;

namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers
{
	public interface ITypeResolver
	{
		Type Resolve(TypeResolverArguments arguments);
	}
}

I then created the following interface for a service class that will delegate to Sitecore.Reflection.ReflectionUtil to get a Type with a provided fully qualified type name:

using System;

namespace Sandbox.Foundation.ObjectResolution.Services.Reflection
{
	public interface IReflectionUtilService
	{
		Type GetTypeInfo(string type);

		object CreateObject(Type type);

		object CreateObject(Type type, object[] parameters);
	}
}

Here’s the class that implements the interface above:

using System;

using Sitecore.Reflection;

namespace Sandbox.Foundation.ObjectResolution.Services.Reflection
{
	public class ReflectionUtilService : IReflectionUtilService
	{
		public Type GetTypeInfo(string type)
		{
			return ReflectionUtil.GetTypeInfo(type);
		}

		public object CreateObject(Type type)
		{
			return ReflectionUtil.CreateObject(type);
		}

		public object CreateObject(Type type, object[] parameters)
		{
			return ReflectionUtil.CreateObject(type, parameters);
		}
	}
}

The class above also creates objects via the ReflectionUtil static class with a passed type and constructor arguments — this will be used in the <createObject /> pipeline further down in this post.

I then defined the following interface for a class that will leverage the IReflectionUtilService service above:

namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers
{
	public interface IReflectionTypeResolver : ITypeResolver
	{
	}
}

This is the class that implements the interface above:

using System;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers;
using Sandbox.Foundation.ObjectResolution.Services.Reflection;

namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers
{
	public class ReflectionTypeResolver : IReflectionTypeResolver
	{
		private readonly IReflectionUtilService _reflectionUtilService;

		public ReflectionTypeResolver(IReflectionUtilService reflectionUtilService)
		{
			_reflectionUtilService = reflectionUtilService;
		}

		public Type Resolve(TypeResolverArguments arguments)
		{
			if (string.IsNullOrWhiteSpace(arguments?.TypeName))
			{
				return null;
			}

			return GetTypeInfo(arguments.TypeName);
		}

		protected virtual Type GetTypeInfo(string typeName) => _reflectionUtilService.GetTypeInfo(typeName);
	}
}

The class above just delegates to the IReflectionUtilService to get the Type with the supplied fully qualified type name.

I then created the following interface and class to represent a pipeline processor to add the ITypeResolver above to the collection of ITypeResolver on the ResolveTypeArgs instance passed to it:

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.AddDefaultTypeResolverProcessor
{
	public interface IAddDefaultTypeResolver
	{
		void Process(ResolveTypeArgs args);
	}
}
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.AddDefaultTypeResolverProcessor
{
	public class AddDefaultTypeResolver : ResolveProcessor<ResolveTypeArgs>, IAddDefaultTypeResolver
	{
		private readonly IReflectionTypeResolver _reflectionTypeResolver;

		public AddDefaultTypeResolver(IReflectionTypeResolver reflectionTypeResolver)
		{
			_reflectionTypeResolver = reflectionTypeResolver;
		}

		protected override bool CanProcess(ResolveTypeArgs args) => base.CanProcess(args) && args.TypeResolvers != null;

		protected override void Execute(ResolveTypeArgs args) => args.TypeResolvers.Add(GetTypeResolver());

		protected virtual ITypeResolver GetTypeResolver() => _reflectionTypeResolver;
	}
}

There isn’t much going on in the class above. The Execute() method just adds the IReflectionTypeResolver to the TypeResolvers collection.

When fishing through the Sitecore Experience Forms assemblies, I noticed the OOTB code was “caching” Types it had resolved from Type fields.. I decided to employ the same approach, and defined the following interface for an object that caches Types:

using System;

namespace Sandbox.Foundation.ObjectResolution.Services.Cachers
{
	public interface ITypeCacher
	{
		void AddTypeToCache(string typeName, Type type);

		Type GetTypeFromCache(string typeName);
	}
}

Here is the class that implements the interface above:

using System;
using System.Collections.Concurrent;

namespace Sandbox.Foundation.ObjectResolution.Services.Cachers
{
	public class TypeCacher : ITypeCacher
	{
		private static readonly ConcurrentDictionary<string, Type> TypeCache = new ConcurrentDictionary<string, Type>();

		public void AddTypeToCache(string typeName, Type type)
		{
			if (string.IsNullOrWhiteSpace(typeName) || type == null)
			{
				return;
			}

			TypeCache.TryAdd(typeName, type);
		}

		public Type GetTypeFromCache(string typeName)
		{
			if (string.IsNullOrWhiteSpace(typeName))
			{
				return null;
			}

			Type type;
			if (!TypeCache.TryGetValue(typeName, out type))
			{
				return null;
			}

			return type;
		}
	}
}

The AddTypeToCache() method does exactly what the method name says — it will add the supplied Type to cache with the provided type name as the key into the ConcurrentDictionary dictionary on this class.

The GetTypeFromCache() method above tries to get a Type from the ConcurrentDictionary instance on this class, and returns to the caller if it was found. If it wasn’t found, null is returned.

The following interface and class serve as a pipeline processor to set a ITypeCacher instance on the ResolveTypeArgs instance passed to it:

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.SetTypeCacherProcessor
{
	public interface ISetTypeCacher
	{
		void Process(ResolveTypeArgs args);
	}
}
using Sandbox.Foundation.ObjectResolution.Services.Cachers;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.SetTypeCacherProcessor
{
	public class SetTypeCacher : ResolveProcessor<ResolveTypeArgs>, ISetTypeCacher
	{
		private readonly ITypeCacher _typeCacher;

		public SetTypeCacher(ITypeCacher typeCacher)
		{
			_typeCacher = typeCacher;
		}

		protected override bool CanProcess(ResolveTypeArgs args) => base.CanProcess(args) && args.UseTypeCache && args.TypeCacher == null;

		protected override void Execute(ResolveTypeArgs args) => args.TypeCacher = GetTypeCacher();

		protected virtual ITypeCacher GetTypeCacher() => _typeCacher;
	}
}

There isn’t much going on in the class above except the injection of the ITypeCacher instance defined further up, and setting that instance on the ResolveTypeArgs instance if it hasn’t already been set.

Now, we need to resolve the Type. The following interface and its implementation class do just that:

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.ResolveTypeProcessor
{
	public interface IResolveType
	{
		void Process(ResolveTypeArgs args);
	}
}
using System;
using System.Linq;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers;
using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.TypeResolvers;
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.ResolveTypeProcessor
{
	public class ResolveType : ResolveProcessor<ResolveTypeArgs>, IResolveType
	{
		private readonly ITypeResolverArgumentsFactory _typeResolverArgumentsFactory;

		public ResolveType(ITypeResolverArgumentsFactory typeResolverArgumentsFactory)
		{
			_typeResolverArgumentsFactory = typeResolverArgumentsFactory;
		}

		protected override bool CanProcess(ResolveTypeArgs args) => base.CanProcess(args) && args.TypeResolvers != null && args.TypeResolvers.Any() && !string.IsNullOrWhiteSpace(args.TypeName);

		protected override void Execute(ResolveTypeArgs args) => args.Type = Resolve(args);

		protected virtual Type Resolve(ResolveTypeArgs args)
		{
			Type type = null;
			if (args.UseTypeCache)
			{
				type = GetTypeFromCache(args);
			}

			if (type == null)
			{
				type = GetTypeInfo(args);
			}

			return type;
		}

		protected virtual Type GetTypeInfo(ResolveTypeArgs args)
		{
			TypeResolverArguments arguments = CreateTypeResolverArguments(args);
			if (arguments == null)
			{
				return null;
			}

			foreach (ITypeResolver typeResolver in args.TypeResolvers)
			{
				Type type = typeResolver.Resolve(arguments);
				if (type != null)
				{
					return type;
				}
			}

			return null;
		}

		protected virtual Type GetTypeFromCache(ResolveTypeArgs args) => args.TypeCacher.GetTypeFromCache(args.TypeName);

		protected virtual TypeResolverArguments CreateTypeResolverArguments(ResolveTypeArgs args) => _typeResolverArgumentsFactory.CreateTypeResolverArguments(args);
	}
}

Just as I had done in the <resolveItem /> pipeline further up in this post, the above processor class will iterate over a collection of “resolvers” on the PipelineArgs instance — in this case it’s the TypeResolvers — and pass an arguments instance to each’s Resolve() method. This arguments instance is created from a factory defined further up in this post.

I then created the following settings class for the service class that will wrap the <resolveType /> pipeline:

namespace Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers
{
	public class TypeResolverServiceSettings
	{
		public string ResolveTypePipelineName { get; set; }
	}
}

The value on the ResolveTypePipelineName property will come from the Sitecore patch file towards the bottom of this post.

I then created the following interface for the service class that will wrap the pipeline — if you are a design patterns buff, this is an example of the adapter pattern:

namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers
{
	public interface ITypeResolverService : ITypeResolver
	{
	}
}

The following class implements the interface above:

using System;

using Sitecore.Abstractions;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType;
using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.TypeResolvers;

namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers
{
	public class TypeResolverService : PipelineObjectResolver<TypeResolverArguments, ResolveTypeArgs, Type>, ITypeResolverService
	{
		private readonly TypeResolverServiceSettings _settings;
		private readonly ITypeResolverArgumentsFactory _typeResolverArgumentsFactory;

		public TypeResolverService(TypeResolverServiceSettings settings, ITypeResolverArgumentsFactory typeResolverArgumentsFactory, BaseCorePipelineManager corePipelineManager)
			: base(corePipelineManager)
		{
			_settings = settings;
			_typeResolverArgumentsFactory = typeResolverArgumentsFactory;
		}

		protected override Type GetObject(ResolveTypeArgs args)
		{
			return args.Type;
		}

		protected override ResolveTypeArgs CreatePipelineArgs(TypeResolverArguments arguments) => _typeResolverArgumentsFactory.CreateResolveTypeArgs(arguments);

		protected override string GetPipelineName() => _settings.ResolveTypePipelineName;
	}
}

I’m not going to go into details about the class above as it’s just like the other service class which wraps the <resolveItem /> defined further above in this post.

Still following? We’re almost there. 😉

<locateObject />

So we now have a way to resolve Items and Types, we now need to find a Type from an Item in the IoC container. I created a PipelineArgs class for a pipeline that does just that:

using System;
using System.Collections.Generic;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;
using Sitecore.Pipelines;

using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectLocators;
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject
{
	public class LocateObjectArgs : PipelineArgs
	{
		public Database Database { get; set; }

		public string ItemPath { get; set; }

		public Language Language { get; set; }

		public Item Item { get; set; }

		public string TypeFieldName { get; set; }

		public string TypeName { get; set; }

		public bool UseTypeCache { get; set; }

		public ITypeResolver TypeResolver { get; set; }

		public Type Type { get; set; }

		public IList<IObjectLocator> Locators { get; set; } = new List<IObjectLocator>();

		public object Object { get; set; }
	}
}

In reality, this next pipeline can supply an object from anywhere — it doesn’t have to be from an IoC container but that’s what I’m doing here. I did, however, make it extendable so you can source an object from wherever you want, even from the Post Office. 😉

I then created the following arguments object for service classes that will “locate” objects:

using System;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;

namespace Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectLocators
{
	public class ObjectLocatorArguments
	{
		public Database Database { get; set; }

		public Language Language { get; set; }

		public string ItemPath { get; set; }

		public Item Item { get; set; }

		public string TypeFieldName { get; set; }

		public string TypeName { get; set; }

		public Type Type { get; set; }

		public bool UseTypeCache { get; set; }
	}
}

As I had done for the previous two “resolvers”, I created a factory to create arguments objects — both for the pipeline and service classes:

using System;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectLocators;
using Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject;

namespace Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectLocators
{
	public interface IObjectLocatorArgumentsFactory
	{
		ObjectLocatorArguments CreateObjectLocatorArguments(ResolveObjectArgs args);
		
		ObjectLocatorArguments CreateObjectLocatorArguments(LocateObjectArgs args);

		ObjectLocatorArguments CreateObjectLocatorArguments(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, Type type = null, bool useTypeCache = false);

		LocateObjectArgs CreateLocateObjectArgs(ObjectLocatorArguments arguments);

		LocateObjectArgs CreateLocateObjectArgs(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, Type type = null, bool useTypeCache = false);
	}
}
using System;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectLocators;
using Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject;

namespace Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectLocators
{
	public class ObjectLocatorArgumentsFactory : IObjectLocatorArgumentsFactory
	{
		public ObjectLocatorArguments CreateObjectLocatorArguments(ResolveObjectArgs args)
		{
			if (args == null)
			{
				return null;
			}

			return CreateObjectLocatorArguments(args.Database, args.Language, args.ItemPath, args.Item, args.TypeFieldName, args.TypeName, args.Type, args.UseTypeCache);
		}

		public ObjectLocatorArguments CreateObjectLocatorArguments(LocateObjectArgs args)
		{
			if (args == null)
			{
				return null;
			}

			return CreateObjectLocatorArguments(args.Database, args.Language, args.ItemPath, args.Item, args.TypeFieldName, args.TypeName, args.Type, args.UseTypeCache);
		}

		public ObjectLocatorArguments CreateObjectLocatorArguments(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, Type type = null, bool useTypeCache = false)
		{
			return new ObjectLocatorArguments
			{
				Database = database,
				Language = language,
				ItemPath = itemPath,
				Item = item,
				TypeFieldName = typeFieldName,
				TypeName = typeName,
				Type = type
			};
		}

		public LocateObjectArgs CreateLocateObjectArgs(ObjectLocatorArguments arguments)
		{
			if (arguments == null)
			{
				return null;
			}

			return CreateLocateObjectArgs(arguments.Database, arguments.Language, arguments.ItemPath, arguments.Item, arguments.TypeFieldName, arguments.TypeName, arguments.Type, arguments.UseTypeCache);
		}

		public LocateObjectArgs CreateLocateObjectArgs(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, Type type = null, bool useTypeCache = false)
		{
			return new LocateObjectArgs
			{
				Database = database,
				Language = language,
				ItemPath = itemPath,
				Item = item,
				TypeFieldName = typeFieldName,
				TypeName = typeName,
				Type = type
			};
		}
	}
}

The above class implements the interface above. It just creates arguments for both the pipeline and service classes.

I then defined the following interface for a pipeline processor to set the ITypeResolver (defined way up above in this post):

namespace Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.SetTypeResolverProcessor
{
	public interface ISetTypeResolver
	{
		void Process(LocateObjectArgs args);
	}
}

This class implements the interface above:

using Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.SetTypeResolverProcessor
{
	public class SetTypeResolver : ResolveProcessor<LocateObjectArgs>, ISetTypeResolver
	{
		private readonly ITypeResolverService _typeResolverService;

		public SetTypeResolver(ITypeResolverService typeResolverService)
		{
			_typeResolverService = typeResolverService;
		}

		protected override bool CanProcess(LocateObjectArgs args) => base.CanProcess(args) && args.TypeResolver == null;

		protected override void Execute(LocateObjectArgs args)
		{
			args.TypeResolver = GetTypeResolver();
		}

		protected virtual ITypeResolver GetTypeResolver() => _typeResolverService;
	}
}

In the class above, I’m injecting the ITypeResolverService into its constructor — this is the service class that wraps the <resolveType /> pipeline defined further up — and set it on the LocateObjectArgs instance if it’s not already set.

Next, I created the following interface for a processor that will “resolve” the type from the TypeResolver set on the LocateObjectArgs instance:

namespace Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.ResolveTypeProcessor
{
	public interface IResolveType
	{
		void Process(LocateObjectArgs args);
	}
}

The following class implements the interface above:

using System;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers;
using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.TypeResolvers;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.ResolveTypeProcessor
{
	public class ResolveType : ResolveProcessor<LocateObjectArgs>, IResolveType
	{
		private readonly ITypeResolverArgumentsFactory _typeResolverArgumentsFactory;

		public ResolveType(ITypeResolverArgumentsFactory typeResolverArgumentsFactory)
		{
			_typeResolverArgumentsFactory = typeResolverArgumentsFactory;
		}

		protected override bool CanProcess(LocateObjectArgs args) => base.CanProcess(args) && args.Type == null && args.TypeResolver != null;

		protected override void Execute(LocateObjectArgs args)
		{
			args.Type = Resolve(args);
		}

		protected virtual Type Resolve(LocateObjectArgs args)
		{
			TypeResolverArguments arguments = CreateTypeResolverArguments(args);
			if (arguments == null)
			{
				return null;
			}

			return args.TypeResolver.Resolve(arguments);
		}

		protected virtual TypeResolverArguments CreateTypeResolverArguments(LocateObjectArgs args) => _typeResolverArgumentsFactory.CreateTypeResolverArguments(args);
	}
}

The class above just “resolves” the type from the TypeResolver set on the LocateObjectArgs instance. Nothing more to see. 😉

I then defined the following interface for a family of classes that “locate” objects from somewhere (perhaps a magical place. 😉 ):

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectLocators;

namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectLocators
{
	public interface IObjectLocator
	{
		object Resolve(ObjectLocatorArguments arguments);
	}
}

Well, we can’t use much magic in this solution, so I’m going to “locate” things in the Sitecore IoC container, so defined the following interface for a class that will employ Service Locator to find it in the Sitecore IoC container:

namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectLocators
{
	public interface IServiceProviderLocator : IObjectLocator
	{
	}
}

This class implements the interface above:

using System;

using Sitecore.DependencyInjection;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectLocators;

namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectLocators
{
	public class ServiceProviderLocator : IServiceProviderLocator
	{
		private readonly IServiceProvider _serviceProvider;

		public ServiceProviderLocator()
		{
			_serviceProvider = GetServiceProvider();
		}

		protected virtual IServiceProvider GetServiceProvider()
		{
			return ServiceLocator.ServiceProvider;
		}

		public object Resolve(ObjectLocatorArguments arguments)
		{
			if (arguments == null || arguments.Type == null)
			{
				return null;
			}

			return GetService(arguments.Type);
		}

		protected virtual object GetService(Type type) => _serviceProvider.GetService(type);
	}
}

In the class above, I’m just passing a type to the System.IServiceProvider’s GetService() method — the IServiceProvider instance is grabbed from the ServiceProvider static member on Sitecore.DependencyInjection.ServiceLocator static class.

Next, I need a processor class to add an instance of the Service Locator IObjectLocator class above to the collection of IObjectLocator instances on the LocateObjectArgs instance, so I defined the following interface for a processor class that does just that:

namespace Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.AddDefaultObjectLocatorProcessor
{
	public interface IAddDefaultObjectLocator
	{
		void Process(LocateObjectArgs args);
	}
}

Here’s the implementation class for the interface above:

using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectLocators;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.AddDefaultObjectLocatorProcessor
{
	public class AddDefaultObjectLocator : ResolveProcessor<LocateObjectArgs>, IAddDefaultObjectLocator
	{
		private readonly IServiceProviderLocator _serviceProviderLocator;

		public AddDefaultObjectLocator(IServiceProviderLocator serviceProviderLocator)
		{
			_serviceProviderLocator = serviceProviderLocator;
		}

		protected override bool CanProcess(LocateObjectArgs args) => base.CanProcess(args) && args.Locators != null;

		protected override void Execute(LocateObjectArgs args) => args.Locators.Add(GetObjectLocator());

		protected virtual IObjectLocator GetObjectLocator() => _serviceProviderLocator;
	}
}

It’s just adding the IServiceProviderLocator instance to the collection of Locators set on the LocateObjectArgs instance.

Great, so we have things that can “locate” objects but need to have a processor that does the execution of that step to actually find those objects. The following interface is for a processor class that does just that:

namespace Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.LocateObjectProcessor
{
	public interface ILocateObject
	{
		void Process(LocateObjectArgs args);
	}
}

And here’s its implementation class:

using System.Linq;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectLocators;
using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectLocators;
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectLocators;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.LocateObjectProcessor
{
	public class LocateObject : ResolveProcessor<LocateObjectArgs>, ILocateObject
	{
		private readonly IObjectLocatorArgumentsFactory _objectLocatorArgumentsFactory;

		public LocateObject(IObjectLocatorArgumentsFactory objectLocatorArgumentsFactory)
		{
			_objectLocatorArgumentsFactory = objectLocatorArgumentsFactory;
		}

		protected override bool CanProcess(LocateObjectArgs args) => base.CanProcess(args) && args.Locators != null && args.Locators.Any() && args.Type != null;

		protected override void Execute(LocateObjectArgs args) => args.Object = Resolve(args);

		protected virtual object Resolve(LocateObjectArgs args)
		{
			ObjectLocatorArguments arguments = CreateObjectLocatorArguments(args);
			if (arguments == null)
			{
				return null;
			}

			foreach (IObjectLocator objectLocator in args.Locators)
			{
				object obj = objectLocator.Resolve(arguments);
				if (obj != null)
				{
					return obj;
				}
			}

			return null;
		}

		protected virtual ObjectLocatorArguments CreateObjectLocatorArguments(LocateObjectArgs args) => _objectLocatorArgumentsFactory.CreateObjectLocatorArguments(args);
	}
}

As I had done in the previous pipelines, I’m just iterating over a collection of classes that “resolve” for a particular thing — here I’m iterating over all IObjectLocator instances set on the LocateObjectArgs instance. If one of them find the object we are looking for, we just set it on the LocateObjectArgs instance.

As I had done for the other pipelines, I created a service class that wraps the new pipeline I am creating. The following class serves as a settings class for that service class:

namespace Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectLocators
{
	public class ObjectLocatorServiceSettings
	{
		public string LocateObjectPipelineName { get; set; }
	}
}

An instance of the class above will be created by the Sitecore Configuration Factory, and its LocateObjectPipelineName property will contain a value defined in the Sitecore patch file further down in this post.

I then created the following interface for the service class that will wrap this new pipeline:

namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectLocators
{
	public interface IObjectLocatorService : IObjectLocator
	{
	}
}

Here’s the class that implements the interface above:

using Sitecore.Abstractions;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectLocators;
using Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject;
using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectLocators;

namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectLocators
{
	public class ObjectLocatorService : PipelineObjectResolver<ObjectLocatorArguments, LocateObjectArgs, object>, IObjectLocatorService
	{
		private readonly ObjectLocatorServiceSettings _settings;
		private readonly IObjectLocatorArgumentsFactory _objectLocatorArgumentsFactory;

		public ObjectLocatorService(ObjectLocatorServiceSettings settings, IObjectLocatorArgumentsFactory objectLocatorArgumentsFactory, BaseCorePipelineManager corePipelineManager)
			: base(corePipelineManager)
		{
			_settings = settings;
			_objectLocatorArgumentsFactory = objectLocatorArgumentsFactory;
		}

		protected override object GetObject(LocateObjectArgs args)
		{
			return args.Object;
		}

		protected override LocateObjectArgs CreatePipelineArgs(ObjectLocatorArguments arguments) => _objectLocatorArgumentsFactory.CreateLocateObjectArgs(arguments);

		protected override string GetPipelineName() => _settings.LocateObjectPipelineName;
	}
}

I’m not going talk much about the class above — it’s following the same pattern as the other classes that wrap their respective pipelines.

<createObject />

So what happens when we cannot find an object via the <locateObject /> pipeline? Well, let’s create it.

I defined the following PipelineArgs class for a new pipeline that creates objects:

using System;
using System.Collections.Generic;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;
using Sitecore.Pipelines;

using Sandbox.Foundation.ObjectResolution.Services.Cachers;
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectCreators;
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject
{
	public class CreateObjectArgs : PipelineArgs
	{
		public Database Database { get; set; }

		public string ItemPath { get; set; }

		public Language Language { get; set; }

		public Item Item { get; set; }

		public string TypeFieldName { get; set; }

		public string TypeName { get; set; }

		public ITypeResolver TypeResolver { get; set; }

		public Type Type { get; set; }

		public object[] Parameters { get; set; }

		public IList<IObjectCreator> Creators { get; set; } = new List<IObjectCreator>();

		public object Object { get; set; }

		public bool UseTypeCache { get; set; }

		public ITypeCacher TypeCacher { get; set; }
	}
}

I then defined the following class for service classes that create objects:

using System;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;

namespace Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectCreators
{
	public class ObjectCreatorArguments
	{
		public Database Database { get; set; }

		public Language Language { get; set; }

		public string ItemPath { get; set; }

		public Item Item { get; set; }

		public string TypeFieldName { get; set; }

		public string TypeName { get; set; }

		public Type Type { get; set; }

		public bool UseTypeCache { get; set; }

		public object[] Parameters { get; set; }
	}
}

Since the “new” keyword promotes tight coupling between classes, I created the following factory interface for classes that create the two arguments types shown above:

using System;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectCreators;
using Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject;

namespace Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectCreators
{
	public interface IObjectCreatorArgumentsFactory
	{
		ObjectCreatorArguments CreateObjectCreatorArguments(ResolveObjectArgs args);

		ObjectCreatorArguments CreateObjectCreatorArguments(CreateObjectArgs args);

		ObjectCreatorArguments CreateObjectCreatorArguments(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, Type type = null, bool useTypeCache = false, object[] parameters = null);

		CreateObjectArgs CreateCreateObjectArgs(ObjectCreatorArguments arguments);

		CreateObjectArgs CreateCreateObjectArgs(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, Type type = null, bool useTypeCache = false, object[] parameters = null);

	}
}

The following class implements the interface above:

using System;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectCreators;
using Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject;

namespace Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectCreators
{
	public class ObjectCreatorArgumentsFactory : IObjectCreatorArgumentsFactory
	{
		public ObjectCreatorArguments CreateObjectCreatorArguments(ResolveObjectArgs args)
		{
			if (args == null)
			{
				return null;
			}

			return CreateObjectCreatorArguments(args.Database, args.Language, args.ItemPath, args.Item, args.TypeFieldName, args.TypeName, args.Type, args.UseTypeCache, args.ObjectCreationParameters);
		}

		public ObjectCreatorArguments CreateObjectCreatorArguments(CreateObjectArgs args)
		{
			if (args == null)
			{
				return null;
			}

			return CreateObjectCreatorArguments(args.Database, args.Language, args.ItemPath, args.Item, args.TypeFieldName, args.TypeName, args.Type, args.UseTypeCache, args.Parameters);
		}

		public ObjectCreatorArguments CreateObjectCreatorArguments(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, Type type = null, bool useTypeCache = false, object[] parameters = null)
		{
			return new ObjectCreatorArguments
			{
				Database = database,
				Language = language,
				ItemPath = itemPath,
				Item = item,
				TypeFieldName = typeFieldName,
				TypeName = typeName,
				Type = type,
				Parameters = parameters
			};
		}

		public CreateObjectArgs CreateCreateObjectArgs(ObjectCreatorArguments arguments)
		{
			if (arguments == null)
			{
				return null;
			}

			return CreateCreateObjectArgs(arguments.Database, arguments.Language, arguments.ItemPath, arguments.Item, arguments.TypeFieldName, arguments.TypeName, arguments.Type, arguments.UseTypeCache, arguments.Parameters);
		}

		public CreateObjectArgs CreateCreateObjectArgs(Database database = null, Language language = null, string itemPath = null, Item item = null, string typeFieldName = null, string typeName = null, Type type = null, bool useTypeCache = false, object[] parameters = null)
		{
			return new CreateObjectArgs
			{
				Database = database,
				Language = language,
				ItemPath = itemPath,
				Item = item,
				TypeFieldName = typeFieldName,
				TypeName = typeName,
				Type = type,
				UseTypeCache = useTypeCache,
				Parameters = parameters
			};
		}
	}
}

The class above just creates CreateObjectArgs and ObjectCreatorArguments instances.

Let’s jump into the bits that comprise the new pipeline.

The following interface is for a processor class that sets the ITypeResolver on the CreateObjectArgs instance:

namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.SetTypeResolverProcessor
{
	public interface ISetTypeResolver
	{
		void Process(CreateObjectArgs args);
	}
}

Here’s the processor class that implements the interface above:

using Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.SetTypeResolverProcessor
{
	public class SetTypeResolver : ResolveProcessor<CreateObjectArgs>, ISetTypeResolver
	{
		private readonly ITypeResolverService _typeResolverService;

		public SetTypeResolver(ITypeResolverService typeResolverService)
		{
			_typeResolverService = typeResolverService;
		}

		protected override bool CanProcess(CreateObjectArgs args) => base.CanProcess(args) && args.Type == null && args.TypeResolver == null;

		protected override void Execute(CreateObjectArgs args)
		{
			args.TypeResolver = GetTypeResolver();
		}

		protected virtual ITypeResolver GetTypeResolver() => _typeResolverService;
	}
}

Nothing special going on — we’ve seen something like this before further up in this post.

Now, we need a processor to “resolve” types. The following interface is for a processor class which does just that:

namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.ResolveTypeProcessor
{
	public interface IResolveType
	{
		void Process(CreateObjectArgs args);
	}
}

And here is the processor class that implements the interface above:

using System;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers;
using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.TypeResolvers;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.ResolveTypeProcessor
{
	public class ResolveType : ResolveProcessor<CreateObjectArgs>, IResolveType
	{
		private readonly ITypeResolverArgumentsFactory _typeResolverArgumentsFactory;

		public ResolveType(ITypeResolverArgumentsFactory typeResolverArgumentsFactory)
		{
			_typeResolverArgumentsFactory = typeResolverArgumentsFactory;
		}

		protected override bool CanProcess(CreateObjectArgs args) => base.CanProcess(args) && args.Type == null && args.TypeResolver != null;

		protected override void Execute(CreateObjectArgs args)
		{
			args.Type = Resolve(args);
		}

		protected virtual Type Resolve(CreateObjectArgs args)
		{
			TypeResolverArguments arguments = CreateTypeResolverArguments(args);
			if (arguments == null)
			{
				return null;
			}

			return args.TypeResolver.Resolve(arguments);
		}

		protected virtual TypeResolverArguments CreateTypeResolverArguments(CreateObjectArgs args) => _typeResolverArgumentsFactory.CreateTypeResolverArguments(args);
	}
}

I’m not going to discuss much on this as we’ve already seen something like this further up in this post.

I then defined the following interface for a family of classes that create objects:

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectCreators;

namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectCreators
{
	public interface IObjectCreator
	{
		object Resolve(ObjectCreatorArguments arguments);
	}
}

Since I’m not good at arts and crafts, we’ll have to use reflection to create objects. The following interface is for a class that uses reflection to create objects:

namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectCreators
{
	public interface IReflectionObjectCreator : IObjectCreator
	{
	}
}

The following class implements the interface above:

using System.Linq;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectCreators;
using Sandbox.Foundation.ObjectResolution.Services.Reflection;

namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectCreators
{
	public class ReflectionObjectCreator : IReflectionObjectCreator
	{
		private readonly IReflectionUtilService _reflectionUtilService;

		public ReflectionObjectCreator(IReflectionUtilService reflectionUtilService)
		{
			_reflectionUtilService = reflectionUtilService;
		}

		public object Resolve(ObjectCreatorArguments arguments)
		{
			if (arguments == null || arguments.Type == null)
			{
				return null;
			}

			if (arguments.Parameters == null || !arguments.Parameters.Any())
			{
				return _reflectionUtilService.CreateObject(arguments.Type);
			}

			return _reflectionUtilService.CreateObject(arguments.Type, arguments.Parameters);
		}
	}
}

This class above just delegates to the IReflectionUtilService instance — this is defined way up above in this post — injected into it for creating objects.

Now we need to put this IReflectionObjectCreator somewhere so it can be used to create objects. The following interface is for a processor class that adds this to a collection of other IObjectCreator defined on the CreateObjectArgs instance:

namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.AddDefaultObjectCreatorProcessor
{
	public interface IAddDefaultObjectCreator
	{
		void Process(CreateObjectArgs args);
	}
}

And here is the magic behind the interface above:

using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectCreators;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.AddDefaultObjectCreatorProcessor
{
	public class AddDefaultObjectCreator : ResolveProcessor<CreateObjectArgs>, IAddDefaultObjectCreator
	{
		private readonly IReflectionObjectCreator _reflectionObjectCreator;

		public AddDefaultObjectCreator(IReflectionObjectCreator reflectionObjectCreator)
		{
			_reflectionObjectCreator = reflectionObjectCreator;
		}

		protected override bool CanProcess(CreateObjectArgs args) => base.CanProcess(args) && args.Creators != null;

		protected override void Execute(CreateObjectArgs args) => args.Creators.Add(GetObjectLocator());

		protected virtual IObjectCreator GetObjectLocator() => _reflectionObjectCreator;
	}
}

We are just adding the IReflectionObjectCreator instance to the Creators collection on the CreateObjectArgs instance.

Now, we need a processor that delegates to each IObjectCreator instance in the collection on the CreateObjectArgs instance. The following interface is for a processor that does that:

namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.CreateObjectProcessor
{
	public interface ICreateObject
	{
		void Process(CreateObjectArgs args);
	}
}

Here’s the above interface’s implementation class:

using System.Linq;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectCreators;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject;
using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectCreators;
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectCreators;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.CreateObjectProcessor
{
	public class CreateObject : ResolveProcessor<CreateObjectArgs>, ICreateObject
	{
		private readonly IObjectCreatorArgumentsFactory _objectCreatorArgumentsFactory;

		public CreateObject(IObjectCreatorArgumentsFactory objectCreatorArgumentsFactory)
		{
			_objectCreatorArgumentsFactory = objectCreatorArgumentsFactory;
		}

		protected override bool CanProcess(CreateObjectArgs args) => base.CanProcess(args) && args.Creators.Any();

		protected override void Execute(CreateObjectArgs args)
		{
			args.Object = CreateObjectFromArguments(args);
		}

		protected virtual object CreateObjectFromArguments(CreateObjectArgs args)
		{
			ObjectCreatorArguments arguments = CreateObjectCreatorArguments(args);
			if (arguments == null)
			{
				return null;
			}

			foreach (IObjectCreator objectCreator in args.Creators)
			{
				object result = CreateObjectFromArguments(objectCreator, arguments);
				if (result != null)
				{
					return result;
				}
			}

			return null;
		}

		protected virtual ObjectCreatorArguments CreateObjectCreatorArguments(CreateObjectArgs args) => _objectCreatorArgumentsFactory.CreateObjectCreatorArguments(args);

		protected virtual object CreateObjectFromArguments(IObjectCreator objectCreator, ObjectCreatorArguments arguments) => objectCreator.Resolve(arguments);
	}
}

The above class just iterates over the IObjectCreator collection on the CreateObjectArgs instance, and tries to create an object using each. The IObjectCreatorArgumentsFactory instance assists in creating the ObjectCreatorArguments instance from the CreateObjectArgs instance so it can make such calls on each IObjectCreator instance.

If an object is created from one them, it just uses that and stops the iteration.

It’s probably a good idea to only cache Types when an object was actually created from the Type. The following interface is for a processor that sets a ITypeCacher on the CreateObjectArgs instance — this class will add the Type to a cache (perhaps in a bank somewhere on the Cayman Islands? 😉 ):

namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.SetTypeCacherProcessor
{
	public interface ISetTypeCacher
	{
		void Process(CreateObjectArgs args);
	}
}

Here’s the implementation class for the interface above:

using Sandbox.Foundation.ObjectResolution.Services.Cachers;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.SetTypeCacherProcessor
{
	public class SetTypeCacher : ResolveProcessor<CreateObjectArgs>, ISetTypeCacher
	{
		private readonly ITypeCacher _typeCacher;

		public SetTypeCacher(ITypeCacher typeCacher)
		{
			_typeCacher = typeCacher;
		}

		protected override bool CanProcess(CreateObjectArgs args) => base.CanProcess(args) && args.UseTypeCache && args.TypeCacher == null;

		protected override void Execute(CreateObjectArgs args) => args.TypeCacher = _typeCacher;
	}
}

It’s just setting the injected ITypeCacher — the implementation class is defined further up in this post — on the CreateObjectArgs instance.

Now, we need to use the ITypeCacher to cache the type. The following interface is for a processor class delegates to the ITypeCacher instance to cache the Type:

namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.CacheTypeProcessor
{
	public interface ICacheType
	{
		void Process(CreateObjectArgs args);
	}
}

Here is the process class which implements the interface above:

namespace Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.CacheTypeProcessor
{
	public class CacheType : ResolveProcessor<CreateObjectArgs>, ICacheType
	{
		protected override bool CanProcess(CreateObjectArgs args) => base.CanProcess(args) && !string.IsNullOrWhiteSpace(args.TypeName) && args.Type != null && args.UseTypeCache && args.TypeCacher != null;

		protected override void Execute(CreateObjectArgs args) => AddTypeToCache(args);

		protected virtual void AddTypeToCache(CreateObjectArgs args) => args.TypeCacher.AddTypeToCache(args.TypeName, args.Type);
	}
}

It should be self-explanatory what’s happening here. If not, please drop a comment below.

Now, we need a service class that wraps this new pipeline. I created the following settings class for that service:

namespace Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectCreators
{
	public class ObjectCreatorServiceSettings
	{
		public string CreateObjectPipelineName { get; set; }
	}
}

An instance of this class is created by the Sitecore Configuration Factory just as the other ones in this post are.

I then defined the following interface for the service class that will wrap this new pipeline — it’s just another IObjectCreator:

namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectCreators
{
	public interface IObjectCreatorService : IObjectCreator
	{
	}
}

This class implements the interface above:

using Sitecore.Abstractions;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectCreators;
using Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject;
using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectCreators;

namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectCreators
{
	public class ObjectCreatorService : PipelineObjectResolver<ObjectCreatorArguments, CreateObjectArgs, object>, IObjectCreatorService
	{
		private readonly ObjectCreatorServiceSettings _settings;
		private readonly IObjectCreatorArgumentsFactory _objectCreatorArgumentsFactory;

		public ObjectCreatorService(ObjectCreatorServiceSettings settings, IObjectCreatorArgumentsFactory objectCreatorArgumentsFactory, BaseCorePipelineManager corePipelineManager)
			: base(corePipelineManager)
		{
			_settings = settings;
			_objectCreatorArgumentsFactory = objectCreatorArgumentsFactory;
		}

		protected override object GetObject(CreateObjectArgs args)
		{
			return args.Object;
		}

		protected override CreateObjectArgs CreatePipelineArgs(ObjectCreatorArguments arguments) => _objectCreatorArgumentsFactory.CreateCreateObjectArgs(arguments);

		protected override string GetPipelineName() => _settings.CreateObjectPipelineName;
	}
}

I’m not going to go into details on the above as you have seen this pattern further above in this post.

<resolveObject />

Now we need a way to glue together all pipelines created above. The following PipelineArgs class is for — yet another pipeline 😉 — that glues everything together:

using System;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;
using Sitecore.Pipelines;

using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectCreators;
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectLocators;
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject
{
	public class ResolveObjectArgs : PipelineArgs
	{
		public Database Database { get; set; }

		public string ItemPath { get; set; }

		public Language Language { get; set; }

		public Item Item { get; set; }

		public string TypeFieldName { get; set; }

		public string TypeName { get; set; }

		public ITypeResolver TypeResolver { get; set; }

		public Type Type { get; set; }

		public IObjectLocator ObjectLocator;

		public bool FoundInContainer { get; set; }

		public IObjectCreator ObjectCreator;

		public bool UseTypeCache { get; set; }

		public object[] ObjectCreationParameters { get; set; }

		public object Object { get; set; }
	}
}

I also created the following class for the service class that will wrap this new pipeline:

using System;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;

namespace Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectResolvers
{
	public class ObjectResolverArguments
	{
		public Database Database { get; set; }

		public Language Language { get; set; }

		public string ItemPath { get; set; }

		public Item Item { get; set; }

		public string TypeFieldName { get; set; }

		public string TypeName { get; set; }

		public Type Type { get; set; }

		public bool UseTypeCache { get; set; }

		public object[] ObjectCreationParameters { get; set; }
	}
}

I bet you are guessing that I’m going to create another factory for these two classes above. Yep, you are correct. Here is the interface for that factory:

using System;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectResolvers;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject;

namespace Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectResolvers
{
	public interface IObjectResolverArgumentsFactory
	{
		ObjectResolverArguments CreateObjectResolverArguments(Database database, string itemPath, string typeFieldName, Language language, object[] objectCreationParameters);

		ObjectResolverArguments CreateObjectResolverArguments(Item item, string typeFieldName, bool useTypeCache, object[] objectCreationParameters);

		ResolveObjectArgs CreateResolveObjectArgs(ObjectResolverArguments arguments);

		ResolveObjectArgs CreateResolveObjectArgs(Database database = null, string itemPath = null, Language language = null, Item item = null, string typeFieldName = null, string typeName = null, Type type = null, bool useTypeCache = false, object[] objectCreationParameters = null);

	}
}

The following class implements the factory interface above:

using System;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectResolvers;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject;

namespace Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectResolvers
{
	public class ObjectResolverArgumentsFactory : IObjectResolverArgumentsFactory
	{
		public ObjectResolverArguments CreateObjectResolverArguments(Database database, string itemPath, string typeFieldName, Language language, object[] objectCreationParameters)
		{
			return new ObjectResolverArguments
			{
				Database = database,
				ItemPath = itemPath,
				TypeFieldName = typeFieldName,
				Language = language,
				ObjectCreationParameters = objectCreationParameters
			};
		}

		public ObjectResolverArguments CreateObjectResolverArguments(Item item, string typeFieldName, bool useTypeCache, object[] objectCreationParameters)
		{
			return new ObjectResolverArguments
			{
				Item = item,
				TypeFieldName = typeFieldName,
				UseTypeCache = useTypeCache,
				ObjectCreationParameters = objectCreationParameters
			};
		}

		public ResolveObjectArgs CreateResolveObjectArgs(ObjectResolverArguments arguments)
		{
			if (arguments == null)
			{
				return null;
			}

			return CreateResolveObjectArgs(arguments.Database, arguments.ItemPath, arguments.Language, arguments.Item, arguments.TypeFieldName, arguments.TypeName, arguments.Type, arguments.UseTypeCache, arguments.ObjectCreationParameters);
		}

		public ResolveObjectArgs CreateResolveObjectArgs(Item item, string typeFieldName, object[] objectCreationParameters)
		{
			return new ResolveObjectArgs
			{
				Item = item,
				TypeFieldName = typeFieldName,
				ObjectCreationParameters = objectCreationParameters
			};
		}

		public ResolveObjectArgs CreateResolveObjectArgs(Database database = null, string itemPath = null, Language language = null, Item item = null, string typeFieldName = null, string typeName = null, Type type = null, bool useTypeCache = false, object[] objectCreationParameters = null)
		{
			return new ResolveObjectArgs
			{
				Database = database,
				ItemPath = itemPath,
				Language = language,
				Item = item,
				TypeFieldName = typeFieldName,
				TypeName = typeName,
				Type= type,
				UseTypeCache = useTypeCache,
				ObjectCreationParameters = objectCreationParameters
			};
		}
	}
}

The following interface is for a processor class that sets the ITypeResolver on the ResolveObjectArgs instance:

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject
{
	public interface ISetTypeResolver
	{
		void Process(ResolveObjectArgs args);
	}
}

Here’s its implementation class:

using Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject
{
	public class SetTypeResolver : ResolveProcessor<ResolveObjectArgs>, ISetTypeResolver
	{
		private readonly ITypeResolverService _typeResolverService;

		public SetTypeResolver(ITypeResolverService typeResolverService)
		{
			_typeResolverService = typeResolverService;
		}

		protected override bool CanProcess(ResolveObjectArgs args) => base.CanProcess(args) && args.TypeResolver == null;

		protected override void Execute(ResolveObjectArgs args)
		{
			args.TypeResolver = GetTypeResolver();
		}

		protected virtual ITypeResolver GetTypeResolver() => _typeResolverService;
	}
}

We have already seen this twice, so I won’t discuss it again. 😉

Next, we need to resolve the type. The following interface is for a processor class that does that type resolution:

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.ResolveTypeProcessor
{
	public interface IResolveType
	{
		void Process(ResolveObjectArgs args);
	}
}

Here’s the class that implements the interface above:

using System;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers;
using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.TypeResolvers;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.ResolveTypeProcessor
{
	public class ResolveType : ResolveProcessor<ResolveObjectArgs>, IResolveType
	{
		private readonly ITypeResolverArgumentsFactory _typeResolverArgumentsFactory;

		public ResolveType(ITypeResolverArgumentsFactory typeResolverArgumentsFactory)
		{
			_typeResolverArgumentsFactory = typeResolverArgumentsFactory;
		}

		protected override bool CanProcess(ResolveObjectArgs args) => base.CanProcess(args) && args.Type == null && args.TypeResolver != null;

		protected override void Execute(ResolveObjectArgs args)
		{
			args.Type = Resolve(args);
		}

		protected virtual Type Resolve(ResolveObjectArgs args)
		{
			TypeResolverArguments arguments = CreateTypeResolverArguments(args);
			if (arguments == null)
			{
				return null;
			}

			return args.TypeResolver.Resolve(arguments);
		}

		protected virtual TypeResolverArguments CreateTypeResolverArguments(ResolveObjectArgs args) => _typeResolverArgumentsFactory.CreateTypeResolverArguments(args);
	}
}

I’m also not going to discuss this as I’ve done this somewhere up above. 😉

Now we need a processor to “locate” objects in the Sitecore IoC container. The following interface is for a processor class that does just that:

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.SetObjectLocatorProcessor
{
	public interface ISetObjectLocator
	{
		void Process(ResolveObjectArgs args);
	}
}

The following class implements the interface above:

using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectLocators;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.SetObjectLocatorProcessor
{
	public class SetObjectLocator : ResolveProcessor<ResolveObjectArgs>, ISetObjectLocator
	{
		private readonly IObjectLocatorService _objectLocatorService;

		public SetObjectLocator(IObjectLocatorService objectLocatorService)
		{
			_objectLocatorService = objectLocatorService;
		}

		protected override bool CanProcess(ResolveObjectArgs args) => base.CanProcess(args) && args.ObjectLocator == null;

		protected override void Execute(ResolveObjectArgs args) => args.ObjectLocator = GetObjectLocator();

		protected virtual IObjectLocator GetObjectLocator() => _objectLocatorService;
	}
}

The class above just sets the IObjectLocatorService — this is the service class which wraps the <locateObject /> pipeline defined further up in this post — on the ResolveObjectArgs instance.

I then created the following interface to delegate to the ObjectLocator property on the ResolveObjectArgs to “locate” the object:

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.LocateObjectProcessor
{
	public interface ILocateObject
	{
		void Process(ResolveObjectArgs args);
	}
}

And here’s the class that implements this interface above:

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectLocators;
using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectLocators;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.LocateObjectProcessor
{
	public class LocateObject : ResolveProcessor<ResolveObjectArgs>, ILocateObject
	{
		private readonly IObjectLocatorArgumentsFactory _objectLocatorArgumentsFactory;

		public LocateObject(IObjectLocatorArgumentsFactory objectLocatorArgumentsFactory)
		{
			_objectLocatorArgumentsFactory = objectLocatorArgumentsFactory;
		}

		protected override bool CanProcess(ResolveObjectArgs args) => base.CanProcess(args) && args.Object == null && args.ObjectLocator != null;

		protected override void Execute(ResolveObjectArgs args)
		{
			args.Object = Locate(args);
			args.FoundInContainer = args.Object != null;
			if (!args.FoundInContainer)
			{
				return;
			}

			AbortPipeline(args);
		}

		protected virtual object Locate(ResolveObjectArgs args) => args.ObjectLocator.Resolve(CreateObjectLocatorArguments(args));

		protected virtual ObjectLocatorArguments CreateObjectLocatorArguments(ResolveObjectArgs args) => _objectLocatorArgumentsFactory.CreateObjectLocatorArguments(args);
	}
}

The above class just tries to “locate” the object using the ObjectLocator set on the ResolveObjectArgs instance.

In the event we can’t find the object via the IObjectLocator, we should create the object instead. I created the following interface to set an IObjectCreator instance on the ResolveObjectArgs instance:

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.SetObjectCreatorProcessor
{
	public interface ISetObjectCreator
	{
		void Process(ResolveObjectArgs args);
	}
}

And here’s its implementation class:

using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectCreators;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.SetObjectCreatorProcessor
{
	public class SetObjectCreator : ResolveProcessor<ResolveObjectArgs>, ISetObjectCreator
	{
		private readonly IObjectCreatorService _objectCreatorService;

		public SetObjectCreator(IObjectCreatorService objectCreatorService)
		{
			_objectCreatorService = objectCreatorService;
		}

		protected override bool CanProcess(ResolveObjectArgs args) => base.CanProcess(args) && args.ObjectCreator == null;

		protected override void Execute(ResolveObjectArgs args) => args.ObjectCreator = GetObjectCreator();

		protected virtual IObjectCreator GetObjectCreator() => _objectCreatorService;
	}
}

The class above just sets the IObjectCreatorService — this is the service class which wraps the <createObject /> pipeline defined further up in this post — on the ResolveObjectArgs instance.

Next, we need to delegate to this IObjectCreator to create the object. The following interface is for a class that creates objects:

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.CreateObjectProcessor
{
	public interface ICreateObject
	{
		void Process(ResolveObjectArgs args);
	}
}

And here’s the implementation of the interface above:

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectCreators;
using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectCreators;

namespace Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.CreateObjectProcessor
{
	public class CreateObject : ResolveProcessor<ResolveObjectArgs>, ICreateObject
	{
		private readonly IObjectCreatorArgumentsFactory _objectCreatorArgumentsFactory;

		public CreateObject(IObjectCreatorArgumentsFactory objectCreatorArgumentsFactory)
		{
			_objectCreatorArgumentsFactory = objectCreatorArgumentsFactory;
		}
		protected override bool CanProcess(ResolveObjectArgs args) => base.CanProcess(args) && args.Object == null && args.ObjectCreator != null;

		protected override void Execute(ResolveObjectArgs args) => args.Object = Resolve(args);

		protected virtual object Resolve(ResolveObjectArgs args) => args.ObjectCreator.Resolve(CreateObjectCreatorArguments(args));

		protected virtual ObjectCreatorArguments CreateObjectCreatorArguments(ResolveObjectArgs args) => _objectCreatorArgumentsFactory.CreateObjectCreatorArguments(args);
	}
}

The class above just delegates to the IObjectCreator instance of the ResolveObjectArgs instance to create the object

Like the other 4 pipelines — holy cannoli, Batman, there are 5 pipelines in total in this solution! — I created a service class that wraps this new pipeline. The following class serves as a settings class for this service:

namespace Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectResolvers
{
	public class ObjectResolverServiceSettings
	{
		public string ResolveObjectPipelineName { get; set; }

		public bool UseTypeCache { get; set; }
	}
}

An instance of the above is created by the Sitecore Configuration Factory.

The following interface defines a family of classes that “resolve” objects. Unlike the other pipelines, we will only have one class that implements this interface:

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectResolvers;

namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectResolvers
{
	public interface IObjectResolver
	{
		TObject Resolve<TObject>(ObjectResolverArguments arguments) where TObject : class;

		object Resolve(ObjectResolverArguments arguments);
	}
}

The following class implements the interface above:

using Sitecore.Abstractions;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectResolvers;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject;
using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectResolvers;

namespace Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectResolvers
{
	public class ObjectResolverService : PipelineObjectResolver<ObjectResolverArguments, ResolveObjectArgs, object>, IObjectResolver
	{
		private readonly ObjectResolverServiceSettings _settings;
		private readonly IObjectResolverArgumentsFactory _objectResolverArgumentsFactory;

		public ObjectResolverService(ObjectResolverServiceSettings settings, IObjectResolverArgumentsFactory objectResolverArgumentsFactory, BaseCorePipelineManager corePipelineManager)
			: base(corePipelineManager)
		{
			_settings = settings;
			_objectResolverArgumentsFactory = objectResolverArgumentsFactory;
		}

		public TObject Resolve<TObject>(ObjectResolverArguments arguments) where TObject : class
		{
			return Resolve(arguments) as TObject;
		}

		protected override object GetObject(ResolveObjectArgs args)
		{
			return args.Object;
		}

		protected override ResolveObjectArgs CreatePipelineArgs(ObjectResolverArguments arguments) => _objectResolverArgumentsFactory.CreateResolveObjectArgs(arguments);

		protected override string GetPipelineName() => _settings.ResolveObjectPipelineName;
	}
}

I then register every single thing above — and I mean EVERYTHING — in the Sitecore IoC via the following IServicesConfigurator class:

using System;

using Microsoft.Extensions.DependencyInjection;

using Sitecore.Abstractions;
using Sitecore.DependencyInjection;

using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ItemResolvers;
using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectCreators;
using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectLocators;
using Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectResolvers;
using Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers;
using Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.AddDefaultObjectCreatorProcessor;
using Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.CacheTypeProcessor;
using Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.CreateObjectProcessor;
using Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.ResolveTypeProcessor;
using Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.SetTypeCacherProcessor;
using Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.SetTypeResolverProcessor;
using Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.AddDefaultObjectLocatorProcessor;
using Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.LocateObjectProcessor;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem.AddDefaultItemResolverProcessor;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem.ResolveItemProcessor;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.SetObjectCreatorProcessor;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.SetObjectLocatorProcessor;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.AddDefaultTypeResolverProcessor;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.SetItemResolverProcessor;
using Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.SetTypeNameProcessor;
using Sandbox.Foundation.ObjectResolution.Services.Cachers;
using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ItemResolvers;
using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectCreators;
using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectLocators;
using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.ObjectResolvers;
using Sandbox.Foundation.ObjectResolution.Services.Factories.Resolvers.TypeResolvers;
using Sandbox.Foundation.ObjectResolution.Services.Reflection;
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ItemResolvers;
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectCreators;
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectLocators;
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.ObjectResolvers;
using Sandbox.Foundation.ObjectResolution.Services.Resolvers.TypeResolvers;

namespace Sandbox.Foundation.ObjectResolution
{
	public class ObjectResolutionConfigurator : IServicesConfigurator
	{
		public void Configure(IServiceCollection serviceCollection)
		{
			ConfigureCachers(serviceCollection);
			ConfigureFactories(serviceCollection);
			ConfigureItemResolvers(serviceCollection);
			ConfigureTypeResolvers(serviceCollection);
			ConfigureObjectCreators(serviceCollection);
			ConfigureObjectLocators(serviceCollection);
			ConfigureObjectResolvers(serviceCollection);
			ConfigureResolveItemPipelineProcessors(serviceCollection);
			ConfigureResolveTypePipelineProcessors(serviceCollection);
			ConfigureLocateObjectPipelineProcessors(serviceCollection);
			ConfigureCreateObjectPipelineProcessors(serviceCollection);
			ConfigureResolveObjectPipelineProcessors(serviceCollection);
			ConfigureOtherServices(serviceCollection);
		}

		private void ConfigureCachers(IServiceCollection serviceCollection)
		{
			serviceCollection.AddSingleton<ITypeCacher, TypeCacher>();
		}

		private void ConfigureFactories(IServiceCollection serviceCollection)
		{
			serviceCollection.AddSingleton<IItemResolverArgumentsFactory, ItemResolverArgumentsFactory>();
			serviceCollection.AddSingleton<ITypeResolverArgumentsFactory, TypeResolverArgumentsFactory>();
			serviceCollection.AddSingleton<IObjectLocatorArgumentsFactory, ObjectLocatorArgumentsFactory>();
			serviceCollection.AddSingleton<IObjectCreatorArgumentsFactory, ObjectCreatorArgumentsFactory>();
			serviceCollection.AddSingleton<IObjectResolverArgumentsFactory, ObjectResolverArgumentsFactory>();
		}

		private void ConfigureItemResolvers(IServiceCollection serviceCollection)
		{
			serviceCollection.AddSingleton<IDatabaseItemResolver, DatabaseItemResolver>();
			serviceCollection.AddSingleton(GetItemResolverServiceSetting);
			serviceCollection.AddSingleton<IItemResolverService, ItemResolverService>();
		}

		private ItemResolverServiceSettings GetItemResolverServiceSetting(IServiceProvider provider)
		{
			return CreateConfigObject<ItemResolverServiceSettings>(provider, "moduleSettings/foundation/objectResolution/itemResolverServiceSettings");
		}

		private void ConfigureTypeResolvers(IServiceCollection serviceCollection)
		{
			serviceCollection.AddSingleton<IReflectionTypeResolver, ReflectionTypeResolver>();
			serviceCollection.AddSingleton(GetTypeResolverServiceSettings);
			serviceCollection.AddSingleton<ITypeResolverService, TypeResolverService>();
		}

		private TypeResolverServiceSettings GetTypeResolverServiceSettings(IServiceProvider provider)
		{
			return CreateConfigObject<TypeResolverServiceSettings>(provider, "moduleSettings/foundation/objectResolution/typeResolverServiceSettings");
		}

		private void ConfigureObjectCreators(IServiceCollection serviceCollection)
		{
			serviceCollection.AddSingleton<IReflectionObjectCreator, ReflectionObjectCreator>();
			serviceCollection.AddSingleton(GetObjectCreatorServiceSettings);
			serviceCollection.AddSingleton<IObjectCreatorService, ObjectCreatorService>();
		}

		private ObjectCreatorServiceSettings GetObjectCreatorServiceSettings(IServiceProvider provider)
		{
			return CreateConfigObject<ObjectCreatorServiceSettings>(provider, "moduleSettings/foundation/objectResolution/objectCreatorServiceSettings");
		}

		private void ConfigureObjectLocators(IServiceCollection serviceCollection)
		{
			serviceCollection.AddSingleton<IServiceProviderLocator, ServiceProviderLocator>();
			serviceCollection.AddSingleton(GetObjectLocatorServiceSettings);
			serviceCollection.AddSingleton<IObjectLocatorService, ObjectLocatorService>();
		}

		private ObjectLocatorServiceSettings GetObjectLocatorServiceSettings(IServiceProvider provider)
		{
			return CreateConfigObject<ObjectLocatorServiceSettings>(provider, "moduleSettings/foundation/objectResolution/objectLocatorServiceSettings");
		}

		private void ConfigureObjectResolvers(IServiceCollection serviceCollection)
		{
			serviceCollection.AddSingleton<IReflectionObjectCreator, ReflectionObjectCreator>();
			serviceCollection.AddSingleton(GetObjectResolverServiceSettings);
			serviceCollection.AddSingleton<IObjectResolver, ObjectResolverService>();
		}

		private ObjectResolverServiceSettings GetObjectResolverServiceSettings(IServiceProvider provider)
		{
			return CreateConfigObject<ObjectResolverServiceSettings>(provider, "moduleSettings/foundation/objectResolution/objectResolverServiceSettings");
		}

		private void ConfigureResolveItemPipelineProcessors(IServiceCollection serviceCollection)
		{
			serviceCollection.AddSingleton<IAddDefaultItemResolver, AddDefaultItemResolver>();
			serviceCollection.AddSingleton<IResolveItem, ResolveItem>();
		}

		private void ConfigureResolveTypePipelineProcessors(IServiceCollection serviceCollection)
		{
			serviceCollection.AddSingleton<ISetItemResolver, SetItemResolver>();
			serviceCollection.AddSingleton<Pipelines.ResolveType.ResolveTypeProcessor.IResolveItem, Pipelines.ResolveType.ResolveTypeProcessor.ResolveItem>();
			serviceCollection.AddSingleton<ISetTypeName, SetTypeName>();
			serviceCollection.AddSingleton<IAddDefaultTypeResolver, AddDefaultTypeResolver>();
			serviceCollection.AddSingleton<Pipelines.ResolveType.SetTypeCacherProcessor.ISetTypeCacher, Pipelines.ResolveType.SetTypeCacherProcessor.SetTypeCacher>();
			serviceCollection.AddSingleton<Pipelines.ResolveType.ResolveTypeProcessor.IResolveType, Pipelines.ResolveType.ResolveTypeProcessor.ResolveType>();
		}

		private void ConfigureLocateObjectPipelineProcessors(IServiceCollection serviceCollection)
		{
			serviceCollection.AddSingleton<Pipelines.LocateObject.SetTypeResolverProcessor.ISetTypeResolver, Pipelines.LocateObject.SetTypeResolverProcessor.SetTypeResolver>();
			serviceCollection.AddSingleton<Pipelines.LocateObject.ResolveTypeProcessor.IResolveType, Pipelines.LocateObject.ResolveTypeProcessor.ResolveType>();
			serviceCollection.AddSingleton<IAddDefaultObjectLocator, AddDefaultObjectLocator>();
			serviceCollection.AddSingleton<ILocateObject, LocateObject>();
		}

		private void ConfigureCreateObjectPipelineProcessors(IServiceCollection serviceCollection)
		{
			serviceCollection.AddSingleton<ISetTypeResolver, SetTypeResolver>();
			serviceCollection.AddSingleton<IResolveType, ResolveType>();
			serviceCollection.AddSingleton<IAddDefaultObjectCreator, AddDefaultObjectCreator>();
			serviceCollection.AddSingleton<ICreateObject, CreateObject>();
			serviceCollection.AddSingleton<ISetTypeCacher, SetTypeCacher>();
			serviceCollection.AddSingleton<ICacheType, CacheType>();
		}

		private void ConfigureResolveObjectPipelineProcessors(IServiceCollection serviceCollection)
		{
			serviceCollection.AddSingleton<Pipelines.ResolveObject.ISetTypeResolver, Pipelines.ResolveObject.SetTypeResolver>();
			serviceCollection.AddSingleton<Pipelines.ResolveObject.ResolveTypeProcessor.IResolveType, Pipelines.ResolveObject.ResolveTypeProcessor.ResolveType>();
			serviceCollection.AddSingleton<ISetObjectLocator, SetObjectLocator>();
			serviceCollection.AddSingleton<Pipelines.ResolveObject.LocateObjectProcessor.ILocateObject, Pipelines.ResolveObject.LocateObjectProcessor.LocateObject>();
			serviceCollection.AddSingleton<ISetObjectCreator, SetObjectCreator>();
			serviceCollection.AddSingleton<Pipelines.ResolveObject.CreateObjectProcessor.ICreateObject, Pipelines.ResolveObject.CreateObjectProcessor.CreateObject>();
		}

		private void ConfigureOtherServices(IServiceCollection serviceCollection)
		{
			serviceCollection.AddSingleton<IReflectionUtilService, ReflectionUtilService>();
		}

		private TConfigObject CreateConfigObject<TConfigObject>(IServiceProvider provider, string path) where TConfigObject : class
		{
			BaseFactory factory = GetService<BaseFactory>(provider);
			return factory.CreateObject(path, true) as TConfigObject;
		}

		private TService GetService<TService>(IServiceProvider provider)
		{
			return provider.GetService<TService>();
		}
	}
}

Finally, I strung all the pieces together using the following Sitecore patch config file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
	<sitecore>
		<services>
			<configurator type="Sandbox.Foundation.ObjectResolution.ObjectResolutionConfigurator, Sandbox.Foundation.ObjectResolution" />
		</services>

		<pipelines>
			<resolveItem>
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem.AddDefaultItemResolverProcessor.IAddDefaultItemResolver, Sandbox.Foundation.ObjectResolution" resolve="true" />
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveItem.ResolveItemProcessor.IResolveItem, Sandbox.Foundation.ObjectResolution" resolve="true" />
			</resolveItem>
			<resolveType>
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.SetItemResolverProcessor.ISetItemResolver, Sandbox.Foundation.ObjectResolution" resolve="true" />
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.ResolveTypeProcessor.IResolveItem, Sandbox.Foundation.ObjectResolution" resolve="true" />
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.SetTypeNameProcessor.ISetTypeName, Sandbox.Foundation.ObjectResolution" resolve="true" />
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.AddDefaultTypeResolverProcessor.IAddDefaultTypeResolver, Sandbox.Foundation.ObjectResolution" resolve="true" />
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.SetTypeCacherProcessor.ISetTypeCacher, Sandbox.Foundation.ObjectResolution" resolve="true" />
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveType.ResolveTypeProcessor.IResolveType, Sandbox.Foundation.ObjectResolution" resolve="true" />
			</resolveType>
			<locateObject>
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.SetTypeResolverProcessor.ISetTypeResolver, Sandbox.Foundation.ObjectResolution" resolve="true" />
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.ResolveTypeProcessor.IResolveType, Sandbox.Foundation.ObjectResolution" resolve="true" />
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.AddDefaultObjectLocatorProcessor.IAddDefaultObjectLocator, Sandbox.Foundation.ObjectResolution" resolve="true" />
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.LocateObject.LocateObjectProcessor.ILocateObject, Sandbox.Foundation.ObjectResolution" resolve="true" />
			</locateObject>
			<createObject>
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.SetTypeResolverProcessor.ISetTypeResolver, Sandbox.Foundation.ObjectResolution" resolve="true" />
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.ResolveTypeProcessor.IResolveType, Sandbox.Foundation.ObjectResolution" resolve="true" />
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.AddDefaultObjectCreatorProcessor.IAddDefaultObjectCreator, Sandbox.Foundation.ObjectResolution" resolve="true" />
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.CreateObjectProcessor.ICreateObject, Sandbox.Foundation.ObjectResolution" resolve="true" />
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.SetTypeCacherProcessor.ISetTypeCacher, Sandbox.Foundation.ObjectResolution" resolve="true" />
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.CreateObject.CacheTypeProcessor.ICacheType, Sandbox.Foundation.ObjectResolution" resolve="true" />
			</createObject>
			<resolveObject>
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.ISetTypeResolver, Sandbox.Foundation.ObjectResolution" resolve="true" />
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.ResolveTypeProcessor.IResolveType, Sandbox.Foundation.ObjectResolution" resolve="true" />
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.SetObjectLocatorProcessor.ISetObjectLocator, Sandbox.Foundation.ObjectResolution" resolve="true" />
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.LocateObjectProcessor.ILocateObject, Sandbox.Foundation.ObjectResolution" resolve="true" />
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.SetObjectCreatorProcessor.ISetObjectCreator, Sandbox.Foundation.ObjectResolution" resolve="true" />
				<processor type="Sandbox.Foundation.ObjectResolution.Pipelines.ResolveObject.CreateObjectProcessor.ICreateObject, Sandbox.Foundation.ObjectResolution" resolve="true" />
			</resolveObject>
		</pipelines>

		<moduleSettings>
			<foundation>
				<objectResolution>
					<itemResolverServiceSettings type="Sandbox.Foundation.ObjectResolution.Models.Resolvers.ItemResolvers.ItemResolverServiceSettings, Sandbox.Foundation.ObjectResolution" singleInstance="true">
						<ResolveItemPipelineName>resolveItem</ResolveItemPipelineName>
					</itemResolverServiceSettings>
					<typeResolverServiceSettings type="Sandbox.Foundation.ObjectResolution.Models.Resolvers.TypeResolvers.TypeResolverServiceSettings, Sandbox.Foundation.ObjectResolution" singleInstance="true">
						<ResolveTypePipelineName>resolveType</ResolveTypePipelineName>
					</typeResolverServiceSettings>
					<objectLocatorServiceSettings type="Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectLocators.ObjectLocatorServiceSettings, Sandbox.Foundation.ObjectResolution" singleInstance="true">
						<LocateObjectPipelineName>locateObject</LocateObjectPipelineName>
					</objectLocatorServiceSettings>
					<objectCreatorServiceSettings type="Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectCreators.ObjectCreatorServiceSettings, Sandbox.Foundation.ObjectResolution" singleInstance="true">
						<CreateObjectPipelineName>createObject</CreateObjectPipelineName>
					</objectCreatorServiceSettings>
					<objectResolverServiceSettings type="Sandbox.Foundation.ObjectResolution.Models.Resolvers.ObjectResolvers.ObjectResolverServiceSettings, Sandbox.Foundation.ObjectResolution" singleInstance="true">
						<ResolveObjectPipelineName>resolveObject</ResolveObjectPipelineName>
						<UseTypeCache>true</UseTypeCache>
					</objectResolverServiceSettings>
				</objectResolution>
			</foundation>
		</moduleSettings>
</sitecore>
</configuration>

In my next post, I will be using the entire system above for resolving custom Forms Submit Actions from the Sitecore IoC container. Stay tuned for that post.

If you have made it this far, hats off to you. 😉

Sorry for throwing so much at you, but that’s what I do. 😉

On closing, I would like to mention that the system above could be used for resolving types from the Sitecore IoC container for WFFM but this is something that I will not investigate. If you happen to get this to work on WFFM, please share in a comment below.

Until next time, keep on Sitecoring.

Advertisements

Write Sitecore Experience Forms Log Entries to a Custom SQL Server Database Table

Not long after I wrote the code for my last post, I continued exploring ways of changing service classes in Sitecore Experience Forms.

One thing that popped out when continuing on this quest was Sitecore.ExperienceForms.Diagnostics.ILogger. I immediately thought “I just wrote code for retrieving Forms configuration settings from a SQL Server database table, why not create a new ILogger service class for storing log entries in a custom SQL table?”

Well, that’s what I did, and the code in this post captures how I went about doing that.

You might be asking “Mike, you know you can just use a SQL appender in log4net, right?”

Well, I certainly could have but what fun would that have been?

Anyways, let’s get started.

We first need a class that represents a log entry. I created the following POCO class to serve that purpose:

using System;

namespace Sandbox.Foundation.Forms.Models.Logging
{
	public class LogEntry
	{
		public Exception Exception { get; set; }

		public string LogEntryType { get; set; }

		public string LogMessage { get; set; }

		public string Message { get; set; }

		public object Owner { get; set; }

		public DateTime CreatedDate { get; set; }
	}
}

Since I hate calling the “new” keyword when creating new instances of classes, I chose to create a factory class. The following interface will be for instances of classes that create LogEntry instances:

using System;

using Sandbox.Foundation.Forms.Models.Logging;

namespace Sandbox.Foundation.Forms.Services.Factories.Diagnostics
{
	public interface ILogEntryFactory
	{
		LogEntry CreateLogEntry(string logEntryType, string message, Exception exception, Type ownerType, DateTime createdDate);
		
		LogEntry CreateLogEntry(string logEntryType, string message, Exception exception, object owner, DateTime createdDate);

		LogEntry CreateLogEntry(string logEntryType, string message, Type ownerType, DateTime createdDate);

		LogEntry CreateLogEntry(string logEntryType, string message, object owner, DateTime createdDate);
	}
}

Well, we can’t do much with just an interface. The following class implements the interface above. It creates an instance of LogEntry with the passed parameters to all methods (assuming the required parameters are passed with the proper values on them):

using System;

using Sandbox.Foundation.Forms.Models.Logging;

namespace Sandbox.Foundation.Forms.Services.Factories.Diagnostics
{
	public class LogEntryFactory : ILogEntryFactory
	{
		public LogEntry CreateLogEntry(string logEntryType, string message, Exception exception, Type ownerType, DateTime createdDate)
		{
			return CreateLogEntry(logEntryType, message, exception, ownerType, createdDate);
		}

		public LogEntry CreateLogEntry(string logEntryType, string message, Exception exception, object owner, DateTime createdDate)
		{
			if (!CanCreateLogEntry(logEntryType, message, owner, createdDate))
			{
				return null;
			}

			return new LogEntry
			{
				LogEntryType = logEntryType,
				Message = message,
				Exception = exception,
				Owner = owner,
				CreatedDate = createdDate
			};
		}

		public LogEntry CreateLogEntry(string logEntryType, string message, Type ownerType, DateTime createdDate)
		{
			return CreateLogEntry(logEntryType, message, ownerType, createdDate);
		}

		public LogEntry CreateLogEntry(string logEntryType, string message, object owner, DateTime createdDate)
		{
			if(!CanCreateLogEntry(logEntryType, message, owner, createdDate))
			{
				return null;
			}

			return new LogEntry
			{
				LogEntryType = logEntryType,
				Message = message,
				Owner = owner,
				CreatedDate = createdDate
			};
		}

		protected virtual bool CanCreateLogEntry(string logEntryType, string message, object owner, DateTime createdDate)
		{
			return !string.IsNullOrWhiteSpace(logEntryType)
				&& !string.IsNullOrWhiteSpace(message)
				&& owner != null
				&& createdDate != DateTime.MinValue
				&& createdDate != DateTime.MaxValue;
		}
	}
}

I didn’t want to send LogEntry instances directly to a repository class instance directly, so I created the following class to represent the entities which will ultimately be stored in the database:

using System;

namespace Sandbox.Foundation.Forms.Models.Logging
{
	public class RepositoryLogEntry
	{
		public string LogEntryType { get; set; }

		public string LogMessage { get; set; }

		public DateTime Created { get; set; }
	}
}

As I had done with LogEntry, I created a factory class for it. The difference here is we will be passing an instance of LogEntry to this new factory so we can create a RepositoryLogEntry instance from it.

The following interface is for factories of RepositoryLogEntry:

using System;

using Sandbox.Foundation.Forms.Models.Logging;

namespace Sandbox.Foundation.Forms.Services.Factories.Diagnostics
{
	public interface IRepositoryLogEntryFactory
	{
		RepositoryLogEntry CreateRepositoryLogEntry(LogEntry entry);
		
		RepositoryLogEntry CreateRepositoryLogEntry(string logEntryType, string logMessage, DateTime created);
	}
}

Now that we have the interface ready to go, we need an implementation class for it. The following class does the job:

using System;

using Sandbox.Foundation.Forms.Models.Logging;

namespace Sandbox.Foundation.Forms.Services.Factories.Diagnostics
{
	public class RepositoryLogEntryFactory : IRepositoryLogEntryFactory
	{
		public RepositoryLogEntry CreateRepositoryLogEntry(LogEntry entry)
		{
			return CreateRepositoryLogEntry(entry.LogEntryType, entry.LogMessage, entry.CreatedDate);
		}

		public RepositoryLogEntry CreateRepositoryLogEntry(string logEntryType, string logMessage, DateTime created)
		{
			if (!CanCreateRepositoryLogEntry(logEntryType, logMessage, created))
			{
				return null;
			}

			return new RepositoryLogEntry
			{
				LogEntryType = logEntryType,
				LogMessage = logMessage,
				Created = created
			};
		}

		protected virtual bool CanCreateRepositoryLogEntry(string logEntryType, string logMessage, DateTime created)
		{
			return !string.IsNullOrWhiteSpace(logEntryType)
					&& !string.IsNullOrWhiteSpace(logMessage)
					&& created != DateTime.MinValue
					&& created != DateTime.MaxValue;
		}
	}
}

I’m following a similiar structure here as I had done in the LogEntryFactory class above. The CanCreateRepositoryLogEntry() method ensures required parameters are passed to methods on the class. If they are not, then a null reference is returned to the caller.

Since I hate hardcoding things, I decided to create a service class that gets the newline character. The following interface is for classes that do that:

namespace Sandbox.Foundation.Forms.Services.Environment
{
	public interface IEnvironmentService
	{
		string GetNewLine();
	}
}

This next class implements the interface above:

namespace Sandbox.Foundation.Forms.Services.Environment
{
	public class EnvironmentService : IEnvironmentService
	{
		public string GetNewLine()
		{
			return System.Environment.NewLine;
		}
	}
}

In the class above, I’m taking advantage of stuff build into the .NET library for getting the newline character.

I love when I discover things like this, albeit wish I had found something like this when trying to find an html break string for something I was working on the other day, but I digress (if you know of a way, please let me know in a comment below 😉 ).

The above interface and class might seem out of place in this post but I am using them when formatting messages for the LogEntry instances further down in another service class. Just keep an eye out for it.

Since I loathe hardcoding strings with a passion, I like to hide these away in Sitecore configuration patch files and hydrate a POCO class instance with the values from the aforementioned configuration. The following class is such a POCO settings object for a service class I will discuss further down in the post:

namespace Sandbox.Foundation.Forms.Models.Logging
{
	public class LogEntryServiceSettings
	{
		public string DebugLogEntryType { get; set; }

		public string ErrorLogEntryType { get; set; }

		public string FatalLogEntryType { get; set; }

		public string InfoLogEntryType { get; set; }

		public string WarnLogEntryType { get; set; }

		public string ExceptionPrefix { get; set; }

		public string MessagePrefix { get; set; }

		public string SourcePrefix { get; set; }

		public string NestedExceptionPrefix { get; set; }

		public string LogEntryTimeFormat { get; set; }
	}
}

Okay, so need we need to know what “type” of LogEntry we are dealing with — is it an error or a warning or what? — before sending to a repository to save in the database. I created the following interface for service classes that return back strings for the different LogEntry types, and also generate a log message from the data on properties on the LogEntry instance — this is the message that will end up in the database for the LogEntry:

using Sandbox.Foundation.Forms.Models.Logging;

namespace Sandbox.Foundation.Forms.Services.Diagnostics
{
	public interface ILogEntryService
	{
		string GetDebugLogEntryType();

		string GetErrorLogEntryType();

		string GetFatalLogEntryType();

		string GetInfoLogEntryType();

		string GetWarnLogEntryType();

		string GenerateLogMessage(LogEntry entry);
	}
}

And here is its implementation class:

using System;
using System.Text;

using Sandbox.Foundation.Forms.Models.Logging;
using Sandbox.Foundation.Forms.Services.Environment;

namespace Sandbox.Foundation.Forms.Services.Diagnostics
{
	public class LogEntryService : ILogEntryService
	{
		private readonly string _newLine;
		private readonly LogEntryServiceSettings _logEntryServiceSettings;

		public LogEntryService(IEnvironmentService environmentService, LogEntryServiceSettings logEntryServiceSettings)
		{
			_newLine = GetNewLine(environmentService);
			_logEntryServiceSettings = logEntryServiceSettings;
		}

		protected virtual string GetNewLine(IEnvironmentService environmentService)
		{
			return environmentService.GetNewLine();
		}

		public string GetDebugLogEntryType()
		{
			return _logEntryServiceSettings.DebugLogEntryType;
		}

		public string GetErrorLogEntryType()
		{
			return _logEntryServiceSettings.ErrorLogEntryType;
		}

		public string GetFatalLogEntryType()
		{
			return _logEntryServiceSettings.FatalLogEntryType;
		}

		public string GetInfoLogEntryType()
		{
			return _logEntryServiceSettings.InfoLogEntryType;
		}

		public string GetWarnLogEntryType()
		{
			return _logEntryServiceSettings.WarnLogEntryType;
		}

		public string GenerateLogMessage(LogEntry entry)
		{
			if(!CanGenerateLogMessage(entry))
			{
				return string.Empty;
			}

			string exceptionMessage = GenerateExceptionMessage(entry.Exception);
			if(string.IsNullOrWhiteSpace(exceptionMessage))
			{
				return $"{entry.Message}";
			}

			return $"{entry.Message} {exceptionMessage}";
		}

		protected virtual bool CanGenerateLogMessage(LogEntry entry)
		{
			return entry != null
					&& !string.IsNullOrWhiteSpace(entry.Message)
					&& entry.Owner != null;
		}

		protected virtual string GenerateExceptionMessage(Exception exception)
		{
			if(exception == null)
			{
				return string.Empty;
			}

			StringBuilder messageBuilder = new StringBuilder();
			messageBuilder.Append(_logEntryServiceSettings.ExceptionPrefix).Append(exception.GetType().FullName); ;
			AppendNewLine(messageBuilder);
			messageBuilder.Append(_logEntryServiceSettings.MessagePrefix).Append(exception.Message);
			AppendNewLine(messageBuilder);

			if (!string.IsNullOrWhiteSpace(exception.Source))
			{
				messageBuilder.Append(_logEntryServiceSettings.SourcePrefix).Append(exception.Source);
				AppendNewLine(messageBuilder);
			}

			if(!string.IsNullOrWhiteSpace(exception.StackTrace))
			{
				messageBuilder.Append(exception.StackTrace);
				AppendNewLine(messageBuilder);
			}
			
			if (exception.InnerException != null)
			{
				AppendNewLine(messageBuilder);
				messageBuilder.Append(_logEntryServiceSettings.NestedExceptionPrefix);
				AppendNewLine(messageBuilder, 3);
				messageBuilder.Append(GenerateExceptionMessage(exception.InnerException));
				AppendNewLine(messageBuilder);
			}
			
			return messageBuilder.ToString();
		}

		protected virtual void AppendNewLine(StringBuilder builder, int repeatCount = 1)
		{
			AppendRepeat(builder, _newLine, repeatCount);
		}

		protected virtual void AppendRepeat(StringBuilder builder, string stringToAppend, int repeatCount)
		{
			if (builder == null || string.IsNullOrWhiteSpace(stringToAppend) || repeatCount < 1)
			{
				return;
			}

			for(int i = 0; i < repeatCount; i++)
			{
				builder.Append(stringToAppend);
			}
		}
	}
}

I’m not going to discuss all the code in the above class as it should be self-explanatory.

I do want to point out GenerateLogMessage() will generate one of two strings, depending on whether an Exception was set on the LogEntry instance.

If an Exception was set, we append the Exception details — the GenerateExceptionMessage() method generates a string from the Exception — onto the end of the LogEntry message

If it was not set, we just return the LogEntry message to the caller.

Well, now we need a place to store the log entries. I used the following SQL script to create a new table for storing these:

USE [ExperienceFormsSettings]
GO

CREATE TABLE [dbo].[ExperienceFormsLog](
	[ID] [uniqueidentifier] NOT NULL,
	[LogEntryType] [nvarchar](max) NOT NULL,
	[LogMessage] [nvarchar](max) NOT NULL,
	[Created] [datetime] NOT NULL,
 CONSTRAINT [PK_ExperienceFormsLog] PRIMARY KEY CLUSTERED 
(
	[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

ALTER TABLE [dbo].[ExperienceFormsLog] ADD  DEFAULT (newsequentialid()) FOR [ID]
GO

I also sprinkled some magical database dust onto the table:

😉

Wonderful, we now can move on to the fun bit — actually writing some code to store these entries into the database table created from the SQL script above.

I wrote the following POCO class to represent a SQL command — either a query or statement (it really doesn’t matter as it will support both):

namespace Sandbox.Foundation.Forms.Models.Logging
{
	public class SqlCommand
	{
		public string Sql { get; set; }

		public object[] Parameters { get; set; }
	}
}

I’m sure I could have found something in Sitecore.Kernel.dll that does exactly what the class above does but I couldn’t find such a thing (if you know of such a class, please share in a comment below).

Now we need a settings class for the SQL Logger I am writing further down in this post. As I had done for the LogEntryService class above, this data will be coming from Sitecore configuration:

namespace Sandbox.Foundation.Forms.Models.Logging
{
	public class SqlLoggerSettings
	{
		public string LogPrefix { get; set; }

		public string LogDatabaseConnectionStringName { get; set; }

		public string InsertLogEntrySqlFormat { get; set; }

		public string ConnectionStringNameColumnName { get; set; }

		public string FieldsPrefixColumnName { get; set; }

		public string FieldsIndexNameColumnName { get; set; }

		public int NotFoundOrdinal { get; set; }

		public string LogEntryTypeParameterName { get; set; }

		public string LogMessageParameterName { get; set; }

		public string CreatedParameterName { get; set; }
	}
}

Now the fun part — creating an implementation of Sitecore.ExperienceForms.Diagnostics.ILogger:

using System;

using Sitecore.Abstractions;
using Sitecore.Data.DataProviders.Sql;
using Sitecore.ExperienceForms.Diagnostics;

using Sandbox.Foundation.Forms.Services.Factories;
using Sandbox.Foundation.Forms.Models.Logging;
using Sandbox.Foundation.Forms.Services.Factories.Diagnostics;

namespace Sandbox.Foundation.Forms.Services.Diagnostics
{
	public class SqlLogger : ILogger
	{
		private readonly SqlLoggerSettings _sqlLoggerSettings;
		private readonly BaseSettings _settings;
		private readonly BaseFactory _factory;
		private readonly SqlDataApi _sqlDataApi;
		private readonly ILogEntryFactory _logEntryFactory;
		private readonly ILogEntryService _logEntryService;
		private readonly IRepositoryLogEntryFactory _repositoryLogEntryFactory;

		public SqlLogger(SqlLoggerSettings sqlLoggerSettings, BaseSettings settings, BaseFactory factory, ISqlDataApiFactory sqlDataApiFactory, ILogEntryFactory logEntryFactory, IRepositoryLogEntryFactory repositoryLogEntryFactory, ILogEntryService logEntryService)
		{
			_sqlLoggerSettings = sqlLoggerSettings;
			_settings = settings;
			_factory = factory;
			_sqlDataApi = CreateSqlDataApi(sqlDataApiFactory);
			_logEntryFactory = logEntryFactory;
			_logEntryService = logEntryService;
			_repositoryLogEntryFactory = repositoryLogEntryFactory;
		}

		protected virtual SqlDataApi CreateSqlDataApi(ISqlDataApiFactory sqlDataApiFactory)
		{
			return sqlDataApiFactory.CreateSqlDataApi(GetLogDatabaseConnectionString());
		}

		protected virtual string GetLogDatabaseConnectionString()
		{
			return _settings.GetConnectionString(GetLogDatabaseConnectionStringName());
		}

		protected virtual string GetLogDatabaseConnectionStringName()
		{
			return _sqlLoggerSettings.LogDatabaseConnectionStringName;
		}

		public void Debug(string message)
		{
			Debug(message, GetDefaultOwner());
		}

		public void Debug(string message, object owner)
		{
			SaveLogEntry(CreateLogEntry(GetDebugLogEntryType(), message, owner, GetLogEntryDateTime()));
		}

		protected virtual string GetDebugLogEntryType()
		{
			return _logEntryService.GetDebugLogEntryType();
		}

		public void LogError(string message)
		{
			LogError(message, null, GetDefaultOwner());
		}

		public void LogError(string message, object owner)
		{
			LogError(message, null, owner);
		}

		public void LogError(string message, Exception exception, Type ownerType)
		{
			LogError(message, exception, (object)ownerType);
		}

		public void LogError(string message, Exception exception, object owner)
		{
			SaveLogEntry(CreateLogEntry(GetErrorLogEntryType(), message, exception, owner, GetLogEntryDateTime()));
		}

		protected virtual string GetErrorLogEntryType()
		{
			return _logEntryService.GetErrorLogEntryType();
		}

		public void Fatal(string message)
		{
			Fatal(message, null, GetDefaultOwner());
		}

		public void Fatal(string message, object owner)
		{
			Fatal(message, null, owner);
		}

		public void Fatal(string message, Exception exception, Type ownerType)
		{
			Fatal(message, exception, (object)ownerType);
		}

		public void Fatal(string message, Exception exception, object owner)
		{
			SaveLogEntry(CreateLogEntry(GetFatalLogEntryType(), message, exception, owner, GetLogEntryDateTime()));
		}

		protected virtual string GetFatalLogEntryType()
		{
			return _logEntryService.GetFatalLogEntryType();
		}

		public void Info(string message)
		{
			Info(message, GetDefaultOwner());
		}

		public void Info(string message, object owner)
		{
			SaveLogEntry(CreateLogEntry(GetInfoLogEntryType(), message, owner, GetLogEntryDateTime()));
		}

		protected virtual string GetInfoLogEntryType()
		{
			return _logEntryService.GetInfoLogEntryType();
		}

		public void Warn(string message)
		{
			Warn(message, GetDefaultOwner());
		}

		public void Warn(string message, object owner)
		{
			SaveLogEntry(CreateLogEntry(GetWarnLogEntryType(), message, owner, GetLogEntryDateTime()));
		}

		protected virtual string AddPrefixToMessage(string message)
		{
			return string.Concat(_sqlLoggerSettings.LogPrefix, message);
		}

		protected virtual object GetDefaultOwner()
		{
			return this;
		}

		protected virtual LogEntry CreateLogEntry(string logEntryType, string message, Exception exception, Type ownerType, DateTime createdDate)
		{
			return _logEntryFactory.CreateLogEntry(logEntryType, message, exception, ownerType, createdDate);
		}

		protected virtual LogEntry CreateLogEntry(string logEntryType, string message, Exception exception, object owner, DateTime createdDate)
		{
			return _logEntryFactory.CreateLogEntry(logEntryType, message, exception, owner, createdDate);
		}

		protected virtual LogEntry CreateLogEntry(string logEntryType, string message, Type ownerType, DateTime createdDate)
		{
			return _logEntryFactory.CreateLogEntry(logEntryType, message, ownerType, createdDate);
		}

		protected virtual LogEntry CreateLogEntry(string logEntryType, string message, object owner, DateTime createdDate)
		{
			return _logEntryFactory.CreateLogEntry(logEntryType, message, owner, createdDate);
		}

		protected virtual string GetWarnLogEntryType()
		{
			return _logEntryService.GetWarnLogEntryType();
		}

		protected virtual DateTime GetLogEntryDateTime()
		{
			return DateTime.Now.ToUniversalTime();
		}

		protected virtual void SaveLogEntry(LogEntry entry)
		{
			if (entry == null)
			{
				return;
			}

			entry.LogMessage = _logEntryService.GenerateLogMessage(entry);

			RepositoryLogEntry repositoryEntry = CreateRepositoryLogEntry(entry);
			if (repositoryEntry == null)
			{
				return;
			}

			SaveRepositoryLogEntry(repositoryEntry);
		}

		protected virtual string GenerateLogMessage(LogEntry entry)
		{
			return _logEntryService.GenerateLogMessage(entry);
		}

		protected virtual RepositoryLogEntry CreateRepositoryLogEntry(LogEntry entry)
		{
			return _repositoryLogEntryFactory.CreateRepositoryLogEntry(entry);
		}

		protected virtual void SaveRepositoryLogEntry(RepositoryLogEntry entry)
		{
			if(!CanLogEntry(entry))
			{
				return;
			}

			SqlCommand insertCommand = GetinsertCommand(entry);
			if(insertCommand == null)
			{
				return;
			}

			ExecuteNoResult(insertCommand);
		}

		protected virtual bool CanLogEntry(RepositoryLogEntry entry)
		{
			return entry != null
					&& !string.IsNullOrWhiteSpace(entry.LogEntryType)
					&& !string.IsNullOrWhiteSpace(entry.LogMessage)
					&& entry.Created > DateTime.MinValue
					&& entry.Created < DateTime.MaxValue;
		}

		protected virtual SqlCommand GetinsertCommand(RepositoryLogEntry entry)
		{
			return new SqlCommand
			{
				Sql = GetInsertLogEntrySql(),
				Parameters = GetinsertCommandParameters(entry)
			};
		}

		protected virtual object[] GetinsertCommandParameters(RepositoryLogEntry entry)
		{
			return new object[]
			{
				GetLogEntryTypeParameterName(),
				entry.LogEntryType,
				GetLogMessageParameterName(),
				entry.LogMessage,
				GetCreatedParameterName(),
				entry.Created
			};
		}

		protected virtual string GetLogEntryTypeParameterName()
		{
			return _sqlLoggerSettings.LogEntryTypeParameterName;
		}

		protected virtual string GetLogMessageParameterName()
		{
			return _sqlLoggerSettings.LogMessageParameterName;
		}

		protected virtual string GetCreatedParameterName()
		{
			return _sqlLoggerSettings.CreatedParameterName;
		}

		protected virtual string GetInsertLogEntrySql()
		{
			return _sqlLoggerSettings.InsertLogEntrySqlFormat;
		}

		protected virtual void ExecuteNoResult(SqlCommand sqlCommand)
		{
			_factory.GetRetryer().ExecuteNoResult(() => { _sqlDataApi.Execute(sqlCommand.Sql, sqlCommand.Parameters); });
		}
	}
}

Since there is a lot of code in the class above, I’m not going to talk about all of it — it should be clear on what this class is doing for the most part.

I do want to highlight that the SaveRepositoryLogEntry() method takes in a RepositoryLogEntry instance; builds up a SqlCommand instance from it as well as the insert SQL statement and parameters from the SqlLoggerSettings instance (these are coming from Sitecore configuration, and there are hooks on this class to allow for overriding these if needed); and passes the SqlCommand instance to the ExecuteNoResult() method which uses the SqlDataApi instance for saving to the database. Plus, I’m leveraging an “out of the box” “retryer” from the Sitecore.Kernel.dll to ensure it makes its way into the database table.

Moreover, I’m reusing the ISqlDataApiFactory instance above from my previous post. Have a read of it so you can see what this factory class does.

Since Experience Forms was built perfectly — 😉 — I couldn’t see any LogEntry instances being saved to my database right away. So went ahead and created some <forms.renderField> pipeline processors to capture some.

The following interface is for a <forms.renderField> pipeline processor to just throw an exception by dividing by zero:

using Sitecore.ExperienceForms.Mvc.Pipelines.RenderField;

namespace Sandbox.Foundation.Forms.Pipelines.RenderField
{
	public interface IThrowExceptionProcessor
	{
		void Process(RenderFieldEventArgs args);
	}
}

Here is its implementation class:

using System;

using Sitecore.ExperienceForms.Diagnostics;
using Sitecore.ExperienceForms.Mvc.Pipelines.RenderField;

namespace Sandbox.Foundation.Forms.Pipelines.RenderField
{
	public class ThrowExceptionProcessor : IThrowExceptionProcessor
	{
		private readonly ILogger _logger;

		public ThrowExceptionProcessor(ILogger logger)
		{
			_logger = logger;
		}

		public void Process(RenderFieldEventArgs args)
		{
			try
			{
				int i = 1 / GetZero();
			}
			catch(Exception ex)
			{
				_logger.LogError(ToString(), ex, this);
			}
		}

		private int GetZero()
		{
			return 0;
		}
	}
}

I’m sure you would never do such a thing, right? 😉

I then created the following interface for another <forms.renderField> pipeline processor to log some information on the RenderFieldEventArgs instance sent to the Process() method:

using Sitecore.ExperienceForms.Mvc.Pipelines.RenderField;

namespace Sandbox.Foundation.Forms.Pipelines.RenderField
{
	public interface ILogRenderedFieldInfo
	{
		void Process(RenderFieldEventArgs args);
	}
}

Here is the implementation class for this:

using Sitecore.ExperienceForms.Diagnostics;
using Sitecore.ExperienceForms.Mvc.Pipelines.RenderField;
using Sitecore.Mvc.Pipelines;

namespace Sandbox.Foundation.Forms.Pipelines.RenderField
{
	public class LogRenderedFieldInfo : MvcPipelineProcessor<RenderFieldEventArgs>, ILogRenderedFieldInfo
	{
		private readonly ILogger _logger;

		public LogRenderedFieldInfo(ILogger logger)
		{
			_logger = logger;
		}

		public override void Process(RenderFieldEventArgs args)
		{
			LogInfo($"ViewModel Details:\n\nName: {args.ViewModel.Name}, ItemId: {args.ViewModel.ItemId}, TemplateId: {args.ViewModel.TemplateId}, FieldTypeItemId: {args.ViewModel.FieldTypeItemId}");
			LogInfo($"RenderingSettings Details\n\nFieldTypeName: {args.RenderingSettings.FieldTypeName}, FieldTypeId: {args.RenderingSettings.FieldTypeId}, FieldTypeIcon: {args.RenderingSettings.FieldTypeIcon}, FieldTypeDisplayName: {args.RenderingSettings.FieldTypeDisplayName}, FieldTypeBackgroundColor: {args.RenderingSettings.FieldTypeBackgroundColor}");
			LogInfo($"Item Details: {args.Item.ID}, Name: {args.Item.Name} FullPath: {args.Item.Paths.FullPath}, TemplateID: {args.Item.TemplateID}");
		}

		protected virtual void LogInfo(string message)
		{
			if(string.IsNullOrWhiteSpace(message))
			{
				return;
			}

			_logger.Info(message);
		}
	}
}

I then registered everything in the Sitecore IoC container using the following configurator:

using System;

using Microsoft.Extensions.DependencyInjection;

using Sitecore.Abstractions;
using Sitecore.DependencyInjection;
using Sitecore.ExperienceForms.Diagnostics;

using Sandbox.Foundation.Forms.Services.Factories.Diagnostics;
using Sandbox.Foundation.Forms.Services.Factories;
using Sandbox.Foundation.Forms.Models.Logging;
using Sandbox.Foundation.Forms.Services.Environment;
using Sandbox.Foundation.Forms.Services.Diagnostics;
using Sandbox.Foundation.Forms.Pipelines.RenderField;

namespace Sandbox.Foundation.Forms
{
	public class SqlLoggerConfigurator : IServicesConfigurator
	{
		public void Configure(IServiceCollection serviceCollection)
		{
			ConfigureConfigObjects(serviceCollection);
			ConfigureFactories(serviceCollection);
			ConfigureServices(serviceCollection);
			ConfigurePipelineProcessors(serviceCollection);
		}

		private void ConfigureConfigObjects(IServiceCollection serviceCollection)
		{
			serviceCollection.AddSingleton(provider => GetLogEntryServiceSettings(provider));
			serviceCollection.AddSingleton(provider => GetSqlLoggerSettings(provider));
		}

		private LogEntryServiceSettings GetLogEntryServiceSettings(IServiceProvider provider)
		{
			return CreateConfigObject<LogEntryServiceSettings>(provider, "moduleSettings/foundation/forms/logEntryServiceSettings");
		}

		private SqlLoggerSettings GetSqlLoggerSettings(IServiceProvider provider)
		{
			return CreateConfigObject<SqlLoggerSettings>(provider, "moduleSettings/foundation/forms/sqlLoggerSettings");
		}

		private TConfigObject CreateConfigObject<TConfigObject>(IServiceProvider provider, string path) where TConfigObject : class
		{
			BaseFactory factory = GetService<BaseFactory>(provider);
			return factory.CreateObject(path, true) as TConfigObject;
		}

		private TService GetService<TService>(IServiceProvider provider)
		{
			return provider.GetService<TService>();
		}

		private void ConfigureFactories(IServiceCollection serviceCollection)
		{
			serviceCollection.AddSingleton<ILogEntryFactory, LogEntryFactory>();
			serviceCollection.AddSingleton<IRepositoryLogEntryFactory, RepositoryLogEntryFactory>();
			serviceCollection.AddSingleton<ISqlDataApiFactory, SqlDataApiFactory>();
		}

		private void ConfigureServices(IServiceCollection serviceCollection)
		{
			serviceCollection.AddSingleton<IEnvironmentService, EnvironmentService>();
			serviceCollection.AddSingleton<ILogEntryService, LogEntryService>();
			serviceCollection.AddSingleton<ILogger, SqlLogger>();
		}

		private void ConfigurePipelineProcessors(IServiceCollection serviceCollection)
		{
			serviceCollection.AddSingleton<ILogRenderedFieldInfo, LogRenderedFieldInfo>();
			serviceCollection.AddSingleton<IThrowExceptionProcessor, ThrowExceptionProcessor>();
		}
	}
}

Note: the GetLogEntryServiceSettings() and the GetSqlLoggerSettings() methods both create settings objects by using the Sitecore Configuration Factory. Ultimately, these settings objects are thrown into the container so they can be injected into the service classes that need them.

I then strung everything together using the following the Sitecore patch configuration file.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
	<sitecore>
		<pipelines>
			<forms.renderField>
				<processor type="Sandbox.Foundation.Forms.Pipelines.RenderField.LogRenderedFieldInfo, Sandbox.Foundation.Forms" resolve="true"/>
				<processor type="Sandbox.Foundation.Forms.Pipelines.RenderField.ThrowExceptionProcessor, Sandbox.Foundation.Forms" resolve="true"/>
			</forms.renderField>
		</pipelines>
		<services>
			<configurator type="Sandbox.Foundation.Forms.SqlLoggerConfigurator, Sandbox.Foundation.Forms" />
			<register serviceType="Sitecore.ExperienceForms.Diagnostics.ILogger, Sitecore.ExperienceForms">
				<patch:delete />
			</register>
		</services>

		<moduleSettings>
			<foundation>
				<forms>
					<logEntryServiceSettings type="Sandbox.Foundation.Forms.Models.Logging.LogEntryServiceSettings, Sandbox.Foundation.Forms" singleInstance="true">
						<DebugLogEntryType>DEBUG</DebugLogEntryType>
						<ErrorLogEntryType>ERROR</ErrorLogEntryType>
						<FatalLogEntryType>FATAL</FatalLogEntryType>
						<InfoLogEntryType>INFO</InfoLogEntryType>
						<WarnLogEntryType>WARN</WarnLogEntryType>
						<ExceptionPrefix>Exception: </ExceptionPrefix>
						<MessagePrefix>Message: </MessagePrefix>
						<SourcePrefix>Source: </SourcePrefix>
						<NestedExceptionPrefix>Nested Exception</NestedExceptionPrefix>
						<LogEntryTimeFormat>HH:mm:ss.ff</LogEntryTimeFormat>
					</logEntryServiceSettings>
					<sqlLoggerSettings type="Sandbox.Foundation.Forms.Models.Logging.SqlLoggerSettings, Sandbox.Foundation.Forms" singleInstance="true">
						<LogPrefix>[Experience Forms]:</LogPrefix>
						<LogDatabaseConnectionStringName>ExperienceFormsSettings</LogDatabaseConnectionStringName>
						<InsertLogEntrySqlFormat>INSERT INTO {0}ExperienceFormsLog{1}({0}LogEntryType{1},{0}LogMessage{1},{0}Created{1})VALUES({2}logEntryType{3},{2}logMessage{3},{2}created{3});</InsertLogEntrySqlFormat>
						<LogEntryTypeParameterName>logEntryType</LogEntryTypeParameterName>
						<LogMessageParameterName>logMessage</LogMessageParameterName>
						<CreatedParameterName>created</CreatedParameterName>
					</sqlLoggerSettings>
				</forms>
			</foundation>
		</moduleSettings>
	</sitecore>
</configuration> 

Ok, let’s take this for a spin.

After building and deploying everything above, I spun up my Sitecore instance:

I then navigated to a form I had created in a previous post:

After the page with my form was done loading, I ran a query on my custom log table and saw this:

As you can see, it worked.

If you have any questions or comments, don’t hesitate to drop these in a comment below.

Until next time, have yourself a Sitecoretastic day!

Prevent Unbucketable Sitecore Items from Being Moved to Bucket Folders

If you’ve been reading my posts lately, you have probably noticed I’ve been having a ton of fun with Sitecore Item Buckets. I absolutely love this feature in Sitecore.

As a matter of, I love Item Buckets so much, I’m doing a presentation on them just next week at the Greater Cincinnati Sitecore Users Group. If you’re in the neighborhood, stop by — even if it’s only to say “Hello”.

Anyways, back to the post.

I noticed the following grey box on the Items Buckets page on the Sitecore Documentation site:

item-buckets-unbucketable-import

This got me thinking: why can’t we build something in Sitecore to prevent this from happening in the first place?

In other words, why can’t we just say “sorry, you can’t move an unbucketable Item into a bucket folder”?

nope

So, that’s what I decided to do — build a solution that prevents this from happening. Let’s have a look at what I came up with.

I first created the following interface for classes whose instances will move a Sitecore item to a destination Item:

using Sitecore.Data.Items;

namespace Sitecore.Sandbox.Utilities.Items.Movers
{
    public interface IItemMover
    {
        bool DisableSecurity { get; set; }

        bool ShouldBeMoved(Item item, Item destination);

        void Move(Item item, Item destination);
    }
}

I then defined the following class which implements the interface above:

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.SecurityModel;

namespace Sitecore.Sandbox.Utilities.Items.Movers
{
    public class ItemMover : IItemMover
    {
        public bool DisableSecurity { get; set; }
        
        public virtual bool ShouldBeMoved(Item item, Item destination)
        {
            return item != null && destination != null;
        }

        public virtual void Move(Item item, Item destination)
        {
            if (!ShouldBeMoved(item, destination))
            {
                return;
            }

            if(DisableSecurity)
            {
                MoveWithoutSecurity(item, destination);
                return;
            }

            MoveWithoutSecurity(item, destination);
        }

        protected virtual void MoveWithSecurity(Item item, Item destination)
        {
            Assert.ArgumentNotNull(item, "item");
            Assert.ArgumentNotNull(destination, "destination");
            item.MoveTo(destination);
        }

        protected virtual void MoveWithoutSecurity(Item item, Item destination)
        {
            Assert.ArgumentNotNull(item, "item");
            Assert.ArgumentNotNull(destination, "destination");
            using (new SecurityDisabler())
            {
                item.MoveTo(destination);
            }
        }
    }
}

Callers of the above code can move an Item from one location to another with/without Sitecore security in place.

The ShouldBeMoved() above is basically a stub that will allow subclasses to define their own rules on whether an Item should be moved, depending on whatever rules must be met.

I then defined the following subclass of the class above which has its own rules on whether an Item should be moved (i.e. move this unbucketable Item out of a bucket folder if makes its way there):

using Sitecore.Data.Items;
using Sitecore.Diagnostics;

using Sitecore.Sandbox.Buckets.Util.Methods;
using Sitecore.Sandbox.Utilities.Items.Movers;

namespace Sitecore.Sandbox.Buckets.Util.Items.Movers
{
    public class UnbucketableItemMover : ItemMover
    {
        protected IItemBucketsFeatureMethods ItemBucketsFeatureMethods { get; set; }

        public override bool ShouldBeMoved(Item item, Item destination)
        {
            return base.ShouldBeMoved(item, destination)
                    && !IsItemBucketable(item)
                    && IsItemInBucket(item)
                    && !IsItemBucketFolder(item)
                    && IsItemBucketFolder(item.Parent)
                    && IsItemBucket(destination);
        }

        protected virtual bool IsItemBucketable(Item item)
        {
            EnsureItemBucketFeatureMethods();
            Assert.ArgumentNotNull(item, "item");
            return ItemBucketsFeatureMethods.IsItemBucketable(item);
        }

        protected virtual bool IsItemInBucket(Item item)
        {
            EnsureItemBucketFeatureMethods();
            Assert.ArgumentNotNull(item, "item");
            return ItemBucketsFeatureMethods.IsItemContainedWithinBucket(item);
        }

        protected virtual bool IsItemBucketFolder(Item item)
        {
            EnsureItemBucketFeatureMethods();
            Assert.ArgumentNotNull(item, "item");
            return ItemBucketsFeatureMethods.IsItemBucketFolder(item);
        }

        protected virtual bool IsItemBucket(Item item)
        {
            EnsureItemBucketFeatureMethods();
            Assert.ArgumentNotNull(item, "item");
            return ItemBucketsFeatureMethods.IsItemBucket(item);
        }

        protected virtual void EnsureItemBucketFeatureMethods()
        {
            Assert.IsNotNull(ItemBucketsFeatureMethods, "ItemBucketsFeatureMethods must be set in configuration!");
        }
    }
}

I’m injecting an instance of an IItemBucketsFeatureMethods class — this interface and its implementation are defined in my previous post; go have a look if you have not read that post so you can be familiar with the IItemBucketsFeatureMethods code — via the Sitecore Configuration Factory which contains common methods I am using in my Item Bucket code solutions (I will be using this in future posts).

The ShouldBeMoved() method basically says that an Item can only be moved when the Item and destination passed aren’t null — this is defined on the base class’ ShouldBeMoved() method; the Item isn’t bucketable; the Item is already in an Item Bucket; the Item isn’t a Bucket Folder; the Item’s parent Item is a Bucket Folder; and the destination is an Item Bucket.

Yes, the above sounds a bit confusing though there is a reason for it — I want to take an unbucketable Item out of a Bucket Folder and move it directly under the Item Bucket instead.

I then created the following class which contains methods that will serve as “item:moved” event handlers:

using System;
using System.Collections.Generic;

using Sitecore.Data;
using Sitecore.Data.Events;
using Sitecore.Data.Items;
using Sitecore.Events;

using Sitecore.Sandbox.Buckets.Util.Methods;
using Sitecore.Sandbox.Utilities.Items.Movers;

namespace Sitecore.Sandbox.Buckets.Events.Items.Move
{
    public class RemoveFromBucketFolderIfNotBucketableHandler
    {
        protected static SynchronizedCollection<ID> ItemsBeingProcessed { get; set; }

        protected IItemBucketsFeatureMethods ItemBucketsFeatureMethods { get; set; }

        protected IItemMover UnbucketableItemMover { get; set; }

        protected void OnItemMoved(object sender, EventArgs args)
        {
            Item item = GetItem(args);
            RemoveFromBucketFolderIfNotBucketable(item);
        }

        static RemoveFromBucketFolderIfNotBucketableHandler()
        {
            ItemsBeingProcessed = new SynchronizedCollection<ID>();
        }

        protected virtual Item GetItem(EventArgs args)
        {
            if (args == null)
            {
                return null;
            }

            return Event.ExtractParameter(args, 0) as Item;
        }

        protected void OnItemMovedRemote(object sender, EventArgs args)
        {
            Item item = GetItemRemote(args);
            RemoveFromBucketFolderIfNotBucketable(item);
        }

        protected virtual Item GetItemRemote(EventArgs args)
        {
            ItemMovedRemoteEventArgs remoteArgs = args as ItemMovedRemoteEventArgs;
            if (remoteArgs == null)
            {
                return null;
            }

            return remoteArgs.Item;
        }

        protected virtual void RemoveFromBucketFolderIfNotBucketable(Item item)
        {
            if(item == null)
            {
                return;
            }
            
            Item itemBucket = GetItemBucket(item);
            if (itemBucket == null)
            {
                return;
            }

            if(!ShouldBeMoved(item, itemBucket))
            {
                return;
            }

            AddItemBeingProcessed(item);
            MoveUnderItemBucket(item, itemBucket);
            RemoveItemBeingProcessed(item);
        }

        protected virtual bool IsItemBeingProcessed(Item item)
        {
            if (item == null)
            {
                return false;
            }

            return ItemsBeingProcessed.Contains(item.ID);
        }

        protected virtual void AddItemBeingProcessed(Item item)
        {
            if (item == null)
            {
                return;
            }

            ItemsBeingProcessed.Add(item.ID);
        }

        protected virtual void RemoveItemBeingProcessed(Item item)
        {
            if (item == null)
            {
                return;
            }

            ItemsBeingProcessed.Remove(item.ID);
        }

        protected virtual Item GetItemBucket(Item item)
        {
            if(ItemBucketsFeatureMethods == null || item == null)
            {
                return null;
            }

            return ItemBucketsFeatureMethods.GetItemBucket(item);
        }

        protected virtual bool ShouldBeMoved(Item item, Item itemBucket)
        {
            if(UnbucketableItemMover == null)
            {
                return false;
            }

            return UnbucketableItemMover.ShouldBeMoved(item, itemBucket);
        }

        protected virtual void MoveUnderItemBucket(Item item, Item itemBucket)
        {
            if (UnbucketableItemMover == null)
            {
                return;
            }

            UnbucketableItemMover.Move(item, itemBucket);
        }
    }
}

Both the OnItemMoved() and OnItemMovedRemote() methods extract the moved Item from their specific methods for getting the Item from the EventArgs instance. If that Item is null, the code exits.

Both methods pass their Item instance to the RemoveFromBucketFolderIfNotBucketable() method which ultimately attempts to grab an Item Bucket ancestor of the Item via the GetItemBucket() method. If no Item Bucket instance is returned, the code exits.

If an Item Bucket was found, the RemoveFromBucketFolderIfNotBucketable() method ascertains whether the Item should be moved — it makes a call to the ShouldBeMoved() method which just delegates to the IItemMover instance injected in via the Sitecore Configuration Factory (have a look at the patch configuration file below).

If the Item should not be moved, then the code exits.

If it should be moved, it is then passed to the MoveUnderItemBucket() method which delegates to the Move() method on the IItemMover instance.

You might be asking “Mike, what’s up with the ItemsBeingProcessed SynchronizedCollection of Item IDs?” I’m using this collection to maintain which Items are currently being moved so we don’t have racing conditions in code.

You might be thinking “Great, we’re done!”

no

We can’t just move an Item from one destination to another, especially when the user selected the first destination. We should let the user know that we will need to move the Item as it is unbucketable. Let’s not be evil.

evil

I created the following class whose Process() method will serve as a custom processor for both the <uiDragItemTo> and <uiMoveItems> pipelines of the Sitecore Client:

using System;
using System.Collections.Generic;
using System.Linq;

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Text;
using Sitecore.Web.UI.Sheer;

using Sitecore.Sandbox.Buckets.Util.Methods;

namespace Sitecore.Sandbox.Buckets.Shell.Framework.Pipelines.MoveItems
{
    public class ConfirmMoveOfUnbucketableItem
    {
        protected string ItemIdsParameterName { get; set; }

        protected IItemBucketsFeatureMethods ItemBucketsFeatureMethods { get; set; }

        protected string ConfirmationMessageFormat { get; set; }

        public void Process(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            IEnumerable<string> itemIds = GetItemIds(args);
            if (itemIds == null || !itemIds.Any() || itemIds.Count() > 1)
            {
                return;
            }
            
            string targetId = GetTargetId(args);
            if (string.IsNullOrWhiteSpace(targetId))
            {
                return;
            }

            Database database = GetDatabase(args);
            if (database == null)
            {
                return;
            }

            Item targetItem = GetItem(database, targetId);
            if (targetItem == null || !IsItemBucketOrIsItemInBucket(targetItem))
            {
                return;
            }

            Item item = GetItem(database, itemIds.First());
            if (item == null || IsItemBucketable(item))
            {
                return;
            }

            Item itemBucket = GetItemBucket(targetItem);
            if (itemBucket == null)
            {
                return;
            }

            SetTokenValues(args, item, itemBucket);
            ConfirmMove(args);
        }

        protected virtual IEnumerable<string> GetItemIds(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Parameters, "args.Parameters");
            string itemIdsParameterName = GetItemIdsParameterName(args);
            Assert.IsNotNullOrEmpty(itemIdsParameterName, "GetItemIdParameterName() cannot return null or the empty string!");
            return new ListString(itemIdsParameterName, '|');
        }

        protected virtual string GetItemIdsParameterName(ClientPipelineArgs args)
        {
            Assert.IsNotNullOrEmpty(ItemIdsParameterName, "ItemIdParameterName must be set in configuration!");
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Parameters, "args.Parameters");
            return args.Parameters[ItemIdsParameterName];
        }

        protected virtual string GetTargetId(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Parameters, "args.Parameters");
            return args.Parameters["target"];
        }

        protected virtual Database GetDatabase(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Parameters, "args.Parameters");
            return Factory.GetDatabase(args.Parameters["database"]);
        }

        protected virtual Item GetItem(Database database, string itemId)
        {
            Assert.ArgumentNotNull(database, "database");
            Assert.ArgumentNotNullOrEmpty(itemId, "itemId");
            try
            {
                return database.GetItem(itemId);

            }
            catch(Exception ex)
            {
                Log.Error(ToString(), ex, this);
            }

            return null;
        }
        
        protected virtual bool IsItemBucketOrIsItemInBucket(Item item)
        {
            EnsureItemBucketsFeatureMethods();
            Assert.ArgumentNotNull(item, "item");
            return IsItemBucket(item) || IsItemInBucket(item);
        }

        protected virtual bool IsItemBucket(Item item)
        {
            EnsureItemBucketsFeatureMethods();
            Assert.ArgumentNotNull(item, "item");
            return ItemBucketsFeatureMethods.IsItemBucket(item);
        }

        protected virtual bool IsItemInBucket(Item item)
        {
            EnsureItemBucketsFeatureMethods();
            Assert.ArgumentNotNull(item, "item");
            return ItemBucketsFeatureMethods.IsItemContainedWithinBucket(item);
        }

        protected virtual bool IsItemBucketable(Item item)
        {
            EnsureItemBucketsFeatureMethods();
            Assert.ArgumentNotNull(item, "item");
            return ItemBucketsFeatureMethods.IsItemBucketable(item);
        }

        protected virtual Item GetItemBucket(Item item)
        {
            EnsureItemBucketsFeatureMethods();
            Assert.ArgumentNotNull(item, "item");
            if(!ItemBucketsFeatureMethods.IsItemBucket(item))
            {
                return ItemBucketsFeatureMethods.GetItemBucket(item);
            }

            return item;
        }

        protected virtual void SetTokenValues(ClientPipelineArgs args, Item item, Item itemBucket)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Parameters, "args.Parameters");
            Assert.ArgumentNotNull(item, "item");
            Assert.ArgumentNotNull(itemBucket, "itemBucket");
            args.Parameters["$itemName"] = item.Name;
            args.Parameters["$itemBucketName"] = itemBucket.Name;
            args.Parameters["$itemBucketFullPath"] = itemBucket.Paths.FullPath;
        }

        protected virtual void ConfirmMove(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if(args.IsPostBack)
            {
                if (args.Result == "yes")
                {
                    ClearResult(args);
                    return;
                }

                if (args.Result == "no")
                {
                    args.AbortPipeline();
                    return;
                }
            }
            else
            {
                SheerResponse.Confirm(GetConfirmationMessage(args));
                args.WaitForPostBack();    
            }   
        }

        protected virtual void ClearResult(ClientPipelineArgs args)
        {
            args.Result = string.Empty;
            args.IsPostBack = false;
        }

        protected virtual string GetConfirmationMessage(ClientPipelineArgs args)
        {
            Assert.IsNotNullOrEmpty(ConfirmationMessageFormat, "ConfirmationMessageFormat must be set in configuration!");
            Assert.ArgumentNotNull(args, "args");
            return ReplaceTokens(ConfirmationMessageFormat, args);
        }

        protected virtual string ReplaceTokens(string messageFormat, ClientPipelineArgs args)
        {
            Assert.ArgumentNotNullOrEmpty(messageFormat, "messageFormat");
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Parameters, "args.Parameters");
            
            string message = messageFormat;
            message = message.Replace("$itemName", args.Parameters["$itemName"]);
            message = message.Replace("$itemBucketName", args.Parameters["$itemBucketName"]);
            message = message.Replace("$itemBucketFullPath", args.Parameters["$itemBucketFullPath"]);
            return message;
        }

        protected virtual void EnsureItemBucketsFeatureMethods()
        {
            Assert.IsNotNull(ItemBucketsFeatureMethods, "ItemBucketsFeatureMethods must be set in configuration!");
        }
    }
}

The Process() method above gets the Item ID for the Item that is being moved; the Item ID for the destination Item — this is referred to as the “target” in the code above; gets the Database instance of where we are moving this Item; the instances of both the Item and target Item; determines if the Target Item is a Bucket Folder or an Item Bucket; determines if the Item is unbucketable; and then the Item Bucket (this could be the target Item).

If any of of the instances above are null, the code exits.

If the Item is unbucketable but is being moved to a Bucket Folder or Item Bucket, we prompt the user with a confirmation dialog asking him/her whether he/she should like to continue given that the Item will be moved directly under the Item Bucket.

If the user clicks the ‘Ok’ button, the Item is moved. Otherwise, the pipeline is aborted and the Item will not be moved at all.

I then pieced all of the above together via the following patch configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <buckets>
      <movers>
        <items>
          <unbucketableItemMover type="Sitecore.Sandbox.Buckets.Util.Items.Movers.UnbucketableItemMover, Sitecore.Sandbox" singleInstance="true">
            <DisableSecurity>true</DisableSecurity>
            <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" />
          </unbucketableItemMover>
        </items>
      </movers>
    </buckets>
    <events>
      <event name="item:moved">
        <handler type="Sitecore.Sandbox.Buckets.Events.Items.Move.RemoveFromBucketFolderIfNotBucketableHandler, Sitecore.Sandbox" method="OnItemMoved">
          <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" />
          <UnbucketableItemMover ref="buckets/movers/items/unbucketableItemMover" />
        </handler>  
      </event>
      <event name="item:moved:remote">
        <handler type="Sitecore.Sandbox.Buckets.Events.Items.Move.RemoveFromBucketFolderIfNotBucketableHandler, Sitecore.Sandbox" method="OnItemMovedRemote">
          <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" />
          <UnbucketableItemMover ref="buckets/movers/items/unbucketableItemMover" />
        </handler>
      </event>
    </events>
    <processors>
      <uiDragItemTo>
        <processor patch:before="processor[@type='Sitecore.Buckets.Pipelines.UI.ItemDrag, Sitecore.Buckets' and @method='Execute']"
                   type="Sitecore.Sandbox.Buckets.Shell.Framework.Pipelines.MoveItems.ConfirmMoveOfUnbucketableItem, Sitecore.Sandbox" mode="on">
          <ItemIdsParameterName>id</ItemIdsParameterName>
          <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" />
          <ConfirmationMessageFormat>You are attempting to move the non-bucketable Item: $itemName to a bucket folder. If you continue, it will be moved directly under the Item Bucket: $itemBucketName ($itemBucketFullPath). Do you wish to continue?</ConfirmationMessageFormat>
        </processor>
      </uiDragItemTo>
      <uiMoveItems>
        <processor patch:before="processor[@type='Sitecore.Buckets.Pipelines.UI.ItemMove, Sitecore.Buckets' and @method='Execute']"
                     type="Sitecore.Sandbox.Buckets.Shell.Framework.Pipelines.MoveItems.ConfirmMoveOfUnbucketableItem, Sitecore.Sandbox" mode="on">
          <ItemIdsParameterName>items</ItemIdsParameterName>
          <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" />
          <ConfirmationMessageFormat>You are attempting to move the non-bucketable Item: $itemName to a bucket folder. If you continue, it will be moved directly under the Item Bucket: $itemBucketName ($itemBucketFullPath). Do you wish to continue?</ConfirmationMessageFormat>
        </processor>
      </uiMoveItems>
    </processors>
  </sitecore>
</configuration>

Let’s see how we did.

jenga-topple

Let’s move this unbucketable Item to an Item Bucket:

move-unbucketable-1

Yes, I’m sure I’m sure:

move-unbucketable-2

I was then prompted with the confirmation dialog as expected:

move-unbucketable-3

As you can see, the Item was placed directly under the Item Bucket:

move-unbucketable-4

If you have any thoughts on this, please drop a comment.

thats-all-folks

Prevent Duplicate Names of Bucketed Sitecore Items

In my previous post, I gave a solution that removes names of Bucket Folder Items from URLs in Sitecore, and also resolves those same URLs when they are called up in a browser.

However, that solution wasn’t complete — code is needed to ensure Bucketed Item names are unique given the solution assumes Bucketed Item names are unique (code in that solution uses the Sitecore.ContentSearch API to find an Item by name within an Item Bucket, and if there are two or more Items with the same name, only one will be returned — this will prevent the resolution of URLs for those other Bucketed page Items).

I decided to take up the challenge on continuing that solution over this previous weekend, and share what I built.

challenge

One thing to note: the solution that follows is not a complete solution for preventing duplicate Bucketed Item names as such a post could go on for ages — actually, I probably would still be writing the code for it. I leave the rest for you guys to do as a homework assignment. 😉

Cat-ate-my-homework-GIF

I first defined the following interface to centralize common methods I have been using in my Sitecore Item Buckets code (I will be reusing this same interface and its implementation in future blog posts):

using Sitecore.Data.Items;

namespace Sitecore.Sandbox.Buckets.Util.Methods
{
    public interface IItemBucketsFeatureMethods
    {
        bool IsItemBucketFolder(Item item);
        
        bool IsItemContainedWithinBucket(Item item);
        
        bool IsItemBucketable(Item item);
        
        Item GetItemBucket(Item item);

        bool IsItemBucket(Item item);

        bool HasBucketedItemWithName(Item itemBucket, string itemName);
    }
}

The following class implements the interface above:

using Sitecore.Buckets.Extensions;
using Sitecore.Buckets.Managers;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

using Sitecore.Sandbox.Buckets.Providers.Items;

namespace Sitecore.Sandbox.Buckets.Util.Methods
{
    public class ItemBucketsFeatureMethods : IItemBucketsFeatureMethods
    {
        private IFindBucketedItemProvider FindBucketedItemProvider { get; set; }

        public virtual bool IsItemBucketFolder(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return item.IsABucketFolder();
        }

        public virtual bool IsItemContainedWithinBucket(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return BucketManager.IsItemContainedWithinBucket(item);
        }

        public virtual bool IsItemBucketable(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return item.IsItemBucketable();
        }

        public virtual Item GetItemBucket(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            Item ancestor = item.GetParentBucketItemOrParent();
            if (!IsItemBucket(ancestor))
            {
                return null;
            }

            return ancestor;
        }

        public virtual bool IsItemBucket(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return item.IsABucket();
        }

        public virtual bool HasBucketedItemWithName(Item itemBucket, string itemName)
        {
            EnsureFindBucketedItemProvider();
            Assert.ArgumentNotNull(itemBucket, "itemBucket");
            Assert.ArgumentNotNullOrEmpty(itemName, "itemName");
            Item item = FindBucketedItemProvider.FindBucketedItemByName(itemBucket, itemName);
            if(item == null)
            {
                return false;
            }

            return true;
        }

        protected virtual void EnsureFindBucketedItemProvider()
        {
            Assert.IsNotNull(FindBucketedItemProvider, "FindBucketedItemProvider must be set in configuration!");
        }
    }
}

I’m not going to go into details of the code above as it is self-explanatory.

However, I do want to call out that I am reusing the IFindBucketedItemProvider code from my previous post. I advise having a look at that code before moving forward.

I then defined the following class whose Process() method will serve as a processor of the <uiDragItemTo> and
<uiMoveItems> pipelines of the Sitecore Client:

using System;
using System.Collections.Generic;
using System.Linq;

using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Text;
using Sitecore.Web.UI.Sheer;

using Sitecore.Sandbox.Buckets.Util.Methods;

namespace Sitecore.Sandbox.Buckets.Shell.Framework.Pipelines.MoveItems
{
    public class HandleDuplicateBucketedItemName
    {
        protected string ItemIdsParameterName { get; set; }

        protected IItemBucketsFeatureMethods ItemBucketsFeatureMethods { get; set; }

        protected string RenameItemMessage { get; set; }

        public void Process(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            IEnumerable<string> itemIds = GetItemIds(args);
            if (itemIds == null || !itemIds.Any() || itemIds.Count() > 1)
            {
                return;
            }
            
            string targetId = GetTargetId(args);
            if (string.IsNullOrWhiteSpace(targetId))
            {
                return;
            }

            Database database = GetDatabase(args);
            if (database == null)
            {
                return;
            }

            Item targetItem = GetItem(database, targetId);
            if (targetItem == null)
            {
                return;
            }

            Item item = GetItem(database, itemIds.First());
            if (item == null)
            {
                return;
            }

            Item itemBucket = GetItemBucket(targetItem);
            if (itemBucket == null || !HasBucketedItemWithName(itemBucket, item.Name))
            {
                return;
            }

            PromptRenameItem(args, item);
        }

        protected virtual IEnumerable<string> GetItemIds(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Parameters, "args.Parameters");
            string itemIdsParameterName = GetItemIdsParameterName(args);
            Assert.IsNotNullOrEmpty(itemIdsParameterName, "GetItemIdParameterName() cannot return null or the empty string!");
            return new ListString(itemIdsParameterName, '|');
        }

        protected virtual string GetItemIdsParameterName(ClientPipelineArgs args)
        {
            Assert.IsNotNullOrEmpty(ItemIdsParameterName, "ItemIdParameterName must be set in configuration!");
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Parameters, "args.Parameters");
            return args.Parameters[ItemIdsParameterName];
        }

        protected virtual string GetTargetId(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Parameters, "args.Parameters");
            return args.Parameters["target"];
        }

        protected virtual Database GetDatabase(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Parameters, "args.Parameters");
            return Factory.GetDatabase(args.Parameters["database"]);
        }

        protected virtual Item GetItem(Database database, string itemId)
        {
            Assert.ArgumentNotNull(database, "database");
            Assert.ArgumentNotNullOrEmpty(itemId, "itemId");
            try
            {
                return database.GetItem(itemId);

            }
            catch(Exception ex)
            {
                Log.Error(ToString(), ex, this);
            }

            return null;
        }

        protected virtual Item GetItemBucket(Item item)
        {
            EnsureItemBucketsFeatureMethods();
            Assert.ArgumentNotNull(item, "item");
            if(!ItemBucketsFeatureMethods.IsItemBucket(item))
            {
                return ItemBucketsFeatureMethods.GetItemBucket(item);
            }

            return item;
        }

        protected virtual bool HasBucketedItemWithName(Item itemBucket, string bucketedItemName)
        {
            EnsureItemBucketsFeatureMethods();
            Assert.ArgumentNotNull(itemBucket, "itemBucket");
            Assert.ArgumentNotNullOrEmpty(bucketedItemName, "bucketedItemName");
            return ItemBucketsFeatureMethods.HasBucketedItemWithName(itemBucket, bucketedItemName);
        }

        protected virtual void PromptRenameItem(ClientPipelineArgs args, Item item)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(item, "item");
            if (args.IsPostBack)
            {
                if(!args.HasResult)
                {
                    args.AbortPipeline();
                    return;
                }

                GetProposedValidItemName(args.Result);
                RenameItem(item, GetProposedValidItemName(args.Result));
                ClearResult(args);
            }
            else
            {
                SheerResponse.Input(GetRenameItemMessage(), string.Empty);
                args.WaitForPostBack();    
            }   
        }

        protected virtual void ClearResult(ClientPipelineArgs args)
        {
            args.Result = string.Empty;
            args.IsPostBack = false;
        }

        protected virtual string GetProposedValidItemName(string itemName)
        {
            Assert.ArgumentNotNull(itemName, "itemName");
            return ItemUtil.ProposeValidItemName(itemName);
        }

        protected virtual void RenameItem(Item item, string name)
        {
            Assert.ArgumentNotNull(item, "item");
            Assert.ArgumentNotNullOrEmpty(name, "name");
            using (new EditContext(item, false, true))
            {
                item.Name = name;
            }
        }

        protected virtual string GetRenameItemMessage()
        {
            Assert.ArgumentNotNull(RenameItemMessage, "RenameItemMessage must be set in configuration!");
            return RenameItemMessage;
        }

        protected virtual void EnsureItemBucketsFeatureMethods()
        {
            Assert.IsNotNull(ItemBucketsFeatureMethods, "ItemBucketsFeatureMethods must be set in configuration!");
        }
    }
}

The Process() method above basically gets the Item IDs of the Items that are moving from the Parameters collection on the arguments object passed to it — these are stored under different keys in the Parameters collection by the <uiDragItemTo> and <uiMoveItems> pipelines, so I’m letting the Sitecore Configuration Factory pass in the name of that parameter for each (see the patch configuration file below).

If more than one Item ID is returned, the code exits.

The Process() method then gets the target Item’s ID; the Database instance; the target Item; and the instance of the Item we are moving. If any of these are null, it exits.

The code then attempts to get the Item Bucket for the target Item — this is done via the GetItemBucket() method which just delegates to the GetItemBucket() method on the IItemBucketsFeatureMethods instance, or returns the passed Item if it is an Item Bucket. If the returned Item is null, the code exits.

The Process() method then calls the HasBucketedItemWithName() — this method just makes a call to a method with the same name on the ItemBucketsFeatureMethods instance — to see if there is another Bucketed Item within the Item Bucket with the same name. If one is not found, we prompt the user for a new Item name, and rename the Item if one is supplied. If no Item name is supplied, we abort the pipeline completely to prevent the user from moving forward.

I do want to highlight one more thing. the RenameItem() method uses a Sitecore.Data.Items.EditContext instance when changing the Item name. I decided to use an instance of this class to have this change be silent and not log any update statistics as this was causing the Item to become un-bucketable after it was moved (no clue as to why this would happen).

I then plugged-in all of the code above via the following patch configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <buckets>
      <methods>
        <itemBucketsFeatureMethods type="Sitecore.Sandbox.Buckets.Util.Methods.ItemBucketsFeatureMethods, Sitecore.Sandbox">
          <FindBucketedItemProvider ref="buckets/providers/items/findBucketedItemProvider" />
        </itemBucketsFeatureMethods>
      </methods>
    </buckets>
    <processors>
      <uiDragItemTo>
        <processor patch:before="processor[@type='Sitecore.Buckets.Pipelines.UI.ItemDrag, Sitecore.Buckets' and @method='Execute']"
                   type="Sitecore.Sandbox.Buckets.Shell.Framework.Pipelines.MoveItems.HandleDuplicateBucketedItemName, Sitecore.Sandbox" mode="on">
          <ItemIdsParameterName>id</ItemIdsParameterName>
          <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" />
          <RenameItemMessage>Duplicate bucketed Item names are not allowed.  Please enter in a new name for the item:</RenameItemMessage>
        </processor>
      </uiDragItemTo>
      <uiMoveItems>
        <processor patch:before="processor[@type='Sitecore.Buckets.Pipelines.UI.ItemMove, Sitecore.Buckets' and @method='Execute']"
                     type="Sitecore.Sandbox.Buckets.Shell.Framework.Pipelines.MoveItems.HandleDuplicateBucketedItemName, Sitecore.Sandbox" mode="on">
          <ItemIdsParameterName>items</ItemIdsParameterName>
          <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" />
          <RenameItemMessage>Duplicate bucketed Item names are not allowed.  Please enter in a new name for the item:</RenameItemMessage>
        </processor>
      </uiMoveItems>
    </processors>
  </sitecore>
</configuration>

plugged-in

Let’s take this for a spin.

Let’s test this out by dragging an Item to an Item Bucket that has a bucketed Item with the same name:

unique-name-move-1

Of course, I do:

unique-name-move-2

I gave it a unique name:

unique-name-move-3

As you can see, the Item was renamed and then moved:

unique-name-move-4

One thing to note: the Item will not be moved if the user clicks the ‘Cancel’ button in the dialog that asks for a new name.

Moreover, the above code only works when moving Items in the Sitecore Client. These pipeline processors will not run when moving Items via Sitecore API code (you’ll have to tap into one of the “move” related events, instead).

I’m going to omit sharing my testing of the “Move To” piece of the code above as it is using the same code. Trust me, it works. 😉

If you have any thoughts on this, please drop a comment.

Omit Sitecore Bucket Folder Item Names from Page Item URLs

In my two previous posts — this post and this post — I used the Sitecore Rules Engine to determine how bucket folders in the Item Buckets feature should be constructed.

I love having the freedom and flexibility to be able to do this.

However, depending on how you generate these folder structures, you might end up with some pretty yucky — ahem, I mean “interesting” — URLs if you have Actions that generate nonsense bucket folder Item names for bucketed Items.

For example, in my previous post, I built a custom Action that reversed the Item ID of the bucketed Item to generated its bucket folder path:

bucketed-links-bucketed-item-page-bucket-folders-2

Yucky, right?

Yuck

The “out of the box” Item Buckets bucket-folder-structure-generating algorithm creates a nice structure based on when the bucketed Item was created, and this is more palatable when looking at it:

bucketed-links-bucketed-item-page-bucket-folders-1

However, we may not be able to always use this “out of the box” algorithm for whatever reason — who knows what requirements will make us do — so let’s explore some code that can clean up these yucky URLs.

clean-up

Let’s first tackle cleaning up the URL generated by the “out of the box” Sitecore.Links.LinkProvider class.

I decided to implement a custom Sitecore pipeline to clean up the URL generated by the GetItemUrl() of the LinkProvider class, and will call this pipeline from a subclass of it (this code is further down in this post).

If you’re going to create a custom pipeline, you’ll need an arguments object for it — this is technically known as a Parameter Object but I will stick with the name “arguments object” and “arguments class” for the class that these objects are instantiated from throughout this post.

The following class is the arguments class for my custom pipeline:

using System;

using Sitecore.Data.Items;
using Sitecore.Links;
using Sitecore.Pipelines;

namespace Sitecore.Sandbox.Buckets.Pipelines.GetBucketedItemLinkUrl
{
    public class GetBucketedItemLinkUrlArgs : PipelineArgs
    {
        public Func<Item, UrlOptions, string> GetItemUrl { get; set; }

        public UrlOptions UrlOptions { get; set; }

        public Item ItemBucket { get; set; }

        public Item BucketedItem { get; set; }

        public string DefaultUrl { get; set; }

        public string BucketedItemUrl { get; set; }
    }
}

I defined the following abstract class which all processors of the custom pipeline must inherit:

namespace Sitecore.Sandbox.Buckets.Pipelines.GetBucketedItemLinkUrl
{
    public abstract class GetBucketedItemLinkUrlProcessor
    {
        public abstract void Process(GetBucketedItemLinkUrlArgs args);
    }
}

All subclasses must implement the abstract Process method above which takes in the arguments object with the class type defined above.

The following class whose instance is used as the first processor of the custom pipeline checks whether required property values are set on the arguments object passed to the pipeline:

using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Buckets.Pipelines.GetBucketedItemLinkUrl
{
    public class EnsureParameters : GetBucketedItemLinkUrlProcessor
    {
        public override void Process(GetBucketedItemLinkUrlArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if(!CanProcess(args))
            {
                args.AbortPipeline();
            }
        }

        protected virtual bool CanProcess(GetBucketedItemLinkUrlArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            return args.GetItemUrl != null
                    && args.BucketedItem != null
                    && !string.IsNullOrWhiteSpace(args.DefaultUrl);
        }
    }
}

The BucketedItem, DefaultUrl — this is the “default” URL generated by the GetItemUrl() method on the “out of the box” LinkProvider class, and GetItemUrl — this is a “pointer” to the GetItemUrl method on the LinkProvider class — properties are required by the custom pipeline when initially called, and these are checked here.

This next class who instance is used for the second processor of the pipeline does some vetting of the bucketed Item passed on the arguments object:

using Sitecore.Data.Items;
using Sitecore.Diagnostics;

using Sitecore.Buckets.Extensions;
using Sitecore.Buckets.Managers;

namespace Sitecore.Sandbox.Buckets.Pipelines.GetBucketedItemLinkUrl
{
    public class InspectBucketedItem : GetBucketedItemLinkUrlProcessor
    {
        public override void Process(GetBucketedItemLinkUrlArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.BucketedItem, "args.BucketedItem");
            
            if (!IsItemContainedWithinBucket(args.BucketedItem) || !IsParentBucketFolder(args.BucketedItem))
            {
                args.AbortPipeline();
            }
        }

        protected virtual bool IsItemContainedWithinBucket(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return BucketManager.IsItemContainedWithinBucket(item);
        }

        protected virtual bool IsParentBucketFolder(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return item.Parent.IsABucketFolder();
        }
    }
}

The instance of the class above checks to see whether the bucketed Item is a descendant of an Item Bucket, and also sees if it is contained within a bucket folder. If one of these is not true, the pipeline is aborted.

The following class whose instance serves as the third processor of the custom pipeline gets the bucketed Item’s Item Bucket:

using Sitecore.Data.Items;
using Sitecore.Diagnostics;

using Sitecore.Buckets.Extensions;

namespace Sitecore.Sandbox.Buckets.Pipelines.GetBucketedItemLinkUrl
{
    public class SetItemBucket : GetBucketedItemLinkUrlProcessor
    {
        public override void Process(GetBucketedItemLinkUrlArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.BucketedItem, "args.BucketedItem");
            Item item = GetItemBucket(args.BucketedItem);
            if(!IsItemBucket(item))
            {
                args.AbortPipeline();
                return;
            }

            args.ItemBucket = item;
        }

        protected virtual Item GetItemBucket(Item bucketedItem)
        {
            Assert.ArgumentNotNull(bucketedItem, "bucketedItem");
            return bucketedItem.GetParentBucketItemOrParent();
        }

        protected virtual bool IsItemBucket(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return item != null && item.IsABucket();
        }
    }
}

Code in the GetItemBucket() method uses the GetParentBucketItemOrParent() extension method of the Item class — this lives in the Sitecore.Buckets.Extensions namespace in Sitecore.Buckets.dll — to get the Item Bucket ancestor for the bucketed Item.

If the Item returned is not an Item Bucket — this check is done in the IsItemBucket() method via another Item class extension method, the IsABucket() method, which also lives in the same namespace as the extension method mentioned above — then the pipeline is aborted.

This next class whose instance serves as the last processor of the custom pipeline generates a URL without the bucket folder names in it:

using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Buckets.Pipelines.GetBucketedItemLinkUrl
{
    public class SetBucketedItemUrl : GetBucketedItemLinkUrlProcessor
    {
        public override void Process(GetBucketedItemLinkUrlArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.GetItemUrl, "args.GetItemUrlMethod");
            Assert.ArgumentNotNull(args.ItemBucket, "args.ItemBucket");
            Assert.ArgumentNotNullOrEmpty(args.DefaultUrl, "args.DefaultUrl");
            string bucketedItemUrl = GetBucketedItemUrl(args);
            if(string.IsNullOrWhiteSpace(bucketedItemUrl))
            {
                args.AbortPipeline();
                return;
            }

            args.BucketedItemUrl = bucketedItemUrl;
        }

        protected virtual string GetBucketedItemUrl(GetBucketedItemLinkUrlArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.GetItemUrl, "args.GetItemUrlMethod");
            Assert.ArgumentNotNull(args.ItemBucket, "args.ItemBucket");
            Assert.ArgumentNotNullOrEmpty(args.DefaultUrl, "args.DefaultUrl");
            string itemBucketUrl = args.GetItemUrl(args.ItemBucket, args.UrlOptions);
            if (string.IsNullOrWhiteSpace(itemBucketUrl))
            {
                return string.Empty;
            }

            string baseUrl = GetExtensionlessUrl(itemBucketUrl);
            string pageUrlPart = GetUrlPagePart(args.DefaultUrl);
            if (string.IsNullOrWhiteSpace(pageUrlPart))
            {
                return string.Empty;
            }

            return string.Join("/", baseUrl, pageUrlPart);
        }

        protected virtual string GetUrlPagePart(string url)
        {
            Assert.ArgumentNotNullOrEmpty(url, "url");
            int lastForwardSlashIndex = url.LastIndexOf("/");
            if (lastForwardSlashIndex < 0)
            {
                return string.Empty;
            }

            return url.Substring(lastForwardSlashIndex + 1);
        }

        protected virtual string GetExtensionlessUrl(string url)
        {
            Assert.ArgumentNotNullOrEmpty(url, "url");
            string extensionlessUrl = url;
            int lastDotIndex = extensionlessUrl.LastIndexOf(".");
            if (lastDotIndex < 0)
            {
                return extensionlessUrl;
            }

            return extensionlessUrl.Substring(0, lastDotIndex);
        }
    }
}

I’m not going to go too much into the details of all the code above. It is basically getting the web page name piece of the URL set in the DefaultUrl property on the arguments object, and appends it to the end of the URL of the Item Bucket without an extension (who uses extensions on URLs anyhow? ¯\_(ツ)_/¯ That’s like so 5 years ago. 😉 ).

The resulting URL is set in the BucketedItemUrl property of the arguments object.

Now that we have code to generate bucket-folder-less URLs, we need to use it. I built the following subclass of Sitecore.Links.LinkProvider which calls it:

using System.Collections.Specialized;

using Sitecore.Buckets.Extensions;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Links;
using Sitecore.Pipelines;

using Sitecore.Sandbox.Buckets.Pipelines.GetBucketedItemLinkUrl;

namespace Sitecore.Sandbox.Buckets.LinkProviders
{
    public class BucketedItemLinkProvider : LinkProvider
    {
        private string GetBucketedItemLinkUrlPipeline { get; set; }

        public override void Initialize(string name, NameValueCollection config)
        {
            Assert.ArgumentNotNullOrEmpty(name, "name");
            Assert.ArgumentNotNull(config, "config");
            base.Initialize(name, config);
            GetBucketedItemLinkUrlPipeline = config["getBucketedItemLinkUrlPipeline"];
        }

        public override string GetItemUrl(Item item, UrlOptions options)
        {
            string url = GetItemUrlFromBase(item, options);
            bool shouldGetBucketedItemUrl = !string.IsNullOrWhiteSpace(GetBucketedItemLinkUrlPipeline) 
                                                && !string.IsNullOrWhiteSpace(url) 
                                                && IsParentBucketFolder(item);
            if (!shouldGetBucketedItemUrl)
            {
                return url;
            }

            string bucketedItemUrl = GetBucketedItemUrl(item, options, url);
            if(string.IsNullOrWhiteSpace(bucketedItemUrl))
            {
                return url;
            }

            return bucketedItemUrl;
        }

        protected virtual bool IsParentBucketFolder(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return item.Parent.IsABucketFolder();
        }

        protected virtual string GetBucketedItemUrl(Item bucketedItem, UrlOptions options, string defaultUrl)
        {
            GetBucketedItemLinkUrlArgs args = new GetBucketedItemLinkUrlArgs
            {
                GetItemUrl = ((someItem, urlOptions) => GetItemUrlFromBase(someItem, urlOptions)),
                UrlOptions = options,
                BucketedItem = bucketedItem,
                DefaultUrl = defaultUrl
            };

            CorePipeline.Run(GetBucketedItemLinkUrlPipeline, args);
            return args.BucketedItemUrl;
        }

        protected virtual string GetItemUrlFromBase(Item item, UrlOptions options)
        {
            return base.GetItemUrl(item, options);
        }
    }
}

I’ve extended the Initialize() method on the base LinkProvider class to read in the name of the custom pipeline (this is set in the patch configuration file further down in this post).

The overridden GetItemUrl() method grabs the URL generated by the same method on the base class for the passed Item. If the custom pipeline’s name is set; the generated URL isn’t null or empty; and the item lives in a bucket folder, the custom pipeline is called with the required parameters set on the arguments object.

If the custom pipeline generated a URL, it is returned to the caller. If not, the URL generated by the base class’ GetItemUrl() method is returned.

You might be thinking “Excellent, Mike! We have code that fixes the issue. Are we done yet?” Not so fast, dear reader — we need write code so Sitecore can resolve these bucket-folder-less URLs.

Since the URLs no longer contain the full path to the bucketed Item, we need a way to find this Item under the Item Bucket. I created the following interface for classes that can find a bucketed Item by name whose ancestor is the Item Bucket:

using Sitecore.Data.Items;

namespace Sitecore.Sandbox.Buckets.Providers.Items
{
    public interface IFindBucketedItemProvider
    {
        Item FindBucketedItemByName(Item bucketItem, string bucketedItemName);
    }
}

The following class implements the interface above:

using System;
using System.Linq;
using System.Linq.Expressions;

using Sitecore.Buckets.Util;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.Linq;
using Sitecore.ContentSearch.Linq.Utilities;
using Sitecore.ContentSearch.SearchTypes;
using Sitecore.ContentSearch.Utilities;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Buckets.Providers.Items
{
    public class FindBucketedItemProvider : IFindBucketedItemProvider
    {
        private ID bucketFolderTemplateId;
        private ID BucketFolderTemplateId
        {
            get
            {
                if(ID.IsNullOrEmpty(bucketFolderTemplateId))
                {
                    bucketFolderTemplateId = GetBucketFolderTemplateId();
                }

                return bucketFolderTemplateId;
            }
        }

        private string PreviewSearchIndexName { get; set; }

        private string LiveSearchIndexName { get; set; }

        private ISearchIndex previewSearchIndex;
        private ISearchIndex PreviewSearchIndex
        {
            get
            {
                if (previewSearchIndex == null)
                {
                    previewSearchIndex = GetPreviewSearchIndex();
                }

                return previewSearchIndex;
            }
        }

        private ISearchIndex liveSearchIndex;
        private ISearchIndex LiveSearchIndex
        {
            get
            {
                if (liveSearchIndex == null)
                {
                    liveSearchIndex = GetLiveSearchIndex();
                }

                return liveSearchIndex;
            }
        }
       
        public virtual Item FindBucketedItemByName(Item bucketItem, string bucketedItemName)
        {
            Assert.ArgumentCondition(!ID.IsNullOrEmpty(BucketFolderTemplateId), "BucketFolderTemplateId", "GetBucketFolderTemplateId() cannot return a null or empty Item ID!");
            Assert.ArgumentNotNull(bucketItem, "bucketItem");
            Assert.ArgumentNotNull(bucketedItemName, "bucketedItemName");

            ISearchIndex searchIndex = GetSearchIndex();
            using (IProviderSearchContext searchContext = searchIndex.CreateSearchContext())
            {
                var predicate = GetSearchPredicate<SearchResultItem>(bucketItem.ID, bucketedItemName);
                IQueryable<SearchResultItem> query = searchContext.GetQueryable<SearchResultItem>().Filter(predicate);
                SearchResults<SearchResultItem> results = query.GetResults();
                if (results.Count() < 1)
                {
                    return null;
                }

                SearchHit<SearchResultItem> hit = results.Hits.First();
                return hit.Document.GetItem();
            }
        }

        protected virtual ISearchIndex GetSearchIndex()
        {
            if (Context.PageMode.IsPreview)
            {
                Assert.IsNotNull(PreviewSearchIndex, "PreviewSearchIndex is null. Double-check the SearchIndexName configuration setting!");
                return PreviewSearchIndex;
            }

            Assert.IsNotNull(LiveSearchIndex, "LiveSearchIndex is null. Double-check the SearchIndexName configuration setting!");
            return LiveSearchIndex;
        }

        protected virtual ISearchIndex GetPreviewSearchIndex()
        {
            Assert.IsNotNullOrEmpty(PreviewSearchIndexName, "PreviewSearchIndexName is empty. Double-check its configuration setting!");
            return GetSearchIndex(PreviewSearchIndexName);
        }

        protected virtual ISearchIndex GetLiveSearchIndex()
        {
            Assert.IsNotNullOrEmpty(LiveSearchIndexName, "LiveSearchIndexName is empty. Double-check its configuration setting!");
            return GetSearchIndex(LiveSearchIndexName);
        }

        protected virtual ISearchIndex GetSearchIndex(string searchIndexName)
        {
            Assert.ArgumentNotNullOrEmpty(searchIndexName, "searchIndexName");
            return ContentSearchManager.GetIndex(searchIndexName);
        }

        protected virtual Expression<Func<T, bool>> GetSearchPredicate<T>(ID bucketItemId, string bucketedItemName) where T : SearchResultItem
        {
            var predicate = PredicateBuilder.True<T>();
            predicate = predicate.And(item => item.Paths.Contains(bucketItemId));
            predicate = predicate.And(item => item.TemplateId != BucketFolderTemplateId);
            predicate = predicate.And(item => item.Parent != bucketItemId);
            predicate = predicate.And(item => item.ItemId != bucketItemId);
            predicate = predicate.And(item => string.Equals(item.Name, bucketedItemName, StringComparison.CurrentCultureIgnoreCase));
            return predicate;
        }

        protected virtual ID GetBucketFolderTemplateId()
        {
            return BucketConfigurationSettings.BucketTemplateId;
        }
    }
}

I’m leveraging the Sitecore.ContentSearch API in the class above to find a bucketed Item with a given name — this is passed as a parameter to the FindBucketedItemByName() method — which is a descendant of the passed Item Bucket.

The GetSearchPredicate() method builds up a “predicate” which basically says “hey, we need an Item that is a descendant of the Item Bucket who is not a bucket folder; isn’t a child of the Item Bucket; isn’t the Item Bucket itself; and has a certain name (though we are ignoring case here)”, and is used by the Sitecore.ContentSearch API code in the FindBucketedItemByName() method.

“Mike, what’s up with the two ISearchIndex instances on the above class?” I have defined two here: one for when we are in Preview mode — I’m using the master search index here — and the other for when we aren’t — this uses the web search index instead.

If results are found, we return the Item instance from the first result in the results collection to the caller.

“Mike, could there ever be multiple Items returned?” Yes, this could happen if we have more than one bucketed Item with the same name, and only the first one in the results collection will be returned. In a future blog post, I will share a solution which will enforce unique Item names for bucketed Items.

Now that we have code that can find a bucketed Item by name under an Item Bucket, we need a custom Item Resolver — this is just a custom <httpRequestBegin> pipeline processor — that uses an instance of the class above to set the context Item for these bucket-folder-less URLs.

The following class does just that:

using System;
using System.Collections.Generic;
using System.Linq;

using Sitecore.Buckets.Extensions;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.HttpRequest;

using Sitecore.Sandbox.Buckets.Providers.Items;

namespace Sitecore.Sandbox.Buckets.Pipelines.HttpRequest
{
    public class BucketedItemResolver : HttpRequestProcessor
    {
        private List<string> TargetSites { get; set; }

        private IFindBucketedItemProvider FindBucketedItemProvider { get; set; }

        public BucketedItemResolver()
        {
            TargetSites = new List<string>();
        }

        public override void Process(HttpRequestArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if(!ShouldProcess(args))
            {
                return;
            }

            StartProfilerOperation();
            string path = MainUtil.DecodeName(args.Url.ItemPath);
            if (string.IsNullOrWhiteSpace(path))
            {
                EndProfilerOperation();
                return;
            }

            int lastForwardSlashIndex = path.LastIndexOf("/");
            if (lastForwardSlashIndex < 0)
            {
                EndProfilerOperation();
                return;
            }

            string parentPath = path.Substring(0, lastForwardSlashIndex);
            Item parentItem = args.GetItem(parentPath);
            if(parentItem == null)
            {
                EndProfilerOperation();
                return;
            }

            if (!parentItem.IsABucket())
            {
                EndProfilerOperation();
                return;
            }
            
            string bucketedItemName = path.Substring(lastForwardSlashIndex + 1);
            Item bucketedItem = FindBucketedItemByName(parentItem, bucketedItemName);
            if(bucketedItem == null)
            {
                EndProfilerOperation();
                return;
            }

            Context.Item = bucketedItem;
            EndProfilerOperation();
        }

        protected virtual bool ShouldProcess(HttpRequestArgs args)
        {
            return Context.Item == null
                    && Context.Database != null
                    && IsTargetSite()
                    && !string.IsNullOrWhiteSpace(args.Url.ItemPath);
        }

        protected virtual bool IsTargetSite()
        {
            return Context.Site != null
                    && TargetSites != null
                    && TargetSites.Any(site => string.Equals(site, Context.Site.Name, StringComparison.CurrentCultureIgnoreCase));
        }

        protected virtual void StartProfilerOperation()
        {
            Profiler.StartOperation("Resolve current bucketed item.");
        }

        protected virtual void EndProfilerOperation()
        {
            Profiler.EndOperation();
        }

        protected virtual Item FindBucketedItemByName(Item bucketItem, string bucketedItemName)
        {
            Assert.IsNotNull(FindBucketedItemProvider, "IFindBucketedItemProvider must be set in configuration!");
            return FindBucketedItemProvider.FindBucketedItemByName(bucketItem, bucketedItemName);
        }
    }
}

Not to go too much into all of the code above, the Process() method basically determines whether it should move forward on processing the request.

When should it do that? If there isn’t already context Item set; the context Database is set; the request is being made in a targeted site — basically this is just a list of site names sourced in the patch configuration file below which is a list of websites we want this code to run in (we don’t want to this code to run in the “shell” website which is what the Sitecore Desktop and Content Editor use); and have an Item path, then the Process() method should continue.

Other code in the Process() method is extracting out the parent’s page’s Item path; grabs an instance of the parent Item; determines whether it is an Item Bucket — if it’s not, then the code exits; the name of the page Item from URL; and then passes the parent Item and Item name to the FindBucketedItemByName() method which delegates to the FindBucketedItemByName() method on the IFindBucketedItemProvider instance to find the bucketed Item.

If a bucketed Item was found, the Process() method sets this as the context Item. Otherwise, it just exits.

I’m also using profiling code in the above class just as can be seen in the “out of the box” Sitecore.Pipelines.HttpRequest.ItemResolver class — this lives in Sitecore.Kernel.dll.

I then super-glued all of the code above in the following patch configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <buckets>
      <providers>
        <items>
          <findBucketedItemProvider type="Sitecore.Sandbox.Buckets.Providers.Items.FindBucketedItemProvider, Sitecore.Sandbox">
            <PreviewSearchIndexName>sitecore_master_index</PreviewSearchIndexName>
            <LiveSearchIndexName>sitecore_web_index</LiveSearchIndexName>
          </findBucketedItemProvider>
        </items>
      </providers>
    </buckets>
    <linkManager>
      <providers>
        <add name="sitecore">
          <patch:attribute name="type">Sitecore.Sandbox.Buckets.LinkProviders.BucketedItemLinkProvider, Sitecore.Sandbox</patch:attribute>
          <patch:attribute name="getBucketedItemLinkUrlPipeline">getBucketedItemLinkUrl</patch:attribute>
        </add>
      </providers>
    </linkManager>
    <pipelines>
      <getBucketedItemLinkUrl>
        <processor type="Sitecore.Sandbox.Buckets.Pipelines.GetBucketedItemLinkUrl.EnsureParameters, Sitecore.Sandbox" />
        <processor type="Sitecore.Sandbox.Buckets.Pipelines.GetBucketedItemLinkUrl.InspectBucketedItem, Sitecore.Sandbox" />
        <processor type="Sitecore.Sandbox.Buckets.Pipelines.GetBucketedItemLinkUrl.SetItemBucket, Sitecore.Sandbox" />
        <processor type="Sitecore.Sandbox.Buckets.Pipelines.GetBucketedItemLinkUrl.SetBucketedItemUrl, Sitecore.Sandbox" />
      </getBucketedItemLinkUrl>
      <httpRequestBegin>
        <processor patch:before="processor[@type='Sitecore.Pipelines.HttpRequest.ItemResolver, Sitecore.Kernel']"
                   type="Sitecore.Sandbox.Buckets.Pipelines.HttpRequest.BucketedItemResolver, Sitecore.Sandbox">
          <TargetSites hint="list">
            <site>website</site>
          </TargetSites>
          <FindBucketedItemProvider ref="buckets/providers/items/findBucketedItemProvider" />
        </processor>  
      </httpRequestBegin>
    </pipelines>
  </sitecore>
</configuration>

Let’s see if this works.

Let’s insert some bucketed Item links into this Rich Text field on my home Item:

bucketed-links-insert-link-rtf

Let’s insert an internal link to this Item:

bucketed-links-insert-link-1

Let’s insert another link to this Item:

bucketed-links-insert-link-2

Let’s insert yet another internal link — let’s insert a link to this Item:

bucketed-links-insert-link-3

After publishing and navigating to my home page, I see this in the html for the links:

bucketed-links-html-rendered

After clicking on the first link, I’m brought to the bucketed Item but have a look at the URL:

bucketed-links-bucketed-item-page

As you can see it worked!

When I finally got this working, I found myself doing a happy dance:

techno-chicken

techno-chickens

If you have any thoughts on this, please drop a comment.

Download Images and Save to the Media Library Via a Custom Content Editor Image Field in Sitecore

Yesterday evening — a Friday evening by the way (what, you don’t code on Friday evenings? 😉 ) — I wanted to have a bit of fun by building some sort of customization in Sitecore but was struggling on what to build.

After about an hour of pondering, it dawned on me: I was determined to build a custom Content Editor Image field that gives content authors the ability to download images from a supplied URL; save the image to disk; upload the image into the Media Libary; and then set it on a custom Image field of an Item.

I’m sure someone has built something like this in the past and may have even uploaded a module that does this to the Sitecore Marketplace — I didn’t really look into whether this had already been done before since I wanted to have some fun by taking on the challenge. What follows is the fruit of that endeavor.

Before I move forward, I would like to caution you on using the code that follows — I have not rigorously tested this code at all so use at your own risk.

Before I began coding, I thought about how I wanted to approach this challenge. I decided I would build a custom Sitecore pipeline to handle this code. Why? Well, quite frankly, it gives you flexibility on customization, and also native Sitecore code is hugely composed of pipelines — why deviate from the framework?

First, I needed a class whose instances would serve as the custom pipeline’s arguments object. The following class was built for that:

using Sitecore.Data;
using Sitecore.Pipelines;

namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary
{
    public class DownloadImageToMediaLibraryArgs : PipelineArgs
    {
        public Database Database { get; set; }

        public string ImageFileName { get; set; }

        public string ImageFilePath { get; set; }

        public string ImageItemName { get; set; }

        public string ImageUrl { get; set; }

        public string MediaId { get; set; }

        public string MediaLibaryFolderPath { get; set; }

        public string MediaPath { get; set; }

        public bool FileBased { get; set; }

        public bool IncludeExtensionInItemName { get; set; }

        public bool OverwriteExisting { get; set; }

        public bool Versioned { get; set; }
    }
}

I didn’t just start off with all of the properties you see on this class — it was an iterative process where I had to go back, add more and even remove some that were no longer needed. You will see why I have these on it from the code below.

I decided to employ the Template method pattern in this code, and defined the following abstract base class which all processors of my custom pipeline will sub-class:

using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary
{
    public abstract class DownloadImageToMediaLibraryProcessor
    {
        public void Process(DownloadImageToMediaLibraryArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if(!CanProcess(args))
            {
                AbortPipeline(args);
                return;
            }

            Execute(args);
        }

        protected abstract bool CanProcess(DownloadImageToMediaLibraryArgs args);

        protected virtual void AbortPipeline(DownloadImageToMediaLibraryArgs args)
        {
            args.AbortPipeline();
        }

        protected abstract void Execute(DownloadImageToMediaLibraryArgs args);
    }
}

All processors of the custom pipeline will have to implement the CanProcess and Execute methods above, and also have the ability to redefine the AbortPipeline method if needed.

The main magic for all processors happen in the Process method above — if the processor can process the data supplied via the arguments object, then it will do so using the Execute method. Otherwise, the pipeline will be aborted via the AbortPipeline method.

The following class serves as the first processor of the custom pipeline.

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.IO;

namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary
{
    public class SetProperties : DownloadImageToMediaLibraryProcessor
    {
        private string UploadDirectory { get; set; }

        protected override bool CanProcess(DownloadImageToMediaLibraryArgs args)
        {
            Assert.IsNotNullOrEmpty(UploadDirectory, "UploadDirectory must be set in configuration!");
            Assert.IsNotNull(args.Database, "args.Database must be supplied!");
            return !string.IsNullOrWhiteSpace(args.ImageUrl)
                && !string.IsNullOrWhiteSpace(args.MediaLibaryFolderPath);
        }

        protected override void Execute(DownloadImageToMediaLibraryArgs args)
        {
            args.ImageFileName = GetFileName(args.ImageUrl);
            args.ImageItemName = GetImageItemName(args.ImageUrl);
            args.ImageFilePath = GetFilePath(args.ImageFileName);
        }

        protected virtual string GetFileName(string url)
        {
            Assert.ArgumentNotNullOrEmpty(url, "url");
            return FileUtil.GetFileName(url);
        }

        protected virtual string GetImageItemName(string url)
        {
            Assert.ArgumentNotNullOrEmpty(url, "url");
            string fileNameNoExtension = GetFileNameNoExtension(url);
            if(string.IsNullOrWhiteSpace(fileNameNoExtension))
            {
                return string.Empty;
            }

            return ItemUtil.ProposeValidItemName(fileNameNoExtension);
        }

        protected virtual string GetFileNameNoExtension(string url)
        {
            Assert.ArgumentNotNullOrEmpty(url, "url");
            return FileUtil.GetFileNameWithoutExtension(url);
        }

        protected virtual string GetFilePath(string fileName)
        {
            Assert.ArgumentNotNullOrEmpty(fileName, "fileName");
            return string.Format("{0}/{1}", FileUtil.MapPath(UploadDirectory), fileName);
        }
    }
}

Instances of the above class will only run if an upload directory is supplied via configuration (see the patch include configuration file down below); a Sitecore Database is supplied (we have to upload this image somewhere); an image URL is supplied (can’t download an image without this); and a Media Library folder is supplied (where are we storing this image?).

The Execute method then sets additional properties on the arguments object that the next processors will need in order to complete their tasks.

The following class serves as the second processor of the custom pipeline. This processor will download the image from the supplied URL:

using System.Net;

namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary
{
    public class DownloadImage : DownloadImageToMediaLibraryProcessor
    {
        protected override bool CanProcess(DownloadImageToMediaLibraryArgs args)
        {
            return !string.IsNullOrWhiteSpace(args.ImageUrl)
                && !string.IsNullOrWhiteSpace(args.ImageFilePath);
        }

        protected override void Execute(DownloadImageToMediaLibraryArgs args)
        {
            using (WebClient client = new WebClient())
            {
                client.DownloadFile(args.ImageUrl, args.ImageFilePath);
            }
        }
    }
}

The processor instance of the above class will only execute when an image URL is supplied and a location on the file system is given — this is the location on the file system where the image will live before being uploaded into the Media Library.

If all checks out, the image is downloaded from the given URL into the specified location on the file system.

The next class serves as the third processor of the custom pipeline. This processor will upload the image on disk to the Media Library:

using System.IO;

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Resources.Media;
using Sitecore.Sites;

namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary
{
    public class UploadImageToMediaLibrary : DownloadImageToMediaLibraryProcessor
    {
        private string Site { get; set; }

        protected override bool CanProcess(DownloadImageToMediaLibraryArgs args)
        {
            Assert.IsNotNullOrEmpty(Site, "Site must be set in configuration!");
            return !string.IsNullOrWhiteSpace(args.MediaLibaryFolderPath)
                && !string.IsNullOrWhiteSpace(args.ImageItemName)
                && !string.IsNullOrWhiteSpace(args.ImageFilePath)
                && args.Database != null;
        }

        protected override void Execute(DownloadImageToMediaLibraryArgs args)
        {
            MediaCreatorOptions options = new MediaCreatorOptions
            {
                Destination = GetMediaLibraryDestinationPath(args),
                FileBased = args.FileBased,
                IncludeExtensionInItemName = args.IncludeExtensionInItemName,
                OverwriteExisting = args.OverwriteExisting,
                Versioned = args.Versioned,
                Database = args.Database
            };
            
            MediaCreator creator = new MediaCreator();
            MediaItem mediaItem;
            using (SiteContextSwitcher switcher = new SiteContextSwitcher(GetSiteContext()))
            {
                using (FileStream fileStream = File.OpenRead(args.ImageFilePath))
                {
                    mediaItem = creator.CreateFromStream(fileStream, args.ImageFilePath, options);
                }
            }
            
            if (mediaItem == null)
            {
                AbortPipeline(args);
                return;
            }
            
            args.MediaId = mediaItem.ID.ToString();
            args.MediaPath = mediaItem.MediaPath;
        }

        protected virtual SiteContext GetSiteContext()
        {
            SiteContext siteContext = SiteContextFactory.GetSiteContext(Site);
            Assert.IsNotNull(siteContext, string.Format("The site: {0} does not exist!", Site));
            return siteContext;
        }

        protected virtual string GetMediaLibraryDestinationPath(DownloadImageToMediaLibraryArgs args)
        {
            return string.Format("{0}/{1}", args.MediaLibaryFolderPath, args.ImageItemName);
        }
    }
}

The processor instance of the class above will only run when we have a Media Library folder location; an Item name for the image; a file system path for the image; and a Database to upload the image to. I also ensure a “site” is supplied via configuration so that I can switch the site context — when using the default of “shell”, I was being brought to the image Item in the Media Library after it was uploaded which was causing the image not to be set on the custom Image field on the Item.

If everything checks out, we upload the image to the Media Library in the specified location.

The next class serves as the last processor of the custom pipeline. This processor just deletes the image from the file system (why keep it around since we are done with it?):

using Sitecore.IO;

namespace Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary
{
    public class DeleteImageFromFileSystem : DownloadImageToMediaLibraryProcessor
    {
        protected override bool CanProcess(DownloadImageToMediaLibraryArgs args)
        {
            return !string.IsNullOrWhiteSpace(args.ImageFilePath)
                && FileUtil.FileExists(args.ImageFilePath);
        }

        protected override void Execute(DownloadImageToMediaLibraryArgs args)
        {
            FileUtil.Delete(args.ImageFilePath);
        }
    }
}

The processor instance of the class above can only delete the image if its path is supplied and the file exists.

If all checks out, the image is deleted.

The next class is the class that serves as the custom Image field:

using System;
using System.Net;

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Pipelines;
using Sitecore.Shell.Framework;
using Sitecore.Web.UI.Sheer;

using Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor
{
    public class Image : Sitecore.Shell.Applications.ContentEditor.Image
    {
        public Image()
            : base()
        {
        }

        public override void HandleMessage(Message message)
        {
            Assert.ArgumentNotNull(message, "message");
            if (string.Equals(message.Name, "contentimage:download", StringComparison.CurrentCultureIgnoreCase))
            {
                GetInputFromUser();
                return;
            }

            base.HandleMessage(message);
        }

        protected void GetInputFromUser()
        {
            RunProcessor("GetImageUrl", new ClientPipelineArgs());
        }

        protected virtual void GetImageUrl(ClientPipelineArgs args)
        {
            if (!args.IsPostBack)
            {
                SheerResponse.Input("Enter the url of the image to download:", string.Empty);
                args.WaitForPostBack();
            }
            else if (args.HasResult && IsValidUrl(args.Result))
            {
                args.Parameters["imageUrl"] = args.Result;
                args.IsPostBack = false;
                RunProcessor("ChooseMediaLibraryFolder", args);
            }
            else
            {
                CancelOperation(args);
            }
        }

        protected virtual bool IsValidUrl(string url)
        {
            if (string.IsNullOrWhiteSpace(url))
            {
                return false;
            }

            try
            {
                HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);
                request.Method = "HEAD";
                request.GetResponse();
            }
            catch (Exception ex)
            {
                SheerResponse.Alert("The specified url is not valid. Please try again.");
                return false;
            }

            return true;
        }

        protected virtual void RunProcessor(string processor, ClientPipelineArgs args)
        {
            Assert.ArgumentNotNullOrEmpty(processor, "processor");
            Sitecore.Context.ClientPage.Start(this, processor, args);
        }

        public void ChooseMediaLibraryFolder(ClientPipelineArgs args)
        {
            if (!args.IsPostBack)
            {
                Dialogs.BrowseItem
                (
                    "Select A Media Library Folder",
                    "Please select a media library folder to store this image.",
                    "Applications/32x32/folder_into.png",
                    "OK",
                    "/sitecore/media library", 
                    string.Empty
                );

                args.WaitForPostBack();
            }
            else if (args.HasResult)
            {
                Item folder = Client.ContentDatabase.Items[args.Result];
                args.Parameters["mediaLibaryFolderPath"] = folder.Paths.FullPath;
                RunProcessor("DownloadImage", args);
            }
            else
            {
                CancelOperation(args);
            }
        }

        protected virtual void DownloadImage(ClientPipelineArgs args)
        {
            DownloadImageToMediaLibraryArgs downloadArgs = new DownloadImageToMediaLibraryArgs
            {
                Database = Client.ContentDatabase,
                ImageUrl = args.Parameters["imageUrl"],
                MediaLibaryFolderPath = args.Parameters["mediaLibaryFolderPath"]
            };

            CorePipeline.Run("downloadImageToMediaLibrary", downloadArgs);
            SetMediaItemInField(downloadArgs);
        }

        protected virtual void SetMediaItemInField(DownloadImageToMediaLibraryArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            if(string.IsNullOrWhiteSpace(args.MediaId) || string.IsNullOrWhiteSpace(args.MediaPath))
            {
                return;
            }

            XmlValue.SetAttribute("mediaid", args.MediaId);
            Value = args.MediaPath;
            Update();
            SetModified();
        }

        protected virtual void CancelOperation(ClientPipelineArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            args.AbortPipeline();
        }
    }
}

The class above subclasses the Sitecore.Shell.Applications.ContentEditor.Image class — this lives in Sitecore.Kernel.dll — which is the “out of the box” Content Editor Image field. The Sitecore.Shell.Applications.ContentEditor.Image class provides hooks that we can override in order to augment functionality which I am doing above.

The magic of this class starts in the HandleMessage method — I intercept the message for a Menu item option that I define below for downloading an image from a URL.

If we are to download an image from a URL, we first prompt the user for a URL via the GetImageUrl method using a Sheer UI api call (note: I am running these methods as one-off client pipeline processors as this is the only way you can get Sheer UI to run properly).

If we have a valid URL, we then prompt the user for a Media Library location via another Sheer UI dialog (this is seen in the ChooseMediaLibraryFolder method).

If the user chooses a location in the Media Library, we then call the DownloadImage method as a client pipeline processor — I had to do this since I was seeing some weird behavior on when the image was being saved into the Media Library — which invokes the custom pipeline for downloading the image to the file system; uploading it into the Media Library; and then removing it from disk.

I then duct-taped everything together using the following patch include configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <controlSources>
      <source mode="on" namespace="Sitecore.Sandbox.Shell.Applications.ContentEditor" assembly="Sitecore.Sandbox" prefix="sandbox-content"/>
    </controlSources>
    <overrideDialogs>
      <override dialogUrl="/sitecore/shell/Applications/Item%20browser.aspx" with="/sitecore/client/applications/dialogs/InsertSitecoreItemViaTreeDialog">
        <patch:delete/>
      </override>
    </overrideDialogs>
    <pipelines>
      <downloadImageToMediaLibrary>
        <processor type="Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary.SetProperties, Sitecore.Sandbox">
          <UploadDirectory>/upload</UploadDirectory>
        </processor>  
        <processor type="Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary.DownloadImage, Sitecore.Sandbox" />
        <processor type="Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary.UploadImageToMediaLibrary, Sitecore.Sandbox">
          <Site>website</Site>
        </processor>
        <processor type="Sitecore.Sandbox.Pipelines.DownloadImageToMediaLibrary.DeleteImageFromFileSystem, Sitecore.Sandbox" />
      </downloadImageToMediaLibrary>
    </pipelines>
  </sitecore>
</configuration>

One thing to note in the above file: I’ve disabled the SPEAK dialog for the Item Browser — you can see this in the <overrideDialogs> xml element — as I wasn’t able to set messaging text on it but could do so using the older Sheer UI dialog.

Now that all code is in place, we need to tell Sitecore that we have a new Image field. I do so by defining it in the Core database:

external-image-core-1

We also need a new Menu item for the “Download Image” link:

external-image-core-2

Let’s take this for a spin!

I added a new field using the custom Image field type to my Sample item template:

template-new-field-external-image

As you can see, we have this new Image field on my “out of the box” home item. Let’s click the “Download Image” link:

home-external-image-1

I was then prompted with a dialog to supply an image URL. I pasted one I found on the internet:

home-external-image-2

After clicking “OK”, I was prompted with another dialog to choose a Media Library location for storing the image. I chose some random folder:

home-external-image-3

After clicking “OK” on that dialog, the image was magically downloaded from the internet; uploaded into the Media Library; and set in the custom Image field on my home item:

home-external-image-4

If you have any comments, thoughts or suggestions on this, please drop a comment.

Addendum:
It’s not a good idea to use the /upload directory for temporarily storing download images — see this post for more details.

If you decide to use this solution — by the way, use this solution at your own risk 😉 — you will have to change the /upload directory in the patch include configuration file above.

Yet Another <httpRequestBegin> Pipeline Processor to Handle ‘Page Not Found’ (404 Status Code) in Sitecore

Last week I pair programmed with fellow Sitecore MVP Akshay Sura on a class that would serve as an <httpRequestBegin> pipeline processor to serve up ‘Page Not Found’ content along with a 404 status code when a user requests a page that does not exist as an Item in the Sitecore XP.

In this solution, the page does not redirect to the ‘Not Found’ page since this results in a 302 status code which isn’t ideal for SEO. Instead, the ‘Page Not Found’ content should appear on the page with the ‘Not Found’ request.

We decided to have our <httpRequestBegin> pipeline processor class not inherit from Sitecore.Pipelines.HttpRequest.ExecuteRequest — this lives in Sitecore.Kernel.dll — as can be seen in the following blog posts:

Why? The solutions in the above are a bit fragile given that they are subclassing Sitecore.Pipelines.HttpRequest.ExecuteRequest which is an example of tight coupling — code changes in Sitecore.Pipelines.HttpRequest.ExecuteRequest could potentially break code within the subclasses.

Further, the implementations of the RedirectOnItemNotFound() method in the above blog posts don’t redirect unless an Exception is encountered which is a bit awkward given the name of the method.

I’m not going to share the exact solution that Akshay and I had built in this blog post. Instead, I’m going to share one that is quite similar — actually the solution below is an enhancement of the solution we had come up with. I added some caching and a few other things (basically put more things into Sitecore configuration so that the solutions is more extendable/changeable):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Links;
using Sitecore.Pipelines.HttpRequest;
using Sitecore.Web;

using Sitecore.Sandbox.Caching;

namespace Sitecore.Sandbox.Pipelines.HttpRequest
{
    public class HandleItemNotFound : HttpRequestProcessor
    {
        private string TargetWebsite { get; set; }

        private string StatusDescription { get; set; }

        private List<string> RelativeUrlPrefixesToIgnore { get; set; }

        protected ICacheProvider CacheProvider { get; private set; }

        protected string CacheKey { get; private set; }

        public HandleItemNotFound()
        {
            RelativeUrlPrefixesToIgnore = new List<string>();
        }

        public override void Process(HttpRequestArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            bool shouldExit = Sitecore.Context.Item != null 
                                || !string.Equals(Context.Site.Name, TargetWebsite, StringComparison.CurrentCultureIgnoreCase) 
                                || StartsWithPrefixToIgnore(args.Url.FilePath);
            if (shouldExit)
            {
                return;
            }

            string notFoundPageItemPath = Sitecore.Context.Site.Properties["notFoundPageItemPath"];
            if (string.IsNullOrWhiteSpace(notFoundPageItemPath))
            {
                return;
            }

            Database database = GetDatabase();
            if (database == null)
            {
                return;
            }

            Item notFoundItem = database.GetItem(notFoundPageItemPath);
            if (notFoundItem == null)
            {
                return;
            }

            string notFoundContent = GetNotFoundPageContent(args, database, notFoundPageItemPath);
            if(!string.IsNullOrWhiteSpace(notFoundContent))
            {
                args.Context.Response.TrySkipIisCustomErrors = true;
                args.Context.Response.StatusCode = 404;
                if (!string.IsNullOrWhiteSpace(StatusDescription))
                {
                    args.Context.Response.StatusDescription = StatusDescription;
                }

                args.Context.Response.Write(notFoundContent);
                args.Context.Response.End();
                return;
            }

            Log.Warn("The 'Not Found Page: {0} shows no content when rendered!", notFoundItem.Paths.FullPath);
        }

        protected virtual bool StartsWithPrefixToIgnore(string url)
        {
            return !string.IsNullOrWhiteSpace(url) && RelativeUrlPrefixesToIgnore.Any(prefix => url.StartsWith(prefix));
        }

        protected virtual Database GetDatabase()
        {
            return Context.ContentDatabase ?? Context.Database;
        }

        protected virtual string GetNotFoundPageContent(HttpRequestArgs args, Database database, string notFoundPageItemPath)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(database, "database");
            Assert.ArgumentNotNullOrEmpty(notFoundPageItemPath, "notFoundPageItemPath");
            string cacheKey = GetCacheKey();
            string content = GetNotFoundPageContentFromCache();
            if(!string.IsNullOrWhiteSpace(content))
            {
                return content;
            }

            Item notFoundItem = database.GetItem(notFoundPageItemPath);
            if (notFoundItem == null)
            {
                return string.Empty;
            }

            string domain = GetDomain(args);
            string url = LinkManager.GetItemUrl(notFoundItem);
            try
            {
                content = WebUtil.ExecuteWebPage(string.Concat(domain, url));
                AddNotFoundPageContentFromCache(content);
                return content;
            }
            catch (Exception ex)
            {
                Log.Error(string.Format("{0} Error - domain: {1}, url: {2}", ToString(), domain, url), ex, this);
            }

            return string.Empty;
        }

        protected virtual string GetNotFoundPageContentFromCache()
        {
            Assert.IsNotNull(CacheProvider, "CacheProvider must be set in configuration!");
            return CacheProvider[GetCacheKey()] as string;
        }

        protected virtual void AddNotFoundPageContentFromCache(string content)
        {
            Assert.IsNotNull(CacheProvider, "CacheProvider must be set in configuration!");
            if(string.IsNullOrWhiteSpace(content))
            {
                return;
            }

            CacheProvider.Add(GetCacheKey(), content);
        }

        protected virtual string GetCacheKey()
        {
            Assert.IsNotNullOrEmpty(CacheKey, "CacheKey must be set in configuration!");
            return CacheKey;
        }

        protected virtual string GetDomain(HttpRequestArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            return args.Context.Request.Url.GetComponents(UriComponents.Scheme | UriComponents.Host, UriFormat.Unescaped);
        }
    }
}

The code in the Process() method above determines whether it should execute. It should only execute when Sitecore.Context.Item is null — this means that previous <httpRequestBegin> pipeline processors could not ascertain which Sitecore Item should be served up for the request — and if the relative url does not start with one of the prefixes to ignore — for example, we don’t want this code to run for media library Item requests which all start with /~/ in a stock Sitecore instance.

Further, the path to the ‘Page Not Found’ Item must be set on the site node within Sitecore configuration. If this is not set, then the code will not execute.

If the code should execute, it tries to grab the ‘Page Not Found’ content from cache — the class above reuses the CacheProvider class which I wrote for my post on storing data outside of the Sitecore XP but using the Sitecore API.

If this does not exist in cache, we basically make a request to the ‘Page Not Found’ Item using Sitecore.Web.WebUtil.ExecuteWebPage; put this content in cache; and then return it to the Process() method.

If there is content to display, we send it out to the response stream.

I then glued everything together using the following patch configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <httpRequestBegin>
        <processor patch:before="processor[@type='Sitecore.Pipelines.HttpRequest.ExecuteRequest, Sitecore.Kernel']"
                   type="Sitecore.Sandbox.Pipelines.HttpRequest.HandleItemNotFound, Sitecore.Sandbox">
          <TargetWebsite>website</TargetWebsite>
          <StatusDescription>Page Not Found</StatusDescription>
          <RelativeUrlPrefixesToIgnore hint="list">
            <Prefix>/~/</Prefix>
          </RelativeUrlPrefixesToIgnore>
          <CacheProvider type="Sitecore.Sandbox.Caching.CacheProvider, Sitecore.Sandbox">
            <param desc="cacheName">[404]</param>
            <param desc="cacheSize">500KB</param>
          </CacheProvider>
          <CacheKey>404Content</CacheKey>
        </processor>
      </httpRequestBegin>
    </pipelines>
    <sites>
      <site name="website">
        <patch:attribute name="notFoundPageItemPath">/sitecore/content/Home/404</patch:attribute>
      </site>
    </sites>
  </sitecore>
</configuration>

In the above configuration file, I am injecting this <httpRequestBegin> pipeline processor to execute before the Sitecore.Pipelines.HttpRequest.ExecuteRequest <httpRequestBegin> pipeline processor.

Let’s see this in action.

I set up an Item in Sitecore to serve as my ‘Page Not Found’ page Item:

404-item

After publishing and navigating to a page url that does not exist in my instance, I get the following:

nope

As you can see, we get the rendered page content for the 404 Item yet stay on the original requested nonexistent page (/nope).

If you have any comments or thoughts on this, please share in a comment.