Home » Design Patterns » Template method pattern

Category Archives: Template method pattern

Restart Your Sitecore Content Management (CM) and Content Delivery (CD) Instances using Custom Events

Last Friday, I had worked on a PowerShell script which is executed by clicking a custom button in the Sitecore Content Editor ribbon using the Content Editor Ribbon integration point offered through Sitecore PowerShell Extensions (SPE) in a UAT environment. This UAT environment has separate Content Management (CM) and Content Delivery (CD) Sitecore instances on two different servers. The script updates some records in an external datasource (data not in Sitecore) but its data is cached in the Sitecore instances outside of the stand Sitecore caching framework. In order to see these updates on the sites which live on these Sitecore instances , I needed a programmatic way to restart all Sitecore instances in the UAT environment I had built this solution for, and the following solution reflects what I had worked on to accomplish this spanning Friday until today — this post defines two custom events in Sitecore — just so I could get this out as soon as possible as it may help you understand custom events, and maybe even help you do something similar to what I am doing here.

I do want to caution you on using code as follows in a production environment — this could cause disruption to your production CDs which are live to the public; be sure to take your CD servers out of a load-balancer before restarting the Sitecore instance on them as you don’t want people showing up to your door with pitchforks and torches asking why production had been down for a bit.

I’ve also incorporated the concept of Sitecore configuration-driven feature toggles which I had discussed in a previous blog post where I had altered the appearance of the Sitecore content tree using a pipeline-backed MasterDataView, and will be repurposing code from that post here — I strongly suggest reading that post to understand the approach being used for this post as I will not discuss much how this works on this post.

Moreover, I have used numerous Configuration Objects sourced from the Sitecore IoC container in this post which I had discuss in another previous post; you might also want to have a read of that post before proceeding in order to understand why I am decorating some classes on this post with the [ServiceConfigObject] attribute.

I first defined the following Configuration Object which will live in the Sitecore IoC container:

using Foundation.DependencyInjection;
using Foundation.DependencyInjection.Enums;
using Foundation.Kernel.Services.FeatureToggle;

namespace Foundation.Kernel.Models.RestartServer
{
	[ServiceConfigObject(ConfigPath = "moduleSettings/foundation/kernel/restartServerSettings", Lifetime = Lifetime.Singleton)]
	public class RestartServerSettings : IFeatureToggleable
	{
		public bool Enabled { get; set; }
	}
}

This Configuration Object will act is the main feature toggle to turn the entire feature on/off based on the Enabled property’s value from Sitecore configuration (see the Sitecore patch configuration file towards the bottom of this post).

I then created the following interface which will ascertain when the entire feature should be turned on/off at a global level, or at an individual piece of functionality’s level:

using Foundation.Kernel.Services.FeatureToggle;

namespace Foundation.Kernel.Services.RestartServer
{
	public interface IRestartServerFeatureToggleService
	{
		bool IsFeatureEnabled();

		bool IsEnabled(IFeatureToggleable toggleable);
	}
}

The following is the implementation of the interface above.

using Foundation.Kernel.Models.RestartServer;
using Foundation.Kernel.Services.FeatureToggle;

namespace Foundation.Kernel.Services.RestartServer
{
	public class RestartServerFeatureToggleService : IRestartServerFeatureToggleService
	{
		private readonly RestartServerSettings _settings;
		private readonly IFeatureToggleService _featureToggleService;

		public RestartServerFeatureToggleService(RestartServerSettings settings, IFeatureToggleService featureToggleService)
		{
			_settings = settings;
			_featureToggleService = featureToggleService;
		}

		public bool IsEnabled(IFeatureToggleable toggleable) => _featureToggleService.IsEnabled(_settings, toggleable);

		public bool IsFeatureEnabled() => _featureToggleService.IsEnabled(_settings);
	}
}

I won’t explain much here as I had talked about feature toggles on a previous configuration-driven post; I suggest reading that to understand this pattern.

Now that we have our feature toggle magic defined above, let’s dive into the code which defines and handles custom events which do the actual restarting of the Sitecore instances.

I created the following Config Object to hold log messaging formatted strings which we will use when writing to the Sitecore log when restarting Sitecore instances:

using Foundation.DependencyInjection;
using Foundation.DependencyInjection.Enums;

namespace Foundation.Kernel.Models.RestartServer.Events
{
	[ServiceConfigObject(ConfigPath = "moduleSettings/foundation/kernel/restartServerEventHandlerSettings", Lifetime = Lifetime.Singleton)]
	public class RestartServerEventHandlerSettings
	{
		public string RestartServerLogMessageFormat { get; set; }

		public string RestartServerRemoteLogMessageFormat { get; set; }
	}
}

We need a way to identify the Sitecore instance we are on in order to determine if the Sitecore instance needs to be restarted, and also when logging information that a restart is happening on that instance. The following Config Object will provide the name of a Sitecore setting which holds thie identifier for the Sitecore instance we are on:

using Foundation.DependencyInjection;
using Foundation.DependencyInjection.Enums;

namespace Foundation.Kernel.Models.Server
{
	[ServiceConfigObject(ConfigPath = "moduleSettings/foundation/kernel/serverServiceSettings", Lifetime = Lifetime.Singleton)]
	public class ServerServiceSettings
	{
		public string InstanceNameSetting { get; set; }
	}
}

The following interface/implementation class will provide the name of the Sitecore instance we are on:

namespace Foundation.Kernel.Services.Server
{
	public interface IServerService
	{
		string GetCurrentServerName();
	}
}

Here’s the implementation of the interface above:

using Sitecore.Abstractions;

using Foundation.Kernel.Models.Server;

namespace Foundation.Kernel.Services.Server
{
	public class ServerService : IServerService
	{
		private readonly ServerServiceSettings _settings;
		private readonly BaseSettings _settingsProvider;

		public ServerService(ServerServiceSettings settings, BaseSettings settingsProvider)
		{
			_settings = settings;
			_settingsProvider = settingsProvider;
		}

		public string GetCurrentServerName() => GetSetting(GetInstanceNameSetting());

		protected virtual string GetInstanceNameSetting() => _settings.InstanceNameSetting;

		protected virtual string GetSetting(string settingName) => _settingsProvider.GetSetting(settingName);
	}
}

The class above consumes the ServerServiceSettings config object then uses the BaseSettings service to get the name of the Sitecore instance based on the value in the setting, and returns it to the caller. There’s really nothing more to it.

Back in 2014, I had written a post on restarting your Sitecore instance using a custom FileWatcher, and in that post I had used the RestartServer() method on the Sitecore.Install.Installer class in Sitecore.Kernel. I will be using this same class but backed by a service class. Here is the interface of that service class:

namespace Foundation.Kernel.Services.Installer
{
	public interface IInstallerService
	{
		void RestartServer();
	}
}

The following implements the interface above:

namespace Foundation.Kernel.Services.Installer
{
	public class InstallerService : IInstallerService
	{
		public void RestartServer() => Sitecore.Install.Installer.RestartServer();
	}
}

The class above just calls the RestartServer() method on the Sitecore.Install.Installer class; ultimately this just does a “touch” on your Web.config to recycle your Sitecore instance.

Like all good Sitecore custom events, we need a custom EventArgs class — well, you don’t really need one if you aren’t passing any data beyond what lives on the System.EventArgs class but I’m sure you will seldom come across such a scenario 😉 — so I defined the following class to be just that class:

using System;
using System.Collections.Generic;

using Sitecore.Events;

namespace Foundation.Kernel.Models.RestartServer.Events
{
	[Serializable]
	public class RestartServerEventArgs : EventArgs, IPassNativeEventArgs
	{
		public List<string> ServerNames { get; protected set; }

		public RestartServerEventArgs(List<string> serverNames)
		{
			ServerNames = serverNames;
		}
	}
}

We will be passing a List of server names just in case there are multiple servers we would like to reboot simultaneously.

Now it’s time to create the event handlers. I created the following abstract class for my two event handler classes to have a centralized place for most of this logic:

using System;
using System.Linq;

using Sitecore.Abstractions;

using Foundation.Kernel.Models.RestartServer.Events;
using Foundation.Kernel.Services.Installer;
using Foundation.Kernel.Services.Server;
using Foundation.Kernel.Services.RestartServer;
using Foundation.Kernel.Services.FeatureToggle;

namespace Foundation.Kernel.Events.RestartServer
{
	public abstract class BaseRestartServerEventHandler : IFeatureToggleable
	{
		private readonly RestartServerEventHandlerSettings _settings;
		private readonly IRestartServerFeatureToggleService _restartServerFeatureToggleService;
		private readonly IServerService _serverService;
		private readonly IInstallerService _installerService;
		private readonly BaseLog _log;

		public bool Enabled { get; set; }

		public BaseRestartServerEventHandler(RestartServerEventHandlerSettings settings, IRestartServerFeatureToggleService restartServerFeatureToggleService, IServerService serverService,  IInstallerService installerService, BaseLog log)
		{
			_settings = settings;
			_restartServerFeatureToggleService = restartServerFeatureToggleService;
			_serverService = serverService;
			_installerService = installerService;
			_log = log;
		}

		protected void ExecuteRestart(object sender, EventArgs args)
		{
			if(!IsEnabled())
			{
				return;
			}

			RestartServerEventArgs restartServerArgs = GetArguments<RestartServerEventArgs>(args);
			if (!ShouldRestartServer(restartServerArgs))
			{
				return;
			}

			string restartLogMessageFormat = GetRestartLogMessageFormat();
			if (!string.IsNullOrWhiteSpace(restartLogMessageFormat))
			{
				LogInfo(GetRestartServerMessage(restartLogMessageFormat, GetCurrentServerName()));
			}
			
			RestartServer();
		}

		protected virtual bool IsEnabled() => _restartServerFeatureToggleService.IsEnabled(this);

		protected abstract string GetRestartLogMessageFormat();

		protected virtual TArgs GetArguments<TArgs>(EventArgs args) where TArgs : EventArgs => args as TArgs;

		protected virtual bool ShouldRestartServer(RestartServerEventArgs restartServerArgs)
		{
			if(restartServerArgs.ServerNames == null || !restartServerArgs.ServerNames.Any())
			{
				return false;
			}

			string currentServerName = GetCurrentServerName();
			if (string.IsNullOrWhiteSpace(currentServerName))
			{
				return false;
			}

			return restartServerArgs.ServerNames.Any(serverName => string.Equals(serverName, currentServerName, StringComparison.OrdinalIgnoreCase));
		}

		protected virtual string GetCurrentServerName() => _serverService.GetCurrentServerName();

		protected virtual string GetRestartServerMessage(string messageFormat, string serverName) => string.Format(messageFormat, serverName);

		protected virtual void RestartServer() => _installerService.RestartServer();

		protected virtual void LogInfo(string message) => _log.Info(message, this);
	}
}

Subclasses of the class above will have methods to handle their events which will delegate to the ExecuteRestart() method. This method will use the feature toggle service class to determine if it should execute or not; if not, it just gracefully exits.

If it does proceed forward, the passed System.EventArgs class is casted as RestartServerEventArgs instance, and the current Sitecore instance’s name is checked against the collection server names in the RestartServerEventArgs instance. If the current server’s name is in the list, the server is restarted though proceeded by a log message indicating that the server is being restarted.

Subclasses of thie abstract class above must provide the log message formatted string so the log message can be written to the Sitecore log.

I then created the following interface for an event handler class to handle a local server restart:

using System;

namespace Foundation.Kernel.Events.RestartServer
{
	public interface IRestartLocalServerEventHandler
	{
		void OnRestartTriggered(object sender, EventArgs args);
	}
}

Here’s the implementation of the interface above:

using System;

using Sitecore.Abstractions;

using Foundation.Kernel.Models.RestartServer.Events;
using Foundation.Kernel.Services.Installer;
using Foundation.Kernel.Services.Server;
using Foundation.Kernel.Services.RestartServer;

namespace Foundation.Kernel.Events.RestartServer
{
	public class RestartLocalServerEventHandler : BaseRestartServerEventHandler, IRestartLocalServerEventHandler
	{
		private readonly RestartServerEventHandlerSettings _settings;

		public RestartLocalServerEventHandler(RestartServerEventHandlerSettings settings, IRestartServerFeatureToggleService restartServerFeatureToggleService, IServerService serverService, IInstallerService installerService, BaseLog log)
			: base(settings, restartServerFeatureToggleService, serverService, installerService, log)
		{
			_settings = settings;
		}

		public void OnRestartTriggered(object sender, EventArgs args) => ExecuteRestart(sender, args);

		protected override string GetRestartLogMessageFormat() => _settings.RestartServerLogMessageFormat;
	}
}

This subclass of the abstract class defined above just supplies its own log formatted message, and the rest of the logic is handled by its parent class.

I then created the following interface of an event handler class to remote events (those that run on CD servers):

using System;

namespace Foundation.Kernel.Events.RestartServer
{
	public interface IRestartRemoteServerEventHandler
	{
		void OnRestartTriggeredRemote(object sender, EventArgs args);
	}
}

The following class implements the interface above:

using System;

using Sitecore.Abstractions;

using Foundation.Kernel.Models.RestartServer.Events;
using Foundation.Kernel.Services.Installer;
using Foundation.Kernel.Services.Server;
using Foundation.Kernel.Services.RestartServer;

namespace Foundation.Kernel.Events.RestartServer
{
	public class RestartRemoteServerEventHandler : BaseRestartServerEventHandler, IRestartRemoteServerEventHandler
	{
		private readonly RestartServerEventHandlerSettings _settings;

		public RestartRemoteServerEventHandler(RestartServerEventHandlerSettings settings, IRestartServerFeatureToggleService restartServerFeatureToggleService, IServerService serverService,  IInstallerService installerService, BaseLog log)
			: base(settings, restartServerFeatureToggleService, serverService, installerService, log)
		{
			_settings = settings;
		}

		public void OnRestartTriggeredRemote(object sender, EventArgs args) => ExecuteRestart(sender, args);

		protected override string GetRestartLogMessageFormat() => _settings.RestartServerRemoteLogMessageFormat;
	}
}

Just as the previous event handler class had done, this class inherits from the abstract class defined above but just defines the GetRestartLogMessageFormat() method to provide its own log message formatted string; we need to identify that this was executed on a CD server in the Sitecore log.

In order to get remote events to work on your CD instances, you need to subscribe to your remote event on your CD server — this is done by listening for an instance of your custom EventArgs, we are using RestartServerEventArgs here, being sent across via the EventQueue — and then raise your custom remote event on our CD instance so your handlers are executed. The following code will be wrappers around static classes in the Sitecore API so I can use Dependency Injection in a custom <initialize> pipeline processor which ties all of this together with these injected service classes.

I will need to call methods on Sitecore.Events.Event in Sitecore.Kernel. I created the following interface/implementation for it:

using System;

using Sitecore.Events;

namespace Foundation.Kernel.Services.Events
{
	public interface IEventService
	{
		TParameter ExtractParameter<TParameter>(EventArgs args, int index) where TParameter : class;

		object ExtractParameter(EventArgs args, int index);

		void RaiseEvent(string eventName, IPassNativeEventArgs args);

		EventResult RaiseEvent(string eventName, params object[] parameters);
	}
}
using System;

using Sitecore.Events;

namespace Foundation.Kernel.Services.Events
{
	public class EventService : IEventService
	{
		public TParameter ExtractParameter<TParameter>(EventArgs args, int index) where TParameter : class => Event.ExtractParameter<TParameter>(args, index);

		public object ExtractParameter(EventArgs args, int index) => Event.ExtractParameter(args, index);

		public void RaiseEvent(string eventName, IPassNativeEventArgs args) => Event.RaiseEvent(eventName, args);

		public EventResult RaiseEvent(string eventName, params object[] parameters) => Event.RaiseEvent(eventName, parameters);
	}
}

I then created the following Config Object to provide the names of the two custom events (check out the patch configuration file at the end of this post to see what these values are):

using Foundation.DependencyInjection;
using Foundation.DependencyInjection.Enums;

namespace Foundation.Kernel.Models.RestartServer.Events
{
	[ServiceConfigObject(ConfigPath = "moduleSettings/foundation/kernel/restartServerEventSettings", Lifetime = Lifetime.Singleton)]
	public class RestartServerEventSettings
	{
		public string RestartCurrentServerEventName { get; set; }

		public string RestartRemoteServerEventName { get; set; }
	}
}

Now, I need a way to raise events, both on a local Sitecore instance but also on a remote instance. I defined the following interface for such a service class:

namespace Foundation.Kernel.Services.Events
{
	public interface IEventTriggerer
	{
		void TriggerEvent(string eventName, params object[] parameters);

		void TriggerRemoteEvent<TEvent>(TEvent evt);

		void TriggerRemoteEvent<TEvent>(TEvent evt, bool triggerGlobally, bool triggerLocally);
	}
}

I then implemented the interface above:

using Sitecore.Abstractions;

namespace Foundation.Kernel.Services.Events
{
	public class EventTriggerer : IEventTriggerer
	{
		private readonly IEventService _eventService;
		private readonly BaseEventQueueProvider _eventQueueProvider; 
		
		public EventTriggerer(IEventService eventService, BaseEventQueueProvider eventQueueProvider)
		{
			_eventService = eventService;
			_eventQueueProvider = eventQueueProvider;
		}

		public void TriggerEvent(string eventName, params object[] parameters) => _eventService.RaiseEvent(eventName, parameters);

		public void TriggerRemoteEvent<TEvent>(TEvent evt) => _eventQueueProvider.QueueEvent(evt);

		public void TriggerRemoteEvent<TEvent>(TEvent evt, bool triggerGlobally, bool triggerLocally) => _eventQueueProvider.QueueEvent(evt, triggerGlobally, triggerLocally);
	}
}

The class above consumes the IEventService service class we defined above so that we can “trigger”/raise local events on the current Sitecore instance, and it also consumes the BaseEventQueueProvider service which comes with stock Sitecore so we can “trigger”/raise events to run on remote servers via the EventQueue.

I also need to use methods on the Sitecore.Eventing.EventManager class in Sitecore.Kernel. The following interface/implementation do just that:

using System;

using Sitecore.Eventing;

namespace Foundation.Kernel.Services.Events
{
	public interface IEventManagerService
	{
		SubscriptionId Subscribe<TEvent>(Action<TEvent> eventHandler);
	}
}
using System;

using Sitecore.Eventing;

namespace Foundation.Kernel.Services.Events
{
	public class EventManagerService : IEventManagerService
	{
		public SubscriptionId Subscribe<TEvent>(Action<TEvent> eventHandler) => EventManager.Subscribe(eventHandler);
	}
}

I do want to call out that there is an underlying service behind the Sitecore.Eventing.EventManager class in Sitecore.Kernel but it’s brought into this static class through some service locator method I had never seen before, and was afraid of traversing too much down this rabbit hole out of fear of sticking my hands into a hornets nest, or even worse, a nest of those murderer hornets we keep hearing about on the news these days; I felt it was best not to go much further into this today. 😉

We can now dive into the <initialize> pipeline processor which binds the “subscribing and trigger remote event” functionality together.

I first created the following interface for the pipeline processor:

using Sitecore.Pipelines;

namespace Foundation.Kernel.Pipelines.Initialize.RestartServer
{
	public interface IRestartServerSubscriber
	{
		void Process(PipelineArgs args);
	}
}

I then implemented the interface above:

using System;

using Sitecore.Eventing;
using Sitecore.Pipelines;

using Foundation.Kernel.Services.Events;
using Foundation.Kernel.Services.FeatureToggle;
using Foundation.Kernel.Services.RestartServer;
using Foundation.Kernel.Models.RestartServer.Events;

namespace Foundation.Kernel.Pipelines.Initialize.RestartServer
{
	public class RestartServerSubscriber: IFeatureToggleable, IRestartServerSubscriber
	{
		private readonly RestartServerEventSettings _settings;
		private readonly IRestartServerFeatureToggleService _restartServerFeatureToggleService;
		private readonly IEventTriggerer _eventTriggerer;
		private readonly IEventManagerService _eventManagerService;

		public bool Enabled { get; set; }

		public RestartServerSubscriber(RestartServerEventSettings settings, IRestartServerFeatureToggleService restartServerFeatureToggleService, IEventTriggerer eventTriggerer, IEventManagerService eventManagerService)
		{
			_settings = settings;
			_restartServerFeatureToggleService = restartServerFeatureToggleService;
			_eventTriggerer = eventTriggerer;
			_eventManagerService = eventManagerService;
		}

		public void Process(PipelineArgs args)
		{
			if(!IsEnabled())
			{
				return;
			}

			Subscribe(GetRaiseRestartServerEventMethod());
		}

		protected virtual bool IsEnabled() => _restartServerFeatureToggleService.IsEnabled(this);

		protected virtual Action<RestartServerEventArgs> GetRaiseRestartServerEventMethod() => new Action<RestartServerEventArgs>(RaiseRestartServerEvent);

		protected virtual void RaiseRestartServerEvent(RestartServerEventArgs args) => RaiseEvent(GetRestartRemoteServerEventName(), args);

		protected virtual string GetRestartRemoteServerEventName() => _settings.RestartRemoteServerEventName;

		protected virtual void RaiseEvent(string eventName, params object[] parameters) => _eventTriggerer.TriggerEvent(eventName, parameters);

		protected virtual SubscriptionId Subscribe<TEvent>(Action<TEvent> eventHandler) => _eventManagerService.Subscribe(eventHandler);
	}
}

The Process() method of the pipeline processor class above uses the feature toggle service we had discuss earlier in this post to determine if it should execute or not.

If it can execute, it will subscribe to the remote event by listening for an RestartServerEventArgs instance being sent across by the EventQueue.

If it one is sent across, it will “trigger” (raise) the custom remote event on the CD server this pipeline processor lives on.

Are you still with me? 😉

Now we need a service class which glues all of the stuff above into a nice simple API which we can call. I defined the following Config Object which will be consumed by this service class; this Config Object determines if Remote events should also run on local servers — I had set this up so I can turn this on for testing/troubleshooting/debugging on my local development instance:

using Foundation.DependencyInjection;
using Foundation.DependencyInjection.Enums;

namespace Foundation.Kernel.Models.RestartServer.Events
{
	[ServiceConfigObject(ConfigPath = "moduleSettings/foundation/kernel/restartServerRemoteEventSettings", Lifetime = Lifetime.Singleton)]
	public class RestartServerRemoteEventSettings
	{
		public bool TriggerRemoteEventLocally { get; set; }

		public bool TriggerRemoteEventGlobally { get; set; }
	}
}

The following interface is for our “simple” API for restarting Sitecore instances:

using System.Collections.Generic;

namespace Foundation.Kernel.Services.RestartServer
{
	public interface IRestartServerService
	{
		void RestartCurrentServer();

		void RestartRemoteServer(string serverName);

		void RestartRemoteServers(List<string> serverNames);
	}
}

Here’s the implementation of the interface above:

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

using Foundation.Kernel.Models.RestartServer.Events;
using Foundation.Kernel.Services.Events;
using Foundation.Kernel.Services.Server;

namespace Foundation.Kernel.Services.RestartServer
{
	public class RestartServerService : IRestartServerService
	{
		private readonly RestartServerEventSettings _eventSettings;
		private readonly RestartServerRemoteEventSettings _remoteEventSettings;
		private readonly IRestartServerFeatureToggleService _restartServerFeatureToggleService;
		private readonly IServerService _serverService;
		private readonly IEventTriggerer _eventTriggerer;

		public RestartServerService(RestartServerEventSettings eventSettings, RestartServerRemoteEventSettings remoteEventSettings, IRestartServerFeatureToggleService restartServerFeatureToggleService, IServerService serverService, IEventTriggerer eventTriggerer)
		{
			_eventSettings = eventSettings;
			_remoteEventSettings = remoteEventSettings;
			_restartServerFeatureToggleService = restartServerFeatureToggleService;
			_serverService = serverService;
			_eventTriggerer = eventTriggerer;
		}

		public void RestartCurrentServer()
		{
			if(!IsFeatureEnabled())
			{
				return;
			}

			TriggerEvent(GetRestartCurrentServerEventName(), CreateRestartServerEventArgs(CreateList(GetCurrentServerName())));
		}

		protected virtual string GetRestartCurrentServerEventName() => _eventSettings.RestartCurrentServerEventName;

		protected virtual string GetCurrentServerName() => _serverService.GetCurrentServerName();

		public void RestartRemoteServer(string serverName) => RestartRemoteServers(CreateList(serverName));

		protected virtual List<string> CreateList(params string[] values) => values?.ToList();

		public void RestartRemoteServers(List<string> serverNames)
		{
			if (!IsFeatureEnabled())
			{
				return;
			}

			TriggerRemoteEvent(CreateRestartServerEventArgs(serverNames));
		}
			

		protected virtual bool IsFeatureEnabled() => _restartServerFeatureToggleService.IsFeatureEnabled();

		protected virtual RestartServerEventArgs CreateRestartServerEventArgs(List<string> serverNames) => new RestartServerEventArgs(serverNames);

		protected virtual void TriggerEvent(string eventName, params object[] parameters) => _eventTriggerer.TriggerEvent(eventName, parameters);

		protected virtual void TriggerRemoteEvent<TEvent>(TEvent evt) => _eventTriggerer.TriggerRemoteEvent(evt, GetTriggerRemoteEventGlobally(), GetTriggerRemoteEventGlobally());

		protected virtual bool GetTriggerRemoteEventGlobally() => _remoteEventSettings.TriggerRemoteEventGlobally;

		protected virtual bool GetTriggerRemoteEventLocally() => _remoteEventSettings.TriggerRemoteEventLocally;
	}
}

The class above implements methods for restarting the current server, one remote server, or multiple remote servers — ultimately, it just delegates to the IEventTriggerer service class we defined further above, and uses other services discussed earlier in this post; there’s really not much to it.

I then stitched everything above together in the following Sitecore patch configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
	<sitecore>
		<events>
			<event name="server:restart">
				<handler type="Foundation.Kernel.Events.RestartServer.IRestartLocalServerEventHandler, Foundation.Kernel" method="OnRestartTriggered" resolve="true">
					<Enabled>true</Enabled>
				</handler>
			</event>
			<event name="server:restart:remote">
				<handler type="Foundation.Kernel.Events.RestartServer.IRestartRemoteServerEventHandler, Foundation.Kernel" method="OnRestartTriggeredRemote" resolve="true">
					<Enabled>true</Enabled>
				</handler>
			</event>
		</events>
		<pipelines>
			<initialize>
				<processor type="Foundation.Kernel.Pipelines.Initialize.RestartServer.IRestartServerSubscriber, Foundation.Kernel" resolve="true">
					<Enabled>true</Enabled>
				</processor>
			</initialize>
		</pipelines>
		<moduleSettings>
			<foundation>
				<kernel>
					<serverServiceSettings type="Foundation.Kernel.Models.Server.ServerServiceSettings, Foundation.Kernel" singleInstance="true">
						<InstanceNameSetting>InstanceName</InstanceNameSetting>
					</serverServiceSettings>
					<restartServerSettings type="Foundation.Kernel.Models.RestartServer.RestartServerSettings, Foundation.Kernel" singleInstance="true">
						<Enabled>true</Enabled>
					</restartServerSettings>
					<restartServerEventHandlerSettings type="Foundation.Kernel.Models.RestartServer.Events.RestartServerEventHandlerSettings, Foundation.Kernel" singleInstance="true">
						<RestartServerLogMessageFormat>Restart Server Event Triggered: Shutting down server {0}</RestartServerLogMessageFormat>
						<RestartServerRemoteLogMessageFormat>Restart Server Remote Event Triggered: Shutting down server {0}</RestartServerRemoteLogMessageFormat>
					</restartServerEventHandlerSettings>
					<restartServerEventSettings type="Foundation.Kernel.Models.RestartServer.Events.RestartServerEventSettings, Foundation.Kernel" singleInstance="true">
						<RestartCurrentServerEventName>server:restart</RestartCurrentServerEventName>
						<RestartRemoteServerEventName>server:restart:remote</RestartRemoteServerEventName>
					</restartServerEventSettings>
					<restartServerRemoteEventSettings type="Foundation.Kernel.Models.RestartServer.Events.RestartServerRemoteEventSettings, Foundation.Kernel" singleInstance="true">
						<!-- setting this to "true" so I can test/debug locally -->
						<TriggerRemoteEventLocally>true</TriggerRemoteEventLocally>
						<TriggerRemoteEventGlobally>true</TriggerRemoteEventGlobally>
					</restartServerRemoteEventSettings>
			</kernel>
			</foundation>
		</moduleSettings>
		<services>

			<!-- General Services -->
			<register
				serviceType="Foundation.Kernel.Services.Installer.IInstallerService, Foundation.Kernel"
				implementationType="Foundation.Kernel.Services.Installer.InstallerService, Foundation.Kernel"
				lifetime="Singleton" />
			<register
				serviceType="Foundation.Kernel.Services.Server.IServerService, Foundation.Kernel"
				implementationType="Foundation.Kernel.Services.Server.ServerService, Foundation.Kernel"
				lifetime="Singleton" />
			<register
				serviceType="Foundation.Kernel.Services.RestartServer.IRestartServerFeatureToggleService, Foundation.Kernel"
				implementationType="Foundation.Kernel.Services.RestartServer.RestartServerFeatureToggleService, Foundation.Kernel"
				lifetime="Singleton" />
			<register
				serviceType="Foundation.Kernel.Services.RestartServer.IRestartServerService, Foundation.Kernel"
				implementationType="Foundation.Kernel.Services.RestartServer.RestartServerService, Foundation.Kernel"
				lifetime="Singleton" />
			
			<!-- Event Related Services -->
			<register
				serviceType="Foundation.Kernel.Services.Events.IEventService, Foundation.Kernel"
				implementationType="Foundation.Kernel.Services.Events.EventService, Foundation.Kernel"
				lifetime="Singleton" />
			<register
				serviceType="Foundation.Kernel.Services.Events.IEventManagerService, Foundation.Kernel"
				implementationType="Foundation.Kernel.Services.Events.EventManagerService, Foundation.Kernel"
				lifetime="Singleton" />
			<register
				serviceType="Foundation.Kernel.Services.Events.IEventTriggerer, Foundation.Kernel"
				implementationType="Foundation.Kernel.Services.Events.EventTriggerer, Foundation.Kernel"
				lifetime="Singleton" />

			<!-- Event Handler Services -->
			
			<register
				serviceType="Foundation.Kernel.Events.RestartServer.IRestartLocalServerEventHandler, Foundation.Kernel"
				implementationType="Foundation.Kernel.Events.RestartServer.RestartLocalServerEventHandler, Foundation.Kernel"
				lifetime="Singleton" />
			<register
				serviceType="Foundation.Kernel.Events.RestartServer.IRestartRemoteServerEventHandler, Foundation.Kernel"
				implementationType="Foundation.Kernel.Events.RestartServer.RestartRemoteServerEventHandler, Foundation.Kernel"
				lifetime="Singleton" />

			<!-- Pipeline Processor Services -->
			<register
				serviceType="Foundation.Kernel.Pipelines.Initialize.RestartServer.IRestartServerSubscriber, Foundation.Kernel"
				implementationType="Foundation.Kernel.Pipelines.Initialize.RestartServer.RestartServerSubscriber, Foundation.Kernel"
				lifetime="Singleton" />
		</services>
		<settings>

			<!-- This is set here for testing. Ideally, you would have this set for every Sitecore instance you have in your specific custom patch file where identify your server -->
			<setting name="InstanceName">
				<patch:attribute name="value">Sandbox</patch:attribute>
			</setting>
		</settings>
	</sitecore>
</configuration>

Considering I had built this to be executed from a PowerShell script hooked into a custom button through the Content Editor Ribbon integration point via Sitecore PowerShell Extensions (SPE), I will be testing this using two simple PowerShell scripts; both will be executed from the Sitecore PowerShell Extensions ISE (why yes, Sitecore MVP Michael West, I had tested this on SPE v6.1.1 😉 ):

Let’s see how we did.

Let’s test this by restarting the current Sitecore instance:

$serviceType = [Foundation.Kernel.Services.RestartServer.IRestartServerService]
$service = [Sitecore.DependencyInjection.ServiceLocator]::ServiceProvider.GetService($serviceType) -as $serviceType  #use service locator to get the IRestartServerService service
$service.RestartCurrentServer() #Let's call the method to restart the current server (CM)

As expected, my Sitecore instance froze up as it was restarting. I then saw this in my logs:

Let’s now restart the remote Sitecore instance:

$serviceType = [Foundation.Kernel.Services.RestartServer.IRestartServerService]
$service = [Sitecore.DependencyInjection.ServiceLocator]::ServiceProvider.GetService($serviceType) -as $serviceType  #use service locator to get the IRestartServerService service
$service.RestartRemoteServer("Sandbox")  #Let's call the method to restart the remote server (CD)

Also as expected, my Sitecore instance froze up as it was restarting — remember, I am testing this on a single development instance of Sitecore where I don’t have a separate CM and CD . I then saw this in my logs:

Let me know if you have questions/comments/fears/dreams/hopes/apsirations/whatever by dropping a comment. 😉

Advertisement

Yet Another Post on Sitecore Content Editor Warnings

Over the years, I’ve written posts on adding custom Content Editor Warnings. Content Editor Warnings give visual cues at the top of an Item in the Content Editor to either take action on something about the Item, or to just convey information about the Item — perhaps the Item is read-only, or maybe there is something wrong with the item; you can do all kinds of stuff with these, just use your imagination on what you would like to convey to your content authors.

The way to add these is to create a custom processor for the <getContentEditorWarnings> pipeline in Sitecore. There really isn’t anything more to it.

Moreover, you can even use Sitecore PowerShell Extensions to create these though I did not do this for this post. Sorry, Sitecore MVP Michael West. 😉

Today, I am sharing a recent example of two Content Editor Warnings which I had worked on which convey to content authors they almost have, or have too many child items within a Media Library folder.

Some code on this post reuses service classes found on my previous post where I discussed how to create a custom MasterDataView driven by a custom pipeline; I recommend having a read of that previous post first before proceeding further in order to have complete grounding on some of the service classes I am using in the solution below.

I had to make a tweak to the ITooManySubItemsService service found on my previous post — I had to make the GetNumberOfItemsToStartWarningUser() and GetMaximumNumberOfItemsInFolder() methods public as I needed to get these two values within the Content Editor Warnings in order to display these values in messages to the content authors via the Content Editor Warning. This service determines these values based on a Config Object service which can also be overriden by each individual piece of functionality in my solution (i.e. the maximum number of child items set on the pipeline processor would override the Config Object service’s value):

using Foundation.Validation.Models.TooManySubItems;

namespace Foundation.Validation.Services.TooManySubItems
{
	public interface ITooManySubItemsService
	{
		bool HasAlmostTooManySubItems(TooManySubItemsServiceParameters parameters);

		bool HasTooManySubItems(TooManySubItemsServiceParameters parameters);

		int GetNumberOfItemsToStartWarningUser(TooManySubItemsServiceParameters parameters); // I've added this 

		int GetMaximumNumberOfItemsInFolder(TooManySubItemsServiceParameters parameters); // I've added this 
	}
}

Here is the modified implementation of the interface above. All I did was change the signature on GetNumberOfItemsToStartWarningUser() and GetMaximumNumberOfItemsInFolder() to be public instead of protected:

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

using Sitecore.Data.Items;

using Foundation.Validation.Models.TooManySubItems;
using Foundation.Validation.Services.TooManySubItems;

namespace Feature.Validation.Services.TooManySubItems
{
	public class TooManySubItemsService : ITooManySubItemsService
	{
		private readonly TooManySubItemsSettings _settings; 

		public TooManySubItemsService(TooManySubItemsSettings settings)
		{
			_settings = settings;
		}

		public bool HasAlmostTooManySubItems(TooManySubItemsServiceParameters parameters)
		{
			int numberOfItemsToStartWarningUser = GetNumberOfItemsToStartWarningUser(parameters);
			int maximumNumberOfItemsInFolder = GetMaximumNumberOfItemsInFolder(parameters);
			if (parameters?.Item == null || numberOfItemsToStartWarningUser < 1 || maximumNumberOfItemsInFolder < 1)
			{
				return false;
			}

			IEnumerable<Item> items = GetItemsWithoutTemplateIds(parameters?.Item.Children, parameters?.TemplateIdsToIgnore);
			return items.Count() >= numberOfItemsToStartWarningUser && items.Count() < maximumNumberOfItemsInFolder;
		}

		public bool HasTooManySubItems(TooManySubItemsServiceParameters parameters)
		{
			int maximumNumberOfItemsInFolder = GetMaximumNumberOfItemsInFolder(parameters);
			if (parameters?.Item == null || maximumNumberOfItemsInFolder < 1)
			{
				return false;
			}

			IEnumerable<Item> items = GetItemsWithoutTemplateIds(parameters?.Item.Children, parameters?.TemplateIdsToIgnore);
			return items.Count() >= maximumNumberOfItemsInFolder;
		}

		public int GetNumberOfItemsToStartWarningUser(TooManySubItemsServiceParameters parameters)
		{
			if (parameters == null || _settings == null)
			{
				return 0;
			}

			if (parameters.NumberOfItemsToStartWarningUser > 0)
			{
				return parameters.NumberOfItemsToStartWarningUser;
			}

			return _settings.NumberOfItemsToStartWarningUser;
		}

		public int GetMaximumNumberOfItemsInFolder(TooManySubItemsServiceParameters parameters)
		{
			if (parameters == null || _settings == null)
			{
				return 0;
			}

			if (parameters.MaximumNumberOfItemsInFolder > 0)
			{
				return parameters.MaximumNumberOfItemsInFolder;
			}

			return _settings.MaximumNumberOfItemsInFolder;
		}

		protected virtual IEnumerable<Item> GetItemsWithoutTemplateIds(IEnumerable<Item> items, IEnumerable<string> templateIds)
		{
			if (items == null)
			{
				return Enumerable.Empty<Item>();
			}

			if(templateIds == null || !templateIds.Any())
			{
				return items;
			}

			return items.Where(item => templateIds.All(templateId => templateId != item.TemplateID.ToString())).ToList();
		}
	}
}

The processors which are defined below will give content authors the ability to take action on the Content Editor Warnings. The options are basically just as collection of link text and Sheer UI commands. In this example, content authors are given the option to create new Media Library folders. The following class is used when sourcing these from each processor’s configuration definition (see the Sitecore patch configuration file further down in this post):

namespace Foundation.Kernel.Models.Pipelines.ContentEditorWarnings
{
	public class ContentEditorWarningOption
	{
		public string Text { get; set; }
		
		public string Link { get; set; }
	}
}

Since the two Content Editor Warning processors are doing very similiar things, I abstracted out most of their logic into a base abstract class, and put it hooks for overriding methods on it; this is an example of the Template Method Pattern for you Design Pattern junkies 😉

using System.Collections.Generic;

using Sitecore.Data.Items;
using Sitecore.Pipelines.GetContentEditorWarnings;

using Foundation.Kernel.Models.Pipelines.ContentEditorWarnings;

using Foundation.Validation.Models.TooManySubItems;
using Foundation.Validation.Services.TooManySubItems.Factories;
using Foundation.Validation.Services.TooManySubItems;
using Foundation.Kernel.Services.FeatureToggle;

namespace Feature.ContentEditor.Pipelines.GetContentEditorWarnings
{
	public abstract class BaseTooManyChildItemsWarningProcessor : IFeatureToggleable, ITooManySubItemsFeature
	{
		private readonly ITooManySubItemsFeatureToggleService _tooManySubItemsFeatureToggleService;
		private readonly ITooManySubItemsServiceParametersFactory _tooManySubItemsServiceParametersFactory;
		private readonly ITooManySubItemsService _tooManySubItemsService;

		public bool Enabled { get; set; }

		protected string MediaLibraryBasePath { get; set; }

		public List<string> TemplateIdsToIgnore { get; set; } = new List<string>();

		public int NumberOfItemsToStartWarningUser { get; set; }

		public int MaximumNumberOfItemsInFolder { get; set; }

		protected List<ContentEditorWarningOption> ContentEditorWarningOptions { get; set; } = new List<ContentEditorWarningOption>();

		public BaseTooManyChildItemsWarningProcessor(ITooManySubItemsFeatureToggleService tooManySubItemsFeatureToggleService, ITooManySubItemsServiceParametersFactory tooManySubItemsServiceParametersFactory, ITooManySubItemsService tooManySubItemsService)
		{
			_tooManySubItemsFeatureToggleService = tooManySubItemsFeatureToggleService;
			_tooManySubItemsServiceParametersFactory = tooManySubItemsServiceParametersFactory;
			_tooManySubItemsService = tooManySubItemsService;
		}

		public void Process(GetContentEditorWarningsArgs args)
		{
			if(!IsEnabled() || !IsInMediaLibrary(args?.Item))
			{
				return;
			}

			TooManySubItemsServiceParameters parameters = CreateParameters(args?.Item, this);
			if(parameters == null)
			{
				return;
			}

			if(!ShouldDisplayWarning(parameters))
			{
				return;
			}

			int maximumNumberOfItemsInFolder = GetMaximumNumberOfItemsInFolder(parameters);
			string warningTitle = GetWarningTitle(args, maximumNumberOfItemsInFolder);
			string warningMessage = GetWarningMessage(args, maximumNumberOfItemsInFolder);
			AddWarning(args, warningTitle, warningMessage, ContentEditorWarningOptions);
		}

		protected virtual bool IsEnabled() => _tooManySubItemsFeatureToggleService.IsEnabled(this);

		protected virtual bool IsInMediaLibrary(Item item)
		{
			if(string.IsNullOrWhiteSpace(item.Paths.FullPath))
			{
				return false;
			}

			return item.Paths.FullPath.Contains(MediaLibraryBasePath);
		}

		protected virtual TooManySubItemsServiceParameters CreateParameters(Item item, ITooManySubItemsFeature feature) => _tooManySubItemsServiceParametersFactory.CreateParameters(item, feature);

		protected abstract bool ShouldDisplayWarning(TooManySubItemsServiceParameters parameters);

		protected abstract string GetWarningTitle(GetContentEditorWarningsArgs args, int maximumNumberOfItemsInFolder);

		protected abstract string GetWarningMessage(GetContentEditorWarningsArgs args, int maximumNumberOfItemsInFolder);

		protected virtual int GetMaximumNumberOfItemsInFolder(TooManySubItemsServiceParameters parameters) =>_tooManySubItemsService.GetMaximumNumberOfItemsInFolder(parameters);

		protected virtual bool HasAlmostTooManySubItems(TooManySubItemsServiceParameters parameters) => _tooManySubItemsService.HasAlmostTooManySubItems(parameters);

		protected virtual bool HasTooManySubItems(TooManySubItemsServiceParameters parameters) => _tooManySubItemsService.HasTooManySubItems(parameters);

		protected virtual void AddWarning(GetContentEditorWarningsArgs args, string title, string message, IEnumerable<ContentEditorWarningOption> options)
		{
			if(args == null)
			{
				return;
			}
			
			GetContentEditorWarningsArgs.ContentEditorWarning warning = CreateNewContentEditorWarning(args); 
			if(warning == null)
			{
				return;
			}

			warning.Title = title;
			warning.Text = message;
			AddOptions(warning, options);
		}

		protected virtual GetContentEditorWarningsArgs.ContentEditorWarning CreateNewContentEditorWarning(GetContentEditorWarningsArgs args) => args?.Add();

		protected virtual void AddOptions(GetContentEditorWarningsArgs.ContentEditorWarning warning, IEnumerable<ContentEditorWarningOption> options)
		{
			if(warning == null || options == null)
			{
				return;
			}

			foreach(ContentEditorWarningOption option in options)
			{
				if(string.IsNullOrWhiteSpace(option.Text) || string.IsNullOrWhiteSpace(option.Link)) continue;
				warning.AddOption(option.Text, option.Link);
			}
		}
	}
}

The class above will only display the Content Editor Warning when the feature is enabled (see my previous post where I discuss how this works using Sitecore configuration feature toggles); the item is in the Media Library; and the ShouldDisplayWarning() method returns true — this must be implemented by subclasses of this abstract base class.

If the Content Editor Warning should display, the Content Editor Warning’s title is retrieved via the GetWarningTitle() method, and its message is retrieved via the GetWarningMessage() method — both of these methods must be implemented by its subclasses.

These are then added to the a new GetContentEditorWarningsArgs.ContentEditorWarning instance created from the GetContentEditorWarningsArgs instance with the options defined in the configuration for the processor.

I then created the following interface for the purposes of registering its implementation in the Sitecore IoC container; this is optional as you could just register it with its implementation type being its service type.

using Sitecore.Pipelines.GetContentEditorWarnings;

namespace Feature.ContentEditor.Pipelines.GetContentEditorWarnings
{
	public interface IAlmostTooManyChildItemsWarningProcessor
	{
		void Process(GetContentEditorWarningsArgs args);
	}
}

Here is the implementation of the interface above. This is the processor class to show content authors when they are approaching too many child items:

using Sitecore.Pipelines.GetContentEditorWarnings;

using Foundation.Validation.Models.TooManySubItems;
using Foundation.Validation.Services.TooManySubItems.Factories;
using Foundation.Validation.Services.TooManySubItems;

namespace Feature.ContentEditor.Pipelines.GetContentEditorWarnings
{
	public class AlmostTooManyChildItemsWarningProcessor : BaseTooManyChildItemsWarningProcessor, IAlmostTooManyChildItemsWarningProcessor
	{
		private string AlmostAtMaxiumTitle { get; set; }

		private string AlmostAtMaxiumMessageFormat { get; set; }

		public AlmostTooManyChildItemsWarningProcessor(ITooManySubItemsFeatureToggleService tooManySubItemsFeatureToggleService, ITooManySubItemsServiceParametersFactory tooManySubItemsServiceParametersFactory, ITooManySubItemsService tooManySubItemsService)
			: base(tooManySubItemsFeatureToggleService, tooManySubItemsServiceParametersFactory, tooManySubItemsService)
		{
		}

		protected override bool ShouldDisplayWarning(TooManySubItemsServiceParameters parameters) => HasAlmostTooManySubItems(parameters);

		protected override string GetWarningTitle(GetContentEditorWarningsArgs args, int maximumNumberOfItemsInFolder) => AlmostAtMaxiumTitle;

		protected override string GetWarningMessage(GetContentEditorWarningsArgs args, int maximumNumberOfItemsInFolder) => string.Format(AlmostAtMaxiumMessageFormat, args?.Item?.Children?.Count, maximumNumberOfItemsInFolder);
	}
}

The implementation above is using the AlmostAtMaxiumTitle and AlmostAtMaxiumMessageFormat strings set via the Sitecore Configuration Factory onto the class instance; these are used when displaying messaging to content authors.

Also, this implementation is using the HasAlmostTooManySubItems() method defined on its base class which ultimately makes a call to the ITooManySubItemsService service class to ascertain whether the folder/item almost has too many child items.

Just as I had done above, I created an interface for the other Content Editor Warning, only for the purpose of registering it in the Sitecore IoC container.

using Sitecore.Pipelines.GetContentEditorWarnings;

namespace Feature.ContentEditor.Pipelines.GetContentEditorWarnings
{
	public interface ITooManyChildItemsWarningProcessor
	{
		void Process(GetContentEditorWarningsArgs args);
	}
}

Here is the implementation of the interface above:

using Sitecore.Pipelines.GetContentEditorWarnings;

using Foundation.Validation.Models.TooManySubItems;
using Foundation.Validation.Services.TooManySubItems.Factories;
using Foundation.Validation.Services.TooManySubItems;

namespace Feature.ContentEditor.Pipelines.GetContentEditorWarnings
{
	public class TooManyChildItemsWarningProcessor : BaseTooManyChildItemsWarningProcessor, ITooManyChildItemsWarningProcessor
	{
		private string AtMaxiumTitle { get; set; }

		private string AtMaxiumMessageFormat { get; set; }

		public TooManyChildItemsWarningProcessor(ITooManySubItemsFeatureToggleService tooManySubItemsFeatureToggleService, ITooManySubItemsServiceParametersFactory tooManySubItemsServiceParametersFactory, ITooManySubItemsService tooManySubItemsService)
			: base(tooManySubItemsFeatureToggleService, tooManySubItemsServiceParametersFactory, tooManySubItemsService)
		{
		}

		protected override bool ShouldDisplayWarning(TooManySubItemsServiceParameters parameters) => HasTooManySubItems(parameters);

		protected override string GetWarningTitle(GetContentEditorWarningsArgs args, int maximumNumberOfItemsInFolder) => AtMaxiumTitle;

		protected override string GetWarningMessage(GetContentEditorWarningsArgs args, int maximumNumberOfItemsInFolder) => string.Format(AtMaxiumMessageFormat, args?.Item?.Children?.Count, maximumNumberOfItemsInFolder);
	}
}

The class above is using the AtMaxiumTitle and AtMaxiumMessageFormat strings set via the Sitecore Configuration Factory from the processor’s configuration definition (see the Sitecore configuration patch file below for both processor definitions), and uses the HasTooManySubItems() method defined on the base class which also just delegates to the ITooManySubItemsService service class.

I then registered everything above in the following Sitecore patch configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/" xmlns:role="http://www.sitecore.net/xmlconfig/role/">
	<sitecore>
		<pipelines>
			<getContentEditorWarnings>
				<processor type="Feature.ContentEditor.Pipelines.GetContentEditorWarnings.IAlmostTooManyChildItemsWarningProcessor, Feature.ContentEditor" patch:before="processor[1]" resolve="true">
					<Enabled>true</Enabled>
					<MediaLibraryBasePath>/sitecore/media library/</MediaLibraryBasePath>
					<AlmostAtMaxiumTitle>This Item Almost Has Too Many Subitems Underneath It!</AlmostAtMaxiumTitle>
					<AlmostAtMaxiumMessageFormat>This item has {0} media libary items underneath it!  The maximum number of subitems allowed is {1}. Consider creating a new media library folder at this time.</AlmostAtMaxiumMessageFormat>
					<!-- By setting these, you can override the default values set for the entire feature set
										<NumberOfItemsToStartWarningUser>95</NumberOfItemsToStartWarningUser>
										<MaximumNumberOfItemsInFolder>100</MaximumNumberOfItemsInFolder>
					-->
					<ContentEditorWarningOptions hint="list">
						<Option type="Foundation.Kernel.Models.Pipelines.ContentEditorWarnings.ContentEditorWarningOption, Foundation.Kernel">
							<Text>New Media Folder</Text>
							<Link>media:newfolder</Link>
						</Option>
					</ContentEditorWarningOptions>
				</processor>
				<processor type="Feature.ContentEditor.Pipelines.GetContentEditorWarnings.ITooManyChildItemsWarningProcessor, Feature.ContentEditor" patch:before="processor[1]" resolve="true">
					<Enabled>true</Enabled>
					<MediaLibraryBasePath>/sitecore/media library/</MediaLibraryBasePath>
					<AtMaxiumTitle>This Item Has Too Many Subitems Underneath It!</AtMaxiumTitle>
					<AtMaxiumMessageFormat>This item has {0} media libary items underneath it! The maximum number of subitems allowed is {1}. It's time to create a new media library folder for more items to upload.</AtMaxiumMessageFormat>
					<!-- By setting these, you can override the default values set for the entire feature set
										<NumberOfItemsToStartWarningUser>95</NumberOfItemsToStartWarningUser>
										<MaximumNumberOfItemsInFolder>100</MaximumNumberOfItemsInFolder>
					-->
					<ContentEditorWarningOptions hint="list">
						<Option type="Foundation.Kernel.Models.Pipelines.ContentEditorWarnings.ContentEditorWarningOption, Foundation.Kernel">
							<Text>New Media Folder</Text>
							<Link>media:newfolder</Link>
						</Option>
					</ContentEditorWarningOptions>
				</processor>
			</getContentEditorWarnings>	
		</pipelines>
		<services>
			<register
				serviceType="Feature.ContentEditor.Pipelines.GetContentEditorWarnings.IAlmostTooManyChildItemsWarningProcessor, Feature.ContentEditor"
				implementationType="Feature.ContentEditor.Pipelines.GetContentEditorWarnings.AlmostTooManyChildItemsWarningProcessor, Feature.ContentEditor"
				lifetime="Singleton" />
			<register
				serviceType="Feature.ContentEditor.Pipelines.GetContentEditorWarnings.ITooManyChildItemsWarningProcessor, Feature.ContentEditor"
				implementationType="Feature.ContentEditor.Pipelines.GetContentEditorWarnings.TooManyChildItemsWarningProcessor, Feature.ContentEditor"
				lifetime="Singleton" />
		</services>
	</sitecore>
</configuration>

Let’s see what these two processors do.

When looking at an Item which is approaching too many child items, we see the following:

When we have a look at an Item which has more than the maximum number set for child items, we see the following:

Please drop a comment if you have questions/comments, or would like to share how you’ve used Content Editor Warnings on projects you have worked on.

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.

A Sitecore Item Buckets GutterRenderer to Convey Which Algorithm Was Used for Creating Bucket Folders

In my previous post, I gave a solution which I leverages the Sitecore Rules Engine to create a custom Item Buckets folder structure for storing bucketable Items.

Last night, I had a thought: what if you needed to know which “algorithm” created a given bucket folder structure for an Item Bucket? How could we go about conveying this type of information?

Immediately, the Sitecore Gutter came to mind — it’s a great way to communicate this type of information visually.

mind-outta-gutter

Before I move forward on the solution I started last night and completed today, let me explain what the Sitecore Gutter is, just in case you are unfamiliar with this feature.

The Sitecore Gutter lives here in the Content Editor:

smart-bucket-gutter-sitecore-gutter

If you right-click in this area, you get a context menu to turn on/off Gutter indicators:

smart-gutter-sitecore-gutter-context-menu

I turned on the Item Buckets Gutter indicator, and now can see which Items are Buckets:

smart-gutter-turn-on-buckets-gutter

There is a huge body of blog posts out on the interwebs which give examples on adding to the Sitecore Gutter. Here are a few posts from some fellow Sitecore MVPs which I highly recommend reading (in order of publish date):

I also wrote a post on how to add to the Sitecore Gutter using the Sitecore PowerShell Extensions module. I recommend having a look at that as well. 😉

Now that we are well-versed — or “wicked smaht” as we Bostonians would alternatively say — on what the Sitecore Gutter is, let’s move on to the solution I came up with.

three-stooges-ejoomicated

Just a “heads up”: there is a lot of code in this solution so don’t freak out and/or get too overwhelmed. Stay the course. 😉

curly-bug-out

I first explored Sitecore.Buckets.Gutters.BucketGutter in Sitecore.Buckets.dll to see if I should take note of anything special I need to know about when creating custom Sitecore.Shell.Applications.ContentEditor.Gutters.GutterRenderer — this lives in Sitecore.Kernel.dll and needs to be subclassed when adding to the Sitecore Gutter — subclasses for Item Buckets.

I noticed there is code in there which ascertains whether the Item Buckets feature is turned on/off, and obviously returns a null instance of Sitecore.Shell.Applications.ContentEditor.Gutters.GutterIconDescriptor — this lives in Sitecore.Kernel.dll — via its GetIconDescriptor() method.

I decided I needed to a way to also ascertain this. I came up with the following interface for classes that determined whether a feature is enabled or not:

namespace Sitecore.Sandbox.Determiners.Features
{
    public interface IFeatureDeterminer
    {
        bool IsEnabled();
    }
}

I then implemented the above interface with the following class:

using Sitecore.ContentSearch;
using Sitecore.ContentSearch.Utilities;

using Sitecore.Sandbox.Determiners.Features;

namespace Sitecore.Sandbox.Buckets.Determiners.Features
{
    public class ItemBucketsFeatureDeterminer : IFeatureDeterminer
    {
        public virtual bool IsEnabled()
        {
            return ContentSearchManager.Locator.GetInstance<IContentSearchConfigurationSettings>().ItemBucketsEnabled();
        }
    }
}

The code in the IsEnabled() method basically returns a boolean indicating whether the Item Buckets feature is turned on/off.

We now need classes whose instances can ascertain whether a bucketed Item’s path matches the paths generated by the bucketing algorithms they represent. I created the following class whose instances would serve as a parameters object to these objects:

using System;

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

namespace Sitecore.Sandbox.Buckets.Ascertainers
{
    public class BucketFolderPathAscertainerParameters
    {
        public Item BucketItem { get; set; }

        public Item BucketedItem { get; set; }

        public DateTime CreationDateOfNewItem { get; set; }
    }
}

We’re just going to pass the Item Bucket, the bucketed Item and the creation date of the bucketed Item.

Next, we need those objects that ascertain whether a given Item Bucket uses a particular bucketing algorithm for its folder structure. I created the following interface for classes whose instances do just that:


namespace Sitecore.Sandbox.Buckets.Ascertainers
{
    public interface IBucketFolderPathAscertainer
    {
        string GetIcon();

        string GetToolTip();

        bool IsFolderPathMatch(BucketFolderPathAscertainerParameters parameters);
    }
}

After implementing two classes which implemented the interface above, I noticed some code similarities between them, and decided to employ Martin Fowler‘s refactoring technique Pull Up Method to move up these code similarities into a base class — I highly recommend reading his book Refactoring: Improving the Design of Existing Code which discusses this refactoring technique as well as a host of others — to make it easier for creating future subclasses and to hopefully abate the chances of code duplication. That exercise gave birth to the following abstract class:

using System;

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

namespace Sitecore.Sandbox.Buckets.Ascertainers
{
    public abstract class BucketFolderPathAscertainer : IBucketFolderPathAscertainer
    {
        private string icon;
        protected string Icon
        {
            get
            {
                Assert.IsNotNullOrEmpty(icon, "Icon must be set in configuration!");
                return icon;
            }
            set
            {
                Assert.IsNotNullOrEmpty(value, "Icon must be set in configuration!");
                icon = value;
            }
        }

        private string toolTip;
        protected string ToolTip
        {
            get
            {
                Assert.IsNotNullOrEmpty(toolTip, "ToolTip must be set in configuration!");
                return toolTip;
            }
            set
            {
                Assert.IsNotNullOrEmpty(value, "ToolTip must be set in configuration!");
                toolTip = value;
            }
        }
        
        public virtual string GetIcon()
        {
            return Icon;
        }

        public virtual string GetToolTip()
        {
            return ToolTip;
        }

        public bool IsFolderPathMatch(BucketFolderPathAscertainerParameters parameters)
        {
            EnsureParameters(parameters);
            ID settingsItemID = GetSettingsItemID();
            Assert.IsTrue(!ID.IsNullOrEmpty(settingsItemID), "GetSettingsItemID() cannot return null or empty!");
            Item settingsItem = parameters.BucketItem.Database.GetItem(settingsItemID);
            Assert.IsNotNull(settingsItem, string.Format("Setting Item does not exist! Make sure it exists! Item ID: {0}", settingsItemID.ToString()));

            string resolvedPath = GetResolvedPath(parameters, settingsItem);
            if (string.IsNullOrWhiteSpace(resolvedPath))
            {
                return false;
            }

            return IsPathMatch(parameters, resolvedPath);
        }

        protected virtual void EnsureParameters(BucketFolderPathAscertainerParameters parameters)
        {
            Assert.ArgumentNotNull(parameters, "parameters");
            Assert.ArgumentNotNull(parameters.BucketItem, "parameters.BucketItem");
            Assert.ArgumentNotNull(parameters.BucketedItem, "parameters.BucketedItem");
            Assert.ArgumentCondition(parameters.BucketItem.Database == parameters.BucketedItem.Database, "parameters.BucketItem.Database", "parameters.BucketItem.Database and parameters.BucketedItem.Database must be the same database");
            Assert.ArgumentCondition(parameters.BucketItem.Axes.IsAncestorOf(parameters.BucketedItem), "parameters.BucketItem", string.Format("parameters.BucketItem", "Bucket Item: {0} must be an ancestor of Bucketed Item: {1}", parameters.BucketItem.ID.ToString(), parameters.BucketedItem.ID.ToString()));
            Assert.ArgumentNotNull(parameters.BucketedItem, "parameters.BucketedItem");
        }

        protected virtual ID GetSettingsItemID()
        {
            return Sitecore.Buckets.Util.Constants.SettingsItemId;
        }

        protected abstract string GetResolvedPath(BucketFolderPathAscertainerParameters parameters, Item settingsItem);

        protected virtual bool IsPathMatch(BucketFolderPathAscertainerParameters parameters, string resolvedPath)
        {
            if (string.IsNullOrWhiteSpace(resolvedPath))
            {
                return false;
            }

            string bucketedFolderPath = parameters.BucketedItem.Paths.ParentPath.Replace(parameters.BucketItem.Paths.FullPath, string.Empty);
            if(bucketedFolderPath.StartsWith("/"))
            {
                bucketedFolderPath = bucketedFolderPath.Substring(1);
            }

            return string.Equals(bucketedFolderPath, resolvedPath, StringComparison.OrdinalIgnoreCase);
        }
    }
}

The Icon and ToolTip property values in the above class live in the patch configuration file further down in this post. The Sitecore Configuration Factory will inject those values into these properties, and the GetIcon() and GetToolTip() will return the values housed in the Icon and ToolTip properties, respectively.

The IsFolderPathMatch() gets the settingsItem Item instance — this Item lives in /sitecore/system/Settings/Buckets/Item Buckets Settings in Sitecore — which is needed by the GetResolvedPath() method — this method is declared abstract and must be implemented by subclasses — whose job it is to get the bucketed Item’s folder path via the algorithm which the subclass implementation represents.

When the algorithm path is return, it is then passed to the IsPathMatch() method which determines if there is a match. If there is a match, true is returned to the caller of the IsFolderPathMatch() method; false is returned otherwise.

The class above combined with its subclasses would be an example of the Template method design pattern in action.

Now, we need a subclass of the above to determine if a bucketed Item’s path was generated by the Sitecore Rules Engine. Since we don’t want the Rules Engine to evaluate the rules defined on /sitecore/system/Settings/Buckets/Item Buckets Settings for the bucketed Item given the Item Bucket’s current state — its “when” Condition will most likely evaluate to false — we need a way to trick the Rules Engine. I came up with the following Condition class that always evaluates to true:

using Sitecore.Rules;
using Sitecore.Rules.Conditions;

namespace Sitecore.Sandbox.Rules
{
    public class AlwaysTrueWhenCondition<TRuleContext> : WhenCondition<TRuleContext> where TRuleContext : RuleContext
    {
        protected override bool Execute(TRuleContext ruleContext)
        {
            return true;
        }
    }
}

There isn’t much going on in the above class. Its Execute() method always returns true.

The following subclass of the BucketFolderPathAscertainer class determines if a bucketed Item’s path was generated by the Sitecore Rules Engine:

using System;
using System.Collections.Generic;

using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Buckets.Rules.Bucketing;
using Sitecore.Rules;
using Sitecore.Sandbox.Rules;

namespace Sitecore.Sandbox.Buckets.Ascertainers
{
    public class RulesDefinedBucketFolderPathAscertainer : BucketFolderPathAscertainer
    {
        protected override string GetResolvedPath(BucketFolderPathAscertainerParameters parameters, Item settingsItem)
        {
            string bucketRulesFieldId = GetBucketRulesFieldId();
            Assert.IsNotNullOrEmpty(bucketRulesFieldId, "GetBucketRulesFieldId() cannot return null or empty!");
            BucketingRuleContext ruleContext = CreateNewBucketingRuleContext(parameters);
            RuleList<BucketingRuleContext> rules = GetRuleList<BucketingRuleContext>(settingsItem, bucketRulesFieldId);
            SetAlwaysTrueWhenConditions(rules.Rules);
            if (rules == null)
            {
                return string.Empty;
            }

            try
            {
                rules.Run(ruleContext);
            }
            catch (Exception ex)
            {
                Log.Error(ToString(), ex, this);
            }

            return ruleContext.ResolvedPath;
        }

        protected virtual string GetBucketRulesFieldId()
        {
            return Sitecore.Buckets.Util.Constants.BucketRulesFieldId;
        }

        protected virtual BucketingRuleContext CreateNewBucketingRuleContext(BucketFolderPathAscertainerParameters parameters)
        {
            return new BucketingRuleContext(parameters.BucketedItem.Database, parameters.BucketItem.ID, parameters.BucketedItem.ID, parameters.BucketedItem.Name,
                            parameters.BucketedItem.TemplateID, parameters.CreationDateOfNewItem)
            {
                NewItemId = parameters.BucketedItem.ID,
                CreationDate = parameters.CreationDateOfNewItem
            };
        }

        protected virtual RuleList<T> GetRuleList<T>(Item settingsItem, string bucketRulesFieldId) where T : BucketingRuleContext
        {
            Assert.ArgumentNotNull(settingsItem, "settingsItem");
            Assert.ArgumentNotNullOrEmpty(bucketRulesFieldId, "bucketRulesFieldId");
            return RuleFactory.GetRules<T>(new[] { settingsItem }, bucketRulesFieldId);
        }

        protected virtual void SetAlwaysTrueWhenConditions<TRuleContext>(IEnumerable<Rule<TRuleContext>> rules) where TRuleContext : RuleContext
        {
            foreach(Rule<TRuleContext> rule in rules)
            {
                rule.Condition = CreateNewAlwaysTrueWhenCondition<TRuleContext>();
            }
        }

        protected virtual AlwaysTrueWhenCondition<TRuleContext> CreateNewAlwaysTrueWhenCondition<TRuleContext>() where TRuleContext : RuleContext
        {
            return new AlwaysTrueWhenCondition<TRuleContext>();
    }
    }
}

I’m not going to go too much into all the methods of this class given that most of the magic happens in the GetResolvedPath() method. It basically replaces all Conditions in the rules Sitecore.Rules.RulesList instance with an instance of the Condition class above; calls the Rules Engine API to evaluate the rules defined on the settingsItem instance though with the Conditions always returning true; and returns the resolved path to the caller.

The following class which also subclasses the BucketFolderPathAscertainer class — if I only had a dollar for every instance of the word “class” or “subclass” in this post — basically wraps an Sitecore.Buckets.Util.IDynamicBucketFolderPath instance — ahem, I mean employs the Adapter design pattern — where its GetResolvedPath() method delegates a call to the IDynamicBucketFolderPath instances GetFolderPath() method:

using Sitecore.Buckets.Util;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Buckets.Ascertainers
{
    public class DynamicBucketFolderPathPathAscertainer : BucketFolderPathAscertainer
    {
        private IDynamicBucketFolderPath PathResolver { get; set; }

        protected override string GetResolvedPath(BucketFolderPathAscertainerParameters parameters, Item settingsItem)
        {
            Assert.IsNotNull(PathResolver, "The IDynamicBucketFolderPath instance named PathResolver must be set in configuration!");
            return PathResolver.GetFolderPath(parameters.BucketItem.Database, parameters.BucketedItem.Name, parameters.BucketedItem.TemplateID,
                                                                parameters.BucketedItem.ID, parameters.BucketItem.ID, parameters.CreationDateOfNewItem);
        }
    }
}

You might be asking “what are we using the above class for?” Well, we are going to inject an instance of Sitecore.Buckets.Util.DateBasedFolderPath into the PathResolver property via the Sitecore Configuration Factory (please see the patch configuration file further down in this post).

I thought it might be cumbersome for a class to make calls to every single IBucketFolderPathAscertainer instance — sure, we only have two above but just think about how messy things will quickly progress if more are added. I decided I would utilize the Composite design pattern via the following class:

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

using Sitecore.Configuration;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Buckets.Ascertainers
{
    
    public class CompositeBucketFolderPathAscertainer : IBucketFolderPathAscertainer
    {
        private string Icon { get; set; }

        private string ToolTip { get; set; }

        private List<IBucketFolderPathAscertainer> FolderPathAscertainers { get; set; }

        public CompositeBucketFolderPathAscertainer()
        {
            FolderPathAscertainers = new List<IBucketFolderPathAscertainer>();
        }

        public string GetIcon()
        {
            return Icon;
        }

        protected virtual void SetIcon(string icon)
        {
            Assert.ArgumentNotNullOrEmpty(icon, "icon");
            Icon = icon;
        }

        public string GetToolTip()
        {
            return ToolTip;
        }

        protected virtual void SetToolTip(string toolTip)
        {
            Assert.ArgumentNotNullOrEmpty(toolTip, "toolTip");
            ToolTip = toolTip;
        }

        public bool IsFolderPathMatch(BucketFolderPathAscertainerParameters parameters)
        {
            if(FolderPathAscertainers == null || !FolderPathAscertainers.Any())
            {
                return false;
            }

            foreach(IBucketFolderPathAscertainer ascertainer in FolderPathAscertainers)
            {
                if(ascertainer.IsFolderPathMatch(parameters))
                {
                    SetIcon(ascertainer.GetIcon());
                    SetToolTip(ascertainer.GetToolTip());
                    return true;
                }
            }

            return false;
        }

        protected virtual void AddFolderPathAscertainer(XmlNode configNode)
        {
            if(configNode == null)
            {
                return;
            }

            IBucketFolderPathAscertainer ascertainer = Factory.CreateObject(configNode, false) as IBucketFolderPathAscertainer;
            Assert.IsNotNull(ascertainer, "An IBucketFolderPathAscertainer was not defined correctly in configuration!");
            FolderPathAscertainers.Add(ascertainer);
        }
    }
}

All configuration-defined IBucketFolderPathAscertainer instances will be added to the FolderPathAscertainers List property via the AddFolderPathAscertainer() method (have a look at the configuration file below to see the AddFolderPathAscertainer() method being there for the Sitecore Configuration Factory to use).

The IsFolderPathMatch() method will then iterate over all IBucketFolderPathAscertainer instances and try to find a match. If a match is found, the instance of the class above will grab and save local copies of that IBucketFolderPathAscertainer instance’s Icon and Tooltip values, and then return true. If no match was found, it returns false.

Now, we need a way to grab one bucketed Item for an Item Bucket. I defined the following interface for a class whose instance will return one Item given another Item:

using Sitecore.Data.Items;

namespace Sitecore.Sandbox.Providers.Items
{
    public interface IItemProvider
    {
        Item GetItem(Item item);
    }
}

The following class implements the interface above:

using System;
using System.Linq;
using System.Linq.Expressions;

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;

using Sitecore.Sandbox.Providers.Items;

namespace Sitecore.Sandbox.Buckets.Providers.Items
{
    public class BucketedItemProvider : IItemProvider
    {
        private string BucketFolderTemplateId { get; set; }

        private string SearchIndexName { get; set; }

        private ISearchIndex searchIndex;
        private ISearchIndex SearchIndex
        {
            get
            {
                if (searchIndex == null && !string.IsNullOrWhiteSpace(SearchIndexName))
                {
                    searchIndex = GetSearchIndex(SearchIndexName);
                }

                return searchIndex;
            }
        }

        public virtual Item GetItem(Item bucketItem)
        {
            ID bucketFolderTemplateId;
            Assert.ArgumentCondition(ID.TryParse(BucketFolderTemplateId, out bucketFolderTemplateId), "BucketFolderTemplateId", "BucketFolderTemplateId cannot be empty and must be a Sitecore.Data.ID! Check its configuration setting!");
            Assert.ArgumentNotNull(bucketItem, "bucketItem");
            Assert.IsNotNullOrEmpty(SearchIndexName, "SearchIndexName is empty. Double-check its configuration setting!");
            Assert.IsNotNull(SearchIndex, "SearchIndex is null. Double-check the SearchIndexName configuration setting!");

            using (IProviderSearchContext searchContext = SearchIndex.CreateSearchContext())
            {
                var predicate = GetSearchPredicate<SearchResultItem>(bucketItem.ID, bucketFolderTemplateId);
                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(string searchIndexName)
        {
            Assert.ArgumentNotNullOrEmpty(searchIndexName, "searchIndexName");
            return ContentSearchManager.GetIndex(searchIndexName);
        }

        protected virtual Expression<Func<T, bool>> GetSearchPredicate<T>(ID bucketItemId, ID bucketFolderTemplateId) 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);
            return predicate;
        }
    }
}

The class above is leveraging the Sitecore.ContentSearch API to get this bucketed Item.

Why am I using the Sitecore.ContentSearch API? Well, imagine if there are thousands if not tens of thousands of bucketed Items under the Item Bucket. A Sitecore query would be slow as molasses, and we need keep performance on our minds at all times for all of our solutions. Don’t lose sight of that on anything you build.

The GetSearchPredicate() method builds up and returns an Expression<Func> instance — let’s call this instance the “predicate”. The predicate basically says we want an Item who is a descendant of the Item Bucket; isn’t a Bucket Folder Item; lives under a Bucket Folder; and isn’t the Item Bucket Item.

The GetItem() method then uses that predicate and the Sitecore.ContentSearch API to gather those SearchResultItem instances, and then returns the Item instance on the first one in the result set if any were returned. If none were found, it returns null.

Since we have a lot of moving parts in this solution — just look at all of the classes you’ve just gone through — I need a way to piece all of this together for a GutterRenderer.

Unfortunately, we can’t magically inject instances into a GutterRenderer via the Sitecore Configuration Factory — well, you can call it but imagine all the calls I would need for this — so I decided to define the following interface whose classes would be used by a GutterRenderer, and these classes would be defined in Sitecore configuration:

using Sitecore.Data.Items;
using Sitecore.Shell.Applications.ContentEditor.Gutters;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor.Gutters
{
    public interface IGutter
    {
        GutterIconDescriptor GetIconDescriptor(Item item);

        bool IsVisible();
    }
}

The following class implements the interface above:

using System;

using Sitecore.Buckets.Extensions;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Shell.Applications.ContentEditor.Gutters;

using Sitecore.Sandbox.Buckets.Ascertainers;
using Sitecore.Sandbox.Determiners.Features;
using Sitecore.Sandbox.Providers.Items;
using Sitecore.Sandbox.Shell.Applications.ContentEditor.Gutters;

namespace Sitecore.Sandbox.Buckets.Gutters
{
    public class FolderPathBucketGutter : IGutter
    {
        private string DefaultIcon { get; set; }

        private string DefaultToolTip { get; set; }

        private string CreatedDatetimeFieldName { get; set; }

        private IFeatureDeterminer ItemBucketsFeatureDeterminer { get; set; }
        
        private IItemProvider BucketedItemProvider { get; set; }

        private IBucketFolderPathAscertainer FolderPathAscertainer { get; set; }

        public virtual GutterIconDescriptor GetIconDescriptor(Item item)
        {
            EnsureRequiredProperties();
            Assert.ArgumentNotNull(item, "item");

            if(!AreItemBucketsEnabled() || !item.IsABucket())
            {
                return null;
            }

            Item bucketedItem = GetBucketedItem(item);
            if(bucketedItem == null)
            {
                return CreateNewGutterIconDescriptor(DefaultIcon, DefaultToolTip);
            }

            BucketFolderPathAscertainerParameters parameters = new BucketFolderPathAscertainerParameters
            {
                BucketItem = item,
                BucketedItem = bucketedItem,
                CreationDateOfNewItem = GetItemCreatedDateTime(bucketedItem)
            };

            if(!FolderPathAscertainer.IsFolderPathMatch(parameters))
            {
                return CreateNewGutterIconDescriptor(DefaultIcon, DefaultToolTip);
            }

            return CreateNewGutterIconDescriptor(FolderPathAscertainer.GetIcon(), FolderPathAscertainer.GetToolTip());
        }

        protected virtual void EnsureRequiredProperties()
        {
            Assert.IsNotNull(FolderPathAscertainer, "FolderPathAscertainer must be defined in configuration!");
            Assert.IsNotNullOrEmpty(DefaultIcon, "DefaultIcon must be defined in configuration!");
            Assert.IsNotNullOrEmpty(DefaultToolTip, "DefaultToolTip must be defined in configuration!");
            Assert.IsNotNullOrEmpty(CreatedDatetimeFieldName, "CreatedDatetimeFieldName must be defined in configuration!");
        }

        public virtual bool IsVisible()
        {
            return AreItemBucketsEnabled();
        }

        protected virtual bool AreItemBucketsEnabled()
        {
            Assert.IsNotNull(ItemBucketsFeatureDeterminer, "ItemBucketsFeatureDeterminer must be set in configuration!");
            return ItemBucketsFeatureDeterminer.IsEnabled();
        }

        protected virtual Item GetBucketedItem(Item bucketItem)
        {
            Assert.IsNotNull(BucketedItemProvider, "BucketedItemProvider must be set in configuration!");
            return BucketedItemProvider.GetItem(bucketItem);
        }

        protected virtual GutterIconDescriptor CreateNewGutterIconDescriptor(string icon, string toolTip)
        {
            Assert.ArgumentNotNullOrEmpty(icon, "icon");
            Assert.ArgumentNotNullOrEmpty(toolTip, "toolTip");
            return new GutterIconDescriptor
            {
                Icon = icon,
                Tooltip = TranslateText(toolTip)
            };
        }

        protected virtual string TranslateText(string text)
        {
            Assert.ArgumentNotNullOrEmpty(text, "text");
            return Translate.Text(text);
        }

        protected virtual DateTime GetItemCreatedDateTime(Item item)
        {
            Assert.IsNotNullOrEmpty(CreatedDatetimeFieldName, "CreatedDatetimeFieldName must be defined in configuration!");
            Assert.ArgumentNotNull(item, "item");
            DateField created = item.Fields[CreatedDatetimeFieldName];
            if(created == null)
            {
                return DateTime.MinValue;
            }

            return created.DateTime;
        }
    }
}

The AreItemBucketsEnabled() method in the above classes determines if the Item Bucket feature is enabled via the injected IBucketFolderPathAscertainer instance. This method is then used by the IsVisible() method which represents the method by the same name on GutterRenderer instances, and is also called by the GetIconDescriptor() method.

If the Item Buckets feature is not enabled, the GetIconDescriptor() method will return null as well as when the passed Item is not an Item Bucket.

If the passed Item is an Item Bucket, the GetIconDescriptor() gets one bucketed Item via the GetBucketedItem() method — this method just delegates to the IItemProvider instance injected into the class instance — and puts this bucketed Item as well as the Item Bucket into a BucketFolderPathAscertainerParameters parameters object instance. The creation date of the bucketed is also set on this parameters object since it is required when ascertaining whether the folder structure was constructed based on its creation date.

The BucketFolderPathAscertainerParameters instance is then passed to the IsFolderPathMatch() method on the injected IBucketFolderPathAscertainer property which determines if there is a folder path match.

If there is a match, a new GutterIconDescriptor instance is returned which contains the appropriate Icon and Tooltip.

If there is no match, then a new GutterIconDescriptor is returned with default values for the Icon and Tooltip.

This next class subclasses the GutterRenderer class:

using System;

using Sitecore.Configuration;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Shell.Applications.ContentEditor.Gutters;

namespace Sitecore.Sandbox.Shell.Applications.ContentEditor.Gutters
{
    public class ConfigDefinedGutterRenderer : GutterRenderer
    {
        private IGutter gutter;
        private IGutter Gutter
        {
            get
            {
                if (gutter == null)
                {
                    gutter = GetInnerGutterRenderer();
                }

                return gutter;
            }
        }

        protected override GutterIconDescriptor GetIconDescriptor(Item item)
        {
            Assert.IsNotNull(Gutter, "Gutter wasn't set properly. Double-check it!");
            return Gutter.GetIconDescriptor(item);
        }

        public override bool IsVisible()
        {
            Assert.IsNotNull(Gutter, "Gutter wasn't set properly. Double-check it!");
            return Gutter.IsVisible();
        }

        protected virtual IGutter GetInnerGutterRenderer()
        {
            string configPath = GetConfigPath();
            if (string.IsNullOrWhiteSpace(configPath))
            {
                Log.Error("ConfigDefinedGutterRenderer: configPath must be set as a parameter!", this);
                return null;
            }

            try
            {
                IGutter gutter = Factory.CreateObject(configPath, false) as IGutter;
                if (gutter == null)
                {
                    Log.Error(string.Format("ConfigDefinedGutterRenderer: the IGutter defined in {0} isn't correctly defined. Double-check it!", configPath), this);
                    return null;
                }

                return gutter;

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

            return null;
        }

        protected virtual string GetConfigPath()
        {
            string key = "configPath";
            if (Parameters.ContainsKey(key))
            {
                return Parameters[key];
            }

            return string.Empty;
        }
    }
}

The GetInnerGutterRenderer() method above calls the Sitecore Configuration Factory to grab an IGutter instance from a configuration path which is set on the Parameters field of the definition Item for this GutterRenderer in the Core database — see the screenshot further down in this post — when the Gutter property is called for the first time, and sets this instance on a private member on the class instance.

Both the GetIconDescriptor() and IsVisible() methods delegate to the methods on the IGutter instance with the same names (quiz time: what design pattern is this class using? 😉 ).

I then duct-taped everything together via the following patch configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <gutters>
      <folderPathBucketGutter type="Sitecore.Sandbox.Buckets.Gutters.FolderPathBucketGutter, Sitecore.Sandbox" singleInstance="true">
        <DefaultIcon>business/32x32/chest_add.png</DefaultIcon>
        <DefaultToolTip>This item is a bucket. You can use this as a content repository.</DefaultToolTip>
        <CreatedDatetimeFieldName>__Created</CreatedDatetimeFieldName>
        <ItemBucketsFeatureDeterminer ref="determiners/features/itemBucketsFeatureDeterminer" />
        <BucketedItemProvider ref="providers/items/bucketedItemProvider" />
        <FolderPathAscertainer ref="ascertainers/buckets/compositeBucketFolderPathAscertainer" />
      </folderPathBucketGutter>
    </gutters>
    <ascertainers>
      <buckets>
        <compositeBucketFolderPathAscertainer type="Sitecore.Sandbox.Buckets.Ascertainers.CompositeBucketFolderPathAscertainer, Sitecore.Sandbox" singleInstance="true">
          <ascertainers hint="raw:AddFolderPathAscertainer">
            <ascertainer ref="ascertainers/buckets/rulesDefinedBucketFolderPathAscertainer" />
            <ascertainer ref="ascertainers/buckets/dateBasedFolderPathAscertainer" />
          </ascertainers>
        </compositeBucketFolderPathAscertainer>
        <dateBasedFolderPathAscertainer type="Sitecore.Sandbox.Buckets.Ascertainers.DynamicBucketFolderPathPathAscertainer, Sitecore.Sandbox" singleInstance="true">
          <Icon>Business/32x32/calendar_down.png</Icon>
          <ToolTip>This item is a bucket. Its bucket folders were generated based on the creation date of the bucketed items. You can use this as a content repository.</ToolTip>
          <PathResolver type="Sitecore.Buckets.Util.DateBasedFolderPath, Sitecore.Buckets" />
        </dateBasedFolderPathAscertainer>
        <rulesDefinedBucketFolderPathAscertainer type="Sitecore.Sandbox.Buckets.Ascertainers.RulesDefinedBucketFolderPathAscertainer, Sitecore.Sandbox"
        singleInstance="true">
          <Icon>Business/32x32/briefcase_add.png</Icon>
          <ToolTip>This item is a bucket. Its bucket folders were generated by the rules engine. You can use this as a content repository.</ToolTip>
        </rulesDefinedBucketFolderPathAscertainer>
      </buckets>
    </ascertainers>
    <determiners>
      <features>
        <itemBucketsFeatureDeterminer type="Sitecore.Sandbox.Buckets.Determiners.Features.ItemBucketsFeatureDeterminer" singleInstance="true" />
      </features>
    </determiners>
    <providers>
      <items>
        <bucketedItemProvider type="Sitecore.Sandbox.Buckets.Providers.Items.BucketedItemProvider" singleInstance="true">
          <BucketFolderTemplateId>{ADB6CA4F-03EF-4F47-B9AC-9CE2BA53FF97}</BucketFolderTemplateId>
          <SearchIndexName>sitecore_master_index</SearchIndexName>
        </bucketedItemProvider>
      </items>
    </providers>
  </sitecore>
</configuration>

We need to let Sitecore know about the new Gutter addition. I did this in the Core database:

smart-bucket-gutter-core-db

One thing to keep in mind is that the Sitecore Rules Engine folder path match will only work when we have an algorithm that will return the same path consistently for a bucketed Item. This unfortunately means I could not use the same Action from my previous post given that it generates random folder paths.

To over come this hurdle, I built the following Action which just reverses the bucketed Item’s ID (oh no, more code 😉 ):

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

using Sitecore.Buckets.Rules.Bucketing;
using Sitecore.Buckets.Rules.Bucketing.Actions;
using Sitecore.Diagnostics;

namespace Sitecore.Sandbox.Buckets.Rules.Actions
{
    public class CreateReversedIDBasedPath<TContext> : CreateIDBasedPath<TContext> where TContext : BucketingRuleContext
    {
        public override void Apply(TContext ruleContext)
        {
            Assert.ArgumentNotNull(ruleContext, "ruleContext");
            base.Apply(ruleContext);
            if (string.IsNullOrWhiteSpace(ruleContext.ResolvedPath))
            {
                return;
            }

            ruleContext.ResolvedPath = ReversePath(ruleContext.ResolvedPath);
        }

        protected virtual string ReversePath(string path)
        {
            if(string.IsNullOrWhiteSpace(path))
            {
                return string.Empty;
            }

            List<string> pieces = path.Split('/').ToList();
            pieces.Reverse();
            return string.Join("/", pieces);
        }
    }
}

I’m not going to go into details of how I set this in the rules on /sitecore/system/Settings/Buckets/Item Buckets Settings — you can see an example of how this is done from my previous post.

Now that everything is set, we can see that the new Gutter option is available:

smart-bucket-gutter-right-click-lets-turn-on

I then turned it on:

smart-gutter-new-gutter-turned-on

As you can see, we have different Gutter icons for different folder structures.

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

Oh, by the way, if you made it all the way to the end of this post, then you deserve a treat. Go get yourself a cookie. You deserve it. 😉

cookie

Until next time, keep up the good fight, one piece of code at time. 😀

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.

Employ the Template Method Design Pattern for Content Editor Warnings in Sitecore

This post is a continuation of a series of posts I’m putting together around using design patterns in Sitecore solutions, and will show a “proof of concept” around using the Template method pattern — a pattern where classes have an abstract base class that defines most of an “algorithm” for how classes that inherit from it work but provides method stubs — these are abstract methods that must be implemented by subclasses to “fill in the blanks” of the “algorithm” — and method hooks — these are virtual methods that can be overridden if needed.

In this “proof of concept”, I am tapping into the <getContentEditorWarnings> pipeline in order to add custom content editor warnings for Items — if you are unfamiliar with content editor warnings in Sitecore, the following screenshot illustrates an “out of the box” content editor warning around publishing and workflow state:

content-editor-warning-example

To start, I defined the following interface for classes that will contain content for warnings that will be displayed in the content editor:

using System.Collections.Generic;

namespace Sitecore.Sandbox.Pipelines.GetContentEditorWarnings
{
    public interface IWarning
    {
        string Title { get; set; }

        string Message { get; set; }

        List<CommandLink> Links { get; set; }

        bool HasContent();

        IWarning Clone();
    }
}

Warnings will have a title, an error message for display, and a list of Sheer UI command links — the CommandLink class is defined further down this post — to be displayed and invoked when clicked.

You might be asking why I am defining this when I can just use what’s available in the Sitecore API? Well, I want to inject these values via the Sitecore Configuration Factory, and hopefully this will become clear once you have a look at the Sitecore configuration file further down in this post.

Next, I defined the following class that implements the interface above:

using System.Collections.Generic;

namespace Sitecore.Sandbox.Pipelines.GetContentEditorWarnings
{
    public class Warning : IWarning
    {
        public string Title { get; set; }

        public string Message { get; set; }

        public List<CommandLink> Links { get; set; }

        public Warning()
        {
            Links = new List<CommandLink>();
        }

        public bool HasContent()
        {
            return !string.IsNullOrWhiteSpace(Title)
                    || !string.IsNullOrWhiteSpace(Title)
                    || !string.IsNullOrWhiteSpace(Message);
        }

        public IWarning Clone()
        {
            IWarning clone = new Warning { Title = Title, Message = Message };
            foreach (CommandLink link in Links)
            {
                clone.Links.Add(new CommandLink { Text = link.Text, Command = link.Command });
            }

            return clone;
        }
    }
}

The HasContent() method just returns “true” if the instance has any content to display though this does not include CommandLinks — what’s the point in displaying these if there is no warning content to be displayed with them?

The Clone() method makes a new instance of the Warning class, and copies values into it — this is useful when defining tokens in strings that must be expanded before being displayed. If we expand them on the instance that is injected via the Sitecore Configuration Factory, the changed strings will persistent in memory until the application pool is recycled for the Sitecore instance.

The following class represents a Sheer UI command link to be displayed in the content editor warning so content editors/authors can take action on the warning:


namespace Sitecore.Sandbox.Pipelines.GetContentEditorWarnings
{
    public class CommandLink
    {
        public string Text { get; set; }

        public string Command { get; set; }
    }
}

I then built the following abstract class to serve as the base class for all classes whose instances will serve as a <getContentEditorWarnings> pipeline processor:

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

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

namespace Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.Template_Method_Pattern
{
    public abstract class ContentEditorWarnings
    {
        public void Process(GetContentEditorWarningsArgs args)
        {
            Assert.ArgumentNotNull(args, "args");
            Assert.ArgumentNotNull(args.Item, "args.Item");
            IEnumerable<IWarning> warnings = GetWarnings(args.Item);
            if(warnings == null || !warnings.Any())
            {
                return;
            }

            foreach(IWarning warning in warnings)
            {
                AddWarning(args, warning);
            }
        }

        protected abstract IEnumerable<IWarning> GetWarnings(Item item);

        private void AddWarning(GetContentEditorWarningsArgs args, IWarning warning)
        {
            if(!warning.HasContent())
            {
                return;
            }

            GetContentEditorWarningsArgs.ContentEditorWarning editorWarning = args.Add();
            if(!string.IsNullOrWhiteSpace(warning.Title))
            {
                editorWarning.Title = TranslateText(warning.Title);
            }

            if(!string.IsNullOrWhiteSpace(warning.Message))
            {
                editorWarning.Text = TranslateText(warning.Message);
            }

            if (!warning.Links.Any())
            {
                return;
            }
            
            foreach(CommandLink link in warning.Links)
            {
                editorWarning.AddOption(TranslateText(link.Text), link.Command);
            }
        }

        protected virtual string TranslateText(string text)
        {
            if(string.IsNullOrWhiteSpace(text))
            {
                return text;
            }

            return Translate.Text(text);
        }
    }
}

So what’s going on in this class? Well, the Process() method gets a collection of IWarnings from the GetWarnings() method — this method must be defined by subclasses of this class; iterates over them; and delegates to the AddWarning() method to add each to the GetContentEditorWarningsArgs instance.

The TranslateText() method calls the Text() method on the Sitecore.Globalization.Translate class — this lives in Sitecore.Kernel.dll — and is used when adding values on IWarning instances to the GetContentEditorWarningsArgs instance. This method is a hook, and can be overridden by subclasses if needed. I am not overriding this method on the subclasses further down in this post.

I then defined the following subclass of the class above to serve as a <getContentEditorWarnings> pipeline processor to warn content authors/editors if an Item has too many child Items:

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

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

namespace Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.Template_Method_Pattern
{
    public class TooManyChildItemsWarnings : ContentEditorWarnings
    {
        private int MaxNumberOfChildItems { get; set; }

        private IWarning Warning { get; set; }

        protected override IEnumerable<IWarning> GetWarnings(Item item)
        {
            AssertProperties();
            if(item.Children.Count <= MaxNumberOfChildItems)
            {
                return new List<IWarning>();
            }

            return new[] { Warning };
        }

        private void AssertProperties()
        {
            Assert.ArgumentCondition(MaxNumberOfChildItems > 0, "MaxNumberOfChildItems", "MaxNumberOfChildItems must be set correctly in configuration!");
            Assert.IsNotNull(Warning, "Warning", "Warning must be set in configuration!");
            Assert.ArgumentCondition(Warning.HasContent(), "Warning", "Warning should have some fields populated from configuration!");
        }
    }
}

The class above is getting its IWarning instance and maximum number of child Items value from Sitecore configuration.

The GetWarnings() method ascertains whether the Item has too many child Items and returns the IWarning instance when it does in a collection — I defined this to be a collection to allow <getContentEditorWarnings> pipeline processors subclassing the abstract base class above to return more than one warning if needed.

I then defined another subclass of the abstract class above to serve as another <getContentEditorWarnings> pipeline processor:

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

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

namespace Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.Template_Method_Pattern
{
    public class HasInvalidCharacetersInNameWarnings : ContentEditorWarnings
    {
        private string CharacterSeparator { get; set; }

        private string Conjunction { get; set; }

        private List<string> InvalidCharacters { get; set; }

        private IWarning Warning { get; set; }

        public HasInvalidCharacetersInNameWarnings()
        {
            InvalidCharacters = new List<string>();
        }

        protected override IEnumerable<IWarning> GetWarnings(Item item)
        {
            AssertProperties();
            HashSet<string> charactersFound = new HashSet<string>();
            foreach (string character in InvalidCharacters)
            {
                if(item.Name.Contains(character))
                {
                    charactersFound.Add(character.ToString());
                }
            }

            if(!charactersFound.Any())
            {
                return new List<IWarning>();
            }

            IWarning warning = Warning.Clone();
            string charactersFoundString = string.Join(CharacterSeparator, charactersFound);
            int lastSeparator = charactersFoundString.LastIndexOf(CharacterSeparator);
            if (lastSeparator < 0)
            {
                warning.Message = ReplaceInvalidCharactersToken(warning.Message, charactersFoundString);
                return new[] { warning };
            }

            warning.Message = ReplaceInvalidCharactersToken(warning.Message, Splice(charactersFoundString, lastSeparator, CharacterSeparator.Length, Conjunction));
            return new[] { warning };
        }

        private void AssertProperties()
        {
            Assert.IsNotNullOrEmpty(CharacterSeparator, "CharacterSeparator", "CharacterSeparator must be set in configuration!");
            Assert.ArgumentCondition(InvalidCharacters != null && InvalidCharacters.Any(), "InvalidCharacters", "InvalidCharacters must be set in configuration!");
            Assert.IsNotNull(Warning, "Warning", "Warning must be set in configuration!");
            Assert.ArgumentCondition(Warning.HasContent(), "Warning", "Warning should have some fields populated from configuration!");
        }

        private static string Splice(string value, int startIndex, int length, string replacement)
        {
            if(string.IsNullOrWhiteSpace(value))
            {
                return value;
            }

            return string.Concat(value.Substring(0, startIndex), replacement, value.Substring(startIndex + length));
        }

        private static string ReplaceInvalidCharactersToken(string value, string replacement)
        {
            return value.Replace("$invalidCharacters", replacement);
        }
    }
}

The above class will return an IWarning instance when an Item has invalid characters in its name — these invalid characters are defined in Sitecore configuration.

The GetWarnings() method iterates over all invalid characters passed from Sitecore configuration and determines if they exist in the Item name. If they do, they are added to a HashSet<string> instance — I’m using a HashSet<string> to ensure the same character isn’t added more than once to the collection — which is be used for constructing the warning message to be displayed to the content author/editor.

Once the GetWarnings() method has iterated through all invalid characters, a string is built using the HashSet<string> instance, and is put in place wherever the $invalidCharacters token is defined in the Message property of the IWarning instance.

I then registered everything above in Sitecore via the following patch configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getContentEditorWarnings>
        <processor type="Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.Template_Method_Pattern.TooManyChildItemsWarnings, Sitecore.Sandbox">
          <MaxNumberOfChildItems>20</MaxNumberOfChildItems>
          <Warning type="Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.Warning, Sitecore.Sandbox">
            <Title>This Item has too many child items!</Title>
            <Message>Please consider converting this Item into an Item Bucket.</Message>
            <Links hint="list">
              <Link type="Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.CommandLink">
                <Text>Convert to Item Bucket</Text>
                <Command>item:bucket</Command>
              </Link>
            </Links>
          </Warning>
        </processor>
        <processor type="Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.Template_Method_Pattern.HasInvalidCharacetersInNameWarnings, Sitecore.Sandbox">
          <CharacterSeparator>,&amp;nbsp;</CharacterSeparator>
          <Conjunction>&amp;nbsp;and&amp;nbsp;</Conjunction>
          <InvalidCharacters hint="list">
            <Character>-</Character>
            <Character>$</Character>
            <Character>1</Character>
          </InvalidCharacters>
          <Warning type="Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.Warning, Sitecore.Sandbox">
            <Title>The name of this Item has invalid characters!</Title>
            <Message>The name of this Item contains $invalidCharacters. Please consider renaming the Item.</Message>
            <Links hint="list">
              <Link type="Sitecore.Sandbox.Pipelines.GetContentEditorWarnings.CommandLink">
                <Text>Rename Item</Text>
                <Command>item:rename</Command>
              </Link>
            </Links>
          </Warning>
        </processor>
      </getContentEditorWarnings>
    </pipelines>
  </sitecore>
</configuration>

As you can see, I am injecting warning data into my <getContentEditorWarnings> pipeline processors as well as other things which are used in code for each.

For the TooManyChildItemsWarnings <getContentEditorWarnings> pipeline processor, we are giving content authors/editors the ability to convert the Item into an Item bucket — we are injecting the item:bucket command via the configuration file above.

For the HasInvalidCharacetersInNameWarnings <getContentEditorWarnings> pipeline processor, we are passing in the Sheer UI command that will launch the Item Rename dialog to give content authors/editors the ability to rename the Item if it has invalid characters in its name.

Let’s see if this works.

I navigated to an Item in my content tree that has less than 20 child Items and has no invalid characters in its name:

no-content-editor-warnings

As you can see, there are no warnings.

Let’s go to another Item, one that not only has more than 20 child Items but also has invalid characters in its name:

both-warnings-appear

As you can see, both warnings are appearing for this Item.

Let’s now rename the Item:

rename-dialog

Great! Now the ‘invalid characters in name” warning is gone. Let’s convert this Item into an Item Bucket:

item-bucket-click-1

After clicking the ‘Convert to Item Bucket’ link, I saw the following dialog:

item-bucket-click-2

After clicking the ‘OK’ button, I saw the following progress dialog:

item-bucket-click-3

As you can see, the Item is now an Item Bucket, and both content editor warnings are gone:

item-bucket-click-4

If you have any thoughts on this, or have ideas on other places where you might want to employ the Template method pattern, please share in a comment.

Also, if you would like to see another example around adding a custom content editor warning in Sitecore, check out an older post of mine where I added one for expanding tokens in fields on an Item.

Until next time, keep on learning and keep on Sitecoring — Sitecoring is a legit verb, isn’t it? 😉

Bucket Items in Sitecore using a Custom Commandlet in Sitecore PowerShell Extensions

Last Wednesday I had the privilege to present Sitecore PowerShell Extensions (SPE) at the Milwaukee Sitecore Meetup. During my presentation, I demonstrated how easy it is to add, execute and reuse PowerShell scripts in SPE, and I showcased version 3.0 of SPE on Sitecore XP 8.

Unfortunately, I ran out of time before showing how one can go about creating a custom commandlet in SPE, and hope to make it up to everyone by sharing the commandlet I wrote for the presentation in this post.

I wrote the following commandlet to convert an Item into an Item Bucket in Sitecore:

using System;
using System.Management.Automation;

using Sitecore.Data.Items;
using Sitecore.Shell.Framework.Commands;

using Cognifide.PowerShell.Commandlets;
using Cognifide.PowerShell.Commandlets.Interactive.Messages;

namespace Sitecore.Sandbox.SPE.Commandlets.Buckets
{
    [Cmdlet(VerbsData.ConvertTo, "Bucket"), OutputType(new Type[] { typeof(Item) })]
    public class ConvertToBucketCommand : BaseItemCommand
    {
        protected override void ProcessItem(Item item)
        {
            try
            {
                PutMessage(new ShellCommandInItemContextMessage(item, "item:bucket"));   
            }
            catch (Exception exception)
            {
                WriteError(new ErrorRecord(exception, "sitecore_new_bucket_error", ErrorCategory.NotSpecified, Item));
            }

            WriteItem(Item);
        }
    }
}

The above commandlet implements the ProcessItem() method — this method is declared abstract in one of the ancestor classes of the class above — and leverages the framework of SPE to invoke a Sheer UI command to bucket the Item passed to the method — one of the ancestor classes of this class passes the Item to be processed.

The above highlights how in SPE we are employing the Template method pattern for many “out of the box” commandlets. This involves inheriting from an abstract base class — Cognifide.PowerShell.Commandlets.BaseItemCommand in Cognifide.PowerShell.dll (this assembly comes with the SPE module) is an example of one of these base classes — and implementing methods that are defined as abstract. The parent or an ancestor class will do the brunt of the work behind the scenes, and use your method implementation for specifics.

As a side note, we also provide method hooks as well — these are virtual methods defined on a base or ancestor class — which you can override to change how they work to meet your particular needs.

I then wired the above up using a Sitecore include configuration file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <powershell>
      <commandlets>
        <add Name="Custom Bucket Commandlets" type="*, Sitecore.Sandbox.SPE" />
      </commandlets>
    </powershell>
  </sitecore>
</configuration>

I deployed the above to my Sitecore instance; loaded up the Integrated Scripting Environment (ISE) in SPE; and saw that my commandlet was registered using the Control-Space shortcut key:

convert-to-bucket-ise-control-space

Let’s take this for a spin. Let’s convert the Home Item into an Item Bucket:

home-before-bucket

Here’s my script to do that:

ise-convert-home-bucket

I clicked the execute button, and then got this confirmation dialog:

ise-convert-home-bucket-confirm

I then clicked the “Ok” button and was immediately presented with this dialog:

ise-convert-home-bucket-processing

As you can see it worked! The Home Item in my content tree is now an Item Bucket:

home-after-bucket

If you have any thoughts on this or ideas for other custom commandlets for SPE, please share in a comment.

If you would like to watch the Milwaukee Sitecore Meetup presentation where I showcased Sitecore PowerShell Extensions — and as a bonus you’ll also get to see some real-life application of SPE from Adam Brauer, Senior Product Engineer at Active Commerce, in this presentation as well — it has been recorded for posterity, and you can watch it here:

Until next time, stay curious, keep experimenting, and let’s keep on sharing all the Sitecore things!

Warn Content Authors of Linking to Items With No Presentation using Custom Field Validators in Sitecore

The other day John West, CTO of Sitecore USA, published his 500th blog post — quite an epic feat if you ask me — where he built a custom field validator that checks whether external links in the Rich Text field resolve:

This got me thinking: what other types of field validators might be useful?

I pondered over this for the past couple of days, and couldn’t think of anything useful but finally did come up with an idea this morning (out of the blue I might add): how about field validators that check to see whether Items linked in General and Internal Link fields have presentation?

After searching through the library of field validators available in Sitecore — I did this to make sure I wouldn’t be wasting my time given that Sitecore offers a lot of field validators “out of the box” (these live under /sitecore/system/Settings/Validation Rules/Field Rules in the master database), so I suggest having a look through these before building a custom one — I came up with the following solution that employs the Template method design pattern:

using System;
using System.Runtime.Serialization;

using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Data.Validators;
using Sitecore.Diagnostics;
using Sitecore.Pipelines.HasPresentation;

namespace Sitecore.Sandbox.Data.Validators.FieldValidators
{
    public abstract class ReferencedItemHasPresentationValidator : StandardValidator
    {
        public override string Name
        {
            get
            {
                return Parameters["Name"];
            }
        }

        public ReferencedItemHasPresentationValidator()
        {
        }

        public ReferencedItemHasPresentationValidator(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {
        }

        protected override ValidatorResult Evaluate()
        {
            Item linkedItem = GetReferencedItem();
            if (linkedItem == null || HasPresentation(linkedItem))
            {
                return ValidatorResult.Valid;
            }

            Text = GetErrorMessage();
            return GetFailedResult(ValidatorResult.Error);
        }

        protected virtual bool HasPresentation(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            return HasPresentationPipeline.Run(item);
        }

        protected abstract Item GetReferencedItem();

        protected virtual string GetErrorMessage()
        {
            string message = Parameters["ErrorMessage"];
            if (string.IsNullOrWhiteSpace(message))
            {
                return string.Empty;
            }

            return GetText(ExpandTokens(message), new[] { GetFieldDisplayName() });
        }

        protected override ValidatorResult GetMaxValidatorResult()
        {
            return GetFailedResult(ValidatorResult.Error);
        }

        protected virtual string ExpandTokens(string value)
        {
            if(string.IsNullOrWhiteSpace(value))
            {
                return value;
            }

            string valueExpanded = value;
            Field field = GetField();
            if(field != null)
            {
                valueExpanded = valueExpanded.Replace("$fieldName", field.Name);
            }

            return valueExpanded;
        }
    }
}

The above abstract class inherits from Sitecore.Data.Validators.StandardValidator in Sitecore.Kernel.dll — this is the base class which most validators in Sitecore inherit from — and checks to see if the Item referenced in the field has presentation (this check is done in the HasPresentation() method which basically delegates to the Run() method on the Sitecore.Pipelines.HasPresentation.HasPresentationPipeline class).

The referenced Item is returned by the GetReferencedItem() method which must be defined by subclasses of the above class.

Further, I’m passing in the validator’s name and error message through parameters (the error message allows for $fieldName as a token, and the ExpandTokens() method replaces this token with the name of the field being validated).

I then created a subclass of the above to return the Item referenced in an Internal Link field:

using System;
using System.Runtime.Serialization;

using Sitecore.Data.Fields;
using Sitecore.Data.Items;

namespace Sitecore.Sandbox.Data.Validators.FieldValidators
{
    [Serializable]
    public class InternalLinkItemHasPresentationValidator : ReferencedItemHasPresentationValidator
    {
        public InternalLinkItemHasPresentationValidator()
        {
        }

        public InternalLinkItemHasPresentationValidator(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {
        }

        protected override Item GetReferencedItem()
        {
            InternalLinkField internalLinkField = GetField();
            if (internalLinkField == null)
            {
                return null;
            }
            
            return internalLinkField.TargetItem;
        }
    }
}

Nothing magical is happening in the above class. The GetReferencedItem() method is casting the field to a Sitecore.Data.Fields.InternalLinkField instance, and returns the value of its TargetItem property.

Now that we have a class to handle Items referenced in Internal Link fields, we need another for General Link fields:

using System;
using System.Runtime.Serialization;

using Sitecore.Data.Fields;
using Sitecore.Data.Items;

namespace Sitecore.Sandbox.Data.Validators.FieldValidators
{
    [Serializable]
    public class GeneralLinkItemHasPresentationValidator : ReferencedItemHasPresentationValidator
    {
        public GeneralLinkItemHasPresentationValidator()
        {
        }

        public GeneralLinkItemHasPresentationValidator(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {
        }

        protected override Item GetReferencedItem()
        {
            LinkField linkField = GetField();
            if (linkField == null)
            {
                return null;
            }
            
            return linkField.TargetItem;
        }
    }
}

The GetReferencedItem() method in the GeneralLinkItemHasPresentationValidator class above does virtually the same thing as the same method in the InternalLinkItemHasPresentationValidator class. The only difference is the GetReferencedItem() method in the class above is casting the field to a Sitecore.Data.Fields.LinkField instance, and returns the value of its TargetItem property.

I then had to map the above field validator classes to Validation Rule Items — these have the /sitecore/templates/System/Validation/Validation Rule template — in Sitecore:

The Internal Link field validator:

internal-link-validator

The General Link field validator:

general-link-validator-item

I then added two Internal Link fields on my Sample item template, and mapped the Internal Link field validator to them:

set-validator-on-internal-link-fields

I also created two General Link fields on my Sample item template, and mapped the General Link field validator to them:

set-validator-on-general-link-fields

Once I had the validators mapped to their specific fields, I went ahead and removed presentation from one of my test items:

no-presentation-bing

Before, the above item had these presentation components on it:

presentation-on-google-item

I then linked to my test items in my test fields. As you can see, there are errors on the “Bing” item which does not have presentation:

invalid-field-links

If you have any thoughts on this, or ideas for other field validators, please share in a comment.

Until next time, have a Sitecorelicious day! 😀